Skip to content

Nothing in the UI says how a transform becomes a file, or what it will cost #33

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 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:

  1. Whether the loop on screen matches the file on disk, or has pending changes.
  2. Which control writes the pending changes to the file. Today that is "Render Video", whose name does not say that it also re-encodes.
  3. 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.
  4. 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_render on 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)

  1. 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.
  2. 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.
  3. 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.
  4. The pending size and the rendered size are distinguishable by more than word order in one grey line.
  5. uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -q green; 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

Related

Activity

  1. smb commented on Sep 5, 2026

    @smb
    Collaborator

    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.py
    reads stored cut frames. The leak is elsewhere: ui/render_controller.py builds
    the 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:

    • "Crop & Cleanup" crosses the cost boundary inside one section: source crop
      changes the cut key; transparency trim, alpha threshold, padding and stretch
      do not.
    • "Output FPS" feeds segmentation sampling, despite a name that sounds like an
      encoding setting.
    • The output directory is not part of the cut key, yet matching cuts are
      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:

    1. Above the existing action, state either "Reuses saved cut — no background
      removal" or "Background removal required — N frames", using the reuse probe
      that already runs.
    2. A pending marker naming the writing action, with the pending size and the
      last exported size labelled separately rather than competing in one grey
      sentence.
    3. Visible invalid-input feedback wired to the action's enablement.
    4. An explicit warning when the displayed cut is not what the next export will
      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.transform does not validate what the user is looking
    at.

    The parts of this that need no product decision are filed as #65. The full
    assessment is 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