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 +12 -9
- package/dist-lib/viewer/exports.cjs +114 -97
- package/dist-lib/viewer/exports.cjs.map +1 -1
- package/dist-lib/viewer/exports.js +114 -97
- package/dist-lib/viewer/exports.js.map +1 -1
- package/docs/miriad-viz-script-writing.md +20 -0
- package/docs/miriad-viz-voice-generation.md +21 -6
- package/package.json +1 -1
- package/template/viewer/src/App.tsx +26 -0
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(
|
|
631
|
-
|
|
632
|
-
|
|
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("
|
|
636
|
-
output.push("
|
|
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("
|
|
639
|
-
output.push(" \u{
|
|
640
|
-
output.push("
|
|
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; }
|