Repository navigation
The page is our atomic unit, but it isn't the unit of meaning #631
gbergengruen
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
The page is our atomic unit, but it isn't the unit of meaning
TL;DR
I set out to check whether
page-sectioningsends one page or several to the LLM. It sends one. Rather than argue from the code, we measured a real book end to end.In a real Spanish novel, 1 in 3 page boundaries splits a sentence — and the resulting fragments are translated and simplified independently, by design.
"Dá-lhe o"into the invented sentence"Diga."Everything below is measured on real books. Method and caveats are in §7.
1. What we measured, and how
Script:
scripts/measure-page-boundaries.mjs(read-only, no LLM calls). For every pair of consecutive pages it takes the last text leaf of page N and the first of page N+1 and scores:isContinuationheuristic, ported verbatim frompackages/pdf/src/positioned-text.ts:918-923— previous page ends with a hyphen, or the next starts with a lowercase letteractivity_*section on both sides, where the continuation has noactivity_instructionbut the previous side doesPruned sections, pruned nodes, and page furniture (
header/footer/page_number) are excluded — without that, running headers make every boundary look broken.Books. The prose case is
Uruguay_Ceibal_1930-el-viaje— a Ceibal educational novel, Spanish (es-UY),mode: dynamic. We ran pages 147-208, a 62-page span chosen because it contains no chapter breaks, so every boundary is genuine mid-narrative. Translation target pt-BR. Two workbooks (cuaderno3,cuaderno5) serve as controls.2. For prose, the chunk is the page
prompts/page_sectioning.liquid:161-164, indynamicmode:Both split conditions concern activity mechanics (
:148,:157). A prose page has neither. Chapter breaks, scene breaks and blank-line separators are explicit non-reasons.Measured, on the narrative window:
Exactly one section per page, no exceptions. In the packaged bundle of the original 25-page run, 18 of 19 files are
pgNNN_sec001(page 23 is the sole page that produced two sections).So the section boundary is the page boundary, and the page boundary is wherever the typesetter's text happened to run out.
And below sectioning there is nothing durable
Leaves are sentences (
page_sectioning.liquid:122). Paragraphs exist only as an LLM-chosen container (config.yaml:8-9) that no code reads — grep finds"paragraph"only as a font scope. Containers don't even carry an id (_render_node.liquid:25; only leaves getdata-id).A continuation heuristic exists (
positioned-text.ts:918-923) but is confined within a block, within a page (:201), and on spreads deliberately per-side (:262-265). Its only consumer isfixed-layout-rendering.ts:126-136, and no book uses fixed layout.The page boundary is then re-hardened at every layer below: one HTML file per section (
packaging/web.ts:355), one XHTML spine item per page (packaging/epub.ts:171-234), one SMIL overlay per page (smil.ts:58-98), and audio that stops and resets at page end (useAudioPlayer.ts:249-257— the code comment says "a TTS index from page N has no meaning on page N+1").3. How often it happens
20 of 60 boundaries (33%) split a sentence. 19 of 60 (32%) leave an open sentence.
Examples, verbatim:
"Por""lo que leí, se trata de alguien…""Le da lo""mismo cómo se llame esa jovencita…""…su camarada le""dio un puñetazo…""…cantando en el""puente del Sena.""Sí, hay""varias anotaciones de abril…"A page whose last word is
"Por"is not an edge case we can wave away — it is 1 of 20 in a 62-page sample.4. What it costs — measured, not predicted
4a. Translation: 3 of 20 broken
Translation gets one call per page and its entire context is
{...languageContext, texts}(translation.ts:75-78).prompts/translation.liquidrule 1 says "Translate each text segment independently" and rule 7 "Do not add, remove, or merge text segments". There is no context field to fix this with."…cantando no"/"ponte do Sena."na ponte. Spanish puente is masculine, Portuguese ponte is feminine. The model contracteden el→nowithout knowing the noun overleaf changes gender between the two languages."Dá-lhe o"/"mesmo como…"Le da lo mismo("he couldn't care less") became word-salad reading as an imperative "give him the"."Por"/"o que li…"Por o que liis ungrammatical — Portuguese requires the contractionPelo que li.17 of 20 survived, and that matters: Spanish→Portuguese is the most forgiving pair we could have picked. Adjectives already carry gender and number, so
"não são questões"/"ocas."came out correct. 15% is a floor, not a typical rate. A more distant target (English, Albanian) would break considerably more.4b. Easy Read: the worst affected step
Easy Read runs after translation on the Portuguese text. Its prompt promises context it structurally cannot have —
easy_read.liquid:55-58says "You will also receive the full text of the section as context. Use the context to understand references, omitted subjects, continuity…", butsection_textis built only from that block's own entries (easy-read.ts:217), and a block is one section of one page. Set against:64("Inventing new information… is forbidden") and:69("use the context to deduce the exact referent"), a page boundary is an instruction collision with no valid resolution.The model resolves it by inventing. Roughly 8-11 of 19 page-final fragments show visible damage, in three categories:
Fabrication
"Dá-lhe o""Diga."— an invented imperative, from nothing"Conta até dez e se põe""Ele conta até 10 e começa."— invents "starts", loses "stands up""Terá tempo para pensar no que o espera""o senhor"as a subjectFalse termination — pg205
"Sim, há"→"Sim, há.", pg189 →"exatamente."Adding a full stop asserts the sentence is complete when it continues overleaf.Pass-through — pg155, pg163, pg174, pg191 and pg201 are left as dangling fragments. On page 201 the entire Easy Read content is the word
"Por".This is the feature built for readers least able to recover from incoherence, and it is the one most affected.
4c. Read-aloud and SMIL — derivable without running TTS
The TTS unit is one
data-idleaf (text-catalog.ts:45-71), there is no terminal-punctuation check and no SSML inspeech.ts, and the page-batched path explicitly joins entries with\n\nso the model treats each as its own sentence (speech-batch.ts:194-199). Because the split sentence is two leaves and the merged one is a single leaf, the unmerged book necessarily produces two clips with a falling cadence on"Por", and the merged book one. SMIL highlighting terminates at the fragment (smil.ts:58); playback stops at page end.4d. Pruning creates adjacencies that never existed
The original 25-page run packages to 19 files —
separatorpages are dropped — so the reader goes straight from page 011's text to page 013's with no cue. Measuring raw page order rather than reading order would have understated the split rate (21% vs 29% on that sample).5. Proof that merging fixes it
We re-ran the identical page range with
spread_pairsset at the flagged boundaries, so both halves arrive in a single sectioning call and a single translation call — the condition any "give the model neighbour context" fix would create."Le da lo mismo cómo se llame esa jovencita ni lo que puede estar pensando Tomassino.""Dá-lhe o"+"mesmo como…""Tanto faz para ele como se chama essa mocinha…"— correct idiom"Diga."+"nem como essa mocinha se chama…""Para o Romeno, tanto faz como essa moça se chama…""Por"+"o que li""Pelo que li, trata-se de alguém muito conhecido…"Both covered cases fixed, at every stage.
Two things worth noticing. First, the merged Easy Read is better than its source — it resolved the pronoun to
"Para o Romeno", which is exactly what its prompt promises and what it could actually do once the sentence was intact. Second, and more important for choosing a fix: the repair happened at the leaf. Sectioning emitted the whole sentence as one leaf, and everything downstream inherited that. The defect was never only "translation lacked context" — the leaf boundary itself was wrong.6. What is not a problem — a retraction
I began this review believing split activities were the headline problem, because the code path is alarming: an orphan half-activity gets its own answer key from a second LLM call seeing only that section, and
activity_multiple_choice_answers.liquid:35instructs it to emit all-falsewhen undeterminable. With an all-falsekey,allCorrectis unreachable (activity-quiz.ts:565-575), so Submit can never become Next — a learner stranded on a page of instruction-less questions.The data does not support this happening. Across
cuaderno3andcuaderno5, S4 fired on 2 of 115 boundaries and both are false positives — the continuation began with its own heading ("¿Cuáles son los sucesos?","Apuntes"). Activities in our workbooks do not span page breaks; each page's activity carries its own instruction.Related: the workbooks show no prose problem either — S1 was 0 of 20 in
cuaderno3and 1 of 95 incuaderno5. This is a narrative-prose issue specifically.The broken-Submit path remains real code and worth hardening cheaply, but it is a latent risk, not an observed defect. I had it as a headline; that was reasoning from what the code permits rather than what the books contain.
7. Limits of this measurement
Stated plainly, because the numbers are only as good as these:
"Diga."fabrication I'm confident about; the fuller Easy Read tally should be checked by a Portuguese speaker.no pontecase wasn't covered: the pairs were chosen on predicted severity before we knew which boundaries actually broke, and page 163 got consumed by a different pair.cuaderno5has no PDF text layer; all its text came from the vision model, so OCR fidelity is a confound unique to it.8. Options
Re-ranked after the measurement. This reverses my earlier instinct, which put the cheap context-only fix first.
Structural — fixes the leaf
C. Cross-page continuity in sectioning. Neighbour page text in the sectioning prompt plus
continuesFromPrev/continuesToNextflags (C1), then an automatic merge post-pass reusing the existingsourcePageIdsmachinery (C2).For: this is what the counterfactual actually did. The render side already honours
sourcePageIds(web-rendering.ts:194-212), and the manualmerge-cross-pageendpoint (pages.ts:2022) already implements the merge and renumber.Against: C2 changes section counts and IDs — the keys for web-rendering, quizzes, text-catalog, TTS ids, SMIL and packaged filenames. Needs a consumer-by-consumer audit. A false-positive merge is worse than a miss.
D. Sentence-level stitching. Extend
isContinuationacross the page boundary and join the two fragments into one leaf.For: narrower blast radius than C2 — it moves text between two leaves rather than sections between pages — and it is the only option that also fixes TTS prosody and SMIL, because it fixes the unit those consume.
Against: a leaf lives in exactly one document, so the joined sentence either disappears from one page or is duplicated. Neither is obviously right, and it interacts with
data-idstability and word timestamps.Context-only — partial
B. Give
translation,easy-readand glossary a read-onlyprev_text/next_text.For: no ID churn, no packaging or runtime change; and it makes the Easy Read prompt honest about the context it already claims to receive.
Against — and this is new information: it is a partial fix. It would plausibly repair the contraction case (emitting
"Pelo"+"que li"), but probably not the idiom, becausetranslation.liquidrule 7 forbids redistributing content across segments and"Tanto faz para ele"does not split cleanly across the original fragments. It leaves two leaves, so TTS and SMIL stay broken.Independent of page boundaries
A. Distribute book context we already compute.
book-summaryruns over the whole book and reaches exactly two prompts —image-captioningandfont-assignment(a typography decision). Not sectioning, rendering, quizzes, glossary, easy-read or translation.metadatais read back only forlanguage_code; grade level and subject never reach a prompt.toc-generation's output is consumed by no prompt at all. Cheap to fix — the plumbing pattern is already in production (image-captioning.ts:127).E. Structure-aware batching. Quizzes are built from fixed 3-page slices of tag-stripped text (
quiz-generation.ts:117-127); the sectioning tree is used only as a filter and discarded.toc-generation's unused output is the obvious input.Small and unrelated
F.
activity_multiple_choice_answers.liquid:35should emit an explicit "undeterminable" the runtime treats as ungraded, rather than all-false. Latent per §6, but it converts a silent trap into a visible gap.G. A real bug found along the way, nothing to do with page boundaries: Easy Read's numeral-simplification converted the indefinite article
uminto the numeral1—"tem um profundo significado"→"tem 1 profundo". Worth its own issue.9. Proposal, for debate
Where I'd most like pushback
mode: pageis set deliberately on all four narrative books. Considered decision about reading experience, or a default nobody revisited? If we want meaningful chunks, the honest answer may be chunking by paragraph or scene and giving up page fidelity for reflowable books — a much bigger conversation than any option above.All reactions