Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
09a7c47
1
Parent(s):
3d4392e
oh yeah
Browse files- src/app/api/generators/search/defaultChannel.ts +70 -0
- src/app/api/generators/search/getNewMediaInfo.ts +146 -0
- src/app/api/utils/blobToDataUri.ts +21 -0
- src/app/api/utils/dataUriToBlob.ts +20 -0
- src/app/dream/embed/page.tsx +12 -0
- src/app/dream/page.tsx +15 -3
- src/app/main.tsx +42 -0
- src/app/state/useStore.ts +27 -1
- src/app/views/public-latent-media-embed-view/index.tsx +21 -0
- src/app/views/public-latent-media-view/index.tsx +77 -0
- src/app/views/public-media-view/index.tsx +10 -5
- src/components/interface/latent-engine/components/content-layer/index.tsx +8 -3
- src/components/interface/latent-engine/components/disclaimers/this-is-ai.tsx +2 -2
- src/components/interface/latent-engine/core/drawSegmentation.ts +36 -0
- src/components/interface/latent-engine/core/engine.tsx +69 -19
- src/components/interface/latent-engine/core/types.ts +10 -3
- src/components/interface/latent-engine/resolvers/image/index.tsx +4 -1
- src/components/interface/latent-engine/store/useLatentEngine.ts +86 -5
- src/components/interface/media-player/index.tsx +5 -4
- src/components/interface/media-player/latent.tsx +1 -1
- src/components/interface/stream-tag/index.tsx +6 -0
- src/lib/clap/clapToDataUri.ts +9 -0
- src/lib/clap/{mockClap.ts → getMockClap.ts} +18 -6
- src/lib/clap/newClap.ts +1 -0
- src/lib/clap/parseClap.ts +134 -21
- src/lib/clap/serializeClap.ts +9 -4
- src/lib/on-device-ai/getInteractiveSegmentationCanvas.tsx +37 -0
- src/lib/on-device-ai/getSegmentationCanvas.tsx +1 -0
- src/lib/on-device-ai/identifyFrame.ts +52 -0
- src/lib/on-device-ai/segmentFrameOnClick.ts +58 -0
- src/lib/utils/relativeCoords.ts +0 -0
- src/types/general.ts +3 -1
src/app/api/generators/search/defaultChannel.ts
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ChannelInfo } from "@/types/general";
|
2 |
+
|
3 |
+
export const defaultChannel: ChannelInfo = {
|
4 |
+
/**
|
5 |
+
* We actually use the dataset ID for the channel ID.
|
6 |
+
*
|
7 |
+
*/
|
8 |
+
id: "d25efcc1-3cc2-4b41-9f41-e3a93300ae5f",
|
9 |
+
|
10 |
+
/**
|
11 |
+
* The name used in the URL for the channel
|
12 |
+
*
|
13 |
+
* eg: my-time-travel-journeys
|
14 |
+
*/
|
15 |
+
slug: "latent",
|
16 |
+
|
17 |
+
/**
|
18 |
+
* username id of the Hugging Face dataset
|
19 |
+
*
|
20 |
+
* ex: f9a38286ec3436a45edd2cca
|
21 |
+
*/
|
22 |
+
// DISABLED FOR NOW
|
23 |
+
// datasetUserId: string
|
24 |
+
|
25 |
+
/**
|
26 |
+
* username slug of the Hugging Face dataset
|
27 |
+
*
|
28 |
+
* eg: jbilcke-hf
|
29 |
+
*/
|
30 |
+
datasetUser: "",
|
31 |
+
|
32 |
+
/**
|
33 |
+
* dataset slug of the Hugging Face dataset
|
34 |
+
*
|
35 |
+
* eg: ai-tube-my-time-travel-journeys
|
36 |
+
*/
|
37 |
+
datasetName: "",
|
38 |
+
|
39 |
+
label: "Latent",
|
40 |
+
|
41 |
+
description: "Latent",
|
42 |
+
|
43 |
+
thumbnail: "",
|
44 |
+
|
45 |
+
model: "SDXL",
|
46 |
+
|
47 |
+
lora: "",
|
48 |
+
|
49 |
+
style: "",
|
50 |
+
|
51 |
+
voice: "",
|
52 |
+
|
53 |
+
music: "",
|
54 |
+
|
55 |
+
/**
|
56 |
+
* The system prompt
|
57 |
+
*/
|
58 |
+
prompt: "",
|
59 |
+
|
60 |
+
likes: 0,
|
61 |
+
|
62 |
+
tags: [],
|
63 |
+
|
64 |
+
updatedAt: new Date().toISOString(),
|
65 |
+
|
66 |
+
/**
|
67 |
+
* Default video orientation
|
68 |
+
*/
|
69 |
+
orientation: "landscape"
|
70 |
+
}
|
src/app/api/generators/search/getNewMediaInfo.ts
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { v4 as uuidv4 } from "uuid"
|
2 |
+
|
3 |
+
import {
|
4 |
+
ChannelInfo,
|
5 |
+
MediaInfo,
|
6 |
+
} from "@/types/general"
|
7 |
+
import { defaultChannel } from "./defaultChannel"
|
8 |
+
|
9 |
+
export function getNewMediaInfo(params: Partial<MediaInfo> = {}): MediaInfo {
|
10 |
+
|
11 |
+
const channel = defaultChannel
|
12 |
+
|
13 |
+
const mediaInfo: MediaInfo = {
|
14 |
+
/**
|
15 |
+
* UUID (v4)
|
16 |
+
*/
|
17 |
+
id: uuidv4(),
|
18 |
+
|
19 |
+
/**
|
20 |
+
* Status of the media
|
21 |
+
*/
|
22 |
+
status: "published",
|
23 |
+
|
24 |
+
/**
|
25 |
+
* Human readable title for the media
|
26 |
+
*/
|
27 |
+
label: "",
|
28 |
+
|
29 |
+
/**
|
30 |
+
* Human readable description for the media
|
31 |
+
*/
|
32 |
+
description: "",
|
33 |
+
|
34 |
+
/**
|
35 |
+
* Content prompt
|
36 |
+
*/
|
37 |
+
prompt: "",
|
38 |
+
|
39 |
+
/**
|
40 |
+
* URL to the media thumbnail
|
41 |
+
*/
|
42 |
+
thumbnailUrl: "",
|
43 |
+
|
44 |
+
/**
|
45 |
+
* URL to a clap file
|
46 |
+
*/
|
47 |
+
clapUrl: "",
|
48 |
+
|
49 |
+
assetUrl: "",
|
50 |
+
|
51 |
+
/**
|
52 |
+
* This is contain the storage URL of the higher-resolution content
|
53 |
+
*/
|
54 |
+
assetUrlHd: "",
|
55 |
+
|
56 |
+
/**
|
57 |
+
* Counter for the number of views
|
58 |
+
*
|
59 |
+
* Note: should be managed by the index to prevent cheating
|
60 |
+
*/
|
61 |
+
numberOfViews: 0,
|
62 |
+
|
63 |
+
/**
|
64 |
+
* Counter for the number of likes
|
65 |
+
*
|
66 |
+
* Note: should be managed by the index to prevent cheating
|
67 |
+
*/
|
68 |
+
numberOfLikes: 0,
|
69 |
+
|
70 |
+
/**
|
71 |
+
* Counter for the number of dislikes
|
72 |
+
*
|
73 |
+
* Note: should be managed by the index to prevent cheating
|
74 |
+
*/
|
75 |
+
numberOfDislikes: 0,
|
76 |
+
|
77 |
+
/**
|
78 |
+
* When was the media updated
|
79 |
+
*/
|
80 |
+
updatedAt: new Date().toISOString(),
|
81 |
+
|
82 |
+
/**
|
83 |
+
* Arbotrary string tags to label the content
|
84 |
+
*/
|
85 |
+
tags: Array.isArray(params.tags) ? [
|
86 |
+
...params.tags,
|
87 |
+
] : [],
|
88 |
+
|
89 |
+
/**
|
90 |
+
* Model name
|
91 |
+
*/
|
92 |
+
model: "SDXL",
|
93 |
+
|
94 |
+
/**
|
95 |
+
* LoRA name
|
96 |
+
*/
|
97 |
+
lora: "",
|
98 |
+
|
99 |
+
/**
|
100 |
+
* style name
|
101 |
+
*/
|
102 |
+
style: "",
|
103 |
+
|
104 |
+
/**
|
105 |
+
* Music prompt
|
106 |
+
*/
|
107 |
+
music: "",
|
108 |
+
|
109 |
+
/**
|
110 |
+
* Voice prompt
|
111 |
+
*/
|
112 |
+
voice: "",
|
113 |
+
|
114 |
+
/**
|
115 |
+
* The channel
|
116 |
+
*/
|
117 |
+
channel,
|
118 |
+
|
119 |
+
/**
|
120 |
+
* Media duration (in seconds)
|
121 |
+
*/
|
122 |
+
duration: 2,
|
123 |
+
|
124 |
+
/**
|
125 |
+
* Media width (eg. 1024)
|
126 |
+
*/
|
127 |
+
width: 1024,
|
128 |
+
|
129 |
+
/**
|
130 |
+
* Media height (eg. 576)
|
131 |
+
*/
|
132 |
+
height: 576,
|
133 |
+
|
134 |
+
/**
|
135 |
+
* General media aspect ratio
|
136 |
+
*/
|
137 |
+
orientation: "landscape",
|
138 |
+
|
139 |
+
/**
|
140 |
+
* Media projection (cartesian by default)
|
141 |
+
*/
|
142 |
+
projection: "latent"
|
143 |
+
}
|
144 |
+
|
145 |
+
return mediaInfo
|
146 |
+
}
|
src/app/api/utils/blobToDataUri.ts
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export async function blobToDataUri(blob: Blob, defaultContentType = ""): Promise<string> {
|
2 |
+
if (typeof window === "undefined") {
|
3 |
+
const arrayBuffer = await blob.arrayBuffer()
|
4 |
+
let buffer = Buffer.from(arrayBuffer)
|
5 |
+
return "data:" + (defaultContentType || blob.type) + ';base64,' + buffer.toString('base64');
|
6 |
+
} else {
|
7 |
+
return new Promise<string>((resolve, reject) => {
|
8 |
+
const reader = new FileReader()
|
9 |
+
reader.onload = _e => {
|
10 |
+
let dataUri = `${reader.result as string || ""}`
|
11 |
+
if (defaultContentType) {
|
12 |
+
dataUri = dataUri.replace("application/octet-stream", defaultContentType)
|
13 |
+
}
|
14 |
+
resolve(dataUri)
|
15 |
+
}
|
16 |
+
reader.onerror = _e => reject(reader.error)
|
17 |
+
reader.onabort = _e => reject(new Error("Read aborted"))
|
18 |
+
reader.readAsDataURL(blob)
|
19 |
+
});
|
20 |
+
}
|
21 |
+
}
|
src/app/api/utils/dataUriToBlob.ts
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
export function dataUriToBlob(dataURI = "", defaultContentType = ""): Blob {
|
3 |
+
dataURI = dataURI.replace(/^data:/, '');
|
4 |
+
|
5 |
+
const type = dataURI.match(/(?:image|application|video|audio|text)\/[^;]+/)?.[0] || defaultContentType;
|
6 |
+
const base64 = dataURI.replace(/^[^,]+,/, '');
|
7 |
+
const arrayBuffer = new ArrayBuffer(base64.length);
|
8 |
+
const typedArray = new Uint8Array(arrayBuffer);
|
9 |
+
|
10 |
+
for (let i = 0; i < base64.length; i++) {
|
11 |
+
typedArray[i] = base64.charCodeAt(i);
|
12 |
+
}
|
13 |
+
console.log("dataUriToBlob DEBUG:", {
|
14 |
+
type,
|
15 |
+
base64: base64.slice(0, 80),
|
16 |
+
arrayBuffer
|
17 |
+
})
|
18 |
+
|
19 |
+
return new Blob([arrayBuffer], { type });
|
20 |
+
}
|
src/app/dream/embed/page.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils/cn"
|
2 |
+
|
3 |
+
export default async function Embed() {
|
4 |
+
return (
|
5 |
+
<div className={cn(
|
6 |
+
`w-full`,
|
7 |
+
`flex flex-col`
|
8 |
+
)}>
|
9 |
+
<a href={process.env.NEXT_PUBLIC_DOMAIN || "#"}>Please go to AiTube.at to fully enjoy this experience.</a>
|
10 |
+
</div>
|
11 |
+
)
|
12 |
+
}
|
src/app/dream/page.tsx
CHANGED
@@ -5,17 +5,29 @@ import { LatentQueryProps } from "@/types/general"
|
|
5 |
import { Main } from "../main"
|
6 |
import { searchResultToMediaInfo } from "../api/generators/search/searchResultToMediaInfo"
|
7 |
import { LatentSearchResult } from "../api/generators/search/types"
|
|
|
|
|
|
|
|
|
8 |
|
9 |
export default async function DreamPage({ searchParams: {
|
10 |
l: latentContent,
|
11 |
} }: LatentQueryProps) {
|
12 |
|
13 |
-
const latentSearchResult = JSON.parse(atob(`${latentContent}`)) as LatentSearchResult
|
14 |
|
15 |
// this will hallucinate the thumbnail on the fly - maybe we should cache it
|
16 |
-
const latentMedia = await searchResultToMediaInfo(latentSearchResult)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
return (
|
19 |
-
<Main
|
20 |
)
|
21 |
}
|
|
|
5 |
import { Main } from "../main"
|
6 |
import { searchResultToMediaInfo } from "../api/generators/search/searchResultToMediaInfo"
|
7 |
import { LatentSearchResult } from "../api/generators/search/types"
|
8 |
+
import { serializeClap } from "@/lib/clap/serializeClap"
|
9 |
+
import { getMockClap } from "@/lib/clap/getMockClap"
|
10 |
+
import { clapToDataUri } from "@/lib/clap/clapToDataUri"
|
11 |
+
import { getNewMediaInfo } from "../api/generators/search/getNewMediaInfo"
|
12 |
|
13 |
export default async function DreamPage({ searchParams: {
|
14 |
l: latentContent,
|
15 |
} }: LatentQueryProps) {
|
16 |
|
17 |
+
// const latentSearchResult = JSON.parse(atob(`${latentContent}`)) as LatentSearchResult
|
18 |
|
19 |
// this will hallucinate the thumbnail on the fly - maybe we should cache it
|
20 |
+
// const latentMedia = await searchResultToMediaInfo(latentSearchResult)
|
21 |
+
|
22 |
+
// TODO: generate the clap from the media info
|
23 |
+
console.log("generating a mock media info and mock clap file")
|
24 |
+
const latentMedia = getNewMediaInfo()
|
25 |
+
|
26 |
+
latentMedia.clapUrl = await clapToDataUri(
|
27 |
+
getMockClap({showDisclaimer: true })
|
28 |
+
)
|
29 |
|
30 |
return (
|
31 |
+
<Main latentMedia={latentMedia} />
|
32 |
)
|
33 |
}
|
src/app/main.tsx
CHANGED
@@ -17,6 +17,8 @@ import { TubeLayout } from "../components/interface/tube-layout"
|
|
17 |
import { PublicMusicVideosView } from "./views/public-music-videos-view"
|
18 |
import { PublicMediaEmbedView } from "./views/public-media-embed-view"
|
19 |
import { PublicMediaView } from "./views/public-media-view"
|
|
|
|
|
20 |
|
21 |
// this is where we transition from the server-side space
|
22 |
// and the client-side space
|
@@ -30,7 +32,12 @@ export function Main({
|
|
30 |
// view,
|
31 |
publicMedia,
|
32 |
publicMedias,
|
|
|
|
|
|
|
|
|
33 |
publicChannelVideos,
|
|
|
34 |
publicTracks,
|
35 |
publicTrack,
|
36 |
channel,
|
@@ -39,9 +46,15 @@ export function Main({
|
|
39 |
// view?: InterfaceView
|
40 |
publicMedia?: MediaInfo
|
41 |
publicMedias?: MediaInfo[]
|
|
|
|
|
|
|
|
|
42 |
publicChannelVideos?: MediaInfo[]
|
|
|
43 |
publicTracks?: MediaInfo[]
|
44 |
publicTrack?: MediaInfo
|
|
|
45 |
channel?: ChannelInfo
|
46 |
}) {
|
47 |
// this could be also a parameter of main, where we pass this manually
|
@@ -53,6 +66,8 @@ export function Main({
|
|
53 |
const setPathname = useStore(s => s.setPathname)
|
54 |
const setPublicChannel = useStore(s => s.setPublicChannel)
|
55 |
const setPublicMedias = useStore(s => s.setPublicMedias)
|
|
|
|
|
56 |
const setPublicChannelVideos = useStore(s => s.setPublicChannelVideos)
|
57 |
const setPublicTracks = useStore(s => s.setPublicTracks)
|
58 |
const setPublicTrack = useStore(s => s.setPublicTrack)
|
@@ -112,6 +127,28 @@ export function Main({
|
|
112 |
|
113 |
}, [publicMedia?.id])
|
114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
|
116 |
useEffect(() => {
|
117 |
// note: it is important to ALWAYS set the current video to videoId
|
@@ -143,6 +180,11 @@ export function Main({
|
|
143 |
{view === "home" && <HomeView />}
|
144 |
{view === "public_media_embed" && <PublicMediaEmbedView />}
|
145 |
{view === "public_media" && <PublicMediaView />}
|
|
|
|
|
|
|
|
|
|
|
146 |
{view === "public_music_videos" && <PublicMusicVideosView />}
|
147 |
{view === "public_channels" && <PublicChannelsView />}
|
148 |
{view === "public_channel" && <PublicChannelView />}
|
|
|
17 |
import { PublicMusicVideosView } from "./views/public-music-videos-view"
|
18 |
import { PublicMediaEmbedView } from "./views/public-media-embed-view"
|
19 |
import { PublicMediaView } from "./views/public-media-view"
|
20 |
+
import { PublicLatentMediaEmbedView } from "./views/public-latent-media-embed-view"
|
21 |
+
import { PublicLatentMediaView } from "./views/public-latent-media-view"
|
22 |
|
23 |
// this is where we transition from the server-side space
|
24 |
// and the client-side space
|
|
|
32 |
// view,
|
33 |
publicMedia,
|
34 |
publicMedias,
|
35 |
+
|
36 |
+
latentMedia,
|
37 |
+
latentMedias,
|
38 |
+
|
39 |
publicChannelVideos,
|
40 |
+
|
41 |
publicTracks,
|
42 |
publicTrack,
|
43 |
channel,
|
|
|
46 |
// view?: InterfaceView
|
47 |
publicMedia?: MediaInfo
|
48 |
publicMedias?: MediaInfo[]
|
49 |
+
|
50 |
+
latentMedia?: MediaInfo
|
51 |
+
latentMedias?: MediaInfo[]
|
52 |
+
|
53 |
publicChannelVideos?: MediaInfo[]
|
54 |
+
|
55 |
publicTracks?: MediaInfo[]
|
56 |
publicTrack?: MediaInfo
|
57 |
+
|
58 |
channel?: ChannelInfo
|
59 |
}) {
|
60 |
// this could be also a parameter of main, where we pass this manually
|
|
|
66 |
const setPathname = useStore(s => s.setPathname)
|
67 |
const setPublicChannel = useStore(s => s.setPublicChannel)
|
68 |
const setPublicMedias = useStore(s => s.setPublicMedias)
|
69 |
+
const setPublicLatentMedia = useStore(s => s.setPublicLatentMedia)
|
70 |
+
const setPublicLatentMedias = useStore(s => s.setPublicLatentMedias)
|
71 |
const setPublicChannelVideos = useStore(s => s.setPublicChannelVideos)
|
72 |
const setPublicTracks = useStore(s => s.setPublicTracks)
|
73 |
const setPublicTrack = useStore(s => s.setPublicTrack)
|
|
|
127 |
|
128 |
}, [publicMedia?.id])
|
129 |
|
130 |
+
useEffect(() => {
|
131 |
+
if (!latentMedias?.length) { return }
|
132 |
+
setPublicLatentMedias(latentMedias)
|
133 |
+
}, [getCollectionKey(latentMedias)])
|
134 |
+
|
135 |
+
useEffect(() => {
|
136 |
+
console.log("latentMedia:", {
|
137 |
+
"id": latentMedia?.id
|
138 |
+
})
|
139 |
+
console.log(latentMedia)
|
140 |
+
setPublicLatentMedia(latentMedia)
|
141 |
+
if (!latentMedia || !latentMedia?.id) { return }
|
142 |
+
if (pathname === "/dream/embed") { return }
|
143 |
+
if (pathname !== "/dream") {
|
144 |
+
// console.log("we are on huggingface apparently!")
|
145 |
+
// router.replace(`/watch?v=${publicMedia.id}`)
|
146 |
+
|
147 |
+
// TODO: add params in the URL to represent the latent result
|
148 |
+
router.replace(`/dream`)
|
149 |
+
}
|
150 |
+
}, [latentMedia?.id])
|
151 |
+
|
152 |
|
153 |
useEffect(() => {
|
154 |
// note: it is important to ALWAYS set the current video to videoId
|
|
|
180 |
{view === "home" && <HomeView />}
|
181 |
{view === "public_media_embed" && <PublicMediaEmbedView />}
|
182 |
{view === "public_media" && <PublicMediaView />}
|
183 |
+
|
184 |
+
{/* latent content is the content that "doesn't exist" (is generated by the AI) */}
|
185 |
+
{view === "public_latent_media_embed" && <PublicLatentMediaEmbedView />}
|
186 |
+
{view === "public_latent_media" && <PublicLatentMediaView />}
|
187 |
+
|
188 |
{view === "public_music_videos" && <PublicMusicVideosView />}
|
189 |
{view === "public_channels" && <PublicChannelsView />}
|
190 |
{view === "public_channel" && <PublicChannelView />}
|
src/app/state/useStore.ts
CHANGED
@@ -68,7 +68,13 @@ export const useStore = create<{
|
|
68 |
setPublicComments: (publicComment: CommentInfo[]) => void
|
69 |
|
70 |
publicMedias: MediaInfo[]
|
71 |
-
setPublicMedias: (publicMedias
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
publicChannelVideos: MediaInfo[]
|
74 |
setPublicChannelVideos: (publicChannelVideos: MediaInfo[]) => void
|
@@ -109,7 +115,15 @@ export const useStore = create<{
|
|
109 |
"/embed": "public_media_embed",
|
110 |
"/music": "public_music_videos",
|
111 |
"/channels": "public_channels",
|
|
|
|
|
112 |
"/channel": "public_channel",
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
"/account": "user_account",
|
114 |
"/account/channel": "user_channel",
|
115 |
}
|
@@ -219,6 +233,18 @@ export const useStore = create<{
|
|
219 |
},
|
220 |
|
221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
222 |
publicTrack: undefined,
|
223 |
setPublicTrack: (publicTrack?: MediaInfo) => {
|
224 |
set({ publicTrack })
|
|
|
68 |
setPublicComments: (publicComment: CommentInfo[]) => void
|
69 |
|
70 |
publicMedias: MediaInfo[]
|
71 |
+
setPublicMedias: (publicMedias?: MediaInfo[]) => void
|
72 |
+
|
73 |
+
latentMedia?: MediaInfo
|
74 |
+
setPublicLatentMedia: (latentMedia?: MediaInfo) => void
|
75 |
+
|
76 |
+
latentMedias: MediaInfo[]
|
77 |
+
setPublicLatentMedias: (latentMedias?: MediaInfo[]) => void
|
78 |
|
79 |
publicChannelVideos: MediaInfo[]
|
80 |
setPublicChannelVideos: (publicChannelVideos: MediaInfo[]) => void
|
|
|
115 |
"/embed": "public_media_embed",
|
116 |
"/music": "public_music_videos",
|
117 |
"/channels": "public_channels",
|
118 |
+
"/dream": "public_latent_media",
|
119 |
+
"/dream/embed": "public_latent_media_embed",
|
120 |
"/channel": "public_channel",
|
121 |
+
|
122 |
+
// those are reserved for future use
|
123 |
+
"/gaming": "public_music_videos",
|
124 |
+
"/live": "public_music_videos",
|
125 |
+
"/tv": "public_music_videos",
|
126 |
+
|
127 |
"/account": "user_account",
|
128 |
"/account/channel": "user_channel",
|
129 |
}
|
|
|
233 |
},
|
234 |
|
235 |
|
236 |
+
latentMedia: undefined,
|
237 |
+
setPublicLatentMedia: (latentMedia?: MediaInfo) => {
|
238 |
+
set({ latentMedia })
|
239 |
+
},
|
240 |
+
|
241 |
+
latentMedias: [],
|
242 |
+
setPublicLatentMedias: (latentMedias: MediaInfo[] = []) => {
|
243 |
+
set({
|
244 |
+
latentMedias: Array.isArray(latentMedias) ? latentMedias : []
|
245 |
+
})
|
246 |
+
},
|
247 |
+
|
248 |
publicTrack: undefined,
|
249 |
setPublicTrack: (publicTrack?: MediaInfo) => {
|
250 |
set({ publicTrack })
|
src/app/views/public-latent-media-embed-view/index.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useStore } from "@/app/state/useStore"
|
4 |
+
import { cn } from "@/lib/utils/cn"
|
5 |
+
|
6 |
+
export function PublicLatentMediaEmbedView() {
|
7 |
+
const media = useStore(s => s.publicMedia)
|
8 |
+
if (!media) { return null }
|
9 |
+
|
10 |
+
// unfortunately we have to disable this,
|
11 |
+
// as we can't afford a dream to be generated in parallel by many X users,
|
12 |
+
// it would be way too costly
|
13 |
+
return (
|
14 |
+
<div className={cn(
|
15 |
+
`w-full`,
|
16 |
+
`flex flex-col`
|
17 |
+
)}>
|
18 |
+
<a href={process.env.NEXT_PUBLIC_DOMAIN || "#"}>Please go to AiTube.at to fully enjoy this experience.</a>
|
19 |
+
</div>
|
20 |
+
)
|
21 |
+
}
|
src/app/views/public-latent-media-view/index.tsx
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useStore } from "@/app/state/useStore"
|
4 |
+
import { cn } from "@/lib/utils/cn"
|
5 |
+
import { MediaPlayer } from "@/components/interface/media-player"
|
6 |
+
|
7 |
+
export function PublicLatentMediaView() {
|
8 |
+
// note:
|
9 |
+
const media = useStore(s => s.latentMedia)
|
10 |
+
console.log("PublicLatentMediaView", {
|
11 |
+
"media (latentMedia)": media,
|
12 |
+
})
|
13 |
+
if (!media) { return null }
|
14 |
+
|
15 |
+
return (
|
16 |
+
<div className={cn(
|
17 |
+
`w-full`,
|
18 |
+
`flex flex-col lg:flex-row`
|
19 |
+
)}>
|
20 |
+
<div className={cn(
|
21 |
+
`flex-grow`,
|
22 |
+
`flex flex-col`,
|
23 |
+
`transition-all duration-200 ease-in-out`,
|
24 |
+
`px-2 xl:px-0`
|
25 |
+
)}>
|
26 |
+
{/** AI MEDIA PLAYER - HORIZONTAL */}
|
27 |
+
<MediaPlayer
|
28 |
+
media={media}
|
29 |
+
enableShortcuts={false}
|
30 |
+
|
31 |
+
// that could be, but let's do it the dirty way for now
|
32 |
+
// currentTime={desiredCurrentTime}
|
33 |
+
|
34 |
+
className="rounded-xl overflow-hidden mb-4"
|
35 |
+
/>
|
36 |
+
|
37 |
+
{/** AI MEDIA TITLE - HORIZONTAL */}
|
38 |
+
<div className={cn(
|
39 |
+
`flex flex-row space-x-2`,
|
40 |
+
`transition-all duration-200 ease-in-out`,
|
41 |
+
`text-lg lg:text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
42 |
+
`mb-2`,
|
43 |
+
)}>
|
44 |
+
<div className="">{media.label}</div>
|
45 |
+
</div>
|
46 |
+
|
47 |
+
{/** MEDIA TOOLBAR - HORIZONTAL */}
|
48 |
+
<div className={cn(
|
49 |
+
`flex flex-col space-y-3 xl:space-y-0 xl:flex-row`,
|
50 |
+
`transition-all duration-200 ease-in-out`,
|
51 |
+
`items-start xl:items-center`,
|
52 |
+
`justify-between`,
|
53 |
+
`mb-2 lg:mb-3`,
|
54 |
+
)}>
|
55 |
+
|
56 |
+
|
57 |
+
</div>
|
58 |
+
|
59 |
+
{/** MEDIA DESCRIPTION - VERTICAL */}
|
60 |
+
<div className={cn(
|
61 |
+
`flex flex-col p-3`,
|
62 |
+
`transition-all duration-200 ease-in-out`,
|
63 |
+
`rounded-xl`,
|
64 |
+
`bg-neutral-700/50`,
|
65 |
+
`text-sm text-zinc-100`,
|
66 |
+
)}>
|
67 |
+
|
68 |
+
{/* DESCRIPTION BLOCK */}
|
69 |
+
<div className="flex flex-row space-x-2 font-medium mb-1">
|
70 |
+
<div>no data</div>
|
71 |
+
</div>
|
72 |
+
<p>{media.description}</p>
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
)
|
77 |
+
}
|
src/app/views/public-media-view/index.tsx
CHANGED
@@ -116,12 +116,17 @@ export function PublicMediaView() {
|
|
116 |
if (!media || !media.id) {
|
117 |
return
|
118 |
}
|
119 |
-
const numberOfViews = await countNewMediaView(mediaId)
|
120 |
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
})
|
126 |
|
127 |
}, [media?.id])
|
|
|
116 |
if (!media || !media.id) {
|
117 |
return
|
118 |
}
|
|
|
119 |
|
120 |
+
try {
|
121 |
+
const numberOfViews = await countNewMediaView(mediaId)
|
122 |
+
|
123 |
+
setPublicMedia({
|
124 |
+
...media,
|
125 |
+
numberOfViews
|
126 |
+
})
|
127 |
+
} catch (err) {
|
128 |
+
console.error(`failed to count the number of view for mediaId ${mediaId}`)
|
129 |
+
}
|
130 |
})
|
131 |
|
132 |
}, [media?.id])
|
src/components/interface/latent-engine/components/content-layer/index.tsx
CHANGED
@@ -1,25 +1,30 @@
|
|
1 |
-
import {
|
|
|
2 |
|
3 |
export const ContentLayer = forwardRef(function ContentLayer({
|
4 |
width = 256,
|
5 |
height = 256,
|
6 |
className = "",
|
7 |
children,
|
|
|
8 |
}: {
|
9 |
width?: number
|
10 |
height?: number
|
11 |
className?: string
|
12 |
children?: ReactNode
|
|
|
13 |
}, ref: ForwardedRef<HTMLDivElement>) {
|
14 |
return (
|
15 |
-
<div className=
|
16 |
absolute
|
17 |
mt-0 mb-0 ml-0 mr-0
|
18 |
flex flex-col
|
19 |
items-center justify-center
|
20 |
-
|
|
|
21 |
style={{ width, height }}
|
22 |
ref={ref}
|
|
|
23 |
>
|
24 |
<div className="h-full aspect-video">
|
25 |
{children}
|
|
|
1 |
+
import { cn } from "@/lib/utils/cn"
|
2 |
+
import { ForwardedRef, forwardRef, MouseEventHandler, ReactNode } from "react"
|
3 |
|
4 |
export const ContentLayer = forwardRef(function ContentLayer({
|
5 |
width = 256,
|
6 |
height = 256,
|
7 |
className = "",
|
8 |
children,
|
9 |
+
onClick,
|
10 |
}: {
|
11 |
width?: number
|
12 |
height?: number
|
13 |
className?: string
|
14 |
children?: ReactNode
|
15 |
+
onClick?: MouseEventHandler<HTMLDivElement>
|
16 |
}, ref: ForwardedRef<HTMLDivElement>) {
|
17 |
return (
|
18 |
+
<div className={cn(`
|
19 |
absolute
|
20 |
mt-0 mb-0 ml-0 mr-0
|
21 |
flex flex-col
|
22 |
items-center justify-center
|
23 |
+
pointer-events-none
|
24 |
+
`, className)}
|
25 |
style={{ width, height }}
|
26 |
ref={ref}
|
27 |
+
onClick={onClick}
|
28 |
>
|
29 |
<div className="h-full aspect-video">
|
30 |
{children}
|
src/components/interface/latent-engine/components/disclaimers/this-is-ai.tsx
CHANGED
@@ -4,12 +4,12 @@ import React from "react"
|
|
4 |
import { cn } from "@/lib/utils/cn"
|
5 |
|
6 |
import { arimoBold, arimoNormal } from "@/lib/fonts"
|
7 |
-
import {
|
8 |
|
9 |
export function ThisIsAI({
|
10 |
streamType,
|
11 |
}: {
|
12 |
-
streamType?:
|
13 |
} = {}) {
|
14 |
|
15 |
return (
|
|
|
4 |
import { cn } from "@/lib/utils/cn"
|
5 |
|
6 |
import { arimoBold, arimoNormal } from "@/lib/fonts"
|
7 |
+
import { ClapStreamType } from "@/lib/clap/types"
|
8 |
|
9 |
export function ThisIsAI({
|
10 |
streamType,
|
11 |
}: {
|
12 |
+
streamType?: ClapStreamType
|
13 |
} = {}) {
|
14 |
|
15 |
return (
|
src/components/interface/latent-engine/core/drawSegmentation.ts
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { MPMask } from "@mediapipe/tasks-vision"
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Draw segmentation result
|
5 |
+
*/
|
6 |
+
export function drawSegmentation(mask?: MPMask, canvas?: HTMLCanvasElement) {
|
7 |
+
|
8 |
+
if (!mask) { throw new Error("drawSegmentation failed: empty mask") }
|
9 |
+
|
10 |
+
if (!canvas) { throw new Error("drawSegmentation failed: cannot access the canvas") }
|
11 |
+
|
12 |
+
const width = mask.width;
|
13 |
+
const height = mask.height;
|
14 |
+
const maskData = mask.getAsFloat32Array();
|
15 |
+
|
16 |
+
canvas.width = width;
|
17 |
+
canvas.height = height;
|
18 |
+
|
19 |
+
console.log("drawSegmentation: drawing..")
|
20 |
+
|
21 |
+
const ctx = canvas.getContext("2d")
|
22 |
+
|
23 |
+
if (!ctx) { throw new Error("drawSegmentation failed: cannot access the 2D context") }
|
24 |
+
|
25 |
+
ctx.fillStyle = "#00000000";
|
26 |
+
ctx.fillRect(0, 0, width, height);
|
27 |
+
ctx.fillStyle = "rgba(18, 181, 203, 0.7)";
|
28 |
+
|
29 |
+
maskData.forEach((category: number, index: number, array: Float32Array) => {
|
30 |
+
if (Math.round(category * 255.0) === 0) {
|
31 |
+
const x = (index + 1) % width;
|
32 |
+
const y = (index + 1 - x) / width;
|
33 |
+
ctx.fillRect(x, y, 1, 1);
|
34 |
+
}
|
35 |
+
})
|
36 |
+
}
|
src/components/interface/latent-engine/core/engine.tsx
CHANGED
@@ -1,32 +1,38 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import React, { useEffect, useRef, useState } from "react"
|
4 |
|
5 |
-
import { mockClap } from "@/lib/clap/mockClap"
|
6 |
import { cn } from "@/lib/utils/cn"
|
7 |
|
8 |
import { useLatentEngine } from "../store/useLatentEngine"
|
9 |
import { PlayPauseButton } from "../components/play-pause-button"
|
10 |
import { StreamTag } from "../../stream-tag"
|
11 |
import { ContentLayer } from "../components/content-layer"
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
function LatentEngine({
|
14 |
-
|
15 |
width,
|
16 |
height,
|
17 |
className = "" }: {
|
18 |
-
|
19 |
width?: number
|
20 |
height?: number
|
21 |
className?: string
|
22 |
}) {
|
23 |
const setContainerDimension = useLatentEngine(s => s.setContainerDimension)
|
24 |
const isLoaded = useLatentEngine(s => s.isLoaded)
|
25 |
-
const
|
26 |
-
const
|
27 |
|
28 |
const setImageElement = useLatentEngine(s => s.setImageElement)
|
29 |
const setVideoElement = useLatentEngine(s => s.setVideoElement)
|
|
|
30 |
|
31 |
const streamType = useLatentEngine(s => s.streamType)
|
32 |
const isStatic = useLatentEngine(s => s.isStatic)
|
@@ -39,6 +45,10 @@ function LatentEngine({
|
|
39 |
const videoLayer = useLatentEngine(s => s.videoLayer)
|
40 |
const segmentationLayer = useLatentEngine(s => s.segmentationLayer)
|
41 |
const interfaceLayer = useLatentEngine(s => s.interfaceLayer)
|
|
|
|
|
|
|
|
|
42 |
|
43 |
const stateRef = useRef({ isInitialized: false })
|
44 |
|
@@ -47,15 +57,29 @@ function LatentEngine({
|
|
47 |
const overlayTimerRef = useRef<NodeJS.Timeout>()
|
48 |
|
49 |
const videoLayerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
50 |
|
51 |
useEffect(() => {
|
52 |
-
if (!stateRef.current.isInitialized) {
|
53 |
stateRef.current.isInitialized = true
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
}
|
58 |
-
}, [])
|
59 |
|
60 |
const isPlayingRef = useRef(isPlaying)
|
61 |
isPlayingRef.current = isPlaying
|
@@ -88,18 +112,35 @@ function LatentEngine({
|
|
88 |
useEffect(() => {
|
89 |
if (!videoLayerRef.current) { return }
|
90 |
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
92 |
setVideoElement(videoElements.at(0))
|
93 |
|
94 |
// images are used for simpler or static experiences
|
95 |
-
const imageElements = Array.from(
|
|
|
|
|
96 |
setImageElement(imageElements.at(0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
})
|
98 |
|
99 |
useEffect(() => {
|
100 |
setContainerDimension({ width: width || 256, height: height || 256 })
|
101 |
}, [width, height])
|
102 |
|
|
|
103 |
return (
|
104 |
<div
|
105 |
style={{ width, height }}
|
@@ -115,24 +156,31 @@ function LatentEngine({
|
|
115 |
|
116 |
{/* main content container */}
|
117 |
<ContentLayer
|
118 |
-
className=""
|
119 |
width={width}
|
120 |
height={height}
|
121 |
ref={videoLayerRef}
|
|
|
122 |
>{videoLayer}</ContentLayer>
|
123 |
|
|
|
124 |
<ContentLayer
|
125 |
-
className=""
|
126 |
width={width}
|
127 |
height={height}
|
128 |
-
|
|
|
|
|
|
|
|
|
129 |
|
|
|
130 |
<ContentLayer
|
131 |
-
className=""
|
132 |
width={width}
|
133 |
height={height}
|
134 |
-
|
135 |
-
|
136 |
|
137 |
{/* content overlay, with the gradient, buttons etc */}
|
138 |
<div className={cn(`
|
@@ -142,6 +190,7 @@ function LatentEngine({
|
|
142 |
items-center justify-end
|
143 |
pt-5 px-3 pb-1
|
144 |
transition-opacity duration-300 ease-in-out
|
|
|
145 |
`,
|
146 |
isOverlayVisible ? "opacity-100" : "opacity-0"
|
147 |
)}
|
@@ -185,6 +234,7 @@ function LatentEngine({
|
|
185 |
flex flex-row flex-none
|
186 |
w-full h-14
|
187 |
items-center justify-between
|
|
|
188 |
`)}>
|
189 |
|
190 |
{/* left-side buttons */}
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import React, { MouseEventHandler, useEffect, useRef, useState } from "react"
|
4 |
|
|
|
5 |
import { cn } from "@/lib/utils/cn"
|
6 |
|
7 |
import { useLatentEngine } from "../store/useLatentEngine"
|
8 |
import { PlayPauseButton } from "../components/play-pause-button"
|
9 |
import { StreamTag } from "../../stream-tag"
|
10 |
import { ContentLayer } from "../components/content-layer"
|
11 |
+
import { MediaInfo } from "@/types/general"
|
12 |
+
import { getMockClap } from "@/lib/clap/getMockClap"
|
13 |
+
import { serializeClap } from "@/lib/clap/serializeClap"
|
14 |
+
import { blobToDataUri } from "@/app/api/utils/blobToDataUri"
|
15 |
+
import { InteractiveSegmentationCanvas } from "@/lib/on-device-ai/getInteractiveSegmentationCanvas"
|
16 |
+
import { InteractiveSegmenterResult } from "@mediapipe/tasks-vision"
|
17 |
|
18 |
function LatentEngine({
|
19 |
+
media,
|
20 |
width,
|
21 |
height,
|
22 |
className = "" }: {
|
23 |
+
media: MediaInfo
|
24 |
width?: number
|
25 |
height?: number
|
26 |
className?: string
|
27 |
}) {
|
28 |
const setContainerDimension = useLatentEngine(s => s.setContainerDimension)
|
29 |
const isLoaded = useLatentEngine(s => s.isLoaded)
|
30 |
+
const imagine = useLatentEngine(s => s.imagine)
|
31 |
+
const open = useLatentEngine(s => s.open)
|
32 |
|
33 |
const setImageElement = useLatentEngine(s => s.setImageElement)
|
34 |
const setVideoElement = useLatentEngine(s => s.setVideoElement)
|
35 |
+
const setSegmentationElement = useLatentEngine(s => s.setSegmentationElement)
|
36 |
|
37 |
const streamType = useLatentEngine(s => s.streamType)
|
38 |
const isStatic = useLatentEngine(s => s.isStatic)
|
|
|
45 |
const videoLayer = useLatentEngine(s => s.videoLayer)
|
46 |
const segmentationLayer = useLatentEngine(s => s.segmentationLayer)
|
47 |
const interfaceLayer = useLatentEngine(s => s.interfaceLayer)
|
48 |
+
const videoElement = useLatentEngine(s => s.videoElement)
|
49 |
+
const imageElement = useLatentEngine(s => s.imageElement)
|
50 |
+
|
51 |
+
const onClickOnSegmentationLayer = useLatentEngine(s => s.onClickOnSegmentationLayer)
|
52 |
|
53 |
const stateRef = useRef({ isInitialized: false })
|
54 |
|
|
|
57 |
const overlayTimerRef = useRef<NodeJS.Timeout>()
|
58 |
|
59 |
const videoLayerRef = useRef<HTMLDivElement>(null)
|
60 |
+
const segmentationLayerRef = useRef<HTMLDivElement>(null)
|
61 |
+
|
62 |
+
const mediaUrl = media.clapUrl || media.assetUrlHd || media.assetUrl
|
63 |
|
64 |
useEffect(() => {
|
65 |
+
if (!stateRef.current.isInitialized && mediaUrl) {
|
66 |
stateRef.current.isInitialized = true
|
67 |
+
|
68 |
+
const fn = async () => {
|
69 |
+
// TODO julian
|
70 |
+
// there is a bug, we can't unpack the .clap when it's from a data-uri :/
|
71 |
+
|
72 |
+
// open(mediaUrl)
|
73 |
+
const mockClap = getMockClap()
|
74 |
+
const mockArchive = await serializeClap(mockClap)
|
75 |
+
// for some reason conversion to data uri doesn't work
|
76 |
+
// const mockDataUri = await blobToDataUri(mockArchive, "application/x-gzip")
|
77 |
+
// console.log("mockDataUri:", mockDataUri)
|
78 |
+
open(mockArchive)
|
79 |
+
}
|
80 |
+
fn()
|
81 |
}
|
82 |
+
}, [mediaUrl])
|
83 |
|
84 |
const isPlayingRef = useRef(isPlaying)
|
85 |
isPlayingRef.current = isPlaying
|
|
|
112 |
useEffect(() => {
|
113 |
if (!videoLayerRef.current) { return }
|
114 |
|
115 |
+
// note how in both cases we are pulling from the videoLayerRef
|
116 |
+
// that's because one day everything will be a video, but for now we
|
117 |
+
// "fake it until we make it"
|
118 |
+
const videoElements = Array.from(
|
119 |
+
videoLayerRef.current.querySelectorAll('.latent-video')
|
120 |
+
) as HTMLVideoElement[]
|
121 |
setVideoElement(videoElements.at(0))
|
122 |
|
123 |
// images are used for simpler or static experiences
|
124 |
+
const imageElements = Array.from(
|
125 |
+
videoLayerRef.current.querySelectorAll('.latent-image')
|
126 |
+
) as HTMLImageElement[]
|
127 |
setImageElement(imageElements.at(0))
|
128 |
+
|
129 |
+
|
130 |
+
if (!segmentationLayerRef.current) { return }
|
131 |
+
|
132 |
+
const segmentationElements = Array.from(
|
133 |
+
segmentationLayerRef.current.querySelectorAll('.segmentation-canvas')
|
134 |
+
) as HTMLCanvasElement[]
|
135 |
+
setSegmentationElement(segmentationElements.at(0))
|
136 |
+
|
137 |
})
|
138 |
|
139 |
useEffect(() => {
|
140 |
setContainerDimension({ width: width || 256, height: height || 256 })
|
141 |
}, [width, height])
|
142 |
|
143 |
+
|
144 |
return (
|
145 |
<div
|
146 |
style={{ width, height }}
|
|
|
156 |
|
157 |
{/* main content container */}
|
158 |
<ContentLayer
|
159 |
+
className="pointer-events-auto"
|
160 |
width={width}
|
161 |
height={height}
|
162 |
ref={videoLayerRef}
|
163 |
+
onClick={onClickOnSegmentationLayer}
|
164 |
>{videoLayer}</ContentLayer>
|
165 |
|
166 |
+
|
167 |
<ContentLayer
|
168 |
+
className="pointer-events-none"
|
169 |
width={width}
|
170 |
height={height}
|
171 |
+
ref={segmentationLayerRef}
|
172 |
+
><canvas
|
173 |
+
className="segmentation-canvas"
|
174 |
+
style={{ width, height }}
|
175 |
+
></canvas></ContentLayer>
|
176 |
|
177 |
+
{/*
|
178 |
<ContentLayer
|
179 |
+
className="pointer-events-auto"
|
180 |
width={width}
|
181 |
height={height}
|
182 |
+
>{interfaceLayer}</ContentLayer>
|
183 |
+
*/}
|
184 |
|
185 |
{/* content overlay, with the gradient, buttons etc */}
|
186 |
<div className={cn(`
|
|
|
190 |
items-center justify-end
|
191 |
pt-5 px-3 pb-1
|
192 |
transition-opacity duration-300 ease-in-out
|
193 |
+
pointer-events-none
|
194 |
`,
|
195 |
isOverlayVisible ? "opacity-100" : "opacity-0"
|
196 |
)}
|
|
|
234 |
flex flex-row flex-none
|
235 |
w-full h-14
|
236 |
items-center justify-between
|
237 |
+
pointer-events-auto
|
238 |
`)}>
|
239 |
|
240 |
{/* left-side buttons */}
|
src/components/interface/latent-engine/core/types.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import { ClapProject, ClapSegment, ClapStreamType } from "@/lib/clap/types"
|
2 |
-
import {
|
|
|
3 |
|
4 |
export type LatentEngineStatus =
|
5 |
| "idle"
|
@@ -30,6 +31,7 @@ export type LatentEngineStore = {
|
|
30 |
height: number
|
31 |
|
32 |
clap: ClapProject
|
|
|
33 |
|
34 |
streamType: ClapStreamType
|
35 |
|
@@ -61,6 +63,7 @@ export type LatentEngineStore = {
|
|
61 |
videoLayerElement?: HTMLDivElement
|
62 |
imageElement?: HTMLImageElement
|
63 |
videoElement?: HTMLVideoElement
|
|
|
64 |
|
65 |
videoLayer: ReactNode
|
66 |
videoBuffer: "A" | "B"
|
@@ -75,13 +78,17 @@ export type LatentEngineStore = {
|
|
75 |
interfaceBufferB: ReactNode
|
76 |
|
77 |
setContainerDimension: ({ width, height }: { width: number; height: number }) => void
|
78 |
-
|
79 |
-
|
80 |
|
81 |
setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => void
|
82 |
setImageElement: (imageElement?: HTMLImageElement) => void
|
83 |
setVideoElement: (videoElement?: HTMLVideoElement) => void
|
|
|
84 |
|
|
|
|
|
|
|
85 |
togglePlayPause: () => boolean
|
86 |
play: () => boolean
|
87 |
pause: () => boolean
|
|
|
1 |
import { ClapProject, ClapSegment, ClapStreamType } from "@/lib/clap/types"
|
2 |
+
import { InteractiveSegmenterResult } from "@mediapipe/tasks-vision"
|
3 |
+
import { MouseEventHandler, ReactNode } from "react"
|
4 |
|
5 |
export type LatentEngineStatus =
|
6 |
| "idle"
|
|
|
31 |
height: number
|
32 |
|
33 |
clap: ClapProject
|
34 |
+
debug: boolean
|
35 |
|
36 |
streamType: ClapStreamType
|
37 |
|
|
|
63 |
videoLayerElement?: HTMLDivElement
|
64 |
imageElement?: HTMLImageElement
|
65 |
videoElement?: HTMLVideoElement
|
66 |
+
segmentationElement?: HTMLCanvasElement
|
67 |
|
68 |
videoLayer: ReactNode
|
69 |
videoBuffer: "A" | "B"
|
|
|
78 |
interfaceBufferB: ReactNode
|
79 |
|
80 |
setContainerDimension: ({ width, height }: { width: number; height: number }) => void
|
81 |
+
imagine: (prompt: string) => Promise<void>
|
82 |
+
open: (src?: string | ClapProject | Blob) => Promise<void>
|
83 |
|
84 |
setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => void
|
85 |
setImageElement: (imageElement?: HTMLImageElement) => void
|
86 |
setVideoElement: (videoElement?: HTMLVideoElement) => void
|
87 |
+
setSegmentationElement: (segmentationElement?: HTMLCanvasElement) => void
|
88 |
|
89 |
+
processClickOnSegment: (data: InteractiveSegmenterResult) => void
|
90 |
+
onClickOnSegmentationLayer: MouseEventHandler<HTMLDivElement>
|
91 |
+
|
92 |
togglePlayPause: () => boolean
|
93 |
play: () => boolean
|
94 |
pause: () => boolean
|
src/components/interface/latent-engine/resolvers/image/index.tsx
CHANGED
@@ -23,6 +23,9 @@ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<
|
|
23 |
// note: the latent-image class is not used for styling, but to grab the component
|
24 |
// from JS when we need to segment etc
|
25 |
return (
|
26 |
-
<img
|
|
|
|
|
|
|
27 |
)
|
28 |
}
|
|
|
23 |
// note: the latent-image class is not used for styling, but to grab the component
|
24 |
// from JS when we need to segment etc
|
25 |
return (
|
26 |
+
<img
|
27 |
+
className="latent-image object-cover h-full"
|
28 |
+
src={assetUrl}
|
29 |
+
/>
|
30 |
)
|
31 |
}
|
src/components/interface/latent-engine/store/useLatentEngine.ts
CHANGED
@@ -4,17 +4,23 @@ import { create } from "zustand"
|
|
4 |
import { ClapProject } from "@/lib/clap/types"
|
5 |
import { newClap } from "@/lib/clap/newClap"
|
6 |
import { sleep } from "@/lib/utils/sleep"
|
7 |
-
import { getSegmentationCanvas } from "@/lib/on-device-ai/getSegmentationCanvas"
|
8 |
|
9 |
import { LatentEngineStore } from "../core/types"
|
10 |
import { resolveSegments } from "../resolvers/resolveSegments"
|
11 |
import { fetchLatentClap } from "../core/fetchLatentClap"
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
14 |
width: 1024,
|
15 |
height: 576,
|
16 |
|
17 |
clap: newClap(),
|
|
|
18 |
|
19 |
streamType: "static",
|
20 |
isStatic: false,
|
@@ -42,7 +48,8 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
42 |
videoLayerElement: undefined,
|
43 |
imageElement: undefined,
|
44 |
videoElement: undefined,
|
45 |
-
|
|
|
46 |
videoLayer: undefined,
|
47 |
videoBuffer: "A",
|
48 |
videoBufferA: null,
|
@@ -62,7 +69,7 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
62 |
})
|
63 |
},
|
64 |
|
65 |
-
|
66 |
set({
|
67 |
isLoaded: false,
|
68 |
isLoading: true,
|
@@ -81,10 +88,30 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
81 |
|
82 |
if (!clap) { return }
|
83 |
|
84 |
-
get().
|
85 |
},
|
86 |
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
set({
|
89 |
clap,
|
90 |
isLoading: false,
|
@@ -99,7 +126,59 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
99 |
setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => { set({ videoLayerElement }) },
|
100 |
setImageElement: (imageElement?: HTMLImageElement) => { set({ imageElement }) },
|
101 |
setVideoElement: (videoElement?: HTMLVideoElement) => { set({ videoElement }) },
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
togglePlayPause: (): boolean => {
|
104 |
const { isLoaded, isPlaying, renderingIntervalId } = get()
|
105 |
if (!isLoaded) { return false }
|
@@ -176,6 +255,7 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
176 |
|
177 |
try {
|
178 |
|
|
|
179 |
// console.log("doing stuff")
|
180 |
let timestamp = performance.now()
|
181 |
|
@@ -189,6 +269,7 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
|
189 |
})
|
190 |
set({ segmentationLayer })
|
191 |
}
|
|
|
192 |
|
193 |
await sleep(500)
|
194 |
|
|
|
4 |
import { ClapProject } from "@/lib/clap/types"
|
5 |
import { newClap } from "@/lib/clap/newClap"
|
6 |
import { sleep } from "@/lib/utils/sleep"
|
7 |
+
// import { getSegmentationCanvas } from "@/lib/on-device-ai/getSegmentationCanvas"
|
8 |
|
9 |
import { LatentEngineStore } from "../core/types"
|
10 |
import { resolveSegments } from "../resolvers/resolveSegments"
|
11 |
import { fetchLatentClap } from "../core/fetchLatentClap"
|
12 |
+
import { dataUriToBlob } from "@/app/api/utils/dataUriToBlob"
|
13 |
+
import { parseClap } from "@/lib/clap/parseClap"
|
14 |
+
import { InteractiveSegmenterResult, MPMask } from "@mediapipe/tasks-vision"
|
15 |
+
import { segmentFrame } from "@/lib/on-device-ai/segmentFrameOnClick"
|
16 |
+
import { drawSegmentation } from "../core/drawSegmentation"
|
17 |
|
18 |
export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
19 |
width: 1024,
|
20 |
height: 576,
|
21 |
|
22 |
clap: newClap(),
|
23 |
+
debug: true,
|
24 |
|
25 |
streamType: "static",
|
26 |
isStatic: false,
|
|
|
48 |
videoLayerElement: undefined,
|
49 |
imageElement: undefined,
|
50 |
videoElement: undefined,
|
51 |
+
segmentationElement: undefined,
|
52 |
+
|
53 |
videoLayer: undefined,
|
54 |
videoBuffer: "A",
|
55 |
videoBufferA: null,
|
|
|
69 |
})
|
70 |
},
|
71 |
|
72 |
+
imagine: async (prompt: string): Promise<void> => {
|
73 |
set({
|
74 |
isLoaded: false,
|
75 |
isLoading: true,
|
|
|
88 |
|
89 |
if (!clap) { return }
|
90 |
|
91 |
+
get().open(clap)
|
92 |
},
|
93 |
|
94 |
+
|
95 |
+
open: async (src?: string | ClapProject | Blob) => {
|
96 |
+
const { debug } = get()
|
97 |
+
set({
|
98 |
+
isLoaded: false,
|
99 |
+
isLoading: true,
|
100 |
+
})
|
101 |
+
|
102 |
+
let clap: ClapProject | undefined = undefined
|
103 |
+
|
104 |
+
try {
|
105 |
+
clap = await parseClap(src, debug)
|
106 |
+
} catch (err) {
|
107 |
+
console.error(`failed to open the Clap: ${err}`)
|
108 |
+
set({
|
109 |
+
isLoading: false,
|
110 |
+
})
|
111 |
+
}
|
112 |
+
|
113 |
+
if (!clap) { return }
|
114 |
+
|
115 |
set({
|
116 |
clap,
|
117 |
isLoading: false,
|
|
|
126 |
setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => { set({ videoLayerElement }) },
|
127 |
setImageElement: (imageElement?: HTMLImageElement) => { set({ imageElement }) },
|
128 |
setVideoElement: (videoElement?: HTMLVideoElement) => { set({ videoElement }) },
|
129 |
+
setSegmentationElement: (segmentationElement?: HTMLCanvasElement) => { set({ segmentationElement }) },
|
130 |
+
|
131 |
+
processClickOnSegment: (result: InteractiveSegmenterResult) => {
|
132 |
+
console.log(`processClickOnSegment: user clicked on something:`, result)
|
133 |
+
|
134 |
+
const { videoElement, imageElement, segmentationElement, debug } = get()
|
135 |
|
136 |
+
if (!result?.categoryMask) {
|
137 |
+
if (debug) {
|
138 |
+
console.log(`processClickOnSegment: no categoryMask, so we skip the click`)
|
139 |
+
}
|
140 |
+
return
|
141 |
+
}
|
142 |
+
|
143 |
+
try {
|
144 |
+
if (debug) {
|
145 |
+
console.log(`processClickOnSegment: callling drawSegmentation`)
|
146 |
+
}
|
147 |
+
drawSegmentation(result.categoryMask, segmentationElement)
|
148 |
+
|
149 |
+
if (debug) {
|
150 |
+
console.log("processClickOnSegment: TODO call data.close() to free the memory!")
|
151 |
+
}
|
152 |
+
result.close()
|
153 |
+
} catch (err) {
|
154 |
+
console.error(`processClickOnSegment: something failed ${err}`)
|
155 |
+
}
|
156 |
+
},
|
157 |
+
onClickOnSegmentationLayer: (event) => {
|
158 |
+
|
159 |
+
const { videoElement, imageElement, segmentationLayer, segmentationElement, debug } = get()
|
160 |
+
if (debug) {
|
161 |
+
console.log("onClickOnSegmentationLayer")
|
162 |
+
}
|
163 |
+
// TODO use the videoElement if this is is video!
|
164 |
+
if (!imageElement) { return }
|
165 |
+
|
166 |
+
const box = event.currentTarget.getBoundingClientRect()
|
167 |
+
|
168 |
+
const px = event.clientX
|
169 |
+
const py = event.clientY
|
170 |
+
|
171 |
+
const x = px / box.width
|
172 |
+
const y = py / box.height
|
173 |
+
console.log(`onClickOnSegmentationLayer: user clicked on `, { x, y, px, py, box, imageElement })
|
174 |
+
|
175 |
+
const fn = async () => {
|
176 |
+
const results: InteractiveSegmenterResult = await segmentFrame(imageElement, x, y)
|
177 |
+
get().processClickOnSegment(results)
|
178 |
+
}
|
179 |
+
fn()
|
180 |
+
},
|
181 |
+
|
182 |
togglePlayPause: (): boolean => {
|
183 |
const { isLoaded, isPlaying, renderingIntervalId } = get()
|
184 |
if (!isLoaded) { return false }
|
|
|
255 |
|
256 |
try {
|
257 |
|
258 |
+
/*
|
259 |
// console.log("doing stuff")
|
260 |
let timestamp = performance.now()
|
261 |
|
|
|
269 |
})
|
270 |
set({ segmentationLayer })
|
271 |
}
|
272 |
+
*/
|
273 |
|
274 |
await sleep(500)
|
275 |
|
src/components/interface/media-player/index.tsx
CHANGED
@@ -22,12 +22,13 @@ export function MediaPlayer({
|
|
22 |
className?: string
|
23 |
// currentTime?: number
|
24 |
}) {
|
25 |
-
|
26 |
|
27 |
-
if (!media
|
|
|
28 |
|
29 |
-
// uncomment one of those to forcefully test the .clap player
|
30 |
-
media.assetUrlHd = "https://huggingface.co/datasets/jbilcke/ai-tube-cinema/tree/main/404.clap"
|
31 |
|
32 |
// uncomment one of those to forcefully test the .splatv player!
|
33 |
// media.assetUrlHd = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/4d/flame/flame.splatv"
|
|
|
22 |
className?: string
|
23 |
// currentTime?: number
|
24 |
}) {
|
25 |
+
console.log("MediaPlayer called for \"" + media?.label + "\"")
|
26 |
|
27 |
+
if (!media) { return null }
|
28 |
+
if (!media?.assetUrl && !media?.clapUrl) { return null }
|
29 |
|
30 |
+
// uncomment one of those to forcefully test the .clap player from an external .clap file
|
31 |
+
// media.assetUrlHd = "https://huggingface.co/datasets/jbilcke/ai-tube-cinema/tree/main/404.clap"
|
32 |
|
33 |
// uncomment one of those to forcefully test the .splatv player!
|
34 |
// media.assetUrlHd = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/4d/flame/flame.splatv"
|
src/components/interface/media-player/latent.tsx
CHANGED
@@ -20,7 +20,7 @@ export function LatentPlayer({
|
|
20 |
// TODO add a play bar which should support fixed, streaming and live modes
|
21 |
return (
|
22 |
<LatentEngine
|
23 |
-
|
24 |
width={width}
|
25 |
height={height}
|
26 |
className={className}
|
|
|
20 |
// TODO add a play bar which should support fixed, streaming and live modes
|
21 |
return (
|
22 |
<LatentEngine
|
23 |
+
media={media}
|
24 |
width={width}
|
25 |
height={height}
|
26 |
className={className}
|
src/components/interface/stream-tag/index.tsx
CHANGED
@@ -15,6 +15,12 @@ export function StreamTag({
|
|
15 |
const isInteractive = streamType === "interactive"
|
16 |
const isLive = streamType === "live"
|
17 |
const isStatic = !isInteractive && !isLive
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
return (
|
20 |
<div className={cn(`
|
|
|
15 |
const isInteractive = streamType === "interactive"
|
16 |
const isLive = streamType === "live"
|
17 |
const isStatic = !isInteractive && !isLive
|
18 |
+
console.log("debug:", {
|
19 |
+
streamType,
|
20 |
+
isInteractive,
|
21 |
+
isLive,
|
22 |
+
isStatic
|
23 |
+
})
|
24 |
|
25 |
return (
|
26 |
<div className={cn(`
|
src/lib/clap/clapToDataUri.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { blobToDataUri } from "@/app/api/utils/blobToDataUri"
|
2 |
+
import { serializeClap } from "./serializeClap"
|
3 |
+
import { ClapProject } from "./types"
|
4 |
+
|
5 |
+
export async function clapToDataUri(clap: ClapProject): Promise<string> {
|
6 |
+
const archive = await serializeClap(clap)
|
7 |
+
const dataUri = await blobToDataUri(archive, "application/x-gzip")
|
8 |
+
return dataUri
|
9 |
+
}
|
src/lib/clap/{mockClap.ts → getMockClap.ts}
RENAMED
@@ -4,12 +4,25 @@ import { ClapProject } from "./types"
|
|
4 |
|
5 |
let defaultSegmentDurationInMs = 2000
|
6 |
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
9 |
}: {
|
10 |
-
|
|
|
|
|
|
|
|
|
11 |
}): ClapProject {
|
12 |
-
const clap = newClap(
|
|
|
|
|
|
|
|
|
13 |
|
14 |
let currentElapsedTimeInMs = 0
|
15 |
let currentSegmentDurationInMs = defaultSegmentDurationInMs
|
@@ -57,8 +70,7 @@ export function mockClap({
|
|
57 |
startTimeInMs: currentElapsedTimeInMs,
|
58 |
endTimeInMs: currentSegmentDurationInMs,
|
59 |
category: "video",
|
60 |
-
|
61 |
-
prompt: "portrait of a man tv news anchor, pierre-jean-hyves, serious, bokeh",
|
62 |
label: "demo",
|
63 |
outputType: "video",
|
64 |
}))
|
|
|
4 |
|
5 |
let defaultSegmentDurationInMs = 2000
|
6 |
|
7 |
+
// const demoPrompt = "closeup of Queen angelfish, bokeh"
|
8 |
+
// const demoPrompt = "portrait of a man tv news anchor, pierre-jean-hyves, serious, bokeh"
|
9 |
+
const demoPrompt = "dogs and cats, playing in garden, balls, trees"
|
10 |
+
|
11 |
+
export function getMockClap({
|
12 |
+
prompt =demoPrompt,
|
13 |
+
showDisclaimer = true,
|
14 |
}: {
|
15 |
+
prompt?: string
|
16 |
+
showDisclaimer?: boolean
|
17 |
+
} = {
|
18 |
+
prompt: demoPrompt,
|
19 |
+
showDisclaimer: true,
|
20 |
}): ClapProject {
|
21 |
+
const clap = newClap({
|
22 |
+
meta: {
|
23 |
+
streamType: "interactive"
|
24 |
+
}
|
25 |
+
})
|
26 |
|
27 |
let currentElapsedTimeInMs = 0
|
28 |
let currentSegmentDurationInMs = defaultSegmentDurationInMs
|
|
|
70 |
startTimeInMs: currentElapsedTimeInMs,
|
71 |
endTimeInMs: currentSegmentDurationInMs,
|
72 |
category: "video",
|
73 |
+
prompt,
|
|
|
74 |
label: "demo",
|
75 |
outputType: "video",
|
76 |
}))
|
src/lib/clap/newClap.ts
CHANGED
@@ -16,6 +16,7 @@ export function newClap(clap: {
|
|
16 |
id: clap?.meta?.id === "string" ? clap.meta.id : uuidv4(),
|
17 |
title: clap?.meta?.title === "string" ? clap.meta.title : "",
|
18 |
description: typeof clap?.meta?.description === "string" ? clap.meta.description : "",
|
|
|
19 |
licence: typeof clap?.meta?.licence === "string" ? clap.meta.licence : "",
|
20 |
orientation: clap?.meta?.orientation === "portrait" ? "portrait" : clap?.meta?.orientation === "square" ? "square" : "landscape",
|
21 |
width: getValidNumber(clap?.meta?.width, 256, 8192, 1024),
|
|
|
16 |
id: clap?.meta?.id === "string" ? clap.meta.id : uuidv4(),
|
17 |
title: clap?.meta?.title === "string" ? clap.meta.title : "",
|
18 |
description: typeof clap?.meta?.description === "string" ? clap.meta.description : "",
|
19 |
+
synopsis: typeof clap?.meta?.synopsis === "string" ? clap.meta.synopsis : "",
|
20 |
licence: typeof clap?.meta?.licence === "string" ? clap.meta.licence : "",
|
21 |
orientation: clap?.meta?.orientation === "portrait" ? "portrait" : clap?.meta?.orientation === "square" ? "square" : "landscape",
|
22 |
width: getValidNumber(clap?.meta?.width, 256, 8192, 1024),
|
src/lib/clap/parseClap.ts
CHANGED
@@ -3,48 +3,158 @@ import { v4 as uuidv4 } from "uuid"
|
|
3 |
|
4 |
import { ClapHeader, ClapMeta, ClapModel, ClapProject, ClapScene, ClapSegment, ClapStreamType } from "./types"
|
5 |
import { getValidNumber } from "@/lib/utils/getValidNumber"
|
|
|
|
|
|
|
6 |
|
7 |
/**
|
8 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
*
|
10 |
* note: it is not really async, because for some reason YAML.parse is a blocking call like for JSON,
|
11 |
-
*
|
12 |
*/
|
13 |
-
export async function parseClap(
|
14 |
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
19 |
-
typeof inputStringOrBlob === "string"
|
20 |
-
? new Blob([inputStringOrBlob], { type: "application/x-yaml" })
|
21 |
-
: inputStringOrBlob;
|
22 |
|
23 |
-
|
|
|
|
|
24 |
|
25 |
-
|
26 |
-
const
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
31 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
throw new Error("invalid clap file (need a clap format header block and project metadata block)")
|
33 |
}
|
34 |
|
35 |
-
|
|
|
|
|
|
|
|
|
36 |
|
37 |
if (maybeClapHeader.format !== "clap-0") {
|
38 |
throw new Error("invalid clap file (sorry, but you can't make up version numbers like that)")
|
39 |
}
|
40 |
|
41 |
|
42 |
-
const maybeClapMeta =
|
43 |
|
44 |
const clapMeta: ClapMeta = {
|
45 |
id: typeof maybeClapMeta.title === "string" ? maybeClapMeta.id : uuidv4(),
|
46 |
title: typeof maybeClapMeta.title === "string" ? maybeClapMeta.title : "",
|
47 |
description: typeof maybeClapMeta.description === "string" ? maybeClapMeta.description : "",
|
|
|
48 |
licence: typeof maybeClapMeta.licence === "string" ? maybeClapMeta.licence : "",
|
49 |
orientation: maybeClapMeta.orientation === "portrait" ? "portrait" : maybeClapMeta.orientation === "square" ? "square" : "landscape",
|
50 |
width: getValidNumber(maybeClapMeta.width, 256, 8192, 1024),
|
@@ -82,12 +192,12 @@ export async function parseClap(inputStringOrBlob: string | Blob): Promise<ClapP
|
|
82 |
const afterTheScenes = afterTheModels + expectedNumberOfScenes
|
83 |
|
84 |
// note: if there are no expected models, maybeModels will be empty
|
85 |
-
const maybeModels =
|
86 |
|
87 |
// note: if there are no expected scenes, maybeScenes will be empty
|
88 |
-
const maybeScenes =
|
89 |
|
90 |
-
const maybeSegments =
|
91 |
|
92 |
const clapModels: ClapModel[] = maybeModels.map(({
|
93 |
id,
|
@@ -191,6 +301,9 @@ export async function parseClap(inputStringOrBlob: string | Blob): Promise<ClapP
|
|
191 |
seed,
|
192 |
}))
|
193 |
|
|
|
|
|
|
|
194 |
return {
|
195 |
meta: clapMeta,
|
196 |
models: clapModels,
|
|
|
3 |
|
4 |
import { ClapHeader, ClapMeta, ClapModel, ClapProject, ClapScene, ClapSegment, ClapStreamType } from "./types"
|
5 |
import { getValidNumber } from "@/lib/utils/getValidNumber"
|
6 |
+
import { dataUriToBlob } from "@/app/api/utils/dataUriToBlob"
|
7 |
+
|
8 |
+
type StringOrBlob = string | Blob
|
9 |
|
10 |
/**
|
11 |
+
* Import a clap file from various data sources into an ClapProject
|
12 |
+
*
|
13 |
+
* Inputs can be:
|
14 |
+
* - a Clap project (which is an object)
|
15 |
+
* - an URL to a remote .clap file
|
16 |
+
* - a string containing a YAML array
|
17 |
+
* - a data uri containing a gzipped YAML array
|
18 |
+
* - a Blob containing a gzipped YAML array
|
19 |
*
|
20 |
* note: it is not really async, because for some reason YAML.parse is a blocking call like for JSON,
|
21 |
+
* there is no async version although we are now in the 20s not 90s
|
22 |
*/
|
23 |
+
export async function parseClap(src?: ClapProject | string | Blob, debug = false): Promise<ClapProject> {
|
24 |
|
25 |
+
try {
|
26 |
+
if (typeof src === "object" && Array.isArray(src?.scenes) && Array.isArray(src?.models)) {
|
27 |
+
if (debug) {
|
28 |
+
console.log("parseClap: input is already a Clap file, nothing to do:", src)
|
29 |
+
}
|
30 |
+
// we can skip verification
|
31 |
+
return src as ClapProject
|
32 |
+
}
|
33 |
+
} catch (err) {
|
34 |
+
// well, this is not a clap project
|
35 |
+
}
|
36 |
|
37 |
+
let stringOrBlob = (src || "") as StringOrBlob
|
|
|
|
|
|
|
38 |
|
39 |
+
// both should work
|
40 |
+
const dataUriHeader1 = "data:application/x-gzip;base64,"
|
41 |
+
const dataUriHeader2 = "data:application/octet-stream;base64,"
|
42 |
|
43 |
+
const inputIsString = typeof stringOrBlob === "string"
|
44 |
+
const inputIsDataUri = typeof stringOrBlob === "string" ? stringOrBlob.startsWith(dataUriHeader1) || stringOrBlob.startsWith(dataUriHeader2) : false
|
45 |
+
const inputIsRemoteFile = typeof stringOrBlob === "string" ? (stringOrBlob.startsWith("http://") || stringOrBlob.startsWith("https://")) : false
|
46 |
+
|
47 |
+
let inputIsBlob = typeof stringOrBlob !== "string"
|
48 |
+
|
49 |
+
let inputYamlArrayString = ""
|
50 |
+
|
51 |
+
if (debug) {
|
52 |
+
console.log(`parseClap: pre-analysis: ${JSON.stringify({
|
53 |
+
inputIsString,
|
54 |
+
inputIsBlob,
|
55 |
+
inputIsDataUri,
|
56 |
+
inputIsRemoteFile
|
57 |
+
}, null, 2)}`)
|
58 |
+
}
|
59 |
+
|
60 |
+
if (typeof stringOrBlob === "string") {
|
61 |
+
if (debug) {
|
62 |
+
console.log("parseClap: input is a string ", stringOrBlob.slice(0, 120))
|
63 |
+
}
|
64 |
+
if (inputIsDataUri) {
|
65 |
+
if (debug) {
|
66 |
+
console.log(`parseClap: input is a data uri archive`)
|
67 |
+
}
|
68 |
+
stringOrBlob = dataUriToBlob(stringOrBlob, "application/x-gzip")
|
69 |
+
if (debug) {
|
70 |
+
console.log(`parseClap: inputBlob = `, stringOrBlob)
|
71 |
+
}
|
72 |
+
inputIsBlob = true
|
73 |
+
} else if (inputIsRemoteFile) {
|
74 |
+
try {
|
75 |
+
if (debug) {
|
76 |
+
console.log(`parseClap: input is a remote .clap file`)
|
77 |
+
}
|
78 |
+
const res = await fetch(stringOrBlob)
|
79 |
+
stringOrBlob = await res.blob()
|
80 |
+
if (!stringOrBlob) { throw new Error("blob is empty") }
|
81 |
+
inputIsBlob = true
|
82 |
+
} catch (err) {
|
83 |
+
// url seems invalid
|
84 |
+
throw new Error(`failed to download the .clap file (${err})`)
|
85 |
+
}
|
86 |
+
} else {
|
87 |
+
if (debug) {
|
88 |
+
console.log("parseClap: input is a text string containing a YAML array")
|
89 |
+
}
|
90 |
+
inputYamlArrayString = stringOrBlob
|
91 |
+
inputIsBlob = false
|
92 |
+
}
|
93 |
+
}
|
94 |
|
95 |
+
if (typeof stringOrBlob !== "string" && stringOrBlob) {
|
96 |
+
if (debug) {
|
97 |
+
console.log("parseClap: decompressing the blob..")
|
98 |
+
}
|
99 |
+
// Decompress the input blob using gzip
|
100 |
+
const decompressedStream = stringOrBlob.stream().pipeThrough(new DecompressionStream('gzip'))
|
101 |
+
|
102 |
+
try {
|
103 |
+
// Convert the stream to text using a Response object
|
104 |
+
const decompressedOutput = new Response(decompressedStream)
|
105 |
+
// decompressedOutput.headers.set("Content-Type", "application/x-gzip")
|
106 |
+
if (debug) {
|
107 |
+
console.log("parseClap: decompressedOutput: ", decompressedOutput)
|
108 |
+
}
|
109 |
+
// const blobAgain = await decompressedOutput.blob()
|
110 |
+
inputYamlArrayString = await decompressedOutput.text()
|
111 |
+
|
112 |
+
if (debug && inputYamlArrayString) {
|
113 |
+
console.log("parseClap: successfully decompressed the blob!")
|
114 |
+
}
|
115 |
+
} catch (err) {
|
116 |
+
const message = `parseClap: failed to decompress (${err})`
|
117 |
+
console.error(message)
|
118 |
+
throw new Error(message)
|
119 |
+
}
|
120 |
+
}
|
121 |
+
|
122 |
+
// we don't need this anymore I think
|
123 |
+
// new Blob([inputStringOrBlob], { type: "application/x-yaml" })
|
124 |
+
|
125 |
+
let maybeArray: any = {}
|
126 |
+
try {
|
127 |
+
if (debug) {
|
128 |
+
console.log("parseClap: parsing the YAML array..")
|
129 |
+
}
|
130 |
+
// Parse YAML string to raw data
|
131 |
+
maybeArray = YAML.parse(inputYamlArrayString)
|
132 |
+
} catch (err) {
|
133 |
+
throw new Error("invalid clap file (input string is not YAML)")
|
134 |
+
}
|
135 |
+
|
136 |
+
if (!Array.isArray(maybeArray) || maybeArray.length < 2) {
|
137 |
throw new Error("invalid clap file (need a clap format header block and project metadata block)")
|
138 |
}
|
139 |
|
140 |
+
if (debug) {
|
141 |
+
console.log("parseClap: the YAML seems okay, continuing decoding..")
|
142 |
+
}
|
143 |
+
|
144 |
+
const maybeClapHeader = maybeArray[0] as ClapHeader
|
145 |
|
146 |
if (maybeClapHeader.format !== "clap-0") {
|
147 |
throw new Error("invalid clap file (sorry, but you can't make up version numbers like that)")
|
148 |
}
|
149 |
|
150 |
|
151 |
+
const maybeClapMeta = maybeArray[1] as ClapMeta
|
152 |
|
153 |
const clapMeta: ClapMeta = {
|
154 |
id: typeof maybeClapMeta.title === "string" ? maybeClapMeta.id : uuidv4(),
|
155 |
title: typeof maybeClapMeta.title === "string" ? maybeClapMeta.title : "",
|
156 |
description: typeof maybeClapMeta.description === "string" ? maybeClapMeta.description : "",
|
157 |
+
synopsis: typeof maybeClapMeta.synopsis === "string" ? maybeClapMeta.synopsis : "",
|
158 |
licence: typeof maybeClapMeta.licence === "string" ? maybeClapMeta.licence : "",
|
159 |
orientation: maybeClapMeta.orientation === "portrait" ? "portrait" : maybeClapMeta.orientation === "square" ? "square" : "landscape",
|
160 |
width: getValidNumber(maybeClapMeta.width, 256, 8192, 1024),
|
|
|
192 |
const afterTheScenes = afterTheModels + expectedNumberOfScenes
|
193 |
|
194 |
// note: if there are no expected models, maybeModels will be empty
|
195 |
+
const maybeModels = maybeArray.slice(afterTheHeaders, afterTheModels) as ClapModel[]
|
196 |
|
197 |
// note: if there are no expected scenes, maybeScenes will be empty
|
198 |
+
const maybeScenes = maybeArray.slice(afterTheModels, afterTheScenes) as ClapScene[]
|
199 |
|
200 |
+
const maybeSegments = maybeArray.slice(afterTheScenes) as ClapSegment[]
|
201 |
|
202 |
const clapModels: ClapModel[] = maybeModels.map(({
|
203 |
id,
|
|
|
301 |
seed,
|
302 |
}))
|
303 |
|
304 |
+
if (debug) {
|
305 |
+
console.log(`parseClap: successfully parsed ${clapModels.length} models, ${clapScenes.length} scenes and ${clapSegments.length} segments`)
|
306 |
+
}
|
307 |
return {
|
308 |
meta: clapMeta,
|
309 |
models: clapModels,
|
src/lib/clap/serializeClap.ts
CHANGED
@@ -125,6 +125,7 @@ export async function serializeClap({
|
|
125 |
id: meta.id || uuidv4(),
|
126 |
title: typeof meta.title === "string" ? meta.title : "Untitled",
|
127 |
description: typeof meta.description === "string" ? meta.description : "",
|
|
|
128 |
licence: typeof meta.licence === "string" ? meta.licence : "",
|
129 |
orientation: meta.orientation === "portrait" ? "portrait" : meta.orientation === "square" ? "square" : "landscape",
|
130 |
width: getValidNumber(meta.width, 256, 8192, 1024),
|
@@ -149,14 +150,18 @@ export async function serializeClap({
|
|
149 |
const blobResult = new Blob([strigifiedResult], { type: "application/x-yaml" })
|
150 |
|
151 |
// Create a stream for the blob
|
152 |
-
const readableStream = blobResult.stream()
|
153 |
|
154 |
// Compress the stream using gzip
|
155 |
-
const compressionStream = new CompressionStream('gzip')
|
156 |
-
const compressedStream = readableStream.pipeThrough(compressionStream)
|
157 |
|
158 |
// Create a new blob from the compressed stream
|
159 |
-
const
|
|
|
|
|
|
|
|
|
160 |
|
161 |
return compressedBlob
|
162 |
}
|
|
|
125 |
id: meta.id || uuidv4(),
|
126 |
title: typeof meta.title === "string" ? meta.title : "Untitled",
|
127 |
description: typeof meta.description === "string" ? meta.description : "",
|
128 |
+
synopsis: typeof meta.synopsis === "string" ? meta.synopsis : "",
|
129 |
licence: typeof meta.licence === "string" ? meta.licence : "",
|
130 |
orientation: meta.orientation === "portrait" ? "portrait" : meta.orientation === "square" ? "square" : "landscape",
|
131 |
width: getValidNumber(meta.width, 256, 8192, 1024),
|
|
|
150 |
const blobResult = new Blob([strigifiedResult], { type: "application/x-yaml" })
|
151 |
|
152 |
// Create a stream for the blob
|
153 |
+
const readableStream = blobResult.stream()
|
154 |
|
155 |
// Compress the stream using gzip
|
156 |
+
const compressionStream = new CompressionStream('gzip')
|
157 |
+
const compressedStream = readableStream.pipeThrough(compressionStream)
|
158 |
|
159 |
// Create a new blob from the compressed stream
|
160 |
+
const response = new Response(compressedStream)
|
161 |
+
|
162 |
+
response.headers.set("Content-Type", "application/x-gzip")
|
163 |
+
|
164 |
+
const compressedBlob = await response.blob()
|
165 |
|
166 |
return compressedBlob
|
167 |
}
|
src/lib/on-device-ai/getInteractiveSegmentationCanvas.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useRef } from "react"
|
2 |
+
import { segmentFrame } from "./segmentFrameOnClick"
|
3 |
+
import { ImageSource, InteractiveSegmenterResult } from "@mediapipe/tasks-vision"
|
4 |
+
|
5 |
+
|
6 |
+
export function InteractiveSegmentationCanvas({
|
7 |
+
src,
|
8 |
+
onClick,
|
9 |
+
}: {
|
10 |
+
src?: ImageSource
|
11 |
+
onClick?: (results: InteractiveSegmenterResult ) => void
|
12 |
+
}) {
|
13 |
+
const segmentationClickRef = useRef<HTMLDivElement>(null)
|
14 |
+
return (
|
15 |
+
<div
|
16 |
+
ref={segmentationClickRef}
|
17 |
+
onClick={(event) => {
|
18 |
+
if (!segmentationClickRef.current || !src || !onClick) { return }
|
19 |
+
|
20 |
+
const box = segmentationClickRef.current.getBoundingClientRect()
|
21 |
+
|
22 |
+
const px = event.clientX
|
23 |
+
const py = event.clientY
|
24 |
+
|
25 |
+
const x = px / box.width
|
26 |
+
const y = py / box.height
|
27 |
+
|
28 |
+
const fn = async () => {
|
29 |
+
const results: InteractiveSegmenterResult = await segmentFrame(src, x, y);
|
30 |
+
onClick(results)
|
31 |
+
}
|
32 |
+
fn()
|
33 |
+
|
34 |
+
}}>
|
35 |
+
</div>
|
36 |
+
)
|
37 |
+
}
|
src/lib/on-device-ai/getSegmentationCanvas.tsx
CHANGED
@@ -26,6 +26,7 @@ export async function getSegmentationCanvas({
|
|
26 |
height: `${height}px`,
|
27 |
};
|
28 |
|
|
|
29 |
const CanvasComponent = () => (
|
30 |
<canvas
|
31 |
ref={(node) => {
|
|
|
26 |
height: `${height}px`,
|
27 |
};
|
28 |
|
29 |
+
console.log("canvas:", canvas)
|
30 |
const CanvasComponent = () => (
|
31 |
<canvas
|
32 |
ref={(node) => {
|
src/lib/on-device-ai/identifyFrame.ts
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
FilesetResolver,
|
3 |
+
ObjectDetector,
|
4 |
+
ObjectDetectorResult,
|
5 |
+
ImageSource
|
6 |
+
} from "@mediapipe/tasks-vision"
|
7 |
+
|
8 |
+
export type VideoObjectDetector = (videoFrame: ImageSource, timestamp: number) => Promise<ObjectDetectorResult>
|
9 |
+
|
10 |
+
const getObjectDetector = async (): Promise<VideoObjectDetector> => {
|
11 |
+
const vision = await FilesetResolver.forVisionTasks(
|
12 |
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
|
13 |
+
);
|
14 |
+
|
15 |
+
const objectDetector = await ObjectDetector.createFromOptions(vision, {
|
16 |
+
baseOptions: {
|
17 |
+
modelAssetPath: `https://storage.googleapis.com/mediapipe-tasks/object_detector/efficientdet_lite0_uint8.tflite`
|
18 |
+
},
|
19 |
+
scoreThreshold: 0.5,
|
20 |
+
runningMode: "VIDEO"
|
21 |
+
});
|
22 |
+
|
23 |
+
const detector: VideoObjectDetector = async (videoFrame: ImageSource, timestamp: number): Promise<ObjectDetectorResult> => {
|
24 |
+
const result = objectDetector.detectForVideo(videoFrame, timestamp)
|
25 |
+
return result
|
26 |
+
}
|
27 |
+
|
28 |
+
return detector
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
const globalState: { detector?: VideoObjectDetector } = {};
|
33 |
+
|
34 |
+
(async () => {
|
35 |
+
globalState.detector = globalState.detector || (await getObjectDetector())
|
36 |
+
})();
|
37 |
+
|
38 |
+
export async function identifyFrame(frame: ImageSource, timestamp: number): Promise<ObjectDetectorResult> {
|
39 |
+
console.log("identifyFrame: loading segmenter..")
|
40 |
+
globalState.detector = globalState.detector || (await getObjectDetector())
|
41 |
+
|
42 |
+
console.log("identifyFrame: segmenting..")
|
43 |
+
return globalState.detector(frame, timestamp)
|
44 |
+
}
|
45 |
+
|
46 |
+
// to run:
|
47 |
+
|
48 |
+
// see doc:
|
49 |
+
// https://developers.google.com/mediapipe/solutions/vision/image_segmenter/web_js#video
|
50 |
+
// imageSegmenter.segmentForVideo(video, startTimeMs, callbackForVideo);
|
51 |
+
|
52 |
+
|
src/lib/on-device-ai/segmentFrameOnClick.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { FilesetResolver, InteractiveSegmenter, InteractiveSegmenterResult, ImageSource } from "@mediapipe/tasks-vision"
|
2 |
+
|
3 |
+
export type InteractiveVideoSegmenter = (videoFrame: ImageSource, x: number, y: number) => Promise<InteractiveSegmenterResult>
|
4 |
+
|
5 |
+
const getInteractiveSegmenter = async (): Promise<InteractiveVideoSegmenter> => {
|
6 |
+
const vision = await FilesetResolver.forVisionTasks(
|
7 |
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
|
8 |
+
);
|
9 |
+
|
10 |
+
const interactiveSegmenter = await InteractiveSegmenter.createFromOptions(vision, {
|
11 |
+
baseOptions: {
|
12 |
+
modelAssetPath:
|
13 |
+
"https://storage.googleapis.com/mediapipe-tasks/interactive_segmenter/ptm_512_hdt_ptm_woid.tflite"
|
14 |
+
},
|
15 |
+
outputCategoryMask: true,
|
16 |
+
outputConfidenceMasks: false,
|
17 |
+
});
|
18 |
+
|
19 |
+
const segmenter: InteractiveVideoSegmenter = (
|
20 |
+
videoFrame: ImageSource,
|
21 |
+
x: number,
|
22 |
+
y: number
|
23 |
+
): Promise<InteractiveSegmenterResult> => {
|
24 |
+
return new Promise((resolve, reject) => {
|
25 |
+
interactiveSegmenter.segment(
|
26 |
+
videoFrame,
|
27 |
+
{
|
28 |
+
keypoint: { x, y }
|
29 |
+
},
|
30 |
+
(results) => {
|
31 |
+
resolve(results)
|
32 |
+
})
|
33 |
+
})
|
34 |
+
}
|
35 |
+
|
36 |
+
return segmenter
|
37 |
+
}
|
38 |
+
|
39 |
+
|
40 |
+
const globalState: { segmenter?: InteractiveVideoSegmenter } = {};
|
41 |
+
|
42 |
+
(async () => {
|
43 |
+
globalState.segmenter = globalState.segmenter || (await getInteractiveSegmenter())
|
44 |
+
})();
|
45 |
+
|
46 |
+
export async function segmentFrame(frame: ImageSource, x: number, y: number): Promise<InteractiveSegmenterResult> {
|
47 |
+
console.log("segmentFrame: loading segmenter..")
|
48 |
+
globalState.segmenter = globalState.segmenter || (await getInteractiveSegmenter())
|
49 |
+
|
50 |
+
console.log("segmentFrame: segmenting..")
|
51 |
+
return globalState.segmenter(frame, x, y)
|
52 |
+
}
|
53 |
+
|
54 |
+
// to run:
|
55 |
+
|
56 |
+
// see doc:
|
57 |
+
// https://developers.google.com/mediapipe/solutions/vision/image_segmenter/web_js#video
|
58 |
+
// imageSegmenter.segmentForVideo(video, startTimeMs, callbackForVideo);
|
src/lib/utils/relativeCoords.ts
ADDED
File without changes
|
src/types/general.ts
CHANGED
@@ -637,9 +637,11 @@ export type InterfaceView =
|
|
637 |
| "user_account"
|
638 |
| "public_channels"
|
639 |
| "public_channel" // public view of a channel
|
640 |
-
| "public_media" // public view of
|
641 |
| "public_media_embed" // for integration into twitter etc
|
642 |
| "public_music_videos" // public music videos - it's a special category, because music is *cool*
|
|
|
|
|
643 |
| "public_gaming" // for AiTube Gaming
|
644 |
| "public_4d" // for AiTube 4D
|
645 |
| "public_live" // for AiTube Live
|
|
|
637 |
| "user_account"
|
638 |
| "public_channels"
|
639 |
| "public_channel" // public view of a channel
|
640 |
+
| "public_media" // public view of an individual media (video, gaussian splat, clap video)
|
641 |
| "public_media_embed" // for integration into twitter etc
|
642 |
| "public_music_videos" // public music videos - it's a special category, because music is *cool*
|
643 |
+
| "public_latent_media" // public view of an individual dream (a latent media, so it's not a "real" file)
|
644 |
+
| "public_latent_media_embed" // for integration into twitter etc (which would be hardcore for our server load.. so maybe not)
|
645 |
| "public_gaming" // for AiTube Gaming
|
646 |
| "public_4d" // for AiTube 4D
|
647 |
| "public_live" // for AiTube Live
|