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:
video/audio → demux audio → ASR → machine-translate → render subtitles (SRT/VTT).subtitle file → parse → (optional reflow) → machine-translate → re-render (SRT/VTT).Ingress B is the fully runnable path today (plain classpath, no bytedeco, no ASR). Ingress A's media Collect, translate, and render stages are done but the path is blocked on a real ASR adapter (the transcriber is still a fail-loud stub).
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 context | Aggregate root | Also | Lifecycle ADT |
|---|---|---|---|
domain.ingestion | MediaAsset | ProbeInfo VO, MediaKind | AssetStatus |
domain.job | TranslationJob | TranslationError (closed err set) | JobState (forward-only FSM) |
domain.transcription | Transcript | Segment entity, Confidence VO | TranscriptStatus |
domain.translation | TranslatedCues | TranslationUnit VO | TranslationStatus |
domain.rendering | SubtitleTrack | Cue entity, SubtitleFormat | TrackStatus |
Shared kernel (engine.shared, bottom of the stack — depends on nothing
above): Language (BCP-47, closed registry), Timecode, TimeRange, SourceRef.
Arrows point down; nothing calls upward. Effects only at the edges (Collect + Boundary); the middle is pure.
| Stratum | Realized by | Role |
|---|---|---|
| Collect | engine.collect.* (hive-system fs + hive-weave bounded concurrency; JavaCV ffmpeg) | path/process effects — probe container facts, demux audio to PCM/WAV |
| Promote | engine.calc.* (pure) | lift boundary DATA (ASR segments, parsed cue-maps, translations) into domain aggregates — no IO |
| Pipeline | engine.api | orchestrate one job over INJECTED ports — a hive-dsl Result railway today (first err short-circuits); hive-events + a JobState FSM are planned |
| Boundary | engine.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 |
| Lib | Use it for |
|---|---|
hive-dsl | defadt/adt-case (closed ADTs) + smart ctors; Result railway (ok/err/let-ok/try-effect) — every fallible fn |
hive-system | Collect: DIP filesystem/path effects (fs/exists?) |
hive-weave | Collect: bounded concurrency (bounded-pmap) — one stateful ffmpeg grabber per task |
hive-di | provider routing: typed EDN/env config resolution (required via .source/.resolve only, so Malli is never dragged in) |
hive-events | planned — event/effect pipeline to drive the JobState FSM (not yet wired) |
hive-test | test alias: trifecta generators (golden + property + mutation) |
Done:
defadt/defrecord + smart ctors).engine.adapters.codec.*), selected per call by a format→codec registry (OCP); pure text ↔ cue-map, no IO.:identity passthrough terminus and an OpenAI-compatible LLM translator (:openrouter / :venice, order/count-preserving, key from pass: or env).calc.reflow cutting stage (drop-music / merge / cap / split-CPS / snap / re-index).Not yet:
:error/no-transcriber-available (ASR never falls back to a fake transcript). This blocks Ingress A.hive-events JobState FSM — the pipeline is the hive-dsl railway for now.In-process via bytedeco — no system ffmpeg required (natives bundled):
javacpp-presets/ffmpeg → raw native bindings (avcodec) + bundled natives.javacv → FFmpegFrameGrabber/FFmpegFrameRecorder wrappers. Use the grabber
by default; drop to raw avcodec only for stream-level constants.Confined to engine.collect.*, behind port.media — engine.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 javacv ↔
presets/ffmpeg together. See the :ffmpeg alias in deps.edn (slim
linux-x86_64 natives; swap the classifier for other platforms).
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).
# Ingress A (demux + ASR) needs the :ffmpeg alias for media AND a real ASR adapter
# (not yet shipped — fails loud with :error/no-transcriber-available until one lands).
clj -M:ffmpeg: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
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |