Don't show the live "recording"/listening indicator until the model is ready

startLive()'s browser-engine branch flipped liveStatus to "listening" (the
red live-dot, "Listening..." hint) the instant the mic was granted, even if
the Whisper pipeline was still loading — the user saw an active-looking
session while nothing was being transcribed yet. transcribeLiveSegment()
already correctly awaits the model-ready gate before posting a segment, so
this was purely a misleading-status bug.

Adds a "loading-model" LiveStatus between "starting" and "listening";
startLive() now waits on the same browserModelReady() gate the transcribe
path uses before claiming "listening", with race-safe cleanup if the load
fails while waiting. Server-engine live is unaffected.

Adds Decisions.md D-034; updates tests/live.md's manual verification steps.
This commit is contained in:
Lucas Winther committed 2026-07-30 08:31:23 +02:00
1 parent c5d193600a
commit 500cf3b36e
4 files changed
+116 -8

No files matched your search

+28 -2
View File
@@ -889,8 +889,34 @@ export function App() {
try {
const mic = await startMicCapture((chunk) => transcriber.push(chunk));
liveRef.current = { mic, transcriber };
setLiveStatus("listening");
const session = { mic, transcriber };
liveRef.current = session;
if (loadProgress.ready) {
// Pipeline already loaded for the current model/device — nothing to
// wait on, go straight to "listening" as before.
setLiveStatus("listening");
} else {
// Model not ready yet (first use, or a model/device switch): don't
// claim "Listening" until the pipeline actually is one, otherwise the
// live-dot/hint imply an active session while nothing is being
// transcribed (D-034). `transcribeLiveSegment` already awaits the
// same gate before posting, so no segments are lost either way.
setLiveStatus("loading-model");
try {
await browserModelReady();
// The session could have been torn down (Stop, or another start)
// while we were waiting — only advance if it's still this one.
if (liveRef.current === session) setLiveStatus("listening");
} catch {
// Load failed; the worker's own "error" handler already calls
// setError() for stage:"load" failures, so don't duplicate it here.
if (liveRef.current === session) {
liveRef.current = null;
await mic.stop().catch(() => {});
setLiveStatus("idle");
}
}
}
} catch (micError) {
setLiveStatus("idle");
const message =