feat: video ingest by clip ranges — timeline selection, range-scoped extraction - #780
Merged
Conversation
A source's cut is now (extraction_fps, ranges): TimeRange values are canonicalized in the kernel — clamped, sorted, merged, whole-clip collapsing to the empty selection — and stored on VideoProvenance, so the same clip over a different selection is a second source. The source-origin index grows a canonical-ranges term (migration 16, FORMAT_VERSION 15 -> 16); rows written before ranges existed need no backfill because an empty selection omits its JSON key. Extraction keeps the no-seek whole-clip read and applies the selection as a select filter over integer output frame numbers; frames keep their t=0 grid indices, so a selected frame is byte-identical to its whole-clip twin and content addressing collapses overlap. One exact count formula (ceil per half-open range) replaces the floor estimate that undercounted by one on fractional products. cf. #769
Video registration accepts a range selection everywhere a source is made: an optional ranges multipart field (a JSON array parsed against the kernel's own TimeRange, so a malformed selection is 422 VALIDATION_ERROR, never a traceback), a repeatable --range START:END on visionset ingest, and a ranges parameter on the MCP ingest tool. VideoProvenanceOut and the CLI/MCP wire projection publish the canonical selection back — the identity spelling, not what the caller typed. IngestStart is untouched: ranges never ride on the run. openapi.json, the generated client and docs/content/mcp-tools.md are regenerated; the wire sample carries a non-empty selection so parity checks the new half. cf. #769
Choosing a clip now mounts a player over an object URL and a hand-rolled multi-range track under it: drag creates a range, handles drag and nudge by one grid step (shift for ten), Delete removes, a click seeks, and the readout speaks in the merged form while segments stay as typed. The selection rides raw into registration — the kernel canonicalizes — and step 2 echoes the canonical ranges beside the probe. Both frame counts move from floor to the mirrored exact formula (clipRanges.ts), so the estimate now matches what extraction emits at every fractional boundary. A clip the browser cannot decode gets no timeline and one line saying it will be ingested whole; registration proceeds unchanged. No new dependencies. cf. #769
The range selection is documented where each surface already speaks: what a selection is and how it canonicalizes (sources, ingest), the exact ceil count superseding the floor estimate (ingest), the timeline and its degradation for a clip the browser cannot decode (ingest, ui), --range on the CLI, the ranges multipart field (api), and the reshaped five-term source-origin index with migration 16 (persistence). The vocabulary stays 'range'; 'partial' keeps meaning damage salvage. cf. #769
…d sizes its player Three review findings on the ingest timeline. A click inside a selected range now previews it the way an editor timeline does — the player seeks to that moment, plays, and stops where the range ends; a pause, ours or the person's, retires the preview, and a click outside every range scrubs without playing. Handle and track drags own their pointer (preventDefault + select-none), so dragging a handle no longer starts the browser's text selection and drags the page instead of the handle. The player is bounded to max-w-md — a preview, not a hero — while the track keeps the full panel width for drag precision. cf. #769
…he cut, timeline under both The video row no longer strands a small player against a field of whitespace. The timeline gains an aside slot: at md and up the compact player sits left and the cut's facts sit beside it — the extraction rate, the frame count and the selection, one aligned group instead of three corners — with the identity hint under them; below md the block stacks. The track spans the full panel under both, editor-style, flanked by 0:00 and the clip's duration, and an empty track invites the drag from inside itself, replacing the orphaned line that used to float below it. The selection sentence moved into the shared module (selectionSummary) so the fact column and the kernel's canonical form speak identically. cf. #769
max-w-md/max-h-56 to max-w-2xl/max-h-84 — the same 3:2 cap, 50% larger; the cut column still sits beside it at wide viewports and the track keeps the panel's full width. cf. #769
Accepted redesign: selection is discrete — a drag paints second cells, a range starts on an exact second and ends on one (the clip's partial final second is the one shorter cell), and handles and arrows walk boundaries. Faint per-second ticks make the cells legible, and only the marker seconds are labelled under the track, as plain numbers — no ruler. The playhead clamps to the track's inside, where 100% used to paint it on the border outside the rounded box. cf. #769
Three pieces from review. Playback started anywhere — the track or the player's own controls — now stops at the end of the clip it began inside: the boundary is armed by the play event itself, held in a ref the throttled timeupdate reads synchronously, and the stop snaps the indicator back onto the clip's edge instead of overshooting past it. The taller bar fills with exactly as many sampled frames as fit its width (slot = bar height x the clip's own aspect, one frame per slot center, rebuilt on real resizes), the movie-maker strip. And a selection now reads against that imagery: the unselected stretches wash out under a theme-aware scrim while a selected clip stays full-color inside a full-height two-pixel primary frame — the tint that vanished into the thumbnails is gone. cf. #769
Rebase adaptation: the clip timeline draws lucide's X — the Tabler package left the dependency tree and the icon gate retires it — and its preview test arms the play boundary through the testing library's own event helper, since the boundary gate now refuses a hand-built synthetic DOM event as an API. The b2iH tokens the component speaks (primary, muted, border, background, ring) survive the preset move unchanged. cf. #769
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a user ingests a video, they can select one or more time ranges on a preview timeline, and frame extraction happens only inside those ranges. Ranges are part of what the source is — the same clip registered over different ranges is a second source, exactly as a different extraction rate is today. The full decision record is the first comment on #769.
By layer:
TimeRangeand one canonicalization (clamp to the clip, sort, merge overlaps and touches; a selection covering the whole clip collapses to the empty one, so "whole clip" has a single identity spelling).VideoProvenance.rangesjoinsextraction_fpsin the source's identity: the service pre-check compares the pair and the source-origin index gains a canonical-ranges term — migration 16, FORMAT_VERSION 15 → 16, no backfill because an empty selection omits its JSON key and lands on the same index term as every pre-existing row. Extraction keeps the no-seek whole-clip read and applies the selection as aselectfilter over integer output frame numbers; frames keep their t = 0 grid indices, so a selected frame is byte-identical to its whole-clip twin and content addressing collapses the overlap of two selections over one file. One exact frame count — per half-open rangeceil(end × fps) − ceil(start × fps)— replaces the floor estimate that undercounted by one on every fractional product.rangesmultipart field on video registration, parsed against the kernel's own bounds so a malformed selection is 422VALIDATION_ERROR;VideoProvenanceOutpublishes the canonical form back; a repeatable--range START:ENDonvisionset ingest; a matchingrangesparameter on the MCP ingest tool. The ingest run request is untouched — ranges never ride on the run.openapi.json, the generated client anddocs/content/mcp-tools.mdare regenerated.The branch is rebased onto current main; the one adaptation that needed its own commit swaps the timeline onto the lucide icon set and rewrites a test that the new synthetic-DOM-event gate refuses.
Found, not fixed:
Test plan: targeted suites ran green at every phase (kernel, server, CLI, MCP, contract parity, ui-core vitest, scripts gates), the count formula and the
selectconstruction were mutation-verified, andscripts/check.shpassed in full — python, frontend, generated artifacts, both chromium suites. CI on this pull request is the exhaustive gate.Closes #769