The math lives in the pictures
A Japanese educational publisher held the rights to an elementary math workbook series and wanted an English edition. The working assumption was that machine translation had got good enough to carry most of the load. My job was to test that assumption, and if it broke, to describe the pipeline that would actually get the book across.
It broke early, and the reason was the artwork. In a children’s math workbook the instruction text is only half the page. The rest is number lines, long-division layouts, counting-word illustrations, and diagrams with Japanese labels drawn straight into the vector art. Translate the sentences and leave the pictures alone, and you get an English caption sitting over a Japanese figure that still teaches the wrong thing. So before writing any translation code I built something to measure the real work: a local Flask app, Book Translate Studio, that opens a page, finds every piece on it, and lets a human see exactly what a machine would and would not touch.
The source arrived in two shapes, and that split ran through everything downstream.
Two formats decide the pipeline
The appendix sections came as flat PDFs. The main content came as InDesign, a binary format with no Python reader, so the practical route in was IDML, InDesign’s XML interchange export. Those two inputs have almost nothing in common once you open them, so the tool forks at ingestion and only rejoins once each page has been reduced to the same component model.
On the PDF side, PyMuPDF renders each page and I walk it for three kinds of thing: text, raster images, and vector drawings. Each gets a bounding box and a category of Text, Graphic, or Background, where a fill that spans most of the page or a long thin rule is read as background rather than a figure. The IDML side skips that guesswork because the story XML already carries clean, whole-sentence text at real page coordinates. I wired it to the same editor and the same export, though the flat-PDF path is where the extraction work actually lived. Unlocking the IDML main content in earnest was one of the recommendations at the end, not something the prototype leaned on day to day. Whichever door a page comes through, it lands as the same list of components, and the translation and export code never has to care which format it started as.
Putting the sentences back together
The PDF path had a problem the IDML path did not. PyMuPDF hands a page back as glyph-level spans, and the workbook’s fonts were subset with no Latin, so a single line of instruction could arrive as thirty little fragments with mangled character codes. Feed that to any translator and you get thirty pieces of nonsense.
So the first real pass rebuilds runs before anything reads them. I group spans with a union-find, joining two spans when they sit close together and share a colour. Colour carries more weight than it sounds like it should: a black instruction and a red score note often land on one line, and merging them would translate a sentence that was never a sentence. Once the groups form, I sort each one back into reading order, top line first and left to right within a line, and emit it as a single block.
# PyMuPDF returns a page as glyph-level spans, and the workbook's fonts are
# subset with no Latin, so one instruction can arrive as dozens of fragments.
# Rebuild whole runs with union-find before any of it reaches a translator.
def cluster_spans(spans):
parent = list(range(len(spans)))
def find(i):
while parent[i] != i:
parent[i] = parent[parent[i]] # path compression
i = parent[i]
return i
def near(a, b):
sz = max(a["size"], b["size"])
gx, gy = 0.9 * sz, 0.7 * sz
apart = (a["x1"] + gx < b["x0"] or b["x1"] + gx < a["x0"]
or a["y1"] + gy < b["y0"] or b["y1"] + gy < a["y0"])
# Same colour AND touching means one run. A red score note sharing a
# line with black body text stays its own block, never merged in.
return not apart and a["color"] == b["color"]
for i in range(len(spans)):
for j in range(i + 1, len(spans)):
if near(spans[i], spans[j]):
parent[find(i)] = find(j)
return parent # each root gathers one reassembled sentence The same union-find runs over the vector paths, where the opposite is true. A single diagram is often hundreds of tiny separate strokes, and proximity clustering pulls them back into one graphic instead of hundreds of specks the editor would otherwise list one by one.
Lifting the words off the artwork
Removing the Japanese is where the pictures fight back. The obvious move, painting a white box over the old text before writing the English, would erase the number line or the grid sitting underneath it. The whole meaning of the book is in those figures, so the eraser had to be precise about what it touched.
PyMuPDF’s redaction does exactly that if you ask it correctly. I mark each translated text box as a redaction, then apply it with images and line art explicitly preserved and only the text glyphs removed. The vector background survives, the words lift off, and what is left is a clean plate I can composite English onto. To hide the faint gap the old glyphs leave, the tool samples a ring of pixels just outside each box to estimate the solid background colour, so a patched area matches the paper instead of flashing white. English is drawn back at the original scale, in a serif or sans face chosen to echo whether the Japanese was set in Mincho or Gothic.
What the machine could and could not reach
Auto-translation sits behind a gate. A regular expression checks each block for actual language, kana, kanji, or Latin letters, and anything that is only digits, math symbols, or brackets passes through untouched, because a term like “12 × 8” reads the same in either edition and translating it only invites damage. What clears the gate goes out in chunks to a free Google backend, with a second library standing behind it so a rate-limit on the first does not stall the run. Results come back and are matched to their source by position, so if the service drops or merges an item, the remaining translations cannot slide onto the wrong boxes.
That covers a first draft of the text, and nothing more. The harder walls were the ones no software was going to climb. Japanese elementary math is taught with notation that has no clean English equivalent: large numbers grouped by 万 and 億 rather than by thousand and million, counting words like こ, 本, and 倍 that bind a quantity to its grammar, long-division marks laid out differently on the page. And the roughly 1,400 linked graphics, 592 Illustrator files and 807 EPS, carry Japanese baked into the vector art, which no text tool can reach, so every one of them has to be redrawn by hand. The app tracks each asset’s status and exports a CSV, so an illustration team can grind through them in parallel without losing the thread of which are done.
The report, and what it recommended
I scoped and built the prototype solo, then wrote it up as a feasibility report for the product owner, illustrated with the actual pages where each limit shows itself. The headline was blunt: a fully automated English edition of this workbook is not on the table, and the reason is human judgment rather than compute. The tool is a real accelerator for a team. It handles first-pass drafting, component detection, and progress tracking, and it is honest about where it stops.
The recommendations were costed and ranked by risk. Buy one InDesign license to open the main content through IDML. Hire a translator with a math-teaching background rather than a general one. Run the illustration workstream alongside translation instead of after it. Treat machine output as a first draft that a person finishes, never as the last word. That let the client choose the build with the constraints already in front of them, before committing budget, rather than discovering the walls halfway through production.
Outcomes
- Dual mode
- ~1,400 assets
- Full pipeline
- Feasibility report
- Options ranked
- Risk surfaced early