From 506cf9e7ff474c78bbb88242d617e2eaee0e826f Mon Sep 17 00:00:00 2001 From: Creatman Date: Wed, 3 Jun 2026 09:02:27 -0400 Subject: [PATCH 01/17] =?UTF-8?q?docs:=20Phase=201=20design=20=E2=80=94=20?= =?UTF-8?q?segment-aligned=20model=20+=20side-by-side=20viewer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch translate+align (few calls, not per-sentence), precomputed word-align for <1s click, HTML side-by-side viewer with block/sentence/word sync highlight, per-document budget tracking (admin), iOS download fix. Includes guardrails audit with 8 🔴 acceptance criteria (XSS-safe render, /api/result hardening, align validation, schemaVersion cache, segment cap, admin-gated usage). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...6-06-03-hebrew-translator-phase1-design.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/plans/2026-06-03-hebrew-translator-phase1-design.md diff --git a/docs/plans/2026-06-03-hebrew-translator-phase1-design.md b/docs/plans/2026-06-03-hebrew-translator-phase1-design.md new file mode 100644 index 0000000..f9e0d21 --- /dev/null +++ b/docs/plans/2026-06-03-hebrew-translator-phase1-design.md @@ -0,0 +1,87 @@ +# Hebrew Translator — Phase 1 Design: segment-aligned model + side-by-side viewer + +**Date:** 2026-06-03 +**Status:** Approved (design-lock) +**Builds on:** `2026-06-01-hebrew-translator-production-design.md` (Phase 0 live). + +## Scope (locked) +Phase 1 delivers the **alignment foundation + a synchronized side-by-side viewer**, plus per-document budget tracking and the iOS download fix. Pixel-faithful file output (PDF overlay, DOCX in-place) is the **next** phase. + +| Decision | Value | +|----------|-------| +| Formats (fidelity files) | both PDF+DOCX — but **next phase**; Phase 1 = viewer | +| Highlight granularity | all three: block → sentence → word | +| Word alignment source | LLM, **precomputed in the batch pass** (not lazy) — required by <1s click latency | +| Viewer rendering | HTML panes (source RTL / target LTR), not pixel-faithful | +| Budget tracking | per-document tokens + cost + actual model, admin-gated | + +## 1. Foundation — `TranslationDocument` (canonical intermediate) +Pipeline becomes extract → structure → batch-translate(+align) → `TranslationDocument` JSON: +```jsonc +{ schemaVersion: 1, sourceLang, targetLang, + usage: { byModel: { "gemini-2.0-flash": {calls,in,out,total,costUsd} }, totals: {...} }, + blocks: [ { id:"b1", type:"paragraph", sentences:[ + { id:"b1s1", source:"שלום עולם", target:"Hello world", + srcTokens:["שלום","עולם"], tgtTokens:["Hello","world"], + align:[ {src:[0],tgt:[0]}, {src:[1],tgt:[1]} ] } ] } ] } +``` + +**Pipeline:** +1. Extract (pdf-parse / mammoth) → blocks. +2. Split blocks → sentences via `Intl.Segmenter` (sentence granularity); NFC-normalize Hebrew first. +3. Tokenize source per sentence via `Intl.Segmenter` (word granularity) → `srcTokens` (server is the source of truth for tokens). +4. **Batch translate + align**: group sentences into chunks (≈15-20 segments / ~1.5k tokens, respecting block boundaries). One LLM call per chunk returns `[{id, target, align}, …]`. A few calls per document, not per-sentence. +5. Assemble `TranslationDocument`; store by random token (TTL) in the result store. + +**Why precompute align:** <1s click latency forbids per-click LLM round-trips (first click would miss cache). Block/sentence highlight is free (shared ids, 1:1); word-align rides along in the same batch calls. + +**Caching:** per chunk/sentence, key = `model + promptVersion + schemaVersion + langs + hash(text)`. Distinct namespace from Phase-0 string cache (value shape changed → collision = crash). + +## 2. Viewer (frontend — new result screen) +- API: `GET /api/result/:token` → `TranslationDocument` (minus admin-only `usage`). `Cache-Control: private, no-store`. +- Two scrollable panes: **left = source** (`dir="rtl"`, `unicode-bidi: plaintext` per segment), **right = target** (LTR). Each word is a span carrying `data-block / data-sentence / data-token`. +- Interaction (bidirectional, from either pane): + - hover/click a **word** → highlight aligned word(s) in the other pane via `align`; + - click a **sentence** → highlight the paired sentence both sides; + - **block** → highlight the block. +- **Rendered only via React text nodes** — never `dangerouslySetInnerHTML` for document content (XSS). +- Client renders the server's `srcTokens`/`tgtTokens` **as-is** (never re-tokenizes) so align indices line up. +- Keeps a **Download** button. + +## 3. Budget tracking (admin) +- `LiteLLMProvider` returns `usage {model, promptTokens, completionTokens, totalTokens, costUsd}` from the response (`usage` + `x-litellm-response-cost` / `_hidden_params`). Capture the **actual** model (fallback-aware), not the alias. +- Worker aggregates across batch calls → `usage.byModel` + totals, stored with the result. +- Admin: per-document panel (`model · in/out tokens · $cost`) + `GET /api/admin/usage` (recent jobs). Gated by `ADMIN_KEY` env (minimal gate until real auth exists). Usage stripped from the public result payload. + +## 4. iOS download fix +Replace blob + `a.download` with a direct anchor to the attachment URL (server already sends `Content-Disposition: attachment`) — reliable on iOS Safari. + +## 5. Error handling (graceful degradation) +- Malformed batch JSON → per-item parse; bad item degrades to sentence-level (no crash), job continues. +- align out-of-range / not covering tokens → drop word-level for that sentence; sentence/block highlight still works. +- Sentence segmentation failure → whole block as one sentence. +- Keep Phase-0 guardrails (refusal/length-ratio). + +## 6. Testing +Unit: `Intl.Segmenter` sentence/word wrappers, align validator (out-of-range→drop), `TranslationDocument` builder, usage aggregator. Adapter: structured-output mock (incl. malformed item). Integration: fixture → valid `TranslationDocument`. Frontend: clicking a word highlights the paired token; XSS fixture (text with `