/** * Punch-in (digital zoom on cuts) — the canonical YouTuber trick to * disguise jump cuts on a single-camera talking head. Cut filler → * punch in slightly on the kept side → the head-jerk vanishes. * * We implement this with an ffmpeg `crop` filter whose width / height / * x / y values are EXPRESSIONS that depend on `t` (current frame * timestamp). Outside any punch range, expressions evaluate to a * full-frame crop (no-op). Inside a range, they evaluate to a centered * sub-frame crop, then a `scale` brings it back to the original size. * * One ffmpeg pass, no segmenting + concat — fast and simple. * * The math, for a punch range with zoom Z (>1, e.g. 1.10 = 10% punch): * crop_w = floor(iw / Z) * crop_h = floor(ih / Z) * crop_x = floor((iw - crop_w) / 2) * crop_y = floor((ih - crop_h) / 2) * * To stitch multiple ranges into a single expression, we wrap each * tier in `if(between(t, a, b), , )`, falling back to a * full-frame crop outside any range. * * Pure logic; the tool wrapper handles ffmpeg invocation. */ export interface PunchInRange { /** Range start (seconds, inclusive). */ startSec: number; /** Range end (seconds, inclusive). */ endSec: number; /** * Zoom multiplier. 1.10 = 10% punch (subtle, the YouTuber default). * 1.0 = no zoom (effectively skipped). Clamped to [1.0, 2.0]. */ zoom: number; } export interface PunchInOptions { /** * Default zoom for ranges where `zoom` is omitted. Default 1.10. * 1.05 is barely perceptible; 1.15 is a clear pop; >1.20 looks like * an effect rather than a hidden cut. */ defaultZoom?: number; /** * Optional smoothing window (seconds) at each edge to prevent the * crop from snapping. The crop gradually ramps from 1.0 → zoom over * `rampSec` at the start, and back at the end. Default 0 (instant). * Use ~0.08s for subtle, ~0.15s for a noticeable push-in. */ rampSec?: number; } /** * Build the `-vf` value for ffmpeg that applies all punch ranges in * order. Returns the empty string if there are no ranges (no-op * caller can decide whether to skip ffmpeg altogether). * * The filter is structured as `crop=W:H:X:Y,scale=iw_orig:ih_orig`, * where W/H/X/Y are nested if-expressions evaluated per-frame. */ export declare function buildPunchInFilter(ranges: PunchInRange[], origWidth: number, origHeight: number, opts?: PunchInOptions): string; /** * Auto-derive punch-in ranges from a list of cut points (e.g. the * timestamps of every silence-cut or filler-cut). One short punch * after each cut hides the discontinuity. * * @param cutPoints Seconds where cuts happen. * @param totalSec Media duration (so the last range gets clipped). * @param holdSec How long each punch lasts after the cut. Default 1.5s. * @param zoom Zoom multiplier. Default 1.10. */ export declare function punchInsAfterCuts(cutPoints: number[], totalSec: number, holdSec?: number, zoom?: number): PunchInRange[]; //# sourceMappingURL=punch-in.d.ts.map