jbilcke-hf HF staff commited on
Commit
c1e4aec
β€’
1 Parent(s): fecef05

work in progress

Browse files
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: AI Bedtime Story
3
  emoji: πŸŒ™
4
  colorFrom: yellow
5
  colorTo: gray
@@ -8,6 +8,6 @@ pinned: true
8
  app_port: 3000
9
  ---
10
 
11
- # AI Bedtime Story
12
 
13
  (To be continued)
 
1
  ---
2
+ title: AI Bedtime Story πŸ›οΈ
3
  emoji: πŸŒ™
4
  colorFrom: yellow
5
  colorTo: gray
 
8
  app_port: 3000
9
  ---
10
 
11
+ # 🌜 AI Bedtime Story πŸ›οΈ
12
 
13
  (To be continued)
package-lock.json CHANGED
@@ -50,6 +50,7 @@
50
  "react-virtualized-auto-sizer": "^1.0.20",
51
  "replicate": "^0.17.0",
52
  "sbd": "^1.0.19",
 
53
  "sharp": "^0.32.5",
54
  "styled-components": "^6.0.7",
55
  "tailwind-merge": "^1.13.2",
@@ -1691,6 +1692,11 @@
1691
  "tslib": "^2.4.0"
1692
  }
1693
  },
 
 
 
 
 
1694
  "node_modules/@tsconfig/node10": {
1695
  "version": "1.0.9",
1696
  "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -2248,6 +2254,11 @@
2248
  "readable-stream": "^3.4.0"
2249
  }
2250
  },
 
 
 
 
 
2251
  "node_modules/brace-expansion": {
2252
  "version": "1.1.11",
2253
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -5815,6 +5826,15 @@
5815
  "node": ">=10"
5816
  }
5817
  },
 
 
 
 
 
 
 
 
 
5818
  "node_modules/set-function-length": {
5819
  "version": "1.1.1",
5820
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
@@ -6084,6 +6104,14 @@
6084
  "url": "https://github.com/sponsors/sindresorhus"
6085
  }
6086
  },
 
 
 
 
 
 
 
 
6087
  "node_modules/styled-components": {
6088
  "version": "6.1.1",
6089
  "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.1.tgz",
 
50
  "react-virtualized-auto-sizer": "^1.0.20",
51
  "replicate": "^0.17.0",
52
  "sbd": "^1.0.19",
53
+ "sentence-splitter": "^4.3.0",
54
  "sharp": "^0.32.5",
55
  "styled-components": "^6.0.7",
56
  "tailwind-merge": "^1.13.2",
 
1692
  "tslib": "^2.4.0"
1693
  }
1694
  },
1695
+ "node_modules/@textlint/ast-node-types": {
1696
+ "version": "13.4.0",
1697
+ "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-13.4.0.tgz",
1698
+ "integrity": "sha512-roVeLjnf8UPntFICb1uEwE2dccC8V/T5N1x7eBxkT3VDmSQkyfIAuGtlpwyH0wNKEwJmjO/2gSm2fCjW5K/rbA=="
1699
+ },
1700
  "node_modules/@tsconfig/node10": {
1701
  "version": "1.0.9",
1702
  "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
 
2254
  "readable-stream": "^3.4.0"
2255
  }
2256
  },
2257
+ "node_modules/boundary": {
2258
+ "version": "2.0.0",
2259
+ "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz",
2260
+ "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="
2261
+ },
2262
  "node_modules/brace-expansion": {
2263
  "version": "1.1.11",
2264
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
 
5826
  "node": ">=10"
5827
  }
5828
  },
5829
+ "node_modules/sentence-splitter": {
5830
+ "version": "4.3.0",
5831
+ "resolved": "https://registry.npmjs.org/sentence-splitter/-/sentence-splitter-4.3.0.tgz",
5832
+ "integrity": "sha512-srJOMqv7JeEmsbVa/N64ULey2N6/OuZzeKWn2Zrj0DiTBlU930JGr/rKKlKQRigzXtLMOtl32/Gm5G3HW8/ULA==",
5833
+ "dependencies": {
5834
+ "@textlint/ast-node-types": "^13.2.0",
5835
+ "structured-source": "^4.0.0"
5836
+ }
5837
+ },
5838
  "node_modules/set-function-length": {
5839
  "version": "1.1.1",
5840
  "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
 
6104
  "url": "https://github.com/sponsors/sindresorhus"
6105
  }
6106
  },
6107
+ "node_modules/structured-source": {
6108
+ "version": "4.0.0",
6109
+ "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz",
6110
+ "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==",
6111
+ "dependencies": {
6112
+ "boundary": "^2.0.0"
6113
+ }
6114
+ },
6115
  "node_modules/styled-components": {
6116
  "version": "6.1.1",
6117
  "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.1.tgz",
package.json CHANGED
@@ -51,6 +51,7 @@
51
  "react-virtualized-auto-sizer": "^1.0.20",
52
  "replicate": "^0.17.0",
53
  "sbd": "^1.0.19",
 
54
  "sharp": "^0.32.5",
55
  "styled-components": "^6.0.7",
56
  "tailwind-merge": "^1.13.2",
 
51
  "react-virtualized-auto-sizer": "^1.0.20",
52
  "replicate": "^0.17.0",
53
  "sbd": "^1.0.19",
54
+ "sentence-splitter": "^4.3.0",
55
  "sharp": "^0.32.5",
56
  "styled-components": "^6.0.7",
57
  "tailwind-merge": "^1.13.2",
src/app/interface/generate/index.tsx CHANGED
@@ -3,21 +3,19 @@
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
  import { useSpring, animated } from "@react-spring/web"
5
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
 
6
 
7
  import { useToast } from "@/components/ui/use-toast"
8
  import { cn } from "@/lib/utils"
9
  import { headingFont } from "@/app/interface/fonts"
10
  import { useCharacterLimit } from "@/lib/useCharacterLimit"
11
- import { generateStory } from "@/app/server/actions/generateStory"
12
- import { getLatestPosts, getPost, postToCommunity } from "@/app/server/actions/community"
13
- import { HotshotImageInferenceSize, Post, SDXLModel, Story, TTSVoice } from "@/types"
14
- import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
15
  import { TooltipProvider } from "@radix-ui/react-tooltip"
16
-
17
  import { useCountdown } from "@/lib/useCountdown"
 
18
 
19
  import { Countdown } from "../countdown"
20
- import { useAudio } from "@/lib/useAudio"
21
 
22
  type Stage = "generate" | "finished"
23
 
@@ -38,32 +36,54 @@ export function Generate() {
38
  const [runs, setRuns] = useState(0)
39
  const runsRef = useRef(0)
40
 
41
- const [story, setStory] = useState<Story>({ text: "", audio: "" })
42
- const storyText = story?.text || ""
43
- const audioData = story?.audio || ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  const [stage, setStage] = useState<Stage>("generate")
46
 
47
  const { toast } = useToast()
48
 
 
 
 
 
 
 
49
  const [typedStoryText, setTypedStoryText] = useState("")
50
  const [typedStoryCharacterIndex, setTypedStoryCharacterIndex] = useState(0)
51
 
52
- const audio = useAudio()
53
-
54
  useEffect(() => {
55
  if (storyText && typedStoryCharacterIndex < storyText.length) {
56
  setTimeout(() => {
57
  setTypedStoryText(typedStoryText + story.text[typedStoryCharacterIndex])
58
  setTypedStoryCharacterIndex(typedStoryCharacterIndex + 1)
 
59
  }, 40)
60
  }
61
  }, [storyText, typedStoryCharacterIndex])
 
62
 
63
  const { progressPercent, remainingTimeInSec } = useCountdown({
64
  isActive: isLocked,
65
  timerId: runs, // everytime we change this, the timer will reset
66
- durationInSec: /*stage === "interpolate" ? 30 :*/ 25, // it usually takes 40 seconds, but there might be lag
67
  onEnd: () => {}
68
  })
69
 
@@ -108,28 +128,17 @@ export function Generate() {
108
  const search = current.toString()
109
  router.push(`${pathname}${search ? `?${search}` : ""}`)
110
 
111
- let story: Story = {
112
- text: "",
113
- audio: ""
114
- }
115
-
116
  const voice: TTSVoice = "CloΓ©e"
117
 
118
  setRuns(runsRef.current + 1)
119
 
120
  try {
121
  // console.log("starting transition, calling generateAnimation")
122
- story = await generateStory(promptDraft, voice)
123
-
124
- console.log("generated story:", story)
125
-
126
- if (!story) {
127
- throw new Error("invalid story")
128
- }
129
 
130
- (window as any)["debugJuju"] = story
131
 
132
- setStory(story)
133
 
134
  } catch (err) {
135
 
@@ -171,21 +180,28 @@ export function Generate() {
171
 
172
 
173
  useEffect(() => {
174
- if (!audioData) {
175
- return
176
- }
177
- console.log("story audio changed!", audioData)
 
178
 
179
- try {
180
- audio(audioData) // play
181
- } catch (err) {
182
- console.error(err)
 
 
 
 
 
183
  }
 
184
 
185
  return () => {
186
  audio() // stop
187
  }
188
- }, [audioData])
189
 
190
  return (
191
  <div
@@ -212,9 +228,10 @@ export function Generate() {
212
  <div
213
  className={cn(
214
  `flex flex-col`,
215
- `flex-grow rounded-2xl md:rounded-3xl`,
216
- `backdrop-blur-md bg-gray-800/30`,
217
- `border-2 border-white/10`,
 
218
  `items-center`,
219
  `space-y-6 md:space-y-8 lg:space-y-12 xl:space-y-14`,
220
  `px-3 py-6 md:px-6 md:py-12 xl:px-8 xl:py-14`,
@@ -252,14 +269,14 @@ export function Generate() {
252
  `w-full`,
253
  `input input-bordered rounded-full`,
254
  `transition-all duration-300 ease-in-out`,
 
255
  `placeholder:text-gray-400`,
256
  `disabled:bg-gray-500 disabled:text-yellow-300 disabled:border-transparent`,
257
  isLocked
258
- ? `bg-gray-600 text-yellow-300 border-transparent`
259
- : `bg-white/10 text-yellow-400 selection:bg-yellow-200`,
260
  `text-left`,
261
- `text-xl leading-10 px-6 h-16 pt-1`,
262
- `selection:bg-yellow-200 selection:text-yellow-200`
263
  )}
264
  value={promptDraft}
265
  onChange={e => setPromptDraft(e.target.value)}
@@ -276,7 +293,7 @@ export function Generate() {
276
  `flex flew-row ml-[-64px] items-center`,
277
  `transition-all duration-300 ease-in-out`,
278
  `text-base`,
279
- `bg-yellow-200`,
280
  `rounded-full`,
281
  `text-right`,
282
  `p-1`,
@@ -289,7 +306,7 @@ export function Generate() {
289
  <span>{nbCharsLimits}</span>
290
  </div>
291
  </div>
292
- <div className="flex flex-row w-52">
293
  <animated.button
294
  style={{
295
  textShadow: "0px 0px 1px #000000ab",
@@ -298,15 +315,16 @@ export function Generate() {
298
  onMouseEnter={() => setOverSubmitButton(true)}
299
  onMouseLeave={() => setOverSubmitButton(false)}
300
  className={cn(
301
- `px-6 py-3`,
302
  `rounded-full`,
303
  `transition-all duration-300 ease-in-out`,
 
304
  isLocked
305
- ? `bg-orange-500/20 border-orange-800/10`
306
- : `bg-yellow-500/80 hover:bg-yellow-400/100 border-yellow-800/20`,
307
  `text-center`,
308
  `w-full`,
309
- `text-2xl text-sky-50`,
310
  `border`,
311
  headingFont.className,
312
  // `transition-all duration-300`,
@@ -342,7 +360,9 @@ export function Generate() {
342
  `items-center`,
343
  `space-y-6 md:space-y-8 lg:space-y-12 xl:space-y-14`,
344
  `px-3 py-6 md:px-6 md:py-12 xl:px-8 xl:py-14`,
345
- story.text ? 'scale-1' : 'scale-0'
 
 
346
  )}>
347
  {assetUrl ? <div
348
  className={cn(
@@ -366,16 +386,23 @@ export function Generate() {
366
  `items-center justify-between`
367
  )}>
368
  <div className={cn(
369
- `flex flex-row flex-grow w-full`
370
  )}>
371
- <p>{storyText}</p>
 
 
 
 
 
 
 
 
372
  </div>
373
  </div>
374
  </div>
375
 
376
  </div>
377
 
378
-
379
  </TooltipProvider>
380
  </div>
381
  )
 
3
  import { useEffect, useRef, useState, useTransition } from "react"
4
  import { useSpring, animated } from "@react-spring/web"
5
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
6
+ import { split } from "sentence-splitter"
7
 
8
  import { useToast } from "@/components/ui/use-toast"
9
  import { cn } from "@/lib/utils"
10
  import { headingFont } from "@/app/interface/fonts"
11
  import { useCharacterLimit } from "@/lib/useCharacterLimit"
12
+ import { generateStoryLines } from "@/app/server/actions/generateStoryLines"
13
+ import { Story, StoryLine, TTSVoice } from "@/types"
 
 
14
  import { TooltipProvider } from "@radix-ui/react-tooltip"
 
15
  import { useCountdown } from "@/lib/useCountdown"
16
+ import { useAudio } from "@/lib/useAudio"
17
 
18
  import { Countdown } from "../countdown"
 
19
 
20
  type Stage = "generate" | "finished"
21
 
 
36
  const [runs, setRuns] = useState(0)
37
  const runsRef = useRef(0)
38
 
39
+ const currentLineIndexRef = useRef(0)
40
+ const [currentLineIndex, setCurrentLineIndex] = useState(0)
41
+
42
+ useEffect(() => {
43
+ currentLineIndexRef.current = currentLineIndex
44
+ }, [currentLineIndex])
45
+
46
+ const [storyLines, setStoryLines] = useState<StoryLine[]>([])
47
+
48
+ // computing those is cheap
49
+ const wholeStory = storyLines.map(line => line.text).join("\n")
50
+ const currentLine = storyLines.at(currentLineIndex)
51
+ const currentLineText = currentLine?.text || ""
52
+ const currentLineAudio = currentLine?.audio || ""
53
+
54
+ // reset the whole player when story changes
55
+ useEffect(() => {
56
+ setCurrentLineIndex(0)
57
+ }, [wholeStory])
58
 
59
  const [stage, setStage] = useState<Stage>("generate")
60
 
61
  const { toast } = useToast()
62
 
63
+ const audio = useAudio()
64
+
65
+ /*
66
+ // to simulate a "typing" effect
67
+ however.. we don't need this as we already have an audio player!
68
+
69
  const [typedStoryText, setTypedStoryText] = useState("")
70
  const [typedStoryCharacterIndex, setTypedStoryCharacterIndex] = useState(0)
71
 
 
 
72
  useEffect(() => {
73
  if (storyText && typedStoryCharacterIndex < storyText.length) {
74
  setTimeout(() => {
75
  setTypedStoryText(typedStoryText + story.text[typedStoryCharacterIndex])
76
  setTypedStoryCharacterIndex(typedStoryCharacterIndex + 1)
77
+ console.log("boom")
78
  }, 40)
79
  }
80
  }, [storyText, typedStoryCharacterIndex])
81
+ */
82
 
83
  const { progressPercent, remainingTimeInSec } = useCountdown({
84
  isActive: isLocked,
85
  timerId: runs, // everytime we change this, the timer will reset
86
+ durationInSec: /*stage === "interpolate" ? 30 :*/ 35, // it usually takes 40 seconds, but there might be lag
87
  onEnd: () => {}
88
  })
89
 
 
128
  const search = current.toString()
129
  router.push(`${pathname}${search ? `?${search}` : ""}`)
130
 
 
 
 
 
 
131
  const voice: TTSVoice = "CloΓ©e"
132
 
133
  setRuns(runsRef.current + 1)
134
 
135
  try {
136
  // console.log("starting transition, calling generateAnimation")
137
+ const newStoryLines = await generateStoryLines(promptDraft, voice)
 
 
 
 
 
 
138
 
139
+ console.log(`generated ${newStoryLines.length} story lines`)
140
 
141
+ setStoryLines(newStoryLines)
142
 
143
  } catch (err) {
144
 
 
180
 
181
 
182
  useEffect(() => {
183
+ const fn = async () => {
184
+ if (!currentLineAudio) {
185
+ return
186
+ }
187
+ console.log("story audio changed!")
188
 
189
+ try {
190
+ console.log("playing audio!")
191
+ await audio(currentLineAudio) // play
192
+ console.log("audio has ended, I think? let's go next!")
193
+ setCurrentLineIndex(currentLineIndexRef.current += 1)
194
+ // TODO change the line
195
+ } catch (err) {
196
+ console.error(err)
197
+ }
198
  }
199
+ fn()
200
 
201
  return () => {
202
  audio() // stop
203
  }
204
+ }, [currentLineAudio])
205
 
206
  return (
207
  <div
 
228
  <div
229
  className={cn(
230
  `flex flex-col`,
231
+ `flex-grow`,
232
+ // `rounded-2xl md:rounded-3xl`,
233
+ // `backdrop-blur-md bg-gray-800/30`,
234
+ // `border-2 border-white/10`,
235
  `items-center`,
236
  `space-y-6 md:space-y-8 lg:space-y-12 xl:space-y-14`,
237
  `px-3 py-6 md:px-6 md:py-12 xl:px-8 xl:py-14`,
 
269
  `w-full`,
270
  `input input-bordered rounded-full`,
271
  `transition-all duration-300 ease-in-out`,
272
+ `backdrop-blur-md `,
273
  `placeholder:text-gray-400`,
274
  `disabled:bg-gray-500 disabled:text-yellow-300 disabled:border-transparent`,
275
  isLocked
276
+ ? `bg-white/10 text-yellow-400/60 selection:bg-yellow-200/60 selection:text-yellow-200/60 border-transparent`
277
+ : `bg-white/10 text-yellow-400/100 selection:bg-yellow-200/100 selection:text-yellow-200/100`,
278
  `text-left`,
279
+ `text-2xl leading-10 px-6 h-16 pt-1`,
 
280
  )}
281
  value={promptDraft}
282
  onChange={e => setPromptDraft(e.target.value)}
 
293
  `flex flew-row ml-[-64px] items-center`,
294
  `transition-all duration-300 ease-in-out`,
295
  `text-base`,
296
+ // `bg-yellow-200`,
297
  `rounded-full`,
298
  `text-right`,
299
  `p-1`,
 
306
  <span>{nbCharsLimits}</span>
307
  </div>
308
  </div>
309
+ <div className="flex flex-row w-44">
310
  <animated.button
311
  style={{
312
  textShadow: "0px 0px 1px #000000ab",
 
315
  onMouseEnter={() => setOverSubmitButton(true)}
316
  onMouseLeave={() => setOverSubmitButton(false)}
317
  className={cn(
318
+ `px-4 h-16`,
319
  `rounded-full`,
320
  `transition-all duration-300 ease-in-out`,
321
+ `backdrop-blur-sm`,
322
  isLocked
323
+ ? `bg-orange-200/50 text-sky-50/80 border-yellow-600/10`
324
+ : `bg-yellow-400/70 text-sky-50 border-yellow-800/20 hover:bg-yellow-400/80`,
325
  `text-center`,
326
  `w-full`,
327
+ `text-2xl `,
328
  `border`,
329
  headingFont.className,
330
  // `transition-all duration-300`,
 
360
  `items-center`,
361
  `space-y-6 md:space-y-8 lg:space-y-12 xl:space-y-14`,
362
  `px-3 py-6 md:px-6 md:py-12 xl:px-8 xl:py-14`,
363
+ storyLines.length
364
+ ? 'scale-100'
365
+ : 'scale-0'
366
  )}>
367
  {assetUrl ? <div
368
  className={cn(
 
386
  `items-center justify-between`
387
  )}>
388
  <div className={cn(
389
+ `flex flex-col flex-grow w-full space-y-2 text-2xl text-blue-200/90`
390
  )}>
391
+ {storyLines.map((line, i) =>
392
+ <div
393
+ key={`${line.text}_${i}`}
394
+
395
+ // TODO change a color if we have progressed at the current index (i)
396
+ className={cn()}
397
+ >{
398
+ line.text
399
+ }</div>)}
400
  </div>
401
  </div>
402
  </div>
403
 
404
  </div>
405
 
 
406
  </TooltipProvider>
407
  </div>
408
  )
src/app/layout.tsx CHANGED
@@ -8,8 +8,8 @@ import './globals.css'
8
  const inter = Inter({ subsets: ['latin'] })
9
 
10
  export const metadata: Metadata = {
11
- title: 'AI Bedtime Story 🌜',
12
- description: 'AI Bedtime Story 🌜',
13
  }
14
 
15
  export default function RootLayout({
 
8
  const inter = Inter({ subsets: ['latin'] })
9
 
10
  export const metadata: Metadata = {
11
+ title: '🌜 AI Bedtime Story πŸ›οΈ',
12
+ description: '🌜 AI Bedtime Story πŸ›οΈ',
13
  }
14
 
15
  export default function RootLayout({
src/app/server/actions/{generateStory.ts β†’ generateStoryLines.ts} RENAMED
@@ -1,11 +1,11 @@
1
  "use server"
2
 
3
- import { Story, TTSVoice } from "@/types"
4
 
5
  const instance = `${process.env.AI_BEDTIME_STORY_API_GRADIO_URL || ""}`
6
  const secretToken = `${process.env.AI_BEDTIME_STORY_API_SECRET_TOKEN || ""}`
7
 
8
- export async function generateStory(prompt: string, voice: TTSVoice): Promise<Story> {
9
  if (!prompt?.length) {
10
  throw new Error(`prompt is too short!`)
11
  }
@@ -34,22 +34,18 @@ export async function generateStory(prompt: string, voice: TTSVoice): Promise<St
34
  // next: { revalidate: 1 }
35
  })
36
 
37
- console.log("res:", res)
38
- const rawJson = await res.json()
39
- console.log("rawJson:", rawJson)
40
- const data = rawJson.data as Story[]
41
- console.log("data:", data)
42
-
43
- const story = data?.[0] || { text: "", audio: "" }
44
 
45
- // console.log("story:", story)
 
46
 
47
- // Recommendation: handle errors
48
- if (res.status !== 200 || !story?.text || !story?.audio) {
49
 
50
- // This will activate the closest `error.js` Error Boundary
51
  throw new Error('Failed to fetch data')
52
  }
53
 
54
- return story
 
 
 
55
  }
 
1
  "use server"
2
 
3
+ import { Story, StoryLine, TTSVoice } from "@/types"
4
 
5
  const instance = `${process.env.AI_BEDTIME_STORY_API_GRADIO_URL || ""}`
6
  const secretToken = `${process.env.AI_BEDTIME_STORY_API_SECRET_TOKEN || ""}`
7
 
8
+ export async function generateStoryLines(prompt: string, voice: TTSVoice): Promise<StoryLine[]> {
9
  if (!prompt?.length) {
10
  throw new Error(`prompt is too short!`)
11
  }
 
34
  // next: { revalidate: 1 }
35
  })
36
 
 
 
 
 
 
 
 
37
 
38
+ const rawJson = await res.json()
39
+ const data = rawJson.data as StoryLine[][]
40
 
41
+ const stories = data?.[0] || []
 
42
 
43
+ if (res.status !== 200) {
44
  throw new Error('Failed to fetch data')
45
  }
46
 
47
+ return stories.map(line => ({
48
+ text: line.text.replaceAll(" .", ".").replaceAll(" ?", "?").replaceAll(" !", "!").trim(),
49
+ audio: line.audio
50
+ }))
51
  }
src/lib/useAudio.ts CHANGED
@@ -1,54 +1,80 @@
1
- import { useCallback, useEffect, useRef } from "react"
2
 
3
- /**
4
- * Custom React hook to play a Base64 WAV file.
5
- */
6
  export function useAudio() {
7
- // Reference to keep track of the current Audio object
8
- const audioRef = useRef<HTMLAudioElement | null>(null);
9
-
10
- // This cleanup function will stop the audio and clean up the audio element
11
- const stopAndCleanupAudio = useCallback(() => {
12
- if (audioRef.current) {
13
- audioRef.current.pause();
14
- audioRef.current.src = ''; // Release the object URL to avoid memory leaks
15
- audioRef.current = null;
16
- }
17
  }, []);
18
 
19
- // Function to load and play the audio, or stop it if called without parameters
 
 
 
 
 
20
  const playAudio = useCallback(
21
- (base64Data?: string) => {
22
- const base64str = `${base64Data || ""}`
23
- if (base64str) {
24
- const base64wav = base64str.startsWith("data:audio/wav")
25
- ? base64str
26
- : `data:audio/wav;base64,${base64str}`
27
-
28
- // Clean up any existing audio first
29
- stopAndCleanupAudio();
30
-
31
- // Create a new Audio object and start playing
32
- audioRef.current = new Audio(base64wav);
33
- audioRef.current.play().catch((e) => {
34
- console.error('Failed to play the audio:', e);
35
- });
36
- } else {
37
- // If no base64 data provided, stop the audio
38
- stopAndCleanupAudio();
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  },
41
- [stopAndCleanupAudio]
42
  );
43
 
44
  // Effect to handle cleanup on component unmount
45
  useEffect(() => {
46
  return () => {
47
- stopAndCleanupAudio();
48
  };
49
- }, [stopAndCleanupAudio]);
50
 
51
  // Return the playAudio function from the hook
52
  return playAudio;
53
- }
54
-
 
1
+ import { useCallback, useEffect, useRef } from 'react';
2
 
 
 
 
3
  export function useAudio() {
4
+ const audioContextRef = useRef<AudioContext | null>(null);
5
+
6
+ const stopAudio = useCallback(() => {
7
+ audioContextRef.current?.close();
8
+ audioContextRef.current = null;
 
 
 
 
 
9
  }, []);
10
 
11
+ // Helper function to handle conversion from Base64 to an ArrayBuffer
12
+ async function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
13
+ const response = await fetch(base64);
14
+ return response.arrayBuffer();
15
+ }
16
+
17
  const playAudio = useCallback(
18
+ async (base64Data?: string) => {
19
+ stopAudio(); // Stop any playing audio first
20
+
21
+ // If no base64 data provided, we don't attempt to play any audio
22
+ if (!base64Data) {
23
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
+
26
+ // Initialize AudioContext
27
+ const audioContext = new AudioContext();
28
+ audioContextRef.current = audioContext;
29
+
30
+ // Format Base64 string if necessary and get ArrayBuffer
31
+ const formattedBase64 =
32
+ base64Data.startsWith('data:audio/wav') || base64Data.startsWith('data:audio/wav;base64,')
33
+ ? base64Data
34
+ : `data:audio/wav;base64,${base64Data}`;
35
+
36
+ console.log(`formattedBase64: ${formattedBase64.slice(0, 50)} (len: ${formattedBase64.length})`);
37
+
38
+ const arrayBuffer = await base64ToArrayBuffer(formattedBase64);
39
+
40
+ return new Promise((resolve, reject) => {
41
+ // Decode the audio data and play
42
+ audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
43
+ // Create a source node and gain node
44
+ const source = audioContext.createBufferSource();
45
+ const gainNode = audioContext.createGain();
46
+
47
+ // Set buffer and gain
48
+ source.buffer = audioBuffer;
49
+ gainNode.gain.value = 1.0;
50
+
51
+ // Connect nodes
52
+ source.connect(gainNode);
53
+ gainNode.connect(audioContext.destination);
54
+
55
+ // Start playback and handle finishing
56
+ source.start();
57
+
58
+ source.onended = () => {
59
+ stopAudio();
60
+ resolve(true);
61
+ };
62
+ }, (error) => {
63
+ console.error('Error decoding audio data:', error);
64
+ reject(error);
65
+ });
66
+ })
67
  },
68
+ [stopAudio]
69
  );
70
 
71
  // Effect to handle cleanup on component unmount
72
  useEffect(() => {
73
  return () => {
74
+ stopAudio();
75
  };
76
+ }, [stopAudio]);
77
 
78
  // Return the playAudio function from the hook
79
  return playAudio;
80
+ }
 
src/types.ts CHANGED
@@ -197,7 +197,11 @@ export type QualityOption = {
197
  label: string
198
  }
199
 
200
- export type Story = {
201
  text: string
202
  audio: string // in base64
 
 
 
 
203
  }
 
197
  label: string
198
  }
199
 
200
+ export type StoryLine = {
201
  text: string
202
  audio: string // in base64
203
+ }
204
+
205
+ export type Story = {
206
+ lines: StoryLine[]
207
  }