retake-vo 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.
- retake_vo-0.1.0/LICENSE +21 -0
- retake_vo-0.1.0/PKG-INFO +168 -0
- retake_vo-0.1.0/README.md +136 -0
- retake_vo-0.1.0/pyproject.toml +42 -0
- retake_vo-0.1.0/setup.cfg +4 -0
- retake_vo-0.1.0/src/retake/__init__.py +4 -0
- retake_vo-0.1.0/src/retake/__main__.py +3 -0
- retake_vo-0.1.0/src/retake/align.py +131 -0
- retake_vo-0.1.0/src/retake/cli.py +75 -0
- retake_vo-0.1.0/src/retake/core.py +229 -0
- retake_vo-0.1.0/src/retake/dsp.py +178 -0
- retake_vo-0.1.0/src/retake/mcp_server.py +56 -0
- retake_vo-0.1.0/src/retake/miragea.py +141 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/PKG-INFO +168 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/SOURCES.txt +18 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/dependency_links.txt +1 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/entry_points.txt +3 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/requires.txt +14 -0
- retake_vo-0.1.0/src/retake_vo.egg-info/top_level.txt +1 -0
- retake_vo-0.1.0/tests/test_retake.py +135 -0
retake_vo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miragea team
|
|
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.
|
retake_vo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: retake-vo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fix a flubbed voiceover line in your own voice: re-voice one sentence and splice it in with matched loudness, EQ and room tone. Open-source Overdub alternative (CLI + MCP), powered by Miragea.
|
|
5
|
+
Author: Miragea team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yyert123/retake
|
|
8
|
+
Project-URL: Source, https://github.com/yyert123/retake
|
|
9
|
+
Project-URL: Issues, https://github.com/yyert123/retake/issues
|
|
10
|
+
Project-URL: Demo, https://huggingface.co/spaces/headvox/retake
|
|
11
|
+
Project-URL: Voice engine, https://miragea.ai
|
|
12
|
+
Keywords: voiceover,overdub,voice cloning,fix voiceover mistake,audio editing,podcast editing,text-based audio editing,descript alternative,mcp,mcp-server,tts,miragea
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Editors
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.22
|
|
22
|
+
Provides-Extra: mcp
|
|
23
|
+
Requires-Dist: mcp>=1.2; extra == "mcp"
|
|
24
|
+
Provides-Extra: hf
|
|
25
|
+
Requires-Dist: huggingface_hub>=0.23; extra == "hf"
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: mcp>=1.2; extra == "all"
|
|
28
|
+
Requires-Dist: huggingface_hub>=0.23; extra == "all"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Retake: fix a voiceover mistake in your own voice (open-source Overdub alternative)
|
|
34
|
+
|
|
35
|
+
**Said "Wednesday" instead of "Thursday"? Don't re-record.** Retake re-voices only the sentence you
|
|
36
|
+
changed, in the same voice, and splices it back into your recording with matched loudness, tone (EQ) and
|
|
37
|
+
room tone. Works with any editor (Premiere, Resolve, Final Cut, Audacity, CapCut) because it simply gives
|
|
38
|
+
you a fixed WAV plus edit markers. CLI for humans, MCP server for AI agents. Voice engine:
|
|
39
|
+
[Miragea](https://miragea.ai).
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install retake-vo
|
|
43
|
+
export MIRAGEA_API_KEY=your_key # https://miragea.ai
|
|
44
|
+
retake transcribe take.wav # numbered sentences + timestamps (cached, pay once)
|
|
45
|
+
retake fix take.wav --replace "Wednesday" --with "Thursday" --voice YOUR_VOICE_ID
|
|
46
|
+
# -> take.fixed.wav, take.fixed.edits.json, take.fixed.markers.csv
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
<!-- mcp-name: io.github.yyert123/retake -->
|
|
50
|
+
|
|
51
|
+
> **Status (v0.1.0, 2026-11-08):** transcription, sentence finding, pause-snapped cut points, EQ/loudness/
|
|
52
|
+
> room-tone matching, splicing, exports, CLI and MCP server are tested (9 offline tests + live Miragea
|
|
53
|
+
> transcription on real recordings). A listen-to-it before/after demo will be added to the
|
|
54
|
+
> [demo page](https://huggingface.co/spaces/headvox/retake) as soon as the voice side is verified end to end.
|
|
55
|
+
|
|
56
|
+
## What it does automatically
|
|
57
|
+
|
|
58
|
+
1. **Transcribes** the take with Miragea speech recognition and splits it into sentences (initials like
|
|
59
|
+
"O. Henry" handled). The transcript is cached next to the file, so later fixes cost nothing extra.
|
|
60
|
+
2. **Finds the sentence** that contains your words; refuses to guess if they appear twice.
|
|
61
|
+
3. **Finds real cut points**: sentence edges are snapped to the quietest stretch of the pause around them,
|
|
62
|
+
so cuts never land inside a word, even though recognition segments can split words.
|
|
63
|
+
4. **Re-voices** the edited sentence with Miragea in your saved voice, or cloned from a reference clip.
|
|
64
|
+
5. **Matches it to the take**: trims silence, matches the long-term spectrum (EQ, 1/3-octave smoothed,
|
|
65
|
+
clamped), matches speech loudness, lays your own room tone under it, equal-power crossfades the seams.
|
|
66
|
+
6. **Exports** a 24-bit WAV at the original sample rate and channels, a JSON edit list (old/new text,
|
|
67
|
+
old/new times, gain), and a CSV of markers you can import into your editor. The original file is never
|
|
68
|
+
modified.
|
|
69
|
+
|
|
70
|
+
## Why Retake (honest comparison)
|
|
71
|
+
|
|
72
|
+
| | **Retake** | Descript Overdub / Regenerate | DaVinci Resolve 21 Speech Generator | Reasonate (REAPER) | Manual ElevenLabs workflow |
|
|
73
|
+
|---|---|---|---|---|---|
|
|
74
|
+
| Price | Free, MIT; pay-as-you-go voice | Subscription | Resolve Studio ($295) | Free + ElevenLabs plan | ElevenLabs plan |
|
|
75
|
+
| Works with your editor | Any (WAV + markers) | Only inside Descript | Only inside Resolve | Only inside REAPER | Any, by hand |
|
|
76
|
+
| Finds the line for you | Yes, by words or line number | Yes | No, you place it | Yes (forced alignment) | No |
|
|
77
|
+
| Cut points at real pauses | Automatic | Automatic | Manual | Automatic | Manual |
|
|
78
|
+
| Loudness match | Automatic | Automatic | Manual | Automatic | Manual |
|
|
79
|
+
| EQ / tone match | Automatic | Partial | Manual | No | Manual (the most-cited pain) |
|
|
80
|
+
| Room tone under patch | Automatic | Partial | No | No | Manual |
|
|
81
|
+
| Length per fix | Up to 2,000 characters | - | ~350 characters | - | - |
|
|
82
|
+
| AI agents (MCP) | Yes, `retake-mcp` | No | No | No | No |
|
|
83
|
+
| Voice engine | Miragea (saved voice or clone) | Descript | Resolve built-in voices / custom WAV | ElevenLabs only | ElevenLabs |
|
|
84
|
+
|
|
85
|
+
Sources: Resolve limits per [davinciresolveclub.com](https://davinciresolveclub.com/davinci-resolve-text-to-speech/);
|
|
86
|
+
Reasonate per its [README](https://github.com/b451c/Reasonate); the EQ-matching pain per an
|
|
87
|
+
[r/editors thread](https://www.reddit.com/r/editors/comments/15s6lau/).
|
|
88
|
+
|
|
89
|
+
## Choosing the voice
|
|
90
|
+
|
|
91
|
+
| Option | When | Example |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `--voice ID` | You saved your voice in your Miragea account (best: consistent, no upload) | `--voice v_abc123` |
|
|
94
|
+
| `--ref-url URL --ref-text "..."` | You have a public clip of the same speaker; giving its exact words uses the cheaper 1x rate | `--ref-url https://raw.githubusercontent.com/you/voice/main/me.wav` |
|
|
95
|
+
| `--host hf:user/repo` | Fully automatic: Retake cuts ~9 s of untouched speech from the take and uploads it to **your public** Hugging Face dataset (needs `pip install "retake-vo[hf]"` and `HF_TOKEN`) | `--host hf:me/voice-refs` |
|
|
96
|
+
|
|
97
|
+
Reference URLs must be public, served directly (no long signed redirects) and under 1,024 characters.
|
|
98
|
+
Miragea has agreed to add a direct upload endpoint; when it ships, Retake will upload the clip from your
|
|
99
|
+
take itself and these options become optional.
|
|
100
|
+
|
|
101
|
+
## More examples
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# several fixes in one pass (each sentence re-voiced once)
|
|
105
|
+
retake fix vo.mp3 --replace "twenty twenty-five" --with "twenty twenty-six" --replace "Jon" --with "John" --voice v1
|
|
106
|
+
|
|
107
|
+
# rewrite a whole sentence by its number from `retake transcribe`
|
|
108
|
+
retake fix vo.wav --line 7 --say "Click Export, then choose MP4." --voice v1 -o vo.v2.wav
|
|
109
|
+
|
|
110
|
+
# keep each generated patch as its own file (for manual placement in a DAW)
|
|
111
|
+
retake fix vo.wav --replace "fourty" --with "forty" --voice v1 --keep-patches
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Use it from Claude, Cursor or Codex (MCP)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install "retake-vo[mcp]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "mcpServers": { "retake": { "command": "retake-mcp", "env": { "MIRAGEA_API_KEY": "your_key" } } } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then ask: *"In ~/Videos/launch/vo.wav, change 'Wednesday' to 'Thursday' using my voice v_abc123."*
|
|
125
|
+
Tools: `transcribe_take(path)` and `fix_line(path, old_words, new_words | line, new_sentence, voice_id | ref_url | host)`.
|
|
126
|
+
|
|
127
|
+
## Python API
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from retake import fix, transcribe
|
|
131
|
+
lines = transcribe("take.wav")["sentences"]
|
|
132
|
+
res = fix("take.wav", [("Wednesday", "Thursday")], voice="v_abc123")
|
|
133
|
+
print(res.output, res.edits[0].new_start)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## FAQ
|
|
137
|
+
|
|
138
|
+
**Will people hear the patch?** Retake does everything an editor would do by hand: cut in the pause,
|
|
139
|
+
match level, tone and room noise, crossfade. Quality still depends on the clone; a saved Miragea voice
|
|
140
|
+
made from a clean 10-20 s sample of the same mic gives the closest match. Listen before you publish.
|
|
141
|
+
|
|
142
|
+
**Does it change the rest of my audio?** No. Everything outside the edited sentences is copied sample for
|
|
143
|
+
sample; later audio shifts by the length difference, which the edit list and markers report.
|
|
144
|
+
|
|
145
|
+
**What does it cost?** Transcription: 3 Miragea points per second of audio, once per file (cached).
|
|
146
|
+
Each fix: Miragea's per-character rate for the new sentence (1x with a saved voice or exact reference text).
|
|
147
|
+
|
|
148
|
+
**Is my key safe?** It is read from `MIRAGEA_API_KEY` only, never logged or written to disk.
|
|
149
|
+
|
|
150
|
+
**Can I fix someone else's recording?** Only with the speaker's consent. Retake is built for fixing your own
|
|
151
|
+
narration or a talent's read you are licensed to edit.
|
|
152
|
+
|
|
153
|
+
**Which formats?** Anything ffmpeg reads (WAV, MP3, M4A, FLAC, MP4, MOV...). Output is WAV by default; use
|
|
154
|
+
`-o fixed.mp3` for MP3.
|
|
155
|
+
|
|
156
|
+
**Error messages?** Every error says what went wrong and how to fix it (missing key, ffmpeg not installed,
|
|
157
|
+
word not found, word found twice, balance, rate limit). Re-running is always safe.
|
|
158
|
+
|
|
159
|
+
## Requirements
|
|
160
|
+
|
|
161
|
+
Python 3.9+, ffmpeg on PATH, a [Miragea](https://miragea.ai) API key.
|
|
162
|
+
|
|
163
|
+
## Related
|
|
164
|
+
|
|
165
|
+
- [miragea-skills](https://github.com/yyert123/miragea-skills): Agent Skills for captions and Remotion narration.
|
|
166
|
+
- [HeadVox](https://github.com/yyert123/headvox): script to voiceover with word-aligned captions.
|
|
167
|
+
|
|
168
|
+
MIT License.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Retake: fix a voiceover mistake in your own voice (open-source Overdub alternative)
|
|
2
|
+
|
|
3
|
+
**Said "Wednesday" instead of "Thursday"? Don't re-record.** Retake re-voices only the sentence you
|
|
4
|
+
changed, in the same voice, and splices it back into your recording with matched loudness, tone (EQ) and
|
|
5
|
+
room tone. Works with any editor (Premiere, Resolve, Final Cut, Audacity, CapCut) because it simply gives
|
|
6
|
+
you a fixed WAV plus edit markers. CLI for humans, MCP server for AI agents. Voice engine:
|
|
7
|
+
[Miragea](https://miragea.ai).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install retake-vo
|
|
11
|
+
export MIRAGEA_API_KEY=your_key # https://miragea.ai
|
|
12
|
+
retake transcribe take.wav # numbered sentences + timestamps (cached, pay once)
|
|
13
|
+
retake fix take.wav --replace "Wednesday" --with "Thursday" --voice YOUR_VOICE_ID
|
|
14
|
+
# -> take.fixed.wav, take.fixed.edits.json, take.fixed.markers.csv
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
<!-- mcp-name: io.github.yyert123/retake -->
|
|
18
|
+
|
|
19
|
+
> **Status (v0.1.0, 2026-11-08):** transcription, sentence finding, pause-snapped cut points, EQ/loudness/
|
|
20
|
+
> room-tone matching, splicing, exports, CLI and MCP server are tested (9 offline tests + live Miragea
|
|
21
|
+
> transcription on real recordings). A listen-to-it before/after demo will be added to the
|
|
22
|
+
> [demo page](https://huggingface.co/spaces/headvox/retake) as soon as the voice side is verified end to end.
|
|
23
|
+
|
|
24
|
+
## What it does automatically
|
|
25
|
+
|
|
26
|
+
1. **Transcribes** the take with Miragea speech recognition and splits it into sentences (initials like
|
|
27
|
+
"O. Henry" handled). The transcript is cached next to the file, so later fixes cost nothing extra.
|
|
28
|
+
2. **Finds the sentence** that contains your words; refuses to guess if they appear twice.
|
|
29
|
+
3. **Finds real cut points**: sentence edges are snapped to the quietest stretch of the pause around them,
|
|
30
|
+
so cuts never land inside a word, even though recognition segments can split words.
|
|
31
|
+
4. **Re-voices** the edited sentence with Miragea in your saved voice, or cloned from a reference clip.
|
|
32
|
+
5. **Matches it to the take**: trims silence, matches the long-term spectrum (EQ, 1/3-octave smoothed,
|
|
33
|
+
clamped), matches speech loudness, lays your own room tone under it, equal-power crossfades the seams.
|
|
34
|
+
6. **Exports** a 24-bit WAV at the original sample rate and channels, a JSON edit list (old/new text,
|
|
35
|
+
old/new times, gain), and a CSV of markers you can import into your editor. The original file is never
|
|
36
|
+
modified.
|
|
37
|
+
|
|
38
|
+
## Why Retake (honest comparison)
|
|
39
|
+
|
|
40
|
+
| | **Retake** | Descript Overdub / Regenerate | DaVinci Resolve 21 Speech Generator | Reasonate (REAPER) | Manual ElevenLabs workflow |
|
|
41
|
+
|---|---|---|---|---|---|
|
|
42
|
+
| Price | Free, MIT; pay-as-you-go voice | Subscription | Resolve Studio ($295) | Free + ElevenLabs plan | ElevenLabs plan |
|
|
43
|
+
| Works with your editor | Any (WAV + markers) | Only inside Descript | Only inside Resolve | Only inside REAPER | Any, by hand |
|
|
44
|
+
| Finds the line for you | Yes, by words or line number | Yes | No, you place it | Yes (forced alignment) | No |
|
|
45
|
+
| Cut points at real pauses | Automatic | Automatic | Manual | Automatic | Manual |
|
|
46
|
+
| Loudness match | Automatic | Automatic | Manual | Automatic | Manual |
|
|
47
|
+
| EQ / tone match | Automatic | Partial | Manual | No | Manual (the most-cited pain) |
|
|
48
|
+
| Room tone under patch | Automatic | Partial | No | No | Manual |
|
|
49
|
+
| Length per fix | Up to 2,000 characters | - | ~350 characters | - | - |
|
|
50
|
+
| AI agents (MCP) | Yes, `retake-mcp` | No | No | No | No |
|
|
51
|
+
| Voice engine | Miragea (saved voice or clone) | Descript | Resolve built-in voices / custom WAV | ElevenLabs only | ElevenLabs |
|
|
52
|
+
|
|
53
|
+
Sources: Resolve limits per [davinciresolveclub.com](https://davinciresolveclub.com/davinci-resolve-text-to-speech/);
|
|
54
|
+
Reasonate per its [README](https://github.com/b451c/Reasonate); the EQ-matching pain per an
|
|
55
|
+
[r/editors thread](https://www.reddit.com/r/editors/comments/15s6lau/).
|
|
56
|
+
|
|
57
|
+
## Choosing the voice
|
|
58
|
+
|
|
59
|
+
| Option | When | Example |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `--voice ID` | You saved your voice in your Miragea account (best: consistent, no upload) | `--voice v_abc123` |
|
|
62
|
+
| `--ref-url URL --ref-text "..."` | You have a public clip of the same speaker; giving its exact words uses the cheaper 1x rate | `--ref-url https://raw.githubusercontent.com/you/voice/main/me.wav` |
|
|
63
|
+
| `--host hf:user/repo` | Fully automatic: Retake cuts ~9 s of untouched speech from the take and uploads it to **your public** Hugging Face dataset (needs `pip install "retake-vo[hf]"` and `HF_TOKEN`) | `--host hf:me/voice-refs` |
|
|
64
|
+
|
|
65
|
+
Reference URLs must be public, served directly (no long signed redirects) and under 1,024 characters.
|
|
66
|
+
Miragea has agreed to add a direct upload endpoint; when it ships, Retake will upload the clip from your
|
|
67
|
+
take itself and these options become optional.
|
|
68
|
+
|
|
69
|
+
## More examples
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# several fixes in one pass (each sentence re-voiced once)
|
|
73
|
+
retake fix vo.mp3 --replace "twenty twenty-five" --with "twenty twenty-six" --replace "Jon" --with "John" --voice v1
|
|
74
|
+
|
|
75
|
+
# rewrite a whole sentence by its number from `retake transcribe`
|
|
76
|
+
retake fix vo.wav --line 7 --say "Click Export, then choose MP4." --voice v1 -o vo.v2.wav
|
|
77
|
+
|
|
78
|
+
# keep each generated patch as its own file (for manual placement in a DAW)
|
|
79
|
+
retake fix vo.wav --replace "fourty" --with "forty" --voice v1 --keep-patches
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Use it from Claude, Cursor or Codex (MCP)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install "retake-vo[mcp]"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "mcpServers": { "retake": { "command": "retake-mcp", "env": { "MIRAGEA_API_KEY": "your_key" } } } }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then ask: *"In ~/Videos/launch/vo.wav, change 'Wednesday' to 'Thursday' using my voice v_abc123."*
|
|
93
|
+
Tools: `transcribe_take(path)` and `fix_line(path, old_words, new_words | line, new_sentence, voice_id | ref_url | host)`.
|
|
94
|
+
|
|
95
|
+
## Python API
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from retake import fix, transcribe
|
|
99
|
+
lines = transcribe("take.wav")["sentences"]
|
|
100
|
+
res = fix("take.wav", [("Wednesday", "Thursday")], voice="v_abc123")
|
|
101
|
+
print(res.output, res.edits[0].new_start)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## FAQ
|
|
105
|
+
|
|
106
|
+
**Will people hear the patch?** Retake does everything an editor would do by hand: cut in the pause,
|
|
107
|
+
match level, tone and room noise, crossfade. Quality still depends on the clone; a saved Miragea voice
|
|
108
|
+
made from a clean 10-20 s sample of the same mic gives the closest match. Listen before you publish.
|
|
109
|
+
|
|
110
|
+
**Does it change the rest of my audio?** No. Everything outside the edited sentences is copied sample for
|
|
111
|
+
sample; later audio shifts by the length difference, which the edit list and markers report.
|
|
112
|
+
|
|
113
|
+
**What does it cost?** Transcription: 3 Miragea points per second of audio, once per file (cached).
|
|
114
|
+
Each fix: Miragea's per-character rate for the new sentence (1x with a saved voice or exact reference text).
|
|
115
|
+
|
|
116
|
+
**Is my key safe?** It is read from `MIRAGEA_API_KEY` only, never logged or written to disk.
|
|
117
|
+
|
|
118
|
+
**Can I fix someone else's recording?** Only with the speaker's consent. Retake is built for fixing your own
|
|
119
|
+
narration or a talent's read you are licensed to edit.
|
|
120
|
+
|
|
121
|
+
**Which formats?** Anything ffmpeg reads (WAV, MP3, M4A, FLAC, MP4, MOV...). Output is WAV by default; use
|
|
122
|
+
`-o fixed.mp3` for MP3.
|
|
123
|
+
|
|
124
|
+
**Error messages?** Every error says what went wrong and how to fix it (missing key, ffmpeg not installed,
|
|
125
|
+
word not found, word found twice, balance, rate limit). Re-running is always safe.
|
|
126
|
+
|
|
127
|
+
## Requirements
|
|
128
|
+
|
|
129
|
+
Python 3.9+, ffmpeg on PATH, a [Miragea](https://miragea.ai) API key.
|
|
130
|
+
|
|
131
|
+
## Related
|
|
132
|
+
|
|
133
|
+
- [miragea-skills](https://github.com/yyert123/miragea-skills): Agent Skills for captions and Remotion narration.
|
|
134
|
+
- [HeadVox](https://github.com/yyert123/headvox): script to voiceover with word-aligned captions.
|
|
135
|
+
|
|
136
|
+
MIT License.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "retake-vo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Fix a flubbed voiceover line in your own voice: re-voice one sentence and splice it in with matched loudness, EQ and room tone. Open-source Overdub alternative (CLI + MCP), powered by Miragea."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Miragea team" }]
|
|
13
|
+
keywords = ["voiceover", "overdub", "voice cloning", "fix voiceover mistake", "audio editing", "podcast editing",
|
|
14
|
+
"text-based audio editing", "descript alternative", "mcp", "mcp-server", "tts", "miragea"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Multimedia :: Sound/Audio :: Editors",
|
|
18
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: End Users/Desktop",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["numpy>=1.22"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
mcp = ["mcp>=1.2"]
|
|
26
|
+
hf = ["huggingface_hub>=0.23"]
|
|
27
|
+
all = ["mcp>=1.2", "huggingface_hub>=0.23"]
|
|
28
|
+
dev = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
retake = "retake.cli:main"
|
|
32
|
+
retake-mcp = "retake.mcp_server:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/yyert123/retake"
|
|
36
|
+
Source = "https://github.com/yyert123/retake"
|
|
37
|
+
Issues = "https://github.com/yyert123/retake/issues"
|
|
38
|
+
Demo = "https://huggingface.co/spaces/headvox/retake"
|
|
39
|
+
"Voice engine" = "https://miragea.ai"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Turn Miragea's segment-level transcript into sentence-level timings.
|
|
2
|
+
|
|
3
|
+
Miragea returns the full transcript plus short segments whose boundaries can
|
|
4
|
+
fall mid-word ("cen" | "ts"). We locate every segment inside the full text,
|
|
5
|
+
interpolate a time for every character, then split the text into sentences.
|
|
6
|
+
Sentence boundaries are later snapped to real pauses in the audio (see dsp).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
|
|
13
|
+
_SENT_RE = re.compile(r"[^.!?…]+(?:[.!?…]+[\"'”’)\]]*|$)", re.S)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Sentence:
|
|
18
|
+
index: int
|
|
19
|
+
text: str
|
|
20
|
+
start: float
|
|
21
|
+
end: float
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return asdict(self)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _norm(ch: str) -> str:
|
|
28
|
+
return ch.lower() if ch.isalnum() else ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def char_times(text: str, segments: list[dict]) -> list[float | None]:
|
|
32
|
+
"""Return an estimated time (seconds) for each character of ``text``."""
|
|
33
|
+
times: list[float | None] = [None] * (len(text) + 1)
|
|
34
|
+
# Build an index of alphanumeric characters of the full text.
|
|
35
|
+
alnum_pos = [i for i, c in enumerate(text) if c.isalnum()]
|
|
36
|
+
alnum_str = "".join(text[i].lower() for i in alnum_pos)
|
|
37
|
+
cursor = 0
|
|
38
|
+
for seg in segments:
|
|
39
|
+
s_txt = "".join(_norm(c) for c in seg.get("text", ""))
|
|
40
|
+
if not s_txt:
|
|
41
|
+
continue
|
|
42
|
+
found = alnum_str.find(s_txt, cursor)
|
|
43
|
+
if found < 0: # tolerate small differences: anchor on the first 4 chars
|
|
44
|
+
found = alnum_str.find(s_txt[:4], cursor)
|
|
45
|
+
if found < 0:
|
|
46
|
+
continue
|
|
47
|
+
a, b = found, min(found + len(s_txt), len(alnum_pos)) - 1
|
|
48
|
+
n = max(1, b - a)
|
|
49
|
+
for k in range(a, b + 1):
|
|
50
|
+
frac = (k - a) / n
|
|
51
|
+
times[alnum_pos[k]] = float(seg["start"]) + frac * (float(seg["end"]) - float(seg["start"]))
|
|
52
|
+
cursor = b + 1
|
|
53
|
+
# Fill gaps by linear interpolation between known points.
|
|
54
|
+
known = [i for i, t in enumerate(times) if t is not None]
|
|
55
|
+
if not known:
|
|
56
|
+
return times
|
|
57
|
+
for i in range(len(times)):
|
|
58
|
+
if times[i] is None:
|
|
59
|
+
left = max((k for k in known if k < i), default=None)
|
|
60
|
+
right = min((k for k in known if k > i), default=None)
|
|
61
|
+
if left is None:
|
|
62
|
+
times[i] = times[right]
|
|
63
|
+
elif right is None:
|
|
64
|
+
times[i] = times[left]
|
|
65
|
+
else:
|
|
66
|
+
f = (i - left) / (right - left)
|
|
67
|
+
times[i] = times[left] + f * (times[right] - times[left])
|
|
68
|
+
return times
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_ABBREV = re.compile(r"(?:\b[A-Z]|\b(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St|vs|etc|e\.g|i\.e|No|Inc|Ltd|U\.S))\.$")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def split_sentences(text: str) -> list[tuple[int, int]]:
|
|
75
|
+
"""Return (start, end) character spans of sentences, whitespace-trimmed.
|
|
76
|
+
|
|
77
|
+
Initials and common abbreviations ("O. Henry", "Dr. Smith") do not end a sentence."""
|
|
78
|
+
spans: list[tuple[int, int]] = []
|
|
79
|
+
for m in _SENT_RE.finditer(text):
|
|
80
|
+
a, b = m.start(), m.end()
|
|
81
|
+
while a < b and text[a].isspace():
|
|
82
|
+
a += 1
|
|
83
|
+
while b > a and text[b - 1].isspace():
|
|
84
|
+
b -= 1
|
|
85
|
+
if a < b and any(c.isalnum() for c in text[a:b]):
|
|
86
|
+
if spans and _ABBREV.search(text[spans[-1][0]:spans[-1][1]]):
|
|
87
|
+
spans[-1] = (spans[-1][0], b)
|
|
88
|
+
else:
|
|
89
|
+
spans.append((a, b))
|
|
90
|
+
return spans
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def sentences_from_transcript(transcript: dict, duration: float | None = None) -> list[Sentence]:
|
|
94
|
+
text = transcript.get("text") or " ".join(s.get("text", "") for s in transcript.get("segments", []))
|
|
95
|
+
segs = transcript.get("segments", [])
|
|
96
|
+
times = char_times(text, segs)
|
|
97
|
+
out = []
|
|
98
|
+
for idx, (a, b) in enumerate(split_sentences(text)):
|
|
99
|
+
first = next(i for i in range(a, b) if text[i].isalnum())
|
|
100
|
+
last = max(i for i in range(a, b) if text[i].isalnum())
|
|
101
|
+
start, end = times[first], times[last]
|
|
102
|
+
if start is None or end is None:
|
|
103
|
+
continue
|
|
104
|
+
out.append(Sentence(idx, text[a:b], round(start, 3), round(end, 3)))
|
|
105
|
+
if out and segs: # the last character is spoken a little before its segment ends
|
|
106
|
+
out[-1].end = round(max(out[-1].end, float(segs[-1]["end"])), 3)
|
|
107
|
+
if duration:
|
|
108
|
+
for s in out:
|
|
109
|
+
s.end = min(s.end, duration)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_sentence(sentences: list[Sentence], needle: str) -> Sentence:
|
|
114
|
+
"""Find the one sentence that contains ``needle`` (case/punctuation-insensitive)."""
|
|
115
|
+
key = "".join(_norm(c) for c in needle)
|
|
116
|
+
hits = [s for s in sentences if key and key in "".join(_norm(c) for c in s.text)]
|
|
117
|
+
if not hits:
|
|
118
|
+
raise LookupError(f'"{needle}" was not found in the transcript. Run `retake transcribe` to see the exact words.')
|
|
119
|
+
if len(hits) > 1:
|
|
120
|
+
lines = "; ".join(f"#{s.index} at {s.start:.1f}s" for s in hits)
|
|
121
|
+
raise LookupError(f'"{needle}" appears in {len(hits)} sentences ({lines}). Add more words or use --line N.')
|
|
122
|
+
return hits[0]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def apply_replacement(sentence: str, old: str, new: str) -> str:
|
|
126
|
+
"""Replace ``old`` with ``new`` inside ``sentence``, ignoring case and spacing differences."""
|
|
127
|
+
pattern = r"\s+".join(re.escape(w) for w in old.split())
|
|
128
|
+
result, n = re.subn(pattern, new, sentence, count=1, flags=re.I)
|
|
129
|
+
if n == 0:
|
|
130
|
+
raise LookupError(f'"{old}" was not found verbatim in: {sentence}')
|
|
131
|
+
return result
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""retake - fix a flubbed voiceover line in your own voice, from the terminal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .core import fix, transcribe
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _fmt(t: float) -> str:
|
|
13
|
+
return f"{int(t // 60):02d}:{t % 60:05.2f}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
p = argparse.ArgumentParser(
|
|
18
|
+
prog="retake",
|
|
19
|
+
description="Fix a wrong word in a recorded voiceover by re-voicing only that sentence in the same voice "
|
|
20
|
+
"(engine: Miragea, https://miragea.ai). Loudness, EQ and room tone are matched automatically.")
|
|
21
|
+
p.add_argument("--version", action="version", version=f"retake {__version__}")
|
|
22
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
23
|
+
|
|
24
|
+
t = sub.add_parser("transcribe", help="list the take's sentences with line numbers and timestamps")
|
|
25
|
+
t.add_argument("take", type=Path)
|
|
26
|
+
t.add_argument("--force", action="store_true", help="ignore the cached transcript and pay to re-transcribe")
|
|
27
|
+
|
|
28
|
+
f = sub.add_parser("fix", help="re-voice the sentence(s) you changed and splice them in")
|
|
29
|
+
f.add_argument("take", type=Path)
|
|
30
|
+
f.add_argument("--replace", action="append", default=[], metavar="OLD", help="words to change (repeatable)")
|
|
31
|
+
f.add_argument("--with", dest="with_", action="append", default=[], metavar="NEW", help="replacement words")
|
|
32
|
+
f.add_argument("--line", action="append", type=int, default=[], metavar="N", help="sentence number to rewrite")
|
|
33
|
+
f.add_argument("--say", action="append", default=[], metavar="TEXT", help="full new text for --line")
|
|
34
|
+
f.add_argument("-o", "--output", type=Path, help="output file (default: <take>.fixed.wav)")
|
|
35
|
+
v = f.add_argument_group("voice (pick one)")
|
|
36
|
+
v.add_argument("--voice", help="voice id saved in your Miragea account")
|
|
37
|
+
v.add_argument("--ref-url", help="public URL of a clip of the same speaker")
|
|
38
|
+
v.add_argument("--ref-text", help="exact words spoken in --ref-url (cheaper: 1x instead of 3x)")
|
|
39
|
+
v.add_argument("--host", metavar="hf:USER/REPO",
|
|
40
|
+
help="auto-cut a reference clip from this take and host it on your PUBLIC Hugging Face dataset")
|
|
41
|
+
f.add_argument("--model", default="expressive", choices=["expressive", "standard"])
|
|
42
|
+
f.add_argument("--no-room-tone", action="store_true")
|
|
43
|
+
f.add_argument("--no-eq", action="store_true")
|
|
44
|
+
f.add_argument("--keep-patches", action="store_true", help="also save each generated patch as its own file")
|
|
45
|
+
|
|
46
|
+
a = p.parse_args(argv)
|
|
47
|
+
try:
|
|
48
|
+
if a.cmd == "transcribe":
|
|
49
|
+
data = transcribe(a.take, force=a.force)
|
|
50
|
+
for s in data["sentences"]:
|
|
51
|
+
print(f"[{s['index']:>3}] {_fmt(s['start'])}-{_fmt(s['end'])} {s['text']}")
|
|
52
|
+
print(f"\n{len(data['sentences'])} lines, cached in {a.take.name}.retake.json (re-runs are free).",
|
|
53
|
+
file=sys.stderr)
|
|
54
|
+
return 0
|
|
55
|
+
if len(a.replace) != len(a.with_):
|
|
56
|
+
p.error("every --replace needs a matching --with")
|
|
57
|
+
if len(a.line) != len(a.say):
|
|
58
|
+
p.error("every --line needs a matching --say")
|
|
59
|
+
res = fix(a.take, list(zip(a.replace, a.with_)), lines=list(zip(a.line, a.say)), output=a.output,
|
|
60
|
+
voice=a.voice, ref_url=a.ref_url, ref_text=a.ref_text, host=a.host, model=a.model,
|
|
61
|
+
room=not a.no_room_tone, eq=not a.no_eq, keep_patches=a.keep_patches)
|
|
62
|
+
for e in res.edits:
|
|
63
|
+
print(f"fixed {_fmt(e.start)}: \"{e.old_text}\"\n -> \"{e.new_text}\" (gain {e.gain_db:+.1f} dB, "
|
|
64
|
+
f"now at {_fmt(e.new_start)})")
|
|
65
|
+
print(f"\nwrote {res.output}\n {res.report}\n {res.markers} (import as markers in your editor)")
|
|
66
|
+
return 0
|
|
67
|
+
except KeyboardInterrupt:
|
|
68
|
+
return 130
|
|
69
|
+
except Exception as exc: # noqa: BLE001 - show a clean message, never a traceback, to end users
|
|
70
|
+
print(f"retake: error: {exc}", file=sys.stderr)
|
|
71
|
+
return 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
sys.exit(main())
|