From b94982cc357b4c144bad8bc15d7a99635419f58d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 15 Jan 2026 12:06:02 +0000 Subject: [PATCH] fix[examples/transcript]: fix transitional deltas (#275) * fix transcript deltas * Update mcp-app.css --- examples/transcript-server/src/mcp-app.css | 9 ++- examples/transcript-server/src/mcp-app.ts | 68 +++++++++++++++++++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/examples/transcript-server/src/mcp-app.css b/examples/transcript-server/src/mcp-app.css index a7f70ce..23a9955 100644 --- a/examples/transcript-server/src/mcp-app.css +++ b/examples/transcript-server/src/mcp-app.css @@ -66,9 +66,14 @@ } .transcript-entry.interim { - opacity: 0.6; - font-style: italic; border-left: 3px solid var(--color-primary); + font-style: normal; +} + +/* Unstable/interim text within entry - dimmed and italic */ +.transcript-entry.interim .interim-delta { + opacity: 0.5; + font-style: italic; } .transcript-entry.sent { diff --git a/examples/transcript-server/src/mcp-app.ts b/examples/transcript-server/src/mcp-app.ts index 3ff8cf8..61adc7b 100644 --- a/examples/transcript-server/src/mcp-app.ts +++ b/examples/transcript-server/src/mcp-app.ts @@ -51,6 +51,8 @@ let animationFrame: number | null = null; // Speech Recognition let recognition: SpeechRecognition | null = null; +let committedResultCount = 0; // How many results have been committed as entries +let interimTranscript = ""; // Current interim (unstable) text // ============================================================================ // MCP App Setup @@ -155,22 +157,44 @@ function startSpeechRecognition(): boolean { recognition.onstart = () => { log.info("Speech recognition started"); + committedResultCount = 0; + interimTranscript = ""; }; recognition.onresult = (event) => { const e = event as SpeechRecognitionEvent; - for (let i = e.resultIndex; i < e.results.length; i++) { + + // Process results, committing newly-finalized ones + interimTranscript = ""; + + for (let i = 0; i < e.results.length; i++) { const result = e.results[i]; const transcript = result[0].transcript; if (result.isFinal) { - addTranscriptEntry(transcript, true); - updateSendButton(); - updateModelContext(); + // Only commit if this result hasn't been committed yet + if (i >= committedResultCount) { + const text = transcript.trim(); + if (text) { + clearInterimTranscript(); + addTranscriptEntry(text, true); + updateSendButton(); + updateModelContext(); + } + committedResultCount = i + 1; + } } else { - updateInterimTranscript(transcript); + // Accumulate all interim text + interimTranscript += transcript; } } + + // Show interim text if any + if (interimTranscript.trim()) { + updateInterimTranscript("", interimTranscript); + } else { + clearInterimTranscript(); + } }; recognition.onerror = (event) => { @@ -204,6 +228,9 @@ function startSpeechRecognition(): boolean { } function stopSpeechRecognition() { + // Commit any accumulated final transcript before stopping + commitFinalTranscript(); + if (recognition) { try { recognition.stop(); @@ -214,6 +241,19 @@ function stopSpeechRecognition() { } } +function commitFinalTranscript() { + // Commit any remaining interim text when stopping + const textToCommit = interimTranscript.trim(); + if (textToCommit) { + clearInterimTranscript(); + addTranscriptEntry(textToCommit, true); + updateSendButton(); + updateModelContext(); + } + committedResultCount = 0; + interimTranscript = ""; +} + // ============================================================================ // UI Helpers // ============================================================================ @@ -271,7 +311,7 @@ function addTranscriptEntry(text: string, isFinal: boolean) { transcriptEl.appendChild(entry); } -function updateInterimTranscript(text: string) { +function updateInterimTranscript(finalText: string, interimText: string) { clearTranscriptPlaceholder(); let interim = transcriptEl.querySelector( @@ -284,7 +324,21 @@ function updateInterimTranscript(text: string) { } const timestamp = new Date().toLocaleTimeString(); - interim.innerHTML = `
${timestamp}
${escapeHtml(text)}`; + + // Show final text (stable) in normal style, interim text (unstable) in delta style + const finalHtml = escapeHtml(finalText); + const interimHtml = interimText + ? `${escapeHtml(interimText)}` + : ""; + + interim.innerHTML = `
${timestamp}
${finalHtml}${interimHtml}`; +} + +function clearInterimTranscript() { + const interim = transcriptEl.querySelector(".transcript-entry.interim"); + if (interim) { + interim.remove(); + } } function escapeHtml(text: string): string {