Generated by scripts/i18n_eval.py. Raw results: eval.json.
APEX now generates its clinical explanation in Spanish as well as English:
analyze_signal(signal, 100, backend="template", lang="es")The interesting part is not the translation. It is that adding a second language nearly removed the safety guarantee for everyone who reads it, in a way that no amount of reviewing the Spanish text would have revealed.
Phase 7 built a consistency gate: APEX may only assert findings the detector actually surfaced, and any report claiming more is flagged before a clinician sees it. It is the mechanism that makes the generated text trustworthy, and Phase 21 showed it catching real fabrications when retrieval went wrong.
It works by matching English impression phrases.
Point it at a Spanish report and it matches nothing. The report parses as asserting no findings at all, which is a subset of anything, so it passes — unconditionally, silently, every time. Measured on 400 PTB-XL records before this phase:
| English report | Spanish report | |
|---|---|---|
| Fabricated finding detected | 100% | 0% |
| Any finding recognised at all | 100% | 0% |
A Spanish report inventing bloqueo completo de rama izquierda on a patient whose only detected finding was atrial fibrillation was reported consistent. The identical fabrication in English was caught.
This is what a health-equity failure looks like in a codebase: not a slur, not a missing translation, but a guardrail that quietly does not extend to the second-largest language group in US healthcare. Nobody reading the Spanish output would notice — the text is fluent, the disclaimer is present, the JSON validates. Only the gate is missing, and only for those patients.
Spanish vocabulary (src/i18n/vocab_es.py) — all 71 SCP statements, hand-authored, with
the finding sentence and the impression term separated exactly as in English. Not
machine-translated: clinical Spanish is not English with Spanish words. Bundle branch
block is bloqueo de rama, not bloqueo del haz; a fascicular block is conventionally
hemibloqueo; atrial enlargement is crecimiento auricular, not the literal
agrandamiento.
One renderer, two languages (src/i18n/render.py) — structure, ordering, and the
merge logic for localized findings are shared; only a phrase bank changes. The alternative,
a second renderer for Spanish, starts identical and drifts. A test asserts English output is
byte-identical to the Phase-6 templater across 200 random cases, so divergence is a
build failure rather than a discovery months later.
The phrase bank carries grammar, not just words:
| English | Spanish | |
|---|---|---|
| Lead clause | "in the inferior leads" | "en las derivaciones inferiores" (feminine plural agreement) |
| Serial list | "V1, V2, and V3" | "V1, V2 y V3" (no serial comma) |
| Rate | "72 bpm" | "72 lpm" |
| Headers | Findings / Impression | Hallazgos / Impresión |
A language-aware gate (src/i18n/parse.py) — the fix. Two properties the English matcher
did not need:
- Accent tolerance on input. The vocabulary spells fibrilación correctly; the matcher folds accents when reading, so a model or clinician typing fibrilacion is still understood. A gate a missing accent can switch off is not a gate.
- Longest match wins. extrasístoles is a substring of extrasístoles auriculares, exactly as "premature complexes" sits inside "atrial premature complexes". Without this, a correct report is charged with a hallucination that never happened, and a gate that cries wolf gets switched off.
400 PTB-XL records, rendered in both languages, gate audited on each.
| Measure | English | Spanish |
|---|---|---|
| Well-formed sections | 100% | 100% |
| Language correctly detected | 100% | 100% |
| Findings round-trip exactly | 100% | 100% |
| Fabricated finding caught | 100% | 100% |
| (before this phase) | 100% | 0% |
Round-trip is measured excluding normal studies. 157 of the 400 records are normal ECGs
whose impression deliberately collapses to "Normal ECG" (the Phase-6 NORM_COMPATIBLE
rule), which suppresses the rhythm term — in both languages, by design. Counting those as
failures would report 64.8% for both languages and blame the translation for a rule it
never touched.
Two further parity invariants, both enforced by tests rather than asserted in prose:
- English rendering is byte-identical to the Phase-6 templater (200/200 random cases).
- The language-aware parser agrees with the Phase-6 parser on English text (300/300).
The first version of the Spanish matcher searched the whole report rather than the
Impression section. Several morphological sentences are word-for-word identical to another
code's impression term — ISCAN's finding is "T-wave inversion", which is exactly INVT's
impression; INJAS's is "ST-segment depression", exactly STD_'s — so every ischemia
report was charged with additionally asserting INVT and STD_. Round-trip accuracy sat at
84%, and the Spanish gate would have been noisier than the English one: the same
second-class treatment, arriving through the opposite door. Restricting to the Impression
section, as the English implementation already did, took both languages to 100%.
Each Spanish term was checked against 26 Spanish-language cardiology articles (264,270 characters, es.wikipedia, CC BY-SA 4.0, quoted for validation only — same licensing footing as Phase 21's corpus).
34 of 67 terms appear in the reference prose. Confirmation is evidence a term is conventional Spanish usage. Non-confirmation is not evidence of error, and the report says so rather than quietly counting it as a pass: the corpus is 26 encyclopedia articles, and roughly half the unconfirmed set are ECG report-writing conventions that encyclopedic prose has no reason to contain — "alteración no diagnóstica de la onda T", "criterios de voltaje para hipertrofia ventricular izquierda", "lesión subendocárdica anteroseptal". The rest are absent because the corpus lacks the relevant article.
The unconfirmed list is a clinician review list, and it is the honest deliverable here. A native-speaking cardiologist reviewing 33 terms is a bounded, actionable task; "we validated the Spanish" would have been a stronger-sounding and less useful claim.
Getting even this far required fixing the matcher twice. Report vocabulary is plural
("extrasístoles auriculares") while reference prose defines the singular ("extrasístole
auricular"), and Spanish pluralizes with +s after a vowel and +es after a consonant —
mixed within a single phrase. Stripping one fixed suffix produced "extrasistol auriculare"
and matched nothing, reporting a perfectly standard term as unconfirmed. Both attempts are
recorded in glossary.py because the failure mode — a review list inflated by matching
bugs, hiding the real problems — is the one that matters.
- Two languages. English and Spanish. The architecture is a phrase bank plus a vocabulary, so a third is additive, but no claim is made about languages not implemented.
- The template backend is the validated path. The LLM backends accept
langand receive a Spanish instruction appended to the (untranslated) constraint block, but their Spanish output has not been measured — Phase 21 showed the generator's faithfulness is fragile, and there is no reason to assume it degrades identically in a second language. The deterministic renderer is consistency-clean by construction; the LLM path is not, in either language. - No native-speaker review yet. Terminology is checked mechanically against reference prose. That is a floor, not a substitute for a Spanish-speaking cardiologist reading the output.
- No dialect variation. Cardiology Spanish is fairly standardized, but usage does differ between Spain and Latin America and this vocabulary does not model that.
- Patient-facing language is out of scope. This is clinician-register Spanish, matching the English. A patient-readable explanation is a different task with different risks.