translation-agent is a workflow-first translation pipeline built in Python on top of LangGraph. The repository is no longer a bootstrap scaffold: it already contains a runnable dry-run runtime, a real-adapter runtime, operational persistence, replay helpers, memory staging and consolidation, artifact publishing, and a test suite that exercises the end-to-end workflow.
The current pipeline executes these stages:
ingest
-> extract_audio
-> fanout_transcription
-> normalize_transcripts
-> review_transcripts
-> adjudicate_transcript
-> background_memory_pipeline
-> generate_translation_candidates
-> normalize_translations
-> review_translations
-> adjudicate_translation
-> background_memory_pipeline
-> finalize_outputs
The workflow is deterministic-first:
- orchestration, retries, routing, persistence, normalization, and memory staging are implemented as deterministic nodes
- transcript and translation review are generated into a fixed prose contract and parsed back into structured review bundles
- adjudication is deterministic scoring with explicit escalation modes:
automatic_finalize,conflict_investigation,stronger_adjudicator, andhuman_review - translation failures are recoverable: the run can preserve the approved transcript, publish a failure manifest, and stop short of a final translation
The repo supports two execution modes.
| Mode | How it is selected | What it uses | Best for |
|---|---|---|---|
fake |
default | deterministic fake adapters and scenario-driven outputs | local development, docs examples, fast tests |
real |
TA_ADAPTER_MODE=real |
ffmpeg, a selectable subset of AssemblyAI, Speechmatics, and Deepgram, plus a role-based translation runtime that defaults to Gemini via the OpenAI-compatible API |
real provider integration |
Notes:
- fake mode requires no provider credentials
- real mode defaults to all three transcription providers when
TA_TRANSCRIPTION_PROVIDERSis unset - real mode optionally supports a comma-separated transcription-provider subset via
TA_TRANSCRIPTION_PROVIDERS - real mode credential requirements depend on the selected transcription providers plus the selected translation provider
- real mode translation defaults to
TA_TRANSLATION_PROVIDER=geminiwithTA_TRANSLATION_MODEL_ID=gemini-2.5-flash - real mode also exposes a reasoning profile that defaults to
TA_REASONING_PROVIDER=openaiwithTA_REASONING_MODEL_ID=gpt-5.4, but review and adjudication still run deterministically in this pass - real mode is currently gated behind the LangGraph Python 3.14 compatibility check unless
TA_ALLOW_LANGGRAPH_PY314_WARNING=1is set - fake mode still writes the full run record, trace, blob artifacts, scorecards, memory batches, consolidations, and prompt-evolution proposals
uv run translation-agent validate-config --json
uv run translation-agent run-job input.wav --job-id demo --json
uv run translation-agent run-job input.wav --job-id demo-zh --target-language zh --json
uv run translation-agent convert-json-to-srt .translation-agent/blobs/.../published/translation.jsonDefaults:
- data root:
.translation-agent/ - blob root:
.translation-agent/blobs/ - local state DB:
.translation-agent/state.sqlite3 - traces:
.translation-agent/traces/
docker compose up --build
uv run translation-agent validate-config --jsonWhat docker compose up --build now does:
- loads repo-root
.env - builds and starts Postgres 18 with
pg_trgmandpgvectoravailable on port55432 - stores Postgres 18 data in the version-compatible
/var/lib/postgresqlvolume layout - runs
translation-agent migrate-dbautomatically after Postgres is healthy - keeps an
appcontainer alive with the repo mounted, a Linux virtualenv cached in a named volume, andvalidate-configalready checked
If you want to run the CLI on the host after the stack is up, no manual export is needed. translation-agent auto-loads the repo-root .env, so uv run translation-agent ... will use the Postgres DSN from that file by default.
If you want to run the CLI inside the container instead, use:
docker compose exec app uv run translation-agent run-job input.wav --job-id demo --jsonThe tracked example is .env.example. This workspace also includes a local .env with the same safe development defaults.
export TA_ADAPTER_MODE=real
export TA_ASSEMBLYAI_API_KEY=...
export TA_SPEECHMATICS_API_KEY=...
export TA_DEEPGRAM_API_KEY=...
export TA_GEMINI_API_KEY=...
export TA_ALLOW_LANGGRAPH_PY314_WARNING=1
uv run translation-agent validate-config --jsonIf the LangGraph compatibility warning disappears in a future dependency update, the explicit opt-in should no longer be necessary.
TA_TRANSCRIPTION_PROVIDERS is optional in real mode. If it is unset, the runtime behaves exactly as before and uses assemblyai,speechmatics,deepgram. If it is set, real mode uses exactly the selected non-empty subset in the configured order.
Gemini-first translation defaults:
export TA_ADAPTER_MODE=real
export TA_ASSEMBLYAI_API_KEY=...
export TA_SPEECHMATICS_API_KEY=...
export TA_DEEPGRAM_API_KEY=...
export TA_GEMINI_API_KEY=...
export TA_ALLOW_LANGGRAPH_PY314_WARNING=1
uv run translation-agent validate-config --json
uv run translation-agent run-job /absolute/path/to/input.mp4 --job-id demo-geminiAssemblyAI-only:
export TA_ADAPTER_MODE=real
export TA_TRANSCRIPTION_PROVIDERS=assemblyai
export TA_ASSEMBLYAI_API_KEY=...
export TA_GEMINI_API_KEY=...
export TA_ALLOW_LANGGRAPH_PY314_WARNING=1
uv run translation-agent validate-config --json
uv run translation-agent run-job /absolute/path/to/input.mp4 --job-id demo-realAssemblyAI + Deepgram:
export TA_ADAPTER_MODE=real
export TA_TRANSCRIPTION_PROVIDERS=assemblyai,deepgram
export TA_ASSEMBLYAI_API_KEY=...
export TA_DEEPGRAM_API_KEY=...
export TA_GEMINI_API_KEY=...
export TA_ALLOW_LANGGRAPH_PY314_WARNING=1
uv run translation-agent validate-config --jsonOpenAI translation override:
export TA_ADAPTER_MODE=real
export TA_TRANSLATION_PROVIDER=openai
export TA_TRANSLATION_MODEL_ID=gpt-5.4-mini
export TA_ASSEMBLYAI_API_KEY=...
export TA_DEEPGRAM_API_KEY=...
export TA_OPENAI_API_KEY=...
export TA_ALLOW_LANGGRAPH_PY314_WARNING=1
uv run translation-agent validate-config --jsonThe CLI entrypoint is translation-agent.
uv run translation-agent validate-config
uv run translation-agent validate-config --jsonIt verifies:
- runtime directories exist or can be created
- SQLite or Postgres connectivity works
- secrets are not leaked in DSN output
- provider credentials are present for the selected real-mode providers when
TA_ADAPTER_MODE=real - real mode is allowed by the current LangGraph compatibility gate
uv run translation-agent list-runs
uv run translation-agent list-runs --jsonThis command lists persisted runs in reverse chronological order. Plain output prints one compact
summary line per run. JSON output returns full persisted run records, including any stored
input_data, output_data, metadata, and error payloads.
uv run translation-agent run-job input.wav --job-id demo
uv run translation-agent run-job input.wav --job-id demo --review auto
uv run translation-agent run-job input.wav --job-id demo --json
uv run translation-agent run-job input.wav --job-id demo-ja --target-language ja --json
uv run translation-agent review-job <run-id> --json
uv run translation-agent approve-review <run-id> --candidate-id <candidate-id> --jsonreview-job now defaults to an exception-only operator flow:
--jsonreturns the primaryflagged_spanscontract,review_mode="exception_only", machine recommendation counts, compatibilityreview_spans, optional resumabledraft_resolution, and legacyreview_diffs- interactive mode launches a single compare workspace that shows only flagged spans, keeps the
source excerpt fixed at the top, places candidate variants side by side, auto-saves every
selection or edit, and exposes only
PublishandRejectas final outcomes
Most runs should auto-publish without opening review-job. Human review now opens only when
machine adjudication still leaves a real blocker such as no surviving candidate, no machine winner,
an unresolved blocking contradiction, or an investigation timeout.
approve-review and resolve-review remain available as legacy automation and compatibility
surfaces. They still map to the internal supervision enums and still publish a synthetic reviewed
translation artifact instead of promoting a machine candidate wholesale.
The public result payload contains:
run_idjob_idstatussourcesource_languagetarget_languageblob_roottrace_pathstate_backendstate_db_targetfailure_reffailure_summaryfailure_reasonsreview_required_stageapproval_refapproved_candidate_idapproved_source_transcript_candidate_idresume_commands
Final statuses currently emitted by the API are:
completedcompleted_with_degraded_transcriptioncompleted_after_human_reviewhuman_review_requiredtranslation_failed
Human review is translation-only in the current CLI. Transcript disagreement no longer blocks the runtime path; instead, translation generation fans out across all surviving transcript candidates, and a later approved translation implicitly selects the canonical transcript candidate.
uv run translation-agent convert-json-to-srt .translation-agent/blobs/.../published/translation.json
uv run translation-agent convert-json-to-srt .translation-agent/blobs/.../published/translation.json --output ./translation-backfill.srt --jsonThis command converts a persisted TranslationCandidate artifact such as published/translation.json
or candidates/translations/*.json into .srt. It does not accept the summary payload at
exports/translation.json because that file does not include timed segments.
from translation_agent.api import RunJobRequest, list_runs, run_job
result = run_job(
RunJobRequest(
source="input.wav",
job_id="demo",
tenant_id="tenant-local",
project_id="project-local",
source_language="en",
target_language="zh-CN",
)
)
print(result.status)
print(result.trace_path)
print([record.run_id for record in list_runs()])The API validates configuration first, persists a request manifest, creates a run record, executes the workflow, writes a JSONL trace, and returns the run summary. list_runs() reads persisted RunRecord items newest-first from the operational store.
A sample successful dry-run writes artifacts like this under the blob root:
jobs/<run_id>-request.json
memory/long-term/store.json
tenants/<tenant>/projects/<project>/languages/<src>-to-<dst>/jobs/<job_id>/
artifacts/audio.json
artifacts/audio.wav
staging/transcripts/*.json
staging/translations/*.json
raw/provider-payloads/*.json
candidates/transcripts/*.json
candidates/translations/*.json
reviews/transcript/*.json
reviews/translation/*.json
approvals/translation.json
learning/transcript-approval.json
decisions/transcript.json
decisions/translation.json
investigations/*.json
memory/recall/*.json
memory/batches/*.json
memory/consolidations/*.json
memory/prompt-evolution/*.json
published/artifacts.json
published/scorecard.json
published/transcript.json
published/translation.json
exports/translation.srt
exports/translation.json
deliveries/translation.json
traces/<run_id>.jsonl
Important behavior:
- artifact keys are scoped by tenant, project, source language, target language, and job ID
- the same job ID can exist safely across tenants or language pairs
- translation failure publishes
translation-failed.jsoninstead ofpublished/translation.json - the scorecard includes routing facts, decision payloads, trace refs, export refs, downstream refs, memory refs, and prompt-evolution refs
To backfill subtitles from a persisted translation artifact:
uv run translation-agent convert-json-to-srt path/to/published/translation.jsonThe settings model accepts more fields than most users need. These are the ones that materially change current behavior.
| Variable | Purpose |
|---|---|
TA_DATA_DIR |
root for local blobs, traces, and SQLite state |
TA_BLOB_DIR |
override blob directory |
TA_TRACE_DIR |
override trace directory |
TA_STATE_DB_PATH |
override SQLite database path |
TA_STATE_DB_DSN |
switch operational state to Postgres |
TA_ADAPTER_MODE |
choose fake or real runtime |
TA_TRANSCRIPTION_PROVIDERS |
comma-separated subset of real-mode transcription providers: assemblyai, speechmatics, deepgram |
TA_ALLOW_LANGGRAPH_PY314_WARNING |
opt into real mode despite the current warning gate |
TA_FFMPEG_BINARY |
override the ffmpeg executable path |
TA_PROVIDER_TIMEOUT_SECONDS |
provider HTTP timeout for transcription providers without a provider-specific override; defaults to 300 seconds |
TA_ADAPTER_RETRY_ATTEMPTS |
retry attempts for provider calls |
TA_ADAPTER_INITIAL_BACKOFF_SECONDS |
initial retry backoff |
TA_ADAPTER_MAX_BACKOFF_SECONDS |
max retry backoff |
TA_ADAPTER_POLL_INTERVAL_SECONDS |
poll interval for async providers |
TA_ADAPTER_POLL_ATTEMPTS |
max provider polling attempts |
TA_GLOBAL_PARALLEL_TOKENS |
shared hard ceiling for run-wide in-flight leaf work; defaults to 8 |
TA_TRANSCRIPTION_MAX_WORKERS |
optional local cap for transcription provider fan-out; unset means auto mode |
TA_TRANSLATION_CANDIDATE_MAX_WORKERS |
optional local cap for outer translation-candidate fan-out; unset means auto mode |
TA_TRANSLATION_CHUNK_MAX_WORKERS |
optional local cap for per-candidate translation chunk fan-out; unset means auto mode |
TA_REVIEW_MAX_WORKERS |
optional local cap for review bundle fan-out; unset means auto mode |
TA_REFERENCE_EVALUATION_MAX_WORKERS |
optional local cap for historical reference evaluation fan-out; unset means auto mode |
TA_MEMORY_DRAIN_MAX_WORKERS |
optional local cap for parallel-safe memory drain fan-out; unset means auto mode |
TA_ASSEMBLYAI_API_KEY |
AssemblyAI credential for real mode |
TA_SPEECHMATICS_API_KEY |
Speechmatics credential for real mode |
TA_DEEPGRAM_API_KEY |
Deepgram credential for real mode |
TA_TRANSLATION_PROVIDER |
translation provider for real mode: gemini or openai |
TA_TRANSLATION_MODEL_ID |
translation model ID for real mode; defaults to gemini-2.5-flash |
TA_REASONING_PROVIDER |
configured reasoning provider profile; defaults to openai |
TA_REASONING_MODEL_ID |
configured reasoning model profile; defaults to gpt-5.4 |
TA_GEMINI_API_KEY |
Gemini credential for real-mode translation when TA_TRANSLATION_PROVIDER=gemini |
TA_GEMINI_BASE_URL |
Gemini OpenAI-compatible base URL override |
TA_OPENAI_API_KEY |
OpenAI credential for real-mode translation when TA_TRANSLATION_PROVIDER=openai, and for future live reasoning integrations |
TA_OPENAI_BASE_URL |
OpenAI-compatible base URL override for OpenAI-backed translation or future reasoning adapters |
TA_DEFAULT_SOURCE_LANGUAGE |
default source language for API and CLI runs when omitted |
TA_DEFAULT_TARGET_LANGUAGE |
default target language for API and CLI runs when omitted |
TA_TRANSLATION_PROMPT_VERSION |
translation prompt version recorded in outputs |
The settings model also exposes TA_WORKSPACE_DIR, TA_LOG_LEVEL, TA_EMIT_CONSOLE_LOGS, and provider base URL overrides. At the moment those are configuration surface area, but the repo’s behavior is primarily driven by the variables listed above.
Parallelism behavior:
TA_GLOBAL_PARALLEL_TOKENSis the hard ceiling across nested fan-out for provider calls and local compute leaf work.- Stage worker caps are local ceilings only. When unset, each stage resolves at call time to
min(task_count, global_parallel_tokens). - Saturation uses queue-and-wait backpressure. Work is delayed until tokens free up rather than being dropped or failed fast.
Environment loading order:
- process environment variables
- repo-root
.env - model defaults
Project conventions in AGENTS.md require uv and ruff.
Useful commands:
uv run ruff check .
uv run detect-secrets scan > .secrets.baseline
uv run pre-commit run --all-files
uv run pyright
uv run pytest -m "unit or slice"
uv run pytest -m contract
uv run pytest -m "integration or migration" -n 2 --dist=loadfile
uv run pytest -m "regression and not staging_only"The pre-commit suite now includes detect-secrets with .secrets.baseline so new credential-like additions are blocked while the current fake/test fixtures stay allowlisted. Regenerate the baseline only when you intentionally add or change known non-secret fixtures.
The highest-signal workflow coverage lives in:
tests/test_phase_two_workflow.pytests/test_phase_three_adapters.pytests/test_phase_four_review.pytests/test_phase_five_memory_publish.pytests/test_phase_six_hardening.pytests/regression/test_runtime_regression.py
Implementation docs live under docs/: