From 672b161cda1cb2274926d5a241e59edfa5dbc86e Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Mon, 11 May 2026 22:46:38 +0200 Subject: [PATCH] fix(transcript): collapse rolling segment echoes Normalize incremental backend hypothesis chains before persistence and ignore stale or replayed webhook callbacks so duplicate transcript text does not survive ingest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/lib/components/RecordButton.svelte | 39 ++++--- src/lib/server/postprocess.ts | 127 +++++++++++++++++++++- src/routes/+page.svelte | 3 +- src/routes/api/webhook/[jobId]/+server.ts | 12 +- src/tests/audio.test.ts | 17 +-- src/tests/postprocess.test.ts | 44 ++++++++ src/tests/webhook.test.ts | 37 +++++++ 7 files changed, 246 insertions(+), 33 deletions(-) diff --git a/src/lib/components/RecordButton.svelte b/src/lib/components/RecordButton.svelte index 97b5de8..e322a2a 100644 --- a/src/lib/components/RecordButton.svelte +++ b/src/lib/components/RecordButton.svelte @@ -16,10 +16,10 @@ type RecordState = 'idle' | 'requesting' | 'recording' | 'stopping'; - let state = $state('idle'); - let error = $state(''); - let elapsed = $state(0); // seconds - let liveData = $state(null); + let recordState: RecordState = $state('idle'); + let error: string = $state(''); + let elapsed: number = $state(0); // seconds + let liveData: Float32Array | null = $state(null); let mediaRecorder: MediaRecorder | null = null; let chunks: Blob[] = []; @@ -60,12 +60,12 @@ async function startRecording() { error = ''; - state = 'requesting'; + recordState = 'requesting'; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch { error = 'Microphone access denied'; - state = 'idle'; + recordState = 'idle'; return; } @@ -81,11 +81,11 @@ elapsed = 0; timerInterval = setInterval(() => elapsed++, 1000); - state = 'recording'; + recordState = 'recording'; } function stopRecording() { - state = 'stopping'; + recordState = 'stopping'; mediaRecorder?.stop(); if (timerInterval) clearInterval(timerInterval); if (animFrame) cancelAnimationFrame(animFrame); @@ -99,7 +99,7 @@ const ext = mime.includes('ogg') ? 'ogg' : mime.includes('mp4') ? 'mp4' : 'webm'; const blob = new Blob(chunks, { type: mime }); const filename = `recording-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.${ext}`; - state = 'idle'; + recordState = 'idle'; ondone?.(blob, filename); } @@ -116,15 +116,18 @@ { length: IDLE_BARS }, (_, i) => 3 + Math.abs(Math.sin(i * 0.7) + Math.cos(i * 0.31)) * 20 ); + const liveBars = $derived.by(() => + liveData ? Array.from(liveData.slice(0, IDLE_BARS), (value) => Number(value)) : [] + );
- {#if state === 'recording'} + {#if recordState === 'recording'}
{formatTime(elapsed)} @@ -170,15 +173,15 @@
- {#if state === 'idle' || state === 'requesting'} + {#if recordState === 'idle' || recordState === 'requesting'} - {:else if state === 'recording'} + {:else if recordState === 'recording'}