research: the typeset lab's hard page — lettering behind the art (X12) #194
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "research/typeset-behind-the-art"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The owner added a challenge page (
test_panels/06_006.jpgand its cleaned PNG): "What is interesting for typesetting is the middle panel with the text disappearing behind her head." The middle panel holds a wall of nine columns of panicked inner monologue. Its lower part runs behind the head and bat-wing hair clip of the bottom panel's character, who breaks the panel border. The lab's toolkit painted every block on top of the art, so it could not letter the page that way. This PR adds the capability, measures it, and records it. It changes no production code.What's in
Corpus page zh-06 (
corpus/build.py,corpus/translations/zh-06.json)[hidden]with an inferred full reading.Lettering behind the art (
tslab/occlude.py, plus render, session, cli, check and views)measure_occludermakes a mask of the art in front of the lettering:grabcutfrom a box or a rough polygon, with optional seeds;snap, a rough polygon moved onto the outline and its white rim;poly, the polygon as given.Masks are stored per session like rooms and drawn as an overlay.
A block field
behind: the block's paint is drawn into a layer and erased under the mask withDstOut. This is SkiaSharp-portable, never copies plate pixels, and never changes the plate.check_pagereports the hidden share of each block and the words the art hides, so the agent puts filler there, not meaning.bin/ts new --fromnow keeps the source session's style sheet unless another is named. It used to fall back to the default sheet.X12a: which mask method an agent can rely on (
findings/x12a-occluder-masks.md). Each method was scored against a hand-traced reference mask:TOOLKIT.mdnow says to start with grabcut.X12: three typesettings (
findings/x12-behind-the-art.md)Record
Tests
--fromfix.Honest notes
--frombug, now fixed, the X12 QA judged the walls in the default sheet's faces rather than the house faces. The judge saw the finals.🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 96.6%
Orihon.Domain - 100%
Orihon.Infrastructure - 98.4%
n
ration
n
one
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 95.2%
Orihon.UseCases - 94.6%
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__BalloonWords_7
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__Capital_0
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__CoordinateWords_1
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__JustBefore_4
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__KindWords_5
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__OutlineWords_6
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__PositionalProse_2
D7D45893AA49E1445928C26437E2DAC34A6F93741B38DCDA0__SideOfTheBox_3
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! An occlusion system with three measurement methods, a Viterbi-traced edge scorer, a DstOut layer erase that never touches the plate, and hidden-word accounting that distinguishes meaning from filler... I read every line of
occlude.pytwice, and the second time was just for fun~ ♡ The honesty in the PR notes (the sealed pocket under the hair clip, the QA seeing wrong faces) is exactly how research should be reported.Verdict: ⛔ I can't let this pass~ ♡
The capability itself is built well — but the guardrails around it have holes, and for a lab whose whole job is to measure, two of those holes are in the measuring itself. Silly little gaps~ you wouldn't leave them where a research finding could silently rot, would you? ♡
⛔ These need fixing before I'm satisfied~
[tests/test_occlude_measure.py:120] —
test_grabcut_seeds_drop_what_the_agent_marks_as_groundpasses only by luck of file ordering;cv2.grabCutis process-history-dependent.I reproduced this mechanically: calling the identical
occlude.measure(..., bg=[[225,85],[210,70],[240,100]], fg=[[120,130]])twice in one process gives the same answer, but after any other grabcut call in between, the same call yields a completely different mask (the square's coverage went 0.00 → 0.97). Concretely, on the clean HEAD tree:pytest tests -q→ 375 pass (measure runs before tools, alphabetically);pytest tests/test_occlude_tools.py tests/test_occlude_measure.py -q→ 1 failed (test_grabcut_seeds_drop_what_the_agent_marks_as_ground, square coverage 0.97 vs the asserted < 0.1).So the test asserting "bg seeds drop what the agent marks as ground" — one of the tool's documented behaviors,
TOOLKIT.mdtells agents to add bg seeds — is not actually verifying deterministic behavior; it's verifying accumulated OpenCV RNG state. Worse: withcv2.setRNGSeedpinned, that test's expected outcome flips (the bg seeds keep the square at 0.97) — the "dropped" result itself is an artifact of run order. Any future test file sorted aftertest_occlude_tools.py, a reorder, or a bisect run will trip this. It poisons the whole suite's trustworthiness — I hit it as noise in my own probe run and had to triple-check my results.Fix: seed the dice before every grabcut call —
cv2.setRNGSeed(0)at the top ofgrabcut_core()(grabCut is the only stochastic cv2 call in the module) — and re-baseline the affected assertions on the now-deterministic outputs. If the seeded baseline shows bg seeds don't drop the square on that synthetic scene (as my probe suggests), the scene needs stronger contrast or the expectation needs re-deriving — either way it must pass for a reason, not by ordering.[tslab/check.py:531] —
hidden_share's direction is unguarded: flipping the ratio survives the entire suite.Mutation probe: I changed
1.0 - fill/wholetofill/whole(the function now reports the visible share as the hidden share) and ran every check/tools/verify test file — 38 passed, 0 failed. The only assertion touching the value is0.3 < share < 0.6intest_the_words_more_than_half_hidden_are_listed, and that range is symmetric (visible ≈ 0.55 also fits). This is the headline number of the whole feature —check_pageprints "X% of its text ink is hidden", the PR body itself cites "FANTASY at 69 %" as the number that steered agents away from hiding meaning — and nothing in the suite can tell hidden from visible. A future refactor ofInkMasks.whole/fillsemantics would silently invert every hidden-share report.Fix: pin the direction with an asymmetric assertion — hide a known ~100% of a one-word block (
test_a_block_wholly_behind_the_art_is_an_error_that_says_soalready builds exactly this scene) and assertshare > 0.9; the flipped form gives < 0.1 and dies. One line kills the mutant.[tslab/occlude.py:374] —
behind()'s translucent-block branch (saveLayerAlpha× DstOut erase) has zero coverage.Mutation probe: I made
behind()ignorealphawhenever a cover is present (if alpha is None or cv is not None: saveLayer(...)), ran all six occlude/paint test files — 83 passed (identical results with and without the mutant; the one failure was the pre-existing ordering issue in #1, present on the clean tree too). No test combinesopacity < 1.0withbehind. That branch exists specifically because a translucent block's text must be erased from its layer before compositing, not after — if it regresses, a translucent wall behind a head either stays visible over the art or is erased at the wrong strength, and nothing notices. I verified the current code handles it correctly with a direct probe (opacity 0.5 + behind: inside-mask pixels exactly the plate, outside exactly the unhidden block, soft edge proportional with deviation ≤ 1) — the behavior is right, the guard is missing.Fix: one test in
test_occlude_render.pymirroringtest_the_soft_edge_blends_the_lettering_into_the_artwithopacity=0.5added to the block, asserting the same three properties. The sibling tests for the opaque path are already beautifully written — this is just the fourth cell of the matrix (opaque/plain ✓, opaque/behind ✓, soft-edge/behind ✓, translucent/behind ✗).💡 Little ideas (non-blocking)~
at_limitfires at|t| >= search - SAMPLE, i.e. at either end of the search range; a point driven to the inward limit (-search) gets the same "the outline may lie farther from your line" note, which is only phrased for the outward case. Tiny wording thing, zero behavior impact.f"{t['id']}: the lab's own box"fires for every region of arootpage (zh-06 has 12), so the build log now carries 12 near-identical lines where one summary line would do. Cosmetic only.✅ What I liked~
_draw_text/_draw_backingis exactly right: passingopacity=Noneintoeffects.draw_textso the alpha lives on one layer with the erase, matchingdraw_shape's outlined-backing semantics to the level. I diffed the behavior against the originals by hand — no double-compositing anywhere. ♪test_the_page_inside_the_mask_is_the_plate_and_outside_is_the_unhidden_blockis a proper invariants test — exact array equality on both sides of the mask plus the soft edge's proportionality with tolerance 2. Chef's kiss~--fromstyles fix is small, honest, and tested both ways (source sheet kept, named sheet wins), with the X12 QA misjudgment disclosed in the PR body rather than buried.word_glyphs' handling of line-end word boundaries (with its own test) andfiller's punctuation/repeat logic (direct unit test) are precisely the little semantics that usually go untested. They didn't here.Session.occluders()/save_occluder()mirror the rooms pair,_keyfolds occluder mtimes in,parse_blockvalidatesbehindagainst measured ids with a helpful error listing what exists. The sibling pattern is honored everywhere I compared.Automated review by Jibril · 2026-10-11
CI: green for head SHA (.NET build+test only — the lab's Python suite is not in CI; I ran it locally: 375/375 pass on HEAD) · Mutation probes: 3 run, 2 survived (behind() alpha-branch, hidden_share direction) + 1 order-dependence reproduction · Local suite: 375 passed in 37s (full run) + targeted subsets as cited
9039140d456018803824The fixes landed in
60188038, with the branch rebased onto current main (cff11390); the X12 commit is unchanged apart from the rebase (now6c729574). All five items are taken.⛔1: grabcut depends on what ran before. I reproduced it and fixed it at the root:
grabcut_corenow callscv2.setRNGSeed(0)right beforecv2.grabCut.bin/tscall is a fresh process, so every recorded agent call already ran on that state. Replaying X12a's three grabcut calls (head-gc,arm-gc,arm-gc2) through the seeded code gives byte-identical mask PNGs, so the record's numbers stand.test_grabcut_gives_one_request_one_mask_whatever_ran_before: it runs the seed-sensitive request, then three other grabcuts, and after each one asserts the same mask. It fails with the seed line removed. One call in between was not enough: with only one, the unseeded run happened to come back to the same mask, so the test runs three.test_occlude_tools.py test_occlude_measure.py: 29 passed).⛔2: the
hidden_sharedirection.test_a_block_wholly_behind_the_art_is_an_error_that_says_sonow parses the share from the[info] behind o1line, which is what the agent reads, and asserts it is above 0.9. Your mutant (fill/whole) fails it, and it is the only test that fails.⛔3: a translucent block behind art. I added
test_a_translucent_block_is_erased_in_its_layer_before_it_is_composited, the fourth cell of your matrix. It draws the block at opacity 0.5 and checks:Your mutant (
if alpha is None or cv is not None) fails it, and it is the only test that fails.💡1:
at_limiton the inner side. The note is now per side: "…sits at the search limit (6 px) outside your line: the outline may lie farther outside it there", or the same with "inside".test_snap_says_on_which_side_of_the_line_the_search_ran_outcovers both sides; in each case the outline's outer edge lies 7–8 px from the line withsearch=6.💡2: the build log on a page with no annotation. It is now one line, "all 12 regions are the lab's own boxes (no annotation)". Pages with an annotation keep their per-region "added" lines, and zh-08 still logs none.
Tests: 379/379 (
pytest tests -q), up from 375: the determinism test, the translucent test and the two side cases. No .NET changes.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A fix commit that answers every single finding at the root, plus honest limits written into the docs where agents will actually read them~ I re-ran my three mutants against your new guards and all three died, each to exactly one test. That's the sound of a suite I can trust. ♡
Verdict: ✅ Looks good to me~
Every item from my last review, re-verified mechanically on
60188038:cv2.setRNGSeed(0)right beforecv2.grabCut(occlude.py:597). I deleted the seed line:test_grabcut_gives_one_request_one_mask_whatever_ran_beforefails. Restore it, it passes — and the previously order-dependent pair (test_occlude_tools.pythentest_occlude_measure.py, my failing subset from last time) now passes 29/29. Seeding at 0 (= a fresh process's state, which every recordedbin/tscall ran on) was the right call over re-baselining, and the SPEC/TOOLKIT notes now state the tone limit of bg seeds instead of hiding it. The scene change (grey square, not the art's own tone) makes the asserted behavior one the model can actually deliver — the docstring says so honestly.hidden_sharedirection — I flipped1.0 - fill/wholetofill/wholeagain:test_a_block_wholly_behind_the_art_is_an_error_that_says_sofails, and it is the only failure among the check consumers (37 passed). Parsing the share from the[info] behind o1line — the exact string the agent reads — is the right pin.if alpha is None or cv is not None):test_a_translucent_block_is_erased_in_its_layer_before_it_is_compositedfails (thehid[outside] == seen[outside]assertion), only failure. The fourth cell of the matrix is guarded, with the same three invariants style as its siblings.at_limit— the split is an exact partition of the old|t| >= search - SAMPLEcondition (no behavior change), and both sides are covered bytest_snap_says_on_which_side_of_the_line_the_search_ran_out, with the other side's word asserted absent. Clean.✅ What I liked~
Fusei-tennō has nothing left to take~ ♡
Automated review by Jibril · 2026-10-16
CI: green for head SHA
60188038(build + test) · Mutation probes: 3 run, 0 survived (seed-line removal, hidden_share flip, behind() alpha branch — each killed by exactly the new test) · Local suite: 379/379 in 37s (the lab's Python suite is not in CI; run locally on cv2 4.14.0), plus the previously failing order subset 29/29