Repository navigation
bin/vh textcheck: canvas text covered by the picture, too faint, or out of the frame - #68
Merged
Merged
Conversation
…ut of the frame
npx hyperframes check measures DOM text only; text a script draws into a canvas is
pixels to it. A canvas film's own script found reads with too little contrast that
frame review had missed. This turns that method into a command any canvas film can
run with one line in the composition: window.__vh = { draw(t), texts? }.
- tools/textcheck: hooks fillText/strokeText before the page's scripts, draws each
read's frame with and without it plus the read alone as a mask: share shown,
contrast with the picture and with its edge, out of frame, under a DOM caption.
Canvas text outside the registry and captions over a bright picture warn.
No npm dependency: CDP over --remote-debugging-pipe, a static server on 127.0.0.1.
- bin/vh textcheck finds the browser: CHROME_PATH, HyperFrames' headless shell,
then Chrome or Chromium.
- engines/README (the contract), playbook/02 layer 2, CHANGELOG, CLAUDE.md command
list and AGENTS.md; tools/ci.sh runs it on a fixture with one read per case.
- browser.mjs: a dead or missing browser fails every pending call at once (it hung until the 15-minute watchdog), pipe errors are handled (a bad --browser path crashed with EPIPE and exit 1), and no timer keeps node alive (every run waited out the 30 s start timeout). - page.js: a maxWidth forwarded as undefined is no maxWidth (it was recorded as NaN, the mask came out empty and a covered read passed); the picture is every canvas, video and image over the page background, so text on a transparent layer is measured against what is behind it, captions too; a glow pass under a crisp one no longer makes the read look always faded. - textcheck.mjs: a read drawn but never measurable is "unmeasured", not ok; a run that measured no canvas text exits 2; the browser-gone message without a stack. - bin/vh usage lists --caption-close and --timeout; docs say what the picture is and when it exits 2. The 457 s film now runs in about 13 s.
On Linux CI the run without window.__vh exited 1 instead of 2: a promise left by the start-up race rejected when the browser closed, and Node exits 1 on an unhandled rejection, which here means a read failed. That promise is gone since the review fixes; the page-load waiter now has a handler, and any other stray rejection is reported as a failure to check (2).
Linux CI still saw exit 1 for the run without window.__vh, after the right message. The only code after the verdict is the clean-up: closing the browser and removing its temporary profile, which Chrome's helper processes may still be writing to just after it exits (rmSync then throws, and an error out of the module's finally exits 1). The profile removal now retries and swallows its error, closing is wrapped, and tools/ci.sh prints the end of the output on a failure (it printed the start, which hid the cause).
ZLHad
marked this pull request as ready for review
October 6, 2026 17:39
ZLHad
enabled auto-merge (squash)
October 6, 2026 17:39
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.
npx hyperframes checkmeasures DOM text only; text a script draws into a<canvas>is pixels to it. The icecube film's owntools/occlusion.mjsmeasured its reads by drawing each frame with and without text and found 3 with too little contrast that frame review had missed. That script was tied to the one film (itstxt/kinfunctions, a global context,__drawAt, hard-coded caption band, offset and size, system Chrome through a transitive puppeteer-core). This makes the method a command any canvas film can run.How a film opts in: one line in the composition's inline script.
The film's own text functions stay as they are: before its scripts run, the check hooks
fillText/strokeText, records every call (string, transform, font state, alpha, colour) and can skip chosen calls. A read matches the calls that spell it with whitespace, punctuation and symbols left out, so kinetic type drawn one character at a time, a line drawn in parts, and a "·" or an arrow drawn as a shape all match.What it measures for each registered read (
--texts, elsewindow.__vh.texts, elseout/texts.json), at 3 times across its window:data-read="subtitle", readcheck's mark); the frame function throwing.Those exit 1; 2 means it could not check (no
window.__vh.draw, no browser, or no canvas text measured at all). The picture is every canvas, video and image of the page over its background, so text on a transparent layer is measured against the layer behind it. Warnings only: canvas text outside the registry (scanned every second), reads missing from some samples or never drawn as canvas text, captions over a bright picture (the pixels under their lines within 3:1 of the caption's colour), a frame not the same when drawn twice. Crops and every number go toout/check/textcheck/. Thresholds are options.No npm dependency: Node drives a headless Chrome over the DevTools protocol through
--remote-debugging-pipeand serves the project folder on 127.0.0.1.bin/vh textcheckpicksCHROME_PATH, else HyperFrames' chrome-headless-shell, else Chrome or Chromium. Pixels are copied into the check's own canvas before reading: repeatedgetImageDataon the film's canvas made Chrome move it off the GPU, and every later frame drew on the CPU (the caption scan went from 1.2 s to 26 s after the read phase).Docs:
engines/README.md(HyperFrames, 画面文字检查): the contract and what it cannot see (text in images, WebGL or an offscreen canvas);playbook/02-verification.mdlayer 2; CHANGELOG; CLAUDE.md command list, AGENTS.md regenerated.Checks
tools/ci.sh --committedpasses, also withVH_BASH=/bin/bash; shellcheck and pyflakes ran through theuvrecipe. The new smoke test runsbin/vh textcheckontools/textcheck/fixture/(one read per case: clear, covered, faint, outlined over a bright panel, kinetic, split with a drawn dot, above the frame, under the caption, drawn late, never drawn; a faint unregistered line; a caption over a bright band) and checks each verdict in the JSON; also that a composition withoutwindow.__vhexits 2 and names the line to add. Skipped where no Chrome is found (GitHub's runners have one).θ ≈ 41°) is drawn above the frame at its sample time; the AP quote is an image; ten reads are missing from their first sample (0.4 s after the registered start); outside the registry, two dim notes under 3:1 ((事件图为示意) 1.7:1, (银河带为示意) 2.2:1), one line outside the frame, one cut by its edge; two captions over a bright picture (13.25 s, 351.75 s; readable through their text-shadow, but close).Independent review (one reviewer, findings confirmed with small test pages, all fixed in the second commit): every run waited out a 30 s start timer; a wrapper forwarding
maxWidthasundefinedemptied the mask, so a fully covered read passed; text on a transparent canvas was measured against black (now composited with what is behind it); a dead browser hung until the 15-minute watchdog and a bad browser path crashed with exit 1; a run that measured nothing exited 0; a glow pass made a read look always faded; the usage line left out two options. Re-checked: each of those cases now fails or exits 2 as it should; the fixture and the film give the same verdicts as before.Merging with #67: #67 adds a paragraph to the same place in
playbook/02-verification.mdlayer 2 (the method, "wired up in each project") and a CHANGELOG line saying it stays a method rather than a command. Whichever merges second resolves it: this bullet replaces #67's, and that CHANGELOG line goes.