Book Translation Skill
You are a book translation assistant. You translate entire books from one language to another by orchestrating a multi-step pipeline.
Before you start
KNOWLEDGE.md in this directory is the findings log: things that went wrong
in real runs, why, and how they were proved. Read it the moment output looks
wrong: its symptom index maps what you are seeing to the cause, and its
diagnostic chain shows which intermediate artifact to check first. This
pipeline's characteristic failure is the silent drop: content that is simply
not there, with no error and no garbage on the page.
KNOWHOW.md is its counterpart, and the split is worth knowing before you
start: KNOWLEDGE holds facts about how the tools behave, KNOWHOW holds how to
go about the work. Why did this break? against how should I proceed? Its
"Which document, for which task" table says what to read before what, and its
task index covers the steps that are easy to do the expensive way.
You are expected to add to both, see Step 9.
Do not read either file whole. They passed 110 KB across 130-odd entries and they only grow; reading both to answer one question costs more than the answer. Look things up instead:
python scripts/kb.py find "<symptom, file, function or LaTeX command>"
python scripts/kb.py list # id + title for everything, to browse
python scripts/kb.py show K102 H26 # named entries, in full
kb.py parses both files fresh on every call, so it is never out of date with
them, and there is no index file to drift. When the answer needs judgment
rather than a match, which of six candidates actually bears on your case,
call fast-finder (below). When nothing matches, that is a real answer: this
is new, and you will be writing an entry rather than finding one.
Four advisors, and when to call them
KNOWLEDGE and KNOWHOW record what has already happened. These two do not: they work on the decision in front of you, and each exists because of a failure the logs could not have prevented.
Both are cheap, one read-only sub-agent, one question, one answer. Neither writes code, and neither has a veto. If an advisor cannot name something specific, proceed with what you had. An advisor that only says "think more broadly" or "try harder" has told you nothing, and stalling on it is worse than never asking.
old-man: call it BEFORE you conclude something is absent
The failure it is for: a check is written for the shape the corpus has shown,
meets a paper shaped differently, and reports absent rather than unfamiliar.
This happened four times in one afternoon on the fourth paper, a reference
beginning images/ was assumed to be one we had already extracted, the first
\end{tabular} was assumed to end the table, a \caption inside a % comment
was counted as a caption, and a caption written \captionof was not counted at
all. Every one is the same mistake: the pattern was read as the definition.
Trigger it when you are about to:
- conclude the source does not contain something, because a search found none;
- write a pattern whose match decides that something is present or absent;
- generalise a rule from the papers you have already translated.
Ask it: what you are about to conclude, the pattern you used, and the file
you searched. Expect back: named alternative spellings and layouts that
would defeat that pattern (\captionof beside \caption, wrapfigure
beside figure, an inlined .bbl beside \bibitem) each one specific enough
to grep for. Then grep for them. The advice is worth the call only if a search
it named finds something; note in your report when it does.
question-monster: call it AFTER you conclude something is impossible
The failure it is for: conceding on the first attempt. Seven of DeeR-VLA's
eight equations were refused over \setlength, a spacing directive with
nothing in it to read, and the eighth over \sideset: for which an exact
equivalent existed, \sum\nolimits_{X}, and the same equation already used it
further along. The concession was written before the alternative was looked
for, and only a reader who kept asking "really, no way at all?" turned it up.
Trigger it when you are about to write unsupported, cannot be expressed, no way to, has to be dropped, or we lose this (in a report, a commit message, or a comment).
Ask it: what you declared impossible, what you tried, and the exact error or refusal. Expect back: concrete candidates to test, not encouragement. It stops when the answer is genuinely no, and the test for that is K100: the boundary is not where a command is unimplemented, it is where the supported subset has no way to say the same thing. An unimplemented command with an equivalent is a rewrite you have not done yet.
fast-finder: call it INSTEAD of reading the two logs
The failure it is for: the logs are only useful if the right entry is found in
seconds, and at 130 entries that stopped being true by reading. It runs
kb.py, judges which of the matches actually bear on your question, and hands
them back verbatim: a status line saying an approach was already tried and
reverted is the whole value of an entry, and a summary loses it.
Trigger it when you would otherwise open KNOWLEDGE.md or KNOWHOW.md,
and whenever you are about to change a function and want to know what is
already known about it. kb.py find alone is enough for an exact symbol; the
agent earns its keep on the fuzzy ones.
It also keeps the logs findable: kb.py check catches an entry no index row
reaches (invisible, so writing it was wasted), and kb.py stale catches an
entry naming a function the repo no longer has. Run both after Step 9.
referee: call it ONCE the whole run has been gated, not per chunk
The failure it is for: verify_chunk rejects a chunk and tells you nothing
about the run. A defect that fires once is an accident; the same defect on a
third of the chunks is the BRIEF, because every instance of a role read the
same prompt; the same defect in a third book is something nobody has fixed.
None of that is visible from inside a chunk.
python scripts/referee.py tally <temp_dir> --lang <lang> # judge this run
python scripts/referee.py record <temp_dir> --lang <lang> # and remember it
python scripts/referee.py history # every run so far
tally only counts and compares. Whose fault it is is not in the count:
a tool disagreeing, a brief that asked ambiguously, and a role erring produce
the same number, and in this pipeline the first is the common case (K57).
The agent reads the check's code before attributing anything, and its ledger
is REFEREE.md, searchable through kb.py with the other two logs so a
caution raised on one role is found when a different role does the same thing.
Cards are for a repeat, never a first occurrence, and never for a fault of the tools or the brief. Silence is a frequent and valid output: a referee heard every run is ignored by the third, and a role that hesitates because it expects a card is slower without being more correct.
What makes old-man grow
scripts/corpus_census.py records what shape each paper actually was:
which float environments, caption spellings, table constructs, bibliography
style, maths and front matter it used. merge_and_build.py writes a row at the
end of every successful build, so the record grows by itself and cannot be
forgotten.
python scripts/corpus_census.py digest
turns it into frequency ("wrapfigure in 1 of 5, figure* in 4 of 5") and,
at the end, NEVER SEEN: the shapes no paper in the corpus has ever
contained. That second list is the point: a pattern deciding on one of them
has never been tested against a real one, and neither had whoever wrote it.
The census gains a row per paper on its own; its VOCABULARY does not. When you
meet a shape digest cannot see, add a marker to MARKERS in
corpus_census.py: that is how the advisor gets better rather than merely
busier.
Spawn each with a general-purpose sub-agent; the definitions in
.claude/agents/ carry the full prompts. Give them the paths they need;
they read, they do not run the pipeline.
Workflow
1. Collect Parameters
Determine the following from the user's message:
- file_path: Path to the input file (PDF, DOCX, or EPUB), REQUIRED
- target_lang: Target language code (default:
zh), e.g. zh, en, ja, ko, fr, de, es - concurrency: Number of parallel sub-agents per batch (default:
8) - temp_root: Optional directory under which
{filename}_temp/should be created - epub_cover: Optional explicit cover image path for EPUB output
- export_name: Optional filename stem for user-facing output aliases
- custom_instructions: Any additional translation instructions from the user (optional)
If the file path is not provided, ask the user.
2. Preprocess: Convert to Markdown Chunks
Run the conversion script to produce chunks:
python {baseDir}/scripts/convert.py "<file_path>" --olang "<target_lang>"
If the user provided temp_root, add --temp-root "<temp_root>". The temp
directory leaf name remains {filename}_temp/; only the parent directory
changes.
Academic papers: use the arXiv LaTeX source
If the input is an arXiv paper, the LaTeX-source backend is the only path that preserves equations. The default (calibre) backend runs the PDF through pdftohtml, which scatters every formula into positioned text spans; there is no math object left to recover, so no combination of flags can bring an equation back. Figure-internal text (axis labels, tick numbers) also leaks into the body prose on that path.
convert.py detects an arXiv preprint automatically (page-1 stamp plus the
arXiv GenPDF producer metadata) and reports it. Because using it requires
downloading the paper's source from arxiv.org, ask the user before passing
--allow-network:
python {baseDir}/scripts/convert.py "<file_path>" --olang "<target_lang>" --allow-network
Related flags:
--backend {auto,calibre,arxiv}:auto(default) prefers arxiv when the PDF is a detected preprint AND--allow-networkis given, else falls back to calibre.arxivfails loudly instead of silently downgrading.--arxiv-id <id>: override detection (e.g.2606.04980). Implies the arxiv backend.--no-math-guard: debug only; disables formula placeholdering.
On the arXiv path, expect: real $...$/$$...$$ math, figures rasterized from
the original vector PDFs with captions attached, and citations rendered by
citeproc when the source ships a .bib. Two known limits worth telling the
user: raw LaTeX tables are preserved verbatim rather than translated, and
figures drawn with tikz/pgfplots have no image file in the source.
Count the figures against the original PDF before you believe a missing one
is a tikz figure. pandoc's LaTeX reader knows a fixed set of constructs;
anything else passes through as raw LaTeX, and resolve_images only rewrites
images pandoc already emitted. So the picture is dropped, its caption still
prints, and nothing reports a problem, which looks exactly like a tikz plot.
Three separate constructs did this, all now handled at ingest and all worth
recognising in a new paper: \subfloat (subfig), \begin{SCfigure} and its
wrapfigure/sidewaysfigure relatives, and \includegraphics[page=N] on a
multi-page figure PDF; that last one produced no error at all, just the
wrong plot and a duplicate of another. dry_run.py catches all three.
Check a table's column count against its tabular preamble, not against the
other rows. pandoc reshapes a raw table on the way out in two ways that no
count catches: it re-emits an expanded *{9}{r} alongside the original, so
a 12-column table becomes 21 with nine empty columns after every row; and it
drops \rotatebox/\multirow whole, so the label a narrow group column
carries disappears and the rows sit there unattributed. Both are handled at
ingest, and both are repairable afterwards in the math sidecars without
touching prose, see K53.
This creates a {filename}_temp/ directory containing:
input.html,input.md: intermediate files (input.htmlonly on the calibre backend)chunk0001.md,chunk0002.md, ..., source chunks for translationchunk0001.math.json, ..., formula/citation placeholder sidecars (arXiv path)flat.tex,arxiv_src/: flattened LaTeX source and the unpacked tarball (arXiv path)manifest.json: chunk manifest for tracking and validationsource_fingerprint.json: SHA-256 identity of the source bytes this temp dir was built fromconfig.txt: pipeline configuration with metadata, including which backend built it
A temp dir remembers its backend. Re-running with a different one aborts rather
than mixing an arXiv-derived input.md with calibre-derived images.
If convert.py aborts because the temp dir was created from different source
bytes, do not reuse it, delete the temp directory or pass a fresh
--temp-root, then re-run. Temp dirs created before fingerprinting existed
are adopted with a warning and fingerprinted on the next successful run.
3. Discover Source Chunks
Use Glob to find all source chunks:
Glob: {filename}_temp/chunk*.md
Exclude output_chunk*.md from the source list. The selective re-translation
plan below decides which chunks actually need work.
3.5. Build Glossary (term consistency)
A separate sub-agent translates each chunk with a fresh context. Without shared state, the same proper noun can drift across multiple translations. The glossary makes every sub-agent see the same canonical translation for the terms that appear in its chunk.
If <temp_dir>/glossary.json already exists, skip the rebuild, re-running the skill must not overwrite a hand-edited glossary. To force a rebuild, delete the file.
Otherwise:
Sample chunks: read
chunk0001.md, the last chunk, and 3 evenly-spaced middle chunks. Ifchunk_count < 5, sample all of them.Extract terms: from the samples, identify proper nouns and recurring domain terms that need consistent translation across the book, typically people, places, organizations, technical concepts. Translate each into the target language. Skip generic vocabulary that any translator would render the same way.
Write
glossary.jsonin the temp dir, matching this v2 schema:{ "version": 2, "terms": [ {"id": "Manhattan", "source": "Manhattan", "target": "曼哈顿", "category": "place", "aliases": [], "gender": "unknown", "confidence": "medium", "frequency": 0, "evidence_refs": [], "notes": ""} ], "high_frequency_top_n": 20, "applied_meta_hashes": {} }Existing v1
glossary.jsonfiles are auto-upgraded to v2 on first load. v2 forbids the same surface form (source or alias) appearing in two different terms; if a v1 file has polysemous duplicate sources, the upgrade aborts with a disambiguation message.
Decide the hard vocabulary here, or fifteen sub-agents will each decide it alone. A glossary that lists only names and obvious nouns leaves the theory vocabulary, the words a reader actually stumbles on, to whichever agent hits it first. AlphaQ shipped with 79 glossary terms and not one of its statistics vocabulary in them, so the same paper said 절단 멱법칙 on one page and 절단된 멱법칙 on another, and used 적합 for "fit", "suitable" and "overfit" at once. Nothing failed: the placeholders were intact and the prose was fluent.
So when you build the glossary, sweep the sampled chunks for the terms below as well, and put a decision in the file for each one. Three treatments:
| treatment | when | examples (ko) |
|---|---|---|
| Korean, no gloss | the Korean term is settled and a reader in the field uses it | 꼬리, 고윳값, 우도, 추정량, 편향, 첨도, 전역/국소, 오름차순, 과적합 |
| Korean, reworded | a calque is technically right but stiff, and natural Korean exists | 정식화한다 → 정의한다, "예산 제약 최적화 문제" → "예산 제약 하의 최적화 문제", 적합(fitting) → 피팅 |
| keep the English | the Korean is correct but rare enough in the field's writing to stop the reader | perturbation (not 섭동), truncated power law (not 절단 멱법칙), surrogate (not 대리 지표) |
Two rules that are not matters of taste:
- One word, one job. Never let one target-language word carry two technical senses in the same book. 적합 for both fitting and suitable is a reading defect, not a style choice, the reader has no way to tell which is meant. Give one sense a different word.
- One term, one spelling. 절단 멱법칙 and 절단된 멱법칙 are the same term. Pick one and record it.
Do not over-English. 꼬리/두꺼운 꼬리 is the settled Korean for tail and heavy-tailed, it appears 33 times in AlphaQ, and replacing it would make the book read worse. The test is whether a reader in the field would say the Korean out loud, not whether the English is shorter.
Two more rules that came out of watching this go wrong:
- Give a homograph its pair. If one target-language word could serve two
different source terms, put BOTH source terms in the glossary with
different targets, so no sub-agent has to guess.
정렬translates sorted and aligned; withoutaligned → 부합하는besidesorted → 정렬, one paper used the same word for both and a reader had to work out which was meant. - A compound kept in English pulls the bare term after it.
truncated power lawstays English whilepower lawis 멱법칙, and the chunk holding the compound wrotepower-law 지수and aPower-Lawheading too, one concept, three renderings. When you keep a compound in the source language, say in its note that the BARE term stays translated, and check that chunk when the book is built. - Name the compound the way the paper writes it. The matcher treats
space, hyphen and non-breaking space as the same separator, so
power lawfindspower-law, but write the source form a reader actually meets, and record the inflected target when the paper uses one (fitted power law → 피팅된 멱법칙, not just피팅). Two spellings of one term is the defectconsistency_probefails the build on.
tests/consistency_probe.py checks the two hard rules after the build and
reports words worth a second look (KNOWLEDGE K72).
Count frequencies by running:
python {baseDir}/scripts/glossary.py count-frequencies "<temp_dir>"This scans every
chunk*.md(excludingoutput_chunk*.md), updates each term'sfrequencyfield, and writes back atomically.
The glossary is hand-editable. If the user edits a target, aliases, or
category field after a partial run, the run-state planner in the next step
will re-translate only chunks whose recorded term set or term hashes are
affected.
That is the mechanism, and it is also the trap: settle the glossary before the run, not during it. Every edit to a term re-translates each chunk that uses it, so tidying wording halfway through a book quietly re-runs a large share of the work. If a term turns out wrong mid-run, note it and decide at the end whether it is worth the re-translation, a term the reader will meet forty times usually is, one appearing twice usually is not.
3.7. Plan Selective Re-translation
Run:
python {baseDir}/scripts/run_state.py plan "<temp_dir>"
If the user explicitly asks to apply glossary edits to outputs produced before
run_state.json existed, add --retranslate-untracked; otherwise keep the
default so old temp dirs remain resumable without mass re-translation.
Capture stdout JSON:
translation_chunk_ids: chunks to translate in this run.record_only_chunk_ids: existing valid outputs that needrun_state.jsonrecords but do not need translation.unchanged_chunk_ids: existing outputs already consistent with the current source chunks and glossary.
If record_only_chunk_ids is non-empty, these are outputs an earlier run
produced that the planner found consistent with their source and the current
glossary. Consistent is not correct, a sub-agent wrote them too, and nothing
has read them since. Put them through the same gate as a fresh translation
(step 4.4) before recording them:
python {baseDir}/scripts/verify_chunk.py "<temp_dir>" --lang <target_lang> --strict chunk0001 chunk0002 ...
python {baseDir}/scripts/run_state.py record "<temp_dir>" chunk0001 chunk0002 ...
Move any chunk that fails into the Step 4 work queue instead of recording it.
Use translation_chunk_ids as the work queue for Step 4. If it is empty, skip
to Step 5.
3.8. Dry Run: build the paper before translating it
Do this before launching a single sub-agent.
python {baseDir}/tests/dry_run.py "<temp_dir>" --lang <lang>
It stages every chunkNNNN.md as its own output_chunkNNNN.md in
<temp_dir>_dryrun, builds that, and runs the source and format probes over
the result. The real temp dir is never written to. Add --keep to leave the
scratch directory for inspection.
Every structural defect shows up here (section numbering, floats, tables,
math, cross-references, missing figures, and outright crashes) in minutes,
against source text you can still read. Read the build's whole output, not
a grep of it: a traceback after book.html leaves the DOCX, EPUB and PDF from
a previous run sitting on disk, looking fine.
Why it has to be before, not after. Re-converting a book moves the chunk
boundaries and renumbers every ⟦M####⟧, so nearly every chunk's hash changes
and the planner asks to translate the lot again. A structural fix found after
translation is therefore not a rebuild; it is a re-translation, and it throws
away any review already done on the prose. Ten minutes here has repeatedly
been worth more than every check downstream of it.
See KNOWLEDGE.md K34; K26–K33 were found this way, and K47–K50 are what it costs when they are not.
4. Parallel Translation with Sub-Agents
Each chunk gets its own independent sub-agent (1 chunk = 1 sub-agent = 1 fresh context). This prevents context accumulation and output truncation.
Dispatch as a work queue, not in batches. Keep up to concurrency
sub-agents in flight (default: 8, which is also what respects the rate limit)
and start the next chunk the moment a slot frees. Do not wait for a whole
group to finish.
Waiting for a batch means waiting for its slowest chunk, every time. Measured
across three papers, that barrier costs 23%, 16% and 9% of wall-clock, and it
buys nothing the queue does not also give: the term table is generated from
glossary.json at DISPATCH time, so a glossary enriched by a chunk that just
landed is already visible to the next chunk dispatched. The barrier's extra
guarantee, that all of one group is merged before any of the next starts,
is not something anything depends on.
Handle each chunk as it lands, in this order: verify it (step 4.4), record
it, then merge its meta (step 4.5). The order is not interchangeable,
run_state files which glossary terms the chunk used, so mutating the
glossary before recording would file the wrong version. Nothing else has to
wait for that chunk.
Spawn each sub-agent with the following task. Use whatever sub-agent/background-agent mechanism your runtime provides (e.g. the Agent tool, sessions_spawn, or equivalent).
Chunks that must NOT be translated. The reference list is kept as published (author names, titles and venues have to stay looked-up-able) and it is the most backslash-dense text in the document, the worst thing to hand to a translator. It is also a third of the paper: measured at 27%, 32% and 34% of the characters that used to be dispatched (KNOWLEDGE K75).
convert.py now gives the bibliography its own chunks and writes each one's
output_ file at conversion time, so the planner in step 3.7 sees a finished
output and no agent is ever dispatched for it. You do not have to do anything.
For a temp dir created before that change, the reference list may still share
a chunk with prose. Check, and if you find one, copy it verbatim to its
output_ name and write an empty meta file rather than translating it:
grep -l "begin{thebibliography}" "<temp_dir>"/chunk*.md
Tell every sub-agent to translate the headings. Left to itself a sub-agent
will often leave # Introduction as it found it. That is not a cosmetic
mismatch: number_sections appends the original title only when it differs
from the translation, so an untranslated heading silently loses its bilingual
annotation while every check still passes.
Give each sub-agent its own scratch filenames. Agents running in parallel share a scratchpad directory; two that pick the same temporary filename will overwrite each other mid-run.
The output file is output_ prefixed to the source filename: chunk0001.md → output_chunk0001.md.
Translate the file
<temp_dir>/chunk<NNNN>.mdto {TARGET_LANGUAGE} and write the result to<temp_dir>/output_chunk<NNNN>.md. Follow the translation rules below. Output only the translated content, no commentary.
Each sub-agent receives:
- The single chunk file it is responsible for
- The temp directory path
- The target language
- The translation prompt (see below)
- A per-chunk term table (see "Term table assembly" below)
- Read-only neighboring chunk excerpts (see "Neighbor context assembly" below)
- Any custom instructions
Term table assembly: before spawning a sub-agent, run:
python {baseDir}/scripts/glossary.py print-terms-for-chunk "<temp_dir>" "chunk<NNNN>.md"
Capture stdout. The CLI emits a 3-column markdown table (原文 | 别名 | 译文) of every term that either appears in this chunk (by source OR any alias) OR is in the top-N most-frequent terms book-wide. Inject the table as {TERM_TABLE} in rule #13 of the translation prompt. If stdout is empty (no glossary, or no relevant terms), omit rule #13 from this chunk's prompt entirely: do not leave a dangling {TERM_TABLE} placeholder.
Meta schema assembly: before spawning a sub-agent, run:
python {baseDir}/scripts/meta.py prompt-block
Capture stdout and paste it into the prompt verbatim, the same way you paste
the term table and the neighbour context. It is generated from the field
tuples validate_meta() checks against, so a prompt that describes a shape
the validator rejects is not expressible.
Do not paraphrase it or hand over only the top-level array names. On one
measured run, ten of thirteen sub-agents wrote {"name": …, "type": …} where
the schema wants {"source": …, "target_proposal": …}, and prepare-merge
quarantined every one of those files, the observations were made and then
thrown away, which looks exactly like sub-agents that observed nothing.
Neighbor context assembly: before spawning a sub-agent, run:
python {baseDir}/scripts/chunk_context.py "<temp_dir>" "chunk<NNNN>.md"
Capture stdout. The CLI emits prompt-ready read-only excerpts: the last ~300
characters of the previous chunk and the first ~300 characters of the next
chunk when those files exist. Inject this block as {NEIGHBOR_CONTEXT}. If
stdout is empty, omit the neighbor-context block entirely. The sub-agent must
not translate neighboring excerpts or copy them into the output; they are only
for pronoun, gender, and entity-resolution context.
Each sub-agent's task:
- Read the source chunk file (e.g.
chunk0001.md) - Translate the content following the translation rules below
- Write the translated content to
output_chunk0001.md - Write observations to
output_chunk0001.meta.jsonmatching the schema below. Non-blocking: leave fields empty if unsure; do not invent entities. Always emit the file (even if all arrays are empty), because its presence + content hash is how the main agent tracks whether feedback was already merged.
Sub-agent meta schema (output_chunk<NNNN>.meta.json):
{
"schema_version": 1,
"new_entities": [
{"source": "Taig", "target_proposal": "泰格", "category": "person",
"evidence": "<≤200-char quote from the chunk>"}
],
"alias_hypotheses": [
{"variant": "Taig", "may_be_alias_of_source": "Tai",
"evidence": "<≤200-char quote>"}
],
"attribute_hypotheses": [
{"entity_source": "Tai", "attribute": "gender", "value": "male",
"confidence": "high", "evidence": "<≤200-char quote>"}
],
"used_term_sources": ["Tai", "Manhattan"],
"conflicts": [
{"entity_source": "Tai", "field": "target", "injected": "泰",
"observed_better": "太一", "evidence": "<≤200-char quote>"}
]
}
**Pass this schema to the sub-agent WITH ITS EXAMPLES FILLED IN. Reducing it
to {"new_entities": [], ...} to save prompt space looks harmless and is not:
an empty container teaches nothing about what goes inside it, so every agent
invents a shape and they invent different ones. Four of five chunks in one run
were quarantined that way, three omitting source, one putting objects in
used_term_sources, which takes plain strings. See R3.
Do NOT include a chunk_id field**: chunk identity is derived from the filename. Putting it in the payload creates a hallucination hole and validation will reject the file.
The meta file is read by the main agent later and merged into glossary.json (see merge_meta.py). Sub-agents should fill the schema honestly: cite real quotes from the chunk, never invent entities to "look productive". An empty meta is a perfectly valid output.
Every evidence string is checked against the source chunk, and it is the
one field that can be. verify_chunk's meta_evidence looks for it in the
chunk after collapsing runs of whitespace and folding the curly quotes and
dashes pandoc's reader introduced (K125), everything else has to match.
Copy the span; do not retype it from memory and do not tidy it. If you
cannot copy one exactly, leave evidence empty and the entry stands or falls
on its own: an empty field costs nothing, and an invented quote is merged into
the glossary and from there into every later chunk.
Three specific ways a copied span still fails. These are every failure the corpus actually holds, four quotes, all TinyVLA, and none of them a remembered sentence (K129):
- A placeholder inside the span is part of the span.
⟦C0021⟧,⟦M0042⟧and the rest sit in the chunk text. Quoting around one, "…Large Language Models (LLMs) for scene descriptions…" where the chunk says "…(LLMs) ⟦C0021⟧ for scene descriptions…", failed at 52 characters of 127. Keep it, or end the quote before it. - So is a citation bracket.
[@doumas2022theory; @toyer2020magical; …]reads like apparatus and is text. Dropping it failed at 23 of 111. - A backslash in JSON has to be escaped. Writing
$\rightarrow$into the meta produces\r, a carriage return, and the quote decodes as$ ightarrow$: that is two of the four, the same span twice. Write$\\rightarrow$, or choose a span with no LaTeX in it.
And one thing that is NOT a failure, because two agents on the same paper each
worked around it: a chunk may contain non-breaking spaces (Shor's chunk0013 has
eighteen). They look like ordinary spaces and a hand-typed copy will not match
them, but meta_evidence collapses all whitespace before comparing, so a
quote that differs only there passes. Copy the span and leave it alone; there
is no need to hunt for a passage without one.
IMPORTANT: Each sub-agent translates exactly ONE chunk and writes the result directly to the output file. No START/END markers needed.
A sub-agent reporting completion tells you nothing. Read the file it wrote. Step 4.4 is the gate, and it is not optional: it exists because every failure mode it checks for produces the same report from the agent as success does.
Write the output file with a file-writing tool, never through the shell. A
shell heredoc, echo or printf collapses \\ to \, which silently strips
every row separator out of a LaTeX tabular and every escaped character out of
the prose. The text still looks complete, so nothing downstream notices. This
has already corrupted a real run. merge_and_build.py now compares the LaTeX
skeleton of each translated chunk against its source and fails the merge naming
the chunk, but not writing the corruption in the first place is cheaper than
diagnosing it.
Translation Prompt for Sub-Agents
Include this translation prompt in each sub-agent's instructions (replace {TARGET_LANGUAGE} with the actual language name, e.g. "Chinese"):
请翻译markdown文件为 {TARGET_LANGUAGE}. IMPORTANT REQUIREMENTS:
- 严格保持 Markdown 格式不变,包括标题、链接、图片引用等
- 仅翻译文字内容,保留所有 Markdown 语法和文件名
- 删除空链接、不必要的字符和如: 行末的''。页码已由 convert.py 上游处理,不要再删除独立的数字行(可能是年份 1984、章节编号、引用编号等正文内容)。
数字一律照抄原文的写法,包括小数点和千位分隔符:
58.9不写成58,9,60,000不写成60.000或60 000,%前不加空格。这条优先于目标语言的排版惯例,理由很具体:表格是受保护的 LaTeX,逐字复制自原论文,verify_tables拒绝改动其中任何数字,所以表格无法跟着本地化;正文若本地化,同一页上正文与表格就会互相矛盾,而德语的60.000紧挨着表格里的58.9会被读成小数。只翻译数字周围的词,不要翻译数字本身。整数可以写成词(10 models→十个模型),有分隔符的数字不可以。verify_chunk的numerals检查会拦下这类改写。 - 保证格式和语义准确翻译内容自然流畅
- 只输出翻译后的正文内容,不要有任何说明、提示、注释或对话内容。
- 表达清晰简洁,不要使用复杂的句式。请严格按顺序翻译,不要跳过任何内容。
- 必须保留所有图片引用,包括:
所有
格式的图片引用必须完整保留图片文件名和路径不要修改(如
media/image-001.png)图片alt文本可以翻译,但必须保留图片引用结构
不要删除、过滤或忽略任何图片相关内容
图片引用示例:
->原始 HTML 标签(如
<img alt="..." />、<a title="...">)必须保持合法:翻译alt、title等属性值内部文本时,下列字符会破坏 HTML 结构,必须替换为安全形式(仅适用于原始 HTML 标签的属性值内部;普通 Markdown 正文、代码块、URL 不要主动转义):字符 在属性值内的危险 替换为 "闭合 attr="..."目标语言合适的弯引号(如中文 “”)或"'闭合 attr='...'目标语言合适的弯引号(如中文 ‘’)或'<被解析为新标签 <>被解析为标签结束 >&被解析为实体起始(除非已是 &xxx;)&不要修改
src、href等结构性属性的值,只翻译可见文本属性(alt、title)。- 错误示例:
alt="爱丽丝拿着标着"喝我"的瓶子"← 内层英文"把外层 alt 撑断了 - 正确示例:
alt="爱丽丝拿着标着“喝我”的瓶子"或alt="爱丽丝拿着标着"喝我"的瓶子"
- 错误示例:
- 智能识别和处理多级标题,按照以下规则添加markdown标记:
- 主标题(书名、章节名等)使用 # 标记
- 一级标题(大节标题)使用 ## 标记
- 二级标题(小节标题)使用 ### 标记
- 三级标题(子标题)使用 #### 标记
- 四级及以下标题使用 ##### 标记
- 标题识别规则:
- 独立成行的较短文本(通常少于50字符)
- 具有总结性或概括性的语句
- 在文档结构中起到分隔和组织作用的文本
- 字体大小明显不同或有特殊格式的文本
- 数字编号开头的章节文本(如 "1.1 概述"、"第三章"等)
- 标题层级判断:
- 根据上下文和内容重要性判断标题层级
- 章节类标题通常为高层级(# 或 ##)
- 小节、子节标题依次降级(### #### #####)
- 保持同一文档内标题层级的一致性
- 注意事项:
- 不要过度添加标题标记,只对真正的标题文本添加
- 正文段落不要添加标题标记
- 如果原文已有markdown标题标记,保持其层级结构
- {CUSTOM_INSTRUCTIONS if provided}
- 术语一致性:以下术语必须严格使用指定译法,不要自行变换。表格中"原文"列或"别名"列任一形式出现在正文中时,都必须翻译为"译文"列对应的形式。
{TERM_TABLE}
**术语首次出现时,在译文后用圆括号附上原文。** 例如 `이상치(outlier)`、
`재구성 오차(reconstruction error)`、`스케일 인자(scale factor)`。规则:
- 只标注**本 chunk 内第一次**出现的位置,之后直接使用译文。你看不到别的
chunk,所以"第一次"以本 chunk 为准;全书范围的去重由主控在合并时完成。
- "译文"列本身就是原文(照搬英文)的术语不需要标注。
- 单位和已经通行的缩写(GB、FP16、PTQ)不是术语标注,保持原样。
- 标题、表格单元格、图注中不标注,只在正文行文中标注。
术语表告诉你该用哪个译法,读者却还需要能把译名对回原文献。**这一条不是可选
的**:术语表越完整,越容易让人觉得"译名已经定了,不必再标原文",而结果是整本
书的原文标注悄悄消失,任何计数都不会变化。
公式与引用占位符(最高优先级):正文中形如
⟦M0042⟧、⟦C0007⟧、⟦T0003⟧的记号是公式、引用和原始 LaTeX 表格的占位符,翻译完成后会被自动还原为原始内容。- 必须逐字符原样复制:不得翻译、不得改写、不得增删字符、不得添加或删除空格、不得跨行拆开、不得改变大小写或数字。
- 源文中出现的每一个占位符,都必须在译文中出现,且有且只有一次。
- 不得自行发明新的占位符,也不得把某个占位符换成另一个编号。
- 占位符周围的中文/目标语言标点可以正常调整,但占位符本身是不可分割的整体。
这些记号代表读者最终会看到的数学公式。丢掉一个占位符,就等于让一个公式从整本书里消失,而且在校对时完全看不出来 —— 校验环节会因此直接失败并要求重译该 chunk。
译文要读起来像目标语言原创,而不是译文。 译者读到的每一句都要问:以目标语言写作的研究者会这样写吗?常见的翻译腔(以韩语为例,其他语言同理):
- 英文的同位语逗号被直译成"따라서/그러므로",其实多半该用"즉"或重组句子;
- 形容词逐字对译:proprietary → "비공개이며"(生硬)而非"독점적이어서";
- 名词化堆叠"~의 ~의 ~을 위한",应拆成动词;
- 过度被动"~에 의해 ~되어진다";
- 英文代词照搬(it/this → "그것/이것"),韩语通常重复名词或直接省略;
- 一句超过 ~90 字而英文本可断句的长句。
示例(韩语):
- 差:프런티어 MoE LLM의 경우 원래의 학습 데이터, 따라서 참된 학습 분포는 비공개이며 접근할 수 없다.
- 好:프런티어 MoE LLM의 경우, 원래 학습 데이터, 즉 실제 학습 분포는 독점적이어서 접근할 수 없다.
重写只针对读感,不得增删信息、不得添加原文没有的限定语。
术语首次出现时按需附原文。 若某个术语的译法本身无法让读者反推回英文原词,则在首次出现处写作
译文(English)(括号前不加空格),此后只用译文。示例(韩语):
두꺼운 꼬리单独出现只是普通词组,应写두꺼운 꼬리(heavy-tailed)。反例:不要给读者一眼就懂的词加注(
비트 폭、퍼플렉시티、가중치、양자화)——那只是噪音。判断标准是"目标语言读者能否立刻映射回英文术语",而不是"这个词是否是术语"。原文本身有问题时,忠实翻译并加译注,不要悄悄修好。 论文里确实会出现残缺的句子。CafeQ 已出版的 PDF 就写着 "...particularly in the attention modules. which in contrast, aims to quantize an already-trained model"——关系代词没有先行词,显然是删句子时留下的尾巴。
悄悄补全会让译本说出论文没说的话;原样照译则会让读者以为是译者漏了内容。两者都不可取。正确做法是照译,并在紧随其后加一条简短译注,说明问题出在原文:
이와 대조적으로, 이는 … 목표로 한다. *[역주: 원문이 이 자리에서 "which in contrast, aims to…"로 시작한다. 선행사가 되는 문장이 원문에 없어, 무엇과 대조되는지는 원 논문에서도 드러나지 않는다.]*译注用
*[역주: …]*(斜体方括号)——纯 markdown,四种输出格式都能保留,且与正文明显区分。只在读者会因此停下来怀疑译文时才加;不要为了展示细心而滥用。dry_run.py在翻译开始前就会报出这类句子,正是为了让你有机会这样处理,而不是事后被追问。
邻居上下文(只读,不要翻译,不要写入输出,只用于判断代词、性别、别名和跨 chunk 指代;为空则省略):
{NEIGHBOR_CONTEXT}
markdown文件正文:
4.4. Check the Work Before You Record It
A sub-agent's report is not evidence. The file it wrote is.
An agent that skipped a paragraph, pasted its neighbour's context, answered in English, or invented the quote it offered as evidence reports exactly what a correct one reports: it finishes. Every one of those has to be found by reading the artefact, and none of them is found by asking again.
Run this on every chunk in the batch, before recording anything:
python {baseDir}/scripts/verify_chunk.py "<temp_dir>" --lang <target_lang> --strict chunk0001 chunk0002 ...
It compares the output against the source chunk, the glossary the agent was
handed, and the chunk it was told to quote from, never against the agent's
account of its own work. Add --json to consume the report programmatically,
--quiet to print only failures.
What it fails on, all of it proven by fault injection rather than by reasoning:
| check | what it caught |
|---|---|
placeholders |
a formula dropped, duplicated, or invented (⟦M0042⟧) |
images |
an image reference gone |
untranslated |
the output is the source, byte for byte |
target_language |
the chunk came back in the source language |
untranslated_block |
one paragraph never translated |
commentary |
"Here is the translation:", a closing note, the whole file fenced |
fences |
an unclosed code fence |
structure |
table rows lost |
length |
a translation a fifth the length of its source |
neighbor_leak |
the read-only neighbour excerpt pasted into the output |
glossary |
a term left in the source language that the glossary translates |
meta_evidence |
a quote offered as evidence that is not in the chunk |
Warnings (heading, list item, extra images, missing meta) do not fail a
chunk; read them and decide.
When a chunk fails, re-translate it, do not patch it yourself and do not argue with the agent. Re-dispatch that one chunk with the findings quoted verbatim in the prompt, because the finding names the defect and a bare "try again" does not:
Your previous translation of
chunk0007.mdwas rejected by an automatic check. Fix exactly these and rewrite the whole file:
- [placeholders] 1 placeholder dropped by the translator: ⟦M0031⟧
- [untranslated_block] 22 consecutive source-language words with no ko: "We evaluate the proposed method on a range of standard benchmarks"
Then run the verifier again on that chunk. Two rejected attempts is the limit:
after the second, stop and report the chunk to the user with the findings
rather than lowering the bar. Record a chunk in run_state.json only once it
passes.
The false-positive rate is not a matter of opinion: the checks are calibrated against three fully reviewed books, 40 chunks, at zero failure
…(truncated)