jbilcke-hf HF staff commited on
Commit
59f9b88
β€’
1 Parent(s): dd3ca23

adding some menus

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. package-lock.json +55 -13
  2. package.json +3 -1
  3. src/app/DEPRECATED_main.txt +143 -0
  4. src/app/layout.tsx +8 -4
  5. src/app/main.tsx +22 -108
  6. src/app/page.tsx +1 -5
  7. src/app/{globals.css β†’ styles/globals.css} +0 -0
  8. src/app/{react-reflex-custom.css β†’ styles/react-reflex-custom.css} +6 -6
  9. src/app/{react-reflex.css β†’ styles/react-reflex.css} +0 -0
  10. src/components/form/form-dir.tsx +58 -0
  11. src/components/form/form-field.tsx +29 -0
  12. src/components/form/form-file.tsx +58 -0
  13. src/components/form/form-input.tsx +108 -0
  14. src/components/form/form-label.tsx +14 -0
  15. src/components/form/form-radio.tsx +36 -0
  16. src/components/form/form-section.tsx +24 -0
  17. src/components/form/form-select.tsx +67 -0
  18. src/components/interface/loader/index.tsx +39 -0
  19. src/components/interface/static-video/index.tsx +1 -1
  20. src/components/interface/system-menu/index.tsx +236 -0
  21. src/components/interface/timeline/index.tsx +1 -1
  22. src/components/interface/top-bar/index.tsx +30 -0
  23. src/components/interface/top-menu/file/index.tsx +72 -0
  24. src/components/interface/top-menu/index.tsx +20 -0
  25. src/components/interface/top-menu/rendering/index.tsx +141 -0
  26. src/components/interface/top-menu/view/index.tsx +98 -0
  27. src/components/settings/SettingsSectionRendering.tsx +19 -0
  28. src/components/settings/index.tsx +57 -0
  29. src/components/ui/menubar.tsx +3 -3
  30. src/components/ui/scroll-area.tsx +48 -0
  31. src/lib/core/DEPRECATED_getSettings.txt +26 -0
  32. src/{components/interface/settings/constants.tsx β†’ lib/core/DEPRECATED_localStorageKeys.txt} +0 -0
  33. src/lib/core/constants.ts +4 -0
  34. src/lib/core/getDefaultSettings.ts +3 -3
  35. src/lib/hooks/index.ts +6 -0
  36. src/lib/hooks/useClapFilePicker.ts +37 -0
  37. src/lib/hooks/useFullscreenStatus.ts +78 -0
  38. src/lib/hooks/useRequestAnimationFrame.ts +1 -2
  39. src/lib/hooks/useScreenplayFilePicker.ts +41 -0
  40. src/lib/utils/getValidBoolean.ts +9 -0
  41. src/lib/utils/getValidNumber.ts +10 -0
  42. src/lib/utils/getValidString.ts +8 -0
  43. src/lib/utils/index.ts +9 -1
  44. src/lib/utils/isValidNumber.ts +3 -0
  45. src/lib/utils/parseComfyVendor.ts +28 -0
  46. src/lib/utils/parseRenderingStrategy.ts +31 -0
  47. src/server/comfy/{formatStoryboardWorkflow.ts β†’ getComfyWorkflow.ts} +15 -4
  48. src/server/comfy/index.ts +7 -4
  49. src/server/comfy/replicate.ts +1 -0
  50. src/settings/rendering/getDefaultSettingsRendering.ts +15 -0
package-lock.json CHANGED
@@ -10,7 +10,7 @@
10
  "dependencies": {
11
  "@aitube/clap": "0.0.24",
12
  "@aitube/engine": "0.0.15",
13
- "@aitube/timeline": "0.0.7",
14
  "@huggingface/hub": "^0.15.0",
15
  "@radix-ui/react-accordion": "^1.1.2",
16
  "@radix-ui/react-avatar": "^1.0.4",
@@ -22,6 +22,7 @@
22
  "@radix-ui/react-label": "^2.0.2",
23
  "@radix-ui/react-menubar": "^1.0.4",
24
  "@radix-ui/react-popover": "^1.0.7",
 
25
  "@radix-ui/react-select": "^2.0.0",
26
  "@radix-ui/react-separator": "^1.0.3",
27
  "@radix-ui/react-slider": "^1.1.2",
@@ -59,6 +60,7 @@
59
  "react-dom": "^18.3.1",
60
  "react-drag-drop-files": "^2.3.10",
61
  "react-hook-consent": "^3.5.3",
 
62
  "react-icons": "^5.2.1",
63
  "react-reflex": "^4.2.6",
64
  "replicate": "^0.30.1",
@@ -97,9 +99,9 @@
97
  }
98
  },
99
  "node_modules/@aitube/timeline": {
100
- "version": "0.0.7",
101
- "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.7.tgz",
102
- "integrity": "sha512-eQbp2fcrazuW6prKfGAfcEVNvHgO+ZVgh5aHJXSQfCDty8dLw54MlvbsZw9RXntxMjGu3OzqgC6PCSAZldebFQ==",
103
  "dependencies": {
104
  "date-fns": "^3.6.0",
105
  "react-virtualized-auto-sizer": "^1.0.24"
@@ -610,9 +612,9 @@
610
  }
611
  },
612
  "node_modules/@eslint-community/regexpp": {
613
- "version": "4.10.0",
614
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
615
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
616
  "engines": {
617
  "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
618
  }
@@ -2130,6 +2132,37 @@
2130
  }
2131
  }
2132
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2133
  "node_modules/@radix-ui/react-select": {
2134
  "version": "2.0.0",
2135
  "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
@@ -2715,9 +2748,9 @@
2715
  }
2716
  },
2717
  "node_modules/@react-three/fiber": {
2718
- "version": "8.16.7",
2719
- "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.7.tgz",
2720
- "integrity": "sha512-D9z/HmTupS/Z3YqBK+rxPgm3Ou7VvOB8J57+AW7JG2FmhCY4u65Gd6r8i8y61BR/paCk0DCPyyJ5w7FiUEHXgA==",
2721
  "dependencies": {
2722
  "@babel/runtime": "^7.17.8",
2723
  "@types/react-reconciler": "^0.26.7",
@@ -4436,9 +4469,9 @@
4436
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
4437
  },
4438
  "node_modules/electron-to-chromium": {
4439
- "version": "1.4.788",
4440
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz",
4441
- "integrity": "sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA=="
4442
  },
4443
  "node_modules/emoji-regex": {
4444
  "version": "9.2.2",
@@ -7349,6 +7382,15 @@
7349
  "react-dom": ">=16.8.0"
7350
  }
7351
  },
 
 
 
 
 
 
 
 
 
7352
  "node_modules/react-icons": {
7353
  "version": "5.2.1",
7354
  "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
 
10
  "dependencies": {
11
  "@aitube/clap": "0.0.24",
12
  "@aitube/engine": "0.0.15",
13
+ "@aitube/timeline": "0.0.8",
14
  "@huggingface/hub": "^0.15.0",
15
  "@radix-ui/react-accordion": "^1.1.2",
16
  "@radix-ui/react-avatar": "^1.0.4",
 
22
  "@radix-ui/react-label": "^2.0.2",
23
  "@radix-ui/react-menubar": "^1.0.4",
24
  "@radix-ui/react-popover": "^1.0.7",
25
+ "@radix-ui/react-scroll-area": "^1.0.5",
26
  "@radix-ui/react-select": "^2.0.0",
27
  "@radix-ui/react-separator": "^1.0.3",
28
  "@radix-ui/react-slider": "^1.1.2",
 
60
  "react-dom": "^18.3.1",
61
  "react-drag-drop-files": "^2.3.10",
62
  "react-hook-consent": "^3.5.3",
63
+ "react-hotkeys-hook": "^4.5.0",
64
  "react-icons": "^5.2.1",
65
  "react-reflex": "^4.2.6",
66
  "replicate": "^0.30.1",
 
99
  }
100
  },
101
  "node_modules/@aitube/timeline": {
102
+ "version": "0.0.8",
103
+ "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.8.tgz",
104
+ "integrity": "sha512-+Eqhdr2tGCU46r9mJ+H/so+QB6BObqerI5j3oG2iSaMSalFIgP1K8zuigMhN/FzKkESF3ORY77ciZnZupcmBDA==",
105
  "dependencies": {
106
  "date-fns": "^3.6.0",
107
  "react-virtualized-auto-sizer": "^1.0.24"
 
612
  }
613
  },
614
  "node_modules/@eslint-community/regexpp": {
615
+ "version": "4.10.1",
616
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz",
617
+ "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==",
618
  "engines": {
619
  "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
620
  }
 
2132
  }
2133
  }
2134
  },
2135
+ "node_modules/@radix-ui/react-scroll-area": {
2136
+ "version": "1.0.5",
2137
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
2138
+ "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
2139
+ "dependencies": {
2140
+ "@babel/runtime": "^7.13.10",
2141
+ "@radix-ui/number": "1.0.1",
2142
+ "@radix-ui/primitive": "1.0.1",
2143
+ "@radix-ui/react-compose-refs": "1.0.1",
2144
+ "@radix-ui/react-context": "1.0.1",
2145
+ "@radix-ui/react-direction": "1.0.1",
2146
+ "@radix-ui/react-presence": "1.0.1",
2147
+ "@radix-ui/react-primitive": "1.0.3",
2148
+ "@radix-ui/react-use-callback-ref": "1.0.1",
2149
+ "@radix-ui/react-use-layout-effect": "1.0.1"
2150
+ },
2151
+ "peerDependencies": {
2152
+ "@types/react": "*",
2153
+ "@types/react-dom": "*",
2154
+ "react": "^16.8 || ^17.0 || ^18.0",
2155
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
2156
+ },
2157
+ "peerDependenciesMeta": {
2158
+ "@types/react": {
2159
+ "optional": true
2160
+ },
2161
+ "@types/react-dom": {
2162
+ "optional": true
2163
+ }
2164
+ }
2165
+ },
2166
  "node_modules/@radix-ui/react-select": {
2167
  "version": "2.0.0",
2168
  "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
 
2748
  }
2749
  },
2750
  "node_modules/@react-three/fiber": {
2751
+ "version": "8.16.8",
2752
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.8.tgz",
2753
+ "integrity": "sha512-Lc8fjATtvQEfSd8d5iKdbpHtRm/aPMeFj7jQvp6TNHfpo8IQTW3wwcE1ZMrGGoUH+w2mnyS+0MK1NLPLnuzGkQ==",
2754
  "dependencies": {
2755
  "@babel/runtime": "^7.17.8",
2756
  "@types/react-reconciler": "^0.26.7",
 
4469
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
4470
  },
4471
  "node_modules/electron-to-chromium": {
4472
+ "version": "1.4.789",
4473
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.789.tgz",
4474
+ "integrity": "sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ=="
4475
  },
4476
  "node_modules/emoji-regex": {
4477
  "version": "9.2.2",
 
7382
  "react-dom": ">=16.8.0"
7383
  }
7384
  },
7385
+ "node_modules/react-hotkeys-hook": {
7386
+ "version": "4.5.0",
7387
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
7388
+ "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
7389
+ "peerDependencies": {
7390
+ "react": ">=16.8.1",
7391
+ "react-dom": ">=16.8.1"
7392
+ }
7393
+ },
7394
  "node_modules/react-icons": {
7395
  "version": "5.2.1",
7396
  "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
package.json CHANGED
@@ -12,7 +12,7 @@
12
  "dependencies": {
13
  "@aitube/clap": "0.0.24",
14
  "@aitube/engine": "0.0.15",
15
- "@aitube/timeline": "0.0.7",
16
  "@huggingface/hub": "^0.15.0",
17
  "@radix-ui/react-accordion": "^1.1.2",
18
  "@radix-ui/react-avatar": "^1.0.4",
@@ -24,6 +24,7 @@
24
  "@radix-ui/react-label": "^2.0.2",
25
  "@radix-ui/react-menubar": "^1.0.4",
26
  "@radix-ui/react-popover": "^1.0.7",
 
27
  "@radix-ui/react-select": "^2.0.0",
28
  "@radix-ui/react-separator": "^1.0.3",
29
  "@radix-ui/react-slider": "^1.1.2",
@@ -61,6 +62,7 @@
61
  "react-dom": "^18.3.1",
62
  "react-drag-drop-files": "^2.3.10",
63
  "react-hook-consent": "^3.5.3",
 
64
  "react-icons": "^5.2.1",
65
  "react-reflex": "^4.2.6",
66
  "replicate": "^0.30.1",
 
12
  "dependencies": {
13
  "@aitube/clap": "0.0.24",
14
  "@aitube/engine": "0.0.15",
15
+ "@aitube/timeline": "0.0.8",
16
  "@huggingface/hub": "^0.15.0",
17
  "@radix-ui/react-accordion": "^1.1.2",
18
  "@radix-ui/react-avatar": "^1.0.4",
 
24
  "@radix-ui/react-label": "^2.0.2",
25
  "@radix-ui/react-menubar": "^1.0.4",
26
  "@radix-ui/react-popover": "^1.0.7",
27
+ "@radix-ui/react-scroll-area": "^1.0.5",
28
  "@radix-ui/react-select": "^2.0.0",
29
  "@radix-ui/react-separator": "^1.0.3",
30
  "@radix-ui/react-slider": "^1.1.2",
 
62
  "react-dom": "^18.3.1",
63
  "react-drag-drop-files": "^2.3.10",
64
  "react-hook-consent": "^3.5.3",
65
+ "react-hotkeys-hook": "^4.5.0",
66
  "react-icons": "^5.2.1",
67
  "react-reflex": "^4.2.6",
68
  "replicate": "^0.30.1",
src/app/DEPRECATED_main.txt ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React, { useEffect, useState } from "react"
4
+ import { FileUploader } from "react-drag-drop-files"
5
+ import {
6
+ ReflexContainer,
7
+ ReflexSplitter,
8
+ ReflexElement
9
+ } from 'react-reflex'
10
+ import { parseClap } from "@aitube/clap"
11
+ import { ClapTimeline, useTimelineState } from "@aitube/timeline"
12
+
13
+ import { Toaster } from "@/components/ui/sonner"
14
+ import { cn } from "@/lib/utils"
15
+ import { TooltipProvider } from "@/components/ui/tooltip"
16
+ import { useQueryStringParams } from "@/lib/hooks/useQueryStringParams"
17
+ import { StaticVideo } from "@/components/interface/static-video"
18
+ import { RenderClap } from "@/components/interface/render-clap"
19
+ import { Timeline } from "@/components/interface/timeline"
20
+ import { TopBar } from "@/components/interface/top-bar"
21
+
22
+ export function Main() {
23
+ const { clapUrl } = useQueryStringParams({
24
+ // clapUrl: `/samples/test.clap`,
25
+ // clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
26
+ clapUrl: '',
27
+ })
28
+
29
+ const [isImporting, setIsImporting] = useState(false)
30
+ const clap = useTimelineState(s => s.clap)
31
+ const setClap = useTimelineState(s => s.setClap)
32
+ const isLoading = useTimelineState(s => s.isLoading)
33
+ const isEmpty = useTimelineState(s => s.isEmpty)
34
+
35
+ const isBusy = isLoading || isImporting
36
+
37
+ const handleChange = async (file: File) => {
38
+ if (file.name.endsWith("clap")) {
39
+ setIsImporting(true)
40
+ try {
41
+ const clap = await parseClap(file)
42
+ await setClap(clap)
43
+ setTimeout(() => {
44
+ setIsImporting(false)
45
+ }, 500)
46
+ } catch (err) {
47
+ console.error(err)
48
+ setIsImporting(false)
49
+ }
50
+ } else {
51
+ setIsImporting(true)
52
+ try {
53
+ const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
54
+ method: "POST",
55
+ headers: { 'Content-Type': 'text/plain' },
56
+ body: await file.text(),
57
+ })
58
+ const blob = await res.blob()
59
+ const clap = await parseClap(blob)
60
+ await setClap(clap)
61
+ setTimeout(() => {
62
+ setIsImporting(false)
63
+ }, 1000)
64
+ } catch (err) {
65
+ console.error(err)
66
+ setIsImporting(false)
67
+ }
68
+ }
69
+ };
70
+
71
+ useEffect(() => {
72
+ (async () => {
73
+ if (!clapUrl) {
74
+ console.log("No clap URL provided")
75
+ return
76
+ }
77
+ setIsImporting(true)
78
+ const res = await fetch(clapUrl)
79
+ const blob = await res.blob()
80
+ const clap = await parseClap(blob)
81
+ await setClap(clap)
82
+ setTimeout(() => {
83
+ setIsImporting(false)
84
+ }, 1000)
85
+ })()
86
+ }, [clapUrl])
87
+
88
+ return (
89
+ <TooltipProvider>
90
+ <div className={cn(`
91
+ fixed
92
+ flex flex-col
93
+ w-screen h-screen
94
+ overflow-hidden
95
+ items-center justify-center
96
+ bg-[rgb(58,54,50)]
97
+ `
98
+ )}
99
+ >
100
+ <TopBar />
101
+ <div className="flex flex-row flex-grow w-full overflow-hidden">
102
+ {clap
103
+ ?
104
+ <ReflexContainer orientation="horizontal">
105
+ <ReflexElement>
106
+ <RenderClap />
107
+ </ReflexElement>
108
+ <ReflexSplitter />
109
+ <ReflexElement className={isLoading ? 'opacity-0' : 'opacity-100'}>
110
+ <Timeline />
111
+ </ReflexElement>
112
+ </ReflexContainer>
113
+ :
114
+ <FileUploader
115
+ handleChange={handleChange}
116
+ name="file" types={["txt", "clap"]}>
117
+ <div className={cn(`
118
+ flex
119
+ w-screen h-screen
120
+ overflow-hidden
121
+ items-center justify-center
122
+ cursor-pointer
123
+ `, isBusy ? 'animate-pulse' : '')}
124
+ style={{
125
+ backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
126
+ }}><p
127
+ className="text-stone-100 font-sans font-thin text-[3vw]"
128
+ style={{ textShadow: "#000 1px 0 3px" }}
129
+ >
130
+ {isBusy ? 'Loading..' :
131
+ <span className="flex flex-center justify-center text-center w-full">
132
+ Click to import a screenplay (.txt)<br/>
133
+ or an OpenClap file (.clap)</span>}
134
+ </p>
135
+ </div>
136
+ </FileUploader>
137
+ }
138
+ <Toaster />
139
+ </div>
140
+ </div>
141
+ </TooltipProvider>
142
+ );
143
+ }
src/app/layout.tsx CHANGED
@@ -1,7 +1,8 @@
1
  import { cn } from '@/lib/utils'
2
- import './globals.css'
3
- import "./react-reflex.css"
4
- import "./react-reflex-custom.css"
 
5
 
6
  import type { Metadata } from 'next'
7
  import { inter } from './fonts'
@@ -22,7 +23,10 @@ export default function RootLayout({
22
  `h-full w-full overflow-none dark`,
23
  inter.className
24
  )}
25
- style={{ overscrollBehaviorX: "none" }}>
 
 
 
26
  {children}
27
  </body>
28
  </html>
 
1
  import { cn } from '@/lib/utils'
2
+
3
+ import './styles/globals.css'
4
+ import "./styles/react-reflex.css"
5
+ import "./styles/react-reflex-custom.css"
6
 
7
  import type { Metadata } from 'next'
8
  import { inter } from './fonts'
 
23
  `h-full w-full overflow-none dark`,
24
  inter.className
25
  )}
26
+ style={{
27
+ overscrollBehaviorX: "none",
28
+ backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
29
+ }}>
30
  {children}
31
  </body>
32
  </html>
src/app/main.tsx CHANGED
@@ -1,138 +1,52 @@
1
  "use client"
2
 
3
  import React, { useEffect, useState } from "react"
4
- import { FileUploader } from "react-drag-drop-files"
5
  import {
6
  ReflexContainer,
7
  ReflexSplitter,
8
  ReflexElement
9
  } from 'react-reflex'
10
- import { parseClap } from "@aitube/clap"
11
- import { ClapTimeline, useTimelineState } from "@aitube/timeline"
12
 
13
  import { Toaster } from "@/components/ui/sonner"
14
  import { cn } from "@/lib/utils"
15
  import { TooltipProvider } from "@/components/ui/tooltip"
16
- import { useQueryStringParams } from "@/lib/hooks/useQueryStringParams"
17
- import { StaticVideo } from "@/components/interface/static-video"
18
  import { RenderClap } from "@/components/interface/render-clap"
19
  import { Timeline } from "@/components/interface/timeline"
 
 
20
 
21
  export function Main() {
22
- const { clapUrl } = useQueryStringParams({
23
- // clapUrl: `/samples/test.clap`,
24
- // clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
25
- clapUrl: '',
26
- })
27
-
28
- const [isImporting, setIsImporting] = useState(false)
29
- const clap = useTimelineState(s => s.clap)
30
- const setClap = useTimelineState(s => s.setClap)
31
- const isLoading = useTimelineState(s => s.isLoading)
32
  const isEmpty = useTimelineState(s => s.isEmpty)
33
-
34
- const isBusy = isLoading || isImporting
35
-
36
- const handleChange = async (file: File) => {
37
- if (file.name.endsWith("clap")) {
38
- setIsImporting(true)
39
- try {
40
- const clap = await parseClap(file)
41
- setClap(clap)
42
- setTimeout(() => {
43
- setIsImporting(false)
44
- }, 500)
45
- } catch (err) {
46
- console.error(err)
47
- setIsImporting(false)
48
- }
49
- } else {
50
- setIsImporting(true)
51
- try {
52
- const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
53
- method: "POST",
54
- headers: { 'Content-Type': 'text/plain' },
55
- body: await file.text(),
56
- })
57
- const blob = await res.blob()
58
- const clap = await parseClap(blob)
59
- setClap(clap)
60
- setTimeout(() => {
61
- setIsImporting(false)
62
- }, 1000)
63
- } catch (err) {
64
- console.error(err)
65
- setIsImporting(false)
66
- }
67
- }
68
- };
69
-
70
- useEffect(() => {
71
- (async () => {
72
- if (!clapUrl) {
73
- console.log("No clap URL provided")
74
- return
75
- }
76
- setIsImporting(true)
77
- const res = await fetch(clapUrl)
78
- const blob = await res.blob()
79
- const clap = await parseClap(blob)
80
- setClap(clap)
81
- setTimeout(() => {
82
- setIsImporting(false)
83
- }, 1000)
84
- })()
85
- }, [clapUrl])
86
-
87
  return (
88
  <TooltipProvider>
89
  <div className={cn(`
90
  fixed
91
- flex
92
- w-screen h-screen
93
- overflow-hidden
94
- items-center justify-center
95
- bg-[rgb(58,54,50)]
96
- `
97
- )}
98
- >
99
- {clap
100
- ?
101
- <ReflexContainer orientation="horizontal">
102
- <ReflexElement>
103
- <RenderClap />
104
- </ReflexElement>
105
- <ReflexSplitter />
106
- <ReflexElement className={isLoading ? 'opacity-0' : 'opacity-100'}>
107
- <Timeline />
108
- </ReflexElement>
109
- </ReflexContainer>
110
- :
111
- <FileUploader
112
- handleChange={handleChange}
113
- name="file" types={["txt", "clap"]}>
114
- <div className={cn(`
115
- flex
116
  w-screen h-screen
117
  overflow-hidden
118
  items-center justify-center
119
- cursor-pointer
120
- `, isBusy ? 'animate-pulse' : '')}
121
  style={{
122
  backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
123
- }}><p
124
- className="text-stone-100 font-sans font-thin text-[3vw]"
125
- style={{ textShadow: "#000 1px 0 3px" }}
126
  >
127
- {isBusy ? 'Loading..' :
128
- <span className="flex flex-center justify-center text-center w-full">
129
- Click to import a screenplay (.txt)<br/>
130
- or an OpenClap file (.clap)</span>}
131
- </p>
132
- </div>
133
- </FileUploader>
134
- }
135
- <Toaster />
 
 
 
 
 
 
 
136
  </div>
137
  </TooltipProvider>
138
  );
 
1
  "use client"
2
 
3
  import React, { useEffect, useState } from "react"
 
4
  import {
5
  ReflexContainer,
6
  ReflexSplitter,
7
  ReflexElement
8
  } from 'react-reflex'
 
 
9
 
10
  import { Toaster } from "@/components/ui/sonner"
11
  import { cn } from "@/lib/utils"
12
  import { TooltipProvider } from "@/components/ui/tooltip"
 
 
13
  import { RenderClap } from "@/components/interface/render-clap"
14
  import { Timeline } from "@/components/interface/timeline"
15
+ import { TopBar } from "@/components/interface/top-bar"
16
+ import { useTimelineState } from "@aitube/timeline"
17
 
18
  export function Main() {
 
 
 
 
 
 
 
 
 
 
19
  const isEmpty = useTimelineState(s => s.isEmpty)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  return (
21
  <TooltipProvider>
22
  <div className={cn(`
23
  fixed
24
+ flex flex-col
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  w-screen h-screen
26
  overflow-hidden
27
  items-center justify-center
28
+ text-stone-900/90 dark:text-stone-100/90
29
+ `)}
30
  style={{
31
  backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
32
+ }}
 
 
33
  >
34
+ <TopBar />
35
+ <div className={cn(
36
+ `flex flex-row flex-grow w-full overflow-hidden`,
37
+ isEmpty ? "opacity-0" : "opacity-100"
38
+ )}>
39
+ <ReflexContainer orientation="horizontal">
40
+ <ReflexElement>
41
+ <RenderClap />
42
+ </ReflexElement>
43
+ <ReflexSplitter />
44
+ <ReflexElement>
45
+ <Timeline />
46
+ </ReflexElement>
47
+ </ReflexContainer>
48
+ <Toaster />
49
+ </div>
50
  </div>
51
  </TooltipProvider>
52
  );
src/app/page.tsx CHANGED
@@ -27,11 +27,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
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
- ``,
32
- `text-stone-900/90 dark:text-stone-100/90`,
33
- `bg-stone-500`,
34
- )}>
35
  {isLoaded && <Main />}
36
  </main>
37
  </>
 
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>
 
 
 
 
31
  {isLoaded && <Main />}
32
  </main>
33
  </>
src/app/{globals.css β†’ styles/globals.css} RENAMED
File without changes
src/app/{react-reflex-custom.css β†’ styles/react-reflex-custom.css} RENAMED
@@ -10,29 +10,29 @@ with some custom colors and sizes
10
 
11
  @layer components {
12
  body .reflex-container > .reflex-splitter {
13
- @apply transition-all duration-200 ease-in-out bg-gray-700;
14
  }
15
 
16
  body .reflex-container > .reflex-splitter.active,
17
  body .reflex-container > .reflex-splitter:hover {
18
- @apply transition-all duration-200 ease-in-out bg-gray-400;
19
  }
20
 
21
  body .horizontal > .reflex-splitter {
22
- @apply h-[2px] bg-gray-700 border-t-gray-700 border-b-gray-700;
23
  }
24
 
25
  body .reflex-container.horizontal > .reflex-splitter:hover,
26
  body .reflex-container.horizontal > .reflex-splitter.active {
27
- @apply bg-gray-400 border-t-gray-400 border-b-gray-400;
28
  }
29
 
30
  body .reflex-container.vertical > .reflex-splitter {
31
- @apply w-[2px] bg-gray-700 border-l-gray-700 border-r-gray-700;
32
  }
33
 
34
  body .reflex-container.vertical > .reflex-splitter:hover,
35
  body .reflex-container.vertical > .reflex-splitter.active {
36
- @apply bg-gray-400 border-l-gray-400 border-r-gray-400;
37
  }
38
  }
 
10
 
11
  @layer components {
12
  body .reflex-container > .reflex-splitter {
13
+ @apply transition-all duration-200 ease-in-out bg-stone-800;
14
  }
15
 
16
  body .reflex-container > .reflex-splitter.active,
17
  body .reflex-container > .reflex-splitter:hover {
18
+ @apply transition-all duration-200 ease-in-out bg-stone-400;
19
  }
20
 
21
  body .horizontal > .reflex-splitter {
22
+ @apply h-[2px] bg-stone-800 border-t-stone-900 border-b-stone-800;
23
  }
24
 
25
  body .reflex-container.horizontal > .reflex-splitter:hover,
26
  body .reflex-container.horizontal > .reflex-splitter.active {
27
+ @apply h-[2px] bg-stone-400 border-t-stone-400 border-b-stone-800;
28
  }
29
 
30
  body .reflex-container.vertical > .reflex-splitter {
31
+ @apply w-[2px] bg-stone-800 border-l-stone-800 border-r-stone-800;
32
  }
33
 
34
  body .reflex-container.vertical > .reflex-splitter:hover,
35
  body .reflex-container.vertical > .reflex-splitter.active {
36
+ @apply w-[2px] bg-stone-400 border-l-stone-400 border-r-stone-400;
37
  }
38
  }
src/app/{react-reflex.css β†’ styles/react-reflex.css} RENAMED
File without changes
src/components/form/form-dir.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChangeEvent, useMemo } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { Input } from "../ui/input"
6
+
7
+ import { FormField } from "./form-field"
8
+
9
+ export function FormDir({
10
+ label,
11
+ className,
12
+ placeholder,
13
+ disabled,
14
+ onChange,
15
+ horizontal,
16
+ accept,
17
+ }: {
18
+ label?: string
19
+ className?: string
20
+ placeholder?: string
21
+ disabled?: boolean
22
+ onChange?: (files: File[]) => void
23
+ horizontal?: boolean
24
+ accept?: string
25
+ }) {
26
+
27
+ const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
28
+ if (disabled) {
29
+ return
30
+ }
31
+ if (!onChange) {
32
+ return
33
+ }
34
+
35
+ const files = Array.from(event.currentTarget.files || [])
36
+ onChange(files)
37
+ }, [])
38
+
39
+ return (
40
+ <FormField
41
+ label={
42
+ `${label}:`
43
+ }
44
+ horizontal={horizontal}
45
+ >
46
+ <Input
47
+ placeholder={`${placeholder || ""}`}
48
+ className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
49
+ disabled={disabled}
50
+ onChange={handleChange}
51
+ type="file"
52
+ {...{directory: ""} as any} // saw it in stack overflow, but the type isn't recognized here.. hmm
53
+ webkitdirectory=""
54
+ accept={accept}
55
+ />
56
+ </FormField>
57
+ )
58
+ }
src/components/form/form-field.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { FormLabel } from "./form-label"
6
+
7
+ export function FormField({ label, children, className, horizontal = false }: {
8
+ label?: string
9
+ children?: ReactNode
10
+ className?: string
11
+ horizontal?: boolean
12
+ }) {
13
+ return (
14
+ <div className={cn(
15
+ `flex flex-col space-y-3`,
16
+ `text-base font-thin text-gray-400`,
17
+ horizontal ? '' : 'w-full',
18
+ )}>
19
+ {label && <FormLabel>{label}</FormLabel>}
20
+ <div className={cn(
21
+ `flex`,
22
+ horizontal ? '' : 'w-full',
23
+ className
24
+ )}>
25
+ {children}
26
+ </div>
27
+ </div>
28
+ )
29
+ }
src/components/form/form-file.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChangeEvent, useMemo, useRef } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { Input } from "../ui/input"
6
+
7
+ import { FormField } from "./form-field"
8
+
9
+ export function FormFile({
10
+ label,
11
+ className,
12
+ placeholder,
13
+ disabled,
14
+ onChange,
15
+ horizontal,
16
+ accept
17
+ }: {
18
+ label?: string
19
+ className?: string
20
+ placeholder?: string
21
+ disabled?: boolean
22
+ onChange?: (files: File[]) => void
23
+ horizontal?: boolean
24
+ accept?: string
25
+ }) {
26
+ const ref = useRef<HTMLInputElement>(null)
27
+
28
+ const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
29
+ if (disabled) {
30
+ return
31
+ }
32
+ if (!onChange) {
33
+ return
34
+ }
35
+
36
+ const files = Array.from(event.currentTarget.files || [])
37
+ onChange(files)
38
+ }, [])
39
+
40
+ return (
41
+ <FormField
42
+ label={
43
+ `${label}:`
44
+ }
45
+ horizontal={horizontal}
46
+ >
47
+ <Input
48
+ ref={ref}
49
+ placeholder={`${placeholder || ""}`}
50
+ className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
51
+ disabled={disabled}
52
+ onChange={handleChange}
53
+ type="file"
54
+ accept={accept}
55
+ />
56
+ </FormField>
57
+ )
58
+ }
src/components/form/form-input.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChangeEvent, HTMLInputTypeAttribute, useMemo, useRef } from "react"
2
+
3
+ import { cn, getValidNumber, isValidNumber } from "@/lib/utils"
4
+
5
+ import { Input } from "../ui/input"
6
+
7
+ import { FormField } from "./form-field"
8
+
9
+ export function FormInput<T>({
10
+ label,
11
+ className,
12
+ placeholder,
13
+ value,
14
+ minValue,
15
+ maxValue,
16
+ defaultValue,
17
+ disabled,
18
+ onChange,
19
+ horizontal,
20
+ type,
21
+ // ...props
22
+ }: {
23
+ label?: string
24
+ className?: string
25
+ placeholder?: string
26
+ value?: T
27
+ minValue?: T
28
+ maxValue?: T
29
+ defaultValue?: T
30
+ disabled?: boolean
31
+ onChange?: (newValue: T) => void
32
+ horizontal?: boolean
33
+ type?: HTMLInputTypeAttribute
34
+ }
35
+ // & Omit<ComponentProps<typeof Input>, "value" | "defaultValue" | "placeholder" | "type" | "className" | "disabled" | "onChange">
36
+ // & ComponentProps<typeof Input>
37
+ ) {
38
+
39
+ const isNumberInput =
40
+ typeof defaultValue === "number"
41
+ || typeof value === "number"
42
+
43
+ const isTextInput =
44
+ typeof defaultValue === "string"
45
+ || typeof value === "number"
46
+
47
+ // we try to use the type provided by the user if possible,
48
+ // otherwise we "guess" it
49
+ const inputType: HTMLInputTypeAttribute = type ||
50
+ (
51
+ isNumberInput ? "number"
52
+ : isTextInput ? "text"
53
+ : "text"
54
+ )
55
+
56
+ const ref = useRef<HTMLInputElement>(null)
57
+
58
+ const handleChange = useMemo(() => (event: ChangeEvent<HTMLInputElement>) => {
59
+ if (disabled) {
60
+ return
61
+ }
62
+ if (!onChange) {
63
+ return
64
+ }
65
+
66
+ const rawStringValue = `${event.currentTarget.value || ""}`
67
+
68
+ // this could be refactorer maybe
69
+ if (isNumberInput) {
70
+ const validMinValue: number = minValue && isValidNumber(minValue) ? (minValue as number) : 0
71
+ const validMaxValue: number = maxValue && isValidNumber(maxValue) ? (maxValue as number) : 1
72
+ const validDefaultValue: number = defaultValue && isValidNumber(defaultValue) ? (defaultValue as number) : 0
73
+
74
+ const numericValue = getValidNumber(
75
+ rawStringValue,
76
+ validMinValue,
77
+ validMaxValue,
78
+ validDefaultValue,
79
+ )
80
+ onChange(numericValue as T)
81
+ } else {
82
+ onChange(rawStringValue as T)
83
+ }
84
+ }, [isNumberInput, isTextInput, minValue, maxValue, defaultValue])
85
+
86
+ return (
87
+ <FormField
88
+ label={
89
+ `${label}:`
90
+ }
91
+ horizontal={horizontal}
92
+ >
93
+ <Input
94
+ ref={ref}
95
+ placeholder={`${placeholder || defaultValue || ""}`}
96
+ className={cn(`w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base`, className)}
97
+ disabled={disabled}
98
+ onChange={handleChange}
99
+ // {...props}
100
+ type={inputType}
101
+ value={`${value || defaultValue}`}
102
+
103
+ // since we are controlling the element with value=*, we should not use defaultValue=*
104
+ // defaultValue={`${defaultValue || ""}`}
105
+ />
106
+ </FormField>
107
+ )
108
+ }
src/components/form/form-label.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export function FormLabel({ children, className }: {
6
+ children?: ReactNode
7
+ className?: string
8
+ }) {
9
+ return (
10
+ <label className={cn(`text-sm font-normal text-gray-300/70`, className)}>{
11
+ children
12
+ }</label>
13
+ )
14
+ }
src/components/form/form-radio.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MdOutlineCheckCircle, MdRadioButtonUnchecked } from "react-icons/md"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { FormField } from "./form-field"
6
+
7
+ export function FormRadio({ label, className, selected, items, horizontal }: {
8
+ label?: string
9
+ className?: string
10
+ selected?: string
11
+ items?: { name: string; label: string; disabled?: boolean }[]
12
+ horizontal?: boolean
13
+ }) {
14
+ return (
15
+ <FormField
16
+ label={label}
17
+ className={cn(`flex-row space-x-5`, className)}
18
+ horizontal={horizontal}>
19
+ {items?.map(item => (
20
+ <div key={item.name} className={cn(
21
+ `flex flex-row items-center space-x-2`,
22
+ selected === item.name
23
+ ? `text-gray-200 font-normal`
24
+ : (
25
+ item.disabled ? `text-gray-600 font-normal` : `text-gray-400 font-normal`
26
+ )
27
+ )}>
28
+ {selected === item.name
29
+ ? <MdOutlineCheckCircle className="text-lg" />
30
+ : <MdRadioButtonUnchecked className="text-lg" />}
31
+ <span>{item.label}</span>
32
+ </div>
33
+ ))}
34
+ </FormField>
35
+ )
36
+ }
src/components/form/form-section.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export function FormSection({ label, children, className, horizontal }: {
6
+ label?: string
7
+ children?: ReactNode
8
+ className?: string
9
+ horizontal?: boolean
10
+ }) {
11
+ return (
12
+ <div className={cn(`flex flex-col space-y-4`)}>
13
+ <h2 className="text-2xl font-thin pb-2 text-gray-200">{label}</h2>
14
+ <div className={cn(
15
+ "flex",
16
+ horizontal
17
+ ? "flex-row space-x-3 justify-start"
18
+ : "flex-col space-y-6"
19
+ )}>
20
+ {children}
21
+ </div>
22
+ </div>
23
+ )
24
+ }
src/components/form/form-select.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { cn } from "@/lib/utils"
3
+
4
+ import { FormField } from "./form-field"
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
6
+
7
+ export function FormSelect<T>({
8
+ label,
9
+ className,
10
+ selectedItemId,
11
+ selectedItemLabel,
12
+ defaultItemId,
13
+ defaultItemLabel,
14
+ items = [],
15
+ onSelect,
16
+ horizontal,
17
+ }: {
18
+ label?: string
19
+ className?: string
20
+ selectedItemId?: string
21
+ selectedItemLabel?: string
22
+ defaultItemId?: string
23
+ defaultItemLabel?: string
24
+ items?: {
25
+ id: string
26
+ label: string
27
+ disabled?: boolean
28
+ value: T
29
+ }[]
30
+ onSelect?: (value?: T) => void
31
+ horizontal?: boolean
32
+ }) {
33
+
34
+ return (
35
+ <FormField
36
+ label={
37
+ typeof defaultItemId !== "undefined" && typeof defaultItemLabel !== "undefined"
38
+ ? `${label} (defaults to ${defaultItemLabel || "N.A."}):`
39
+ : `${label}:`
40
+ }
41
+ className={cn(``, className)}
42
+ horizontal={horizontal}>
43
+ <Select
44
+ onValueChange={(newSelectedItemId: string) => {
45
+ if (!onSelect) {
46
+ return
47
+ }
48
+ const selectedItem = items.find(item => item.id === newSelectedItemId)
49
+ onSelect(selectedItem?.value)
50
+ }}
51
+ defaultValue={selectedItemId}>
52
+ <SelectTrigger className="w-full md:w-52 lg:w-56 xl:w-64 font-normal text-base">
53
+ <SelectValue placeholder={selectedItemLabel} />
54
+ </SelectTrigger>
55
+ <SelectContent>
56
+ {items?.map(item =>
57
+ <SelectItem
58
+ key={item.id}
59
+ value={item.id}>{
60
+ item.label
61
+ }</SelectItem>
62
+ )}
63
+ </SelectContent>
64
+ </Select>
65
+ </FormField>
66
+ )
67
+ }
src/components/interface/loader/index.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+
3
+ export function Loader({
4
+ isLoading = false,
5
+ className = ""
6
+ }: {
7
+ isLoading?: boolean
8
+ className?: string
9
+ }) {
10
+ // TODO: read our global state
11
+
12
+ return (
13
+ <div className={cn(`
14
+ fixed
15
+ z-50
16
+ flex
17
+ w-screen h-screen
18
+ top-0 left-0 right-0 bottom-0
19
+ p-0 m-0
20
+ overflow-hidden
21
+ items-center justify-center
22
+
23
+ backdrop-blur-lg
24
+ `,
25
+ isLoading
26
+ ? 'opacity-100 pointer-events-auto'
27
+ : 'pointer-events-none opacity-0',
28
+ className)}
29
+ style={{
30
+ // backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #34353655, #343536 )"
31
+ }}>
32
+ <p
33
+ className={cn("text-stone-100 font-sans font-thin text-[3vw]")}
34
+ style={{ textShadow: "#000 1px 0 3px" }}>
35
+ <span className="">Loading..</span>
36
+ </p>
37
+ </div>
38
+ )
39
+ }
src/components/interface/static-video/index.tsx CHANGED
@@ -3,7 +3,7 @@
3
  import React, { ReactNode, useEffect, useRef } from "react"
4
 
5
  import { useTimelineState } from "@aitube/timeline"
6
- import useRequestAnimationFrame from "@/lib/hooks/useRequestAnimationFrame"
7
 
8
  export function StaticVideo({
9
  video = "",
 
3
  import React, { ReactNode, useEffect, useRef } from "react"
4
 
5
  import { useTimelineState } from "@aitube/timeline"
6
+ import { useRequestAnimationFrame } from "@/lib/hooks/useRequestAnimationFrame"
7
 
8
  export function StaticVideo({
9
  video = "",
src/components/interface/system-menu/index.tsx ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const MenubarMenu = MenubarPrimitive.Menu
10
+
11
+ const MenubarGroup = MenubarPrimitive.Group
12
+
13
+ const MenubarPortal = MenubarPrimitive.Portal
14
+
15
+ const MenubarSub = MenubarPrimitive.Sub
16
+
17
+ const MenubarRadioGroup = MenubarPrimitive.RadioGroup
18
+
19
+ const Menubar = React.forwardRef<
20
+ React.ElementRef<typeof MenubarPrimitive.Root>,
21
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
22
+ >(({ className, ...props }, ref) => (
23
+ <MenubarPrimitive.Root
24
+ ref={ref}
25
+ className={cn(
26
+ "flex h-9 items-center space-x-1 rounded-md border-bottom border-gray-800 bg-white p-1 dark:border-gray-800 dark:bg-stone-800",
27
+ className
28
+ )}
29
+ {...props}
30
+ />
31
+ ))
32
+ Menubar.displayName = MenubarPrimitive.Root.displayName
33
+
34
+ const MenubarTrigger = React.forwardRef<
35
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
36
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
37
+ >(({ className, ...props }, ref) => (
38
+ <MenubarPrimitive.Trigger
39
+ ref={ref}
40
+ className={cn(
41
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-normal outline-none focus:bg-stone-100 focus:text-gray-900 data-[state=open]:bg-stone-100 data-[state=open]:text-gray-900 dark:focus:bg-stone-700 dark:focus:text-gray-50 dark:data-[state=open]:bg-stone-700 dark:data-[state=open]:text-gray-50",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ ))
47
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
48
+
49
+ const MenubarSubTrigger = React.forwardRef<
50
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
51
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
52
+ inset?: boolean
53
+ }
54
+ >(({ className, inset, children, ...props }, ref) => (
55
+ <MenubarPrimitive.SubTrigger
56
+ ref={ref}
57
+ className={cn(
58
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[state=open]:bg-stone-100 data-[state=open]:text-gray-900 dark:focus:bg-stone-700 dark:focus:text-gray-50 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-gray-100",
59
+ inset && "pl-8",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ <ChevronRight className="ml-auto h-4 w-4" />
66
+ </MenubarPrimitive.SubTrigger>
67
+ ))
68
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
69
+
70
+ const MenubarSubContent = React.forwardRef<
71
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
72
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
73
+ >(({ className, ...props }, ref) => (
74
+ <MenubarPrimitive.SubContent
75
+ ref={ref}
76
+ className={cn(
77
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border-transparent bg-white p-1 text-gray-950 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-700 dark:bg-stone-800 dark:text-gray-100",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ))
83
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
84
+
85
+ const MenubarContent = React.forwardRef<
86
+ React.ElementRef<typeof MenubarPrimitive.Content>,
87
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
88
+ >(
89
+ (
90
+ { className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
91
+ ref
92
+ ) => (
93
+ <MenubarPrimitive.Portal>
94
+ <MenubarPrimitive.Content
95
+ ref={ref}
96
+ align={align}
97
+ alignOffset={alignOffset}
98
+ sideOffset={sideOffset}
99
+ className={cn(
100
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-700 dark:bg-stone-800 dark:text-gray-100",
101
+ className
102
+ )}
103
+ {...props}
104
+ />
105
+ </MenubarPrimitive.Portal>
106
+ )
107
+ )
108
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName
109
+
110
+ const MenubarItem = React.forwardRef<
111
+ React.ElementRef<typeof MenubarPrimitive.Item>,
112
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
113
+ inset?: boolean
114
+ }
115
+ >(({ className, inset, ...props }, ref) => (
116
+ <MenubarPrimitive.Item
117
+ ref={ref}
118
+ className={cn(
119
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-50",
120
+ inset && "pl-8",
121
+ className
122
+ )}
123
+ {...props}
124
+ />
125
+ ))
126
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName
127
+
128
+ const MenubarCheckboxItem = React.forwardRef<
129
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
130
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
131
+ >(({ className, children, checked, ...props }, ref) => (
132
+ <MenubarPrimitive.CheckboxItem
133
+ ref={ref}
134
+ className={cn(
135
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-100",
136
+ className
137
+ )}
138
+ checked={checked}
139
+ {...props}
140
+ >
141
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
142
+ <MenubarPrimitive.ItemIndicator>
143
+ <Check className="h-4 w-4" />
144
+ </MenubarPrimitive.ItemIndicator>
145
+ </span>
146
+ {children}
147
+ </MenubarPrimitive.CheckboxItem>
148
+ ))
149
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
150
+
151
+ const MenubarRadioItem = React.forwardRef<
152
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
153
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
154
+ >(({ className, children, ...props }, ref) => (
155
+ <MenubarPrimitive.RadioItem
156
+ ref={ref}
157
+ className={cn(
158
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-stone-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-700 dark:focus:text-gray-100",
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
164
+ <MenubarPrimitive.ItemIndicator>
165
+ <Circle className="h-2 w-2 fill-current" />
166
+ </MenubarPrimitive.ItemIndicator>
167
+ </span>
168
+ {children}
169
+ </MenubarPrimitive.RadioItem>
170
+ ))
171
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
172
+
173
+ const MenubarLabel = React.forwardRef<
174
+ React.ElementRef<typeof MenubarPrimitive.Label>,
175
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
176
+ inset?: boolean
177
+ }
178
+ >(({ className, inset, ...props }, ref) => (
179
+ <MenubarPrimitive.Label
180
+ ref={ref}
181
+ className={cn(
182
+ "px-2 py-1.5 text-sm font-semibold",
183
+ inset && "pl-8",
184
+ className
185
+ )}
186
+ {...props}
187
+ />
188
+ ))
189
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName
190
+
191
+ const MenubarSeparator = React.forwardRef<
192
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
193
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
194
+ >(({ className, ...props }, ref) => (
195
+ <MenubarPrimitive.Separator
196
+ ref={ref}
197
+ className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-700", className)}
198
+ {...props}
199
+ />
200
+ ))
201
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
202
+
203
+ const MenubarShortcut = ({
204
+ className,
205
+ ...props
206
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
207
+ return (
208
+ <span
209
+ className={cn(
210
+ "ml-auto text-xs tracking-widest text-gray-500 dark:text-gray-400",
211
+ className
212
+ )}
213
+ {...props}
214
+ />
215
+ )
216
+ }
217
+ MenubarShortcut.displayname = "MenubarShortcut"
218
+
219
+ export {
220
+ Menubar,
221
+ MenubarMenu,
222
+ MenubarTrigger,
223
+ MenubarContent,
224
+ MenubarItem,
225
+ MenubarSeparator,
226
+ MenubarLabel,
227
+ MenubarCheckboxItem,
228
+ MenubarRadioGroup,
229
+ MenubarRadioItem,
230
+ MenubarPortal,
231
+ MenubarSubContent,
232
+ MenubarSubTrigger,
233
+ MenubarGroup,
234
+ MenubarSub,
235
+ MenubarShortcut,
236
+ }
src/components/interface/timeline/index.tsx CHANGED
@@ -7,7 +7,7 @@ export function Timeline() {
7
  <ClapTimeline
8
  showFPS={false}
9
  className={cn(
10
-
11
  )}
12
  />
13
  )
 
7
  <ClapTimeline
8
  showFPS={false}
9
  className={cn(
10
+ "bg-[rgb(58,54,50)]"
11
  )}
12
  />
13
  )
src/components/interface/top-bar/index.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ClapProject } from "@aitube/clap"
2
+ import { useTimelineState } from "@aitube/timeline"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ import { TopMenu } from "../top-menu"
7
+
8
+
9
+ export function TopBar() {
10
+ const clap: ClapProject = useTimelineState(s => s.clap)
11
+
12
+ return (
13
+ <div className={cn(
14
+ `flex flex-row`,
15
+ `w-full h-10`,
16
+ `bg-stone-900 items-center`,
17
+ `border-b`,
18
+ `border-b-stone-700`,
19
+ )}>
20
+ <TopMenu />
21
+ <div className={cn(
22
+ `flex flex-row flex-grow`,
23
+ `items-center justify-center`,
24
+ `text-xs text-stone-300`
25
+ )}>
26
+ {clap?.meta?.title || "Untitled"}
27
+ </div>
28
+ </div>
29
+ )
30
+ }
src/components/interface/top-menu/file/index.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+ import { useEffect } from "react"
3
+ import { useTimelineState } from "@aitube/timeline"
4
+ import { useHotkeys } from "react-hotkeys-hook"
5
+ import { parseClap } from "@aitube/clap"
6
+
7
+ import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from "@/components/ui/menubar"
8
+ import { useClapFilePicker, useQueryStringParams, useScreenplayFilePicker } from "@/lib/hooks"
9
+ import { Loader } from "../../loader"
10
+
11
+ export function TopMenuFile() {
12
+ const { clapUrl } = useQueryStringParams({
13
+ // clapUrl: `/samples/test.clap`,
14
+ // clapUrl: `/samples/Afterglow%20v10%20X%20Rewrite%20Bryan%20E.%20Harris%202023.clap`,
15
+ clapUrl: '',
16
+ })
17
+
18
+ const isTimelineLoading: boolean = useTimelineState(s => s.isLoading)
19
+ const setClap = useTimelineState(s => s.setClap)
20
+ const saveClapAs = useTimelineState(s => s.saveClapAs)
21
+
22
+ const clapPicker = useClapFilePicker()
23
+ const screenplayPicker = useScreenplayFilePicker()
24
+
25
+ const isLoading = isTimelineLoading || clapPicker.isLoading || screenplayPicker.isLoading
26
+
27
+ // const setShowSettings = useInterfaceSettings(s => s.setShowSettings)
28
+ useHotkeys('ctrl+o', () => clapPicker.openFilePicker(), { preventDefault: true }, [])
29
+ useHotkeys('meta+o', () => clapPicker.openFilePicker(), { preventDefault: true }, [])
30
+ useHotkeys('ctrl+s', () => saveClapAs({ embedded: true }), { preventDefault: true }, [])
31
+ useHotkeys('meta+s', () => saveClapAs({ embedded: true }), { preventDefault: true }, [])
32
+
33
+ useEffect(() => {
34
+ (async () => {
35
+ if (!clapUrl) {
36
+ console.log("No clap URL provided")
37
+ return
38
+ }
39
+ const res = await fetch(clapUrl)
40
+ const blob = await res.blob()
41
+ const clap = await parseClap(blob)
42
+ await setClap(clap)
43
+ })()
44
+ }, [clapUrl])
45
+
46
+ return (
47
+ <>
48
+ <MenubarMenu>
49
+ <MenubarTrigger>File</MenubarTrigger>
50
+ <MenubarContent>
51
+ <MenubarItem onClick={() => {
52
+ clapPicker.openFilePicker()
53
+ }}>
54
+ Open project (.clap)<MenubarShortcut>⌘O</MenubarShortcut>
55
+ </MenubarItem>
56
+ <MenubarItem onClick={() => {
57
+ saveClapAs({ embedded: true })
58
+ }}>
59
+ Save project (.clap)<MenubarShortcut>⌘S</MenubarShortcut>
60
+ </MenubarItem>
61
+ <MenubarSeparator />
62
+ <MenubarItem onClick={() => {
63
+ screenplayPicker.openFilePicker()
64
+ }}>
65
+ Import screenplay (.txt)
66
+ </MenubarItem>
67
+ </MenubarContent>
68
+ </MenubarMenu>
69
+ <Loader isLoading={isLoading} />
70
+ </>
71
+ )
72
+ }
src/components/interface/top-menu/index.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Menubar } from "@/components/ui/menubar"
2
+
3
+ import { TopMenuFile } from "./file"
4
+ import { TopMenuView } from "./view"
5
+ import { TopMenuRendering } from "./rendering"
6
+
7
+ export function TopMenu() {
8
+ return (
9
+ <Menubar>
10
+ <TopMenuFile />
11
+ {/*
12
+ <TopMenuEdit />
13
+ <TopMenuPlayback />
14
+ <TopMenuAssistant />
15
+ */}
16
+ <TopMenuRendering />
17
+ {/*<TopMenuView />*/}
18
+ </Menubar>
19
+ )
20
+ }
src/components/interface/top-menu/rendering/index.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+
5
+ import {
6
+ MenubarCheckboxItem,
7
+ MenubarContent,
8
+ MenubarItem,
9
+ MenubarMenu,
10
+ MenubarSeparator,
11
+ MenubarSub,
12
+ MenubarSubContent,
13
+ MenubarSubTrigger,
14
+ MenubarTrigger
15
+ } from "@/components/ui/menubar"
16
+ import { useSettingsRendering } from "@/settings/rendering"
17
+ import { RenderingStrategy } from "@/types"
18
+
19
+ export function TopMenuRendering() {
20
+
21
+ const storyboardRenderingStrategy = useSettingsRendering((s) => s.storyboardRenderingStrategy)
22
+ const setStoryboardRenderingStrategy = useSettingsRendering((s) => s.setStoryboardRenderingStrategy)
23
+
24
+ const videoRenderingStrategy = useSettingsRendering((s) => s.videoRenderingStrategy)
25
+ const setVideoRenderingStrategy = useSettingsRendering((s) => s.setVideoRenderingStrategy)
26
+
27
+ const labels = {
28
+ [RenderingStrategy.ON_DEMAND]: "Render on click",
29
+ [RenderingStrategy.ON_SCREEN_ONLY]: "Render visible items",
30
+ [RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]: "Render visible + surrounding items",
31
+ [RenderingStrategy.ON_SCREEN_THEN_ALL]: "Full rendering (for GPU-rich people)"
32
+ }
33
+
34
+ return (
35
+ <MenubarMenu>
36
+ <MenubarTrigger>Rendering</MenubarTrigger>
37
+ <MenubarContent>
38
+
39
+ <MenubarSub>
40
+ <MenubarSubTrigger>Storyboards generation</MenubarSubTrigger>
41
+ <MenubarSubContent>
42
+ <MenubarCheckboxItem
43
+ disabled
44
+ checked={storyboardRenderingStrategy === RenderingStrategy.ON_DEMAND}
45
+ onClick={(e) => {
46
+ setStoryboardRenderingStrategy(RenderingStrategy.ON_DEMAND)
47
+ e.stopPropagation()
48
+ e.preventDefault()
49
+ return false
50
+ }}>
51
+ {labels[RenderingStrategy.ON_DEMAND]}
52
+ </MenubarCheckboxItem>
53
+ <MenubarCheckboxItem
54
+ disabled
55
+ checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_ONLY}
56
+ onClick={(e) => {
57
+ setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_ONLY)
58
+ e.stopPropagation()
59
+ e.preventDefault()
60
+ return false
61
+ }}>
62
+ {labels[RenderingStrategy.ON_SCREEN_ONLY]}
63
+ </MenubarCheckboxItem>
64
+ <MenubarCheckboxItem
65
+ disabled
66
+ checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_SURROUNDING}
67
+ onClick={(e) => {
68
+ setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_SURROUNDING)
69
+ e.stopPropagation()
70
+ e.preventDefault()
71
+ return false
72
+ }}>
73
+ {labels[RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]}
74
+ </MenubarCheckboxItem>
75
+ <MenubarCheckboxItem
76
+ disabled
77
+ checked={storyboardRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_ALL}
78
+ onClick={(e) => {
79
+ setStoryboardRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_ALL)
80
+ e.stopPropagation()
81
+ e.preventDefault()
82
+ return false
83
+ }}>
84
+ {labels[RenderingStrategy.ON_SCREEN_THEN_ALL]}
85
+ </MenubarCheckboxItem>
86
+ </MenubarSubContent>
87
+ </MenubarSub>
88
+
89
+ <MenubarSub>
90
+ <MenubarSubTrigger>Videos generation</MenubarSubTrigger>
91
+ <MenubarSubContent>
92
+ <MenubarCheckboxItem
93
+ disabled
94
+ checked={videoRenderingStrategy === RenderingStrategy.ON_DEMAND}
95
+ onClick={(e) => {
96
+ setVideoRenderingStrategy(RenderingStrategy.ON_DEMAND)
97
+ e.stopPropagation()
98
+ e.preventDefault()
99
+ return false
100
+ }}>
101
+ {labels[RenderingStrategy.ON_DEMAND]}
102
+ </MenubarCheckboxItem>
103
+ <MenubarCheckboxItem
104
+ disabled
105
+ checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_ONLY}
106
+ onClick={(e) => {
107
+ setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_ONLY)
108
+ e.stopPropagation()
109
+ e.preventDefault()
110
+ return false
111
+ }}>
112
+ {labels[RenderingStrategy.ON_SCREEN_ONLY]}
113
+ </MenubarCheckboxItem>
114
+ <MenubarCheckboxItem
115
+ disabled
116
+ checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_SURROUNDING}
117
+ onClick={(e) => {
118
+ setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_SURROUNDING)
119
+ e.stopPropagation()
120
+ e.preventDefault()
121
+ return false
122
+ }}>
123
+ {labels[RenderingStrategy.ON_SCREEN_THEN_SURROUNDING]}
124
+ </MenubarCheckboxItem>
125
+ <MenubarCheckboxItem
126
+ disabled
127
+ checked={videoRenderingStrategy === RenderingStrategy.ON_SCREEN_THEN_ALL}
128
+ onClick={(e) => {
129
+ setVideoRenderingStrategy(RenderingStrategy.ON_SCREEN_THEN_ALL)
130
+ e.stopPropagation()
131
+ e.preventDefault()
132
+ return false
133
+ }}>
134
+ {labels[RenderingStrategy.ON_SCREEN_THEN_ALL]}
135
+ </MenubarCheckboxItem>
136
+ </MenubarSubContent>
137
+ </MenubarSub>
138
+ </MenubarContent>
139
+ </MenubarMenu>
140
+ )
141
+ }
src/components/interface/top-menu/view/index.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useEffect } from "react"
4
+
5
+ import {
6
+ MenubarCheckboxItem,
7
+ MenubarContent,
8
+ MenubarMenu,
9
+ MenubarSeparator,
10
+ MenubarTrigger
11
+ } from "@/components/ui/menubar"
12
+ import { useFullscreenStatus } from "@/lib/hooks"
13
+ import { useSettingsView } from "@/settings/view"
14
+
15
+ export function TopMenuView() {
16
+ const [isFullscreen, setFullscreen, ref] = useFullscreenStatus()
17
+
18
+ // we want the whole body to become fullscreen
19
+ // TODO: use pointer lock, to prevent the mouse from going up
20
+ useEffect(() => {
21
+ if (typeof window !== "undefined") {
22
+ ref.current = document.body
23
+ }
24
+ }, [])
25
+
26
+ const showTimeline = useSettingsView((s) => s.showTimeline)
27
+ const setShowTimeline = useSettingsView((s) => s.setShowTimeline)
28
+
29
+ const showExplorer = useSettingsView((s) => s.showExplorer)
30
+ const setShowExplorer = useSettingsView((s) => s.setShowExplorer)
31
+
32
+ const showChat = useSettingsView((s) => s.showChat)
33
+ const setShowChat = useSettingsView((s) => s.setShowChat)
34
+
35
+ const showVideoPlayer = useSettingsView((s) => s.showVideoPlayer)
36
+ const setShowVideoPlayer = useSettingsView((s) => s.setShowVideoPlayer)
37
+
38
+ return (
39
+ <MenubarMenu>
40
+ <MenubarTrigger>View</MenubarTrigger>
41
+ <MenubarContent>
42
+ <MenubarCheckboxItem
43
+ checked={isFullscreen}
44
+ onClick={(e) => {
45
+ // currently isFullscreen is a bit buggy and might not reflect the correct value
46
+ // setFullscreen(!isFullscreen)
47
+
48
+ // so to be sure we use setFullscreen in "toggle" mode
49
+ // (ie. we don't pass a boolean, so it will act as a current value switch)
50
+ setFullscreen()
51
+
52
+ e.stopPropagation()
53
+ e.preventDefault()
54
+ return false
55
+ }}>
56
+ Toggle fullscreen
57
+ </MenubarCheckboxItem>
58
+ <MenubarSeparator />
59
+ <MenubarCheckboxItem
60
+ checked={showTimeline}
61
+ onClick={(e) => {
62
+ setShowTimeline(!showTimeline)
63
+ e.stopPropagation()
64
+ e.preventDefault()
65
+ return false
66
+ }}
67
+ >Show timeline</MenubarCheckboxItem>
68
+ <MenubarCheckboxItem
69
+ checked={showExplorer}
70
+ onClick={(e) => {
71
+ setShowExplorer(!showExplorer)
72
+ e.stopPropagation()
73
+ e.preventDefault()
74
+ return false
75
+ }}
76
+ >Show asset explorer</MenubarCheckboxItem>
77
+ <MenubarCheckboxItem
78
+ checked={showChat}
79
+ onClick={(e) => {
80
+ setShowChat(!showChat)
81
+ e.stopPropagation()
82
+ e.preventDefault()
83
+ return false
84
+ }}
85
+ >Show chat assistant</MenubarCheckboxItem>
86
+ <MenubarCheckboxItem
87
+ checked={showVideoPlayer}
88
+ onClick={(e) => {
89
+ setShowVideoPlayer(!showVideoPlayer)
90
+ e.stopPropagation()
91
+ e.preventDefault()
92
+ return false
93
+ }}
94
+ >Show video player</MenubarCheckboxItem>
95
+ </MenubarContent>
96
+ </MenubarMenu>
97
+ )
98
+ }
src/components/settings/SettingsSectionRendering.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FormSection } from "@/components/form/form-section"
2
+ import { useSettingsRendering } from "@/settings/rendering"
3
+
4
+ export function SettingsSectionRendering() {
5
+
6
+ const comfyUiApiVendor = useSettingsRendering(s => s.comfyUiApiVendor)
7
+ const comfyUiApiKey = useSettingsRendering(s => s.comfyUiApiKey)
8
+ const storyboardRenderingStrategy = useSettingsRendering(s => s.storyboardRenderingStrategy)
9
+ const videoRenderingStrategy = useSettingsRendering(s => s.videoRenderingStrategy)
10
+ const maxNbAssetsToGenerateInParallel = useSettingsRendering(s => s.maxNbAssetsToGenerateInParallel)
11
+
12
+ return (
13
+ <div className="flex flex-col space-y-6 justify-between">
14
+ <FormSection label="TODO">
15
+ <p>TODO</p>
16
+ </FormSection>
17
+ </div>
18
+ )
19
+ }
src/components/settings/index.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useTransition } from "react"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog"
5
+ import { ScrollArea } from "@/components/ui/scroll-area"
6
+ import { cn } from "@/lib/utils"
7
+ import { useSettingsView } from "@/settings/view"
8
+
9
+ import { SettingsSectionRendering } from "./SettingsSectionRendering"
10
+
11
+ export function SettingsDialog() {
12
+
13
+ const showSettings = useSettingsView(s => s.showSettings)
14
+ const setShowSettings = useSettingsView(s => s.setShowSettings)
15
+ const [_isPending, startTransition] = useTransition()
16
+
17
+ const panels = {
18
+ rendering: <SettingsSectionRendering />,
19
+ }
20
+
21
+ const panelLabels = {
22
+ rendering: "Rendering",
23
+ } as any
24
+
25
+ const [configPanel, setConfigPanel] = useState<keyof typeof panels>("rendering")
26
+
27
+ return (
28
+ <Dialog open={showSettings} onOpenChange={setShowSettings}>
29
+ <DialogContent className={cn(
30
+ // DialogContent comes with some hardcoded values so we need to override them
31
+ `w-[95w] md:w-[85vw] max-w-6xl h-[80%]`,
32
+ `flex flex-row`
33
+ )}>
34
+ <ScrollArea className="flex flex-col h-full w-44">
35
+ <div className="flex flex-col items-end">
36
+ {Object.keys(panels).map(key => (
37
+ <Button
38
+ key={key}
39
+ variant="ghost"
40
+ className="flex flex-col capitalize w-full items-end text-right text-md"
41
+ onClick={() => setConfigPanel(key as keyof typeof panels)}>{panelLabels[key]}</Button>
42
+ ))}
43
+ </div>
44
+ </ScrollArea>
45
+
46
+ <div className="flex flex-col h-full flex-grow justify-between max-w-[calc(100%-200px)]">
47
+ <ScrollArea className="flex flex-row h-full">
48
+ {panels[configPanel]}
49
+ </ScrollArea>
50
+ <DialogFooter className="text-gray-800">
51
+ <Button onClick={() => { setShowSettings(false) }}>Close</Button>
52
+ </DialogFooter>
53
+ </div>
54
+ </DialogContent>
55
+ </Dialog>
56
+ )
57
+ }
src/components/ui/menubar.tsx CHANGED
@@ -23,7 +23,7 @@ const Menubar = React.forwardRef<
23
  <MenubarPrimitive.Root
24
  ref={ref}
25
  className={cn(
26
- "flex h-10 items-center space-x-1 rounded-md border border-stone-200 bg-white p-1 dark:border-stone-800 dark:bg-stone-950",
27
  className
28
  )}
29
  {...props}
@@ -74,7 +74,7 @@ const MenubarSubContent = React.forwardRef<
74
  <MenubarPrimitive.SubContent
75
  ref={ref}
76
  className={cn(
77
- "z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
78
  className
79
  )}
80
  {...props}
@@ -97,7 +97,7 @@ const MenubarContent = React.forwardRef<
97
  alignOffset={alignOffset}
98
  sideOffset={sideOffset}
99
  className={cn(
100
- "z-50 min-w-[12rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
101
  className
102
  )}
103
  {...props}
 
23
  <MenubarPrimitive.Root
24
  ref={ref}
25
  className={cn(
26
+ "flex h-10 items-center space-x-1 rounded-md border border-stone-200 bg-white p-1 dark:border-stone-900 dark:bg-stone-900",
27
  className
28
  )}
29
  {...props}
 
74
  <MenubarPrimitive.SubContent
75
  ref={ref}
76
  className={cn(
77
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-900 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-900 dark:text-stone-50",
78
  className
79
  )}
80
  {...props}
 
97
  alignOffset={alignOffset}
98
  sideOffset={sideOffset}
99
  className={cn(
100
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-900 shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-900 dark:text-stone-50",
101
  className
102
  )}
103
  {...props}
src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const ScrollArea = React.forwardRef<
9
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <ScrollAreaPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative overflow-hidden", className)}
15
+ {...props}
16
+ >
17
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
18
+ {children}
19
+ </ScrollAreaPrimitive.Viewport>
20
+ <ScrollBar />
21
+ <ScrollAreaPrimitive.Corner />
22
+ </ScrollAreaPrimitive.Root>
23
+ ))
24
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25
+
26
+ const ScrollBar = React.forwardRef<
27
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
28
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
29
+ >(({ className, orientation = "vertical", ...props }, ref) => (
30
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
31
+ ref={ref}
32
+ orientation={orientation}
33
+ className={cn(
34
+ "flex touch-none select-none transition-colors",
35
+ orientation === "vertical" &&
36
+ "h-full w-2.5 border-l border-l-transparent p-[1px]",
37
+ orientation === "horizontal" &&
38
+ "h-2.5 flex-col border-t border-t-transparent p-[1px]",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-stone-200 dark:bg-stone-800" />
44
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
45
+ ))
46
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47
+
48
+ export { ScrollArea, ScrollBar }
src/lib/core/DEPRECATED_getSettings.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getValidNumber } from "@aitube/clap"
2
+
3
+ import { ComfyVendor, Settings } from "@/types"
4
+ import { localStorageKeys } from "@/components/interface/settings/constants"
5
+
6
+ import { getValidString, parseRenderingStrategy } from "../utils"
7
+
8
+ import { getDefaultSettings } from "./getDefaultSettings"
9
+ import { HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL } from "./constants"
10
+
11
+ export function getSettings(): Settings {
12
+ const defaultSettings = getDefaultSettings()
13
+ try {
14
+ return {
15
+ comfyVendor: getValidString(localStorage?.getItem?.(localStorageKeys.comfyVendor), defaultSettings.comfyVendor) as ComfyVendor,
16
+ comfyApiKey: getValidString(localStorage?.getItem?.(localStorageKeys.comfyApiKey), defaultSettings.comfyApiKey),
17
+ storyboardGenerationStrategy: parseRenderingStrategy(localStorage?.getItem?.(localStorageKeys.storyboardGenerationStrategy), defaultSettings.storyboardGenerationStrategy),
18
+ videoGenerationStrategy: parseRenderingStrategy(localStorage?.getItem?.(localStorageKeys.videoGenerationStrategy), defaultSettings.videoGenerationStrategy),
19
+ maxNbAssetsToGenerateInParallel: getValidNumber(localStorage?.getItem?.(localStorageKeys.maxNbAssetsToGenerateInParallel), 1, HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL, defaultSettings.maxNbAssetsToGenerateInParallel),
20
+ }
21
+ } catch (err) {
22
+ return {
23
+ ...defaultSettings
24
+ }
25
+ }
26
+ }
src/{components/interface/settings/constants.tsx β†’ lib/core/DEPRECATED_localStorageKeys.txt} RENAMED
File without changes
src/lib/core/constants.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ // do we really want a high value here?
3
+ // who is seriously ready to spawn 32 GPUs in parallels for this?
4
+ export const HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL = 32
src/lib/core/getDefaultSettings.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { AssetGenerationStrategy, ComfyVendor, Settings } from "@/types"
2
 
3
  export function getDefaultSettings(): Settings {
4
  return {
5
  comfyVendor: ComfyVendor.CUSTOM,
6
  comfyApiKey: "",
7
 
8
- storyboardGenerationStrategy: AssetGenerationStrategy.ON_DEMAND,
9
- videoGenerationStrategy: AssetGenerationStrategy.ON_DEMAND,
10
 
11
  maxNbAssetsToGenerateInParallel: 1,
12
  }
 
1
+ import { RenderingStrategy, ComfyVendor, Settings } from "@/types"
2
 
3
  export function getDefaultSettings(): Settings {
4
  return {
5
  comfyVendor: ComfyVendor.CUSTOM,
6
  comfyApiKey: "",
7
 
8
+ storyboardGenerationStrategy: RenderingStrategy.ON_DEMAND,
9
+ videoGenerationStrategy: RenderingStrategy.ON_DEMAND,
10
 
11
  maxNbAssetsToGenerateInParallel: 1,
12
  }
src/lib/hooks/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export { useDebounce } from "./useDebounce"
2
+ export { useClapFilePicker } from "./useClapFilePicker"
3
+ export { useFullscreenStatus } from "./useFullscreenStatus"
4
+ export { useQueryStringParams } from "./useQueryStringParams"
5
+ export { useRequestAnimationFrame } from "./useRequestAnimationFrame"
6
+ export { useScreenplayFilePicker } from "./useScreenplayFilePicker"
src/lib/hooks/useClapFilePicker.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react"
2
+ import { useFilePicker } from "use-file-picker"
3
+ import { parseClap } from "@aitube/clap"
4
+ import { useTimelineState } from "@aitube/timeline"
5
+
6
+
7
+ export function useClapFilePicker() {
8
+ const setClap = useTimelineState(s => s.setClap)
9
+ const [isLoading, setIsLoading] = useState(false)
10
+
11
+ const { openFilePicker, filesContent, loading } = useFilePicker({
12
+ accept: '.clap',
13
+ readAs: "ArrayBuffer"
14
+ })
15
+
16
+ const fileData = filesContent[0]
17
+
18
+ useEffect(() => {
19
+ const fn = async () => {
20
+ if (fileData?.name) {
21
+ try {
22
+ setIsLoading(true)
23
+ const blob = new Blob([fileData.content])
24
+ const clap = await parseClap(blob)
25
+ await setClap(clap)
26
+ } catch (err) {
27
+ console.error("failed to load the Clap file:", err)
28
+ } finally {
29
+ setIsLoading(false)
30
+ }
31
+ }
32
+ }
33
+ fn()
34
+ }, [fileData?.name])
35
+
36
+ return { openFilePicker, filesContent, fileData, isLoading: loading || isLoading }
37
+ }
src/lib/hooks/useFullscreenStatus.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React, { useState, useLayoutEffect, useRef, MutableRefObject } from 'react'
4
+
5
+ interface FullscreenElement {
6
+ fullscreenElement?: Element;
7
+ mozFullScreenElement?: Element;
8
+ msFullscreenElement?: Element;
9
+ webkitFullscreenElement?: Element;
10
+ }
11
+
12
+ declare global {
13
+ interface Document extends FullscreenElement {}
14
+ }
15
+
16
+ export function useFullscreenStatus(): [boolean, (requestedValue?: boolean) => void, MutableRefObject<Element | null>] {
17
+ const elRef = useRef<Element | null>(null);
18
+ const [isFullscreen, setIsFullscreen] = useState(
19
+ typeof document !== "undefined"
20
+ ? Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()])
21
+ : false
22
+ );
23
+
24
+ const setFullscreen = (maybeValue?: boolean) => {
25
+ if (!elRef.current) return;
26
+
27
+ const isFullScreen = typeof document !== "undefined" ? Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()]) : false
28
+
29
+ const shouldBeFullScreen = typeof maybeValue === "boolean" ? maybeValue : !isFullScreen
30
+
31
+ // nothing to do
32
+ if (isFullScreen === shouldBeFullScreen) {
33
+ return
34
+ }
35
+
36
+ const operation = shouldBeFullScreen
37
+ ? elRef.current.requestFullscreen()
38
+ : (typeof document !== "undefined" ? document.exitFullscreen() : Promise.resolve(true))
39
+
40
+ operation.then(() => {
41
+ setIsFullscreen(shouldBeFullScreen);
42
+ })
43
+ .catch(() => {
44
+ if (typeof document !== "undefined") {
45
+ // make sure we grab a fresh value here
46
+ const isFullScreen = Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()])
47
+ setIsFullscreen(isFullScreen);
48
+ }
49
+ });
50
+ };
51
+
52
+ useLayoutEffect(() => {
53
+ if (typeof document !== "undefined") {
54
+ document.onfullscreenchange = () =>
55
+ setIsFullscreen(Boolean((document as FullscreenElement)[getBrowserFullscreenElementProp()]));
56
+
57
+ return () => {
58
+ document.onfullscreenchange = null;
59
+ };
60
+ }
61
+ }, []);
62
+
63
+ return [isFullscreen, setFullscreen, elRef];
64
+ }
65
+
66
+ function getBrowserFullscreenElementProp(): keyof FullscreenElement {
67
+ if (typeof document.fullscreenElement !== "undefined") {
68
+ return "fullscreenElement";
69
+ } else if (typeof document.mozFullScreenElement !== "undefined") {
70
+ return "mozFullScreenElement";
71
+ } else if (typeof document.msFullscreenElement !== "undefined") {
72
+ return "msFullscreenElement";
73
+ } else if (typeof document.webkitFullscreenElement !== "undefined") {
74
+ return "webkitFullscreenElement";
75
+ } else {
76
+ throw new Error("fullscreenElement is not supported by this browser");
77
+ }
78
+ }
src/lib/hooks/useRequestAnimationFrame.ts CHANGED
@@ -12,7 +12,7 @@ type Config = {
12
  *
13
  * @description keep your `nextAnimationFrameHandler` as simple and performant as possible with the least amount of dependencies and transformations. It will be called frequently and this can lead to bad UX
14
  */
15
- const useRequestAnimationFrame = (
16
  nextAnimationFrameHandler: (progress: number) => void,
17
  {
18
  duration = Number.POSITIVE_INFINITY,
@@ -60,4 +60,3 @@ const useRequestAnimationFrame = (
60
  }, [shouldAnimate])
61
  }
62
 
63
- export default useRequestAnimationFrame
 
12
  *
13
  * @description keep your `nextAnimationFrameHandler` as simple and performant as possible with the least amount of dependencies and transformations. It will be called frequently and this can lead to bad UX
14
  */
15
+ export const useRequestAnimationFrame = (
16
  nextAnimationFrameHandler: (progress: number) => void,
17
  {
18
  duration = Number.POSITIVE_INFINITY,
 
60
  }, [shouldAnimate])
61
  }
62
 
 
src/lib/hooks/useScreenplayFilePicker.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react"
2
+ import { useTimelineState } from "@aitube/timeline"
3
+ import { parseClap } from "@aitube/clap"
4
+ import { useFilePicker } from "use-file-picker"
5
+
6
+ export function useScreenplayFilePicker() {
7
+ const setClap = useTimelineState(s => s.setClap)
8
+ const [isLoading, setIsLoading] = useState(false)
9
+
10
+ const { openFilePicker, filesContent, loading } = useFilePicker({
11
+ accept: '.txt',
12
+ readAs: "Text"
13
+ })
14
+
15
+ const fileData = filesContent[0]
16
+
17
+ useEffect(() => {
18
+ const fn = async () => {
19
+ if (fileData?.name) {
20
+ try {
21
+ setIsLoading(true)
22
+ const res = await fetch("https://jbilcke-hf-broadway-api.hf.space", {
23
+ method: "POST",
24
+ headers: { 'Content-Type': 'text/plain' },
25
+ body: fileData.content,
26
+ })
27
+ const blob = await res.blob()
28
+ const clap = await parseClap(blob)
29
+ await setClap(clap)
30
+ } catch (err) {
31
+ console.error("failed to import the screenplay:", err)
32
+ } finally {
33
+ setIsLoading(false)
34
+ }
35
+ }
36
+ }
37
+ fn()
38
+ }, [fileData?.name])
39
+
40
+ return { openFilePicker, filesContent, fileData, isLoading: loading || isLoading }
41
+ }
src/lib/utils/getValidBoolean.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export const getValidBoolean = (something: any, defaultValue: boolean) => {
2
+ if (typeof something === "boolean") {
3
+ return something
4
+ }
5
+
6
+ const strValue = `${something || defaultValue}`.toLowerCase()
7
+
8
+ return strValue === "true" || strValue === "1" || strValue === "on"
9
+ }
src/lib/utils/getValidNumber.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export const getValidNumber = (something: any, minValue: number, maxValue: number, defaultValue: number) => {
2
+ const strValue = `${something || defaultValue}`
3
+ const numValue = Number(strValue)
4
+ const isValid = !isNaN(numValue) && isFinite(numValue)
5
+ if (!isValid) {
6
+ return defaultValue
7
+ }
8
+ return Math.max(minValue, Math.min(maxValue, numValue))
9
+
10
+ }
src/lib/utils/getValidString.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export function getValidString(something: any, defaultValue: string) {
2
+ const strValue = `${something || defaultValue}`
3
+ try {
4
+ return JSON.parse(strValue)
5
+ } catch (err) {
6
+ return strValue
7
+ }
8
+ }
src/lib/utils/index.ts CHANGED
@@ -1 +1,9 @@
1
- export { cn } from "./cn"
 
 
 
 
 
 
 
 
 
1
+ export { cn } from "./cn"
2
+ export { debounceAsync } from "./debounceAsync"
3
+ export { debounceSync } from "./debounceSync"
4
+ export { getValidBoolean } from "./getValidBoolean"
5
+ export { getValidNumber } from "./getValidNumber"
6
+ export { getValidString } from "./getValidString"
7
+ export { isValidNumber } from "./isValidNumber"
8
+ export { parseComfyVendor } from "./parseComfyVendor"
9
+ export { parseRenderingStrategy } from "./parseRenderingStrategy"
src/lib/utils/isValidNumber.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function isValidNumber(input?: any) {
2
+ return typeof input === "number" && !isNaN(input) && isFinite(input)
3
+ }
src/lib/utils/parseComfyVendor.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComfyVendor } from "@/types"
2
+
3
+ export function parseComfyVendor(input: any, defaultVendor?: ComfyVendor): ComfyVendor {
4
+
5
+ let unknownString = `${input || ""}`.trim()
6
+
7
+ // the "normal" case
8
+ if (Object.values(ComfyVendor).includes(unknownString as ComfyVendor)) {
9
+ return unknownString as ComfyVendor
10
+ }
11
+
12
+ let vendor: ComfyVendor = defaultVendor || ComfyVendor.NONE
13
+
14
+ unknownString = unknownString.toLowerCase()
15
+
16
+ if (unknownString === "none" || unknownString === "undefined" || unknownString === "") {
17
+ vendor = ComfyVendor.NONE
18
+ }
19
+ else if (unknownString === "huggingface" || unknownString === "hugging_face") {
20
+ vendor = ComfyVendor.HUGGINGFACE
21
+ }
22
+ else if (unknownString === "replicate") {
23
+ vendor = ComfyVendor.REPLICATE
24
+ } else {
25
+ vendor = ComfyVendor.NONE
26
+ }
27
+ return vendor
28
+ }
src/lib/utils/parseRenderingStrategy.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RenderingStrategy } from "@/types"
2
+
3
+ export function parseRenderingStrategy(input: any, defaultStrategy?: RenderingStrategy): RenderingStrategy {
4
+
5
+ let unknownString = `${input || ""}`.trim()
6
+
7
+ // the "normal" case
8
+ if (Object.values(RenderingStrategy).includes(unknownString as RenderingStrategy)) {
9
+ return unknownString as RenderingStrategy
10
+ }
11
+
12
+ let strategy: RenderingStrategy = defaultStrategy || RenderingStrategy.ON_DEMAND
13
+
14
+ unknownString = unknownString.toLowerCase()
15
+
16
+ if (unknownString === "on_demand") {
17
+ strategy = RenderingStrategy.ON_DEMAND
18
+ }
19
+ else if (unknownString === "on_screen_only") {
20
+ strategy = RenderingStrategy.ON_SCREEN_ONLY
21
+ }
22
+ else if (unknownString === "on_screen_then_surrounding") {
23
+ strategy = RenderingStrategy.ON_SCREEN_THEN_SURROUNDING
24
+ }
25
+ else if (unknownString === "on_screen_then_all") {
26
+ strategy = RenderingStrategy.ON_SCREEN_THEN_ALL
27
+ } else {
28
+ strategy = RenderingStrategy.ON_DEMAND
29
+ }
30
+ return strategy
31
+ }
src/server/comfy/{formatStoryboardWorkflow.ts β†’ getComfyWorkflow.ts} RENAMED
@@ -1,12 +1,23 @@
 
1
  import { getVideoPrompt } from "@aitube/engine"
2
 
3
  import { ComfyNode, RenderRequest } from "@/types"
4
 
5
- export function formatStoryboardWorkflow(request: RenderRequest) {
 
 
 
 
 
 
 
 
 
 
6
  // parse the node array from the ComfyUI workflow
7
- const nodes = Object.values(JSON.parse(request.comfyWorkflow)) as ComfyNode[]
8
 
9
- const storyboardPrompt = getVideoPrompt(
10
  request.segments,
11
  request.entities
12
  )
@@ -14,7 +25,7 @@ export function formatStoryboardWorkflow(request: RenderRequest) {
14
  for (const node of nodes) {
15
  if (typeof node.inputs.text === "string") {
16
  if (node._meta.title.includes("Prompt")) {
17
- node.inputs.text = storyboardPrompt
18
  }
19
  }
20
  }
 
1
+ import { ClapSegmentCategory } from "@aitube/clap"
2
  import { getVideoPrompt } from "@aitube/engine"
3
 
4
  import { ComfyNode, RenderRequest } from "@/types"
5
 
6
+ // TODO move this to @aitube/engine or @aitube/engine-comfy
7
+ export function getComfyWorkflow(request: RenderRequest) {
8
+
9
+ let comfyWorkflow = "{}"
10
+
11
+ if (request.segment.category === ClapSegmentCategory.STORYBOARD) {
12
+ comfyWorkflow = request.storyboardWorkflow
13
+ } else if (request.segment.category === ClapSegmentCategory.VIDEO) {
14
+ comfyWorkflow = request.videoWorkflow
15
+ }
16
+
17
  // parse the node array from the ComfyUI workflow
18
+ const nodes = Object.values(JSON.parse(comfyWorkflow)) as ComfyNode[]
19
 
20
+ const visualPrompt = getVideoPrompt(
21
  request.segments,
22
  request.entities
23
  )
 
25
  for (const node of nodes) {
26
  if (typeof node.inputs.text === "string") {
27
  if (node._meta.title.includes("Prompt")) {
28
+ node.inputs.text = visualPrompt
29
  }
30
  }
31
  }
src/server/comfy/index.ts CHANGED
@@ -4,16 +4,19 @@ import { RenderRequest } from "@/types"
4
 
5
  import { run as runWithReplicate } from "./replicate"
6
  import { run as runWithHuggingFace } from "./huggingface"
7
- import { formatStoryboardWorkflow } from "./formatStoryboardWorkflow"
8
 
9
- export async function run(request: RenderRequest): Promise<string> {
 
 
10
 
11
- const workflow = formatStoryboardWorkflow(request)
12
 
13
  // TODO support Hugging Face as well
14
  // const await runWithHuggingFace({
 
15
  const result = await runWithReplicate({
16
- apiKey: request.comfyApiKey,
17
  workflow,
18
  })
19
 
 
4
 
5
  import { run as runWithReplicate } from "./replicate"
6
  import { run as runWithHuggingFace } from "./huggingface"
7
+ import { getComfyWorkflow } from "./getComfyWorkflow"
8
 
9
+ // TODO: at some point in the future we will
10
+ // move src/server/comfy to @aitube/engine
11
+ export async function render(request: RenderRequest): Promise<string> {
12
 
13
+ const workflow = getComfyWorkflow(request)
14
 
15
  // TODO support Hugging Face as well
16
  // const await runWithHuggingFace({
17
+
18
  const result = await runWithReplicate({
19
+ apiKey: request.comfyUiApiKey,
20
  workflow,
21
  })
22
 
src/server/comfy/replicate.ts CHANGED
@@ -12,6 +12,7 @@ export async function run({
12
 
13
  const replicate = new Replicate({ auth: apiKey })
14
 
 
15
  const cogId = "fofr/any-comfyui-workflow:74f12621dc9f9b7cdca50d03941b8ddb3a368d7f5a1bb16fb7e1b87f05d96bf5"
16
 
17
  const output = await replicate.run(cogId, {
 
12
 
13
  const replicate = new Replicate({ auth: apiKey })
14
 
15
+ // https://replicate.com/fofr/any-comfyui-workflow
16
  const cogId = "fofr/any-comfyui-workflow:74f12621dc9f9b7cdca50d03941b8ddb3a368d7f5a1bb16fb7e1b87f05d96bf5"
17
 
18
  const output = await replicate.run(cogId, {
src/settings/rendering/getDefaultSettingsRendering.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ComfyVendor, RenderingStrategy } from "@/types"
2
+
3
+ import { SettingsRenderingState } from "./types"
4
+
5
+ export function getDefaultSettingsRendering(): SettingsRenderingState {
6
+ const state: SettingsRenderingState = {
7
+ comfyUiApiVendor: ComfyVendor.NONE,
8
+ comfyUiApiKey: "",
9
+ storyboardRenderingStrategy: RenderingStrategy.ON_DEMAND,
10
+ videoRenderingStrategy: RenderingStrategy.ON_DEMAND,
11
+ maxNbAssetsToGenerateInParallel: 1,
12
+ }
13
+
14
+ return state
15
+ }