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
|
- [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`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`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
|
- [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
|
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
|
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
|
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
|
`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
|
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
|
loudness measurement (`VESKSLOP_TTS_STREAM_GAIN_DB` compensates if needed) and cannot use VOX
|
||||||
prosody, which needs the whole sentence.
|
prosody, which needs the whole sentence.
|
||||||
The Half-Life voices are first-class entries in the voice list: **VOX (Half-Life)** and **HEV
|
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
|
fi
|
||||||
cat "$TMP/sw$INDEX.raw" >> "$OUT" 2>/dev/null || { echo "stream write failed" >&2; exit 1; }
|
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 [ "$GAP" -gt 0 ] && [ "$INDEX" -lt "$WORD_COUNT" ]; then
|
||||||
if "$MICROSLOP_FFMPEG" -hide_banner -loglevel error -y -f lavfi -i "anullsrc=r=48000:cl=mono" \
|
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}')" \
|
-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.
|
* 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 { AudioManager } from "./audio.js";
|
||||||
import type { CaptureSegment } from "./capture.js";
|
import type { CaptureSegment } from "./capture.js";
|
||||||
import { CapturePipeline } from "./capture.js";
|
import { CapturePipeline } from "./capture.js";
|
||||||
@@ -147,17 +147,35 @@ export class AudioService {
|
|||||||
return { queued: true, id, position: this.queue.length };
|
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> {
|
private async playWhenReady(file: string): Promise<void> {
|
||||||
for (let attempt = 0; attempt < 400; attempt++) {
|
for (let attempt = 0; attempt < 400; attempt++) {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
// not created yet
|
// not created yet
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 25));
|
await new Promise(resolve => setTimeout(resolve, 25));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await this.manager.play(file);
|
await this.manager.play(file);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
unlinkSync(`${file}.ready`);
|
||||||
|
} catch {
|
||||||
|
// best effort
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user