diff --git a/apps/x/apps/renderer/scripts/generate-tour-audio.mjs b/apps/x/apps/renderer/scripts/generate-tour-audio.mjs index 2a180179..26d1535a 100644 --- a/apps/x/apps/renderer/scripts/generate-tour-audio.mjs +++ b/apps/x/apps/renderer/scripts/generate-tour-audio.mjs @@ -10,6 +10,10 @@ * Run whenever a step's narration text or the tour voice changes: * cd apps/x && npm run deps # script imports core's built output * node apps/renderer/scripts/generate-tour-audio.mjs + * + * Pass step ids to regenerate only those clips (e.g. to re-roll one whose + * synthesis came out glitchy): + * node apps/renderer/scripts/generate-tour-audio.mjs welcome done */ import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' @@ -29,15 +33,24 @@ if (start === -1 || end === -1) throw new Error('Could not locate TOUR_STEPS in const block = src.slice(start, end) const steps = [] -const re = /id: '([^']+)'[\s\S]*?text:\s*("[^"]*"|'[^']*')/g +// voiceText, when present, is the spoken variant of the bubble text. +const re = /id: '([^']+)'[\s\S]*?text:\s*("[^"]*"|'[^']*')(?:,\s*voiceText:\s*("[^"]*"|'[^']*'))?/g for (let m; (m = re.exec(block)); ) { - // The capture is a JS string literal from our own source; evaluate it to - // resolve the quoting. - steps.push({ id: m[1], text: new Function(`return ${m[2]}`)() }) + // The captures are JS string literals from our own source; evaluate them + // to resolve the quoting. + steps.push({ id: m[1], text: new Function(`return ${m[3] ?? m[2]}`)() }) } if (steps.length === 0) throw new Error('Parsed zero tour steps — regex out of sync with product-tour.tsx?') console.log(`Parsed ${steps.length} tour steps`) +const only = process.argv.slice(2) +if (only.length > 0) { + const unknown = only.filter((id) => !steps.some((s) => s.id === id)) + if (unknown.length > 0) throw new Error(`Unknown step ids: ${unknown.join(', ')}`) + steps.splice(0, steps.length, ...steps.filter((s) => only.includes(s.id))) + console.log(`Regenerating only: ${only.join(', ')}`) +} + await mkdir(outDir, { recursive: true }) for (const step of steps) { process.stdout.write(`synthesizing ${step.id}... `) diff --git a/apps/x/apps/renderer/src/assets/tour/welcome.mp3 b/apps/x/apps/renderer/src/assets/tour/welcome.mp3 index de478f4f..846fc872 100644 Binary files a/apps/x/apps/renderer/src/assets/tour/welcome.mp3 and b/apps/x/apps/renderer/src/assets/tour/welcome.mp3 differ diff --git a/apps/x/apps/renderer/src/components/product-tour.tsx b/apps/x/apps/renderer/src/components/product-tour.tsx index 45fe43b3..b6e5fe8d 100644 --- a/apps/x/apps/renderer/src/components/product-tour.tsx +++ b/apps/x/apps/renderer/src/components/product-tour.tsx @@ -40,6 +40,8 @@ type TourStep = { vignette?: TourVignetteKind title: string text: string + /** Spoken narration, when it should differ from the bubble text. */ + voiceText?: string } const TOUR_STEPS: TourStep[] = [ @@ -47,6 +49,7 @@ const TOUR_STEPS: TourStep[] = [ id: 'welcome', title: 'All aboard! ⚓', text: "I'm your captain for the next minute. The lights are down and the water's in — let me row you across Rowboat, one stop at a time. Use Next or your arrow keys.", + voiceText: "I'm your captain for the next minute. The lights are down and the water's in — let me row you across Rowboat, one stop at a time.", }, { id: 'home', @@ -518,7 +521,7 @@ export function ProductTour({ if (clip) { speakUrlRef.current(clip) } else if (ttsAvailableRef.current) { - speakRef.current(step.text) + speakRef.current(step.voiceText ?? step.text) } if (stepIndex === TOUR_STEPS.length - 1) { setConfettiOn(true)