jbilcke-hf HF staff commited on
Commit
93ad82e
1 Parent(s): e17bdf7

add toggle for video orientation

Browse files
package-lock.json CHANGED
@@ -48,6 +48,7 @@
48
  "react": "18.3.1",
49
  "react-device-frameset": "^1.3.4",
50
  "react-dom": "18.3.1",
 
51
  "sharp": "^0.33.2",
52
  "sonner": "^1.4.0",
53
  "tailwind-merge": "^2.2.1",
@@ -55,6 +56,7 @@
55
  "tailwindcss-animate": "^1.0.7",
56
  "ts-node": "^10.9.2",
57
  "typescript": "5.4.5",
 
58
  "usehooks-ts": "^2.14.0",
59
  "uuid": "^9.0.1",
60
  "yaml": "^2.4.1",
@@ -4371,6 +4373,17 @@
4371
  "node": "^10.12.0 || >=12.0.0"
4372
  }
4373
  },
 
 
 
 
 
 
 
 
 
 
 
4374
  "node_modules/file-type": {
4375
  "version": "16.5.4",
4376
  "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
@@ -6327,6 +6340,14 @@
6327
  "react": "^18.3.1"
6328
  }
6329
  },
 
 
 
 
 
 
 
 
6330
  "node_modules/react-is": {
6331
  "version": "16.13.1",
6332
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -7481,6 +7502,20 @@
7481
  }
7482
  }
7483
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7484
  "node_modules/use-sidecar": {
7485
  "version": "1.1.2",
7486
  "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
 
48
  "react": "18.3.1",
49
  "react-device-frameset": "^1.3.4",
50
  "react-dom": "18.3.1",
51
+ "react-icons": "^5.2.0",
52
  "sharp": "^0.33.2",
53
  "sonner": "^1.4.0",
54
  "tailwind-merge": "^2.2.1",
 
56
  "tailwindcss-animate": "^1.0.7",
57
  "ts-node": "^10.9.2",
58
  "typescript": "5.4.5",
59
+ "use-file-picker": "^2.1.2",
60
  "usehooks-ts": "^2.14.0",
61
  "uuid": "^9.0.1",
62
  "yaml": "^2.4.1",
 
4373
  "node": "^10.12.0 || >=12.0.0"
4374
  }
4375
  },
4376
+ "node_modules/file-selector": {
4377
+ "version": "0.2.4",
4378
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
4379
+ "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
4380
+ "dependencies": {
4381
+ "tslib": "^2.0.3"
4382
+ },
4383
+ "engines": {
4384
+ "node": ">= 10"
4385
+ }
4386
+ },
4387
  "node_modules/file-type": {
4388
  "version": "16.5.4",
4389
  "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
 
6340
  "react": "^18.3.1"
6341
  }
6342
  },
6343
+ "node_modules/react-icons": {
6344
+ "version": "5.2.0",
6345
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.0.tgz",
6346
+ "integrity": "sha512-n52Y7Eb4MgQZHsSZOhSXv1zs2668/hBYKfSRIvKh42yExjyhZu0d1IK2CLLZ3BZB1oo13lDfwx2vOh2z9FTV6Q==",
6347
+ "peerDependencies": {
6348
+ "react": "*"
6349
+ }
6350
+ },
6351
  "node_modules/react-is": {
6352
  "version": "16.13.1",
6353
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
 
7502
  }
7503
  }
7504
  },
7505
+ "node_modules/use-file-picker": {
7506
+ "version": "2.1.2",
7507
+ "resolved": "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.2.tgz",
7508
+ "integrity": "sha512-ZEIzRi1wXeIXDWr5i55gRBVER8rTkSGskDUY94bciTTAZJHlBnOTRLL/LDYjgz6d+US3yELHnRvtBhLxFGtB0A==",
7509
+ "dependencies": {
7510
+ "file-selector": "0.2.4"
7511
+ },
7512
+ "engines": {
7513
+ "node": ">=12"
7514
+ },
7515
+ "peerDependencies": {
7516
+ "react": ">=16"
7517
+ }
7518
+ },
7519
  "node_modules/use-sidecar": {
7520
  "version": "1.1.2",
7521
  "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
package.json CHANGED
@@ -49,6 +49,7 @@
49
  "react": "18.3.1",
50
  "react-device-frameset": "^1.3.4",
51
  "react-dom": "18.3.1",
 
52
  "sharp": "^0.33.2",
53
  "sonner": "^1.4.0",
54
  "tailwind-merge": "^2.2.1",
@@ -56,6 +57,7 @@
56
  "tailwindcss-animate": "^1.0.7",
57
  "ts-node": "^10.9.2",
58
  "typescript": "5.4.5",
 
59
  "usehooks-ts": "^2.14.0",
60
  "uuid": "^9.0.1",
61
  "yaml": "^2.4.1",
 
49
  "react": "18.3.1",
50
  "react-device-frameset": "^1.3.4",
51
  "react-dom": "18.3.1",
52
+ "react-icons": "^5.2.0",
53
  "sharp": "^0.33.2",
54
  "sonner": "^1.4.0",
55
  "tailwind-merge": "^2.2.1",
 
57
  "tailwindcss-animate": "^1.0.7",
58
  "ts-node": "^10.9.2",
59
  "typescript": "5.4.5",
60
+ "use-file-picker": "^2.1.2",
61
  "usehooks-ts": "^2.14.0",
62
  "uuid": "^9.0.1",
63
  "yaml": "^2.4.1",
src/app/main.tsx CHANGED
@@ -1,8 +1,10 @@
1
  "use client"
2
 
3
- import React, { useRef, useTransition } from 'react'
 
4
  import { ClapProject } from '@aitube/clap'
5
- import Image from "next/image"
 
6
  import { DeviceFrameset } from 'react-device-frameset'
7
  import 'react-device-frameset/styles/marvel-devices.min.css'
8
 
@@ -19,6 +21,11 @@ import { exportClapToVideo } from './server/aitube/exportClapToVideo'
19
 
20
  import { useStore } from './store'
21
  import HFLogo from "./hf-logo.svg"
 
 
 
 
 
22
 
23
  export function Main() {
24
  const [_isPending, startTransition] = useTransition()
@@ -26,26 +33,31 @@ export function Main() {
26
  const promptDraft = useRef("")
27
  promptDraft.current = storyPromptDraft
28
  const storyPrompt = useStore(s => s.storyPrompt)
 
29
  const status = useStore(s => s.status)
30
  const storyGenerationStatus = useStore(s => s.storyGenerationStatus)
31
  const voiceGenerationStatus = useStore(s => s.voiceGenerationStatus)
32
  const imageGenerationStatus = useStore(s => s.imageGenerationStatus)
33
  const videoGenerationStatus = useStore(s => s.videoGenerationStatus)
34
- const generatedClap = useStore(s => s.generatedClap)
35
- const generatedVideo = useStore(s => s.generatedVideo)
 
36
  const setStoryPromptDraft = useStore(s => s.setStoryPromptDraft)
37
  const setStoryPrompt = useStore(s => s.setStoryPrompt)
38
  const setStatus = useStore(s => s.setStatus)
 
39
  const error = useStore(s => s.error)
40
  const setError = useStore(s => s.setError)
41
  const setStoryGenerationStatus = useStore(s => s.setStoryGenerationStatus)
42
  const setVoiceGenerationStatus = useStore(s => s.setVoiceGenerationStatus)
43
  const setImageGenerationStatus = useStore(s => s.setImageGenerationStatus)
44
  const setVideoGenerationStatus = useStore(s => s.setVideoGenerationStatus)
45
- const setGeneratedClap = useStore(s => s.setGeneratedClap)
46
  const setGeneratedVideo = useStore(s => s.setGeneratedVideo)
47
  const progress = useStore(s => s.progress)
48
  const setProgress = useStore(s => s.setProgress)
 
 
49
 
50
  const hasPendingTasks =
51
  storyGenerationStatus === "generating" ||
@@ -55,6 +67,28 @@ export function Main() {
55
 
56
  const isBusy = status === "generating" || hasPendingTasks
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  const handleSubmit = async () => {
59
 
60
  startTransition(async () => {
@@ -68,14 +102,17 @@ export function Main() {
68
  setStoryGenerationStatus("generating")
69
  setStoryPrompt(promptDraft.current)
70
 
71
- clap = await createClap({ prompt: promptDraft.current })
 
 
 
72
 
73
  if (!clap) { throw new Error(`failed to create the clap`) }
74
 
75
  if (clap.segments.length <= 1) { throw new Error(`failed to generate more than one segments`) }
76
 
77
  console.log(`handleSubmit(): received a clap = `, clap)
78
- setGeneratedClap(clap)
79
  setStoryGenerationStatus("finished")
80
  } catch (err) {
81
  setStoryGenerationStatus("error")
@@ -99,7 +136,7 @@ export function Main() {
99
  if (!clap) { throw new Error(`failed to edit the storyboards`) }
100
 
101
  console.log(`handleSubmit(): received a clap with images = `, clap)
102
- setGeneratedClap(clap)
103
  setImageGenerationStatus("finished")
104
  } catch (err) {
105
  setImageGenerationStatus("error")
@@ -120,7 +157,7 @@ export function Main() {
120
  if (!clap) { throw new Error(`failed to edit the dialogues`) }
121
 
122
  console.log(`handleSubmit(): received a clap with dialogues = `, clap)
123
- setGeneratedClap(clap)
124
  setVoiceGenerationStatus("finished")
125
  } catch (err) {
126
  setVoiceGenerationStatus("error")
@@ -139,6 +176,7 @@ export function Main() {
139
  assetUrl = await exportClapToVideo({ clap })
140
 
141
  console.log(`handleSubmit(): received a video: ${assetUrl.slice(0, 60)}...`)
 
142
  setVideoGenerationStatus("finished")
143
  } catch (err) {
144
  setVideoGenerationStatus("error")
@@ -156,6 +194,12 @@ export function Main() {
156
  })
157
  }
158
 
 
 
 
 
 
 
159
  return (
160
  <div className={cn(
161
  `fixed`,
@@ -269,42 +313,70 @@ export function Main() {
269
  pt-2 md:pt-4
270
  "
271
  style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}
272
- >Make vertical video stories using AI ✨</p>
273
  </div>
274
  </CardHeader>
275
- <CardContent className="flex flex-col">
276
- <div className="
277
- flex flex-col
278
- transition-all duration-200 ease-in-out
279
- space-y-2 md:space-y-4 mt-0
280
- ">
281
- <TextareaField
282
- // label="My story:"
283
- // disabled={modelState != 'ready'}
284
- onChange={(e) => {
285
- setStoryPromptDraft(e.target.value)
286
- promptDraft.current = e.target.value
287
- }}
288
- placeholder="Yesterday I was at my favorite pizza place and.."
289
- inputClassName="
290
  transition-all duration-200 ease-in-out
291
- h-32 md:h-56 lg:h-64
292
- "
293
- disabled={isBusy}
294
- value={storyPromptDraft}
295
- />
296
- </div>
297
- <div className="flex-flex-row space-y-3 pt-4">
298
- <div className="flex flex-row justify-end items-center">
299
  <div className="
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  w-full
301
  flex flex-row
302
- justify-end items-center
303
  space-x-3">
304
- {/*
 
305
  <Button
306
- onClick={handleSubmit}
307
- disabled={!storyPromptDraft || isBusy || !generatedClap}
308
  // variant="ghost"
309
  className={cn(
310
  `text-sm md:text-base lg:text-lg`,
@@ -314,10 +386,57 @@ export function Main() {
314
  storyPromptDraft ? "opacity-100" : "opacity-80"
315
  )}
316
  >
317
- <span className="mr-1">Save</span>
318
  </Button>
319
- */}
320
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  <Button
322
  onClick={handleSubmit}
323
  disabled={!storyPromptDraft || isBusy}
@@ -330,19 +449,15 @@ export function Main() {
330
  storyPromptDraft ? "opacity-100" : "opacity-80"
331
  )}
332
  >
333
- <span className="mr-1.5">Create</span><span className="hidden md:inline">👉</span><span className="inline md:hidden">👇</span>
334
  </Button>
335
  </div>
 
 
336
  </div>
337
- <div className="
338
- text-stone-900/90 dark:text-stone-100/90
339
- text-sm md:text-base lg:text-lg
340
- w-full text-right">
341
-
342
- </div>
343
- </div>
344
- </CardContent>
345
- </Card>
346
  </div>
347
  <div className={cn(
348
  `flex flex-col items-center justify-center`,
@@ -353,16 +468,25 @@ export function Main() {
353
  )}>
354
 
355
  <div className={cn(`
356
- -mt-24 md:mt-0
357
  transition-all duration-200 ease-in-out
358
- scale-[0.7] md:scale-[0.9] lg:scale-[1.2]
359
- `)}>
 
 
 
360
  <DeviceFrameset
361
  device="Nexus 5"
362
  // color="black"
363
 
364
- // note: videos are generated in 576
 
 
365
  // so we need to keep the same ratio here
 
 
 
 
366
  width={288}
367
  height={512}
368
  >
@@ -389,12 +513,14 @@ export function Main() {
389
  : <span>&nbsp;</span> // to prevent layout changes
390
  }</p>
391
  </div>
392
- : generatedVideo ? <video
393
- src={generatedVideo}
394
  controls
395
- autoPlay
396
  playsInline
397
- muted
 
 
 
398
  loop
399
  className="object-cover"
400
  style={{
@@ -404,27 +530,32 @@ export function Main() {
404
  items-center justify-center
405
  text-lg text-center"></div>}
406
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  </DeviceFrameset>
408
  </div>
409
  </div>
410
  </div>
411
-
412
- <div className="
413
- md:absolute md:bottom-0 md:right-0
414
- flex flex-row items-center justify-end
415
- w-full p-6
416
- font-sans">
417
- <span className="text-stone-950/60 text-2xs"
418
- style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}>
419
- Powered by
420
- </span>
421
- <span className="ml-1.5 mr-1">
422
- <Image src={HFLogo} alt="Hugging Face" width="16" height="16" />
423
- </span>
424
- <span className="text-stone-950/60 text-xs font-bold"
425
- style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}>Hugging Face</span>
426
-
427
- </div>
428
  </div>
429
  <Toaster />
430
  </div>
 
1
  "use client"
2
 
3
+ import React, { useEffect, useRef, useTransition } from 'react'
4
+ import { IoMdPhoneLandscape, IoMdPhonePortrait } from 'react-icons/io'
5
  import { ClapProject } from '@aitube/clap'
6
+ import Image from 'next/image'
7
+ import { useFilePicker } from 'use-file-picker'
8
  import { DeviceFrameset } from 'react-device-frameset'
9
  import 'react-device-frameset/styles/marvel-devices.min.css'
10
 
 
21
 
22
  import { useStore } from './store'
23
  import HFLogo from "./hf-logo.svg"
24
+ import { fileToBase64 } from '@/lib/base64/fileToBase64'
25
+ import { Input } from '@/components/ui/input'
26
+ import { Field } from '@/components/form/field'
27
+ import { Label } from '@/components/form/label'
28
+ import { VideoOrientation } from './types'
29
 
30
  export function Main() {
31
  const [_isPending, startTransition] = useTransition()
 
33
  const promptDraft = useRef("")
34
  promptDraft.current = storyPromptDraft
35
  const storyPrompt = useStore(s => s.storyPrompt)
36
+ const orientation = useStore(s => s.orientation)
37
  const status = useStore(s => s.status)
38
  const storyGenerationStatus = useStore(s => s.storyGenerationStatus)
39
  const voiceGenerationStatus = useStore(s => s.voiceGenerationStatus)
40
  const imageGenerationStatus = useStore(s => s.imageGenerationStatus)
41
  const videoGenerationStatus = useStore(s => s.videoGenerationStatus)
42
+ const currentClap = useStore(s => s.currentClap)
43
+ const currentVideo = useStore(s => s.currentVideo)
44
+ const currentVideoOrientation = useStore(s => s.currentVideoOrientation)
45
  const setStoryPromptDraft = useStore(s => s.setStoryPromptDraft)
46
  const setStoryPrompt = useStore(s => s.setStoryPrompt)
47
  const setStatus = useStore(s => s.setStatus)
48
+ const toggleOrientation = useStore(s => s.toggleOrientation)
49
  const error = useStore(s => s.error)
50
  const setError = useStore(s => s.setError)
51
  const setStoryGenerationStatus = useStore(s => s.setStoryGenerationStatus)
52
  const setVoiceGenerationStatus = useStore(s => s.setVoiceGenerationStatus)
53
  const setImageGenerationStatus = useStore(s => s.setImageGenerationStatus)
54
  const setVideoGenerationStatus = useStore(s => s.setVideoGenerationStatus)
55
+ const setCurrentClap = useStore(s => s.setCurrentClap)
56
  const setGeneratedVideo = useStore(s => s.setGeneratedVideo)
57
  const progress = useStore(s => s.progress)
58
  const setProgress = useStore(s => s.setProgress)
59
+ const saveClap = useStore(s => s.saveClap)
60
+ const loadClap = useStore(s => s.loadClap)
61
 
62
  const hasPendingTasks =
63
  storyGenerationStatus === "generating" ||
 
67
 
68
  const isBusy = status === "generating" || hasPendingTasks
69
 
70
+
71
+ const { openFilePicker, filesContent, loading } = useFilePicker({
72
+ accept: '.clap',
73
+ readAs: "ArrayBuffer"
74
+ })
75
+
76
+ const fileData = filesContent[0]
77
+
78
+ useEffect(() => {
79
+ const fn = async () => {
80
+ if (fileData?.name) {
81
+ try {
82
+ const blob = new Blob([fileData.content])
83
+ await loadClap(blob, fileData.name)
84
+ } catch (err) {
85
+ console.error("failed to load the Clap file:", err)
86
+ }
87
+ }
88
+ }
89
+ fn()
90
+ }, [fileData?.name])
91
+
92
  const handleSubmit = async () => {
93
 
94
  startTransition(async () => {
 
102
  setStoryGenerationStatus("generating")
103
  setStoryPrompt(promptDraft.current)
104
 
105
+ clap = await createClap({
106
+ prompt: promptDraft.current,
107
+ orientation: useStore.getState().orientation,
108
+ })
109
 
110
  if (!clap) { throw new Error(`failed to create the clap`) }
111
 
112
  if (clap.segments.length <= 1) { throw new Error(`failed to generate more than one segments`) }
113
 
114
  console.log(`handleSubmit(): received a clap = `, clap)
115
+ setCurrentClap(clap)
116
  setStoryGenerationStatus("finished")
117
  } catch (err) {
118
  setStoryGenerationStatus("error")
 
136
  if (!clap) { throw new Error(`failed to edit the storyboards`) }
137
 
138
  console.log(`handleSubmit(): received a clap with images = `, clap)
139
+ setCurrentClap(clap)
140
  setImageGenerationStatus("finished")
141
  } catch (err) {
142
  setImageGenerationStatus("error")
 
157
  if (!clap) { throw new Error(`failed to edit the dialogues`) }
158
 
159
  console.log(`handleSubmit(): received a clap with dialogues = `, clap)
160
+ setCurrentClap(clap)
161
  setVoiceGenerationStatus("finished")
162
  } catch (err) {
163
  setVoiceGenerationStatus("error")
 
176
  assetUrl = await exportClapToVideo({ clap })
177
 
178
  console.log(`handleSubmit(): received a video: ${assetUrl.slice(0, 60)}...`)
179
+
180
  setVideoGenerationStatus("finished")
181
  } catch (err) {
182
  setVideoGenerationStatus("error")
 
194
  })
195
  }
196
 
197
+ // note: we are interested in the *current* video orientation,
198
+ // not the requested video orientation requested for the next video
199
+ const isLandscape = currentVideoOrientation === VideoOrientation.LANDSCAPE
200
+ const isPortrait = currentVideoOrientation === VideoOrientation.PORTRAIT
201
+ const isSquare = currentVideoOrientation === VideoOrientation.SQUARE
202
+
203
  return (
204
  <div className={cn(
205
  `fixed`,
 
313
  pt-2 md:pt-4
314
  "
315
  style={{ textShadow: "rgb(255 255 255 / 19%) 0px 0px 2px" }}
316
+ >Make video stories using AI ✨</p>
317
  </div>
318
  </CardHeader>
319
+ <CardContent
320
+ className="flex flex-col space-y-3"
321
+ >
322
+
323
+ {/* LEFT MENU BUTTONS + MAIN PROMPT INPUT */}
324
+ <div className="flex flex-row space-x-3 w-full">
325
+
326
+
327
+ {/*
328
+ <div className="
329
+ flex flex-col
330
+
331
+ w-32 bg-yellow-600
 
 
332
  transition-all duration-200 ease-in-out
333
+ space-y-2 md:space-y-4
334
+ ">
335
+ put menu here
336
+ </div>
337
+ */}
338
+
339
+ {/* MAIN PROMPT INPUT */}
 
340
  <div className="
341
+ flex flex-col
342
+ flex-1
343
+ transition-all duration-200 ease-in-out
344
+ space-y-2 md:space-y-4
345
+ ">
346
+ <TextareaField
347
+ // label="My story:"
348
+ // disabled={modelState != 'ready'}
349
+ onChange={(e) => {
350
+ setStoryPromptDraft(e.target.value)
351
+ promptDraft.current = e.target.value
352
+ }}
353
+ placeholder="Yesterday I was at my favorite pizza place and.."
354
+ inputClassName="
355
+ transition-all duration-200 ease-in-out
356
+ h-32 md:h-56 lg:h-64
357
+ "
358
+ disabled={isBusy}
359
+ value={storyPromptDraft}
360
+ />
361
+
362
+ {/* END OF MAIN PROMPT INPUT */}
363
+ </div>
364
+
365
+ {/* END OF LEFT MENU BUTTONS + MAIN PROMPT INPUT */}
366
+ </div>
367
+
368
+ {/* ACTION BAR */}
369
+
370
+ <div className="
371
  w-full
372
  flex flex-row
373
+ justify-between items-center
374
  space-x-3">
375
+
376
+ {/*
377
  <Button
378
+ onClick={() => load()}
379
+ disabled={isBusy}
380
  // variant="ghost"
381
  className={cn(
382
  `text-sm md:text-base lg:text-lg`,
 
386
  storyPromptDraft ? "opacity-100" : "opacity-80"
387
  )}
388
  >
389
+ <span className="mr-1">Load project</span>
390
  </Button>
391
+ */}
392
+
393
+ {/*
394
+ <Button
395
+ onClick={() => saveClap()}
396
+ disabled={!currentClap || isBusy}
397
+ // variant="ghost"
398
+ className={cn(
399
+ `text-sm md:text-base lg:text-lg`,
400
+ `bg-stone-800/90 text-amber-400/100 dark:bg-stone-800/90 dark:text-amber-400/100`,
401
+ `font-bold`,
402
+ `hover:bg-stone-800/100 hover:text-amber-300/100 dark:hover:bg-stone-800/100 dark:hover:text-amber-300/100`,
403
+ storyPromptDraft ? "opacity-100" : "opacity-80"
404
+ )}
405
+ >
406
+ <span className="mr-1">Save preset</span>
407
+ </Button>
408
+ */}
409
+ <div></div>
410
+
411
+
412
+ <div className="
413
+ flex flex-row
414
+ justify-between items-center
415
+ space-x-3
416
+ select-none
417
+ ">
418
+ {/* ORIENTATION SWITCH */}
419
+ <div className="
420
+ flex flex-row
421
+ justify-between items-center
422
+ cursor-pointer
423
+ "
424
+ onClick={() => toggleOrientation()}>
425
+ <div>Orientation:</div>
426
+ <div className="
427
+ w-10 h-10
428
+ flex flex-row items-center justify-center
429
+ "
430
+ >
431
+ <div className={cn(
432
+ `transition-all duration-200 ease-in-out`,
433
+ orientation === VideoOrientation.LANDSCAPE ? `rotate-90` : `rotate-0`
434
+ )}>
435
+ <IoMdPhonePortrait size={24} />
436
+ </div>
437
+ </div>
438
+ </div>
439
+ {/* END OF ORIENTATION SWITCH */}
440
  <Button
441
  onClick={handleSubmit}
442
  disabled={!storyPromptDraft || isBusy}
 
449
  storyPromptDraft ? "opacity-100" : "opacity-80"
450
  )}
451
  >
452
+ <span className="mr-1.5">Create</span><span className="hidden md:inline">👉</span><span className="inline md:hidden">👇</span>
453
  </Button>
454
  </div>
455
+
456
+ {/* END OF ACTION BAR */}
457
  </div>
458
+
459
+ </CardContent>
460
+ </Card>
 
 
 
 
 
 
461
  </div>
462
  <div className={cn(
463
  `flex flex-col items-center justify-center`,
 
468
  )}>
469
 
470
  <div className={cn(`
471
+ -mt-8 md:mt-0
472
  transition-all duration-200 ease-in-out
473
+ `,
474
+ isLandscape
475
+ ? `scale-[0.9] md:scale-[0.75] lg:scale-[0.9] xl:scale-[1.0] 2xl:scale-[1.1]`
476
+ : `scale-[0.8] md:scale-[0.9] lg:scale-[1.1]`
477
+ )}>
478
  <DeviceFrameset
479
  device="Nexus 5"
480
  // color="black"
481
 
482
+ landscape={isLandscape}
483
+
484
+ // note 1: videos are generated in 1024x576 or 576x1024
485
  // so we need to keep the same ratio here
486
+
487
+ // note 2: width and height are fixed, if width always stays 512
488
+ // that's because the landscape={} parameter will do the switch for us
489
+
490
  width={288}
491
  height={512}
492
  >
 
513
  : <span>&nbsp;</span> // to prevent layout changes
514
  }</p>
515
  </div>
516
+ : currentVideo ? <video
517
+ src={currentVideo}
518
  controls
 
519
  playsInline
520
+ // I think we can't autoplay with sound,
521
+ // so let's disable auto-play
522
+ // autoPlay
523
+ // muted
524
  loop
525
  className="object-cover"
526
  style={{
 
530
  items-center justify-center
531
  text-lg text-center"></div>}
532
  </div>
533
+
534
+ <div className={cn(`
535
+ fixed
536
+ flex flex-row items-center justify-center
537
+ bg-transparent
538
+ font-sans
539
+ -mb-0
540
+ `,
541
+ isLandscape ? 'h-4' : 'h-16'
542
+ )}
543
+ style={{ width: isPortrait ? 288 : 512 }}>
544
+ <span className="text-stone-100/50 text-4xs"
545
+ style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>
546
+ Powered by
547
+ </span>
548
+ <span className="ml-1 mr-0.5">
549
+ <Image src={HFLogo} alt="Hugging Face" width="14" height="14" />
550
+ </span>
551
+ <span className="text-stone-100/80 text-3xs font-semibold"
552
+ style={{ textShadow: "rgb(0 0 0 / 80%) 0px 0px 2px" }}>Hugging Face</span>
553
+
554
+ </div>
555
  </DeviceFrameset>
556
  </div>
557
  </div>
558
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  </div>
560
  <Toaster />
561
  </div>
src/app/server/aitube/createClap.ts CHANGED
@@ -3,21 +3,26 @@
3
  import { ClapProject } from "@aitube/clap"
4
  import { createClap as apiCreateClap } from "@aitube/client"
5
 
 
6
  import { getToken } from "./getToken"
7
 
 
 
 
 
 
8
  export async function createClap({
9
  prompt = "",
 
10
  }: {
11
  prompt: string
 
12
  }): Promise<ClapProject> {
13
  const clap: ClapProject = await apiCreateClap({
14
  prompt,
15
 
16
- // the vertical video look 🤳
17
- // initially I used 1024x512 (a 2:1 ratio)
18
- // but that is a bit too extreme, most phones only take 16:9
19
- height: 1024,
20
- width: 576,
21
 
22
  token: await getToken()
23
  })
 
3
  import { ClapProject } from "@aitube/clap"
4
  import { createClap as apiCreateClap } from "@aitube/client"
5
 
6
+ import { VideoOrientation } from "../../types"
7
  import { getToken } from "./getToken"
8
 
9
+ // initially I used 1024x512 (a 2:1 ratio)
10
+ // but that is a bit too extreme, most phones only take 16:9
11
+ const RESOLUTION_LONG = 1024
12
+ const RESOLUTION_SHORT = 576
13
+
14
  export async function createClap({
15
  prompt = "",
16
+ orientation = VideoOrientation.PORTRAIT,
17
  }: {
18
  prompt: string
19
+ orientation: VideoOrientation
20
  }): Promise<ClapProject> {
21
  const clap: ClapProject = await apiCreateClap({
22
  prompt,
23
 
24
+ height: orientation === VideoOrientation.PORTRAIT ? RESOLUTION_LONG : RESOLUTION_SHORT,
25
+ width: orientation === VideoOrientation.PORTRAIT ? RESOLUTION_SHORT : RESOLUTION_LONG,
 
 
 
26
 
27
  token: await getToken()
28
  })
src/app/server/aitube/editClapDialogues.ts CHANGED
@@ -1,7 +1,7 @@
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
- import { editClapDialogues as apiEditClapDialogues } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
@@ -12,6 +12,7 @@ export async function editClapDialogues({
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapDialogues({
14
  clap,
 
15
  token: await getToken()
16
  })
17
 
 
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
+ import { editClapDialogues as apiEditClapDialogues, ClapCompletionMode } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
 
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapDialogues({
14
  clap,
15
+ completionMode: ClapCompletionMode.FULL,
16
  token: await getToken()
17
  })
18
 
src/app/server/aitube/editClapStoryboards.ts CHANGED
@@ -1,7 +1,7 @@
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
- import { editClapStoryboards as apiEditClapStoryboards } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
@@ -12,6 +12,7 @@ export async function editClapStoryboards({
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapStoryboards({
14
  clap,
 
15
  token: await getToken()
16
  })
17
 
 
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
+ import { editClapStoryboards as apiEditClapStoryboards, ClapCompletionMode } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
 
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapStoryboards({
14
  clap,
15
+ completionMode: ClapCompletionMode.FULL,
16
  token: await getToken()
17
  })
18
 
src/app/server/aitube/editClapVideos.ts CHANGED
@@ -1,7 +1,7 @@
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
- import { editClapVideos as apiEditClapVideos } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
@@ -12,6 +12,7 @@ export async function editClapVideos({
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapVideos({
14
  clap,
 
15
  token: await getToken()
16
  })
17
 
 
1
  "use server"
2
 
3
  import { ClapProject } from "@aitube/clap"
4
+ import { editClapVideos as apiEditClapVideos, ClapCompletionMode } from "@aitube/client"
5
 
6
  import { getToken } from "./getToken"
7
 
 
12
  }): Promise<ClapProject> {
13
  const newClap: ClapProject = await apiEditClapVideos({
14
  clap,
15
+ completionMode: ClapCompletionMode.FULL,
16
  token: await getToken()
17
  })
18
 
src/app/store.ts CHANGED
@@ -1,21 +1,40 @@
1
  "use client"
2
 
3
- import { GlobalStatus, TaskStatus } from "@/types"
4
- import { ClapProject } from "@aitube/clap"
5
  import { create } from "zustand"
6
 
 
 
 
 
 
7
  export const useStore = create<{
 
 
8
  storyPromptDraft: string
9
  storyPrompt: string
 
 
 
 
 
10
  status: GlobalStatus
11
  storyGenerationStatus: TaskStatus
12
  voiceGenerationStatus: TaskStatus
13
  imageGenerationStatus: TaskStatus
14
  videoGenerationStatus: TaskStatus
15
- generatedClap?: ClapProject
16
- generatedVideo: string
 
 
 
 
17
  progress: number
18
  error: string
 
 
 
 
19
  setStoryPromptDraft: (storyPromptDraft: string) => void
20
  setStoryPrompt: (storyPrompt: string) => void
21
  setStatus: (status: GlobalStatus) => void
@@ -23,22 +42,51 @@ export const useStore = create<{
23
  setVoiceGenerationStatus: (voiceGenerationStatus: TaskStatus) => void
24
  setImageGenerationStatus: (imageGenerationStatus: TaskStatus) => void
25
  setVideoGenerationStatus: (videoGenerationStatus: TaskStatus) => void
26
- setGeneratedClap: (generatedClap?: ClapProject) => void
27
- setGeneratedVideo: (generatedVideo: string) => void
 
 
 
28
  setProgress: (progress: number) => void
29
  setError: (error: string) => void
 
 
30
  }>((set, get) => ({
 
 
31
  storyPromptDraft: "Yesterday I was at my favorite pizza place and..",
32
  storyPrompt: "",
 
33
  status: "idle",
34
  storyGenerationStatus: "idle",
35
  voiceGenerationStatus: "idle",
36
  imageGenerationStatus: "idle",
37
  videoGenerationStatus: "idle",
38
- generatedClap: undefined,
39
- generatedVideo: "",
 
40
  progress: 0,
41
  error: "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  setStoryPromptDraft: (storyPromptDraft: string) => { set({ storyPromptDraft }) },
43
  setStoryPrompt: (storyPrompt: string) => { set({ storyPrompt }) },
44
  setStatus: (status: GlobalStatus) => { set({ status }) },
@@ -46,8 +94,49 @@ export const useStore = create<{
46
  setVoiceGenerationStatus: (voiceGenerationStatus: TaskStatus) => { set({ voiceGenerationStatus }) },
47
  setImageGenerationStatus: (imageGenerationStatus: TaskStatus) => { set({ imageGenerationStatus }) },
48
  setVideoGenerationStatus: (videoGenerationStatus: TaskStatus) => { set({ videoGenerationStatus }) },
49
- setGeneratedClap: (generatedClap?: ClapProject) => { set({ generatedClap }) },
50
- setGeneratedVideo: (generatedVideo: string) => { set({ generatedVideo }) },
 
 
 
 
 
 
 
51
  setProgress: (progress: number) => { set({ progress }) },
52
  setError: (error: string) => { set({ error }) },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }))
 
1
  "use client"
2
 
3
+ import { ClapProject, parseClap, serializeClap } from "@aitube/clap"
 
4
  import { create } from "zustand"
5
 
6
+ import { GlobalStatus, TaskStatus } from "@/types"
7
+
8
+ import { VideoOrientation } from "./types"
9
+ import { getVideoOrientation } from "@/lib/utils/getVideoOrientation"
10
+
11
  export const useStore = create<{
12
+ mainCharacterImage: string
13
+ mainCharacterVoice: string
14
  storyPromptDraft: string
15
  storyPrompt: string
16
+
17
+ // the desired orientation for the next video
18
+ // but this won't impact the actual orientation of the fake device container
19
+ orientation: VideoOrientation
20
+
21
  status: GlobalStatus
22
  storyGenerationStatus: TaskStatus
23
  voiceGenerationStatus: TaskStatus
24
  imageGenerationStatus: TaskStatus
25
  videoGenerationStatus: TaskStatus
26
+ currentClap?: ClapProject
27
+ currentVideo: string
28
+
29
+ // orientation of the currently loaded video (which can be different from `orientation`)
30
+ // it will impact the actual orientation of the fake device container
31
+ currentVideoOrientation: VideoOrientation
32
  progress: number
33
  error: string
34
+ toggleOrientation: () => void
35
+ setCurrentVideoOrientation: (currentVideoOrientation: VideoOrientation) => void
36
+ setMainCharacterImage: (mainCharacterImage: string) => void
37
+ setMainCharacterVoice: (mainCharacterVoice: string) => void
38
  setStoryPromptDraft: (storyPromptDraft: string) => void
39
  setStoryPrompt: (storyPrompt: string) => void
40
  setStatus: (status: GlobalStatus) => void
 
42
  setVoiceGenerationStatus: (voiceGenerationStatus: TaskStatus) => void
43
  setImageGenerationStatus: (imageGenerationStatus: TaskStatus) => void
44
  setVideoGenerationStatus: (videoGenerationStatus: TaskStatus) => void
45
+ setCurrentClap: (currentClap?: ClapProject) => void
46
+
47
+ // note: this will preload the video, and compute the orientation too
48
+ setGeneratedVideo: (generatedVideo: string) => Promise<void>
49
+
50
  setProgress: (progress: number) => void
51
  setError: (error: string) => void
52
+ saveClap: (fileName?: string) => Promise<void>
53
+ loadClap: (blob: Blob, fileName?: string) => Promise<void>
54
  }>((set, get) => ({
55
+ mainCharacterImage: "",
56
+ mainCharacterVoice: "",
57
  storyPromptDraft: "Yesterday I was at my favorite pizza place and..",
58
  storyPrompt: "",
59
+ orientation: VideoOrientation.PORTRAIT,
60
  status: "idle",
61
  storyGenerationStatus: "idle",
62
  voiceGenerationStatus: "idle",
63
  imageGenerationStatus: "idle",
64
  videoGenerationStatus: "idle",
65
+ currentClap: undefined,
66
+ currentVideo: "",
67
+ currentVideoOrientation: VideoOrientation.PORTRAIT,
68
  progress: 0,
69
  error: "",
70
+ toggleOrientation: () => {
71
+ const { orientation: previousOrientation, currentVideoOrientation, currentVideo } = get()
72
+ const orientation =
73
+ previousOrientation === VideoOrientation.LANDSCAPE
74
+ ? VideoOrientation.PORTRAIT
75
+ : VideoOrientation.LANDSCAPE
76
+
77
+ set({
78
+ orientation,
79
+
80
+ // we normally don't touch the currentVideoOrientation since it will already contain a video
81
+ currentVideoOrientation:
82
+ currentVideo
83
+ ? currentVideoOrientation
84
+ : orientation
85
+ })
86
+ },
87
+ setCurrentVideoOrientation: (currentVideoOrientation: VideoOrientation) => { set({ currentVideoOrientation }) },
88
+ setMainCharacterImage: (mainCharacterImage: string) => { set({ mainCharacterImage }) },
89
+ setMainCharacterVoice: (mainCharacterVoice: string) => { set({ mainCharacterVoice }) },
90
  setStoryPromptDraft: (storyPromptDraft: string) => { set({ storyPromptDraft }) },
91
  setStoryPrompt: (storyPrompt: string) => { set({ storyPrompt }) },
92
  setStatus: (status: GlobalStatus) => { set({ status }) },
 
94
  setVoiceGenerationStatus: (voiceGenerationStatus: TaskStatus) => { set({ voiceGenerationStatus }) },
95
  setImageGenerationStatus: (imageGenerationStatus: TaskStatus) => { set({ imageGenerationStatus }) },
96
  setVideoGenerationStatus: (videoGenerationStatus: TaskStatus) => { set({ videoGenerationStatus }) },
97
+ setCurrentClap: (currentClap?: ClapProject) => { set({ currentClap }) },
98
+ setGeneratedVideo: async (currentVideo: string): Promise<void> => {
99
+ const currentVideoOrientation = await getVideoOrientation(currentVideo)
100
+ set({
101
+ currentVideo,
102
+ currentVideoOrientation
103
+
104
+ })
105
+ },
106
  setProgress: (progress: number) => { set({ progress }) },
107
  setError: (error: string) => { set({ error }) },
108
+ saveClap: async (fileName: string = "untitled_story.clap"): Promise<void> => {
109
+ const { currentClap } = get()
110
+
111
+ if (!currentClap) { throw new Error(`cannot save a clap.. if there is no clap`) }
112
+
113
+ const currentClapBlob: Blob = await serializeClap(currentClap)
114
+
115
+ // Create an object URL for the compressed clap blob
116
+ const objectUrl = URL.createObjectURL(currentClapBlob)
117
+
118
+ // Create an anchor element and force browser download
119
+ const anchor = document.createElement("a")
120
+ anchor.href = objectUrl
121
+
122
+ anchor.download = fileName
123
+
124
+ document.body.appendChild(anchor) // Append to the body (could be removed once clicked)
125
+ anchor.click() // Trigger the download
126
+
127
+ // Cleanup: revoke the object URL and remove the anchor element
128
+ URL.revokeObjectURL(objectUrl)
129
+ document.body.removeChild(anchor)
130
+ },
131
+ loadClap: async (blob: Blob, fileName: string = "untitled_story.clap"): Promise<void> => {
132
+ if (!blob) {
133
+ throw new Error(`missing blob`)
134
+ }
135
+
136
+ const currentClap: ClapProject = await parseClap(blob)
137
+
138
+ set({
139
+ currentClap,
140
+ })
141
+ },
142
  }))
src/app/types.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export enum VideoOrientation {
2
+ PORTRAIT = "portrait",
3
+ LANDSCAPE = "landscape",
4
+ SQUARE = "square"
5
+ }
src/components/form/field.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ export function Field({ children }: { children: ReactNode }) {
4
+ return (
5
+ <div className="flex flex-col space-y-2">{children}</div>
6
+ )
7
+ }
src/components/form/input-field.tsx CHANGED
@@ -25,4 +25,4 @@ export function InputField({
25
  <Input {...props} className={cn("text-xl", inputClassName)} />
26
  </div>
27
  )
28
- }
 
25
  <Input {...props} className={cn("text-xl", inputClassName)} />
26
  </div>
27
  )
28
+ }
src/components/form/label.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ import { cn } from "@/lib/utils/cn"
4
+
5
+ export function Label({ children, className = "" }: { children: ReactNode; className?: string }) {
6
+ return (
7
+ <label className={cn(`text-base font-semibold text-zinc-700`, className)}>{children}</label>
8
+ )
9
+ }
src/components/form/switch-field.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComponentProps } from "react"
2
+
3
+ import { Label } from "@/components/ui/label"
4
+ import { cn } from "@/lib/utils/cn"
5
+
6
+ import { Switch } from "../ui/switch"
7
+
8
+ export function SwitchField({
9
+ label,
10
+ className = "",
11
+ labelClassName = "",
12
+ switchClassName = "",
13
+ ...props
14
+ }: ComponentProps<typeof Switch> & {
15
+ label?: string;
16
+ className?: string;
17
+ labelClassName?: string;
18
+ switchClassName?: string;
19
+ }) {
20
+ return (
21
+ <div className={cn(
22
+ `flex flex-col space-y-3 items-start`,
23
+ className
24
+ )}>
25
+ {label && <Label className={cn(`
26
+ text-base md:text-lg lg:text-xl
27
+ text-stone-900/90 dark:text-stone-100/90
28
+ `, labelClassName)}>{label}</Label>}
29
+ <Switch {...props} className={switchClassName} />
30
+ </div>
31
+ )
32
+ }
src/lib/base64/fileToBase64.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export function fileToBase64(file: File | Blob): Promise<string> {
2
+ return new Promise((resolve, reject) => {
3
+ const fileReader = new FileReader();
4
+ fileReader.readAsDataURL(file);
5
+ fileReader.onload = () => { resolve(`${fileReader.result}`); };
6
+ fileReader.onerror = (error) => { reject(error); };
7
+ });
8
+ }
src/lib/utils/getVideoOrientation.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VideoOrientation } from "@/app/types"
2
+
3
+ /**
4
+ * Determine the video orientation from a video URL (data-uri or hosted)
5
+ *
6
+ * @param url
7
+ * @returns
8
+ */
9
+ export async function getVideoOrientation(url: string): Promise<VideoOrientation> {
10
+ return new Promise<VideoOrientation>(resolve => {
11
+ const video = document.createElement('video')
12
+ video.addEventListener( "loadedmetadata", function () {
13
+ resolve(
14
+ this.videoHeight < this.videoWidth ? VideoOrientation.LANDSCAPE :
15
+ this.videoHeight > this.videoWidth ? VideoOrientation.PORTRAIT :
16
+ VideoOrientation.SQUARE
17
+ )
18
+ }, false)
19
+ video.src = url
20
+ })
21
+ }