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
+65 -19
View File
@@ -686,33 +686,67 @@ export function App() {
const ws = new WebSocket(wsUrl);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "update") {
setLiveCommitted(data.update.committedText);
setLiveTentative(data.update.tentativeText);
setLivePending(data.update.pendingFinalPasses ?? 0);
if (data.update.error) setError(data.update.error);
} else if (data.type === "result") {
setResult(data.result);
// 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");
liveWsRef.current = null;
liveRef.current = null;
if (message) setError(message);
};
ws.onmessage = (event) => {
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") {
const update = data.update as {
committedText: string;
tentativeText: string;
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") {
setResult(data.result as TranscriptResult);
endSession(null);
} else if (data.type === "error") {
setError(data.error);
setLiveStatus("idle");
liveWsRef.current = null;
liveRef.current = null;
endSession(typeof data.error === "string" ? data.error : "Live transcription failed.");
}
};
ws.onerror = () => {
setError("WebSocket connection error.");
setLiveStatus("idle");
liveWsRef.current = null;
liveRef.current = null;
// A genuine connection failure also fires "close" right after, which
// does the actual cleanup — this only needs to supply the message
// (onclose can't distinguish a clean vs. abrupt closure on its own).
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 () => {
@@ -796,7 +830,19 @@ export function App() {
await session.mic.stop();
if (liveWsRef.current) {
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 {
try {
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) {
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") {
const transcriber = (ws as any).transcriber as LiveTranscriber;
transcriber.stop().then((result) => {