Markdown version: https://ai.hiina.space/reports/two-stage-user-styled-translation/07-a-concrete-record-design-and-where-the.md · index

Index of everything on this site: https://ai.hiina.space/index.md

Two-stage styled translation exists only in pieces: 7. A concrete record design, and where the cache sits

7. A concrete record design, and where the cache sits

The intermediate record: grounded fields plus a few of mine

The sketch below shows one line's record. The Japanese is invented for illustration. A record is written once per line by the frontier pass with full-script context, validated against a JSON schema, and stored.

{
  "line_id": "ch03_0412",
  "source_hash": "sha256:…",
  "prev_line_id": "ch03_0411",
  "speaker": "yui", "addressee": "tanaka", "listeners": ["tanaka"],
  "source_ja": "先輩、まだ起きてたんですか?",
  "literal_en": "Senpai, you were still awake?",
  "neutral_en": "You're still up, {addr0}?",
  "address_terms": [{"slot": "addr0", "span_ja": "先輩", "refers": "tanaka",
     "kind": "title_as_name", "signals": "junior to senior, warm",
     "renderings": {"keep": "senpai", "name_suffix": "Tanaka-senpai", "drop": "Tanaka"}}],
  "register": {"politeness": "teineigo", "markers": ["んですか"], "shift_from_prev": "none"},
  "intent": "express surprise; mild concern",
  "subtext": "she stayed up hoping to catch him alone",
  "delivery": "surprised, soft",
  "relationship": {"status": "addressee senior", "intimacy": "close"},
  "voice_markers": {"in_source": ["polite even when flustered"], "localization_added": []},
  "referents": [],
  "wordplay": null,
  "harshness": "none",
  "scene": "late night, club room, after the festival",
  "lemmas": [{"surface": "起きて", "lemma": "起きる", "jmdict": "<entry id>"}],
  "clauses": [{"ja": "まだ起きてたんですか", "en": "you're still up"}],
  "spoiler_level": 0,
  "flags": [], "confidence": 0.9,
  "provenance": {"writer_model": "…", "record_version": 3}
}
Field Grounded in Status
line_id, prev_line_id Yarn #line: IDs (Yarn); Ren'Py translate IDs (Ren'Py); GalTransl index Sourced; prev_line_id is mine, for gettext-style disambiguation
source_hash GalTransl retranslates on changed post_jp (README_EN) Sourced pattern
speaker, addressee, listeners, delivery Alconost speaker, listener, emotion (Alconost); VNTL [Speaker]: tags; GalTransl name Sourced
character data (stored once, referenced by ID) VNTL name, gender, aliases (VNTL card); GalTransl dictionary notes; VNDB glossary info with spoiler flag (VNDB-Glossary-Builder) Sourced
relationship CiF relationships and statuses (McCoy et al.); Alconost relationship notes Sourced
intent, subtext CiF primary intent; Valve concepts; Versu surface and subtext quips (Versu) Sourced
referents CiF social facts database; pronoun failures with implicit context (Mohammed and Niculae) Sourced need, my field
register CoCoA-MT formal/informal/respectful labels (CoCoA-MT) My field, sourced vocabulary
address_terms with slot renderings, neutral_en as a template J-Novel Club [Mr. Joey|Jonouchi-san] proposal; MF2 and Fluent selectors; Committee of Zero toggle My synthesis
voice_markers (source vs localisation) Yakuwarigo thesis (Leiden); Dragon Quest IV (LoL) My synthesis
wordplay (mechanism, literal gloss, candidates) JOKER 2026 results (JOKER) My synthesis
harshness Jun et al. disposition result; Llama2 refusals in WMT 2023 My synthesis
scene (short, local) Rolling summary beat longer context but not page-level (COLING 2025) My field, sourced limit
flags, confidence, provenance GalTransl problem, trans_by, proofread_comment Sourced pattern
literal_en, lemmas, clauses Learner-overlay design (companion report), hint rungs 4-5 and clause-level hybrid mode My synthesis

Per-game character records belong in a separate table referenced by ID, as GalTransl and VNTL keep them, so a nickname fix propagates without rewriting lines. Keep the scene field short: the manga results say beyond one page of context, more hurts, and the record already carries the discourse facts that matter.

The user style profile is a small canonical object, for example {"honorifics": "keep|name_suffix|drop", "name_order": "western|japanese", "puns": "literal_with_note|localize", "register": "match|flatten", "voice": "none|pirate|…", "free_text": "…"}. Its hash, together with the renderer model ID and prompt version, is the style key. Named knobs follow PERSONAGE's evidence; the free-text escape hatch is what makes "pirate" possible.

Render after the record lookup, cache the render, and do closed-class flags in code

The render step sits between two caches. The record store is keyed by line_id with source_hash for invalidation, filled offline from an extracted script, and versioned. The render cache is keyed by (line_id, record_version, style_hash, renderer_id) and filled ahead of the reader: when line N displays, render N+1 to N+k in the background. Live hooks deliver text, not IDs, so the hook side needs a lookup from normalised source text plus previous-line hash to line_id. That is the gettext msgctxt problem, and GalTransl++'s surrounding-sentence cache lookups are the precedent (GalTranslPP). For a hooked game whose script cannot be extracted, the "offline" pass degrades to first-encounter writing with only past context, which is the single-stage case again; extraction tools such as GalTransl's extract-translate-inject workflow are what make the split worth it.

Within the render step, apply the deterministic layer first. Honorific keep, suffix or drop, and name order are slot fills on neutral_en from address_terms, done in code, with no model call. The IWSLT result (rule-based 95.5/100 against learned 26.2% informal) and the Committee of Zero toggle both say closed-class transforms belong there. If the style profile asks for nothing open-ended, the line is done. Only when voice, register: flatten, pun localisation or free text is set does the local model run, with the filled neutral line as a starting point, the record fields as constraints, and the style profile at the end of the prompt. A post-check verifies the hard facts (names from the glossary, honorific presence or absence by regex, length ratio, leftover kana, borrowing GalTransl's problem analyser) and falls back to the deterministic render on failure, following Valve's specificity-with-fallback rule.

Keep the Japanese in the renderer's prompt

Four lines of evidence say the source should still reach the renderer. Meaning pivots lose information: AMR generation managed about 2 of 3 on semantic accuracy and only matched a cascade (Fan and Gardent), and GF's interlingua failed on idioms and disambiguation (GF). Every successful LLM-era intermediate method (MAPS, TransAgents, TEaR) keeps the source alongside its notes. The record is written by a model and is unverifiable prose in places; the source is the only ground truth the renderer can check it against. And the source line costs 20-60 tokens, trivial at thousands of tokens per second of prefill. The counter-argument is real: small models attend poorly to context (Mohammed and Niculae), and a renderer handed both might re-translate from the Japanese and ignore the record. That is testable with counterfactual records (experiment 2), and the design should treat the record as authoritative for names, address terms and register, and the source as a check on meaning. This search found no study that ran this exact comparison.


Text licensed CC BY 4.0 (attribution: Hiina, ai.hiina.space). Source: https://github.com/hiinaspace/ai-hiina-space