miriad-viz 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/index.js CHANGED
@@ -627,18 +627,21 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
627
627
  output.push(" Each line has an id, speaker, text, and style.");
628
628
  output.push("");
629
629
  output.push(" WORKFLOW:");
630
- output.push(" 1. Ask human about voice preferences per speaker");
631
- output.push(" 2. Generate one audio file per script line");
632
- output.push(" 3. Save as: audio/{lineId}.mp3");
630
+ output.push(
631
+ " 1. List available voices: GET /v1/voices (header: xi-api-key: $ELEVENLABS_API_KEY)"
632
+ );
633
+ output.push(" 2. Read data/script.json \u2014 get speakers and line counts");
634
+ output.push(' 3. Suggest a voice for each speaker (be proactive, not "what do you want?")');
635
+ output.push(" 4. Generate one test clip per speaker for human approval");
636
+ output.push(" 5. Generate all clips: audio/{lineId}.mp3");
633
637
  output.push(" \u26A0\uFE0F Filename MUST match script line ID exactly");
634
638
  output.push(" narrator-01 \u2192 audio/narrator-01.mp3");
635
- output.push(" 4. Human listens and gives feedback");
636
- output.push(" 5. Regenerate specific lines as needed");
639
+ output.push(" 6. Human listens and gives feedback");
640
+ output.push(" 7. Regenerate specific lines as needed");
637
641
  output.push("");
638
- output.push(" CAPABILITIES:");
639
- output.push(" \u{1F5E3}\uFE0F Text-to-Speech \u2014 multi-speaker with distinct voices");
640
- output.push(" API: POST /v1/text-to-speech/{voice_id}");
641
- output.push(" Voices: https://elevenlabs.io/voice-library");
642
+ output.push(" API:");
643
+ output.push(" \u{1F50D} List voices: GET /v1/voices");
644
+ output.push(" \u{1F5E3}\uFE0F Generate clip: POST /v1/text-to-speech/{voice_id}");
642
645
  output.push("");
643
646
  output.push(" When approved: npx miriad-viz next --voices-approved");
644
647
  output.push("");
@@ -2102,6 +2102,103 @@ var textStyle2 = {
2102
2102
  textAlign: "center",
2103
2103
  maxWidth: "100%"
2104
2104
  };
2105
+
2106
+ // src/viewer/audio-position.ts
2107
+ function progressToAudioPosition(progress, timingFile) {
2108
+ if (timingFile.lines.length === 0) return null;
2109
+ if (progress < 0 || progress > 1) return null;
2110
+ for (let i = 0; i < timingFile.lines.length; i++) {
2111
+ const line = timingFile.lines[i];
2112
+ if (progress >= line.progressStart && progress <= line.progressEnd) {
2113
+ const progressRange = line.progressEnd - line.progressStart;
2114
+ if (progressRange <= 0) {
2115
+ return {
2116
+ clipId: line.id,
2117
+ offsetSec: 0,
2118
+ audioFile: line.audioFile,
2119
+ lineIndex: i
2120
+ };
2121
+ }
2122
+ const fraction = (progress - line.progressStart) / progressRange;
2123
+ const offsetSec = fraction * line.durationSec;
2124
+ return {
2125
+ clipId: line.id,
2126
+ offsetSec,
2127
+ audioFile: line.audioFile,
2128
+ lineIndex: i
2129
+ };
2130
+ }
2131
+ }
2132
+ return null;
2133
+ }
2134
+
2135
+ // src/viewer/useAudioPlayback.ts
2136
+ var SEEK_TOLERANCE_SEC = 0.15;
2137
+ function useAudioPlayback({
2138
+ progress,
2139
+ playing,
2140
+ timingFile,
2141
+ audioBaseUrl
2142
+ }) {
2143
+ const audioCache = react.useRef(/* @__PURE__ */ new Map());
2144
+ const activeClipRef = react.useRef(null);
2145
+ const baseUrl = audioBaseUrl.endsWith("/") ? audioBaseUrl : `${audioBaseUrl}/`;
2146
+ const getAudio = react.useCallback(
2147
+ (clipId, audioFile) => {
2148
+ const cached = audioCache.current.get(clipId);
2149
+ if (cached) return cached;
2150
+ const audio = new Audio(`${baseUrl}${audioFile}`);
2151
+ audio.preload = "auto";
2152
+ audioCache.current.set(clipId, audio);
2153
+ return audio;
2154
+ },
2155
+ [baseUrl]
2156
+ );
2157
+ const stopAll = react.useCallback(() => {
2158
+ for (const audio of audioCache.current.values()) {
2159
+ audio.pause();
2160
+ }
2161
+ activeClipRef.current = null;
2162
+ }, []);
2163
+ react.useEffect(() => {
2164
+ return () => {
2165
+ for (const audio of audioCache.current.values()) {
2166
+ audio.pause();
2167
+ audio.src = "";
2168
+ }
2169
+ audioCache.current.clear();
2170
+ };
2171
+ }, []);
2172
+ react.useEffect(() => {
2173
+ if (!timingFile || !playing) {
2174
+ stopAll();
2175
+ return;
2176
+ }
2177
+ const position = progressToAudioPosition(progress, timingFile);
2178
+ if (!position) {
2179
+ stopAll();
2180
+ return;
2181
+ }
2182
+ const { clipId, offsetSec, audioFile } = position;
2183
+ const audio = getAudio(clipId, audioFile);
2184
+ if (activeClipRef.current !== clipId) {
2185
+ stopAll();
2186
+ activeClipRef.current = clipId;
2187
+ audio.currentTime = offsetSec;
2188
+ audio.play().catch(() => {
2189
+ });
2190
+ } else {
2191
+ const drift = Math.abs(audio.currentTime - offsetSec);
2192
+ if (drift > SEEK_TOLERANCE_SEC) {
2193
+ audio.currentTime = offsetSec;
2194
+ }
2195
+ if (audio.paused) {
2196
+ audio.play().catch(() => {
2197
+ });
2198
+ }
2199
+ }
2200
+ }, [progress, playing, timingFile, getAudio, stopAll]);
2201
+ }
2105
2202
  var FALLBACK_DURATION = 120;
2106
2203
  function usePlayback(durationSeconds = FALLBACK_DURATION) {
2107
2204
  const [progress, setProgress] = react.useState(0);
@@ -2236,9 +2333,12 @@ function SceneContents({ devMode, progress }) {
2236
2333
  ] });
2237
2334
  }
2238
2335
  var VIZ_DATA_URL = "./data/viz-data.json";
2336
+ var TIMING_URL = "./data/timing.json";
2337
+ var AUDIO_BASE_URL = "./audio/";
2239
2338
  var activeTestScene = getTestScene();
2240
2339
  function App() {
2241
2340
  const [vizData, setVizData] = react.useState(null);
2341
+ const [timingFile, setTimingFile] = react.useState(null);
2242
2342
  const [error, setError] = react.useState(null);
2243
2343
  const [devMode, setDevMode] = react.useState(getInitialDevMode);
2244
2344
  const viewportOverride = useViewportOverride();
@@ -2324,6 +2424,20 @@ function App() {
2324
2424
  chunkLJG3H4FA_cjs.useVizStore.getState().setDataSummary(extractDataSummary(enriched));
2325
2425
  }).catch((err) => setError(err instanceof Error ? err.message : String(err)));
2326
2426
  }, []);
2427
+ react.useEffect(() => {
2428
+ if (activeTestScene) return;
2429
+ fetch(TIMING_URL).then((res) => {
2430
+ if (!res.ok) return null;
2431
+ return res.json();
2432
+ }).then((data4) => setTimingFile(data4)).catch(() => {
2433
+ });
2434
+ }, []);
2435
+ useAudioPlayback({
2436
+ progress: playback.progress,
2437
+ playing: playback.playing,
2438
+ timingFile,
2439
+ audioBaseUrl: AUDIO_BASE_URL
2440
+ });
2327
2441
  react.useMemo(() => {
2328
2442
  chunkLJG3H4FA_cjs.useVizStore.getState().setPlaying(playback.playing);
2329
2443
  }, [playback.playing]);
@@ -2522,103 +2636,6 @@ var errorStyle = {
2522
2636
  fontFamily: "monospace"
2523
2637
  };
2524
2638
 
2525
- // src/viewer/audio-position.ts
2526
- function progressToAudioPosition(progress, timingFile) {
2527
- if (timingFile.lines.length === 0) return null;
2528
- if (progress < 0 || progress > 1) return null;
2529
- for (let i = 0; i < timingFile.lines.length; i++) {
2530
- const line = timingFile.lines[i];
2531
- if (progress >= line.progressStart && progress <= line.progressEnd) {
2532
- const progressRange = line.progressEnd - line.progressStart;
2533
- if (progressRange <= 0) {
2534
- return {
2535
- clipId: line.id,
2536
- offsetSec: 0,
2537
- audioFile: line.audioFile,
2538
- lineIndex: i
2539
- };
2540
- }
2541
- const fraction = (progress - line.progressStart) / progressRange;
2542
- const offsetSec = fraction * line.durationSec;
2543
- return {
2544
- clipId: line.id,
2545
- offsetSec,
2546
- audioFile: line.audioFile,
2547
- lineIndex: i
2548
- };
2549
- }
2550
- }
2551
- return null;
2552
- }
2553
-
2554
- // src/viewer/useAudioPlayback.ts
2555
- var SEEK_TOLERANCE_SEC = 0.15;
2556
- function useAudioPlayback({
2557
- progress,
2558
- playing,
2559
- timingFile,
2560
- audioBaseUrl
2561
- }) {
2562
- const audioCache = react.useRef(/* @__PURE__ */ new Map());
2563
- const activeClipRef = react.useRef(null);
2564
- const baseUrl = audioBaseUrl.endsWith("/") ? audioBaseUrl : `${audioBaseUrl}/`;
2565
- const getAudio = react.useCallback(
2566
- (clipId, audioFile) => {
2567
- const cached = audioCache.current.get(clipId);
2568
- if (cached) return cached;
2569
- const audio = new Audio(`${baseUrl}${audioFile}`);
2570
- audio.preload = "auto";
2571
- audioCache.current.set(clipId, audio);
2572
- return audio;
2573
- },
2574
- [baseUrl]
2575
- );
2576
- const stopAll = react.useCallback(() => {
2577
- for (const audio of audioCache.current.values()) {
2578
- audio.pause();
2579
- }
2580
- activeClipRef.current = null;
2581
- }, []);
2582
- react.useEffect(() => {
2583
- return () => {
2584
- for (const audio of audioCache.current.values()) {
2585
- audio.pause();
2586
- audio.src = "";
2587
- }
2588
- audioCache.current.clear();
2589
- };
2590
- }, []);
2591
- react.useEffect(() => {
2592
- if (!timingFile || !playing) {
2593
- stopAll();
2594
- return;
2595
- }
2596
- const position = progressToAudioPosition(progress, timingFile);
2597
- if (!position) {
2598
- stopAll();
2599
- return;
2600
- }
2601
- const { clipId, offsetSec, audioFile } = position;
2602
- const audio = getAudio(clipId, audioFile);
2603
- if (activeClipRef.current !== clipId) {
2604
- stopAll();
2605
- activeClipRef.current = clipId;
2606
- audio.currentTime = offsetSec;
2607
- audio.play().catch(() => {
2608
- });
2609
- } else {
2610
- const drift = Math.abs(audio.currentTime - offsetSec);
2611
- if (drift > SEEK_TOLERANCE_SEC) {
2612
- audio.currentTime = offsetSec;
2613
- }
2614
- if (audio.paused) {
2615
- audio.play().catch(() => {
2616
- });
2617
- }
2618
- }
2619
- }, [progress, playing, timingFile, getAudio, stopAll]);
2620
- }
2621
-
2622
2639
  Object.defineProperty(exports, "SIM_HOURS_PER_SECOND", {
2623
2640
  enumerable: true,
2624
2641
  get: function () { return chunkIHAAQH6X_cjs.SIM_HOURS_PER_SECOND; }