Repository navigation
An accidental range nudge silently costs a full re-segmentation; the cheap edit is the one that asks #34
Description
Context
Found in the manual field test of PR #27 (issue #25, transform stage) on 2026-09-04, reported at #27 (comment) (item 9). It happened to the tester during the test: an accidental change to the timeline range started a full render, decode plus segmentation, with no confirmation and no way back. Every piece of this is working as designed, and #25 explicitly leaves the range in the cut key ("Changing the source crop or range without re-segmentation" is out of scope). What is undecided is whether the user should be asked, told, or able to undo before the cost is incurred. That is a product decision for the maintainer, so it is a separate issue rather than a commit on the PR.
Current State (verified 2026-09-04 on feat/issue-25-transform-stage at 094c783)
Two range-like edits sit close to each other with opposite costs:
| Edit | Where in the UI | In the cut cache key? | Cost |
|---|---|---|---|
Timeline range (SamplingSpec.start/end, src/matteloop/core/specs.py:53-57) |
IN/OUT handles (src/matteloop/ui/timeline.py:410-411), "Set IN" / "Set OUT" / "Reset Range" (timeline.py:244-248), and "Start" / "End" / "Duration" in the "Time & Sampling" section (src/matteloop/ui/inspector.py:646-650) |
Yes: cut_cache_key_inputs src/matteloop/core/fingerprints.py:187-221 includes _sampling(request.sampling) (:210), which hashes start, end, fps (:320-331) |
Full decode and segmentation |
Transform Trim (TransformSpec.first_frame/last_frame, specs.py:374) |
"Trim" heading in the Transform section (src/matteloop/ui/transform_group.py:292-298) |
No: transform does not appear in the mapping |
Framing and encode only |
A note on naming, since the tester reported "two controls named Trim": the literal pair in the UI is the "Global trim" checkbox in "Crop & Cleanup" (inspector.py:233, the FramingSpec.trim transparency trim) and the Transform "Trim" heading; both are cheap. The expensive control is the range, labelled IN/OUT and Start/End. Naming is therefore a contributing factor, not the cause.
The confirmation is on the wrong side. When Render is pressed:
- If a matching cut set exists, the user gets "Matching cut set found" with Rebuild preselected (
src/matteloop/ui/render_controller.py:349-375). That is the cheap path, and it asks. - If the range changed,
find_matching_cut_workspace(src/matteloop/jobs/render.py:549-575) computes a different key,open_promotedfinds nothing, and the function returnsNone(:571-577). The controller then goes straight to_resolve_collision(render_controller.py:313-325,:337-348): if the output file does not exist yet,_startruns immediately with no dialog. If it exists, the "Output already exists" dialog appears (:455-470), which talks about the file, not about segmentation. - A range edit does stale the preview (
src/matteloop/core/timeline_reducer.py:40-45), so if the preview was current, pressing Render first shows "Preview recommended" (render_controller.py:212-236,:238-256). Its text is about verifying the cutout; it says nothing about cost, and if the user previewed after the nudge (the natural thing to do), it does not appear at all.
None of the three dialogs that can precede a full render names the path being taken. The first signal is the job dialog itself, titled "Rendering video" with stage "Decode" (render_controller.py:645-652), after the job has started.
There is no undo. No undo of any kind exists in src/matteloop (grep for undo), and "Reset Range" resets to the full source, not to the previous range.
The transform stage does not notice. TransformStageController._state_changed reads state.parameters only (src/matteloop/ui/transform_stage.py:300-302); the session closes only on shutdown or when a new artifact opens. After a range nudge the result loop keeps playing the previous cut while the render request already points at a key that does not exist.
Related silent case, for completeness. The cut set is looked up under the output directory: open_promoted(request.output.directory, cache_key) (render.py:569), root <output>/.matteloop-work/cuts/<key> (src/matteloop/jobs/workspace/_platform.py:120-126, layout in _common.py:5-7). Changing the output directory (OutputDirectoryChanged, src/matteloop/core/parameters.py:132, reducer :296-303, which does not even stale the preview) therefore also re-segments silently, although the previous cut set is intact one directory over.
Why it matters
The transform stage exists so that trial and error is cheap. The one edit that is not cheap sits a few pixels from the ones that are, looks the same, and is the only one that starts the expensive path without asking. A user who has been burned once will stop touching the timeline, or stop trusting that Render is safe to press, which undoes the point of #25.
Directions, none chosen
- Confirm before discarding a valid cut set. The render controller already probes for a matching set (
render_controller.py:292-325); when the probe returnsNoneand a cut set for this source was previously in use, ask before starting the full path. Cheapest to build; costs one dialog on genuinely intended re-renders. - Name the path in the pre-render dialog. Whichever dialog precedes the start ("Preview recommended", "Output already exists", or a new one) states "This will decode and segment N frames" versus "This re-encodes the existing cut set". Addresses this issue and criterion 2 of issue A with one mechanism.
- Undo for the range, or a "restore the range of the last render" action next to "Reset Range". Reverts the accident instead of warning about it; does nothing for the intended-but-regretted case.
- Visual distinction in the inspector: mark the "Time & Sampling" fields (and the source crop, and the model) as "changes require segmentation", in contrast to Transform and Crop & Cleanup. Purely informational; needs no new control flow.
- Look up the cut set independently of the output directory, or fall back to the previous directory's set. Fixes only the related case.
Whatever is chosen should respect docs/engineering-guardrails.md:182 (G4): a warning, not a refusal, and an intended re-segmentation must remain one click away.
Acceptance Criteria (outcome-level)
- A render that will run segmentation because the range (or output directory) changed since the last render does not start without the user having seen, before the start, that it will.
- The cheap path is never asked more than the expensive one for the same request.
- An accidental range change can be reverted to the range of the last render without retyping it, or the user is warned before it costs anything; at least one of the two.
- The existing reuse dialog test (
tests/ui/test_render_controller.py:560) and the range test (:427) still pass or are updated with the decision; a new test covers theNoneprobe result on a source that had a valid cut set. uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -qgreen.
Out of Scope
- Taking the range out of the cut key or trimming the stored cut to a new range without segmentation. Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #25 rules that out; the frames do not exist.
- The general question of what "Render Video" does to a transform (issue A) and the "Edit crop" view mode (issue B).
Related
- Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #25 — range stays in the cut key by design.
- Issue A — the pre-render cost signal; direction 2 above covers both.
- PR Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #27 — field test comment, item 9.
Activity
smb commentedon Sep 5, 2026
This issue has partly drifted from the code. Verified on 2026-09-05 at
419e84b:The controller now closes the cut session when the source identity changes.
The session is retained only when the same source's range, model, edge
treatment or source crop change. So the described loss no longer applies to
loading a different video; it applies to nudging the range of the video already
open.
That narrower case is real and worth keeping open — it is the expensive edit that
does not ask, sitting next to cheap edits that look identical.
Related: a separate defect at the same boundary is filed as #60. A delayed reuse
probe can combine a captured request with a newer source identity, so a render
can process the previous video and have its output accepted as belonging to the
current one. Different mechanism, same place in
ui/render_controller.py, andwhoever fixes one should read the other.
Context in #66.