/** * Filler-word detection on word-timestamped transcripts. * * The single biggest creator-time-saver in long-form editing: scan the * transcript word-by-word, mark every "um" / "uh" / "like" / "you know" * range, then EMIT KEEP RANGES (the inverse). That keep list goes * straight into write_edl → import_edl → done. * * What counts as a "filler" is configurable. The defaults reflect the * canonical short list every YouTube auto-cutter (FireCut, Wisecut, * Descript) ships with. We also support multi-word phrases ("you know", * "i mean", "kind of") because the most distracting fillers are * two-word verbal tics. * * Pure logic only — no I/O. The tool wrapper handles transcript loading * and EDL emission. */ import type { Transcript } from "./whisper.js"; /** A contiguous range of words to remove. */ export interface FillerRange { startSec: number; endSec: number; /** The actual filler text (lowercase). Useful for surfacing to the user. */ text: string; /** Word indices within the source segment (start inclusive, end exclusive). */ startWordIndex: number; endWordIndex: number; } /** A range we keep (between or around fillers). */ export interface KeepRange { startSec: number; endSec: number; } /** * Default filler vocabulary. Single words and common multi-word verbal * tics. The matcher normalizes punctuation and case, so we list bare * surface forms. */ export declare const DEFAULT_FILLERS: readonly ["um", "uh", "uhm", "umm", "uhh", "er", "ah", "hmm", "you know", "i mean", "kind of", "sort of", "like", "actually", "basically", "literally", "honestly", "right", "so"]; export interface DetectFillersOptions { /** Filler vocabulary; defaults to DEFAULT_FILLERS. Case-insensitive. */ fillers?: readonly string[]; /** * Trim the filler-cut start by this many ms so the trailing audio of * the PREVIOUS word isn't clipped. Positive = keep more of the previous * word (cut starts later than whisper's word boundary). Default 0 ms. * * Note: this used to default to 20 ms with the OPPOSITE sign — the cut * would EXPAND outward by 20 ms on each side, eating ~5 s of speech * across a typical podcast cut. Reverted to a 0 default in commit * `keep more of the speaker's actual audio` (May 2026); set to a small * positive value (e.g. 15 ms) only if whisper is over-reporting word * end times in your specific dataset. */ paddingStartMs?: number; /** * Trim the filler-cut end by this many ms so the leading audio of the * NEXT word isn't clipped. Positive = keep more of the next word (cut * ends earlier than whisper's word boundary). Default 0 ms. See note on * paddingStartMs above for the bug-fix history. */ paddingEndMs?: number; /** * Minimum gap between adjacent fillers below which we MERGE them into * a single cut (avoids producing 3-frame clips). Default 150ms. */ mergeGapMs?: number; /** * Aggressive single words like "like" / "so" / "actually" / "right" / * "honestly" / "literally" / "basically" are filler ONLY when they * appear as standalone disfluencies. When `aggressiveSingleWords` is * true, these are included; when false (the default), only the safe-list * (um/uh/er/hmm/you know/i mean/kind of/sort of) is applied. * * Default is **false** — the conservative path — because the tool * permanently removes audio and words like "like"/"so" are frequently * substantive. Pass `true` for ruthless cuts on monologue-style content * where the speaker's verbal tics are clearly fillers. * * Changed from `true` default (May 2026): the old default caused * unintended removal of substantive speech for callers who didn't * explicitly configure this option. */ aggressiveSingleWords?: boolean; } /** * Find filler-word ranges across the whole transcript. Requires * word-level timestamps (transcribe with wordTimestamps=true). * * Returns ranges in temporal order with no overlaps; adjacent ranges * within `mergeGapMs` are merged into a single cut. */ export declare function detectFillerRanges(transcript: Transcript, opts?: DetectFillersOptions): FillerRange[]; /** * Compute KEEP ranges between fillers. The returned list is what you * pass into write_edl as a sequence of source-in / source-out pairs. * * Adjacent keep ranges with a tiny gap (< minKeepDurSec) get dropped — * a 40ms keep clip between two filler cuts is just noise. */ export declare function keepRangesFromFillers(fillers: FillerRange[], totalSec: number, minKeepDurSec?: number): KeepRange[]; /** * From a sorted keep-range list, compute the TIMELINE-space cut points. * * After `import_edl` lands the keep ranges concatenated onto the timeline, * each junction between adjacent keeps becomes a visible cut at the * timeline-relative timestamp = sum of all earlier keep durations. * * Example: keeps = [[0,10], [12,25], [28,40]] * timeline = [0..10] + [10..23] + [23..35] * cuts at = 10s, 23s (two junctions) * * Why this matters: tools like `add_sfx_to_timeline` and `add_sfx_at_cuts` * place SFX at TIMELINE timestamps. If callers pass SOURCE timestamps from * the transcript, the drift compounds with every cut and the SFX lands * progressively further out of sync (fine for the first ~30s, off by 20+s * by the end of a typical podcast). This helper exists to remove that * footgun. */ export declare function keepRangesToTimelineCuts(keeps: KeepRange[]): number[]; /** * Frame-align keep ranges. Rounds OUTWARD (start floors, end ceils) so * we never DROP speech that lives in a partial frame at the keep's edge. * * Was: rounds inward (start ceils, end floors). Inward rounding never * extends INTO an adjacent filler, but it ALSO loses up to one frame at * each end of every keep — ~33 ms at 30 fps, ~17 ms at 60 fps. Across * a 73-cut podcast that's 5+ seconds of speech disappearing into * sub-frame rounding error. * * Trade-off: outward rounding can include up to one frame of an adjacent * filler at each junction. That's ~33 ms at 30 fps — well within whisper * timing noise (typically ±50 ms) and inaudible compared to losing 5 s * of actual speech. Worth it. */ export declare function keepRangesToFrameRanges(keeps: KeepRange[], fps: number): Array<{ startFrame: number; endFrame: number; }>; /** * Quick stats for the agent to surface to the user. "I removed 47 * filler words totalling 8.3 seconds" is a much better summary than a * raw range list. */ export interface FillerStats { count: number; totalRemovedSec: number; /** Top-5 most frequent fillers, descending. */ topFillers: Array<{ text: string; count: number; }>; } export declare function summarizeFillers(fillers: FillerRange[]): FillerStats; //# sourceMappingURL=filler-words.d.ts.map