Commit
•
93ad82e
1
Parent(s):
e17bdf7
add toggle for video orientation
Browse files- package-lock.json +35 -0
- package.json +2 -0
- src/app/main.tsx +206 -75
- src/app/server/aitube/createClap.ts +10 -5
- src/app/server/aitube/editClapDialogues.ts +2 -1
- src/app/server/aitube/editClapStoryboards.ts +2 -1
- src/app/server/aitube/editClapVideos.ts +2 -1
- src/app/store.ts +99 -10
- src/app/types.ts +5 -0
- src/components/form/field.tsx +7 -0
- src/components/form/input-field.tsx +1 -1
- src/components/form/label.tsx +9 -0
- src/components/form/switch-field.tsx +32 -0
- src/lib/base64/fileToBase64.ts +8 -0
- src/lib/utils/getVideoOrientation.ts +21 -0
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
|
|
|
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
|
35 |
-
const
|
|
|
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
|
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({
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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 |
-
|
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
|
273 |
</div>
|
274 |
</CardHeader>
|
275 |
-
<CardContent
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
">
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
placeholder="Yesterday I was at my favorite pizza place and.."
|
289 |
-
inputClassName="
|
290 |
transition-all duration-200 ease-in-out
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
<div className="flex flex-row justify-end items-center">
|
299 |
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
w-full
|
301 |
flex flex-row
|
302 |
-
justify-
|
303 |
space-x-3">
|
304 |
-
|
|
|
305 |
<Button
|
306 |
-
onClick={
|
307 |
-
disabled={
|
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">
|
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 |
-
|
334 |
</Button>
|
335 |
</div>
|
|
|
|
|
336 |
</div>
|
337 |
-
|
338 |
-
|
339 |
-
|
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-
|
357 |
transition-all duration-200 ease-in-out
|
358 |
-
|
359 |
-
|
|
|
|
|
|
|
360 |
<DeviceFrameset
|
361 |
device="Nexus 5"
|
362 |
// color="black"
|
363 |
|
364 |
-
|
|
|
|
|
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> </span> // to prevent layout changes
|
390 |
}</p>
|
391 |
</div>
|
392 |
-
:
|
393 |
-
src={
|
394 |
controls
|
395 |
-
autoPlay
|
396 |
playsInline
|
397 |
-
|
|
|
|
|
|
|
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> </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 |
-
|
17 |
-
|
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 {
|
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 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
27 |
-
|
|
|
|
|
|
|
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 |
-
|
39 |
-
|
|
|
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 |
-
|
50 |
-
setGeneratedVideo: (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|