Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
001cba6
1
Parent(s):
7f4dc49
working on the assistant
Browse files- package-lock.json +4 -4
- package.json +1 -1
- src/app/embed/README.md +7 -0
- src/app/embed/embed.tsx +51 -0
- src/app/embed/page.tsx +33 -0
- src/app/page.tsx +3 -5
- src/components/assistant/{ChatView.FINISH_ME → ChatView.tsx} +17 -10
- src/components/monitor/DynamicPlayer/DynamicBuffer.tsx +41 -0
- src/components/monitor/DynamicPlayer/StoryboardBuffer.tsx +29 -0
- src/components/monitor/DynamicPlayer/index.tsx +63 -31
- src/controllers/README.md +83 -0
- src/controllers/monitor/getDefaultMonitorState.ts +1 -0
- src/controllers/monitor/types.ts +4 -0
- src/controllers/monitor/useMonitor.ts +32 -1
- src/controllers/renderer/useRenderer.ts +17 -3
- src/lib/core/constants.ts +1 -1
package-lock.json
CHANGED
@@ -10,7 +10,7 @@
|
|
10 |
"dependencies": {
|
11 |
"@aitube/clap": "0.0.26",
|
12 |
"@aitube/engine": "0.0.19",
|
13 |
-
"@aitube/timeline": "0.0.
|
14 |
"@fal-ai/serverless-client": "^0.10.3",
|
15 |
"@huggingface/hub": "^0.15.1",
|
16 |
"@huggingface/inference": "^2.7.0",
|
@@ -114,9 +114,9 @@
|
|
114 |
}
|
115 |
},
|
116 |
"node_modules/@aitube/timeline": {
|
117 |
-
"version": "0.0.
|
118 |
-
"resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.
|
119 |
-
"integrity": "sha512-
|
120 |
"dependencies": {
|
121 |
"date-fns": "^3.6.0",
|
122 |
"react-virtualized-auto-sizer": "^1.0.24"
|
|
|
10 |
"dependencies": {
|
11 |
"@aitube/clap": "0.0.26",
|
12 |
"@aitube/engine": "0.0.19",
|
13 |
+
"@aitube/timeline": "0.0.22",
|
14 |
"@fal-ai/serverless-client": "^0.10.3",
|
15 |
"@huggingface/hub": "^0.15.1",
|
16 |
"@huggingface/inference": "^2.7.0",
|
|
|
114 |
}
|
115 |
},
|
116 |
"node_modules/@aitube/timeline": {
|
117 |
+
"version": "0.0.22",
|
118 |
+
"resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.22.tgz",
|
119 |
+
"integrity": "sha512-T3/DY+c3tZgR5rwzcgptN4JzUI9VfxbvuO5/4SP6uHVccoA3SX5X2smmpzO1lLcfwHoyy+HvE+TejcKAG2oTPg==",
|
120 |
"dependencies": {
|
121 |
"date-fns": "^3.6.0",
|
122 |
"react-virtualized-auto-sizer": "^1.0.24"
|
package.json
CHANGED
@@ -12,7 +12,7 @@
|
|
12 |
"dependencies": {
|
13 |
"@aitube/clap": "0.0.26",
|
14 |
"@aitube/engine": "0.0.19",
|
15 |
-
"@aitube/timeline": "0.0.
|
16 |
"@fal-ai/serverless-client": "^0.10.3",
|
17 |
"@huggingface/hub": "^0.15.1",
|
18 |
"@huggingface/inference": "^2.7.0",
|
|
|
12 |
"dependencies": {
|
13 |
"@aitube/clap": "0.0.26",
|
14 |
"@aitube/engine": "0.0.19",
|
15 |
+
"@aitube/timeline": "0.0.22",
|
16 |
"@fal-ai/serverless-client": "^0.10.3",
|
17 |
"@huggingface/hub": "^0.15.1",
|
18 |
"@huggingface/inference": "^2.7.0",
|
src/app/embed/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# /embed
|
2 |
+
|
3 |
+
`/embed` is a simplified paged used to render a .clap project without
|
4 |
+
much of the editing UI
|
5 |
+
|
6 |
+
Note that for users without any rendering settings,
|
7 |
+
this will only be able to playback pre-generated content
|
src/app/embed/embed.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import React, { useRef } from "react"
|
4 |
+
import { useTimeline } from "@aitube/timeline"
|
5 |
+
|
6 |
+
import { Toaster } from "@/components/ui/sonner"
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
import { TooltipProvider } from "@/components/ui/tooltip"
|
9 |
+
import { Monitor } from "@/components/monitor"
|
10 |
+
|
11 |
+
import { SettingsDialog } from "@/components/settings"
|
12 |
+
import { LoadingDialog } from "@/components/dialogs/loader/LoadingDialog"
|
13 |
+
import { TopBar } from "@/components/toolbars/top-bar"
|
14 |
+
|
15 |
+
export function Embed() {
|
16 |
+
const ref = useRef<HTMLDivElement>(null)
|
17 |
+
const isEmpty = useTimeline(s => s.isEmpty)
|
18 |
+
|
19 |
+
return (
|
20 |
+
<TooltipProvider>
|
21 |
+
<div
|
22 |
+
ref={ref}
|
23 |
+
className={cn(`
|
24 |
+
dark
|
25 |
+
select-none
|
26 |
+
fixed
|
27 |
+
flex flex-col
|
28 |
+
w-screen h-screen
|
29 |
+
overflow-hidden
|
30 |
+
items-center justify-center
|
31 |
+
font-light
|
32 |
+
text-stone-900/90 dark:text-stone-100/90
|
33 |
+
`)}
|
34 |
+
style={{
|
35 |
+
backgroundImage: "repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #37353455, #373534 )"
|
36 |
+
}}
|
37 |
+
>
|
38 |
+
<TopBar />
|
39 |
+
<div className={cn(
|
40 |
+
`flex flex-row flex-grow w-full overflow-hidden`,
|
41 |
+
isEmpty ? "opacity-0" : "opacity-100"
|
42 |
+
)}>
|
43 |
+
<Monitor />
|
44 |
+
</div>
|
45 |
+
<SettingsDialog />
|
46 |
+
<LoadingDialog />
|
47 |
+
<Toaster />
|
48 |
+
</div>
|
49 |
+
</TooltipProvider>
|
50 |
+
);
|
51 |
+
}
|
src/app/embed/page.tsx
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useEffect, useState } from "react"
|
4 |
+
import Head from "next/head"
|
5 |
+
import Script from "next/script"
|
6 |
+
|
7 |
+
import { Embed } from "./embed"
|
8 |
+
|
9 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
10 |
+
|
11 |
+
export default function EmbedPage() {
|
12 |
+
const [isLoaded, setLoaded] = useState(false)
|
13 |
+
useEffect(() => { setLoaded(true) }, [])
|
14 |
+
return (
|
15 |
+
<>
|
16 |
+
<Head>
|
17 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
18 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
19 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
20 |
+
</Head>
|
21 |
+
<Script id="gtm">{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
22 |
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
23 |
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
24 |
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
25 |
+
})(window,document,'script','dataLayer','GTM-WD55Z2KN');`}</Script>
|
26 |
+
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WD55Z2KN"
|
27 |
+
height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
|
28 |
+
<main>
|
29 |
+
{isLoaded && <Embed />}
|
30 |
+
</main>
|
31 |
+
</>
|
32 |
+
)
|
33 |
+
}
|
src/app/page.tsx
CHANGED
@@ -4,13 +4,11 @@ import { useEffect, useState } from "react"
|
|
4 |
import Head from "next/head"
|
5 |
import Script from "next/script"
|
6 |
|
7 |
-
import { cn } from "@/lib/utils/cn"
|
8 |
-
|
9 |
import { Main } from "./main"
|
10 |
|
11 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
12 |
|
13 |
-
export default function
|
14 |
const [isLoaded, setLoaded] = useState(false)
|
15 |
useEffect(() => { setLoaded(true) }, [])
|
16 |
return (
|
@@ -24,8 +22,8 @@ export default function Page() {
|
|
24 |
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
25 |
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
26 |
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
27 |
-
})(window,document,'script','dataLayer','GTM-
|
28 |
-
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-
|
29 |
height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
|
30 |
<main>
|
31 |
{isLoaded && <Main />}
|
|
|
4 |
import Head from "next/head"
|
5 |
import Script from "next/script"
|
6 |
|
|
|
|
|
7 |
import { Main } from "./main"
|
8 |
|
9 |
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
10 |
|
11 |
+
export default function MainPage() {
|
12 |
const [isLoaded, setLoaded] = useState(false)
|
13 |
useEffect(() => { setLoaded(true) }, [])
|
14 |
return (
|
|
|
22 |
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
23 |
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
24 |
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
25 |
+
})(window,document,'script','dataLayer','GTM-WD55Z2KN');`}</Script>
|
26 |
+
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WD55Z2KN"
|
27 |
height="0" width="0" style={{ display: "none", visibility: "hidden" }}></iframe></noscript>
|
28 |
<main>
|
29 |
{isLoaded && <Main />}
|
src/components/assistant/{ChatView.FINISH_ME → ChatView.tsx}
RENAMED
@@ -1,14 +1,15 @@
|
|
1 |
"use client"
|
2 |
|
3 |
import { useState, useTransition } from "react"
|
4 |
-
import { ClapOutputType, ClapProject,
|
5 |
-
|
6 |
-
import { Input } from "@/components/ui/input"
|
7 |
-
|
8 |
-
import { queryAssistant } from "@/app/api/assistant/providers/openai/askAssistant"
|
9 |
-
import { useSettingsa } from "@/controllers/settings"
|
10 |
import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, useTimeline } from "@aitube/timeline"
|
|
|
11 |
import { useAssistant } from "@/controllers/assistant/useAssistant"
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
export function ChatView() {
|
14 |
const [_isPending, startTransition] = useTransition()
|
@@ -20,9 +21,9 @@ export function ChatView() {
|
|
20 |
*/
|
21 |
|
22 |
const [draft, setDraft] = useState("")
|
23 |
-
const runCommand = useAssistant((
|
24 |
-
const history = useAssistant((
|
25 |
-
const addEventToHistory = useAssistant((
|
26 |
|
27 |
/*
|
28 |
const updateSegment = useApp((state) => state.updateSegment)
|
@@ -142,7 +143,8 @@ export function ChatView() {
|
|
142 |
})
|
143 |
console.log("Creating new existing segment:", newSeg)
|
144 |
|
145 |
-
|
|
|
146 |
|
147 |
addEventToHistory({
|
148 |
senderId: "assistant",
|
@@ -157,11 +159,16 @@ export function ChatView() {
|
|
157 |
label: prompt,
|
158 |
})
|
159 |
|
|
|
|
|
|
|
|
|
160 |
updateSegment({
|
161 |
...match,
|
162 |
prompt,
|
163 |
label: prompt,
|
164 |
})
|
|
|
165 |
|
166 |
addEventToHistory({
|
167 |
senderId: "assistant",
|
|
|
1 |
"use client"
|
2 |
|
3 |
import { useState, useTransition } from "react"
|
4 |
+
import { ClapOutputType, ClapProject, ClapSegment, ClapSegmentCategory, newSegment } from "@aitube/clap"
|
|
|
|
|
|
|
|
|
|
|
5 |
import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, useTimeline } from "@aitube/timeline"
|
6 |
+
|
7 |
import { useAssistant } from "@/controllers/assistant/useAssistant"
|
8 |
+
import { queryAssistant } from "@/app/api/assistant/providers/openai/askAssistant"
|
9 |
+
import { useSettings } from "@/controllers/settings"
|
10 |
+
|
11 |
+
import { ChatBubble } from "./ChatBubble"
|
12 |
+
import { Input } from "../ui/input"
|
13 |
|
14 |
export function ChatView() {
|
15 |
const [_isPending, startTransition] = useTransition()
|
|
|
21 |
*/
|
22 |
|
23 |
const [draft, setDraft] = useState("")
|
24 |
+
const runCommand = useAssistant((s) => s.runCommand)
|
25 |
+
const history = useAssistant((s) => s.history)
|
26 |
+
const addEventToHistory = useAssistant((s) => s.addEventToHistory)
|
27 |
|
28 |
/*
|
29 |
const updateSegment = useApp((state) => state.updateSegment)
|
|
|
143 |
})
|
144 |
console.log("Creating new existing segment:", newSeg)
|
145 |
|
146 |
+
console.log(`TODO Julian: add the segment!!`)
|
147 |
+
// addSegment(newSeg)
|
148 |
|
149 |
addEventToHistory({
|
150 |
senderId: "assistant",
|
|
|
159 |
label: prompt,
|
160 |
})
|
161 |
|
162 |
+
console.log(`TODO Julian: update the segment!!`)
|
163 |
+
// addSegment(newSeg)
|
164 |
+
|
165 |
+
/*
|
166 |
updateSegment({
|
167 |
...match,
|
168 |
prompt,
|
169 |
label: prompt,
|
170 |
})
|
171 |
+
*/
|
172 |
|
173 |
addEventToHistory({
|
174 |
senderId: "assistant",
|
src/components/monitor/DynamicPlayer/DynamicBuffer.tsx
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { ClapOutputType, ClapSegment } from "@aitube/clap"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
|
7 |
+
import { VideoClipBuffer } from "./VideoClipBuffer"
|
8 |
+
import { StoryboardBuffer } from "./StoryboardBuffer"
|
9 |
+
|
10 |
+
export const DynamicBuffer = ({
|
11 |
+
segment,
|
12 |
+
isPlaying = false,
|
13 |
+
isVisible = false,
|
14 |
+
}: {
|
15 |
+
segment?: ClapSegment
|
16 |
+
isPlaying?: boolean
|
17 |
+
isVisible?: boolean
|
18 |
+
}): JSX.Element | null => {
|
19 |
+
const src = `${segment?.assetUrl || ""}`
|
20 |
+
|
21 |
+
if (!src) { return null }
|
22 |
+
|
23 |
+
const outputType = segment!.outputType
|
24 |
+
|
25 |
+
const className = cn(isVisible ? `opacity-100` : `opacity-0`)
|
26 |
+
|
27 |
+
return (
|
28 |
+
<>
|
29 |
+
{outputType === ClapOutputType.VIDEO
|
30 |
+
? <VideoClipBuffer
|
31 |
+
src={src}
|
32 |
+
isPlaying={isPlaying}
|
33 |
+
className={className}
|
34 |
+
/>
|
35 |
+
: <StoryboardBuffer
|
36 |
+
src={src}
|
37 |
+
className={className}
|
38 |
+
/>}
|
39 |
+
</>
|
40 |
+
)
|
41 |
+
}
|
src/components/monitor/DynamicPlayer/StoryboardBuffer.tsx
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
|
3 |
+
export function StoryboardBuffer({
|
4 |
+
src,
|
5 |
+
className,
|
6 |
+
}: {
|
7 |
+
src?: string;
|
8 |
+
className?: string;
|
9 |
+
}) {
|
10 |
+
|
11 |
+
if (!src) { return null }
|
12 |
+
|
13 |
+
return (
|
14 |
+
<img
|
15 |
+
className={cn(
|
16 |
+
`absolute`,
|
17 |
+
`h-full w-full rounded-md overflow-hidden`,
|
18 |
+
|
19 |
+
// iseally we could only use the ease-out and duration-150
|
20 |
+
// to avoid a weird fade to grey,
|
21 |
+
// but the ease out also depends on which video is on top of each other,
|
22 |
+
// in term of z-index, so we should also intervert this
|
23 |
+
`transition-all duration-100 ease-out`,
|
24 |
+
className
|
25 |
+
)}
|
26 |
+
src={src}
|
27 |
+
/>
|
28 |
+
)
|
29 |
+
}
|
src/components/monitor/DynamicPlayer/index.tsx
CHANGED
@@ -8,9 +8,10 @@ import { useTimeline } from "@aitube/timeline"
|
|
8 |
|
9 |
import { useRequestAnimationFrame } from "@/lib/hooks"
|
10 |
import { MonitoringMode } from "@/controllers/monitor/types"
|
11 |
-
import { VideoClipBuffer } from "./VideoClipBuffer"
|
12 |
import { useRenderLoop } from "@/controllers/renderer/useRenderLoop"
|
13 |
import { useRenderer } from "@/controllers/renderer/useRenderer"
|
|
|
|
|
14 |
|
15 |
export const DynamicPlayer = ({
|
16 |
className,
|
@@ -25,20 +26,43 @@ export const DynamicPlayer = ({
|
|
25 |
// this should only be called once and at only one place in the project!
|
26 |
useRenderLoop()
|
27 |
|
28 |
-
const {
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
-
// the upcoming video we want to preload (note: we just want to preload it, not display it just yet)
|
33 |
-
const preloadVideoUrl = upcomingVideoSegment?.assetUrl || ""
|
34 |
|
35 |
-
const
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
const [activeBufferNumber, setActiveBufferNumber] = useState(1)
|
38 |
|
39 |
const timeoutRef = useRef<NodeJS.Timeout>()
|
40 |
|
41 |
-
const fadeDurationInMs =
|
42 |
|
43 |
useEffect(() => {
|
44 |
setMonitoringMode(MonitoringMode.DYNAMIC)
|
@@ -56,57 +80,65 @@ export const DynamicPlayer = ({
|
|
56 |
|
57 |
})
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
useEffect(() => {
|
60 |
// trivial case: we are at the initial state
|
61 |
-
if (!
|
62 |
-
|
63 |
-
|
64 |
setActiveBufferNumber(1)
|
65 |
}
|
66 |
-
}, [
|
67 |
-
|
|
|
|
|
|
|
|
|
68 |
|
69 |
-
// console.log("cursorInSteps:", cursorInSteps)
|
70 |
useEffect(() => {
|
71 |
-
/*
|
72 |
-
console.log("ATTENTION: something changed among those: ", {
|
73 |
-
currentVideoUrl, preloadVideoUrl
|
74 |
-
})
|
75 |
-
*/
|
76 |
|
77 |
clearTimeout(timeoutRef.current)
|
78 |
|
79 |
const newActiveBufferNumber = activeBufferNumber === 1 ? 2 : 1
|
80 |
-
// console.log(`our pre-loaded video should already be available in buffer ${newActiveBufferNumber}`)
|
81 |
-
|
82 |
setActiveBufferNumber(newActiveBufferNumber)
|
83 |
|
84 |
timeoutRef.current = setTimeout(() => {
|
85 |
// by now one buffer should be visible, and the other should be hidden
|
86 |
// so let's update the invisible one
|
87 |
if (newActiveBufferNumber === 2) {
|
88 |
-
|
89 |
} else {
|
90 |
-
|
91 |
}
|
92 |
}, fadeDurationInMs + 200) // let's add some security in here
|
93 |
|
94 |
return () => {
|
95 |
clearTimeout(timeoutRef.current)
|
96 |
}
|
97 |
-
|
|
|
|
|
|
|
98 |
|
99 |
return (
|
100 |
<div className={cn(`@container flex flex-col flex-grow w-full`, className)}>
|
101 |
-
<
|
102 |
-
|
103 |
isPlaying={isPlaying}
|
104 |
-
|
105 |
/>
|
106 |
-
<
|
107 |
-
|
108 |
isPlaying={isPlaying}
|
109 |
-
|
110 |
/>
|
111 |
</div>
|
112 |
)
|
|
|
8 |
|
9 |
import { useRequestAnimationFrame } from "@/lib/hooks"
|
10 |
import { MonitoringMode } from "@/controllers/monitor/types"
|
|
|
11 |
import { useRenderLoop } from "@/controllers/renderer/useRenderLoop"
|
12 |
import { useRenderer } from "@/controllers/renderer/useRenderer"
|
13 |
+
import { ClapSegment } from "@aitube/clap"
|
14 |
+
import { DynamicBuffer } from "./DynamicBuffer"
|
15 |
|
16 |
export const DynamicPlayer = ({
|
17 |
className,
|
|
|
26 |
// this should only be called once and at only one place in the project!
|
27 |
useRenderLoop()
|
28 |
|
29 |
+
const {
|
30 |
+
activeVideoSegment,
|
31 |
+
upcomingVideoSegment,
|
32 |
+
activeStoryboardSegment,
|
33 |
+
upcomingStoryboardSegment
|
34 |
+
} = useRenderer(s => s.bufferedSegments)
|
35 |
+
|
36 |
+
console.log(`DynamicPlayer:`, {
|
37 |
+
activeVideoSegment,
|
38 |
+
upcomingVideoSegment,
|
39 |
+
activeStoryboardSegment,
|
40 |
+
upcomingStoryboardSegment
|
41 |
+
})
|
42 |
|
|
|
|
|
43 |
|
44 |
+
const currentSegment =
|
45 |
+
activeVideoSegment?.assetUrl
|
46 |
+
? activeVideoSegment
|
47 |
+
: activeStoryboardSegment?.assetUrl
|
48 |
+
? activeStoryboardSegment
|
49 |
+
: undefined
|
50 |
+
|
51 |
+
// the upcoming asset we want to preload (note: we just want to preload it, not display it just yet)
|
52 |
+
const preloadSegment =
|
53 |
+
upcomingVideoSegment?.assetUrl
|
54 |
+
? upcomingVideoSegment
|
55 |
+
: upcomingStoryboardSegment?.assetUrl
|
56 |
+
? upcomingStoryboardSegment
|
57 |
+
: undefined
|
58 |
+
|
59 |
+
const [dataUriBuffer1, setDataUriBuffer1] = useState<ClapSegment | undefined>()
|
60 |
+
const [dataUriBuffer2, setDataUriBuffer2] = useState<ClapSegment | undefined>()
|
61 |
const [activeBufferNumber, setActiveBufferNumber] = useState(1)
|
62 |
|
63 |
const timeoutRef = useRef<NodeJS.Timeout>()
|
64 |
|
65 |
+
const fadeDurationInMs = 250
|
66 |
|
67 |
useEffect(() => {
|
68 |
setMonitoringMode(MonitoringMode.DYNAMIC)
|
|
|
80 |
|
81 |
})
|
82 |
|
83 |
+
// performance optimization:
|
84 |
+
// we only look at the first part since it might be huge
|
85 |
+
// for assets, using a smaller header lookup like 256 or even 512 doesn't seem to be enough
|
86 |
+
const currentSegmentKey = `${currentSegment?.assetUrl || ""}`.slice(0, 1024)
|
87 |
+
const preloadSegmentKey = `${preloadSegment?.assetUrl || ""}`.slice(0, 1024)
|
88 |
+
|
89 |
+
const dataUriBuffer1Key = `${dataUriBuffer1?.assetUrl || ""}`.slice(0, 1024)
|
90 |
+
const dataUriBuffer2Key = `${dataUriBuffer2?.assetUrl || ""}`.slice(0, 1024)
|
91 |
+
|
92 |
useEffect(() => {
|
93 |
// trivial case: we are at the initial state
|
94 |
+
if (!dataUriBuffer1 && !dataUriBuffer2) {
|
95 |
+
setDataUriBuffer1(currentSegment)
|
96 |
+
setDataUriBuffer2(preloadSegment)
|
97 |
setActiveBufferNumber(1)
|
98 |
}
|
99 |
+
}, [
|
100 |
+
dataUriBuffer1Key,
|
101 |
+
dataUriBuffer2Key,
|
102 |
+
currentSegmentKey,
|
103 |
+
preloadSegmentKey
|
104 |
+
])
|
105 |
|
|
|
106 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
clearTimeout(timeoutRef.current)
|
109 |
|
110 |
const newActiveBufferNumber = activeBufferNumber === 1 ? 2 : 1
|
|
|
|
|
111 |
setActiveBufferNumber(newActiveBufferNumber)
|
112 |
|
113 |
timeoutRef.current = setTimeout(() => {
|
114 |
// by now one buffer should be visible, and the other should be hidden
|
115 |
// so let's update the invisible one
|
116 |
if (newActiveBufferNumber === 2) {
|
117 |
+
setDataUriBuffer1(preloadSegment)
|
118 |
} else {
|
119 |
+
setDataUriBuffer2(preloadSegment)
|
120 |
}
|
121 |
}, fadeDurationInMs + 200) // let's add some security in here
|
122 |
|
123 |
return () => {
|
124 |
clearTimeout(timeoutRef.current)
|
125 |
}
|
126 |
+
}, [
|
127 |
+
currentSegmentKey,
|
128 |
+
preloadSegmentKey
|
129 |
+
])
|
130 |
|
131 |
return (
|
132 |
<div className={cn(`@container flex flex-col flex-grow w-full`, className)}>
|
133 |
+
<DynamicBuffer
|
134 |
+
segment={dataUriBuffer1}
|
135 |
isPlaying={isPlaying}
|
136 |
+
isVisible={activeBufferNumber === 1}
|
137 |
/>
|
138 |
+
<DynamicBuffer
|
139 |
+
segment={dataUriBuffer2}
|
140 |
isPlaying={isPlaying}
|
141 |
+
isVisible={activeBufferNumber === 2}
|
142 |
/>
|
143 |
</div>
|
144 |
)
|
src/controllers/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# /controllers
|
2 |
+
|
3 |
+
Each controller represents a core features of Clapper.
|
4 |
+
|
5 |
+
They each have their own state (managed using Zustand, so it can be used in and out of a React component), and should be considered like tiny independent apps (each manage one important thing), although they deeply relate to each other.
|
6 |
+
|
7 |
+
For instance multiple controllers might need to tap into the Settings controller to pull parameters or default values.
|
8 |
+
|
9 |
+
|
10 |
+
## Assistant
|
11 |
+
|
12 |
+
The chatbot assistant.
|
13 |
+
|
14 |
+
Usage:
|
15 |
+
|
16 |
+
```typescript
|
17 |
+
useAssistant()
|
18 |
+
```
|
19 |
+
|
20 |
+
## Audio
|
21 |
+
|
22 |
+
Audio management, including playing back audio files for dialogues, sound effects and music.
|
23 |
+
|
24 |
+
Usage:
|
25 |
+
|
26 |
+
```typescript
|
27 |
+
useAudio()
|
28 |
+
```
|
29 |
+
|
30 |
+
## IO
|
31 |
+
|
32 |
+
Input/output management, to import and export various file formats.
|
33 |
+
|
34 |
+
|
35 |
+
Usage:
|
36 |
+
|
37 |
+
```typescript
|
38 |
+
useIO()
|
39 |
+
```
|
40 |
+
|
41 |
+
## Monitor
|
42 |
+
|
43 |
+
The video monitor is the big video display component used to preview an existing (pre-generated) full video, or preview a work in progress project composed of many tiny video clips.
|
44 |
+
|
45 |
+
The monitor is responsible for handling the play, pause, seek etc..
|
46 |
+
functions.
|
47 |
+
|
48 |
+
Usage:
|
49 |
+
|
50 |
+
```typescript
|
51 |
+
useMonitor()
|
52 |
+
```
|
53 |
+
|
54 |
+
## Renderer
|
55 |
+
|
56 |
+
This is the engine that can generate a video stream on the fly from a sequence of tiny video and audio clips.
|
57 |
+
|
58 |
+
Usage:
|
59 |
+
|
60 |
+
```typescript
|
61 |
+
useRenderer()
|
62 |
+
```
|
63 |
+
|
64 |
+
## Settings
|
65 |
+
|
66 |
+
Responsible for organizing user preferences, which are serialized
|
67 |
+
and persisted into the browser local storage, that way no login or account management is necessary.
|
68 |
+
|
69 |
+
Usage:
|
70 |
+
|
71 |
+
```typescript
|
72 |
+
useSettings()
|
73 |
+
```
|
74 |
+
|
75 |
+
## UI
|
76 |
+
|
77 |
+
The user interface controller, through which you can show/hidden elements of the interface.
|
78 |
+
|
79 |
+
Usage:
|
80 |
+
|
81 |
+
```typescript
|
82 |
+
useUI()
|
83 |
+
```
|
src/controllers/monitor/getDefaultMonitorState.ts
CHANGED
@@ -2,6 +2,7 @@ import { MonitoringMode, MonitorState } from "./types"
|
|
2 |
|
3 |
export function getDefaultMonitorState(): MonitorState {
|
4 |
const state: MonitorState = {
|
|
|
5 |
mode: MonitoringMode.NONE,
|
6 |
lastTimelineUpdateAtInMs: 0,
|
7 |
isPlaying: false,
|
|
|
2 |
|
3 |
export function getDefaultMonitorState(): MonitorState {
|
4 |
const state: MonitorState = {
|
5 |
+
shortcutsAreBound: false,
|
6 |
mode: MonitoringMode.NONE,
|
7 |
lastTimelineUpdateAtInMs: 0,
|
8 |
isPlaying: false,
|
src/controllers/monitor/types.ts
CHANGED
@@ -5,6 +5,7 @@ export enum MonitoringMode {
|
|
5 |
}
|
6 |
|
7 |
export type MonitorState = {
|
|
|
8 |
mode: MonitoringMode
|
9 |
lastTimelineUpdateAtInMs: number
|
10 |
isPlaying: boolean
|
@@ -12,6 +13,9 @@ export type MonitorState = {
|
|
12 |
}
|
13 |
|
14 |
export type MonitorControls = {
|
|
|
|
|
|
|
15 |
setMonitoringMode: (mode: MonitoringMode) => void
|
16 |
|
17 |
setStaticVideoRef: (staticVideoRef: HTMLVideoElement) => void
|
|
|
5 |
}
|
6 |
|
7 |
export type MonitorState = {
|
8 |
+
shortcutsAreBound: boolean
|
9 |
mode: MonitoringMode
|
10 |
lastTimelineUpdateAtInMs: number
|
11 |
isPlaying: boolean
|
|
|
13 |
}
|
14 |
|
15 |
export type MonitorControls = {
|
16 |
+
|
17 |
+
bindShortcuts: () => void
|
18 |
+
|
19 |
setMonitoringMode: (mode: MonitoringMode) => void
|
20 |
|
21 |
setStaticVideoRef: (staticVideoRef: HTMLVideoElement) => void
|
src/controllers/monitor/useMonitor.ts
CHANGED
@@ -11,7 +11,32 @@ import { getDefaultMonitorState } from "./getDefaultMonitorState"
|
|
11 |
|
12 |
export const useMonitor = create<MonitorStore>((set, get) => ({
|
13 |
...getDefaultMonitorState(),
|
|
|
|
|
|
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
setMonitoringMode: (mode: MonitoringMode) => {
|
16 |
set({ mode })
|
17 |
},
|
@@ -104,4 +129,10 @@ export const useMonitor = create<MonitorStore>((set, get) => ({
|
|
104 |
set({ lastTimelineUpdateAtInMs })
|
105 |
},
|
106 |
|
107 |
-
}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
export const useMonitor = create<MonitorStore>((set, get) => ({
|
13 |
...getDefaultMonitorState(),
|
14 |
+
|
15 |
+
bindShortcuts: () => {
|
16 |
+
if (get().shortcutsAreBound) { return }
|
17 |
|
18 |
+
document.addEventListener("keydown", (event) => {
|
19 |
+
const element = event.target as unknown as HTMLElement
|
20 |
+
|
21 |
+
if (event.code === "Space" &&
|
22 |
+
// those exception are important, otherwise we won't be able to add spaces
|
23 |
+
// in the search boxes, edit fields, or even the script editor
|
24 |
+
element.nodeName !== "INPUT" &&
|
25 |
+
element.nodeName !== "TEXTAREA") {
|
26 |
+
console.log("[SHORTCUT DETECTED] User pressed space key outside a text input: toggling video playback")
|
27 |
+
|
28 |
+
// prevent the default behavior, which is strange (automatic scroll to the buttom)
|
29 |
+
// https://www.jankollars.com/posts/preventing-space-scrolling/
|
30 |
+
event.preventDefault()
|
31 |
+
|
32 |
+
get().togglePlayback()
|
33 |
+
}
|
34 |
+
})
|
35 |
+
|
36 |
+
set({
|
37 |
+
shortcutsAreBound: true
|
38 |
+
})
|
39 |
+
},
|
40 |
setMonitoringMode: (mode: MonitoringMode) => {
|
41 |
set({ mode })
|
42 |
},
|
|
|
129 |
set({ lastTimelineUpdateAtInMs })
|
130 |
},
|
131 |
|
132 |
+
}))
|
133 |
+
|
134 |
+
setTimeout(() => {
|
135 |
+
if (typeof document !== "undefined") {
|
136 |
+
useMonitor.getState().bindShortcuts()
|
137 |
+
}
|
138 |
+
}, 0)
|
src/controllers/renderer/useRenderer.ts
CHANGED
@@ -46,8 +46,7 @@ export const useRenderer = create<RendererStore>((set, get) => ({
|
|
46 |
const segments = clapSegments as RuntimeSegment[]
|
47 |
|
48 |
const results: BufferedSegments = getDefaultBufferedSegments()
|
49 |
-
|
50 |
-
|
51 |
|
52 |
// we could use a temporal index to keep things efficient here
|
53 |
// thiere is this relatively recent algorithm, the IB+ Tree,
|
@@ -60,7 +59,6 @@ export const useRenderer = create<RendererStore>((set, get) => ({
|
|
60 |
|
61 |
if (inActiveShot) {
|
62 |
const isActiveVideo = segment.category === ClapSegmentCategory.VIDEO && segment.assetUrl
|
63 |
-
// const isActiveStoryboard = segment.category === ClapSegmentCategory.STORYBOARD && segment.assetUrl
|
64 |
if (isActiveVideo) {
|
65 |
results.activeSegments.push(segment)
|
66 |
results.activeVideoSegment = segment
|
@@ -79,6 +77,14 @@ export const useRenderer = create<RendererStore>((set, get) => ({
|
|
79 |
results.activeAudioSegments.push(segment)
|
80 |
results.activeSegmentsCacheKey = getSegmentCacheKey(segment, results.activeSegmentsCacheKey)
|
81 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
}
|
83 |
|
84 |
const inUpcomingShot =
|
@@ -107,6 +113,14 @@ export const useRenderer = create<RendererStore>((set, get) => ({
|
|
107 |
results.upcomingAudioSegments.push(segment)
|
108 |
results.upcomingSegmentsCacheKey = getSegmentCacheKey(segment, results.upcomingSegmentsCacheKey)
|
109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
}
|
111 |
|
112 |
}
|
|
|
46 |
const segments = clapSegments as RuntimeSegment[]
|
47 |
|
48 |
const results: BufferedSegments = getDefaultBufferedSegments()
|
49 |
+
|
|
|
50 |
|
51 |
// we could use a temporal index to keep things efficient here
|
52 |
// thiere is this relatively recent algorithm, the IB+ Tree,
|
|
|
59 |
|
60 |
if (inActiveShot) {
|
61 |
const isActiveVideo = segment.category === ClapSegmentCategory.VIDEO && segment.assetUrl
|
|
|
62 |
if (isActiveVideo) {
|
63 |
results.activeSegments.push(segment)
|
64 |
results.activeVideoSegment = segment
|
|
|
77 |
results.activeAudioSegments.push(segment)
|
78 |
results.activeSegmentsCacheKey = getSegmentCacheKey(segment, results.activeSegmentsCacheKey)
|
79 |
}
|
80 |
+
|
81 |
+
const isActiveStoryboard = segment.category === ClapSegmentCategory.STORYBOARD && segment.assetUrl
|
82 |
+
if (isActiveStoryboard) {
|
83 |
+
results.activeSegments.push(segment)
|
84 |
+
results.activeStoryboardSegment = segment
|
85 |
+
results.activeSegmentsCacheKey = getSegmentCacheKey(segment, results.activeSegmentsCacheKey)
|
86 |
+
}
|
87 |
+
|
88 |
}
|
89 |
|
90 |
const inUpcomingShot =
|
|
|
113 |
results.upcomingAudioSegments.push(segment)
|
114 |
results.upcomingSegmentsCacheKey = getSegmentCacheKey(segment, results.upcomingSegmentsCacheKey)
|
115 |
}
|
116 |
+
|
117 |
+
const isUpcomingStoryboard = segment.category === ClapSegmentCategory.STORYBOARD && segment.assetUrl
|
118 |
+
if (isUpcomingStoryboard) {
|
119 |
+
results.upcomingSegments.push(segment)
|
120 |
+
results.upcomingStoryboardSegment = segment
|
121 |
+
results.upcomingSegmentsCacheKey = getSegmentCacheKey(segment, results.upcomingSegmentsCacheKey)
|
122 |
+
}
|
123 |
+
|
124 |
}
|
125 |
|
126 |
}
|
src/lib/core/constants.ts
CHANGED
@@ -4,7 +4,7 @@
|
|
4 |
export const HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL = 32
|
5 |
|
6 |
export const APP_NAME = "Clapper AI"
|
7 |
-
export const APP_REVISION = "
|
8 |
|
9 |
export const APP_DOMAIN = "Clapper.app"
|
10 |
export const APP_LINK = "https://clapper.app"
|
|
|
4 |
export const HARD_LIMIT_NB_MAX_ASSETS_TO_GENERATE_IN_PARALLEL = 32
|
5 |
|
6 |
export const APP_NAME = "Clapper AI"
|
7 |
+
export const APP_REVISION = "r20240611-2256"
|
8 |
|
9 |
export const APP_DOMAIN = "Clapper.app"
|
10 |
export const APP_LINK = "https://clapper.app"
|