Skip to content

An accidental range nudge silently costs a full re-segmentation; the cheap edit is the one that asks #34

Open

Description

@sensitron

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_promoted finds nothing, and the function returns None (: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, _start runs 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 returns None and 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)

  1. 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.
  2. The cheap path is never asked more than the expensive one for the same request.
  3. 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.
  4. 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 the None probe result on a source that had a valid cut set.
  5. uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -q green.

Out of Scope

Related

Activity

  1. smb commented on Sep 5, 2026

    @smb
    Collaborator

    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, and
    whoever fixes one should read the other.

    Context in #66.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions