Skip to content

Decontaminate colors: work on a writable copy instead of the frozen segmentation result - #54

Merged
sensitron merged 1 commit into
mainfrom
fix/issue-40-decontaminate-read-only
Sep 5, 2026
Merged

sensitron merged 1 commit into
mainfrom
fix/issue-40-decontaminate-read-only

Conversation

@sensitron

Copy link
Copy Markdown
Collaborator

Refs #40

EdgeMode.DECONTAMINATE_COLORS failed every preview and every render:
_produce_cut_frame handed the segmentation host's result straight into
_decontaminate_edge_colors_in_place, and that result is deliberately frozen.

Variant 1 from the issue, as decided by the maintainer: the postprocess now runs
on a writable copy. The host's setflags(write=False) is untouched — that array
is a copy out of the shared-memory slot the child process writes into, and the
freeze is the guard against a later mutation aliasing that buffer. The cost is
one full-frame copy per frame, and only in this edge mode.

Variants 2 (drop the freeze) and 3 (make the postprocess pure) were not pursued.
Nothing surfaced during implementation that argues against variant 1.

The trigger

tests/jobs/test_preview.py::test_decontaminate_is_deterministic_local_rgba_postprocess,
failing with ValueError: assignment destination is read-only at
render.py:1603 (rgba[alpha == 0, :3] = 0).

That failure only becomes reachable once the fakes stop being more permissive
than the real port. The fakes returned freshly allocated writable arrays, so
every test touching this mode ran against a value the production path never
produces — the divergence sat exactly on the property the code breaks on. They
now return read-only arrays through frozen_segmentation_result. Both the
preview test above and the new render test were confirmed to fail on the
unfixed code with that exact ValueError, and to pass with the fix.

Guardrails baseline

scripts/guardrails-baseline.json moves render.py from 2871 to 2872 lines.
The file sat exactly on its previous baseline, so the one added line left no
room; the alternative was contorting the fix to fit the number.

An independent Codex (gpt-5.6-sol) review filed this as P2, reading
docs/engineering-guardrails.md as prohibiting baseline increases. The doc
prohibits silent ones — it provides an explicit path of "say so to the user,
get agreement", which is what happened here, and this section is that agreement
made visible. Recorded because the maintainer decision, not the finding, is what
settled it: the doc frames justified growth as "a real feature landing in a
frozen module", and a one-line bug fix is admittedly not a feature.

Reviewers who would rather hold the ceiling: the zero-growth alternative is a
walrus (segmented := segmented.copy()) inside the existing call, which keeps
2871 but hides the extra full-frame buffer from RgbaOwnershipTracker.

Not verified here — needs Windows

Acceptance criteria 1 and 2 are open. Both need real model weights and real
video, and neither exists on the Linux devbox this was built on:

  1. A preview with the mode on a real segmentation host produces a frame.
  2. A render with the mode completes and its output differs from the default
    mode's on a frame with translucent edges.

They were deliberately not attempted rather than faked. Criterion 2 is covered
in spirit by the new render-level test (two renders of one frozen frame under
STANDARD and DECONTAMINATE_COLORS must differ, and the segmenter's array
must come back unwritten and still read-only), but that runs against a fake.
Hence Refs #40 and not Closes — whether the issue closes is a call for after
the Windows check.

Gate

ruff, mypy (106 files), scripts/check_guardrails.py and
QT_QPA_PLATFORM=offscreen pytest -q all green: 1783 passed, 4 skipped, against
a measured baseline of 1782 passed, 4 skipped on main.

Unrelated, but worth knowing for anyone hitting it: this checkout's venv carried
stale 0.1.2 dist-info while the CLI reported 0.2.1, which fails
test_headless_commands_run_in_fresh_guarded_interpreters[--version]. Plain
uv sync does not repair it; uv sync --reinstall-package matteloop does. No
code change here — flagging it because it will recur after the next version bump.

🤖 Generated with Claude Code

Trigger: test tests/jobs/test_preview.py::test_decontaminate_is_deterministic_
local_rgba_postprocess, which fails with "ValueError: assignment destination is
read-only" at render.py:1603 (rgba[alpha == 0, :3] = 0) as soon as the fake
segmenter returns a frozen array the way the real host does.

_produce_cut_frame now decontaminates a writable copy when the edge mode is
DECONTAMINATE_COLORS, and registers that copy with the ownership tracker so the
extra full-frame buffer stays accounted for.

segmentation_host.py keeps its setflags(write=False). That array is a copy out
of the shared-memory slot the child process writes into, and freezing it is the
guard against a later mutation aliasing that buffer; the postprocess is the side
that has to yield. The cost is one full-frame copy per frame, only in this mode.

The fakes in tests returned freshly allocated writable arrays, so every test
touching this mode ran against a value the production path never produces. They
now hand back read-only arrays via frozen_segmentation_result, which is what
makes this class of defect reachable from the suite at all.

tests/jobs/test_render.py gains a render-level case: one frozen frame rendered
under STANDARD and under DECONTAMINATE_COLORS must produce differing output, and
the segmenter's array must come back unwritten and still read-only.

scripts/guardrails-baseline.json moves render.py to 2872 lines. The file sat
exactly on its previous baseline, so the single added line needed the ratchet to
move with it rather than the fix to be contorted around it.

Refs #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sensitron
sensitron merged commit b9a2535 into main Sep 5, 2026
11 checks passed
@sensitron
sensitron deleted the fix/issue-40-decontaminate-read-only branch September 5, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant