Bugfixes for server-based microphone decoding..

This commit is contained in:
Lucas Winther
2026-07-28 07:37:22 +02:00
parent a0ef5548af
commit 1a1a162c53
2 changed files with 76 additions and 22 deletions
+67 -21
View File
@@ -685,34 +685,68 @@ export function App() {
if (options.task) wsUrl.searchParams.set("task", options.task); if (options.task) wsUrl.searchParams.set("task", options.task);
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
// Ends the session locally: stops the mic (releasing the hardware and
// the browser's recording indicator — nothing else holds this
// MediaStream) and clears the refs so a stale onclose/onerror firing
// later is a no-op (guarded by `liveWsRef.current === ws` below).
const endSession = (message: string | null) => {
liveWsRef.current = null;
const session = liveRef.current;
liveRef.current = null;
void session?.mic.stop().catch(() => {});
setLiveCommitted("");
setLiveTentative("");
setLiveStatus("idle");
if (message) setError(message);
};
ws.onmessage = (event) => { ws.onmessage = (event) => {
const data = JSON.parse(event.data); let data: { type: string; [key: string]: unknown };
try {
data = JSON.parse(event.data);
} catch {
// Not a frame we understand — ignore rather than crash the handler
// and strand the session in "listening"/"stopping" forever.
return;
}
if (data.type === "update") { if (data.type === "update") {
setLiveCommitted(data.update.committedText); const update = data.update as {
setLiveTentative(data.update.tentativeText); committedText: string;
setLivePending(data.update.pendingFinalPasses ?? 0); tentativeText: string;
if (data.update.error) setError(data.update.error); pendingFinalPasses?: number;
error?: string;
};
setLiveCommitted(update.committedText);
setLiveTentative(update.tentativeText);
setLivePending(update.pendingFinalPasses ?? 0);
if (update.error) setError(update.error);
} else if (data.type === "result") { } else if (data.type === "result") {
setResult(data.result); setResult(data.result as TranscriptResult);
setLiveCommitted(""); endSession(null);
setLiveTentative("");
setLiveStatus("idle");
liveWsRef.current = null;
liveRef.current = null;
} else if (data.type === "error") { } else if (data.type === "error") {
setError(data.error); endSession(typeof data.error === "string" ? data.error : "Live transcription failed.");
setLiveStatus("idle");
liveWsRef.current = null;
liveRef.current = null;
} }
}; };
ws.onerror = () => { ws.onerror = () => {
setError("WebSocket connection error."); // A genuine connection failure also fires "close" right after, which
setLiveStatus("idle"); // does the actual cleanup — this only needs to supply the message
liveWsRef.current = null; // (onclose can't distinguish a clean vs. abrupt closure on its own).
liveRef.current = null; if (liveWsRef.current === ws) {
setError(
"Lost connection to the server during live transcription. The microphone has been stopped.",
);
}
};
ws.onclose = () => {
// Fires after a normal finish too (onmessage already nulled the ref
// there), so this only acts on an *unexpected* disconnect — otherwise
// stop() would hang forever waiting for a "result" that never comes,
// and the mic would stay open with no code left holding a reference
// to it.
if (liveWsRef.current === ws) endSession(null);
}; };
ws.onopen = async () => { ws.onopen = async () => {
@@ -796,7 +830,19 @@ export function App() {
await session.mic.stop(); await session.mic.stop();
if (liveWsRef.current) { if (liveWsRef.current) {
liveWsRef.current.send(JSON.stringify({ type: "stop" })); if (liveWsRef.current.readyState === WebSocket.OPEN) {
liveWsRef.current.send(JSON.stringify({ type: "stop" }));
} else {
// The connection is already closing/closed — send() would silently
// drop the "stop" frame (WebSocket.send() does not throw for a
// non-OPEN socket), and nothing would ever answer with a "result",
// leaving the UI stuck on "Finishing up…" forever. Its close/error
// handler already stops the mic; just clear the ref and settle.
liveWsRef.current = null;
liveRef.current = null;
setLiveStatus("idle");
setError("Live transcription lost its connection to the server before it could finish.");
}
} else { } else {
try { try {
const final = await session.transcriber!.stop(); const final = await session.transcriber!.stop();
+9 -1
View File
@@ -65,7 +65,15 @@ export const liveWebSocket = {
message(ws: ServerWebSocket<LiveSessionData>, message: string | Buffer | Uint8Array) { message(ws: ServerWebSocket<LiveSessionData>, message: string | Buffer | Uint8Array) {
if (typeof message === "string") { if (typeof message === "string") {
const parsed = JSON.parse(message); let parsed: any;
try {
parsed = JSON.parse(message);
} catch {
// Not a frame we understand — ignore rather than crash the handler
// (which would otherwise take the whole connection down for every
// client sharing this Bun.serve process, not just this session).
return;
}
if (parsed.type === "stop") { if (parsed.type === "stop") {
const transcriber = (ws as any).transcriber as LiveTranscriber; const transcriber = (ws as any).transcriber as LiveTranscriber;
transcriber.stop().then((result) => { transcriber.stop().then((result) => {