Repository navigation
feat(cli): judge text behind a transparent video by its decoded alpha in check - #5657
Conversation
… in check The layout audit treated every <video> as opaque over its whole box, so a caption between footage and a remove-background cutout always failed text_occluded and needed data-layout-allow-occlusion, which also hid a word the subject's head fully covered. check now draws the video's current (seeked) frame into one reused, downscaled scratch canvas, once per sample and only for a video a text probe lands under. A fully opaque frame keeps today's behaviour. A frame with transparency is judged per rendered word against its alpha: partial cover is an info finding carrying the most covered word's share, and a word 80% hidden or two neighbouring words each half hidden is an error. A frame that cannot be read falls back to opaque with a video_alpha_unread info note. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Edit accuracy: accurate 271 (base branch 271), smooth 180 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (2)
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at d62bc0cddb25bd7e00fb4a93987028c6d69f058e. Reading the matte instead of treating the whole video box as opaque is the right fix, and most of this checks out (details below). I'm requesting changes for one regression: a word hidden by a narrow part of the matte now gets no finding, where main reports an error.
Blocker: narrow matte coverage goes unreported
The per-word matte check (matteOcclusionIssue) runs only when occlusionCoverage returns an occluder (layout-audit.browser.js:1525, if (!occluder) return null;). With this PR, a probe point hits the video only where its alpha actually covers the text. The coarse grid is 9 columns × 3 rows per line, at 3/10/20/35/50/65/80/90/97% of the width, so neighbouring columns can be 15% of the line apart. A matte feature that fits between two columns gets no grid hit at all: an arm, a mic stand, a small subject, or a head under a long caption. In that case occluder stays null and the word-level check never runs.
Reproduced in chrome-headless-shell 152:
- Clip: VP9-alpha, opaque only at source x 76–104, shown at 2× (page x 152–208).
- Caption:
ab cd ef gh ij …at 24px monospace, placed so the 20% and 35% probe columns land near 139px and 221px. - main:
text_occludederror,coveredFraction: 1. - This PR: no finding, though
ghis 100% hidden.
When the strip is moved onto a probe column, the PR correctly reports "gh" is 100% behind a transparent video's matte, so the word-level check itself works once it is reached.
Suggested fix: in occlusionCoverage, remember every matte-classified video the stack walk finds above the text, including at points where its alpha is clear. Then run matteOcclusionIssue whenever there are no opaque hits and at least one matte was seen, rather than only when occluder is set. A Chromium test with a strip between two probe columns would pin it.
Non-blocking
- A fully transparent frame counts as unread. That covers the cutout before the person walks in, or a fade. Each caption under it then becomes a hard
text_occludederror, plusvideo_alpha_unread, which is the same false positive this PR removes elsewhere. The body says this is deliberate, so a failed draw can't hide text. It would help to say so in the docs, or to tell a real blank frame apart from a failed draw:readyStateis already checked, so a frame that has been read and is all clear could be trusted. docs/packages/cli.mdxlists the unread causes as "not decoded yet, or cross-origin". The code also reports still seeking, fully transparent and no frame size.- The Chromium suite is skipped in CI without
PUPPETEER_EXECUTABLE_PATH. In theTest (runtime)log all 8 tests are skipped, so nothing in CI checks that VP9 alpha survivesdrawImage. The body says so, and it matches the existinglayout-audit.chromium.test.ts. - Mixed hits. Text under both an opaque box and a matte now counts only the matte pixels that actually cover it, not the whole video box. That's a small change from main, though a more accurate one.
Checked and fine
- Fallback. Every unread case falls back to opaque, which is the old behaviour: not decoded, seeking,
SecurityError, draw error, no size, all clear. An opaque VP9 clip takes exactly the old path (Chromium test 1,coveredFraction: 1). H.264 reads as opaque. ProRes 4444.movnever decodes in Chromium, so it also stays opaque. - Shared canvas. A cross-origin read doesn't taint the shared scratch canvas for later same-origin reads: resizing it resets its state, and a same-origin cutout was still read after a cross-origin failure.
- Frame cache. The cache resets at the start of each
auditLayoutcall, so frames don't carry over between samples. Frames are downscaled to at most 480px, and per-word sampling is capped at 48×48. - Image path. The shared
paintedSourceMapping/sourcePixelAtis byte-equivalent for<img>. - Codes and waiver. The new code is in both
LayoutIssueCodeand the parser list, and the waiver is still checked first. - Tests. I ran all 8 Chromium tests locally (chrome-headless-shell 152) and all 280 layout-audit tests, and they pass. With main's
layout-audit.browser.jsswapped in, 16 of the new tests fail. - Manifest.
gen-skills-manifest --checkis in sync. CI is green; every check that ran passed.
— Rames
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed exact head d62bc0cddb25bd7e00fb4a93987028c6d69f058e. Additive to Rames's narrow-matte finding: two other rendered-pixel regressions need fixes.
The per-audit frame cache, bounded canvas/sampling, new issue-code parser wiring and existing waiver path are consistent. Independently ran 209 layout tests, including all eight actual Chromium/FFmpeg tests; these pass. The actual hosted runtime log runs the 14 mocked-alpha cases but skips all eight Chromium cases.
Blocker — mirrored cutouts read alpha on the wrong side (packages/cli/src/commands/layout-audit.browser.js:1030, :1080, :1212). paintedSourceMapping uses only an axis-aligned bounding box, so transform:scaleX(-1) leaves source coordinates unmirrored. Using the PR's actual VP9 cutout fixture with that CSS transform, the screenshot shows the left SHIP fully visible and right SHIP fully hidden. The audit instead reports left SHIP 100% hidden and returns no finding for right SHIP. Base catches the hidden right word. Map viewport coordinates through the actual element transform, or conservatively retain opaque handling for unsupported transforms; add a real-browser mirrored fixture.
Blocker — a video's opaque CSS background disappears from occlusion (packages/cli/src/commands/layout-audit.browser.js:1235). Returning the matte result before considering the element's background treats clear media pixels as clear painted pixels. Add background:#123456 to the PR's cutout video and place SHIP behind its clear half: the actual Chromium screenshot contains no visible text, but this head returns no text_occluded finding. The identical base control returns the expected error. Preserve solid/background-image occlusion when resolving matte pixels; add a regression test.
Both witnesses used real decoded VP9 alpha in local Chromium, with actual screenshots, not mocked canvas output. No live Desktop/model/provider run. Existing CodeQL path alert 1172 reports its latest instance fixed; current CI has no failed/pending checks, which does not cover these two cases. Rames's separately demonstrated between-grid narrow-matte blocker also remains applicable.
— Magi (desktop-review)
Verdict: REQUEST CHANGES
Reasoning: The alpha exemption currently lets completely hidden words pass for mirrored cutouts and videos with opaque CSS backgrounds. Correct the painted-pixel mapping/composition before relaxing the gate.
Review follow-ups on #5657: - A matte the probe walk passes over, even at clear-alpha points, now triggers the per-word check, so a feature between two probe columns (an arm, a mic stand) still fails the word it hides. - Mirrored videos and images map viewport points through the flip; a rotated, skewed or 3D-transformed video falls back to opaque with video_alpha_unread. - A video with an opaque CSS background keeps its whole box occluding. - cli.mdx lists every unread cause and why a fully transparent frame is one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks both, fixed in 23c920f:
All 8 new tests fail on d62bc0c; 316 layout tests pass locally with the Chromium suite on. Required CI is green at 23c920f. |
jrusso1020
left a comment
There was a problem hiding this comment.
Approved at 23c920f9b9. This resolves my request for changes at d62bc0cd.
The narrow-matte blocker is fixed. occluderAt now records every matte video it passes in the stack above the text, including at points where its alpha is clear. It only records hits above the text, because the walk still returns at the text element itself. When no probe hit something opaque and a matte was seen, occlusionCoverage returns that matte as the occluder and hands every seen matte to matteOcclusionIssue. The per-word check therefore runs even when no probe point lands on the matte's opaque pixels. If any probe does hit something opaque, the old whole-box path still applies.
The other two cases, also fixed:
- Mirrored cutouts.
paintFlipswalks the element and its ancestors, and flips the source lookup for a negativescaleX/scaleYfrom eithertransformor thescaleproperty. A rotation, a skew or a 3D matrix anywhere up the chain returnsnull. The video is then treated as unread (opaque) with the reason given, rather than read misaligned. - Opaque CSS background. A video with an opaque CSS background is no longer read as a matte, so its whole box occludes again.
Ran locally with Chromium 133 (PUPPETEER_EXECUTABLE_PATH set), all 30 of 30 layout-audit.video-alpha* tests pass at this head. With only layout-audit.browser.js reverted to d62bc0cd, all four new Chromium cases fail (narrow strip, mirrored, opaque background, rotated), so they test the fix.
Nits:
- The Chromium suite still skips in CI, because the CLI test job doesn't set
PUPPETEER_EXECUTABLE_PATH. These four cases only run on a machine that sets it. - The docs now list every unreadable cause, and explain that a fully transparent frame is treated as unread on purpose. That covers my earlier docs nit.
Required CI is green at this head. I didn't run a real composition through hyperframes check.
— Rames
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 23c920f9b92be2c253cbe0d8599eeced043aa2e9. Both of my rendered-pixel blockers are fixed; Rames's narrow-matte repair is verified too.
packages/cli/src/commands/layout-audit.browser.js:1057tracks flips through the video and its ancestors; unsupported rotation/skew/3D falls back conservatively. My original real-Chromium mirrored-cutout witness now flags the hidden right word and leaves the visible left word clear. Additional ancestortransform:scaleX(-1)and standalonescale:-1 1controls also pass.matteFrameForpreserves an opaque CSS background, so my original background witness again reports the fully hidden word.occlusionCoverageremembers mattes even at clear-alpha probe points, making the narrow-strip per-word check reachable.
Independent results: 249 layout tests pass with Chromium enabled (including 12 video-alpha Chromium cases and 32 other Chromium cases), plus three independent browser probes. Restoring only the previous d62bc0cd production audit makes exactly eight new regression tests fail (22 still pass). Manifest regeneration check passes. I read the complete four-file successor delta and actual hosted logs: the 18 mocked-alpha tests execute; the 12 Chromium cases still skip in CI, but ran locally here. All current required checks pass.
Limits: real browser/decoded synthetic VP9 fixtures, not live Desktop or a real project run through the complete CLI command. No model/provider calls, merge or release.
— Magi (desktop-review)
Verdict: APPROVE
Reasoning: The three demonstrated occlusion regressions are corrected and independently protected by passing browser witnesses and failing-old-code controls.
What
hyperframes checknow judges text under a<video>by the frame the video actually shows at each sample. A transparent video, such as aremove-backgroundcutout, covers text only where its decoded alpha is. An opaque video still covers its whole box, as it does today.text_occludedfinding. ItscoveredFractionand message give the most covered word's share, for exampleits most covered word "FASTER" is 60% hidden.video_alpha_unread, that says why.data-layout-allow-occlusionon the text still silences the finding.Why
To put text behind the speaker, an author stacks the footage
<video>, then the captions, then a VP9-alpha cutout above them byz-index. The layout audit treated every<video>as opaque across its whole box but sampled an<img>'s real alpha (isOpaqueOccluderinlayout-audit.browser.js). So every caption behind a WebM cutout failedtext_occluded, including captions in the transparent part of the frame. Authors had to blanket-silence it withdata-layout-allow-occlusion, and that also hid real problems, such as a word fully covered by the head.On a sample project (1920x1080 footage, a synthetic head-and-shoulders VP9-alpha cutout, 7 caption elements, 9 samples):
text_occludedNOWandwith, which sit entirely in the transparent part)"today" is 100% behind…;"code" and "today" are 53% and 100%…), 3 infos (FASTER60%,less28%,BUILD9%)Related work
Refs #5651. That PR's
skills/hyperframes/references/captions.md("Text behind the speaker", line 44) currently says:Once this lands, it should say:
Line 45's "Measure the rendered word boxes against the matte…" can then point at
check, which now does that measurement. Separately, the waiver is read from the text element itself, so putting it on the cutout never silenced anything.How
All in
packages/cli/src/commands/layout-audit.browser.js:readVideoAlphaFramedraws the video's current frame into one shared scratch canvas (willReadFrequently), downscaled so its longer side is at most 480px, and keeps only the alpha channel. Results are cached per video for oneauditLayoutcall, which is one sample time. A video is read only when a text probe'selementsFromPointwalk reaches it, which means it is painted above some text. Videos under no text are never decoded.opaqueand nothing changes from today. If some pixels would not occlude, it is amatte, andisOpaqueOccluderjudges each probe point by that pixel's alpha × the video's opacity chain. This is the same rule<img>already uses. Theobject-fit/object-positionmapping was moved out ofimageAlphaAtintopaintedSourceMapping/sourcePixelAt, so images and videos share it. A frame that reads back fully transparent counts as unread, not as clear, so a failed draw cannot hide text.Rangeover a\S+match, and its client rects are sampled at the matte's resolution (at most 48x48 per rect). Coverage is the area-weighted share under occluding alpha. These are the per-word, rendered-box measurements the retired embedded-captionscheck-occlusion.cjsmade, and the thresholds (80% lost; two neighbours at 50%) are from itscomposition-craft.md. Mixed hits (an opaque box plus a matte) keep today's grid and atomic-label rules.checkseeks through the runtime, whose seek barrier (__hfWaitForSeekCompletion, fed byholdSeekBarrierUntilVideoLandsin core'smedia.ts) holds until a seeking<video>firesseeked/loadeddata. The audit then refuses any frame that is stillseekingor belowHAVE_CURRENT_DATA, and reports it asvideo_alpha_unreadrather than reading a stale frame. The Chromium test proves the read frame is the seeked one: a cutout whose subject switches sides at 0.5s gives opposite verdicts at 0.25s and 0.75s.video_alpha_unreadis added toLayoutIssueCodeand the parser's code list. ThecoveredFractiondoc is updated. Docs that described the old behaviour are updated:docs/prompting/rules-and-anti-patterns.mdxsaid "a caption designed to sit behind a matted subject needs one" waiver.docs/guides/remove-background.mdxanddocs/packages/cli.mdxgain a short paragraph, and so doesskills/hyperframes-cli/references/lint-validate-inspect.md(withskills-manifest.jsonregenerated). No other skills are edited.Performance. On the sample project, in chrome-headless-shell 152 on an M-series Mac, the in-page
__hyperframesLayoutAuditcall took a median of 2.3–2.5 ms per sample on main and 4.7–5.5 ms on this branch, over 3 runs of 9 samples each. That is about +3 ms per sample, or about +30 ms percheck. Whole-checkwall time was 11.6 s on main and 12.1–12.9 s on this branch, which is within run-to-run noise. Results were identical with--no-browser-gpu(SwiftShader) and with the default hardware mode.Test plan
Unit tests added:
packages/cli/src/commands/layout-audit.video-alpha.browser.test.ts(happy-dom, mocked frame alpha). They cover:coveredFraction: 0.3;SecurityError, all-transparent) → opaque error plusvideo_alpha_unread.Browser test added:
packages/cli/src/commands/layout-audit.video-alpha.chromium.test.ts. Its fixtures are VP9-alpha WebMs generated with ffmpeg inbeforeAll(a few KB, nothing committed), served with byte ranges so Chromium can seek. It covers:coveredFraction: 1;preload="none"video → opaque error plusvideo_alpha_unread.It runs when
PUPPETEER_EXECUTABLE_PATHis set and ffmpeg is on PATH, likelayout-audit.chromium.test.ts, so CI skips it.Manual testing performed:
checkon the sample project above, before and after, in hardware and SwiftShader modes.Documentation updated
Comments follow CONTRIBUTING.md "Comments"
Commands, run from the repo root unless noted:
No visible change
Nothing in
packages/studioorpackages/playerchanges, and no Studio or player UI changes. What changes is the text and JSON output ofhyperframes check:text_occludedseverity,coveredFractionand message for text under a transparent video, plus the newvideo_alpha_unreadinfo finding. The finding counts above show the before and after.🤖 Generated with Claude Code