Wait for the first streamed word before playing
This commit is contained in:
@@ -40,6 +40,12 @@ mobile draft at 390px.
|
||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||
- [x] C69 Streaming silence fix. Word mode with streaming on played nothing but the hum: the
|
||||
daemon started ffplay as soon as the streamed file passed 44 bytes (the bare WAV header), so
|
||||
the player hit EOF and exited before the first word landed. The wrapper now touches
|
||||
`<file>.ready` after the first word, and `playWhenReady` waits for it (size threshold and
|
||||
timeout fallbacks for other providers). Verified by capturing `veskslop_tts.monitor`: peak
|
||||
-11.2 dBFS with 7 word gaps, versus -48 dBFS (hum only) before.
|
||||
- [x] C68 Half-Life voices and a tidier Radio kit. `vox` and `hev` are now real voices in the
|
||||
list (Keith Bell with the VOX/HEV profile; VOX implies its prosody), so the preset dropdown and
|
||||
the VOX/clean toggles left the Radio kit; the clean option appears only for the VOX voice, and
|
||||
|
||||
+3
-1
@@ -170,7 +170,9 @@ from the TTS panel:
|
||||
s16le, so only the character changes. Echo gained `tts.echoDelayMs` (10-400) and
|
||||
`tts.echoLevel` (0-0.95) alongside the amount. **Stream word mode** (`tts.streamWords`, on by
|
||||
default) makes the wrapper write the WAV while it synthesizes, so playback starts on the first
|
||||
word: measured first sound 0.5 s instead of 2.4 s on a ten-word line. Streaming skips the
|
||||
word: measured first sound 0.5 s instead of 2.4 s on a ten-word line. The wrapper touches
|
||||
`<file>.ready` after the first word and the daemon waits for that before starting playback, so a
|
||||
player never opens the bare WAV header and exits before the words arrive. Streaming skips the
|
||||
loudness measurement (`VESKSLOP_TTS_STREAM_GAIN_DB` compensates if needed) and cannot use VOX
|
||||
prosody, which needs the whole sentence.
|
||||
The Half-Life voices are first-class entries in the voice list: **VOX (Half-Life)** and **HEV
|
||||
|
||||
@@ -602,6 +602,9 @@ if [ "$STREAM_WORDS" = "1" ]; then
|
||||
fi
|
||||
cat "$TMP/sw$INDEX.raw" >> "$OUT" 2>/dev/null || { echo "stream write failed" >&2; exit 1; }
|
||||
|
||||
# The player waits for this marker so it does not start on the bare header.
|
||||
if [ "$INDEX" = "1" ]; then : > "$OUT.ready"; fi
|
||||
|
||||
if [ "$GAP" -gt 0 ] && [ "$INDEX" -lt "$WORD_COUNT" ]; then
|
||||
if "$MICROSLOP_FFMPEG" -hide_banner -loglevel error -y -f lavfi -i "anullsrc=r=48000:cl=mono" \
|
||||
-t "$(awk -v g="$GAP" 'BEGIN{printf "%.3f", g/1000}')" \
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* them in the transcript, and runs speech to text when a provider is set.
|
||||
*/
|
||||
|
||||
import { statSync } from "node:fs";
|
||||
import { statSync, unlinkSync } from "node:fs";
|
||||
import { AudioManager } from "./audio.js";
|
||||
import type { CaptureSegment } from "./capture.js";
|
||||
import { CapturePipeline } from "./capture.js";
|
||||
@@ -147,17 +147,35 @@ export class AudioService {
|
||||
return { queued: true, id, position: this.queue.length };
|
||||
}
|
||||
|
||||
/** Wait for the wrapper's streaming file to appear, then play it while it grows. */
|
||||
/**
|
||||
* Wait until the wrapper's streaming file has real audio in it, then play it while it
|
||||
* grows. The wrapper touches `<file>.ready` after the first word, which is the reliable
|
||||
* signal; a size threshold and a timeout cover providers that do not mark readiness.
|
||||
*/
|
||||
private async playWhenReady(file: string): Promise<void> {
|
||||
for (let attempt = 0; attempt < 400; attempt++) {
|
||||
try {
|
||||
if (statSync(file).size > 44) break;
|
||||
if (statSync(`${file}.ready`).size >= 0) break;
|
||||
} catch {
|
||||
// no marker yet
|
||||
}
|
||||
try {
|
||||
if (statSync(file).size > 100_000) break;
|
||||
} catch {
|
||||
// not created yet
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
}
|
||||
|
||||
try {
|
||||
await this.manager.play(file);
|
||||
} finally {
|
||||
try {
|
||||
unlinkSync(`${file}.ready`);
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
||||
Reference in New Issue
Block a user