Liking cljdoc? Tell your friends :D

vtranslate-engine

JVM Clojure domain core for vtranslate — automated video subtitle translation. It carries the whole domain model (DDD aggregates + a shared kernel), the pure pipeline, and the driven adapters. A thin babashka CLI (../vtranslate-cli) drives it across a process boundary (subprocess EDN transport). This repo owns all domain types; the CLI owns none.

Two ingress paths:

  • Ingress A (media, with ASR): video/audio → demux audio → ASR → machine-translate → render subtitles (SRT/VTT).
  • Ingress B (subtitle, no ASR): subtitle file → parse → (optional reflow) → machine-translate → re-render (SRT/VTT).

Ingress B is fully runnable on the plain classpath. Ingress A is runnable with a configured ASR backend; :onnx-bytedeco supplies a raw Whisper ONNX path on the optional :onnx classpath.

Domain model (M0) — 5 bounded contexts + shared kernel

All aggregates are hive-dsl defadt (closed sums) + defrecord value objects behind smart constructors that return a Result — validation lives at construction, invalid states are unrepresentable. No Malli on the engine classpath. DDD: cross-aggregate references are by id, never embedded.

Bounded contextAggregate rootAlsoLifecycle ADT
domain.ingestionMediaAssetProbeInfo VO, MediaKindAssetStatus
domain.jobTranslationJobTranslationError (closed err set)JobState (forward-only FSM)
domain.transcriptionTranscriptSegment entity, Confidence VOTranscriptStatus
domain.translationTranslatedCuesTranslationUnit VOTranslationStatus
domain.renderingSubtitleTrackCue entity, SubtitleFormatTrackStatus

Shared kernel (engine.shared, bottom of the stack — depends on nothing above): Language (BCP-47, closed registry), Timecode, TimeRange, SourceRef.

Architecture — CPPB strata

Arrows point down; nothing calls upward. Effects only at the edges (Collect + Boundary); the middle is pure.

StratumRealized byRole
Collectengine.collect.* (hive-system fs + hive-weave bounded concurrency; JavaCV ffmpeg)path/process effects — probe container facts, demux audio to PCM/WAV
Promoteengine.calc.* (pure)lift boundary DATA (ASR segments, parsed cue-maps, translations) into domain aggregates — no IO
Pipelineengine.apiorchestrate one job over INJECTED ports — a hive-dsl Result railway today (first err short-circuits); hive-events + a JobState FSM are planned
Boundaryengine.port.* / engine.adapters.* / engine.providers.*ports = defprotocol; driven adapters = defrecord; the DIP seam is wired by engine.wiring (open build-port defmulti, OCP) and entered from engine.main

Foundation libs — do NOT reinvent their macros

LibUse it for
hive-dsldefadt/adt-case (closed ADTs) + smart ctors; Result railway (ok/err/let-ok/try-effect) — every fallible fn
hive-systemCollect: DIP filesystem/path effects (fs/exists?)
hive-weaveCollect: bounded concurrency (bounded-pmap) — one stateful ffmpeg grabber per task
hive-diprovider routing: typed EDN/env config resolution (required via .source/.resolve only, so Malli is never dragged in)
hive-eventsplanned — event/effect pipeline to drive the JobState FSM (not yet wired)
hive-testtest alias: trifecta generators (golden + property + mutation)

Status

Done:

  • M0 — domain model: all 5 bounded contexts + shared kernel (defadt/defrecord + smart ctors).
  • M2 — subtitle codecs: SRT + WebVTT render/parse (engine.adapters.codec.*), selected per call by a format→codec registry (OCP); pure text ↔ cue-map, no IO.
  • M4 — provider routing (config → registry → router, fail-loud) + translator adapters: :identity passthrough terminus and an OpenAI-compatible LLM translator (:openrouter / :venice, order/count-preserving, key from pass: or env).
  • M5 — Collect: in-process ffmpeg via bytedeco JavaCV (probe + audio extract), behind the media port — no bytedeco type crosses the boundary, µs→ms converted at the edge.
  • M6Ingress B (no-ASR): subtitle → translate → re-render, runs end-to-end WITHOUT any ASR adapter or bytedeco. Optional pure calc.reflow cutting stage (drop-music / merge / cap / split-CPS / snap / re-index).
  • Raw Whisper ONNX ASR — JavaCPP ONNX Runtime sessions, Slaney log-mel frontend, cacheless greedy decoder, byte-level BPE, timestamps, chunking, and fail-loud model validation behind :onnx-bytedeco.

Not yet:

  • hive-events JobState FSM — the pipeline is the hive-dsl railway for now.

Raw ONNX Whisper ASR

The :onnx-bytedeco provider expects a cacheless Hugging Face Whisper export in one directory:

  • encoder.onnx with input_features -> last_hidden_state
  • decoder.onnx with input_ids + encoder_hidden_states -> logits
  • tokenizer.json

Filenames can be overridden with :encoder-file, :decoder-file, and :tokenizer-file under :transcriber-opts. Run its native unit suite with clj -M:onnx:test --focus vtranslate.engine.adapters.transcriber.onnx-bytedeco-native-test. Set VT_ONNX_MODEL_DIR and VT_ONNX_WAV to make that suite execute a real model and the reusable ITranscriber contract; otherwise the heavyweight smoke is skipped. Input WAV must be 16 kHz mono PCM.

ffmpeg (Collect, M5)

In-process via bytedeco — no system ffmpeg required (natives bundled):

  • javacpp-presets/ffmpeg → raw native bindings (avcodec) + bundled natives.
  • javacvFFmpegFrameGrabber/FFmpegFrameRecorder wrappers. Use the grabber by default; drop to raw avcodec only for stream-level constants.

Confined to engine.collect.*, behind port.mediaengine.domain never sees a bytedeco type. Traps encoded in the adapter: the grabber is stateful + not thread-safe → one-per-task, with-open closes on throw; ffmpeg counts in microseconds → convert to integer ms at the boundary; pin javacvpresets/ffmpeg together. See the :ffmpeg alias in deps.edn (slim linux-x86_64 natives; swap the classifier for other platforms).

Dev

clj -P                 # resolve deps (downloads git libs)
clj -M:dev             # nREPL for REPL-driven dev
clj -M:test            # kaocha: unit + property + contract suites

# Ingress B (no ASR) — runs on the plain classpath. The spec is one EDN map on argv
# (or stdin); a subtitle extension selects this path. Prints an EDN Result, exit 0/1.
clj -M:run '{:job-id "j1" :source "in.srt" :source-language "en" :target-language "pt-BR" :format :format/srt}'
#   default translator is :identity (structural passthrough smoke run); for real MT
#   set VT_TRANSLATOR=openrouter (or :config {:translator :openrouter} in the spec).

# Raw ONNX Ingress A: configure :transcriber :onnx-bytedeco and model-dir in
# config.edn, then include both native aliases.
clj -M:ffmpeg:onnx:run '{:job-id "j1" :source "in.mp4" :source-language "en" :target-language "pt-BR"}'

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close