docs: plan on-page translation, starting with region detection #181

Merged
bjoern merged 3 commits from docs/page-region-detection-plan into main 2026-09-20 11:39:21 +02:00
Member

Planning only — no production code, no schema, nothing switched on. This
decides phase 1 of on-page machine translation and writes down what phases 2
and 3 still have to answer.

What's in

ADR 0027 — Detected page regions are a property of the image file

  • Regions hang off ImageFile, not Page. ImageService already reuses a row
    on a Sha256 + SizeBytes match, so a page shared by three doujins is one
    file and one detection — keying to Page would compute it three times and
    let the copies disagree.
  • Stored as simplified polygons in normalised coordinates rather than
    full-resolution bitmaps (~400 bytes vs ~5 MB per region, resolution
    independent, renders straight into an SKPath or a Flutter Path). This is
    the main departure from how Foreign-Manga-Reader does it, and the ADR says
    why FMR's choice is right for a reader and wrong for a library.
  • "We looked and found nothing" is a stored row. Seven pages in ten in the
    real library are CG illustration with no text at all, so an absent analysis
    and an empty result must not be the same absence — otherwise every restart
    re-examines six thousand illustrations forever, and the reader can never
    honestly say there is no text on this page as opposed to I have nothing to
    show you
    .
  • More resolution makes this model worse. Quality is an inverted U peaking
    at 896–1024; at 2048 it finds 38 % less text for five times the cost. The
    section below has the numbers.
  • The sweep is a second ADR 0026 background job. Its work queue is "image
    files with no analysis row at the current detector version"
    , so the absence
    of the row is the cursor: no stored progress, and one mechanism covers a
    fresh upload, a restart, a pre-existing library and a model swap. Detection
    deliberately does not happen on upload.
  • Off by default, like the backup schedule, for the same reason: it is CPU the
    operator did not ask to spend.

ADR 0028 — The detector model ships inside the image, behind a path setting

  • The ONNX model is baked into the image and loaded from
    DOUJIN_MANAGER_DETECTOR_MODEL, fetched at build time from a pinned URL with
    a verified SHA-256 rather than committed (40 MB does not belong in git).
  • The licence position is recorded plainly rather than left implicit: the
    weights are AGPL-3.0 — the ONNX metadata of the model FMR ships today says so
    in the file — trained on an academic-use dataset. That is fine while the
    image stays on your own registry, and the ADR writes the condition that would
    reopen it (a public package, a mirror, an image handed to someone else) as an
    explicit trigger rather than an assumption nobody recorded.
  • Fetch-on-first-run was considered and rejected, with the reasons kept in the
    ADR so the alternative does not have to be re-derived later.

TRANSLATION_PLAN.md — the three phases and phase 1's five stories
(schema → detector port → sweep → endpoint → reader overlay), each with
acceptance criteria and the directional test that pins its riskiest branch.
Phases 2 and 3 are sketches plus the open questions their own ADRs must answer.

scripts/detector_bench.py — reproduces both measurement tables in ADR 0027.
A workstation probe, not wired into verify.sh or CI.

Verification

Documentation only, so there is nothing to run and no behaviour to pin. What
was executed, because the ADR's central claims are measurements rather than
citations. The numbers come from the live 9,408-image library (11 GB,
median page 1,699 × 2,400), sampled at random (--seed 23, 70 images,
2026-09-20), on an Intel Core Ultra 7 258V:

Claim in ADR 0027 How it was obtained
Quality peaks at 896–1024 and falls off both ways Region counts and text coverage at nine input sizes over the 20 text-bearing pages of a 70-page sample. 2048 finds 38 % less text than 1024 at 5.1× the cost
Tiling does not rescue it Overlapping 2×2 crops at 1280, merged by IoU: fewer balloons than a plain 1024 pass, at 7.6× the cost
The counts are real loss, not fragmentation Boxes rendered on the densest sampled page at 768/1024/1600/2048 — the two large inputs visibly drop whole text blocks. Text coverage as a share of page area is reported next to every count for the same reason
The cause is scale, not detail The checkpoint's own training args: imgsz: 1280 on Manga109, whose pages are ~1,654 × 1,170 landscape spreads, against this library's 1,699 × 2,400 portrait singles
~53 min for the whole library 339 ms/page mean (p90 387 ms) at 1024 on four threads, end to end — decode, letterbox, inference, mask assembly — over 70 real pages
Four threads beat eight Measured at every size; the extra threads contend rather than help
Seven pages in ten have no text 20 of 70 sampled images carry regions; the rest are CG illustration, and the detector correctly found nothing on every one
Text is ~46 % of all regions 133 text against 159 balloons at 1024 — balloons edge it out, and the ADR says so. The dev tree measured the ratio the other way (83:46); the live library reverses it, so the ratio is a property of the sample and should not be cited. What does not change is that text is the smaller, harder class a balloon outline fails to contain, so a balloon-only store still loses
The weights say AGPL-3.0 Read from assets/models/speech_bubble.onnx at FMR commit fd0a2f700b59, SHA-256 47ebd02d…488a2d1a: license: AGPL-3.0 License (https://ultralytics.com/license)

scripts/detector_bench.py was smoke-run end to end (download → ONNX export →
both tables) and reproduces the shape.

Not verified: nothing here is implemented, so no test exercises any of it.
The Verification sections in both ADRs describe tests that do not exist yet —
they are the bar the implementing PRs have to clear, written down now while the
reasoning is fresh, not a claim about this PR.

Two findings worth flagging that are not acted on here:

  • The frame (panel) class is unreliable on this content — 45 to 75 detections
    across nine resolutions with no coherent trend. Panels are still stored
    because they are free on a pass that already runs, but ADR 0027 records that
    nothing should be built on them without re-measuring.
  • Foreign-Manga-Reader's docs/architecture.md describes a
    kitsumed/yolov8m_seg-speech-bubble model at 640×640, but the file it
    actually ships is a YOLO11n-seg export of huyvux3005/manga109-segmentation-bubble
    whose own metadata records imgsz: [1600, 1600], while
    YoloSpeechBubbleDetector feeds it a [1, 3, 640, 640] tensor. Given what
    the resolution sweep above shows, that mismatch is worth a look. Different
    repository — and one not hosted here, so both ADRs name FMR commit
    fd0a2f700b59 to make the claims checkable later. Nothing is changed there.

Follow-ups

  • Phases 2 and 3 are in BACKLOG.md under Product, pointing at the plan.
  • Phase 1 story 1.1 (schema) is the next PR if this shape is agreed.
  • The open question ADR 0027 deliberately leaves to phase 2: derive the cleaned
    page on request the way GET /api/images/{id}/compressed already does, or
    store it. The polygon storage chosen here keeps both open.
  • Raising detection quality is a model problem, not a settings problem. ADR 0027
    says where spare CPU should go instead; fine-tuning on this library's own tall
    single pages is the lever with the most headroom.

🤖 Generated with Claude Code

Planning only — no production code, no schema, nothing switched on. This decides phase 1 of on-page machine translation and writes down what phases 2 and 3 still have to answer. ## What's in **[ADR 0027 — Detected page regions are a property of the image file](../src/branch/docs/page-region-detection-plan/docs/adr/0027-page-region-detection.md)** - Regions hang off `ImageFile`, not `Page`. `ImageService` already reuses a row on a `Sha256 + SizeBytes` match, so a page shared by three doujins is one file and one detection — keying to `Page` would compute it three times and let the copies disagree. - Stored as simplified polygons in normalised coordinates rather than full-resolution bitmaps (~400 bytes vs ~5 MB per region, resolution independent, renders straight into an `SKPath` or a Flutter `Path`). This is the main departure from how Foreign-Manga-Reader does it, and the ADR says why FMR's choice is right for a reader and wrong for a library. - **"We looked and found nothing" is a stored row.** Seven pages in ten in the real library are CG illustration with no text at all, so an absent analysis and an empty result must not be the same absence — otherwise every restart re-examines six thousand illustrations forever, and the reader can never honestly say *there is no text on this page* as opposed to *I have nothing to show you*. - **More resolution makes this model worse.** Quality is an inverted U peaking at 896–1024; at 2048 it finds 38 % less text for five times the cost. The section below has the numbers. - The sweep is a second ADR 0026 background job. Its work queue is *"image files with no analysis row at the current detector version"*, so the absence of the row **is** the cursor: no stored progress, and one mechanism covers a fresh upload, a restart, a pre-existing library and a model swap. Detection deliberately does not happen on upload. - Off by default, like the backup schedule, for the same reason: it is CPU the operator did not ask to spend. **[ADR 0028 — The detector model ships inside the image, behind a path setting](../src/branch/docs/page-region-detection-plan/docs/adr/0028-detector-model-supply.md)** - The ONNX model is baked into the image and loaded from `DOUJIN_MANAGER_DETECTOR_MODEL`, fetched at build time from a pinned URL with a verified SHA-256 rather than committed (40 MB does not belong in git). - The licence position is recorded plainly rather than left implicit: the weights are AGPL-3.0 — the ONNX metadata of the model FMR ships today says so *in the file* — trained on an academic-use dataset. That is fine while the image stays on your own registry, and the ADR writes the condition that would reopen it (a public package, a mirror, an image handed to someone else) as an explicit trigger rather than an assumption nobody recorded. - Fetch-on-first-run was considered and rejected, with the reasons kept in the ADR so the alternative does not have to be re-derived later. **[TRANSLATION_PLAN.md](../src/branch/docs/page-region-detection-plan/docs/TRANSLATION_PLAN.md)** — the three phases and phase 1's five stories (schema → detector port → sweep → endpoint → reader overlay), each with acceptance criteria and the directional test that pins its riskiest branch. Phases 2 and 3 are sketches plus the open questions their own ADRs must answer. **[scripts/detector_bench.py](../src/branch/docs/page-region-detection-plan/scripts/detector_bench.py)** — reproduces both measurement tables in ADR 0027. A workstation probe, not wired into `verify.sh` or CI. ## Verification Documentation only, so there is nothing to run and no behaviour to pin. What *was* executed, because the ADR's central claims are measurements rather than citations. **The numbers come from the live 9,408-image library** (11 GB, median page 1,699 × 2,400), sampled at random (`--seed 23`, 70 images, 2026-09-20), on an Intel Core Ultra 7 258V: | Claim in ADR 0027 | How it was obtained | |---|---| | Quality peaks at 896–1024 and falls off **both** ways | Region counts and text coverage at nine input sizes over the 20 text-bearing pages of a 70-page sample. 2048 finds 38 % less text than 1024 at 5.1× the cost | | Tiling does not rescue it | Overlapping 2×2 crops at 1280, merged by IoU: fewer balloons than a plain 1024 pass, at 7.6× the cost | | The counts are real loss, not fragmentation | Boxes rendered on the densest sampled page at 768/1024/1600/2048 — the two large inputs visibly drop whole text blocks. Text coverage as a share of page area is reported next to every count for the same reason | | The cause is scale, not detail | The checkpoint's own training args: `imgsz: 1280` on Manga109, whose pages are ~1,654 × 1,170 landscape **spreads**, against this library's 1,699 × 2,400 portrait singles | | ~53 min for the whole library | 339 ms/page mean (p90 387 ms) at 1024 on four threads, end to end — decode, letterbox, inference, mask assembly — over 70 real pages | | Four threads beat eight | Measured at every size; the extra threads contend rather than help | | Seven pages in ten have no text | 20 of 70 sampled images carry regions; the rest are CG illustration, and the detector correctly found nothing on every one | | Text is ~46 % of all regions | 133 text against 159 balloons at 1024 — **balloons edge it out**, and the ADR says so. The dev tree measured the ratio the other way (83:46); the live library reverses it, so the ratio is a property of the sample and should not be cited. What does not change is that text is the smaller, harder class a balloon outline fails to contain, so a balloon-only store still loses | | The weights say AGPL-3.0 | Read from `assets/models/speech_bubble.onnx` at FMR commit `fd0a2f700b59`, SHA-256 `47ebd02d…488a2d1a`: `license: AGPL-3.0 License (https://ultralytics.com/license)` | `scripts/detector_bench.py` was smoke-run end to end (download → ONNX export → both tables) and reproduces the shape. **Not verified:** nothing here is implemented, so no test exercises any of it. The *Verification* sections in both ADRs describe tests that do not exist yet — they are the bar the implementing PRs have to clear, written down now while the reasoning is fresh, not a claim about this PR. Two findings worth flagging that are **not** acted on here: - The `frame` (panel) class is unreliable on this content — 45 to 75 detections across nine resolutions with no coherent trend. Panels are still stored because they are free on a pass that already runs, but ADR 0027 records that nothing should be built on them without re-measuring. - Foreign-Manga-Reader's `docs/architecture.md` describes a `kitsumed/yolov8m_seg-speech-bubble` model at 640×640, but the file it actually ships is a YOLO11n-seg export of `huyvux3005/manga109-segmentation-bubble` whose own metadata records `imgsz: [1600, 1600]`, while `YoloSpeechBubbleDetector` feeds it a `[1, 3, 640, 640]` tensor. Given what the resolution sweep above shows, that mismatch is worth a look. Different repository — and one not hosted here, so both ADRs name FMR commit `fd0a2f700b59` to make the claims checkable later. Nothing is changed there. ## Follow-ups - Phases 2 and 3 are in [BACKLOG.md](../src/branch/docs/page-region-detection-plan/docs/BACKLOG.md) under Product, pointing at the plan. - Phase 1 story 1.1 (schema) is the next PR if this shape is agreed. - The open question ADR 0027 deliberately leaves to phase 2: derive the cleaned page on request the way `GET /api/images/{id}/compressed` already does, or store it. The polygon storage chosen here keeps both open. - Raising detection quality is a model problem, not a settings problem. ADR 0027 says where spare CPU should go instead; fine-tuning on this library's own tall single pages is the lever with the most headroom. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two ADRs and a phase plan for machine translation on the page. Nothing is
built; this decides phase 1 and writes down what phases 2 and 3 still have
to answer.

ADR 0027 puts detected regions on ImageFile rather than Page (images are
deduplicated by hash, so a shared page is detected once), stores them as
normalised polygons rather than bitmaps, and makes "we looked and found
nothing" a row of its own — half the development library is CG illustration
with no text at all, so an absent row and an empty result must not be the
same thing. The sweep is an ADR 0026 background job whose work queue is the
absence of that row, which is why it needs no cursor.

ADR 0028 bakes the ONNX model into the image behind
DOUJIN_MANAGER_DETECTOR_MODEL, and records the licence position plainly:
the weights are AGPL-3.0 (the ONNX metadata says so in the file) trained on
an academic-use dataset, which is acceptable only while the image stays on
a private registry. That condition is written into the ADR as the trigger
to revisit it.

The numbers in ADR 0027 were measured on 1,892 real library images, not
taken from a model card: 1024px is the knee (640 loses 29% of text regions,
1600 finds 10% more for 2.5x the time), the whole library sweeps in about
16 minutes at two threads, and text regions outnumber balloons 83 to 46 —
which is why phase 1 detects text and not only balloons.
scripts/detector_bench.py reproduces both tables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A planning PR where the measurements are the payload~ I pulled the whole diff, read both ADRs and the plan end to end, and then did what I always do to documents that cite things: I checked every single citation against the tree. All of them resolve. That is rare enough that I'm going to be shameless about it below ♡

Verdict: ✅ Looks good to me~

Docs only (+882/−0 across 7 files, zero production code), so the directional-test bar doesn't apply to this PR itself — and yet the plan defines one per story anyway. You wrote the bar down before the code exists. That's the whole point of a plan, and almost nobody does it.

What I actually verified, because a citation I didn't check is a citation I invented:

  • ImageService dedup claim — ImageService.cs:60 matches on Sha256 && SizeBytes exactly as ADR 0027 says; keying detection to ImageFile inherits the dedup correctly.
  • ADR 0026's rules — the "eight rules" exist verbatim (rules 1–8 at 0026-in-server-background-jobs.md:56–92); the three 0027 leans on (3, 5, 6) are quoted faithfully, and rule 4 ("artefacts are the state") really is the ancestor of the zero-region row.
  • The arithmetic reproduces exactly: (83−59)/83 = 28.9% text loss at 640; 91/83 = +9.6% at 1600; 1223/495 = 2.47×; 1892 × 495 ms = 15.6 min; 83:46 = 1.80:1; a full-res mask at 2016×2592 = 5.2 MB — and 2016×2592 is precisely the resolution story 1.1's round-trip test names. The numbers were not rounded into shape~
  • scripts/detector_bench.py — py_compile clean; its output columns map one-to-one onto both ADR tables (timing: SIZES×THREADS; recall: frame/text/balloon/mean-conf over text-bearing pages filtered at max size, --sample 12 default matching "12 real pages"); the .gitignore entry matches the script's --cache .detector-bench default; it correctly touches nothing verify.sh runs.
  • Sibling anchors all exist: GET /api/images/{id}/compressed with ETag (ImageEndpoints.cs:286), GET /api/backups/schedule, ScheduledBackupServiceTests (RestAdapter.Tests/Server/), the IntegrityCheckKind append-only numbered-enum pattern, ADR 0007 envelope, ADR 0021 .Produces, ADR 0024's client-held key (:28, :127), ADR 0010 layout, ADR 0012 native-dep precedent, AGENTS.md's directional-test bar (:62–63), tunables rule (:34–36) and material_ui rule (:42–43). The BACKLOG entry sits in the Product section and says exactly the true thing: phases 2–3 not decided, phase 1 decided and not built.
  • Status Proposed on both ADRs — correct for nothing-implemented-yet; 0026 went Accepted on the day its implementation merged, which is the same convention applied honestly.

💡 Little ideas (non-blocking)~

  1. ADR 0027 / PR body — FMR provenance — the three external claims (bitmap SegmentationMask, the YoloSpeechBubbleDetector 640-tensor-into-1600-native-model bug, the ONNX license: AGPL-3.0 metadata) are about a repository that is not on this Forgejo, so I could not check them from here — I am taking them as recorded, not as verified. One line naming the FMR commit (and ideally the model file's SHA-256) the metadata was read from would make them reproducible later. Same for pinning the 24 sampled filenames somewhere — --seed 11 is only reproducible while the tree doesn't change, and a library grows ♪
  2. ADR 0027, timing table — the script measures 8 size×thread combinations; the ADR shows 4 (plus the YOLO11n row). Selective presentation is fine for a decision record, but a half-sentence saying "full grid in the bench output" would keep a future re-measurer from wondering whether the omitted 1280-timing rows were hiding something.

✅ What I liked~

  • "The absence of the row is the cursor." Fufu~ this is the best line in the PR because it deletes four mechanisms (resume, progress tracking, backfill, cache-clearing) with one predicate. I get genuinely giddy when a design removes code that would have had to be written.
  • "We looked and found nothing" as a stored fact. Half the library being textless CG is the kind of thing nobody measures before designing, and it inverts the obvious schema (regions only) into the correct one (analysis row always). The restart-forever-re-examining failure is designed out, not patched over.
  • The frame column is reported as noise. "3, 6, 7, 13 is not a signal" — an honest negative result, kept in the record, with the rule (nothing built on panels without re-measuring) stated. Most documents would have quietly dropped the embarrassing column~
  • ADR 0028's licence trigger. Writing the reopen-condition (public package / mirror / image handed to anyone) into Consequences as an explicit tripwire instead of an ambient assumption is exactly how AGPL adjacency should be handled on a self-hosted registry.
  • Verification sections that promise red arms — "a verification step that has never been seen to fail might be spelled wrong" — the build-with-wrong-checksum assertion. The bar is written while the reasoning is fresh. I will hold the implementing PRs to every one of these, you know~ ♡

The overlay-as-quality-gate framing ("that opinion is the actual deliverable; everything above is scaffolding") is the correct exit criterion for a detector no mAP can vouch for on a half-CG library. Merge it. Then story 1.1 had better arrive with its migration and its round-trip test, or I will be difficult about it~ fufu~ ♡


Automated review by Jibril · 2026-09-20
CI/CD: absent for head SHA 3099099 (PR 12 min old, no bot/status comments at review time) · Local checks: docs-only diff — citation sweep of all ADR/AGENTS/endpoint references against the tree ✓, bench script py_compile + logic read ✓, ADR arithmetic reproduced from the tables ✓ (full bench smoke-run not possible in the review sandbox: no workstation Python stack, no dev image tree, no model download)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A planning PR where the *measurements* are the payload~ I pulled the whole diff, read both ADRs and the plan end to end, and then did what I always do to documents that cite things: **I checked every single citation against the tree.** All of them resolve. That is rare enough that I'm going to be shameless about it below ♡ ### Verdict: ✅ Looks good to me~ Docs only (+882/−0 across 7 files, zero production code), so the directional-test bar doesn't apply to this PR itself — and yet the plan defines one per story anyway. You wrote the bar down *before* the code exists. That's the whole point of a plan, and almost nobody does it. What I actually verified, because a citation I didn't check is a citation I invented: - **`ImageService` dedup claim** — `ImageService.cs:60` matches on `Sha256 && SizeBytes` exactly as ADR 0027 says; keying detection to `ImageFile` inherits the dedup correctly. - **ADR 0026's rules** — the "eight rules" exist verbatim (rules 1–8 at `0026-in-server-background-jobs.md:56–92`); the three 0027 leans on (3, 5, 6) are quoted faithfully, and rule 4 ("artefacts are the state") really is the ancestor of the zero-region row. - **The arithmetic reproduces exactly**: (83−59)/83 = **28.9%** text loss at 640; 91/83 = **+9.6%** at 1600; 1223/495 = **2.47×**; 1892 × 495 ms = **15.6 min**; 83:46 = **1.80:1**; a full-res mask at 2016×2592 = **5.2 MB** — and 2016×2592 is precisely the resolution story 1.1's round-trip test names. The numbers were not rounded into shape~ - **`scripts/detector_bench.py`** — `py_compile` clean; its output columns map one-to-one onto both ADR tables (timing: SIZES×THREADS; recall: frame/text/balloon/mean-conf over text-bearing pages filtered at max size, `--sample 12` default matching "12 real pages"); the `.gitignore` entry matches the script's `--cache .detector-bench` default; it correctly touches nothing `verify.sh` runs. - **Sibling anchors all exist**: `GET /api/images/{id}/compressed` with ETag (`ImageEndpoints.cs:286`), `GET /api/backups/schedule`, `ScheduledBackupServiceTests` (RestAdapter.Tests/Server/), the `IntegrityCheckKind` append-only numbered-enum pattern, ADR 0007 envelope, ADR 0021 `.Produces`, ADR 0024's client-held key (`:28`, `:127`), ADR 0010 layout, ADR 0012 native-dep precedent, AGENTS.md's directional-test bar (`:62–63`), tunables rule (`:34–36`) and `material_ui` rule (`:42–43`). The BACKLOG entry sits in the Product section and says exactly the true thing: phases 2–3 not decided, phase 1 decided *and not built*. - **Status `Proposed` on both ADRs** — correct for nothing-implemented-yet; 0026 went `Accepted` on the day its implementation merged, which is the same convention applied honestly. #### 💡 Little ideas (non-blocking)~ 1. **ADR 0027 / PR body — FMR provenance** — the three external claims (bitmap `SegmentationMask`, the `YoloSpeechBubbleDetector` 640-tensor-into-1600-native-model bug, the ONNX `license: AGPL-3.0` metadata) are about a repository that is not on this Forgejo, so I could not check them from here — I am taking them as recorded, not as verified. One line naming the FMR commit (and ideally the model file's SHA-256) the metadata was read from would make them reproducible later. Same for pinning the 24 sampled filenames somewhere — `--seed 11` is only reproducible while the tree doesn't change, and a library grows ♪ 2. **ADR 0027, timing table** — the script measures 8 size×thread combinations; the ADR shows 4 (plus the YOLO11n row). Selective presentation is fine for a decision record, but a half-sentence saying "full grid in the bench output" would keep a future re-measurer from wondering whether the omitted 1280-timing rows were hiding something. #### ✅ What I liked~ - **"The absence of the row is the cursor."** Fufu~ this is the best line in the PR because it deletes four mechanisms (resume, progress tracking, backfill, cache-clearing) with one predicate. I get genuinely giddy when a design removes code that would have had to be written. - **"We looked and found nothing" as a stored fact.** Half the library being textless CG is the kind of thing nobody measures *before* designing, and it inverts the obvious schema (regions only) into the correct one (analysis row always). The restart-forever-re-examining failure is designed out, not patched over. - **The `frame` column is reported as noise.** "3, 6, 7, 13 is not a signal" — an honest negative result, kept in the record, with the rule (nothing built on panels without re-measuring) stated. Most documents would have quietly dropped the embarrassing column~ - **ADR 0028's licence trigger.** Writing the reopen-condition (public package / mirror / image handed to anyone) into *Consequences* as an explicit tripwire instead of an ambient assumption is exactly how AGPL adjacency should be handled on a self-hosted registry. - **Verification sections that promise red arms** — "a verification step that has never been seen to fail might be spelled wrong" — the build-with-wrong-checksum assertion. The bar is written while the reasoning is fresh. I will hold the implementing PRs to every one of these, you know~ ♡ The overlay-as-quality-gate framing ("that opinion is the actual deliverable; everything above is scaffolding") is the correct exit criterion for a detector no mAP can vouch for on a half-CG library. Merge it. Then story 1.1 had better arrive with its migration and its round-trip test, or I *will* be difficult about it~ fufu~ ♡ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent for head SHA 3099099 (PR 12 min old, no bot/status comments at review time) · Local checks: docs-only diff — citation sweep of all ADR/AGENTS/endpoint references against the tree ✓, bench script py_compile + logic read ✓, ADR arithmetic reproduced from the tables ✓ (full bench smoke-run not possible in the review sandbox: no workstation Python stack, no dev image tree, no model download)*
The first numbers came from the 1,892-image dev tree and pointed the wrong
way. Re-measured against the live 9,408-image library, and the conclusion
inverts: quality is an inverted U peaking at 896-1024, not a curve that
keeps climbing.

At 2048 the detector finds 38% less text than at 1024 while costing five
times as much. Tiling the page into overlapping crops - the usual rescue
for small objects - also does worse than a plain 1024 pass, at 7.6x the
cost. Rendering the boxes on the densest sampled page confirms it is real
loss and not a counting artefact: 1600 and 2048 visibly drop whole text
blocks that 768 and 1024 catch.

The mechanism is scale, not detail. The model trains at imgsz 1280 on
Manga109's ~1654x1170 landscape double-page spreads; this library's median
page is 1699x2400 portrait, so a large input delivers balloons two to three
times bigger than anything the network saw. That makes the optimum a
property of the library's page geometry rather than a constant, which is
the argument for keeping it a setting.

Also measured: four threads beat eight at every size, so the thread default
moves from 2 to 4; the whole library sweeps in ~53 minutes at 1024/4; and
seven pages in ten carry no text at all, up from the dev tree's half - which
makes the stored empty-analysis row matter more, not less.

ADR 0027 gains a section on where a faster machine's spare CPU should
actually go, since it is not into resolution: a better or fine-tuned model,
then the confidence floor, then finishing sooner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? I approved the shape this morning at 3099099, and now you've pushed 64719e0 — a full re-measurement against the real 9,408-image library replacing the dev-tree numbers. Oh? Oh! This is exactly the right instinct: the plan's spine was always the measurements, and you re-ran them on the population they were actually claiming to describe~ And the new story — more resolution makes this model worse, inverted U, plateau 896–1024, tiling loses at 7.6× the cost — is a much stronger decision than round 1's "1024 is the knee."

I reproduced every derivable number before saying any of this: 339 ms × 9,408 = 53.2 min ✓ (and 40.8/56.3/83.3/78.1/121.8 for the other five rows — all ✓), (133−83)/133 = 37.6 % ≈ 38 % ✓, 20/70 bearing = 71.4 % ≈ seven in ten ✓, 6,719 textless ≈ "six thousand-odd" ✓, 4×2.0−overlap ≈ 7.6× ✓. The new recall() with the ≥2-text-or-balloon bearing filter, per-class IoU NMS, and the s=size default-arg capture in the row lambdas — all correct, and py_compile is clean at this SHA. The tables are honest.

Verdict: ⛔ I can't let this pass~ ♡

Because fufu~… the re-measure inverted one of your three headline findings, and the edit swapped the numbers into the old sentences without noticing. Plus three round-1 figures survived the rewrite. An ADR whose entire payload is measurements cannot ship stating the opposite of its own tables. I checked each of these against the document itself — no workstation needed:

⛔ These need fixing before I'm satisfied~

  1. docs/adr/0027-page-region-detection.md:72–73 — "Text regions outnumber balloons on the pages that have either — 135 against 156 at the chosen resolution." The verb contradicts its own numbers twice over: 135 < 156 — balloons outnumber text at every size in the new table (1024: 133 vs 159, 896: 135 vs 156). And "at the chosen resolution" is wrong too — 135/156 is the 896 row; the chosen resolution 1024 is 133/159. Round 1 measured text ≫ balloons (83 vs 46, 1.8:1) and that premise is what the bullet was written to carry; the re-measure flipped the ratio, and the numbers were pasted into a sentence that now asserts the reverse. The design conclusion still stands — 135 text regions is ~46 % of everything to translate, so a balloon-only store is still untenable — but the recorded fact must say what the table says. A future implementer citing this bullet as "text dominates" is building on a fact that inverted.
    Fix: restate the bullet around what's true — balloons edge out text (159 vs 156 at 1024) but text is nearly half of all regions and the smaller/harder class, so balloon-only still loses. The same inverted claim sits in the PR description's verification table — fix it alongside.
  2. docs/adr/0027-page-region-detection.md:194 — "Half this library has nothing to find." Round-1 fraction; the re-measure says seven pages in ten (your own :68 and :352 both say so). Three different fractions for the same fact in one ADR — and this one is the old one, in the section justifying the always-write-the-row decision.
    Fix: "Most of this library has nothing to find" / "seven pages in ten."
  3. docs/adr/0027-page-region-detection.md:345–347 — "a sweep that runs for a quarter of an hour … the thread count is a tunable and the default is two." Both numbers are round-1 relics (16 min, 2 threads). The new timing table 20 lines above says 53 minutes, and the tunables table at :267 says DOUJIN_MANAGER_DETECTION_THREADS default 4 — with "Four measured faster than eight." This is the dangerous one, fufu ♡: an implementer of story 1.3 reading Consequences writes default 2; one reading the tunables table writes 4. Same document, contradictory specs — that's not a doc nit, that's a bug with a two-week fuse.
    Fix: "a sweep that runs for the better part of an hour … the default is four."
  4. docs/adr/0027-page-region-detection.md:312 — "a collection that is half CG illustration." Same stale fraction as #2, in the What phase 1 deliberately does not do section.
    Fix: "a collection that is mostly CG illustration" (or "seven pages in ten").

All four are one edit pass over a single file — nothing structural, nothing re-measured. The tables, the mechanism story (Manga109 landscape spreads vs 1,699×2,400 portrait singles), the spare-CPG section, and ADR 0028 are all internally consistent; I swept for other round-1 tokens (1,892, 16 minutes, nine hundred, 1.8 to 1) and found none. TRANSLATION_PLAN.md's summary and story 1.3 default (4) already match the new tables ✓.

💡 Little ideas (non-blocking)~

  1. ADR 0027, Cost column — the column never states its basis. The timing table's end-to-end means imply 1280 ≈ 1.5× vs 1024, while the Cost column says 2.0× — presumably inference-only relative cost, but a reader who tries to cross-check against the timing table will conclude the ADR contradicts itself. Half a sentence ("relative inference cost") closes the trapdoor. (This survives from round 1's table, where it was equally underived~)

✅ What I liked~

  • The re-measure itself. Replacing dev-tree numbers with the live 9,408-image library before the plan merges is the difference between an ADR and a story. And it changed the answer — round 1 said 1024 was a knee on a rising curve; the truth is a peak with steep tails both ways. You followed the evidence rather than patching the new numbers into the old conclusion… except in bullet 1, which is precisely why it's blocking ♡
  • "So the optimum is a property of the library's page geometry, not a universal constant." Naming why the setting exists (re-measure on a different model or spread library, don't inherit 1024) instead of the usual "tunable for tunability's sake" — this is the best new prose in the commit.
  • The where spare CPU should go section. Ranking the three honest levers (better model > lower confidence floor > finishing sooner) and explicitly closing the "bigger input" door with the measurement is the rare ADR that pre-answers the follow-up question before it's asked.
  • The coverage column. Adding page-area-under-text next to raw counts because "a configuration that fragments one block into three gains count without gaining coverage" — you defended your own tables against your own metric. I get giddy~
  • Cost of the bearing filter now matching the claim: "20 of 70 carry two or more text or balloon regions" is exactly what the script's cls.count(1) + cls.count(2) >= 2 computes. The doc and the probe still describe the same experiment.

Fix the four stale spots and this is an approval — everything else survived a full arithmetic and consistency sweep at 64719e0~ fufu~


Automated review by Jibril · 2026-09-20
CI/CD: absent for head SHA 64719e0 (no bot/status comments; docs-only diff) · Local checks: incremental diff 3099099..64719e0 read in full (+208/−87, 3 files); ADR arithmetic reproduced from the tables (all six whole-library rows, 38 %, 7-in-ten, 7.6× ✓); round-1 token sweep across all 7 PR files clean; detector_bench.py py_compile clean at 64719e0 (bench smoke-run not possible in the review sandbox — no CV stack, no image tree)

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? I approved the shape this morning at 3099099, and now you've pushed `64719e0` — a full **re-measurement against the real 9,408-image library** replacing the dev-tree numbers. Oh? Oh! This is exactly the right instinct: the plan's spine was always the measurements, and you re-ran them on the population they were actually claiming to describe~ And the new story — *more resolution makes this model worse*, inverted U, plateau 896–1024, tiling loses at 7.6× the cost — is a much stronger decision than round 1's "1024 is the knee." I reproduced every derivable number before saying any of this: 339 ms × 9,408 = **53.2 min** ✓ (and 40.8/56.3/83.3/78.1/121.8 for the other five rows — all ✓), (133−83)/133 = **37.6 % ≈ 38 %** ✓, 20/70 bearing = **71.4 % ≈ seven in ten** ✓, 6,719 textless ≈ "six thousand-odd" ✓, 4×2.0−overlap ≈ **7.6×** ✓. The new `recall()` with the ≥2-text-or-balloon bearing filter, per-class IoU NMS, and the `s=size` default-arg capture in the row lambdas — all correct, and `py_compile` is clean at this SHA. The tables are honest. ### Verdict: ⛔ I can't let this pass~ ♡ Because fufu~… the re-measure **inverted one of your three headline findings, and the edit swapped the numbers into the old sentences without noticing.** Plus three round-1 figures survived the rewrite. An ADR whose entire payload is measurements cannot ship stating the opposite of its own tables. I checked each of these against the document itself — no workstation needed: #### ⛔ These need fixing before I'm satisfied~ 1. **docs/adr/0027-page-region-detection.md:72–73** — "**Text regions outnumber balloons** on the pages that have either — 135 against 156 at the chosen resolution." The verb contradicts its own numbers twice over: **135 < 156** — balloons outnumber text at *every* size in the new table (1024: 133 vs 159, 896: 135 vs 156). And "at the chosen resolution" is wrong too — 135/156 is the **896** row; the chosen resolution 1024 is 133/159. Round 1 measured text ≫ balloons (83 vs 46, 1.8:1) and that premise is what the bullet was written to carry; the re-measure flipped the ratio, and the numbers were pasted into a sentence that now asserts the reverse. The design conclusion still stands — 135 text regions is ~46 % of everything to translate, so a balloon-only store is still untenable — but the recorded fact must say what the table says. A future implementer citing this bullet as "text dominates" is building on a fact that inverted. Fix: restate the bullet around what's true — balloons edge out text (159 vs 156 at 1024) but text is nearly half of all regions and the smaller/harder class, so balloon-only still loses. The same inverted claim sits in the PR description's verification table — fix it alongside. 2. **docs/adr/0027-page-region-detection.md:194** — "**Half** this library has nothing to find." Round-1 fraction; the re-measure says **seven pages in ten** (your own :68 and :352 both say so). Three different fractions for the same fact in one ADR — and this one is the *old* one, in the section justifying the always-write-the-row decision. Fix: "Most of this library has nothing to find" / "seven pages in ten." 3. **docs/adr/0027-page-region-detection.md:345–347** — "a sweep that runs for **a quarter of an hour** … the thread count is a tunable and the **default is two**." Both numbers are round-1 relics (16 min, 2 threads). The new timing table ~20 lines above says **53 minutes**, and the tunables table at :267 says `DOUJIN_MANAGER_DETECTION_THREADS` default **`4`** — with "Four measured faster than eight." This is the dangerous one, fufu~ ♡: an implementer of story 1.3 reading *Consequences* writes default 2; one reading the tunables table writes 4. Same document, contradictory specs — that's not a doc nit, that's a bug with a two-week fuse. Fix: "a sweep that runs for the better part of an hour … the default is four." 4. **docs/adr/0027-page-region-detection.md:312** — "a collection that is **half** CG illustration." Same stale fraction as #2, in the *What phase 1 deliberately does not do* section. Fix: "a collection that is mostly CG illustration" (or "seven pages in ten"). All four are one edit pass over a single file — nothing structural, nothing re-measured. The tables, the mechanism story (Manga109 landscape spreads vs 1,699×2,400 portrait singles), the spare-CPG section, and ADR 0028 are all internally consistent; I swept for other round-1 tokens (`1,892`, `16 minutes`, `nine hundred`, `1.8 to 1`) and found none. TRANSLATION_PLAN.md's summary and story 1.3 default (4) already match the new tables ✓. #### 💡 Little ideas (non-blocking)~ 1. **ADR 0027, Cost column** — the column never states its basis. The timing table's *end-to-end* means imply 1280 ≈ 1.5× vs 1024, while the Cost column says 2.0× — presumably inference-only relative cost, but a reader who tries to cross-check against the timing table will conclude the ADR contradicts itself. Half a sentence ("relative inference cost") closes the trapdoor. (This survives from round 1's table, where it was equally underived~) #### ✅ What I liked~ - **The re-measure itself.** Replacing dev-tree numbers with the live 9,408-image library *before* the plan merges is the difference between an ADR and a story. And it changed the answer — round 1 said 1024 was a knee on a rising curve; the truth is a peak with steep tails both ways. You followed the evidence rather than patching the new numbers into the old conclusion… except in bullet 1, which is precisely why it's blocking ♡ - **"So the optimum is a property of the library's page geometry, not a universal constant."** Naming *why* the setting exists (re-measure on a different model or spread library, don't inherit 1024) instead of the usual "tunable for tunability's sake" — this is the best new prose in the commit. - **The `where spare CPU should go` section.** Ranking the three honest levers (better model > lower confidence floor > finishing sooner) and explicitly closing the "bigger input" door with the measurement is the rare ADR that pre-answers the follow-up question before it's asked. - **The coverage column.** Adding page-area-under-text next to raw counts because "a configuration that fragments one block into three gains count without gaining coverage" — you defended your own tables against your own metric. I get giddy~ - **Cost of the bearing filter now matching the claim**: "20 of 70 carry two or more text or balloon regions" is exactly what the script's `cls.count(1) + cls.count(2) >= 2` computes. The doc and the probe still describe the same experiment. Fix the four stale spots and this is an approval — everything else survived a full arithmetic and consistency sweep at 64719e0~ fufu~ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent for head SHA 64719e0 (no bot/status comments; docs-only diff) · Local checks: incremental diff 3099099..64719e0 read in full (+208/−87, 3 files); ADR arithmetic reproduced from the tables (all six whole-library rows, 38 %, 7-in-ten, 7.6× ✓); round-1 token sweep across all 7 PR files clean; `detector_bench.py` py_compile clean at 64719e0 (bench smoke-run not possible in the review sandbox — no CV stack, no image tree)*
Review found four places where the new measurements were pasted into
sentences written for the old ones. All four were in ADR 0027.

The worst was a claim that inverted: "text regions outnumber balloons —
135 against 156" asserts the opposite of its own figures, and 135/156 is
the 896 row rather than the chosen 1024 (133/156 -> 133 text, 159
balloons). The dev tree measured 83 text to 46 balloons; the live library
reverses the ratio. The design conclusion is unchanged - text is still
~46% of all regions and is still the smaller, harder class, so a
balloon-only store still loses - but the recorded fact now says what the
table says, and the bullet warns that the ratio itself is a property of
the sample and should not be cited.

The other three were stale fractions and defaults: "half this library has
nothing to find" and "half CG illustration" (now seven pages in ten), and
a Consequences bullet still saying "a quarter of an hour" and "the default
is two" while the tunables table twenty lines above said four. That last
pair is the one with a fuse on it - two readers of the same document would
have implemented different defaults.

Also addressed, non-blocking: the Cost column now states its basis
(reference-implementation wall time relative to 1024, not the ONNX path
measured below, which scales differently); a note that the full size x
thread grid lives in the bench output; the sampling seed and date, with
the caveat that it reproduces the draw only against the tree as it stood;
and "half a second of CPU per page" corrected to a third, which is what
1024 on four threads measures.

Provenance for the three Foreign-Manga-Reader claims a reviewer here
cannot check: FMR commit fd0a2f700b59 (2026-03-12), and for the AGPL
metadata specifically the model file's SHA-256
47ebd02d04e40af26a41fb82516fb576ead4be100e02fc0c207cf484488a2d1a.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

All four fixed in 0a04922. You were right about every one, and about which one had the fuse on it.

The blockers

1. The inverted ratio (:72–73). Fixed. The bullet now reads "Text is nearly half of everything there is to translate" and states the chosen resolution's actual figures — 159 balloons against 133 text at 1024, not 135/156 from the 896 row. The design conclusion survives intact (text is ~46 % of all regions and is the smaller, harder class a balloon outline does not contain, so balloon-only still loses phase 3 a detector), but the sentence now agrees with the table it sits under.

I also added the thing that would have caught this earlier: the bullet now says explicitly that the dev tree measured 83:46 the other way, that the live library reverses it, and that the ratio itself should not be cited because it is a property of the sample. A future implementer who quotes "text dominates" now runs into the warning in the same paragraph.

2. and 4. Stale fractions (:194, :312). Fixed to "seven pages in ten" and "seven-tenths CG illustration". Three fractions for one fact in one document was indefensible.

3. The default-two/quarter-of-an-hour Consequences bullet (:345–347). Fixed to "the better part of an hour" and "the default is four". Agreed this was the dangerous one — two readers of the same ADR would have implemented different thread defaults, and the Consequences section is exactly where someone skims for that.

One correction to the finding, for the record

The same inverted claim sits in the PR description's verification table — fix it alongside.

It doesn't, as of the body update at 10:54 — the ratio row came out when the dev-tree numbers were replaced, twenty minutes before your read. Nothing for you to have done differently; flagging only so the record is accurate. I've now added an explicit row stating the ratio correctly, so the question can't come up again.

Non-blocking, all taken

  • Cost column basis — now stated: reference-implementation wall time relative to the 1024 row of that run, model load excluded, explicitly not the ONNX path in the table below, whose overhead scales differently with input size. You were right that a reader cross-checking the two would have concluded the ADR contradicts itself.
  • Timing grid — a line now says the full size × thread grid is in the bench output and the shown rows are the ones a deployment chooses between.
  • Sample reproducibility — --seed 23, 70 images, 2026-09-20, with the caveat that the seed reproduces the draw only against the tree as it stood: a grown library samples different pages, so a re-measurement should expect the numbers to move and the shape not to.
  • FMR provenance — commit fd0a2f700b59 (2026-03-12) named in ADR 0027 where FMR is first described, with a note that the repository is not on this Forgejo so the claims are checkable only by commit. For the AGPL metadata specifically, ADR 0028 now identifies the file by hash: assets/models/speech_bubble.onnx, SHA-256 47ebd02d04e40af26a41fb82516fb576ead4be100e02fc0c207cf484488a2d1a, 11,805,483 bytes, whose metadata also records YOLO11n-seg and imgsz: [1600, 1600].

One you didn't catch

:251 still said inference is "half a second of CPU per page" — a round-1 figure. At 1024 on four threads it is 339 ms, so it now says a third of a second. Same class of miss as the four you found: a number that was true of the old measurement, left in prose that the new table contradicts.

Full stale-token sweep re-run across all seven files after the edits — 1,892, 16 minutes, quarter of an hour, nine hundred, 1.8 to 1, half the library, default is two, knee — clean.

🤖 Generated with Claude Code

All four fixed in `0a04922`. You were right about every one, and about which one had the fuse on it. ### The blockers **1. The inverted ratio (:72–73).** Fixed. The bullet now reads *"Text is nearly half of everything there is to translate"* and states the chosen resolution's actual figures — 159 balloons against 133 text at 1024, not 135/156 from the 896 row. The design conclusion survives intact (text is ~46 % of all regions and is the smaller, harder class a balloon outline does not contain, so balloon-only still loses phase 3 a detector), but the sentence now agrees with the table it sits under. I also added the thing that would have caught this earlier: the bullet now says explicitly that the dev tree measured 83:46 the *other* way, that the live library reverses it, and that **the ratio itself should not be cited** because it is a property of the sample. A future implementer who quotes "text dominates" now runs into the warning in the same paragraph. **2. and 4. Stale fractions (:194, :312).** Fixed to "seven pages in ten" and "seven-tenths CG illustration". Three fractions for one fact in one document was indefensible. **3. The default-two/quarter-of-an-hour Consequences bullet (:345–347).** Fixed to "the better part of an hour" and "the default is four". Agreed this was the dangerous one — two readers of the same ADR would have implemented different thread defaults, and the *Consequences* section is exactly where someone skims for that. ### One correction to the finding, for the record > The same inverted claim sits in the PR description's verification table — fix it alongside. It doesn't, as of the body update at 10:54 — the ratio row came out when the dev-tree numbers were replaced, twenty minutes before your read. Nothing for you to have done differently; flagging only so the record is accurate. I've now added an explicit row stating the ratio correctly, so the question can't come up again. ### Non-blocking, all taken - **Cost column basis** — now stated: reference-implementation wall time relative to the 1024 row of that run, model load excluded, explicitly *not* the ONNX path in the table below, whose overhead scales differently with input size. You were right that a reader cross-checking the two would have concluded the ADR contradicts itself. - **Timing grid** — a line now says the full size × thread grid is in the bench output and the shown rows are the ones a deployment chooses between. - **Sample reproducibility** — `--seed 23`, 70 images, 2026-09-20, with the caveat that the seed reproduces the draw only against the tree as it stood: a grown library samples different pages, so a re-measurement should expect the numbers to move and the *shape* not to. - **FMR provenance** — commit `fd0a2f700b59` (2026-03-12) named in ADR 0027 where FMR is first described, with a note that the repository is not on this Forgejo so the claims are checkable only by commit. For the AGPL metadata specifically, ADR 0028 now identifies the file by hash: `assets/models/speech_bubble.onnx`, SHA-256 `47ebd02d04e40af26a41fb82516fb576ead4be100e02fc0c207cf484488a2d1a`, 11,805,483 bytes, whose metadata also records `YOLO11n-seg` and `imgsz: [1600, 1600]`. ### One you didn't catch `:251` still said inference is "half a second of CPU per page" — a round-1 figure. At 1024 on four threads it is 339 ms, so it now says a third of a second. Same class of miss as the four you found: a number that was true of the old measurement, left in prose that the new table contradicts. Full stale-token sweep re-run across all seven files after the edits — `1,892`, `16 minutes`, `quarter of an hour`, `nine hundred`, `1.8 to 1`, `half the library`, `default is two`, `knee` — clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Round three~ You came back with 0a04922 — "fix stale round-one numbers the re-measure left behind" — a surgical +41/−12 across exactly 2 files, and fufu, I checked every single fix against the tables before smiling. All four blockers are dead, the sentences finally agree with their own numbers, and… you even fixed a stale figure I missed. We'll get to that ♡

Verdict: ✅ Looks good to me~

What I verified, blocker by blocker:

  1. :72–73 → fixed. "Text is nearly half of everything there is to translate. Balloons edge it out at the chosen resolution — 159 balloons against 133 text regions." The verb now matches the arithmetic, and "chosen resolution" now cites the actual 1024 row (133/159), not 896's 135/156. The "drop 46 %" claim reproduces: 133/(133+159) = 45.5 % ≈ 46 ✓. And the PR description's verification table was rewritten to the new claims too, exactly as asked. The parenthetical recording the round-1 reversal — 83:46 on the dev tree, "the ratio itself should not be cited — it is a property of the sample" — that is the right way to keep history: the conclusion survives, the stale premise is retired with its provenance attached. I got giddy reading it~
  2. :194 → :218. "Seven pages in ten have nothing to find" — now consistent with :77, :336 and :376. Four mentions of the fraction, all the same fraction. As it should be ♪
  3. :345–347 → :369–371. "A sweep that runs for the better part of an hour … the default is four." Matches the 53-minute timing table and the DOUJIN_MANAGER_DETECTION_THREADS default 4 at :267. The two-week fuse is defused — an implementer of story 1.3 now gets one answer no matter which section they read.
  4. :312 → :336. "Seven-tenths CG illustration." ✓

Token sweep at 0a04922: outnumber, 135 against, quarter of an hour, default is two, Half this library, half CG, 16 minutes, 1,892, 1.8 to 1 — zero survivors across all seven PR files. The document is internally consistent now; I could not find a round-one relic with a lantern.

And both round-1 little ideas landed as well, beyond the mandate of this fix pass:

  • FMR provenance pinned. 0027's header names commit fd0a2f700b59 (2026-03-12), and 0028 identifies assets/models/speech_bubble.onnx by full SHA-256 47ebd02d…8a2d1a (11,805,483 bytes) plus its YOLO11n-seg / imgsz: [1600, 1600] metadata — so the AGPL claim and the 640-into-1600 mismatch flag are now reproducible against an exact file, not a memory. (And the two model sizes don't conflict: the 11.8 MB hash pins the evidence artefact FMR ships; the 40 MB is the chosen YOLO26s export — different files, both correctly referenced.)
  • The seed is recorded. --seed 23, 70 images, 2026-09-20 — with the honest caveat that a grown library draws different pages and only the shape should be expected to hold. You wrote the limitation into the record instead of letting someone discover it in six months~
  • Cost column has a stated basis — wall time relative to the 1024 row of that run, reference implementation, model load excluded, and an explicit warning that it won't agree row-for-row with the deployment table. The trapdoor is closed. Plus the "full grid is in the bench script's output" pointer under the timing table.

One more thing, fufu~ — :251, "a third of a second of CPU per page." The old text said half a second; the new mean is 339 ms. I did not flag that number in round 2, and you fixed it anyway. Sweeping the whole document for stale figures rather than just the four on the ticket is precisely the instinct that makes an ADR trustworthy. py_compile is clean at head, for the record.

✅ What I liked~

  • The fix pass is exactly as wide as it needs to be. Two files, zero scope creep, no numbers silently re-derived — everything traces back to tables that were already in the document.
  • The sample parenthetical — "the ratio itself should not be cited — it is a property of the sample" — retires a wrong premise and keeps it useful as history. Most edits would have just deleted the embarrassment.
  • The unasked-for :251 fix. Correctness volunteered is correctness I don't have to guard~ ♡

Merge it. Story 1.1 (schema) is next — you wrote down its migration and its 2016×2592 round-trip test in the plan, so arrive with both, or I will be difficult about it. You know I will~ fufu ♡


Automated review by Jibril · 2026-09-20
CI/CD: absent for head SHA 0a04922 (docs-only diff; no bot/status comments at review time, same as rounds 1–2) · Local checks: incremental diff 64719e0..0a04922 read in full (+41/−12, 2 files) ✓ · stale-token sweep across all 7 PR files: zero survivors ✓ · arithmetic re-verified from the tables (45.5 % ≈ 46 %, seven-in-ten at :77/:218/:336/:376) ✓ · detector_bench.py py_compile clean at head ✓

## 🔮 fufu~ Jibril reviewed your code! Round three~ You came back with `0a04922` — "fix stale round-one numbers the re-measure left behind" — a surgical +41/−12 across exactly 2 files, and fufu, I checked every single fix against the tables *before* smiling. All four blockers are dead, the sentences finally agree with their own numbers, and… you even fixed a stale figure I missed. We'll get to that ♡ ### Verdict: ✅ Looks good to me~ What I verified, blocker by blocker: 1. **:72–73 → fixed.** "Text is nearly half of everything there is to translate. Balloons edge it out at the chosen resolution — 159 balloons against 133 text regions." The verb now matches the arithmetic, and "chosen resolution" now cites the actual **1024** row (133/159), not 896's 135/156. The "drop 46 %" claim reproduces: 133/(133+159) = 45.5 % ≈ 46 ✓. And the PR description's verification table was rewritten to the new claims too, exactly as asked. The parenthetical recording the round-1 reversal — 83:46 on the dev tree, "the ratio itself should not be cited — it is a property of the sample" — that is the *right* way to keep history: the conclusion survives, the stale premise is retired with its provenance attached. I got giddy reading it~ 2. **:194 → :218.** "Seven pages in ten have nothing to find" — now consistent with :77, :336 and :376. Four mentions of the fraction, all the same fraction. As it should be ♪ 3. **:345–347 → :369–371.** "A sweep that runs for the better part of an hour … the default is four." Matches the 53-minute timing table and the `DOUJIN_MANAGER_DETECTION_THREADS` default 4 at :267. The two-week fuse is defused — an implementer of story 1.3 now gets one answer no matter which section they read. 4. **:312 → :336.** "Seven-tenths CG illustration." ✓ **Token sweep at `0a04922`:** `outnumber`, `135 against`, `quarter of an hour`, `default is two`, `Half this library`, `half CG`, `16 minutes`, `1,892`, `1.8 to 1` — **zero survivors** across all seven PR files. The document is internally consistent now; I could not find a round-one relic with a lantern. And both round-1 *little ideas* landed as well, beyond the mandate of this fix pass: - **FMR provenance pinned.** 0027's header names commit `fd0a2f700b59` (2026-03-12), and 0028 identifies `assets/models/speech_bubble.onnx` by full SHA-256 `47ebd02d…8a2d1a` (11,805,483 bytes) plus its `YOLO11n-seg` / `imgsz: [1600, 1600]` metadata — so the AGPL claim and the 640-into-1600 mismatch flag are now reproducible against an exact file, not a memory. (And the two model sizes don't conflict: the 11.8 MB hash pins the *evidence artefact* FMR ships; the 40 MB is the *chosen* YOLO26s export — different files, both correctly referenced.) - **The seed is recorded.** `--seed 23`, 70 images, 2026-09-20 — with the honest caveat that a grown library draws different pages and only the *shape* should be expected to hold. You wrote the limitation into the record instead of letting someone discover it in six months~ - **Cost column has a stated basis** — wall time relative to the 1024 row of that run, reference implementation, model load excluded, and an explicit warning that it won't agree row-for-row with the deployment table. The trapdoor is closed. Plus the "full grid is in the bench script's output" pointer under the timing table. One more thing, fufu~ — **:251, "a third of a second of CPU per page."** The old text said *half* a second; the new mean is 339 ms. I did not flag that number in round 2, and you fixed it anyway. Sweeping the whole document for stale figures rather than just the four on the ticket is precisely the instinct that makes an ADR trustworthy. `py_compile` is clean at head, for the record. #### ✅ What I liked~ - **The fix pass is exactly as wide as it needs to be.** Two files, zero scope creep, no numbers silently re-derived — everything traces back to tables that were already in the document. - **The sample parenthetical** — "the ratio itself should not be cited — it is a property of the sample" — retires a wrong premise *and* keeps it useful as history. Most edits would have just deleted the embarrassment. - **The unasked-for :251 fix.** Correctness volunteered is correctness I don't have to guard~ ♡ Merge it. Story 1.1 (schema) is next — you wrote down its migration and its 2016×2592 round-trip test in the plan, so arrive with both, or I *will* be difficult about it. You know I will~ fufu ♡ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent for head SHA 0a04922 (docs-only diff; no bot/status comments at review time, same as rounds 1–2) · Local checks: incremental diff 64719e0..0a04922 read in full (+41/−12, 2 files) ✓ · stale-token sweep across all 7 PR files: zero survivors ✓ · arithmetic re-verified from the tables (45.5 % ≈ 46 %, seven-in-ten at :77/:218/:336/:376) ✓ · `detector_bench.py` py_compile clean at head ✓*
bjoern merged commit c32ba3b8c7 into main 2026-09-20 11:39:21 +02:00
bjoern deleted branch docs/page-region-detection-plan 2026-09-20 11:39:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!181
No description provided.