fix(image): one bad tile can no longer kill the whole canvas frame apply - #90
Merged
Conversation
On air 2026-08-01 (F10 acceptance §6): with Pillow absent and the stream switched to mono_g4, the lazy `from PIL import Image` raised ModuleNotFoundError inside the tile transcode — an exception type Canvas.apply's per-tile catch (CodecDecodeError only) did not cover. The exception unwound AFTER _last_base_seq was adopted and BEFORE any tile applied, so the canvas silently froze while appearing to track the stream, and the request_keyframe path was skipped because the exception unwound through web_ui's ingest (paho swallows handler exceptions). Two layers now guarantee the invariant: - transcode_to_webp folds any transcoder exception into CodecDecodeError (with the original as __cause__), keeping the caller-facing taxonomy; - Canvas.apply's per-tile catch broadens to Exception as the backstop — drop the tile, request a keyframe, keep applying the rest. Regression tests reproduce the exact incident (mono_g4 + missing PIL) and an arbitrary mid-frame transcoder death: remaining tiles still apply, arrived_ms refreshes, request_keyframe is set. Suite 1093/2446. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the base-station image pipeline so a single tile transcoder failure (including non-CodecDecodeError exceptions like missing Pillow) cannot abort an entire Canvas.apply() call and silently freeze the displayed canvas.
Changes:
- Wrap unexpected transcoder exceptions in
codec_decode.transcode_to_webp()intoCodecDecodeError(preserving the original as__cause__). - Broaden
Canvas.apply()per-tile exception handling to ensure a bad tile is dropped, a keyframe is requested, and the rest of the frame continues applying. - Add regression tests covering the reported on-air incident and mid-frame transcoder failure behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_canvas_transcode_errors.py | Adds targeted tests reproducing the incident and asserting frame-level survivability under transcoder exceptions. |
| LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/codec_decode.py | Ensures any non-CodecDecodeError thrown by a transcoder is folded into CodecDecodeError for consistent caller handling. |
| LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/canvas.py | Prevents per-tile exceptions from aborting frame application; requests a keyframe and continues processing remaining tiles. |
Suppressed comments (1)
LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/canvas.py:160
- The per-tile handler now catches all Exception types, but the update reason prefix still says "codec_decode_error" and the warning log drops the traceback. This makes non-codec failures harder to diagnose and the reason string misleading.
LOG.warning("canvas: dropping tile %d codec=%d: %s",
tile.index, frame.codec, exc)
update.request_keyframe = True
update.reason = update.reason or f"codec_decode_error: {exc}"
continue
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.assertEqual(upd.updated_indices, []) | ||
| # The keyframe-era tiles are untouched, not refreshed and not zeroed. | ||
| self.assertEqual(self.canvas._tiles[5].arrived_ms, 50) | ||
| self.assertEqual(self.canvas._tiles[7].arrived_ms, 50) |
…label Copilot's active comment: the incident-repro test asserted tiles 5 and 7 untouched but skipped tile 6, the other tile the failing frame carried. Now all three are pinned. Copilot's suppressed comment (valid): the broadened catch labelled every failure "codec_decode_error" and dropped the traceback. Split back into two handlers — CodecDecodeError keeps its label and warning; the backstop uses LOG.exception (traceback preserved) and a distinct "tile_apply_error: <Type>: <msg>" reason. Backstop test pins the label. Suite 1093/2446 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the pre-existing canvas bug surfaced during the F10 on-air acceptance (2026-08-01, RESULTS §6; found by PR #89's verification session).
The incident
With Pillow absent on the base station and the stream switched to mono_g4, the tile transcoder's lazy
from PIL import ImageraisedModuleNotFoundError— an exception typeCanvas.apply's per-tile catch (CodecDecodeErroronly) did not cover. The exception unwound after_last_base_seqwas adopted and before any tile applied, so the canvas silently froze while appearing to track the stream, and therequest_keyframepath never ran (paho swallows handler exceptions above it). The only visible symptom was F10's stale-tile reports showing a canvas that never freshened.The fix — two layers
codec_decode.transcode_to_webp: any transcoder exception other thanCodecDecodeErroris folded intoCodecDecodeError(original exception preserved as__cause__) — every transcoder failure is a decode failure from the caller's perspective.Canvas.apply: the per-tile catch broadens toExceptionas the invariant's backstop — drop the tile, request a keyframe, keep applying the rest of the frame.Tests
test_canvas_transcode_errors.py(4 new):request_keyframeset with the PIL reason, existing tiles untouched;CodecDecodeErrorstill passes through unwrapped;RuntimeErroron the 2nd of 3 tiles): tiles 1 and 3 still apply with fresharrived_ms.Full base-station suite (CI invocation): 1093 passed, 2446 subtests — including the pre-existing
test_bad_codec_drops_tile_and_requests_keyframe, unchanged.🤖 Generated with Claude Code