jbilcke-hf HF staff commited on
Commit
09a7c47
1 Parent(s): 3d4392e
Files changed (32) hide show
  1. src/app/api/generators/search/defaultChannel.ts +70 -0
  2. src/app/api/generators/search/getNewMediaInfo.ts +146 -0
  3. src/app/api/utils/blobToDataUri.ts +21 -0
  4. src/app/api/utils/dataUriToBlob.ts +20 -0
  5. src/app/dream/embed/page.tsx +12 -0
  6. src/app/dream/page.tsx +15 -3
  7. src/app/main.tsx +42 -0
  8. src/app/state/useStore.ts +27 -1
  9. src/app/views/public-latent-media-embed-view/index.tsx +21 -0
  10. src/app/views/public-latent-media-view/index.tsx +77 -0
  11. src/app/views/public-media-view/index.tsx +10 -5
  12. src/components/interface/latent-engine/components/content-layer/index.tsx +8 -3
  13. src/components/interface/latent-engine/components/disclaimers/this-is-ai.tsx +2 -2
  14. src/components/interface/latent-engine/core/drawSegmentation.ts +36 -0
  15. src/components/interface/latent-engine/core/engine.tsx +69 -19
  16. src/components/interface/latent-engine/core/types.ts +10 -3
  17. src/components/interface/latent-engine/resolvers/image/index.tsx +4 -1
  18. src/components/interface/latent-engine/store/useLatentEngine.ts +86 -5
  19. src/components/interface/media-player/index.tsx +5 -4
  20. src/components/interface/media-player/latent.tsx +1 -1
  21. src/components/interface/stream-tag/index.tsx +6 -0
  22. src/lib/clap/clapToDataUri.ts +9 -0
  23. src/lib/clap/{mockClap.ts → getMockClap.ts} +18 -6
  24. src/lib/clap/newClap.ts +1 -0
  25. src/lib/clap/parseClap.ts +134 -21
  26. src/lib/clap/serializeClap.ts +9 -4
  27. src/lib/on-device-ai/getInteractiveSegmentationCanvas.tsx +37 -0
  28. src/lib/on-device-ai/getSegmentationCanvas.tsx +1 -0
  29. src/lib/on-device-ai/identifyFrame.ts +52 -0
  30. src/lib/on-device-ai/segmentFrameOnClick.ts +58 -0
  31. src/lib/utils/relativeCoords.ts +0 -0
  32. 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 publicMedia={latentMedia} />
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: MediaInfo[]) => void
 
 
 
 
 
 
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
- setPublicMedia({
122
- ...media,
123
- numberOfViews
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 { ForwardedRef, forwardRef, ReactNode } from "react"
 
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 { StreamType } from "@/types/general"
8
 
9
  export function ThisIsAI({
10
  streamType,
11
  }: {
12
- streamType?: 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
- url,
15
  width,
16
  height,
17
  className = "" }: {
18
- url: string
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 openLatentClapFile = useLatentEngine(s => s.openLatentClapFile)
26
- const openClapFile = useLatentEngine(s => s.openClapFile)
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
- console.log("let's load an experience")
55
- // openClapFile(mockClap({ showDisclaimer: true }))
56
- openLatentClapFile("short story about a podracer race")
 
 
 
 
 
 
 
 
 
 
 
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
- const videoElements = Array.from(videoLayerRef.current.querySelectorAll('.latent-video')) as HTMLVideoElement[]
 
 
 
 
 
92
  setVideoElement(videoElements.at(0))
93
 
94
  // images are used for simpler or static experiences
95
- const imageElements = Array.from(videoLayerRef.current.querySelectorAll('.latent-image')) as HTMLImageElement[]
 
 
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
- >{segmentationLayer}</ContentLayer>
 
 
 
 
129
 
 
130
  <ContentLayer
131
- className=""
132
  width={width}
133
  height={height}
134
- >{interfaceLayer}</ContentLayer>
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 { ReactNode } from "react"
 
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
- openLatentClapFile: (prompt: string) => Promise<void>
79
- openClapFile: (clap: ClapProject) => void
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 className="latent-image object-cover" src={assetUrl} />
 
 
 
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
- openLatentClapFile: async (prompt: string): Promise<void> => {
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().openClapFile(clap)
85
  },
86
 
87
- openClapFile: (clap: ClapProject) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // console.log("MediaPlayer called for \"" + media?.label + "\"")
26
 
27
- if (!media || !media?.assetUrl) { return null }
 
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
- url={media.clapUrl}
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
- export function mockClap({
8
- showDisclaimer
 
 
 
 
 
9
  }: {
10
- showDisclaimer: boolean
 
 
 
 
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
- // prompt: "closeup of Queen angelfish, bokeh",
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
- * import a Clap file (from a plain text string)
 
 
 
 
 
 
 
9
  *
10
  * note: it is not really async, because for some reason YAML.parse is a blocking call like for JSON,
11
- * they is no async version although we are now in the 20s not 90s
12
  */
13
- export async function parseClap(inputStringOrBlob: string | Blob): Promise<ClapProject> {
14
 
15
- // Decompress the input blob using gzip
16
- const decompressor = new DecompressionStream('gzip');
 
 
 
 
 
 
 
 
 
17
 
18
- const inputBlob =
19
- typeof inputStringOrBlob === "string"
20
- ? new Blob([inputStringOrBlob], { type: "application/x-yaml" })
21
- : inputStringOrBlob;
22
 
23
- const decompressedStream = inputBlob.stream().pipeThrough(decompressor);
 
 
24
 
25
- // Convert the stream to text using a Response object
26
- const text = await new Response(decompressedStream).text();
27
-
28
- // Parse YAML string to raw data
29
- const rawData = YAML.parse(text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- if (!Array.isArray(rawData) || rawData.length < 2) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  throw new Error("invalid clap file (need a clap format header block and project metadata block)")
33
  }
34
 
35
- const maybeClapHeader = rawData[0] as ClapHeader
 
 
 
 
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 = rawData[1] as ClapMeta
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 = rawData.slice(afterTheHeaders, afterTheModels) as ClapModel[]
86
 
87
  // note: if there are no expected scenes, maybeScenes will be empty
88
- const maybeScenes = rawData.slice(afterTheModels, afterTheScenes) as ClapScene[]
89
 
90
- const maybeSegments = rawData.slice(afterTheScenes) as ClapSegment[]
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 compressedBlob = await new Response(compressedStream).blob();
 
 
 
 
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 a 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_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