miragea-dub 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.
- miragea_dub-0.1.0/.gitignore +7 -0
- miragea_dub-0.1.0/CHANGELOG.md +7 -0
- miragea_dub-0.1.0/LICENSE +21 -0
- miragea_dub-0.1.0/PKG-INFO +159 -0
- miragea_dub-0.1.0/README.md +120 -0
- miragea_dub-0.1.0/docs/make_images.py +185 -0
- miragea_dub-0.1.0/llms.txt +15 -0
- miragea_dub-0.1.0/pyproject.toml +63 -0
- miragea_dub-0.1.0/src/miragea_dub/__init__.py +3 -0
- miragea_dub-0.1.0/src/miragea_dub/__main__.py +3 -0
- miragea_dub-0.1.0/src/miragea_dub/cli.py +240 -0
- miragea_dub-0.1.0/src/miragea_dub/client.py +225 -0
- miragea_dub-0.1.0/src/miragea_dub/config.py +56 -0
- miragea_dub-0.1.0/src/miragea_dub/demo.py +66 -0
- miragea_dub-0.1.0/src/miragea_dub/diarize.py +252 -0
- miragea_dub-0.1.0/src/miragea_dub/errors.py +121 -0
- miragea_dub-0.1.0/src/miragea_dub/mcp_server.py +153 -0
- miragea_dub-0.1.0/src/miragea_dub/media.py +173 -0
- miragea_dub-0.1.0/src/miragea_dub/mix.py +151 -0
- miragea_dub-0.1.0/src/miragea_dub/pipeline.py +253 -0
- miragea_dub-0.1.0/src/miragea_dub/project.py +172 -0
- miragea_dub-0.1.0/src/miragea_dub/sentences.py +55 -0
- miragea_dub-0.1.0/src/miragea_dub/subtitles.py +60 -0
- miragea_dub-0.1.0/src/miragea_dub/transcribe.py +121 -0
- miragea_dub-0.1.0/src/miragea_dub/translate.py +141 -0
- miragea_dub-0.1.0/src/miragea_dub/voices.py +368 -0
- miragea_dub-0.1.0/tests/conftest.py +97 -0
- miragea_dub-0.1.0/tests/test_core.py +297 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-11-11
|
|
4
|
+
- First release: prepare / translate / render / dub CLI, MCP server (6 tools), Agent Skill.
|
|
5
|
+
- Miragea transcription regrouped into whole sentences; built-in GPU-free diarization; per-speaker voice cloning from the video.
|
|
6
|
+
- Timing fit with speed limit and shorten report; ducked, file, Demucs or removed background; dubbed MP4, M4A, SRT, VTT.
|
|
7
|
+
- Reference hosting with automatic fallback (litterbox, uguu, tmpfiles).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 miragea-dub contributors
|
|
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,159 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: miragea-dub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source multi-speaker video dubbing: transcribe, find who speaks when, translate, and re-voice every speaker with their own cloned voice, with timing fit, ducked background, dubbed MP4, multi-language audio track and SRT/VTT. CLI + MCP server + Agent Skill, voiced by Miragea.
|
|
5
|
+
Project-URL: Homepage, https://codeberg.org/miragea-voice/miragea-dub
|
|
6
|
+
Project-URL: Documentation, https://codeberg.org/miragea-voice/miragea-dub#readme
|
|
7
|
+
Project-URL: Issues, https://codeberg.org/miragea-voice/miragea-dub/issues
|
|
8
|
+
Project-URL: Miragea API, https://miragea.ai
|
|
9
|
+
Author: miragea-dub contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-skill,ai-dubbing,ai-voice-over,dubbing,elevenlabs-dubbing-alternative,heygen-alternative,localization,mcp,mcp-server,miragea,multi-speaker,speaker-diarization,srt,subtitles,text-to-speech,video-dubbing,video-translation,voice-cloning,youtube-multi-language-audio
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: anyio>=4
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: mcp>=1.9
|
|
27
|
+
Requires-Dist: numpy>=1.26
|
|
28
|
+
Requires-Dist: pydantic>=2
|
|
29
|
+
Provides-Extra: demucs
|
|
30
|
+
Requires-Dist: demucs>=4; extra == 'demucs'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
34
|
+
Provides-Extra: pyannote
|
|
35
|
+
Requires-Dist: pyannote-audio>=3.1; extra == 'pyannote'
|
|
36
|
+
Provides-Extra: whisper
|
|
37
|
+
Requires-Dist: faster-whisper>=1.0; extra == 'whisper'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/docs/images/cover.png" alt="miragea-dub banner: two speakers' lanes in English above the same two lanes dubbed into Spanish, each speaker keeping their own voice" width="100%">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
# miragea-dub: multi-speaker AI video dubbing, open source
|
|
45
|
+
|
|
46
|
+
**miragea-dub is an open-source command-line tool, MCP server and Agent Skill that dubs a video into another language while keeping a separate cloned voice for every speaker.** It transcribes the video, works out who speaks when, lets you (or your AI agent) translate each line, clones every speaker's voice from the video itself with the [Miragea](https://miragea.ai) voice API, fits each translated line into the original timing, ducks the original audio underneath, and exports a dubbed MP4, a stand-alone audio track for YouTube multi-language audio, and SRT/VTT subtitles. MIT licensed, Python 3.10+, works offline in `demo` mode.
|
|
47
|
+
|
|
48
|
+
[](LICENSE) [](pyproject.toml) [](tests)
|
|
49
|
+
|
|
50
|
+
## Demo: a real two-speaker dub
|
|
51
|
+
|
|
52
|
+
An English clip with two speakers (Ana and Ben) dubbed into Spanish. Both voices were cloned from the clip itself, nothing was pre-recorded:
|
|
53
|
+
|
|
54
|
+
- [Watch the dubbed MP4 (Spanish, burned-in subtitles, original English kept as 2nd audio track)](https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/examples/demo-dubbed-es.mp4)
|
|
55
|
+
- [Original English audio](https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/examples/real/original-audio.mp3)
|
|
56
|
+
- [Spanish subtitles](examples/output/two-voices.es.srt) and [timing report](examples/output/two-voices.es.report.json)
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
Round-trip check: transcribing the Spanish track again returns the translated script almost word for word, and the diarizer assigned all 6 lines to the right speaker.
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pipx install miragea-dub # or: uvx miragea-dub ... (needs ffmpeg on PATH)
|
|
66
|
+
miragea-dub demo # offline end-to-end run with test tones, no key needed
|
|
67
|
+
export MIRAGEA_API_KEY=... # create a key at https://miragea.ai/workspace/api
|
|
68
|
+
miragea-dub doctor # checks ffmpeg, key, connectivity
|
|
69
|
+
|
|
70
|
+
# 1) transcribe + find speakers -> dub.json
|
|
71
|
+
miragea-dub prepare interview.mp4 --to es --speakers 2
|
|
72
|
+
# 2) translate: let your AI agent fill "translation" in dub.json, or use any OpenAI-compatible LLM
|
|
73
|
+
miragea-dub translate interview.dub/dub.json --translator openai
|
|
74
|
+
# 3) voice, fit, mix, export
|
|
75
|
+
miragea-dub render interview.dub/dub.json --model expressive
|
|
76
|
+
# or all in one: miragea-dub dub interview.mp4 --to es --translator openai
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Outputs next to the video: `interview.es.mp4` (dub track + original track + subtitles), `interview.es.m4a`, `.srt`, `.vtt` and `interview.es.report.json`.
|
|
80
|
+
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
1. **Transcribe**: Miragea speech-to-text (no GPU), local `faster-whisper`, or your own SRT/VTT/JSON. Miragea returns subtitle cues cut at a fixed width, so cues are regrouped into whole sentences using the punctuated full text.
|
|
86
|
+
2. **Diarize**: a built-in, GPU-free speaker clustering on voice features (or `pyannote`, or an RTTM file). Speaker tags already in your transcript (`[Ana] ...`, `S2: ...`) are kept.
|
|
87
|
+
3. **Translate**: every line gets a length budget so the translation can be spoken inside the original time slot. Backends: `agent` (your AI agent edits `dub.json`), `openai` (any OpenAI-compatible endpoint), `file`, `copy`.
|
|
88
|
+
4. **Clone and voice**: an 8-20 second reference clip is cut per speaker from the video, hosted on a temporary public link, and each line is synthesized with that voice through the Miragea API. Requests are idempotent and cached, so a re-run never pays twice.
|
|
89
|
+
5. **Fit and mix**: lines are sped up (up to 1.35x by default) to fit, the original audio is ducked under speech (or removed, or replaced by your music stem, or separated with Demucs), then everything is muxed.
|
|
90
|
+
|
|
91
|
+
### Real timing report
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
Lines that could not fit even at the speed limit are listed in `lines_to_shorten`; shorten them in `dub.json` and run `render` again (only changed lines are re-voiced).
|
|
98
|
+
|
|
99
|
+
## Use it from an AI agent
|
|
100
|
+
|
|
101
|
+
**MCP server** (Claude Desktop, Claude Code, Cursor, VS Code, Windsurf):
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{ "mcpServers": { "miragea-dub": { "command": "uvx", "args": ["--from", "miragea-dub", "miragea-dub-mcp"],
|
|
105
|
+
"env": { "MIRAGEA_API_KEY": "your-key" } } } }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Tools: `prepare_dub`, `save_translations`, `show_project`, `estimate_dub`, `render_dub`, `list_voices`. The agent is the translator, so no second LLM key is needed.
|
|
109
|
+
|
|
110
|
+
**Agent Skill**: install `video-dubbing` from [miragea-skills](https://codeberg.org/miragea-voice/miragea-skills), or tell your agent: `Install https://miragea.ai/skills/miragea-audio/install.md`.
|
|
111
|
+
|
|
112
|
+
## How it compares
|
|
113
|
+
|
|
114
|
+
| | miragea-dub | [VideoLingo](https://github.com/Huanshere/VideoLingo) | [pyvideotrans](https://github.com/jianchang512/pyvideotrans) | Hosted dubbing apps |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
| License | MIT | Apache-2.0 | GPL-3.0 | closed SaaS |
|
|
117
|
+
| Interface | CLI + MCP + Agent Skill | Streamlit app | desktop GUI + CLI | web app |
|
|
118
|
+
| Separate cloned voice per speaker | yes (from the video itself) | depends on TTS backend | depends on backend | yes |
|
|
119
|
+
| Runs without a GPU | yes (cloud ASR + voices) | local models recommended | optional | n/a |
|
|
120
|
+
| Your AI agent can drive it | yes (MCP, skill) | no | no | usually no |
|
|
121
|
+
| Editable project file between steps | `dub.json` | partly | partly | web editor |
|
|
122
|
+
| Speaker count / timing report | yes | no | no | varies |
|
|
123
|
+
|
|
124
|
+
Star counts at time of writing: VideoLingo 19.1k, pyvideotrans 19.7k. Those projects are bigger and have GUIs; miragea-dub is the small, scriptable, agent-friendly option.
|
|
125
|
+
|
|
126
|
+
## FAQ
|
|
127
|
+
|
|
128
|
+
**How do I dub a video into another language with AI?** Install miragea-dub, run `miragea-dub dub video.mp4 --to es --translator openai`, and you get a dubbed MP4 plus subtitles.
|
|
129
|
+
|
|
130
|
+
**Can I dub a video with two or more speakers and keep each voice?** Yes. Speakers are separated by diarization (or by tags in your own transcript) and each one is cloned from their own lines.
|
|
131
|
+
|
|
132
|
+
**Does it work for a YouTube video with multiple audio tracks?** The `.m4a` output is the stand-alone dubbed track you can upload as an additional language audio track; the `.mp4` already carries dub and original tracks.
|
|
133
|
+
|
|
134
|
+
**Is there a free open-source alternative to ElevenLabs or HeyGen dubbing?** The pipeline, diarization, timing fit and mixing are free and MIT; the voices come from the Miragea API, which needs a key.
|
|
135
|
+
|
|
136
|
+
**Can an AI agent (Claude, Cursor, Codex) dub videos?** Yes, through the MCP server or the `video-dubbing` skill. The agent writes the translations itself.
|
|
137
|
+
|
|
138
|
+
**How do I keep the dubbed speech from running longer than the original?** Every line gets a word or character budget before translation, long lines are sped up to the limit, and what still does not fit is reported for shortening.
|
|
139
|
+
|
|
140
|
+
**Does it keep background music?** By default the original audio is ducked under speech. Use `--background file` with a music stem, `demucs` to separate stems, or `remove`.
|
|
141
|
+
|
|
142
|
+
**Which languages?** Anything your translator and the Miragea voices handle; the repo's own example is English to Spanish.
|
|
143
|
+
|
|
144
|
+
## Troubleshooting
|
|
145
|
+
|
|
146
|
+
- `litterbox upload failed (HTTP 403)`: the default `--ref-host auto` already falls back to other free hosts; or use `--ref-host "cmd:your-upload-command {path}"`, or `prefix:` with `--ref-dir` for your own server.
|
|
147
|
+
- Wrong speaker count: pass `--speakers N`, or supply `--transcript` with `[Name]` tags.
|
|
148
|
+
- A line sounds rushed: shorten its `translation` in `dub.json` and run `render` again.
|
|
149
|
+
- `401`: create a key at https://miragea.ai/workspace/api and `export MIRAGEA_API_KEY`.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
uv venv && uv pip install -e ".[dev]"
|
|
155
|
+
python -m pytest # 28 tests, offline: Miragea API is mocked with httpx.MockTransport
|
|
156
|
+
python docs/make_images.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Base URL defaults to `https://miragea.ai/v1` (override with `MIRAGEA_BASE_URL`). See [CHANGELOG](CHANGELOG.md) and [llms.txt](llms.txt). MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/docs/images/cover.png" alt="miragea-dub banner: two speakers' lanes in English above the same two lanes dubbed into Spanish, each speaker keeping their own voice" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# miragea-dub: multi-speaker AI video dubbing, open source
|
|
6
|
+
|
|
7
|
+
**miragea-dub is an open-source command-line tool, MCP server and Agent Skill that dubs a video into another language while keeping a separate cloned voice for every speaker.** It transcribes the video, works out who speaks when, lets you (or your AI agent) translate each line, clones every speaker's voice from the video itself with the [Miragea](https://miragea.ai) voice API, fits each translated line into the original timing, ducks the original audio underneath, and exports a dubbed MP4, a stand-alone audio track for YouTube multi-language audio, and SRT/VTT subtitles. MIT licensed, Python 3.10+, works offline in `demo` mode.
|
|
8
|
+
|
|
9
|
+
[](LICENSE) [](pyproject.toml) [](tests)
|
|
10
|
+
|
|
11
|
+
## Demo: a real two-speaker dub
|
|
12
|
+
|
|
13
|
+
An English clip with two speakers (Ana and Ben) dubbed into Spanish. Both voices were cloned from the clip itself, nothing was pre-recorded:
|
|
14
|
+
|
|
15
|
+
- [Watch the dubbed MP4 (Spanish, burned-in subtitles, original English kept as 2nd audio track)](https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/examples/demo-dubbed-es.mp4)
|
|
16
|
+
- [Original English audio](https://codeberg.org/miragea-voice/miragea-dub/raw/branch/main/examples/real/original-audio.mp3)
|
|
17
|
+
- [Spanish subtitles](examples/output/two-voices.es.srt) and [timing report](examples/output/two-voices.es.report.json)
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
Round-trip check: transcribing the Spanish track again returns the translated script almost word for word, and the diarizer assigned all 6 lines to the right speaker.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pipx install miragea-dub # or: uvx miragea-dub ... (needs ffmpeg on PATH)
|
|
27
|
+
miragea-dub demo # offline end-to-end run with test tones, no key needed
|
|
28
|
+
export MIRAGEA_API_KEY=... # create a key at https://miragea.ai/workspace/api
|
|
29
|
+
miragea-dub doctor # checks ffmpeg, key, connectivity
|
|
30
|
+
|
|
31
|
+
# 1) transcribe + find speakers -> dub.json
|
|
32
|
+
miragea-dub prepare interview.mp4 --to es --speakers 2
|
|
33
|
+
# 2) translate: let your AI agent fill "translation" in dub.json, or use any OpenAI-compatible LLM
|
|
34
|
+
miragea-dub translate interview.dub/dub.json --translator openai
|
|
35
|
+
# 3) voice, fit, mix, export
|
|
36
|
+
miragea-dub render interview.dub/dub.json --model expressive
|
|
37
|
+
# or all in one: miragea-dub dub interview.mp4 --to es --translator openai
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Outputs next to the video: `interview.es.mp4` (dub track + original track + subtitles), `interview.es.m4a`, `.srt`, `.vtt` and `interview.es.report.json`.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
1. **Transcribe**: Miragea speech-to-text (no GPU), local `faster-whisper`, or your own SRT/VTT/JSON. Miragea returns subtitle cues cut at a fixed width, so cues are regrouped into whole sentences using the punctuated full text.
|
|
47
|
+
2. **Diarize**: a built-in, GPU-free speaker clustering on voice features (or `pyannote`, or an RTTM file). Speaker tags already in your transcript (`[Ana] ...`, `S2: ...`) are kept.
|
|
48
|
+
3. **Translate**: every line gets a length budget so the translation can be spoken inside the original time slot. Backends: `agent` (your AI agent edits `dub.json`), `openai` (any OpenAI-compatible endpoint), `file`, `copy`.
|
|
49
|
+
4. **Clone and voice**: an 8-20 second reference clip is cut per speaker from the video, hosted on a temporary public link, and each line is synthesized with that voice through the Miragea API. Requests are idempotent and cached, so a re-run never pays twice.
|
|
50
|
+
5. **Fit and mix**: lines are sped up (up to 1.35x by default) to fit, the original audio is ducked under speech (or removed, or replaced by your music stem, or separated with Demucs), then everything is muxed.
|
|
51
|
+
|
|
52
|
+
### Real timing report
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
Lines that could not fit even at the speed limit are listed in `lines_to_shorten`; shorten them in `dub.json` and run `render` again (only changed lines are re-voiced).
|
|
59
|
+
|
|
60
|
+
## Use it from an AI agent
|
|
61
|
+
|
|
62
|
+
**MCP server** (Claude Desktop, Claude Code, Cursor, VS Code, Windsurf):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "mcpServers": { "miragea-dub": { "command": "uvx", "args": ["--from", "miragea-dub", "miragea-dub-mcp"],
|
|
66
|
+
"env": { "MIRAGEA_API_KEY": "your-key" } } } }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Tools: `prepare_dub`, `save_translations`, `show_project`, `estimate_dub`, `render_dub`, `list_voices`. The agent is the translator, so no second LLM key is needed.
|
|
70
|
+
|
|
71
|
+
**Agent Skill**: install `video-dubbing` from [miragea-skills](https://codeberg.org/miragea-voice/miragea-skills), or tell your agent: `Install https://miragea.ai/skills/miragea-audio/install.md`.
|
|
72
|
+
|
|
73
|
+
## How it compares
|
|
74
|
+
|
|
75
|
+
| | miragea-dub | [VideoLingo](https://github.com/Huanshere/VideoLingo) | [pyvideotrans](https://github.com/jianchang512/pyvideotrans) | Hosted dubbing apps |
|
|
76
|
+
|---|---|---|---|---|
|
|
77
|
+
| License | MIT | Apache-2.0 | GPL-3.0 | closed SaaS |
|
|
78
|
+
| Interface | CLI + MCP + Agent Skill | Streamlit app | desktop GUI + CLI | web app |
|
|
79
|
+
| Separate cloned voice per speaker | yes (from the video itself) | depends on TTS backend | depends on backend | yes |
|
|
80
|
+
| Runs without a GPU | yes (cloud ASR + voices) | local models recommended | optional | n/a |
|
|
81
|
+
| Your AI agent can drive it | yes (MCP, skill) | no | no | usually no |
|
|
82
|
+
| Editable project file between steps | `dub.json` | partly | partly | web editor |
|
|
83
|
+
| Speaker count / timing report | yes | no | no | varies |
|
|
84
|
+
|
|
85
|
+
Star counts at time of writing: VideoLingo 19.1k, pyvideotrans 19.7k. Those projects are bigger and have GUIs; miragea-dub is the small, scriptable, agent-friendly option.
|
|
86
|
+
|
|
87
|
+
## FAQ
|
|
88
|
+
|
|
89
|
+
**How do I dub a video into another language with AI?** Install miragea-dub, run `miragea-dub dub video.mp4 --to es --translator openai`, and you get a dubbed MP4 plus subtitles.
|
|
90
|
+
|
|
91
|
+
**Can I dub a video with two or more speakers and keep each voice?** Yes. Speakers are separated by diarization (or by tags in your own transcript) and each one is cloned from their own lines.
|
|
92
|
+
|
|
93
|
+
**Does it work for a YouTube video with multiple audio tracks?** The `.m4a` output is the stand-alone dubbed track you can upload as an additional language audio track; the `.mp4` already carries dub and original tracks.
|
|
94
|
+
|
|
95
|
+
**Is there a free open-source alternative to ElevenLabs or HeyGen dubbing?** The pipeline, diarization, timing fit and mixing are free and MIT; the voices come from the Miragea API, which needs a key.
|
|
96
|
+
|
|
97
|
+
**Can an AI agent (Claude, Cursor, Codex) dub videos?** Yes, through the MCP server or the `video-dubbing` skill. The agent writes the translations itself.
|
|
98
|
+
|
|
99
|
+
**How do I keep the dubbed speech from running longer than the original?** Every line gets a word or character budget before translation, long lines are sped up to the limit, and what still does not fit is reported for shortening.
|
|
100
|
+
|
|
101
|
+
**Does it keep background music?** By default the original audio is ducked under speech. Use `--background file` with a music stem, `demucs` to separate stems, or `remove`.
|
|
102
|
+
|
|
103
|
+
**Which languages?** Anything your translator and the Miragea voices handle; the repo's own example is English to Spanish.
|
|
104
|
+
|
|
105
|
+
## Troubleshooting
|
|
106
|
+
|
|
107
|
+
- `litterbox upload failed (HTTP 403)`: the default `--ref-host auto` already falls back to other free hosts; or use `--ref-host "cmd:your-upload-command {path}"`, or `prefix:` with `--ref-dir` for your own server.
|
|
108
|
+
- Wrong speaker count: pass `--speakers N`, or supply `--transcript` with `[Name]` tags.
|
|
109
|
+
- A line sounds rushed: shorten its `translation` in `dub.json` and run `render` again.
|
|
110
|
+
- `401`: create a key at https://miragea.ai/workspace/api and `export MIRAGEA_API_KEY`.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uv venv && uv pip install -e ".[dev]"
|
|
116
|
+
python -m pytest # 28 tests, offline: Miragea API is mocked with httpx.MockTransport
|
|
117
|
+
python docs/make_images.py
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Base URL defaults to `https://miragea.ai/v1` (override with `MIRAGEA_BASE_URL`). See [CHANGELOG](CHANGELOG.md) and [llms.txt](llms.txt). MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Render README images from the REAL example run in examples/output (python3 docs/make_images.py)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
OUT = ROOT / "docs" / "images"
|
|
11
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
F = "/usr/share/fonts/truetype/dejavu/"
|
|
13
|
+
SANS, BOLD, MONO, MONOB = F + "DejaVuSans.ttf", F + "DejaVuSans-Bold.ttf", F + "DejaVuSansMono.ttf", F + "DejaVuSansMono-Bold.ttf"
|
|
14
|
+
BG, PANEL = (15, 17, 26), (27, 31, 46)
|
|
15
|
+
ACCENT, ACCENT2 = (252, 163, 17), (34, 211, 238)
|
|
16
|
+
TEXT, MUTED, GREEN, PINK = (236, 238, 245), (150, 158, 180), (74, 222, 128), (244, 114, 182)
|
|
17
|
+
SPK = {"S1": PINK, "S2": ACCENT2}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def font(p, s):
|
|
21
|
+
return ImageFont.truetype(p, s)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def gradient(w, h, top, bottom):
|
|
25
|
+
img = Image.new("RGB", (w, h), top)
|
|
26
|
+
d = ImageDraw.Draw(img)
|
|
27
|
+
for y in range(h):
|
|
28
|
+
t = y / max(1, h - 1)
|
|
29
|
+
d.line([(0, y), (w, y)], fill=tuple(int(a + (b - a) * t) for a, b in zip(top, bottom)))
|
|
30
|
+
return img
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cover():
|
|
34
|
+
w, h = 1280, 540
|
|
35
|
+
img = gradient(w, h, (20, 33, 61), (9, 12, 22))
|
|
36
|
+
d = ImageDraw.Draw(img)
|
|
37
|
+
d.text((64, 56), "OPEN-SOURCE \u00b7 CLI + MCP SERVER + AGENT SKILL", font=font(BOLD, 20), fill=ACCENT)
|
|
38
|
+
d.text((64, 96), "miragea-dub", font=font(BOLD, 68), fill=TEXT)
|
|
39
|
+
d.text((64, 184), "Dub a video into another language,", font=font(SANS, 30), fill=TEXT)
|
|
40
|
+
d.text((64, 224), "keeping a separate cloned voice per speaker", font=font(SANS, 30), fill=TEXT)
|
|
41
|
+
lanes = [("EN", "Ana", PINK, [(0.0, .1), (.25, .12), (.55, .22)]), ("EN", "Ben", ACCENT2, [(.12, .13), (.45, .1)])]
|
|
42
|
+
d.text((64, 300), "ORIGINAL", font=font(BOLD, 15), fill=MUTED)
|
|
43
|
+
for i, (_, name, col, spans) in enumerate(lanes):
|
|
44
|
+
y = 324 + i * 44
|
|
45
|
+
d.text((64, y + 8), name, font=font(BOLD, 18), fill=col)
|
|
46
|
+
d.rounded_rectangle([130, y, 640, y + 34], radius=8, fill=PANEL)
|
|
47
|
+
for a, b in spans:
|
|
48
|
+
d.rounded_rectangle([140 + a * 480, y + 6, 140 + (a + b) * 480 + 40, y + 28], radius=6, fill=col)
|
|
49
|
+
d.text((700, 300), "DUBBED (ES)", font=font(BOLD, 15), fill=MUTED)
|
|
50
|
+
for i, (_, name, col, spans) in enumerate(lanes):
|
|
51
|
+
y = 324 + i * 44
|
|
52
|
+
d.rounded_rectangle([700, y, 1216, y + 34], radius=8, fill=PANEL)
|
|
53
|
+
for a, b in spans:
|
|
54
|
+
d.rounded_rectangle([710 + a * 480, y + 6, 710 + (a + b) * 480 + 40, y + 28], radius=6, fill=col)
|
|
55
|
+
d.text((64, 450), "pipx install miragea-dub", font=font(MONOB, 24), fill=GREEN)
|
|
56
|
+
d.text((1216, 456), "voices: Miragea API", font=font(SANS, 18), fill=MUTED, anchor="ra")
|
|
57
|
+
img.save(OUT / "cover.png", optimize=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def box(d, xy, title, lines, color):
|
|
61
|
+
d.rounded_rectangle(xy, radius=16, fill=PANEL, outline=color, width=3)
|
|
62
|
+
x0, y0, x1, _ = xy
|
|
63
|
+
d.text(((x0 + x1) / 2, y0 + 18), title, font=font(BOLD, 21), fill=TEXT, anchor="ma")
|
|
64
|
+
for i, ln in enumerate(lines):
|
|
65
|
+
d.text(((x0 + x1) / 2, y0 + 58 + i * 27), ln, font=font(SANS, 16), fill=MUTED, anchor="ma")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def arrow(d, x0, y, x1, color):
|
|
69
|
+
d.line([(x0, y), (x1 - 4, y)], fill=color, width=4)
|
|
70
|
+
d.polygon([(x1, y), (x1 - 14, y - 9), (x1 - 14, y + 9)], fill=color)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def flow():
|
|
74
|
+
w, h = 1500, 560
|
|
75
|
+
img = gradient(w, h, (16, 18, 28), (11, 12, 19))
|
|
76
|
+
d = ImageDraw.Draw(img)
|
|
77
|
+
d.text((w / 2, 30), "How miragea-dub dubs a video", font=font(BOLD, 30), fill=TEXT, anchor="ma")
|
|
78
|
+
steps = [("1. Transcribe", ["Miragea ASR, whisper", "or your SRT/VTT", "-> whole sentences"], ACCENT),
|
|
79
|
+
("2. Diarize", ["who speaks when", "built-in, no GPU", "(or pyannote / RTTM)"], ACCENT2),
|
|
80
|
+
("3. Translate", ["your AI agent, any", "OpenAI-style LLM,", "or a file; length budgets"], ACCENT),
|
|
81
|
+
("4. Clone + voice", ["8-20 s reference per", "speaker, Miragea API,", "parallel, cached"], PINK),
|
|
82
|
+
("5. Fit + mix", ["speed to fit each slot,", "duck the original,", "mux tracks + subs"], ACCENT2)]
|
|
83
|
+
bw, gap, y0 = 250, 45, 110
|
|
84
|
+
for i, (t, lines, col) in enumerate(steps):
|
|
85
|
+
x = 40 + i * (bw + gap)
|
|
86
|
+
box(d, [x, y0, x + bw, y0 + 180], t, lines, col)
|
|
87
|
+
if i < len(steps) - 1:
|
|
88
|
+
arrow(d, x + bw + 4, y0 + 90, x + bw + gap - 2, MUTED)
|
|
89
|
+
d.line([(w / 2, y0 + 180), (w / 2, 360)], fill=GREEN, width=4)
|
|
90
|
+
d.polygon([(w / 2, 362), (w / 2 - 9, 348), (w / 2 + 9, 348)], fill=GREEN)
|
|
91
|
+
d.rounded_rectangle([250, 366, 1250, 500], radius=16, fill=(20, 40, 34), outline=GREEN, width=3)
|
|
92
|
+
d.text((w / 2, 380), "Files you can publish", font=font(BOLD, 22), fill=TEXT, anchor="ma")
|
|
93
|
+
d.text((w / 2, 420), "video.es.mp4 (dub + original audio + subtitles) video.es.m4a .srt .vtt",
|
|
94
|
+
font=font(MONO, 16), fill=GREEN, anchor="ma")
|
|
95
|
+
d.text((w / 2, 450), "video.es.report.json: which lines were sped up or need shortening", font=font(MONO, 16),
|
|
96
|
+
fill=GREEN, anchor="ma")
|
|
97
|
+
d.text((40, 524), "Each step writes dub.json, so you can edit a line and re-render; finished lines are never paid twice.",
|
|
98
|
+
font=font(SANS, 16), fill=MUTED)
|
|
99
|
+
img.save(OUT / "flow.png", optimize=True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def terminal(name, title, lines, width=1360):
|
|
103
|
+
m = font(MONO, 19)
|
|
104
|
+
lh = 30
|
|
105
|
+
h = 70 + lh * len(lines) + 26
|
|
106
|
+
img = Image.new("RGB", (width, h), (12, 14, 22))
|
|
107
|
+
d = ImageDraw.Draw(img)
|
|
108
|
+
d.rounded_rectangle([0, 0, width - 1, h - 1], radius=14, fill=(22, 25, 36), outline=(52, 58, 80), width=2)
|
|
109
|
+
d.rectangle([2, 40, width - 3, h - 3], fill=(13, 15, 23))
|
|
110
|
+
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
|
|
111
|
+
d.ellipse([20 + i * 26, 13, 34 + i * 26, 27], fill=c)
|
|
112
|
+
d.text((width / 2, 11), title, font=font(SANS, 16), fill=MUTED, anchor="ma")
|
|
113
|
+
y = 58
|
|
114
|
+
for ln in lines:
|
|
115
|
+
x = 24
|
|
116
|
+
for seg, col in ln:
|
|
117
|
+
d.text((x, y), seg, font=m, fill=col)
|
|
118
|
+
x += d.textlength(seg, font=m)
|
|
119
|
+
y += lh
|
|
120
|
+
img.save(OUT / name, optimize=True)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def P(s, c=TEXT):
|
|
124
|
+
return (s, c)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def terminals():
|
|
128
|
+
rep = json.loads((ROOT / "examples/output/two-voices.es.report.json").read_text())
|
|
129
|
+
terminal("terminal-dub.png", "miragea-dub dub (real run, English -> Spanish, 2 speakers)", [
|
|
130
|
+
[P("$ ", GREEN), P("miragea-dub dub two-voices.mp4 --to es --speakers 2 --translator file \\")],
|
|
131
|
+
[P(" --translations two-voices.es.translations.json --model expressive")],
|
|
132
|
+
[P("\u203a Transcript: 6 lines, language en", MUTED)],
|
|
133
|
+
[P("\u203a Speakers: 2", MUTED)],
|
|
134
|
+
[P("\u203a Reference clip for S1: 10.0s from 2 line(s) \u203a Hosted reference for S1", MUTED)],
|
|
135
|
+
[P("\u203a Reference clip for S2: 9.2s from 2 line(s) \u203a Hosted reference for S2", MUTED)],
|
|
136
|
+
[P("\u203a Voicing 6 lines for 2 speaker(s) with miragea", MUTED)],
|
|
137
|
+
[P("\u203a Done: 6 lines, 4 sped up, 0 trimmed", MUTED)],
|
|
138
|
+
[P("status: ", ACCENT2), P("done", GREEN), P(" speakers: ", ACCENT2), P(str(rep["speakers"]))],
|
|
139
|
+
[P("max_speed_used: ", ACCENT2), P(str(rep["max_speed_used"])), P(" lines_to_shorten: ", ACCENT2), P("[]", GREEN)],
|
|
140
|
+
[P("video: ", ACCENT2), P("two-voices.es.mp4"), P(" srt/vtt: ", ACCENT2), P("two-voices.es.srt / .vtt")],
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def timeline():
|
|
145
|
+
rep = json.loads((ROOT / "examples/output/two-voices.es.report.json").read_text())
|
|
146
|
+
total = max(f["placed_end"] for f in rep["fit"]) + 0.5
|
|
147
|
+
w, h = 1480, 400
|
|
148
|
+
img = gradient(w, h, (16, 18, 28), (11, 12, 19))
|
|
149
|
+
d = ImageDraw.Draw(img)
|
|
150
|
+
d.text((40, 24), "Real timing report: every Spanish line placed in the original slot", font=font(BOLD, 24), fill=TEXT)
|
|
151
|
+
x0, span = 140, 1300
|
|
152
|
+
for i, spk in enumerate(["S1", "S2"]):
|
|
153
|
+
y = 90 + i * 100
|
|
154
|
+
d.text((40, y + 22), spk, font=font(BOLD, 22), fill=SPK[spk])
|
|
155
|
+
d.rounded_rectangle([x0, y, x0 + span, y + 70], radius=10, fill=PANEL)
|
|
156
|
+
for f in rep["fit"]:
|
|
157
|
+
if f["speaker"] != spk:
|
|
158
|
+
continue
|
|
159
|
+
a = x0 + span * f["start"] / total
|
|
160
|
+
slot = x0 + span * (f["start"] + f["slot"]) / total
|
|
161
|
+
end = x0 + span * f["placed_end"] / total
|
|
162
|
+
d.rounded_rectangle([a, y + 8, slot, y + 30], radius=5, outline=MUTED, width=2)
|
|
163
|
+
d.rounded_rectangle([a, y + 38, end, y + 62], radius=5, fill=SPK[spk])
|
|
164
|
+
d.text((a + 6, y + 40), f"#{f['id']} x{f['speed']:.2f}", font=font(SANS, 13), fill=BG)
|
|
165
|
+
d.text((40, 310), "Outlined bar: the slot the original line occupied. Solid bar: the dubbed line after fitting (x = speed-up).",
|
|
166
|
+
font=font(SANS, 16), fill=MUTED)
|
|
167
|
+
d.text((40, 340), f"{len(rep['fit'])} lines, max speed-up {rep['max_speed_used']}, {rep['trimmed']} trimmed. "
|
|
168
|
+
"Source: examples/output/two-voices.es.report.json", font=font(SANS, 16), fill=MUTED)
|
|
169
|
+
img.save(OUT / "timeline.png", optimize=True)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def gif():
|
|
173
|
+
subprocess.run(["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-ss", "0", "-t", "12", "-i",
|
|
174
|
+
str(ROOT / "examples/demo-dubbed-es.mp4"), "-vf",
|
|
175
|
+
"fps=8,scale=640:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=96[p];[b][p]paletteuse=dither=bayer",
|
|
176
|
+
str(OUT / "demo.gif")], check=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
cover()
|
|
181
|
+
flow()
|
|
182
|
+
terminals()
|
|
183
|
+
timeline()
|
|
184
|
+
gif()
|
|
185
|
+
print(sorted(p.name for p in OUT.iterdir()))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# miragea-dub
|
|
2
|
+
|
|
3
|
+
> Open-source CLI, MCP server and Agent Skill that dubs videos into another language with a separate cloned voice per speaker (Miragea voice API). Outputs dubbed MP4, multi-language audio track, SRT/VTT.
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
- [README](https://codeberg.org/miragea-voice/miragea-dub/src/branch/main/README.md): quick start, how it works, comparison, FAQ
|
|
7
|
+
- [Skill](https://codeberg.org/miragea-voice/miragea-skills/src/branch/main/skills/video-dubbing/SKILL.md): agent instructions
|
|
8
|
+
- [Changelog](https://codeberg.org/miragea-voice/miragea-dub/src/branch/main/CHANGELOG.md)
|
|
9
|
+
|
|
10
|
+
## Facts
|
|
11
|
+
- Install: pipx install miragea-dub (PyPI), needs ffmpeg
|
|
12
|
+
- Key: MIRAGEA_API_KEY from https://miragea.ai/workspace/api ; API base https://miragea.ai/v1
|
|
13
|
+
- Commands: prepare, translate, render, dub, estimate, voices, doctor, demo, mcp
|
|
14
|
+
- MCP tools: prepare_dub, save_translations, show_project, estimate_dub, render_dub, list_voices
|
|
15
|
+
- License: MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "miragea-dub"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Open-source multi-speaker video dubbing: transcribe, find who speaks when, translate, and re-voice every speaker with their own cloned voice, with timing fit, ducked background, dubbed MP4, multi-language audio track and SRT/VTT. CLI + MCP server + Agent Skill, voiced by Miragea."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "miragea-dub contributors" }]
|
|
14
|
+
keywords = ["video-dubbing", "ai-dubbing", "dubbing", "video-translation", "voice-cloning", "multi-speaker", "speaker-diarization", "ai-voice-over", "youtube-multi-language-audio", "heygen-alternative", "elevenlabs-dubbing-alternative", "subtitles", "srt", "mcp", "mcp-server", "agent-skill", "text-to-speech", "localization", "miragea"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
24
|
+
"Topic :: Multimedia :: Video",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["httpx>=0.27", "numpy>=1.26", "mcp>=1.9", "anyio>=4", "pydantic>=2"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
whisper = ["faster-whisper>=1.0"]
|
|
31
|
+
pyannote = ["pyannote.audio>=3.1"]
|
|
32
|
+
demucs = ["demucs>=4"]
|
|
33
|
+
dev = ["pytest>=8", "ruff>=0.5"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://codeberg.org/miragea-voice/miragea-dub"
|
|
37
|
+
Documentation = "https://codeberg.org/miragea-voice/miragea-dub#readme"
|
|
38
|
+
Issues = "https://codeberg.org/miragea-voice/miragea-dub/issues"
|
|
39
|
+
"Miragea API" = "https://miragea.ai"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
miragea-dub = "miragea_dub.cli:main"
|
|
43
|
+
miragea-dub-mcp = "miragea_dub.mcp_server:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/miragea_dub"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
exclude = ["examples", "docs/images", "docs/media", ".venv", "examples/output"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
addopts = "-q"
|
|
54
|
+
pythonpath = ["."]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 150
|
|
58
|
+
target-version = "py310"
|
|
59
|
+
src = ["src"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "W", "I", "B", "UP"]
|
|
63
|
+
ignore = ["B905"]
|