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.
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """Retake: fix a flubbed voiceover line in your own voice (Miragea engine)."""
2
+ __version__ = "0.1.0"
3
+
4
+ from .core import fix, transcribe # noqa: E402,F401
@@ -0,0 +1,3 @@
1
+ import sys
2
+ from .cli import main
3
+ sys.exit(main())
@@ -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())