jbilcke-hf HF staff commited on
Commit
48c7837
1 Parent(s): 3420ebd

working on the portal

Browse files
src/app/main.tsx CHANGED
@@ -11,9 +11,8 @@ import { Toaster } from "@/components/ui/sonner"
11
  import { TextareaField } from "@/components/form/textarea-field"
12
 
13
  import { cn, generateRandomStory } from "@/lib/utils"
14
- import { defaultPrompt } from "./config"
15
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
16
- import { useIsBusy, useOrientation, useProgressTimer, useQueryStringParams, useStoryPromptDraft } from "@/lib/hooks"
17
  import { BottomBar, VideoPreview } from "@/components/interface"
18
  import { MainTitle } from "@/components/interface/main-title"
19
  import { LoadClapButton } from "@/components/interface/load-clap-button"
@@ -21,9 +20,11 @@ import { SaveClapButton } from "@/components/interface/save-clap-button"
21
  import { useProcessors } from "@/lib/hooks/useProcessors"
22
  import { Characters } from "@/components/interface/characters"
23
  import { useOAuth } from "@/lib/oauth/useOAuth"
24
- import { useStore } from "./store"
25
  import { AuthWall } from "@/components/interface/auth-wall"
26
 
 
 
 
27
  export function Main() {
28
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
29
  const { isBusy } = useIsBusy()
@@ -32,7 +33,7 @@ export function Main() {
32
  useQueryStringParams()
33
 
34
  const showAuthWall = useStore(s => s.showAuthWall)
35
- const { isLoggedIn, enableOAuthWall } = useOAuth({ debug: true })
36
 
37
  return (
38
  <TooltipProvider>
 
11
  import { TextareaField } from "@/components/form/textarea-field"
12
 
13
  import { cn, generateRandomStory } from "@/lib/utils"
 
14
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
15
+ import { useIsBusy, useOrientation, useQueryStringParams, useStoryPromptDraft } from "@/lib/hooks"
16
  import { BottomBar, VideoPreview } from "@/components/interface"
17
  import { MainTitle } from "@/components/interface/main-title"
18
  import { LoadClapButton } from "@/components/interface/load-clap-button"
 
20
  import { useProcessors } from "@/lib/hooks/useProcessors"
21
  import { Characters } from "@/components/interface/characters"
22
  import { useOAuth } from "@/lib/oauth/useOAuth"
 
23
  import { AuthWall } from "@/components/interface/auth-wall"
24
 
25
+ import { defaultPrompt } from "./config"
26
+ import { useStore } from "./store"
27
+
28
  export function Main() {
29
  const { storyPromptDraft, setStoryPromptDraft, promptDraftRef } = useStoryPromptDraft()
30
  const { isBusy } = useIsBusy()
 
33
  useQueryStringParams()
34
 
35
  const showAuthWall = useStore(s => s.showAuthWall)
36
+ const { isLoggedIn, enableOAuthWall } = useOAuth()
37
 
38
  return (
39
  <TooltipProvider>
src/app/portal/page.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import Head from "next/head"
5
+ import Script from "next/script"
6
+
7
+ import { cn } from "@/lib/utils/cn"
8
+
9
+ import { Portal } from "./portal"
10
+
11
+ // https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
12
+
13
+ export default function PortalPage() {
14
+ const [isLoaded, setLoaded] = useState(false)
15
+ useEffect(() => { setLoaded(true) }, [])
16
+ return (
17
+ <>
18
+ <Head>
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
20
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
22
+ </Head>
23
+ <Script id="gtm">{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
24
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
25
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
26
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
27
+ })(window,document,'script','dataLayer','GTM-K98T8ZFZ');`}</Script>
28
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-K98T8ZFZ"
29
+ height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
30
+ <main className={cn(
31
+ `dark flex inset-0 w-screen h-screen items-center justify-center`,
32
+ )}>
33
+ {isLoaded && <Portal />}
34
+ </main>
35
+ </>
36
+ )
37
+ }
src/app/portal/portal.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React from "react"
4
+
5
+ import { useQueryStringParams } from "@/lib/hooks"
6
+ import { VideoPreview } from "@/components/interface"
7
+
8
+ // we are going to need to do the same thing on AiTube's side,
9
+ // which is a waste of time of energy.. possibles alternatives are
10
+ // to put all the codebase in a shared NPM package, or just move everything
11
+ // to AiTube, then display an iframe widget similar to what Hugging
12
+ // Face does for Spaces, and call it a day.
13
+ //
14
+ // we could also make it in a way where only the rendering is done
15
+ // on AiTube's side, as if it was a YouTube player or something!
16
+ // but the problem is that we would need to POST the .clap, or host it
17
+ // (it won't fit in the iframe query params)
18
+
19
+ export function Portal() {
20
+ useQueryStringParams()
21
+ return (
22
+ <VideoPreview embed />
23
+ );
24
+ }
src/app/store.ts CHANGED
@@ -21,7 +21,7 @@ export const useStore = create<{
21
 
22
  status: GlobalStatus
23
  stage: GenerationStage
24
-
25
  parseGenerationStatus: TaskStatus
26
  storyGenerationStatus: TaskStatus
27
  assetGenerationStatus: TaskStatus
@@ -81,6 +81,7 @@ export const useStore = create<{
81
  orientation: ClapMediaOrientation.PORTRAIT,
82
  status: "idle",
83
  stage: "idle",
 
84
  parseGenerationStatus: "idle",
85
  storyGenerationStatus: "idle",
86
  assetGenerationStatus: "idle",
@@ -173,12 +174,13 @@ export const useStore = create<{
173
  get().syncStatusAndStageState()
174
  },
175
  syncStatusAndStageState: () => {
176
- const { status, storyGenerationStatus, assetGenerationStatus, soundGenerationStatus, musicGenerationStatus, voiceGenerationStatus, imageGenerationStatus, videoGenerationStatus, finalGenerationStatus } = get()
177
 
178
  // note: we don't really have "stages" since some things run in parallel,
179
  // and some parallel tasks may finish before the others
180
  // still, we need to estimate how long things should take, so it has some usefulness
181
  let stage: GenerationStage =
 
182
  storyGenerationStatus === "generating" ? "story" :
183
  assetGenerationStatus === "generating" ? "entities" :
184
  musicGenerationStatus === "generating" ? "music" :
@@ -194,7 +196,25 @@ export const useStore = create<{
194
  // that is because we can have parallelism
195
  const isBusy = stage !== "idle" || status === "generating"
196
 
197
- set({ isBusy, stage })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  },
199
  setCurrentClap: (currentClap?: ClapProject) => { set({ currentClap }) },
200
  setCurrentVideo: async (currentVideo: string): Promise<void> => {
 
21
 
22
  status: GlobalStatus
23
  stage: GenerationStage
24
+ statusMessage: string
25
  parseGenerationStatus: TaskStatus
26
  storyGenerationStatus: TaskStatus
27
  assetGenerationStatus: TaskStatus
 
81
  orientation: ClapMediaOrientation.PORTRAIT,
82
  status: "idle",
83
  stage: "idle",
84
+ statusMessage: "",
85
  parseGenerationStatus: "idle",
86
  storyGenerationStatus: "idle",
87
  assetGenerationStatus: "idle",
 
174
  get().syncStatusAndStageState()
175
  },
176
  syncStatusAndStageState: () => {
177
+ const { status, parseGenerationStatus, storyGenerationStatus, assetGenerationStatus, soundGenerationStatus, musicGenerationStatus, voiceGenerationStatus, imageGenerationStatus, videoGenerationStatus, finalGenerationStatus } = get()
178
 
179
  // note: we don't really have "stages" since some things run in parallel,
180
  // and some parallel tasks may finish before the others
181
  // still, we need to estimate how long things should take, so it has some usefulness
182
  let stage: GenerationStage =
183
+ parseGenerationStatus === "generating" ? "parse" :
184
  storyGenerationStatus === "generating" ? "story" :
185
  assetGenerationStatus === "generating" ? "entities" :
186
  musicGenerationStatus === "generating" ? "music" :
 
196
  // that is because we can have parallelism
197
  const isBusy = stage !== "idle" || status === "generating"
198
 
199
+
200
+ const statusMessage = isBusy ? (
201
+ // note: some of those tasks are running in parallel,
202
+ // and some are super-slow (like music or video)
203
+ // by carefully selecting in which order we set the ternaries,
204
+ // we can create the illusion that we just have a succession of reasonably-sized tasks
205
+ storyGenerationStatus === "generating" ? "Writing story.."
206
+ : parseGenerationStatus === "generating" ? "Loading the project.."
207
+ : assetGenerationStatus === "generating" ? "Casting characters.."
208
+ : imageGenerationStatus === "generating" ? "Creating storyboards.."
209
+ : soundGenerationStatus === "generating" ? "Recording sounds.."
210
+ : videoGenerationStatus === "generating" ? "Filming shots.."
211
+ : musicGenerationStatus === "generating" ? "Producing music.."
212
+ : voiceGenerationStatus === "generating" ? "Recording dialogues.."
213
+ : finalGenerationStatus === "generating" ? "Editing final cut.."
214
+ : "Please wait.."
215
+ ) : ""
216
+
217
+ set({ isBusy, stage, statusMessage })
218
  },
219
  setCurrentClap: (currentClap?: ClapProject) => { set({ currentClap }) },
220
  setCurrentVideo: async (currentVideo: string): Promise<void> => {
src/components/interface/device-frame.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+ import Image from "next/image"
3
+
4
+ import { DeviceFrameset } from "react-device-frameset"
5
+ import "react-device-frameset/styles/marvel-devices.min.css"
6
+
7
+ import { useOrientation } from "@/lib/hooks"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ import HFLogo from "../../app/hf-logo.svg"
11
+
12
+ export function DeviceFrame({ children, companion, showFrame = false }: {
13
+ children?: ReactNode
14
+ companion?: ReactNode
15
+ showFrame?: boolean
16
+ }) {
17
+
18
+ const { isLandscape, isPortrait } = useOrientation()
19
+
20
+ if (!showFrame) {
21
+ return children
22
+ }
23
+
24
+ return (
25
+ <div className={cn(`
26
+ -mt-8 md:mt-0
27
+ transition-all duration-200 ease-in-out
28
+ `,
29
+ isLandscape
30
+ ? `scale-[0.9] md:scale-[0.75] lg:scale-[0.9] xl:scale-[1.0] 2xl:scale-[1.1]`
31
+ : `scale-[0.8] md:scale-[0.9] lg:scale-[1.1]`
32
+ )}>
33
+ <DeviceFrameset
34
+ device="Nexus 5"
35
+ // color="black"
36
+
37
+ landscape={isLandscape}
38
+
39
+ // note 1: videos are generated in 1024x576 or 576x1024
40
+ // so we need to keep the same ratio here
41
+
42
+ // note 2: width and height are fixed, if width always stays 512
43
+ // that's because the landscape={} parameter will do the switch for us
44
+
45
+ width={288}
46
+ height={512}
47
+ >
48
+ <div className="
49
+ flex flex-col items-center justify-center
50
+ w-full h-full
51
+ bg-black text-white
52
+ ">
53
+ {children}
54
+ </div>
55
+
56
+ <div className={cn(`
57
+ fixed
58
+ flex flex-row items-center justify-center
59
+ bg-transparent
60
+ font-sans
61
+ -mb-0
62
+ `,
63
+ isLandscape ? 'h-4' : 'h-14'
64
+ )}
65
+ style={{ width: isPortrait ? 288 : 512 }}>
66
+ <span className="text-stone-100/50 text-4xs"
67
+ style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>
68
+ Powered by
69
+ </span>
70
+ <span className="ml-1 mr-0.5">
71
+ <Image src={HFLogo} alt="Hugging Face" width={14} height={13} />
72
+ </span>
73
+ <span className="text-stone-100/80 text-3xs font-semibold"
74
+ style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>Hugging Face</span>
75
+
76
+ </div>
77
+ </DeviceFrameset>
78
+
79
+ {companion}
80
+ </div>
81
+ )
82
+ }
src/components/interface/download-video.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+ import { FaCloudDownloadAlt } from "react-icons/fa"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export function DownloadVideo({
7
+ video = "",
8
+ disabled = false,
9
+ onClick,
10
+ children = <>Download</>
11
+ }: {
12
+ video?: string
13
+ disabled?: boolean
14
+ onClick: () => void
15
+ children?: ReactNode
16
+ }) {
17
+
18
+ return (
19
+ <>{
20
+ (video && video.length > 128)
21
+ ? <div
22
+ className={cn(`
23
+ w-full
24
+ flex flex-row
25
+ items-center justify-center
26
+ transition-all duration-150 ease-in-out
27
+
28
+ text-stone-800
29
+
30
+ group
31
+ pt-2 md:pt-4
32
+ `,
33
+ disabled ? 'opacity-50' : 'cursor-pointer opacity-100 hover:scale-110 active:scale-150 hover:text-stone-950 active:text-black'
34
+ )}
35
+ style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}
36
+ onClick={disabled ? undefined : onClick}
37
+ >
38
+ <div className="
39
+ text-base md:text-lg lg:text-xl
40
+ transition-all duration-150 ease-out
41
+ group-hover:animate-swing
42
+ "><FaCloudDownloadAlt /></div>
43
+ <div className="text-xs md:text-sm lg:text-base">&nbsp;{children}</div>
44
+ </div> : null}</>
45
+ )
46
+ }
src/components/interface/video-preview.tsx CHANGED
@@ -1,190 +1,56 @@
1
  "use client"
2
 
3
  import React from "react"
4
- import { FaCloudDownloadAlt } from "react-icons/fa"
5
- import Image from "next/image"
6
- import { DeviceFrameset } from "react-device-frameset"
7
- import "react-device-frameset/styles/marvel-devices.min.css"
8
 
9
- import { useOrientation } from "@/lib/hooks/useOrientation"
10
  import { useProgressTimer } from "@/lib/hooks/useProgressTimer"
11
- import { cn } from "@/lib/utils/cn"
12
 
13
  import { useStore } from "../../app/store"
14
- import HFLogo from "../../app/hf-logo.svg"
15
  import { Login } from "./login"
16
- import { useOAuth } from "@/lib/oauth/useOAuth"
 
 
17
 
18
- export function VideoPreview() {
 
 
 
 
19
 
20
- const { isLoggedIn, enableOAuthWall } = useOAuth()
21
-
22
- const status = useStore(s => s.status)
23
- const parseGenerationStatus = useStore(s => s.parseGenerationStatus)
24
- const storyGenerationStatus = useStore(s => s.storyGenerationStatus)
25
- const assetGenerationStatus = useStore(s => s.assetGenerationStatus)
26
- const soundGenerationStatus = useStore(s => s.soundGenerationStatus)
27
- const musicGenerationStatus = useStore(s => s.musicGenerationStatus)
28
- const voiceGenerationStatus = useStore(s => s.voiceGenerationStatus)
29
- const imageGenerationStatus = useStore(s => s.imageGenerationStatus)
30
- const videoGenerationStatus = useStore(s => s.videoGenerationStatus)
31
- const finalGenerationStatus = useStore(s => s.finalGenerationStatus)
32
  const currentVideo = useStore(s => s.currentVideo)
33
-
34
  const error = useStore(s => s.error)
35
-
36
  const saveVideo = useStore(s => s.saveVideo)
37
-
38
-
39
- const { isBusy, progress } = useProgressTimer()
40
-
41
- const {
42
- isLandscape,
43
- isPortrait,
44
- } = useOrientation()
45
-
46
- const placeholder = <div
47
- className="
48
- text-base
49
- text-center
50
- text-stone-50/90 dark:text-stone-50/90
51
- "
52
- >{
53
- error ? <span>{error}</span> :
54
- <span>No video yet</span>
55
- }</div>
56
 
57
  return (
58
- <div className={cn(`
59
- -mt-8 md:mt-0
60
- transition-all duration-200 ease-in-out
61
- `,
62
- isLandscape
63
- ? `scale-[0.9] md:scale-[0.75] lg:scale-[0.9] xl:scale-[1.0] 2xl:scale-[1.1]`
64
- : `scale-[0.8] md:scale-[0.9] lg:scale-[1.1]`
65
- )}>
66
- <DeviceFrameset
67
- device="Nexus 5"
68
- // color="black"
69
-
70
- landscape={isLandscape}
71
-
72
- // note 1: videos are generated in 1024x576 or 576x1024
73
- // so we need to keep the same ratio here
74
-
75
- // note 2: width and height are fixed, if width always stays 512
76
- // that's because the landscape={} parameter will do the switch for us
77
-
78
- width={288}
79
- height={512}
80
- >
81
- <div className="
82
- flex flex-col items-center justify-center
83
- w-full h-full
84
- bg-black text-white
85
- ">
86
- {
87
- !isLoggedIn ? <div className="
88
- flex flex-col items-center justify-center
89
- space-y-2
90
- ">
91
- <div className="
92
- text-base
93
- text-center
94
- text-stone-50/90 dark:text-stone-50/90
95
- ">Please login to generate videos:</div>
96
- <Login />
97
- </div>
98
- : isBusy ? <div className="
99
- flex flex-col
100
- items-center justify-center
101
- text-center space-y-1.5">
102
- <p className="text-2xl font-bold">{progress}%</p>
103
- <p className="text-base text-white/70">{
104
- isBusy
105
- ? (
106
- // note: some of those tasks are running in parallel,
107
- // and some are super-slow (like music or video)
108
- // by carefully selecting in which order we set the ternaries,
109
- // we can create the illusion that we just have a succession of reasonably-sized tasks
110
- storyGenerationStatus === "generating" ? "Writing story.."
111
- : parseGenerationStatus === "generating" ? "Loading the project.."
112
- : assetGenerationStatus === "generating" ? "Casting characters.."
113
- : imageGenerationStatus === "generating" ? "Creating storyboards.."
114
- : soundGenerationStatus === "generating" ? "Recording sounds.."
115
- : videoGenerationStatus === "generating" ? "Filming shots.."
116
- : musicGenerationStatus === "generating" ? "Producing music.."
117
- : voiceGenerationStatus === "generating" ? "Recording dialogues.."
118
- : finalGenerationStatus === "generating" ? "Editing final cut.."
119
- : "Please wait.."
120
- )
121
- : status === "error"
122
- ? <span>{error || ""}</span>
123
- : placeholder // to prevent layout changes
124
- }</p>
125
- </div>
126
- : (currentVideo && currentVideo?.length > 128) ? <video
127
- src={currentVideo}
128
- controls
129
- playsInline
130
- // I think we can't autoplay with sound,
131
- // so let's disable auto-play
132
- // autoPlay
133
- // muted
134
- loop
135
- className="object-cover"
136
- style={{
137
- }}
138
- /> : placeholder}
139
- </div>
140
-
141
- <div className={cn(`
142
- fixed
143
- flex flex-row items-center justify-center
144
- bg-transparent
145
- font-sans
146
- -mb-0
147
- `,
148
- isLandscape ? 'h-4' : 'h-14'
149
- )}
150
- style={{ width: isPortrait ? 288 : 512 }}>
151
- <span className="text-stone-100/50 text-4xs"
152
- style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>
153
- Powered by
154
- </span>
155
- <span className="ml-1 mr-0.5">
156
- <Image src={HFLogo} alt="Hugging Face" width={14} height={13} />
157
- </span>
158
- <span className="text-stone-100/80 text-3xs font-semibold"
159
- style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>Hugging Face</span>
160
-
161
  </div>
162
- </DeviceFrameset>
163
-
164
- {(currentVideo && currentVideo.length > 128) ? <div
165
- className={cn(`
166
- w-full
167
- flex flex-row
168
- items-center justify-center
169
- transition-all duration-150 ease-in-out
170
-
171
- text-stone-800
172
-
173
- group
174
- pt-2 md:pt-4
175
- `,
176
- isBusy ? 'opacity-50' : 'cursor-pointer opacity-100 hover:scale-110 active:scale-150 hover:text-stone-950 active:text-black'
177
- )}
178
- style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}
179
- onClick={isBusy ? undefined : saveVideo}
180
- >
181
- <div className="
182
- text-base md:text-lg lg:text-xl
183
- transition-all duration-150 ease-out
184
- group-hover:animate-swing
185
- "><FaCloudDownloadAlt /></div>
186
- <div className="text-xs md:text-sm lg:text-base">&nbsp;Download</div>
187
- </div> : null}
188
- </div>
189
  )
190
  }
 
1
  "use client"
2
 
3
  import React from "react"
 
 
 
 
4
 
 
5
  import { useProgressTimer } from "@/lib/hooks/useProgressTimer"
6
+ import { useOAuth } from "@/lib/oauth/useOAuth"
7
 
8
  import { useStore } from "../../app/store"
 
9
  import { Login } from "./login"
10
+ import { Video } from "./video"
11
+ import { DownloadVideo } from "./download-video"
12
+ import { DeviceFrame } from "./device-frame"
13
 
14
+ export function VideoPreview({
15
+ embed
16
+ }: {
17
+ embed?: boolean
18
+ }) {
19
 
20
+ const { isLoggedIn } = useOAuth()
 
 
 
 
 
 
 
 
 
 
 
21
  const currentVideo = useStore(s => s.currentVideo)
22
+ const statusMessage = useStore(s => s.statusMessage)
23
  const error = useStore(s => s.error)
 
24
  const saveVideo = useStore(s => s.saveVideo)
25
+ const { isBusy, progress } = useProgressTimer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  return (
28
+ <DeviceFrame
29
+ companion={
30
+ <DownloadVideo
31
+ disabled={isBusy}
32
+ video={currentVideo}
33
+ onClick={saveVideo}
34
+ />
35
+ }
36
+ showFrame={!embed}
37
+ >
38
+ <Video
39
+ video={currentVideo}
40
+ isBusy={isBusy}
41
+ progress={progress}
42
+ status={statusMessage}
43
+ error={error}
44
+ >{
45
+ !isLoggedIn
46
+ ? <div className="flex flex-col items-center justify-center space-y-2">
47
+ <div className="text-base text-center text-stone-50/90 dark:text-stone-50/90">
48
+ Please login to generate videos:
49
+ </div>
50
+ <Login />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
+ : null
53
+ }</Video>
54
+ </DeviceFrame>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  )
56
  }
src/components/interface/video.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { ReactNode } from "react"
2
+
3
+ export function Video({
4
+ video = "",
5
+ isBusy = false,
6
+ progress = 0,
7
+ status = "",
8
+ error = "",
9
+ children = undefined,
10
+ }: {
11
+ video: string
12
+ isBusy: boolean
13
+ progress: number
14
+ status: string
15
+ error: ReactNode
16
+ children?: ReactNode
17
+ } = {
18
+ video: "",
19
+ isBusy: false,
20
+ progress: 0,
21
+ status: "",
22
+ error: "",
23
+ children: undefined,
24
+ }) {
25
+ const placeholder = <div
26
+ className="
27
+ text-base
28
+ text-center
29
+ text-stone-50/90 dark:text-stone-50/90
30
+ "
31
+ >{
32
+ error ? <span>{error}</span> :
33
+ <span>No video yet</span>
34
+ }</div>
35
+
36
+ const hasVideoContent = Boolean(video && video?.length > 128)
37
+
38
+ return (
39
+ <>{
40
+ children ? children : isBusy ? <div className="
41
+ flex flex-col
42
+ items-center justify-center
43
+ text-center space-y-1.5">
44
+ <p className="text-2xl font-bold">{progress}%</p>
45
+ <p className="text-base text-white/70">{
46
+ status
47
+ ? status
48
+ : error
49
+ ? <span>{error}</span>
50
+ : placeholder // to prevent layout changes
51
+ }</p>
52
+ </div>
53
+ : hasVideoContent ? <video
54
+ src={video}
55
+ controls
56
+ playsInline
57
+ // I think we can't autoplay with sound,
58
+ // so let's disable auto-play
59
+ // autoPlay
60
+ // muted
61
+ loop
62
+ className="object-cover"
63
+ style={{
64
+ }}
65
+ /> : placeholder
66
+ }</>
67
+ )
68
+ }
src/types.ts CHANGED
@@ -11,6 +11,7 @@ export type GlobalStatus =
11
  | "error"
12
 
13
  export type GenerationStage =
 
14
  | "story"
15
  | "entities"
16
  | "sounds"
 
11
  | "error"
12
 
13
  export type GenerationStage =
14
+ | "parse"
15
  | "story"
16
  | "entities"
17
  | "sounds"