From d97f12ab490ea9c46409df10e5f261e0a896bea7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 20:01:02 +0000 Subject: [PATCH 1/2] Detect untranscribed filled pauses as [*] filler placeholders When VAD marks speech that no ASR token covers (typical um/uh that Whisper drops), insert whisper-timestamped-style [*] words into the transcript. They render distinctly in the transcript and word timeline, count as fillers, and are cut by Remove filler words. Short Silero hangover gaps are ignored so real word tails are not mistaken for fillers. Co-authored-by: Wassim Gharbi --- components/Timeline.tsx | 22 ++++-- components/TranscriptPanel.tsx | 10 ++- lib/disfluencies.ts | 130 ++++++++++++++++++++++++++++++++ lib/fillers.ts | 5 +- tests/disfluencies-test.ts | 112 +++++++++++++++++++++++++++ tests/fillers-test.ts | 9 +++ workers/transcription.worker.ts | 13 +++- 7 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 lib/disfluencies.ts create mode 100644 tests/disfluencies-test.ts diff --git a/components/Timeline.tsx b/components/Timeline.tsx index ecc1044..3bcdffc 100644 --- a/components/Timeline.tsx +++ b/components/Timeline.tsx @@ -33,6 +33,7 @@ import { trimEdgeBounds, } from "@/lib/edits"; import type { ClipSegment, Word } from "@/lib/types"; +import { isDisfluencyPlaceholder } from "@/lib/disfluencies"; import { VAD_SAMPLE_RATE } from "@/lib/vad"; import { useCutRanges } from "@/hooks/useCutRanges"; import { useIsDark } from "@/hooks/useIsDark"; @@ -726,6 +727,7 @@ export default function Timeline() { const hovered = hoveredWordId === w.id; const cutOut = isWordCutOut(w, cuts); const wordSelected = selectedWordIds.includes(w.id); + const placeholder = isDisfluencyPlaceholder(w.text); const showWordHandles = showHandles && (hovered || wWidth > 28); return (
setHoveredWordId(w.id)} onPointerLeave={() => diff --git a/components/TranscriptPanel.tsx b/components/TranscriptPanel.tsx index 003b2a1..c472acc 100644 --- a/components/TranscriptPanel.tsx +++ b/components/TranscriptPanel.tsx @@ -16,6 +16,7 @@ import { X, } from "lucide-react"; import { useEditorStore } from "@/lib/store"; +import { isDisfluencyPlaceholder } from "@/lib/disfluencies"; import { findFillerWordIds } from "@/lib/fillers"; import { findSilenceRanges } from "@/lib/silences"; import { @@ -60,18 +61,23 @@ const WordSpan = memo(function WordSpan({ active: boolean; onClick: (word: Word, el: HTMLElement) => void; }) { + const placeholder = isDisfluencyPlaceholder(word.text); // The trailing space lives inside the span so that selection and deletion // highlights are continuous across words instead of breaking at each gap. return ( onClick(word, e.currentTarget)} className={`py-0.5 cursor-pointer transition-colors duration-75 ${cutOut ? "word-deleted bg-red-50 text-red-600 line-through decoration-red-300 dark:bg-red-950/40 dark:text-red-400 dark:decoration-red-800" : active ? "bg-neutral-200/80 text-zinc-900 dark:bg-neutral-700/80 dark:text-zinc-50" - : "text-zinc-800 hover:bg-neutral-50 dark:text-zinc-200 dark:hover:bg-neutral-800/60" + : placeholder + ? "font-medium text-amber-700/90 hover:bg-amber-50 dark:text-amber-400/90 dark:hover:bg-amber-950/40" + : "text-zinc-800 hover:bg-neutral-50 dark:text-zinc-200 dark:hover:bg-neutral-800/60" }`} > {word.text}{" "} @@ -315,7 +321,7 @@ export default function TranscriptPanel() { {status === "ready" && fillerIds.length > 0 && (