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.
- doblarr-0.1.0/LICENSE +21 -0
- doblarr-0.1.0/MANIFEST.in +2 -0
- doblarr-0.1.0/PKG-INFO +443 -0
- doblarr-0.1.0/README.md +412 -0
- doblarr-0.1.0/config.example.yaml +120 -0
- doblarr-0.1.0/doblarr/__init__.py +10 -0
- doblarr-0.1.0/doblarr/__main__.py +8 -0
- doblarr-0.1.0/doblarr/artifacts.py +54 -0
- doblarr-0.1.0/doblarr/auth.py +39 -0
- doblarr-0.1.0/doblarr/cache.py +48 -0
- doblarr-0.1.0/doblarr/cli.py +104 -0
- doblarr-0.1.0/doblarr/clients/__init__.py +1 -0
- doblarr-0.1.0/doblarr/clients/base.py +104 -0
- doblarr-0.1.0/doblarr/clients/plex.py +91 -0
- doblarr-0.1.0/doblarr/clients/radarr.py +26 -0
- doblarr-0.1.0/doblarr/clients/sonarr.py +32 -0
- doblarr-0.1.0/doblarr/clients/translator.py +341 -0
- doblarr-0.1.0/doblarr/clients/voicebox.py +232 -0
- doblarr-0.1.0/doblarr/config.py +161 -0
- doblarr-0.1.0/doblarr/config_schema.py +168 -0
- doblarr-0.1.0/doblarr/discovery.py +230 -0
- doblarr-0.1.0/doblarr/errors.py +48 -0
- doblarr-0.1.0/doblarr/events.py +62 -0
- doblarr-0.1.0/doblarr/ffmpeg.py +64 -0
- doblarr-0.1.0/doblarr/jobs.py +332 -0
- doblarr-0.1.0/doblarr/library_service.py +115 -0
- doblarr-0.1.0/doblarr/logging_setup.py +97 -0
- doblarr-0.1.0/doblarr/model_pool.py +34 -0
- doblarr-0.1.0/doblarr/models.py +76 -0
- doblarr-0.1.0/doblarr/pipeline.py +344 -0
- doblarr-0.1.0/doblarr/plex_labels.py +114 -0
- doblarr-0.1.0/doblarr/presets.py +19 -0
- doblarr-0.1.0/doblarr/recipes.py +166 -0
- doblarr-0.1.0/doblarr/review.py +73 -0
- doblarr-0.1.0/doblarr/routes/__init__.py +1 -0
- doblarr-0.1.0/doblarr/routes/configuration.py +32 -0
- doblarr-0.1.0/doblarr/routes/jobs.py +284 -0
- doblarr-0.1.0/doblarr/routes/library.py +64 -0
- doblarr-0.1.0/doblarr/routes/series.py +182 -0
- doblarr-0.1.0/doblarr/routes/titles.py +299 -0
- doblarr-0.1.0/doblarr/routes/voice_catalog.py +159 -0
- doblarr-0.1.0/doblarr/scheduler.py +69 -0
- doblarr-0.1.0/doblarr/server.py +175 -0
- doblarr-0.1.0/doblarr/services.py +66 -0
- doblarr-0.1.0/doblarr/stages/__init__.py +19 -0
- doblarr-0.1.0/doblarr/stages/audition.py +113 -0
- doblarr-0.1.0/doblarr/stages/common.py +174 -0
- doblarr-0.1.0/doblarr/stages/diarize.py +162 -0
- doblarr-0.1.0/doblarr/stages/extract.py +80 -0
- doblarr-0.1.0/doblarr/stages/fit_timing.py +188 -0
- doblarr-0.1.0/doblarr/stages/mix.py +227 -0
- doblarr-0.1.0/doblarr/stages/mux.py +150 -0
- doblarr-0.1.0/doblarr/stages/prepare.py +48 -0
- doblarr-0.1.0/doblarr/stages/quality.py +174 -0
- doblarr-0.1.0/doblarr/stages/separate.py +68 -0
- doblarr-0.1.0/doblarr/stages/synthesize.py +339 -0
- doblarr-0.1.0/doblarr/stages/transcribe.py +275 -0
- doblarr-0.1.0/doblarr/stages/translate.py +77 -0
- doblarr-0.1.0/doblarr/store.py +190 -0
- doblarr-0.1.0/doblarr/subtitles.py +56 -0
- doblarr-0.1.0/doblarr/telemetry.py +59 -0
- doblarr-0.1.0/doblarr/versions.py +99 -0
- doblarr-0.1.0/doblarr/voices.py +133 -0
- doblarr-0.1.0/doblarr/webhooks.py +68 -0
- doblarr-0.1.0/doblarr.egg-info/PKG-INFO +443 -0
- doblarr-0.1.0/doblarr.egg-info/SOURCES.txt +133 -0
- doblarr-0.1.0/doblarr.egg-info/dependency_links.txt +1 -0
- doblarr-0.1.0/doblarr.egg-info/entry_points.txt +2 -0
- doblarr-0.1.0/doblarr.egg-info/requires.txt +21 -0
- doblarr-0.1.0/doblarr.egg-info/top_level.txt +1 -0
- doblarr-0.1.0/pyproject.toml +80 -0
- doblarr-0.1.0/setup.cfg +4 -0
- doblarr-0.1.0/setup.py +19 -0
- doblarr-0.1.0/tests/test_audio_quality.py +78 -0
- doblarr-0.1.0/tests/test_cancellation.py +110 -0
- doblarr-0.1.0/tests/test_casts.py +194 -0
- doblarr-0.1.0/tests/test_checkpoint.py +136 -0
- doblarr-0.1.0/tests/test_clients.py +137 -0
- doblarr-0.1.0/tests/test_config_overrides.py +42 -0
- doblarr-0.1.0/tests/test_dialogue_planning.py +98 -0
- doblarr-0.1.0/tests/test_diarize.py +101 -0
- doblarr-0.1.0/tests/test_episode_runner.py +61 -0
- doblarr-0.1.0/tests/test_events.py +126 -0
- doblarr-0.1.0/tests/test_extract.py +41 -0
- doblarr-0.1.0/tests/test_fit_timing.py +103 -0
- doblarr-0.1.0/tests/test_generation_edges.py +99 -0
- doblarr-0.1.0/tests/test_generation_integration.py +121 -0
- doblarr-0.1.0/tests/test_generation_reliability.py +145 -0
- doblarr-0.1.0/tests/test_generation_throughput.py +65 -0
- doblarr-0.1.0/tests/test_jobfile.py +109 -0
- doblarr-0.1.0/tests/test_mix.py +80 -0
- doblarr-0.1.0/tests/test_mux.py +52 -0
- doblarr-0.1.0/tests/test_plex_refresh.py +144 -0
- doblarr-0.1.0/tests/test_recipes.py +178 -0
- doblarr-0.1.0/tests/test_review.py +98 -0
- doblarr-0.1.0/tests/test_script_cache.py +113 -0
- doblarr-0.1.0/tests/test_separate.py +65 -0
- doblarr-0.1.0/tests/test_series.py +119 -0
- doblarr-0.1.0/tests/test_server.py +175 -0
- doblarr-0.1.0/tests/test_smoke.py +182 -0
- doblarr-0.1.0/tests/test_spa.py +43 -0
- doblarr-0.1.0/tests/test_store.py +92 -0
- doblarr-0.1.0/tests/test_tease.py +146 -0
- doblarr-0.1.0/tests/test_telemetry.py +29 -0
- doblarr-0.1.0/tests/test_transcribe_whisper.py +138 -0
- doblarr-0.1.0/tests/test_translation.py +200 -0
- doblarr-0.1.0/tests/test_versions.py +59 -0
- doblarr-0.1.0/tests/test_voice_catalog.py +54 -0
- doblarr-0.1.0/tests/test_webhooks.py +92 -0
- doblarr-0.1.0/web/android-chrome-192x192.png +0 -0
- doblarr-0.1.0/web/android-chrome-512x512.png +0 -0
- doblarr-0.1.0/web/apple-touch-icon.png +0 -0
- doblarr-0.1.0/web/favicon-16x16.png +0 -0
- doblarr-0.1.0/web/favicon-32x32.png +0 -0
- doblarr-0.1.0/web/favicon.ico +0 -0
- doblarr-0.1.0/web/index.html +383 -0
- doblarr-0.1.0/web/js/api.js +46 -0
- doblarr-0.1.0/web/js/app.js +339 -0
- doblarr-0.1.0/web/js/dom.js +21 -0
- doblarr-0.1.0/web/js/episodes.js +76 -0
- doblarr-0.1.0/web/js/identity.js +24 -0
- doblarr-0.1.0/web/js/jobs-data.js +9 -0
- doblarr-0.1.0/web/js/jobs.js +240 -0
- doblarr-0.1.0/web/js/library.js +147 -0
- doblarr-0.1.0/web/js/recipes.js +119 -0
- doblarr-0.1.0/web/js/review.js +127 -0
- doblarr-0.1.0/web/js/settings-model.js +118 -0
- doblarr-0.1.0/web/js/settings.js +112 -0
- doblarr-0.1.0/web/js/state.js +15 -0
- doblarr-0.1.0/web/js/title-routing.js +24 -0
- doblarr-0.1.0/web/js/title.js +504 -0
- doblarr-0.1.0/web/js/voice-picker.js +99 -0
- doblarr-0.1.0/web/logo.png +0 -0
- doblarr-0.1.0/web/site.webmanifest +20 -0
- 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.
|
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).
|