doblarr 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. doblarr-0.1.0/LICENSE +21 -0
  2. doblarr-0.1.0/MANIFEST.in +2 -0
  3. doblarr-0.1.0/PKG-INFO +443 -0
  4. doblarr-0.1.0/README.md +412 -0
  5. doblarr-0.1.0/config.example.yaml +120 -0
  6. doblarr-0.1.0/doblarr/__init__.py +10 -0
  7. doblarr-0.1.0/doblarr/__main__.py +8 -0
  8. doblarr-0.1.0/doblarr/artifacts.py +54 -0
  9. doblarr-0.1.0/doblarr/auth.py +39 -0
  10. doblarr-0.1.0/doblarr/cache.py +48 -0
  11. doblarr-0.1.0/doblarr/cli.py +104 -0
  12. doblarr-0.1.0/doblarr/clients/__init__.py +1 -0
  13. doblarr-0.1.0/doblarr/clients/base.py +104 -0
  14. doblarr-0.1.0/doblarr/clients/plex.py +91 -0
  15. doblarr-0.1.0/doblarr/clients/radarr.py +26 -0
  16. doblarr-0.1.0/doblarr/clients/sonarr.py +32 -0
  17. doblarr-0.1.0/doblarr/clients/translator.py +341 -0
  18. doblarr-0.1.0/doblarr/clients/voicebox.py +232 -0
  19. doblarr-0.1.0/doblarr/config.py +161 -0
  20. doblarr-0.1.0/doblarr/config_schema.py +168 -0
  21. doblarr-0.1.0/doblarr/discovery.py +230 -0
  22. doblarr-0.1.0/doblarr/errors.py +48 -0
  23. doblarr-0.1.0/doblarr/events.py +62 -0
  24. doblarr-0.1.0/doblarr/ffmpeg.py +64 -0
  25. doblarr-0.1.0/doblarr/jobs.py +332 -0
  26. doblarr-0.1.0/doblarr/library_service.py +115 -0
  27. doblarr-0.1.0/doblarr/logging_setup.py +97 -0
  28. doblarr-0.1.0/doblarr/model_pool.py +34 -0
  29. doblarr-0.1.0/doblarr/models.py +76 -0
  30. doblarr-0.1.0/doblarr/pipeline.py +344 -0
  31. doblarr-0.1.0/doblarr/plex_labels.py +114 -0
  32. doblarr-0.1.0/doblarr/presets.py +19 -0
  33. doblarr-0.1.0/doblarr/recipes.py +166 -0
  34. doblarr-0.1.0/doblarr/review.py +73 -0
  35. doblarr-0.1.0/doblarr/routes/__init__.py +1 -0
  36. doblarr-0.1.0/doblarr/routes/configuration.py +32 -0
  37. doblarr-0.1.0/doblarr/routes/jobs.py +284 -0
  38. doblarr-0.1.0/doblarr/routes/library.py +64 -0
  39. doblarr-0.1.0/doblarr/routes/series.py +182 -0
  40. doblarr-0.1.0/doblarr/routes/titles.py +299 -0
  41. doblarr-0.1.0/doblarr/routes/voice_catalog.py +159 -0
  42. doblarr-0.1.0/doblarr/scheduler.py +69 -0
  43. doblarr-0.1.0/doblarr/server.py +175 -0
  44. doblarr-0.1.0/doblarr/services.py +66 -0
  45. doblarr-0.1.0/doblarr/stages/__init__.py +19 -0
  46. doblarr-0.1.0/doblarr/stages/audition.py +113 -0
  47. doblarr-0.1.0/doblarr/stages/common.py +174 -0
  48. doblarr-0.1.0/doblarr/stages/diarize.py +162 -0
  49. doblarr-0.1.0/doblarr/stages/extract.py +80 -0
  50. doblarr-0.1.0/doblarr/stages/fit_timing.py +188 -0
  51. doblarr-0.1.0/doblarr/stages/mix.py +227 -0
  52. doblarr-0.1.0/doblarr/stages/mux.py +150 -0
  53. doblarr-0.1.0/doblarr/stages/prepare.py +48 -0
  54. doblarr-0.1.0/doblarr/stages/quality.py +174 -0
  55. doblarr-0.1.0/doblarr/stages/separate.py +68 -0
  56. doblarr-0.1.0/doblarr/stages/synthesize.py +339 -0
  57. doblarr-0.1.0/doblarr/stages/transcribe.py +275 -0
  58. doblarr-0.1.0/doblarr/stages/translate.py +77 -0
  59. doblarr-0.1.0/doblarr/store.py +190 -0
  60. doblarr-0.1.0/doblarr/subtitles.py +56 -0
  61. doblarr-0.1.0/doblarr/telemetry.py +59 -0
  62. doblarr-0.1.0/doblarr/versions.py +99 -0
  63. doblarr-0.1.0/doblarr/voices.py +133 -0
  64. doblarr-0.1.0/doblarr/webhooks.py +68 -0
  65. doblarr-0.1.0/doblarr.egg-info/PKG-INFO +443 -0
  66. doblarr-0.1.0/doblarr.egg-info/SOURCES.txt +133 -0
  67. doblarr-0.1.0/doblarr.egg-info/dependency_links.txt +1 -0
  68. doblarr-0.1.0/doblarr.egg-info/entry_points.txt +2 -0
  69. doblarr-0.1.0/doblarr.egg-info/requires.txt +21 -0
  70. doblarr-0.1.0/doblarr.egg-info/top_level.txt +1 -0
  71. doblarr-0.1.0/pyproject.toml +80 -0
  72. doblarr-0.1.0/setup.cfg +4 -0
  73. doblarr-0.1.0/setup.py +19 -0
  74. doblarr-0.1.0/tests/test_audio_quality.py +78 -0
  75. doblarr-0.1.0/tests/test_cancellation.py +110 -0
  76. doblarr-0.1.0/tests/test_casts.py +194 -0
  77. doblarr-0.1.0/tests/test_checkpoint.py +136 -0
  78. doblarr-0.1.0/tests/test_clients.py +137 -0
  79. doblarr-0.1.0/tests/test_config_overrides.py +42 -0
  80. doblarr-0.1.0/tests/test_dialogue_planning.py +98 -0
  81. doblarr-0.1.0/tests/test_diarize.py +101 -0
  82. doblarr-0.1.0/tests/test_episode_runner.py +61 -0
  83. doblarr-0.1.0/tests/test_events.py +126 -0
  84. doblarr-0.1.0/tests/test_extract.py +41 -0
  85. doblarr-0.1.0/tests/test_fit_timing.py +103 -0
  86. doblarr-0.1.0/tests/test_generation_edges.py +99 -0
  87. doblarr-0.1.0/tests/test_generation_integration.py +121 -0
  88. doblarr-0.1.0/tests/test_generation_reliability.py +145 -0
  89. doblarr-0.1.0/tests/test_generation_throughput.py +65 -0
  90. doblarr-0.1.0/tests/test_jobfile.py +109 -0
  91. doblarr-0.1.0/tests/test_mix.py +80 -0
  92. doblarr-0.1.0/tests/test_mux.py +52 -0
  93. doblarr-0.1.0/tests/test_plex_refresh.py +144 -0
  94. doblarr-0.1.0/tests/test_recipes.py +178 -0
  95. doblarr-0.1.0/tests/test_review.py +98 -0
  96. doblarr-0.1.0/tests/test_script_cache.py +113 -0
  97. doblarr-0.1.0/tests/test_separate.py +65 -0
  98. doblarr-0.1.0/tests/test_series.py +119 -0
  99. doblarr-0.1.0/tests/test_server.py +175 -0
  100. doblarr-0.1.0/tests/test_smoke.py +182 -0
  101. doblarr-0.1.0/tests/test_spa.py +43 -0
  102. doblarr-0.1.0/tests/test_store.py +92 -0
  103. doblarr-0.1.0/tests/test_tease.py +146 -0
  104. doblarr-0.1.0/tests/test_telemetry.py +29 -0
  105. doblarr-0.1.0/tests/test_transcribe_whisper.py +138 -0
  106. doblarr-0.1.0/tests/test_translation.py +200 -0
  107. doblarr-0.1.0/tests/test_versions.py +59 -0
  108. doblarr-0.1.0/tests/test_voice_catalog.py +54 -0
  109. doblarr-0.1.0/tests/test_webhooks.py +92 -0
  110. doblarr-0.1.0/web/android-chrome-192x192.png +0 -0
  111. doblarr-0.1.0/web/android-chrome-512x512.png +0 -0
  112. doblarr-0.1.0/web/apple-touch-icon.png +0 -0
  113. doblarr-0.1.0/web/favicon-16x16.png +0 -0
  114. doblarr-0.1.0/web/favicon-32x32.png +0 -0
  115. doblarr-0.1.0/web/favicon.ico +0 -0
  116. doblarr-0.1.0/web/index.html +383 -0
  117. doblarr-0.1.0/web/js/api.js +46 -0
  118. doblarr-0.1.0/web/js/app.js +339 -0
  119. doblarr-0.1.0/web/js/dom.js +21 -0
  120. doblarr-0.1.0/web/js/episodes.js +76 -0
  121. doblarr-0.1.0/web/js/identity.js +24 -0
  122. doblarr-0.1.0/web/js/jobs-data.js +9 -0
  123. doblarr-0.1.0/web/js/jobs.js +240 -0
  124. doblarr-0.1.0/web/js/library.js +147 -0
  125. doblarr-0.1.0/web/js/recipes.js +119 -0
  126. doblarr-0.1.0/web/js/review.js +127 -0
  127. doblarr-0.1.0/web/js/settings-model.js +118 -0
  128. doblarr-0.1.0/web/js/settings.js +112 -0
  129. doblarr-0.1.0/web/js/state.js +15 -0
  130. doblarr-0.1.0/web/js/title-routing.js +24 -0
  131. doblarr-0.1.0/web/js/title.js +504 -0
  132. doblarr-0.1.0/web/js/voice-picker.js +99 -0
  133. doblarr-0.1.0/web/logo.png +0 -0
  134. doblarr-0.1.0/web/site.webmanifest +20 -0
  135. doblarr-0.1.0/web/styles.css +233 -0
doblarr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jhd3197
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include config.example.yaml
2
+ recursive-include web *.html *.css *.js *.png *.ico *.webmanifest
doblarr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,443 @@
1
+ Metadata-Version: 2.4
2
+ Name: doblarr
3
+ Version: 0.1.0
4
+ Summary: AI dubbing for your media library
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/jhd3197/Doblarr
7
+ Project-URL: Issues, https://github.com/jhd3197/Doblarr/issues
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: requests>=2.31
12
+ Requires-Dist: PyYAML>=6.0
13
+ Requires-Dist: pysubs2>=1.6
14
+ Requires-Dist: tenacity>=8
15
+ Requires-Dist: fastapi>=0.110
16
+ Requires-Dist: uvicorn>=0.27
17
+ Requires-Dist: pydantic>=2
18
+ Requires-Dist: prompture[anthropic]>=1.12.0
19
+ Provides-Extra: real
20
+ Requires-Dist: demucs>=4.0; extra == "real"
21
+ Requires-Dist: whisperx; extra == "real"
22
+ Requires-Dist: pyannote.audio>=3.1; extra == "real"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: pytest-cov>=5; extra == "dev"
26
+ Requires-Dist: httpx>=0.27; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Requires-Dist: mypy>=1.10; extra == "dev"
29
+ Requires-Dist: types-requests>=2.31; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ <h1 align="center">Doblarr</h1>
33
+
34
+ <p align="center">
35
+ <strong>AI dubbing for your media library.</strong><br/>
36
+ The missing link in your *arr stack — turn a foreign-language film into an added,
37
+ translated audio track, voiced by cloned speaker voices.
38
+ </p>
39
+
40
+ <p align="center">
41
+ <em>Doblaje</em> (Spanish: dubbing) + <code>-arr</code>. Sits next to Bazarr:
42
+ Bazarr does subtitles, Doblarr does dubs.
43
+ </p>
44
+
45
+ ---
46
+
47
+ ## What it does
48
+
49
+ Given a video (e.g. a Korean movie) and optionally its subtitles, Doblarr produces
50
+ a new audio track — say English or Spanish — spoken in voices cloned from the
51
+ original actors, and muxes it back in as **"AI - ES"** without touching the
52
+ original. Plex/Jellyfin then just show it as another audio option.
53
+
54
+ Doblarr owns the movie-specific pipeline. **[voicebox](https://github.com/jamiepine/voicebox)**
55
+ (MIT) is the voice-cloning + TTS engine, called over HTTP — not vendored — so the
56
+ two stay decoupled and voicebox upgrades come for free.
57
+
58
+ ## What's real today
59
+
60
+ The **app** is live — a real backend + web UI you can run and use:
61
+
62
+ - **Web UI + API** (`doblarr serve`) — serves the interface and a REST API.
63
+ - **Library scan** — reads your **Radarr (movies) + Sonarr (shows)** and classifies
64
+ every title as **needs-dub / partial / available** by looking at its actual audio
65
+ tracks vs. your target languages. Shows in the Library page with live counts.
66
+ - **Settings** — edit the config from the UI; saved to `config.yaml` (secrets redacted,
67
+ never clobbered).
68
+ - **Job queue** — enqueue a dub from the Library; a background worker runs it and the
69
+ Dubs page + Overview update live.
70
+
71
+ The worker defaults to **dry-run** (`dub.dry_run: true`), which plans stages without
72
+ producing audio. Real extraction, separation, subtitle transcription, translation,
73
+ speech generation, timing, mixing and muxing are implemented. Set dry-run to false
74
+ when the required local services and dependencies are ready.
75
+
76
+ For an interrupted first episode with existing audio stems, the explicit
77
+ `scripts/finish_episode.py` runner saves translation batches and individual voice
78
+ clips, then assembles a full-length video. See [episode recovery](docs/episode-recovery.md).
79
+
80
+ ## Running it
81
+
82
+ ### Install from PyPI
83
+
84
+ ```bash
85
+ pip install doblarr
86
+ python -c "from importlib.resources import files; from pathlib import Path; Path('config.yaml').write_bytes(files('doblarr').joinpath('config.example.yaml').read_bytes())"
87
+ doblarr serve
88
+ ```
89
+
90
+ Run the configuration-copy command in a new directory, then edit `config.yaml`
91
+ before starting the server. The package includes the web UI. Real dubbing also
92
+ requires FFmpeg/ffprobe on PATH, a running Voicebox service, and the optional
93
+ ML dependencies (`pip install "doblarr[real]"`). Install a PyTorch build matching
94
+ your platform/GPU before installing that extra. The default mode is dry-run.
95
+
96
+ ### Run from source
97
+
98
+ ```bash
99
+ pip install -r requirements.txt # core + FastAPI/uvicorn
100
+ cp config.example.yaml config.yaml # set Radarr/Sonarr URLs + API keys
101
+ python -m doblarr serve # http://127.0.0.1:6363
102
+ ```
103
+
104
+ ### AI translation
105
+
106
+ Prompture is the shared AI translation layer for every real translation provider.
107
+ It handles structured JSON generation and provider capabilities; Doblarr validates
108
+ nonempty translated text and an exact one-to-one mapping of segment IDs before TTS.
109
+ Malformed responses get two attempts, followed by individual-line attempts for a
110
+ failed batch. Exhausted attempts fail the job rather than substitute source dialogue.
111
+ Character budgets remain approximate dubbing guidance, not a guarantee of audio duration.
112
+
113
+ Existing configuration remains supported:
114
+
115
+ - `translate.provider: claude` uses Prompture's Claude driver with the existing
116
+ model ID and `ANTHROPIC_API_KEY` (or Prompture's `CLAUDE_API_KEY`).
117
+ - `translate.provider: prompture` accepts `provider/model` and an optional endpoint.
118
+ - `translate.provider: voicebox` adapts the service's local LLM through the same
119
+ Prompture schema and validation pipeline.
120
+ - `translate.provider: passthrough` is an explicit stub for development.
121
+
122
+ Prompture is installed with the core dependencies. Translation loads it lazily,
123
+ so dry runs do not initialize an AI provider. Parsed-response usage metadata is
124
+ available on the translator's `last_usage`; Voicebox does not report token usage.
125
+
126
+ ### API authentication
127
+
128
+ Set `web.api_key` in `config.yaml` to lock the API: every `/api/*` route (except
129
+ `/api/health` and `/api/health/ready`) then requires the `X-Api-Key: <key>` header
130
+ (or `?api_key=<key>`). The web UI prompts for the key once and remembers it. With no
131
+ key configured the API stays open (fine for a trusted home network) and a warning is
132
+ logged at startup.
133
+
134
+ Security notes: `config.yaml` holds your *arr/Plex keys in plaintext — protect it with
135
+ filesystem permissions (it's gitignored). Doblarr serves plain HTTP; put it behind a
136
+ reverse proxy for HTTPS if you expose it beyond localhost/LAN.
137
+
138
+ ### Webhooks (Radarr/Sonarr → Doblarr)
139
+
140
+ Doblarr accepts the standard *arr webhook JSON at `POST /api/webhooks/radarr` and
141
+ `POST /api/webhooks/sonarr`. A **Download** (import) event schedules a library rescan —
142
+ a burst of webhooks coalesces into one scan (`discovery.webhook_debounce`, default 30s);
143
+ **Test** events just return 200; other event types are ignored. If `filtering.auto_label`
144
+ is on, the rescan also syncs Plex labels.
145
+
146
+ Setup in Radarr/Sonarr: **Settings → Connect → Add → Webhook** —
147
+ URL `http://<doblarr-host>:6363/api/webhooks/radarr` (or `.../sonarr`), trigger
148
+ **On Import/On Upgrade**. If you set `web.api_key`, add a header `X-Api-Key: <key>`
149
+ in the webhook settings (no key configured → webhooks are open like the rest of the API).
150
+
151
+ ### Persistence & resume
152
+
153
+ Jobs and the last library scan live in a SQLite database (`paths.db`, default
154
+ `<work_dir>/doblarr.db`; in Docker that's inside the mounted `/data`), so the Dubs
155
+ page and Overview survive restarts. A legacy `work/jobs.json` is imported once and
156
+ renamed to `jobs.json.migrated`. Jobs interrupted mid-run are re-queued at startup,
157
+ and the pipeline reuses artifacts whose input and configuration manifests still
158
+ match. Source artifacts are shared across target languages; translated scripts and
159
+ outputs use separate language namespaces. Completed TTS clips are verified by
160
+ content fingerprints, and interrupted waits resume the saved remote generation ID. Enqueue with `"force": true` to redo every stage. After a
161
+ real (non-dry-run) mux, Doblarr asks Plex to refresh that item so the new
162
+ "`<Language>` AI" track shows up at once (`plex.auto_refresh`, default on; failures
163
+ never fail the job).
164
+
165
+ ### Faster generation and dialogue review
166
+
167
+ Choose **Custom**, **Preview**, or **Final** in generation settings. Preview uses
168
+ preset voices, the preview engine, faster separation, and no translation repair
169
+ retries; assign existing compatible Voicebox profile IDs first. Final enables
170
+ duration fitting. Custom respects your individual settings. Engine availability
171
+ and throughput depend on your Voicebox installation.
172
+
173
+ Use **Audition voices** on a title, or `--kind audition` on the CLI, for a short
174
+ WAV montage covering speakers, fast dialogue, quiet/loud passages, and different
175
+ points in the source. Transcription and speaker detection still inspect the source;
176
+ separation and speech generation run on the selected excerpts.
177
+
178
+ Completed or failed jobs with dialogue snapshots expose **Review** on the Dubs page.
179
+ Listen to a line, edit its wording, timing, voice, or delivery, then render changes.
180
+ A new job reuses matching clips and rebuilds the mix/export; the previous review
181
+ snapshot stays available. **Generate a new take** invalidates that line explicitly.
182
+ Delivery instructions require a compatible Qwen engine.
183
+
184
+ Translation supports scene context, terminology dictionaries, and bounded shortening
185
+ of overlong lines. Speech checks flag silence, clipping, duration problems, and
186
+ optional ASR mismatches. Loudness normalization and configurable ducking preserve
187
+ background dynamics. Unresolved flags remain visible for human review.
188
+
189
+ Each run writes stage timings and cache/retry counters to `work/reports`, available
190
+ through `GET /api/jobs/{id}/report`. See [the generation guide](docs/generation-roadmap.md)
191
+ for configuration, benchmark acceptance, and current limitations.
192
+
193
+ ### Shows and narrator voices
194
+
195
+ Click an episode title to open its own page at
196
+ `/title/tvdb-<show-id>/episode/<sonarr-episode-id>/voices`. Each title tab has
197
+ its own URL (`plan`, `voices`, `jobs`, or `meta`; shows also have `episodes`),
198
+ so refresh, shared links, and browser back/forward preserve the current workspace.
199
+ Movies use `/title/tmdb-<movie-id>/<tab>` and shows use `/title/tvdb-<show-id>/<tab>`.
200
+ Older links without a tab automatically open the appropriate default tab. Voice assignments, narrator
201
+ settings, audition actions, plans, and jobs are scoped to that episode; the back
202
+ button returns to its show. Episode plans initially inherit the show's saved plan.
203
+
204
+ **Browse all voices & samples** and **Find matching voice** expose saved profiles
205
+ and both preset catalogs provided by the connected Voicebox version (Kokoro and
206
+ Qwen CustomVoice). Filtering and ranking use language, declared voice gender, and
207
+ listening tags. Set a character role such as **Older man**, audition a candidate,
208
+ then save the cast. Unknown ages stay unknown, and diarization creates neutral
209
+ speaker labels rather than guessing age/gender. Voice traits can be tagged after
210
+ listening. Each cast assignment saves its engine so mixed-engine casts work.
211
+
212
+ Catalog browsing is read-only. Selecting a preset registers it as a Voicebox
213
+ profile if necessary; **Generate sample** submits a short TTS request. Qwen accepts
214
+ delivery directions, while Kokoro presets require their declared language. Model
215
+ availability and the resulting age/timbre still need auditioning. Text-only voice
216
+ design is not enabled: the installed Voicebox exposes its metadata but does not
217
+ implement the full generation path. Closing the picker stops polling/playback;
218
+ a submitted preview can finish in Voicebox history.
219
+
220
+ TV show pages open on **Episodes**, grouped by season, including episodes Sonarr
221
+ knows about that are not downloaded. Select the dub language to see source audio,
222
+ completed AI outputs, active jobs, and missing dubs separately. Queue individual
223
+ files, selected episodes, or missing dubs; shared files and active jobs are skipped.
224
+ A series folder is never sent to the media pipeline. Refresh episodes to fetch new
225
+ Sonarr inventory or the latest generation status.
226
+
227
+ In **Speakers & voices**, pick a saved narrator voice and optionally a Qwen delivery
228
+ direction, then **Save narrator**. These are defaults for new jobs in that show or
229
+ movie. Explicit episode character assignments take precedence. Use **Voices** on
230
+ an episode row to rename discovered speakers, choose their roles and voices, and
231
+ set delivery direction. Use **Audition** on that episode to hear the result before
232
+ queueing its full dub. Create/clone additional profiles in Voicebox and refresh the
233
+ voice list. A missing diarization model can still yield a single-narrator fallback;
234
+ voice settings do not recover undetected speakers.
235
+
236
+ ### Teasers & voice casting
237
+
238
+ Before committing to a full dub, queue a **tease** (Library card → "Tease", or
239
+ `POST /api/jobs` with `"kind": "tease"`): Doblarr dubs only the first
240
+ `dub.teaser_minutes` (default 10) into `<title>.tease.mkv` so you can audition the
241
+ voices. Tease artifacts live in a separate `.tease` namespace and never poison the
242
+ full dub's checkpoint cache.
243
+
244
+ Every detected speaker gets **one voice** from a per-title **voice cast**, labeled by
245
+ archetype ("Narrator", "Adult M 1", "Adult F 2", …) and auto-assigned on the first
246
+ tease. The cast persists (SQLite `voice_casts` table) and is reused by the full dub —
247
+ edit it from a Library card's "Cast" button (`GET`/`PUT /api/cast`, voices from
248
+ `GET /api/voices`, which proxies voicebox profiles or falls back to
249
+ `dub.preset_voices`). Multi-speaker casting lands with diarization (pyannote); until
250
+ then jobs fall back to a single narrator voice.
251
+
252
+ Open the UI, go to **Library** to see your real collection, and **Queue dub** on a
253
+ needs-dub title to watch it flow through the queue. The CLI still works too:
254
+ `python -m doblarr dub "<file>" --from ko --to es --subs film.srt --dry-run`.
255
+
256
+ ### Docker
257
+
258
+ Runs next to your other -arrs; reaches Radarr/Sonarr/Plex via `host.docker.internal`.
259
+
260
+ ```bash
261
+ cp config.docker.example.yaml config/config.yaml # fill in URLs + keys
262
+ docker compose up -d --build # http://localhost:6363
263
+ ```
264
+
265
+ Tagged releases (`git tag v0.2.0 && git push --tags`) build a multi-arch image to
266
+ `ghcr.io/<owner>/doblarr` (`latest` + the version tag) and cut a GitHub release with
267
+ auto-generated notes — see `.github/workflows/release.yml`.
268
+
269
+ `config/` holds `config.yaml`; `data/` holds the job store + generated Kometa
270
+ fragment. The image is the app only (no ML stack) — the worker runs dry-run until
271
+ Demucs/voicebox are added.
272
+
273
+ ## Pipeline
274
+
275
+ | # | Stage | Tool | Status |
276
+ |---|-------|------|--------|
277
+ | 1 | Extract audio | ffmpeg | ✅ real |
278
+ | 2 | Separate dialogue vs music+FX | Demucs `htdemucs_ft` | ✅ real |
279
+ | 3 | Timed transcript | subtitles (pysubs2) / WhisperX | ✅ real |
280
+ | 4 | Speaker diarization | pyannote 3.1 | ✅ real |
281
+ | 5 | Translate (dubbing-aware, length-budgeted) | Claude | ✅ real |
282
+ | 6 | Clone voices + synthesize lines | **voicebox** | ✅ wiring |
283
+ | 7 | Fit timing (isochrony) | ffmpeg `atempo` | ✅ real |
284
+ | 8 | Mix dialogue over M&E + ducking | ffmpeg `sidechaincompress` | ✅ real |
285
+ | 9 | Mux new track back | ffmpeg | ✅ real |
286
+
287
+ The whole thing runs end-to-end today in **`--dry-run`** (prints the plan, no heavy
288
+ deps). Stubs marked 🚧 are the build-out work, each isolated in its own module
289
+ under `doblarr/stages/`.
290
+
291
+ ## Project layout
292
+
293
+ ```
294
+ doblarr/
295
+ cli.py # CLI: serve / dub / check
296
+ server.py # app assembly, lifespan, authentication, SSE and static UI
297
+ routes/ # configuration, library, jobs and title API routers
298
+ library_service.py # discovery cache, persisted scan state and webhook orchestration
299
+ config.py # YAML config + defaults, env overrides, secret redaction
300
+ config_schema.py # pydantic validation of config.yaml (warnings, never fatal)
301
+ auth.py # X-Api-Key dependency for /api/* (optional; web.api_key)
302
+ discovery.py # library scan: needs-dub / partial / available
303
+ jobs.py # job queue (SQLite) + background worker (cancel-aware)
304
+ store.py # sqlite3 Database: WAL, migrations, jobs + scan_state
305
+ events.py # EventBus: in-process pub/sub with replay buffer
306
+ services.py # lazy cached service clients (DI seam) from Config
307
+ webhooks.py # *arr webhook classification + debounced rescan
308
+ cache.py # TTL cache for library scans
309
+ ffmpeg.py # run_ffmpeg/run_ffprobe with FFmpegError + cancel
310
+ logging_setup.py # console + rotating file + uvicorn + SSE log stream
311
+ scheduler.py # periodic rescan thread
312
+ models.py # DubJob / Segment / Speaker
313
+ pipeline.py # runs the stages in order (progress, cancel, resume)
314
+ clients/
315
+ base.py # ArrClient: Session + tenacity retry + uniform errors
316
+ radarr.py # Radarr API (movies)
317
+ sonarr.py # Sonarr API (shows)
318
+ plex.py # Plex API (labels; token in header)
319
+ voicebox.py # voicebox HTTP client (transcribe, profiles, generate, audio)
320
+ translator.py # shared Prompture structured translation
321
+ stages/ # one module per pipeline step (see table above)
322
+ web/index.html # application shell
323
+ web/styles.css # shared styles
324
+ web/js/app.js # routing, navigation and feature wiring
325
+ web/js/settings-model.js # field metadata shared by settings and title plans
326
+ web/js/api.js # JSON requests, authentication and consistent errors
327
+ web/js/jobs-data.js # coalesced job requests shared across screens
328
+ web/js/ # settings, library, jobs and title feature controllers
329
+ ```
330
+
331
+ ## API
332
+
333
+ | Method | Path | Purpose |
334
+ |--------|------|---------|
335
+ | GET | `/api/health` | liveness (process up) |
336
+ | GET | `/api/health/ready` | readiness (voicebox up + a source configured), 503 otherwise |
337
+ | GET | `/api/library` | scan Radarr+Sonarr, classify every title (`?refresh=true` bypasses the scan cache) |
338
+ | GET/POST | `/api/config` | read (redacted) / save config |
339
+ | GET/POST | `/api/jobs` | list / enqueue dub jobs (`force: true` ignores cached artifacts) |
340
+ | POST | `/api/jobs/clear-finished` | remove done+failed+cancelled jobs |
341
+ | DELETE | `/api/jobs/{id}` | remove one job (a *running* job is cancelled instead) |
342
+ | GET | `/api/jobs/{id}/report` | stage timings and generation counters |
343
+ | GET / POST | `/api/jobs/{id}/review` | read dialogue snapshot / queue line edits |
344
+ | GET | `/api/jobs/{id}/clips/{index}` | listen to a generated line (HTTP Range) |
345
+ | GET | `/api/jobs/{id}/file` | stream the produced dub/tease (HTTP Range; only under output/work dirs) |
346
+ | GET | `/api/events` | SSE stream of job/scan/log events (replay + live; `?api_key=` from browsers) |
347
+ | POST | `/api/webhooks/radarr` | Radarr webhook (Download → debounced rescan; Test → 200) |
348
+ | POST | `/api/webhooks/sonarr` | Sonarr webhook (same) |
349
+ | GET/PUT | `/api/cast` | read / save a title's voice cast (`?key=` or `?path=`/`?tmdb_id=`/`?title=`) |
350
+ | GET/PUT | `/api/plan` | read / save per-title configuration overrides |
351
+ | GET | `/api/voices` | voice list (voicebox profiles, else `dub.preset_voices`) |
352
+
353
+ The UI consumes `/api/events` via `EventSource` for live job progress and a log
354
+ tail (slow polling as a fallback). Cancelling a running job stops it between
355
+ pipeline stages and kills
356
+ any in-flight ffmpeg process. Cancelling a Voicebox wait also requests remote
357
+ cancellation; if the server cannot be reached, remote generation may continue.
358
+
359
+ ## Development
360
+
361
+ ```bash
362
+ pip install -e ".[dev]" # app + pytest/pytest-cov/ruff/mypy/httpx
363
+ python -m pytest -q # test suite (no network or *arr services needed)
364
+ python -m pytest -q --cov=doblarr --cov-report=term-missing # with coverage
365
+ ruff check . # lint
366
+ mypy doblarr/ # type check
367
+ # pre-commit install # optional: run ruff+mypy as git hooks (.pre-commit-config.yaml)
368
+ ```
369
+
370
+ ### Frontend checks
371
+
372
+ The UI uses native JavaScript modules. There is no frontend build step and Node
373
+ is needed only for development checks. Use Node 22 or newer:
374
+
375
+ ```bash
376
+ npm ci
377
+ npx playwright install chromium
378
+ npm run check # lint + API tests + browser regressions
379
+ npm test # fast API/helper tests only
380
+ npm run test:browser # settings, queue errors and title-plan browser flows
381
+ ```
382
+
383
+ Browser tests start an isolated FastAPI server with temporary configuration and
384
+ storage on port 8766; they do not use your media services or local config. They
385
+ use `.venv` when available, otherwise `python`; set `PYTHON` to choose another
386
+ interpreter. Python tests share a `client_factory` fixture in `tests/conftest.py`
387
+ for isolated API clients with automatic cleanup. Tests that exercise worker
388
+ startup use an explicit application lifespan.
389
+
390
+ For backend auto-reload during development:
391
+
392
+ ```bash
393
+ python -m uvicorn doblarr.server:create_app --factory --reload --port 6363
394
+ ```
395
+
396
+ Settings defaults belong in `ConfigModel` in `doblarr/config_schema.py`.
397
+ Presentation metadata belongs in `web/js/settings-model.js`; title plans reuse
398
+ those field definitions. Controls inherited from the design mockup that had no
399
+ backend setting have been removed. Add API calls through `api()` and shared job
400
+ reads through `getJobs()` so authentication, error handling and concurrent reads
401
+ stay consistent. Each feature controller receives navigation callbacks from
402
+ `app.js`, keeping feature imports free of circular dependencies.
403
+
404
+ CI checks Python lint/types/tests and the frontend checks on pull requests and
405
+ pushes to `dev`, `main` and `master`.
406
+
407
+ ## Roadmap
408
+
409
+ - [x] Web UI + REST API + job queue (a real *arr shell)
410
+ - [x] Library discovery from Radarr + Sonarr
411
+ - [x] Settings read/save from the UI
412
+ - [x] Plex labeling + hide (Kometa handoff) + scheduled auto-sync
413
+ - [x] Docker packaging
414
+ - [ ] **The real dub** — flip the worker to `dry_run=False` once these land:
415
+ - [x] `separate` (Demucs two-stems), `diarize` (pyannote), `whisper` transcribe
416
+ (whisperx/faster-whisper), `fit_timing` (atempo stretch), `mix`
417
+ (sidechain ducking) — `pip install doblarr[real]`
418
+ - [x] wire `ClaudeTranslator` (Anthropic Messages API, numbered-lines protocol)
419
+ - [x] voicebox running locally on `17493`
420
+ - [ ] Voices page from real diarization/cloning data
421
+ - [ ] Radarr/Sonarr/Plex webhook trigger → auto-dub new foreign titles overnight
422
+ - [ ] Borrow & re-implement the duration-matching + ducking approach proven by
423
+ [neutrinus/dubarr](https://github.com/neutrinus/dubarr) (GPL — study, don't copy)
424
+
425
+ ## License
426
+
427
+ MIT — see [LICENSE](LICENSE). voicebox is MIT; neutrinus/dubarr is GPL-3.0 (used
428
+ only as a reference to re-implement from, never copied in).
429
+
430
+
431
+ ### Share a dub recipe
432
+
433
+ Movies and episodes have a **Recipes** tab with a persistent `/recipes` route.
434
+ Export a `.dobdub` file containing saved generation settings, pronunciation rules,
435
+ character directions and voice names. Import it on the matching local title,
436
+ review its contents, choose local voices and engines, then apply it. Queue generation
437
+ separately after checking the character assignments.
438
+
439
+ Version 1 is recipe-only JSON: no audio, video, dialogue, subtitles, cloned voice
440
+ samples, credentials, or local file paths. Translation services and model locations
441
+ remain local. Different models, source cuts and speaker detection can produce different
442
+ results. Expected runtime is optional release information, not automatic verification.
443
+ See [the recipe format and API](docs/dub-recipes.md).