jbilcke-hf HF staff commited on
Commit
001cba6
1 Parent(s): 7f4dc49

working on the assistant

Browse files
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.21",
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.21",
118
- "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.21.tgz",
119
- "integrity": "sha512-FQ7YPYG7K3F4RlfW5EboenkGn6Y4a/UjlU+ZDKUBIbZSTx15QiNXFBsLcxwj58FLThuHp/90TuHog202MOI+lw==",
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.21",
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 Page() {
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-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 />}
 
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, ClapScene, ClapSegment, ClapSegmentCategory, ClapSegmentFilteringMode, filterSegments, newSegment } from "@aitube/clap"
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((state) => state.runCommand)
24
- const history = useAssistant((state) => state.history)
25
- const addEventToHistory = useAssistant((state) => state.addEventToHistory)
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
- addSegment(newSeg)
 
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 { activeVideoSegment, upcomingVideoSegment } = useRenderer(s => s.bufferedSegments)
29
-
30
- const currentVideoUrl = activeVideoSegment?.assetUrl || ""
 
 
 
 
 
 
 
 
 
 
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 [buffer1Value, setBuffer1Value] = useState("")
36
- const [buffer2Value, setBuffer2Value] = useState("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  const [activeBufferNumber, setActiveBufferNumber] = useState(1)
38
 
39
  const timeoutRef = useRef<NodeJS.Timeout>()
40
 
41
- const fadeDurationInMs = 300
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 (!buffer1Value && !buffer2Value) {
62
- setBuffer1Value(currentVideoUrl)
63
- setBuffer2Value(preloadVideoUrl)
64
  setActiveBufferNumber(1)
65
  }
66
- }, [buffer1Value, currentVideoUrl, preloadVideoUrl])
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
- setBuffer1Value(preloadVideoUrl)
89
  } else {
90
- setBuffer2Value(preloadVideoUrl)
91
  }
92
  }, fadeDurationInMs + 200) // let's add some security in here
93
 
94
  return () => {
95
  clearTimeout(timeoutRef.current)
96
  }
97
- }, [currentVideoUrl, preloadVideoUrl])
 
 
 
98
 
99
  return (
100
  <div className={cn(`@container flex flex-col flex-grow w-full`, className)}>
101
- <VideoClipBuffer
102
- src={buffer1Value}
103
  isPlaying={isPlaying}
104
- className={cn(activeBufferNumber === 1 ? `opacity-100` : `opacity-0`)}
105
  />
106
- <VideoClipBuffer
107
- src={buffer2Value}
108
  isPlaying={isPlaying}
109
- className={cn(activeBufferNumber === 2 ? `opacity-100` : `opacity-0`)}
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
- // console.log("useRenderer: computeBufferedSegments() called")
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 = "r2024-06-11"
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"