Skip to content

bin/vh textcheck: canvas text covered by the picture, too faint, or out of the frame - #68

Merged
ZLHad merged 4 commits into
mainfrom
claude/textcheck
Oct 6, 2026
Merged

ZLHad merged 4 commits into
mainfrom
claude/textcheck

Conversation

@ZLHad

@ZLHad ZLHad commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

npx hyperframes check measures DOM text only; text a script draws into a <canvas> is pixels to it. The icecube film's own tools/occlusion.mjs measured 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 (its txt/kin functions, 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.

window.__vh = { draw: (t) => drawFrame(t), texts: TEXTS };   // texts optional: readcheck's texts.json format

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, else window.__vh.texts, else out/texts.json), at 3 times across its window:

  • A = the frame as drawn, B = the same frame with only this read skipped, M = the read alone as a mask;
  • shown: the glyph pixels that differ from B or are the read's own colour; under 70% = covered;
  • contrast: its contrast with the picture under it, and with a 2–4 px ring around it in A (so an outline, a shadow or a plate counts); both under 3:1 = too faint;
  • outside or cut by the frame; under a DOM caption (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 to out/check/textcheck/. Thresholds are options.

No npm dependency: Node drives a headless Chrome over the DevTools protocol through --remote-debugging-pipe and serves the project folder on 127.0.0.1. bin/vh textcheck picks CHROME_PATH, else HyperFrames' chrome-headless-shell, else Chrome or Chromium. Pixels are copied into the check's own canvas before reading: repeated getImageData on 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.md layer 2; CHANGELOG; CLAUDE.md command list, AGENTS.md regenerated.

Checks

  • tools/ci.sh --committed passes, also with VH_BASH=/bin/bash; shellcheck and pyflakes ran through the uv recipe. The new smoke test runs bin/vh textcheck on tools/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 without window.__vh exits 2 and names the line to add. Skipped where no Chrome is found (GitHub's runners have one).
  • Ran with chrome-headless-shell 150 and 152 (HyperFrames' own) and with Chrome 154; no Chrome process left behind, also when the output pipe closes early.
  • A copy of the 457 s icecube film with the line added (the project itself untouched): 107 reads in about 13 s on an M3 Max. 105 pass; 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 maxWidth as undefined emptied 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.md layer 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.

ZLHad added 4 commits October 7, 2026 00:50
…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
ZLHad marked this pull request as ready for review October 6, 2026 17:39
@ZLHad
ZLHad enabled auto-merge (squash) October 6, 2026 17:39
@ZLHad
ZLHad merged commit a27d8a9 into main Oct 6, 2026
2 checks passed
@ZLHad
ZLHad deleted the claude/textcheck branch October 6, 2026 17:40
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