Skip to content

Commit b94982c

Browse files
authored
fix[examples/transcript]: fix transitional deltas (#275)
* fix transcript deltas * Update mcp-app.css
1 parent c73d363 commit b94982c

2 files changed

Lines changed: 68 additions & 9 deletions

File tree

examples/transcript-server/src/mcp-app.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,14 @@
6666
}
6767

6868
.transcript-entry.interim {
69-
opacity: 0.6;
70-
font-style: italic;
7169
border-left: 3px solid var(--color-primary);
70+
font-style: normal;
71+
}
72+
73+
/* Unstable/interim text within entry - dimmed and italic */
74+
.transcript-entry.interim .interim-delta {
75+
opacity: 0.5;
76+
font-style: italic;
7277
}
7378

7479
.transcript-entry.sent {

examples/transcript-server/src/mcp-app.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ let animationFrame: number | null = null;
5151

5252
// Speech Recognition
5353
let recognition: SpeechRecognition | null = null;
54+
let committedResultCount = 0; // How many results have been committed as entries
55+
let interimTranscript = ""; // Current interim (unstable) text
5456

5557
// ============================================================================
5658
// MCP App Setup
@@ -155,22 +157,44 @@ function startSpeechRecognition(): boolean {
155157

156158
recognition.onstart = () => {
157159
log.info("Speech recognition started");
160+
committedResultCount = 0;
161+
interimTranscript = "";
158162
};
159163

160164
recognition.onresult = (event) => {
161165
const e = event as SpeechRecognitionEvent;
162-
for (let i = e.resultIndex; i < e.results.length; i++) {
166+
167+
// Process results, committing newly-finalized ones
168+
interimTranscript = "";
169+
170+
for (let i = 0; i < e.results.length; i++) {
163171
const result = e.results[i];
164172
const transcript = result[0].transcript;
165173

166174
if (result.isFinal) {
167-
addTranscriptEntry(transcript, true);
168-
updateSendButton();
169-
updateModelContext();
175+
// Only commit if this result hasn't been committed yet
176+
if (i >= committedResultCount) {
177+
const text = transcript.trim();
178+
if (text) {
179+
clearInterimTranscript();
180+
addTranscriptEntry(text, true);
181+
updateSendButton();
182+
updateModelContext();
183+
}
184+
committedResultCount = i + 1;
185+
}
170186
} else {
171-
updateInterimTranscript(transcript);
187+
// Accumulate all interim text
188+
interimTranscript += transcript;
172189
}
173190
}
191+
192+
// Show interim text if any
193+
if (interimTranscript.trim()) {
194+
updateInterimTranscript("", interimTranscript);
195+
} else {
196+
clearInterimTranscript();
197+
}
174198
};
175199

176200
recognition.onerror = (event) => {
@@ -204,6 +228,9 @@ function startSpeechRecognition(): boolean {
204228
}
205229

206230
function stopSpeechRecognition() {
231+
// Commit any accumulated final transcript before stopping
232+
commitFinalTranscript();
233+
207234
if (recognition) {
208235
try {
209236
recognition.stop();
@@ -214,6 +241,19 @@ function stopSpeechRecognition() {
214241
}
215242
}
216243

244+
function commitFinalTranscript() {
245+
// Commit any remaining interim text when stopping
246+
const textToCommit = interimTranscript.trim();
247+
if (textToCommit) {
248+
clearInterimTranscript();
249+
addTranscriptEntry(textToCommit, true);
250+
updateSendButton();
251+
updateModelContext();
252+
}
253+
committedResultCount = 0;
254+
interimTranscript = "";
255+
}
256+
217257
// ============================================================================
218258
// UI Helpers
219259
// ============================================================================
@@ -271,7 +311,7 @@ function addTranscriptEntry(text: string, isFinal: boolean) {
271311
transcriptEl.appendChild(entry);
272312
}
273313

274-
function updateInterimTranscript(text: string) {
314+
function updateInterimTranscript(finalText: string, interimText: string) {
275315
clearTranscriptPlaceholder();
276316

277317
let interim = transcriptEl.querySelector(
@@ -284,7 +324,21 @@ function updateInterimTranscript(text: string) {
284324
}
285325

286326
const timestamp = new Date().toLocaleTimeString();
287-
interim.innerHTML = `<div class="timestamp">${timestamp}</div>${escapeHtml(text)}`;
327+
328+
// Show final text (stable) in normal style, interim text (unstable) in delta style
329+
const finalHtml = escapeHtml(finalText);
330+
const interimHtml = interimText
331+
? `<span class="interim-delta">${escapeHtml(interimText)}</span>`
332+
: "";
333+
334+
interim.innerHTML = `<div class="timestamp">${timestamp}</div>${finalHtml}${interimHtml}`;
335+
}
336+
337+
function clearInterimTranscript() {
338+
const interim = transcriptEl.querySelector(".transcript-entry.interim");
339+
if (interim) {
340+
interim.remove();
341+
}
288342
}
289343

290344
function escapeHtml(text: string): string {

0 commit comments

Comments
 (0)