Skip to content

Translate a note's transcript into a chosen language (Claude provider, stored beside the transcript) - #48

Merged
Chaddacus merged 5 commits into
devfrom
codex/translation
Sep 1, 2026
Merged

Chaddacus merged 5 commits into
devfrom
codex/translation

Conversation

@Chaddacus

@Chaddacus Chaddacus commented Sep 1, 2026

Copy link
Copy Markdown
Owner

What

Slice 2 of the multilingual work. The transcript stays in the spoken language (ADR-004); this adds Translate to as a separate stored artifact.

  • backend/apps/translations/: NoteTranslation (one row per note and target language, unique), TranslationProvider boundary with a Claude implementation (anthropic 1.3.0, messages.parse with a pydantic output_format), a Celery task that validates the provider output against the input units (same ids, order, count, no empty text) before storing full text plus per-segment translations, and GET|POST /api/notes/<id>/translations/.
  • Frontend: TranslationPanel on the note page with disabled / empty / pending / failed / completed states, translated speaker turns aligned by segment id. Shared transcript helpers moved to utils/transcript.ts.
  • Governance (Standard 9): docs/ai/translation-capability-contract.md, docs/ai/translation-eval-suite.md, docs/adr/005-translation-provider.md, eval suite backend/evals/translation (marker ai_eval, fails closed without ANTHROPIC_API_KEY), .claude/verification.json.
  • Compose files pass ANTHROPIC_API_KEY and CLIO_TRANSLATION_MODEL to backend and celery. Without the key the endpoint answers 503 and the UI shows "Translation is not enabled on this server."

Why a second key

Deepgram transcribes only. Translation is an LLM call. See ADR-005.

Status: capability NOT READY

No Anthropic key exists in the vault yet, so the eval suite and the live request shape are unverified. Everything else is proven with a faked provider. Do not enable in production until pytest evals/translation -m ai_eval passes with a real key.

Evidence

  • backend: pytest 167 passed (28 new in tests/test_translations.py); ruff clean; mypy --python-version 3.12 apps/translations clean.
  • frontend: tsc --noEmit clean; eslint clean on touched files; Playwright e2e/tests/translation.spec.js 3 mocked-API state tests pass (screenshot inspected), live test skipped without keys.
  • Rest of the e2e suite: green except the pre-existing record-page heading assertion, fixed in ci: run the pipeline on 3.12, fix the gates it was hiding, and cover dev #47.

Depends on #47 for green CI (anthropic needs Python 3.10+; CI moves to 3.12 there).

Review and CI (updated)

  • Fresh-context adversarial review: 2 majors + 4 minors found and fixed (f3d4557); re-review found 1 major (in-flight task re-inserting an invalidated row) + 2 minors, fixed (conditional UPDATE writes, no traceback logging, schema derived from the pydantic model). Final verification pending.
  • dev (with ci: run the pipeline on 3.12, fix the gates it was hiding, and cover dev #47) merged in at 18ae4cc. CI on that head: all six jobs green, including e2e.
  • Local gates on the merged tree: ruff clean, mypy 56 files clean, backend 190 passed, tsc clean, eslint clean over src, Playwright translation + navigation 11 passed.

…der boundary

New apps.translations module: NoteTranslation rows per (note, target language),
a TranslationProvider protocol with a Claude implementation using structured
output, a Celery task that validates the provider output against the unit
contract before storing text and per-segment translations, and a REST adapter
at /api/notes/<id>/translations/ (GET reports whether the server can translate;
POST dispatches or returns the stored row). The transcript is never modified.

Frontend: TranslationPanel on the note page with disabled, empty, pending,
failed, and completed states, including translated speaker turns aligned by
segment id; shared transcript helpers moved to utils/transcript.ts.

Governance: capability contract, deterministic eval suite (marker ai_eval,
fails closed without ANTHROPIC_API_KEY), ADR-005, .claude/verification.json,
README env and testing notes. Compose files pass the new env through.
…the provider boundary

- Re-transcription and a manual transcript edit now call the translations
  module's public invalidate_translations_for_note; stale rows were shown under
  a fresh header and their per-turn view broke on the new segment ids.
- The Claude provider uses messages.create with a JSON-schema output_config and
  validates the text itself, so truncation (stop_reason max_tokens) and
  malformed output become controlled failures instead of a pydantic exception
  escaping the boundary; failure reasons never carry model output.
- Retry policy: transient provider failures (rate limit, 5xx, connection) are
  retried with the row left pending; refusals, truncation, schema and contract
  violations, and configuration errors fail fast. Unexpected exceptions are
  logged by type only.
- Compose passes CLIO_TRANSLATION_EFFORT and CLIO_TRANSLATION_TIMEOUT_SECONDS.
- TranslationPanel renders a load-error state with Retry instead of vanishing.
- Tests: invalidation on retranscribe/edit, segments untouched, retry policy,
  provider boundary with a faked SDK client; Playwright load-error case.
- Contract and ADR updated with invalidation and retry policy.
…schema definition

- Task writes are conditional UPDATEs on the row pk; zero rows means the note
  was re-transcribed or edited while the provider call was in flight and the
  result (or failure) is discarded. A plain save() re-inserted the stale row.
- OUTPUT_SCHEMA is derived from _TranslationOut (refs inlined, titles dropped,
  objects closed) and a test binds it to the SDK's own transform of the model.
- Unexpected task exceptions are logged by type without a traceback, and the
  test observes the real log records through caplog's handler.
QuerySet.update() bypasses auto_now, so the row's updated_at froze at creation;
the panel orders translations by it and it is provenance for the artifact.
@Chaddacus
Chaddacus merged commit 897f8c7 into dev Sep 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant