Repository navigation
Nothing in the UI says how a transform becomes a file, or what it will cost #33
Description
Context
Found in the manual field test of PR #27 (issue #25, transform stage) on 2026-09-04, reported at #27 (comment) (item 7). Five defects from that test were fixed on feat/issue-25-transform-stage (9a3e102, da4940f, da61a22, 4da87d2, 094c783). This one is not a defect with an obvious fix: the behaviour is the one issue #25 asked for, and the question is what the UI should say about it. That is a product decision for the maintainer, so it is filed separately rather than as another commit on the PR.
The field tester is an experienced user of the app, not a newcomer, and asked twice during the test how to save a transform.
Current State (verified 2026-09-04 on feat/issue-25-transform-stage at 094c783)
| Fact | Where |
|---|---|
| Editing Trim, Crop or Resize updates the result player only; the WebP on disk changes on an explicit render. This is the design decision recorded in #25 ("Editing a transform field updates only the result player (preview). The WebP on disk changes only on an explicit Rebuild click"). | _reduce_transform src/matteloop/core/parameters.py:330-339 replaces the spec in state and nothing else; the sidecar is written only inside the rebuild, src/matteloop/jobs/render.py:1426 |
| The only control that writes the transform out is "Render Video". | src/matteloop/ui/action_shelf.py:25; enabled from can_render, src/matteloop/ui/presenter.py:132 |
| There is no Apply/Commit affordance in the Transform group or the Output section, no dirty marker, no badge, no button-text change, and no copy anywhere stating the above. | src/matteloop/ui/transform_group.py:290-330 (layout: Trim, Crop, Resize rows, readout), src/matteloop/ui/inspector.py:663-675 (Output: Directory, Filename, Maximum size) |
| A transform edit deliberately does not stale the preview, so the existing "Settings changed — preview again" marker that every framing edit produces never appears for a transform. | parameters.py:331 docstring "a transform never stales the preview"; the marker comes from PreviewInvalidated, src/matteloop/ui/presenter.py:59 |
can_render is not conditioned on the transform being valid. The button stays enabled while the Resize section is flagging its own inline error. |
src/matteloop/core/state.py:743; _RESIZE_ERROR transform_group.py:62, shown by _show_resize_error :587-589; a failing validate_for is shown in the readout, :548-561 |
The readout puts the pending size and the rendered size in one grey line, e.g. 20 of 25 frames · 1.333 s · 384 × 128 px · rendered 634 × 719 px, 4.3 MiB. That line is the only place the two differ. |
_readout_text transform_group.py:563-578 |
| A transform-only change takes the rebuild path and skips segmentation, but the user learns that only after pressing Render, from the "Matching cut set found" dialog ("Rebuild reuses the cuts and only reruns framing and encoding"). | render.py:1027-1039; dialog src/matteloop/ui/render_controller.py:349-375 |
| After choosing Rebuild, a second dialog follows because the output file already exists ("Output already exists", Cancel is the default). Two confirmations for a re-encode that changes nothing in the cut set. | render_controller.py:337-348, :455-470 |
| The job dialog then titles the run "Rebuilding from edited cuts" although no cut was edited. | render_controller.py:645-648 |
| There is a "Rebuild from edited cuts" button in the Workspace section, which is not the rebuild the transform stage needs and stays disabled in this flow. | src/matteloop/ui/inspector.py:136; can_rebuild state.py:744-749 |
The tester read the readout line as a defect ("the resize does nothing"), because a changed size next to an unchanged "rendered" size has no other signal attached to it.
What the user must be able to tell at a glance
Both questions the tester asked are the same one seen from two sides: what happens when I press the button, and what will it cost me? Whatever the visual solution is, a user looking at the Transform section and the action shelf should be able to answer, without pressing anything:
- Whether the loop on screen matches the file on disk, or has pending changes.
- Which control writes the pending changes to the file. Today that is "Render Video", whose name does not say that it also re-encodes.
- Whether pressing it will re-segment (minutes) or only re-encode (seconds). The distinction already exists in the code (
find_matching_cut_workspace,render.py:549-575) and is decided before the job starts; it is simply not surfaced until afterwards. - Whether the button will fail on the current input. A render with an invalid resize should not be offered as if it would succeed.
Point 3 is the one with the largest effect on behaviour: the fear of a multi-minute re-render suppresses exactly the trial-and-error this stage exists for. The cheap path is the common one for the transform stage, and nothing tells the user that in advance.
Directions, none chosen
Listed to frame the decision, not to prescribe it. They are not mutually exclusive.
- A pending-state marker on the Transform section or the result surface, analogous to the existing "Settings changed — preview again" marker, saying that the loop shows staged changes and naming the button that writes them.
- Copy on the shelf or the button: the button text or an adjacent line could state the path it will take ("Re-encode" vs "Render"), computed from the same probe the render controller runs today (
render_controller.py:292-325), or at least from whether anything outside the transform changed since the last render. - Gate
can_renderon transform validity, or keep it enabled and let the inline error carry the explanation; both are defensible, but today neither the button nor the readout says which will happen. - Fold the two dialogs into one for the rebuild path, or drop the collision dialog when the rebuild replaces the file the same request produced.
- Rename the job dialog title for a transform rebuild so it does not claim edited cuts.
The tester's own first suggestion was a separate "Apply" button. Issue B (the "Edit crop" view switch) argues why that would make the file-writing control less clear rather than more; the argument applies here too.
Acceptance Criteria (outcome-level, independent of the chosen visual)
- With a cut open and a transform field changed, the UI states that the result loop shows unrendered changes and names the control that writes them, without the user hovering or pressing anything.
- Before the render starts, the UI states whether the current request will reuse the cut set or re-segment. The "Matching cut set found" dialog may stay, but it must not be the first place the user learns which path is taken.
- A render request with an invalid transform is either not offered or is visibly marked as failing before the press; the existing inline error (
transform_group.py:62) stays. - The pending size and the rendered size are distinguishable by more than word order in one grey line.
uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -qgreen; the existing reuse and collision dialog tests (tests/ui/test_render_controller.py:535,:560) still pass or are updated with the decision.
Out of Scope
- Changing what a render does. The staged-preview design from Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #25 stands; this issue is about telling the user so.
- The "Edit crop" checkbox semantics (issue B) and the cost asymmetry of the timeline range (issue C), which are filed separately because each is a decision of its own.
Related
- Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #25 — the design decision this issue makes visible.
- PR Transform stage on a finished cut: trim, output crop, resize, and a looping result preview #27 — field test comment, item 7.
docs/engineering-guardrails.md:182(G4, degrade never refuse) if the decision on criterion 3 is to keep the button enabled.
Activity
smb commentedon Sep 5, 2026
The design question in this issue was assessed on 2026-09-05, including whether
the workflow should be restructured — tabs, a separate cut-based editor, or a
wizard. The conclusion argues against all three.
The structure is substantially right; this is a labelling and feedback
problem. That is the cheapest possible answer to this issue and it appears to
be the true one.
On separating the transform stage from rendering: the instinct that the cost
boundary should be explicit is correct, but a separate view is not how to get
there, because transform input is already cut-based.
ui/transform_stage.pyreads stored cut frames. The leak is elsewhere:
ui/render_controller.pybuildsthe export request from the current source settings, so changing IN/OUT can
leave the previous cut on screen while redirecting the next export. The
architecture already has the separation; the request path crosses it. A new view
would hide that rather than fix it — and #60 is where it actually gets fixed.
Three boundary problems worth recording here, because they explain why the
current grouping confuses even an experienced user:
changes the cut key; transparency trim, alpha threshold, padding and stretch
do not.
encoding setting.
searched under it — so "only the output settings changed" cannot safely imply
a cheap export.
The smallest version worth shipping, with the current layout untouched:
removal" or "Background removal required — N frames", using the reuse probe
that already runs.
last exported size labelled separately rather than competing in one grey
sentence.
produce.
Two traps that make a cheap version wrong. The render fingerprint does not
include transforms, so a pending-state marker needs its own small record of the
last successful export — dimensions alone are not enough. And an invalid resize
entry stays widget-local while core state keeps the last valid transform, so
validating
ParameterState.transformdoes not validate what the user is lookingat.
The parts of this that need no product decision are filed as #65. The full
assessment is in #66.