headvox 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.
headvox-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HeadVox 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.
headvox-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: headvox
3
+ Version: 0.1.0
4
+ Summary: Turn a script into a talking-head voiceover with word-aligned SRT/VTT captions. Chinese, English and Japanese, free neural voices, one command.
5
+ Author: HeadVox contributors
6
+ License: MIT
7
+ Keywords: tts,text-to-speech,voiceover,talking-head,subtitles,srt,vtt,captions,edge-tts,chinese-tts,ai-voice,video,ffmpeg,youtube,shorts
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: Chinese (Simplified)
14
+ Classifier: Natural Language :: English
15
+ Classifier: Natural Language :: Japanese
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
19
+ Classifier: Topic :: Multimedia :: Video
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: edge
24
+ Requires-Dist: edge-tts>=7.0; extra == "edge"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # HeadVox
30
+
31
+ **Turn a script into a talking‑head voiceover — with word‑aligned captions — in one command.**
32
+
33
+ HeadVox reads a plain‑text or Markdown script, speaks it with a neural TTS voice,
34
+ stitches the clips into a single track with natural pauses, and writes
35
+ **SRT + WebVTT captions that line up with the audio**, plus a JSON timing manifest
36
+ you can feed to a talking‑head avatar or a video editor. Point it at a video clip
37
+ and it muxes the voiceover on, loops the clip to length, and attaches the subtitles.
38
+
39
+ Chinese, English and Japanese work out of the box (mixed CJK/Latin scripts
40
+ segment correctly). The voices come from the free [`edge-tts`](https://github.com/rany2/edge-tts)
41
+ service; HeadVox does **not** bundle or re‑host any model.
42
+
43
+ ```bash
44
+ pip install "headvox[edge]"
45
+ headvox script.md -o out/ --voice zh-female --video avatar.mp4
46
+ # -> out/voiceover.mp3 out/voiceover.srt out/voiceover.vtt
47
+ # out/voiceover.json out/voiceover.mp4
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Why
53
+
54
+ Making a short explainer or a "talking head" clip usually means three fiddly
55
+ steps: generate the audio, hand‑time the subtitles, and mux it onto video.
56
+ HeadVox does all three from one script and keeps them in sync, so the captions
57
+ actually match the voice instead of drifting a few hundred milliseconds off.
58
+
59
+ ## Features
60
+
61
+ - **One command, three outputs** — audio, captions (SRT **and** VTT), and a timing manifest.
62
+ - **Word‑aligned captions.** With the `edge` backend HeadVox uses real word
63
+ boundaries, then breaks lines at punctuation and a character budget
64
+ (a CJK character counts as two columns) so no caption runs off screen.
65
+ - **Pacing you control.** Sentence and paragraph pauses are automatic; drop
66
+ `[pause]`, `[pause 1.5s]` or `[pause 400ms]` anywhere for a beat.
67
+ - **Markdown‑aware.** Headings, emphasis, links, code fences and lists are
68
+ reduced to the words a narrator would actually say.
69
+ - **Pluggable backends.** `edge` (online neural voices) and `sine` (offline,
70
+ no network, used by the tests and for dry runs). Register your own in a few lines.
71
+ - **Caches** every clip, so re‑rendering after a tiny script edit is nearly free.
72
+ - **Video muxing** with optional looped, ducked background music, and either
73
+ soft subtitles (toggleable) or burned‑in captions.
74
+ - **Pure‑Python, tiny deps.** Only `ffmpeg`/`ffprobe` on your PATH, and
75
+ `edge-tts` if you use the online voices.
76
+
77
+ ## Install
78
+
79
+ ```bash
80
+ pip install "headvox[edge]" # online neural voices
81
+ pip install headvox # library + offline 'sine' backend only
82
+ ```
83
+
84
+ You also need **ffmpeg** (with `ffprobe`) on your PATH — <https://ffmpeg.org/download.html>.
85
+
86
+ ## Usage
87
+
88
+ ```bash
89
+ # Simplest: script in, files out
90
+ headvox talk.md -o out/
91
+
92
+ # Pick a voice and tweak the delivery
93
+ headvox talk.md -o out/ --voice en-female --rate +6 --pitch +1
94
+
95
+ # Put it on a clip, loop the clip to the voiceover length, duck some music under it
96
+ headvox talk.md -o out/ --video avatar.mp4 --music bed.mp3
97
+
98
+ # Burn captions into the picture instead of a soft track
99
+ headvox talk.md -o out/ --video avatar.mp4 --burn-subs
100
+
101
+ # See how the script will be segmented without calling any TTS
102
+ headvox talk.md --dry-run
103
+ ```
104
+
105
+ Voice presets: `zh-female`, `zh-male`, `en-female`, `en-male`, `ja-female`, `ja-male`
106
+ — or pass any `edge-tts` voice id directly (e.g. `--voice zh-CN-YunxiNeural`).
107
+ Run `edge-tts --list-voices` to see them all. Behind a proxy? Use `--proxy`
108
+ or set `HEADVOX_PROXY`.
109
+
110
+ ### As a library
111
+
112
+ ```python
113
+ from headvox import synthesize, Script, VoiceSettings
114
+
115
+ res = synthesize(
116
+ Script.from_file("talk.md"),
117
+ "out/",
118
+ backend="edge",
119
+ settings=VoiceSettings(voice="zh-CN-XiaoxiaoNeural", rate_pct=5),
120
+ )
121
+ print(res.duration_ms, "ms,", len(res.cues), "captions")
122
+ for cue in res.cues:
123
+ print(cue.start_ms, cue.text)
124
+ ```
125
+
126
+ `synthesize()` returns paths to the audio, SRT, VTT and manifest, the total
127
+ duration, and the per‑segment word timings.
128
+
129
+ ## Timing manifest
130
+
131
+ `voiceover.json` holds everything downstream tools need to drive an avatar or
132
+ re‑style captions:
133
+
134
+ ```json
135
+ {
136
+ "tool": "headvox",
137
+ "backend": "edge",
138
+ "duration_ms": 19600,
139
+ "segments": [
140
+ {"index": 0, "text": "Hi everyone, and welcome back.",
141
+ "start_ms": 2398, "end_ms": 4049,
142
+ "words": [{"text": "Hi", "start_ms": 2398, "end_ms": 2560}, ...]}
143
+ ],
144
+ "cues": [{"index": 1, "start_ms": 262, "end_ms": 1573, "text": "HeadVox demo"}]
145
+ }
146
+ ```
147
+
148
+ ## Write your own backend
149
+
150
+ ```python
151
+ from headvox.backends import SegmentAudio, WordMark, register_backend
152
+
153
+ class MyBackend:
154
+ name = "mine"
155
+ def synthesize_segment(self, text, settings):
156
+ audio, fmt, dur = my_tts(text, settings.voice) # bytes, "wav"/"mp3", ms
157
+ return SegmentAudio(audio, fmt, dur, [WordMark("hi", 0, 200)])
158
+
159
+ register_backend("mine", lambda **kw: MyBackend())
160
+ ```
161
+
162
+ Then `headvox script.md -b mine`.
163
+
164
+ ## How it works
165
+
166
+ 1. **Parse** the script to speech segments, stripping Markdown and honouring pause directives.
167
+ 2. **Synthesize** each segment through the chosen backend (cached by text + voice settings).
168
+ 3. **Assemble** one 24 kHz track, trimming the padding some engines add so your pauses are exact.
169
+ 4. **Caption** from the word boundaries, breaking lines at punctuation within a width budget.
170
+ 5. **Mux** (optional) onto video with ffmpeg, with ducked music and soft or burned‑in subtitles.
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ git clone <your fork>
176
+ cd headvox
177
+ pip install -e ".[dev]"
178
+ pytest # the suite runs fully offline via the 'sine' backend
179
+ ```
180
+
181
+ ## License
182
+
183
+ MIT — see [LICENSE](LICENSE). HeadVox is original work and is not affiliated
184
+ with Microsoft or the `edge-tts` project; it simply uses `edge-tts` as one
185
+ optional backend.
@@ -0,0 +1,157 @@
1
+ # HeadVox
2
+
3
+ **Turn a script into a talking‑head voiceover — with word‑aligned captions — in one command.**
4
+
5
+ HeadVox reads a plain‑text or Markdown script, speaks it with a neural TTS voice,
6
+ stitches the clips into a single track with natural pauses, and writes
7
+ **SRT + WebVTT captions that line up with the audio**, plus a JSON timing manifest
8
+ you can feed to a talking‑head avatar or a video editor. Point it at a video clip
9
+ and it muxes the voiceover on, loops the clip to length, and attaches the subtitles.
10
+
11
+ Chinese, English and Japanese work out of the box (mixed CJK/Latin scripts
12
+ segment correctly). The voices come from the free [`edge-tts`](https://github.com/rany2/edge-tts)
13
+ service; HeadVox does **not** bundle or re‑host any model.
14
+
15
+ ```bash
16
+ pip install "headvox[edge]"
17
+ headvox script.md -o out/ --voice zh-female --video avatar.mp4
18
+ # -> out/voiceover.mp3 out/voiceover.srt out/voiceover.vtt
19
+ # out/voiceover.json out/voiceover.mp4
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Why
25
+
26
+ Making a short explainer or a "talking head" clip usually means three fiddly
27
+ steps: generate the audio, hand‑time the subtitles, and mux it onto video.
28
+ HeadVox does all three from one script and keeps them in sync, so the captions
29
+ actually match the voice instead of drifting a few hundred milliseconds off.
30
+
31
+ ## Features
32
+
33
+ - **One command, three outputs** — audio, captions (SRT **and** VTT), and a timing manifest.
34
+ - **Word‑aligned captions.** With the `edge` backend HeadVox uses real word
35
+ boundaries, then breaks lines at punctuation and a character budget
36
+ (a CJK character counts as two columns) so no caption runs off screen.
37
+ - **Pacing you control.** Sentence and paragraph pauses are automatic; drop
38
+ `[pause]`, `[pause 1.5s]` or `[pause 400ms]` anywhere for a beat.
39
+ - **Markdown‑aware.** Headings, emphasis, links, code fences and lists are
40
+ reduced to the words a narrator would actually say.
41
+ - **Pluggable backends.** `edge` (online neural voices) and `sine` (offline,
42
+ no network, used by the tests and for dry runs). Register your own in a few lines.
43
+ - **Caches** every clip, so re‑rendering after a tiny script edit is nearly free.
44
+ - **Video muxing** with optional looped, ducked background music, and either
45
+ soft subtitles (toggleable) or burned‑in captions.
46
+ - **Pure‑Python, tiny deps.** Only `ffmpeg`/`ffprobe` on your PATH, and
47
+ `edge-tts` if you use the online voices.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install "headvox[edge]" # online neural voices
53
+ pip install headvox # library + offline 'sine' backend only
54
+ ```
55
+
56
+ You also need **ffmpeg** (with `ffprobe`) on your PATH — <https://ffmpeg.org/download.html>.
57
+
58
+ ## Usage
59
+
60
+ ```bash
61
+ # Simplest: script in, files out
62
+ headvox talk.md -o out/
63
+
64
+ # Pick a voice and tweak the delivery
65
+ headvox talk.md -o out/ --voice en-female --rate +6 --pitch +1
66
+
67
+ # Put it on a clip, loop the clip to the voiceover length, duck some music under it
68
+ headvox talk.md -o out/ --video avatar.mp4 --music bed.mp3
69
+
70
+ # Burn captions into the picture instead of a soft track
71
+ headvox talk.md -o out/ --video avatar.mp4 --burn-subs
72
+
73
+ # See how the script will be segmented without calling any TTS
74
+ headvox talk.md --dry-run
75
+ ```
76
+
77
+ Voice presets: `zh-female`, `zh-male`, `en-female`, `en-male`, `ja-female`, `ja-male`
78
+ — or pass any `edge-tts` voice id directly (e.g. `--voice zh-CN-YunxiNeural`).
79
+ Run `edge-tts --list-voices` to see them all. Behind a proxy? Use `--proxy`
80
+ or set `HEADVOX_PROXY`.
81
+
82
+ ### As a library
83
+
84
+ ```python
85
+ from headvox import synthesize, Script, VoiceSettings
86
+
87
+ res = synthesize(
88
+ Script.from_file("talk.md"),
89
+ "out/",
90
+ backend="edge",
91
+ settings=VoiceSettings(voice="zh-CN-XiaoxiaoNeural", rate_pct=5),
92
+ )
93
+ print(res.duration_ms, "ms,", len(res.cues), "captions")
94
+ for cue in res.cues:
95
+ print(cue.start_ms, cue.text)
96
+ ```
97
+
98
+ `synthesize()` returns paths to the audio, SRT, VTT and manifest, the total
99
+ duration, and the per‑segment word timings.
100
+
101
+ ## Timing manifest
102
+
103
+ `voiceover.json` holds everything downstream tools need to drive an avatar or
104
+ re‑style captions:
105
+
106
+ ```json
107
+ {
108
+ "tool": "headvox",
109
+ "backend": "edge",
110
+ "duration_ms": 19600,
111
+ "segments": [
112
+ {"index": 0, "text": "Hi everyone, and welcome back.",
113
+ "start_ms": 2398, "end_ms": 4049,
114
+ "words": [{"text": "Hi", "start_ms": 2398, "end_ms": 2560}, ...]}
115
+ ],
116
+ "cues": [{"index": 1, "start_ms": 262, "end_ms": 1573, "text": "HeadVox demo"}]
117
+ }
118
+ ```
119
+
120
+ ## Write your own backend
121
+
122
+ ```python
123
+ from headvox.backends import SegmentAudio, WordMark, register_backend
124
+
125
+ class MyBackend:
126
+ name = "mine"
127
+ def synthesize_segment(self, text, settings):
128
+ audio, fmt, dur = my_tts(text, settings.voice) # bytes, "wav"/"mp3", ms
129
+ return SegmentAudio(audio, fmt, dur, [WordMark("hi", 0, 200)])
130
+
131
+ register_backend("mine", lambda **kw: MyBackend())
132
+ ```
133
+
134
+ Then `headvox script.md -b mine`.
135
+
136
+ ## How it works
137
+
138
+ 1. **Parse** the script to speech segments, stripping Markdown and honouring pause directives.
139
+ 2. **Synthesize** each segment through the chosen backend (cached by text + voice settings).
140
+ 3. **Assemble** one 24 kHz track, trimming the padding some engines add so your pauses are exact.
141
+ 4. **Caption** from the word boundaries, breaking lines at punctuation within a width budget.
142
+ 5. **Mux** (optional) onto video with ffmpeg, with ducked music and soft or burned‑in subtitles.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ git clone <your fork>
148
+ cd headvox
149
+ pip install -e ".[dev]"
150
+ pytest # the suite runs fully offline via the 'sine' backend
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT — see [LICENSE](LICENSE). HeadVox is original work and is not affiliated
156
+ with Microsoft or the `edge-tts` project; it simply uses `edge-tts` as one
157
+ optional backend.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "headvox"
7
+ version = "0.1.0"
8
+ description = "Turn a script into a talking-head voiceover with word-aligned SRT/VTT captions. Chinese, English and Japanese, free neural voices, one command."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "HeadVox contributors" }]
13
+ keywords = [
14
+ "tts", "text-to-speech", "voiceover", "talking-head", "subtitles", "srt", "vtt",
15
+ "captions", "edge-tts", "chinese-tts", "ai-voice", "video", "ffmpeg", "youtube", "shorts",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: End Users/Desktop",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Natural Language :: Chinese (Simplified)",
24
+ "Natural Language :: English",
25
+ "Natural Language :: Japanese",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
29
+ "Topic :: Multimedia :: Video",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ edge = ["edge-tts>=7.0"]
35
+ dev = ["pytest>=7"]
36
+
37
+ [project.scripts]
38
+ headvox = "headvox.cli:main"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
1
+ """HeadVox — turn a script into a talking-head voiceover with aligned captions.
2
+
3
+ HeadVox reads a plain-text or Markdown script, synthesizes it segment by
4
+ segment through a pluggable TTS backend, stitches the audio into a single
5
+ track, and emits word-aligned subtitles plus a timing manifest you can use
6
+ to drive a talking-head avatar or a video editor.
7
+
8
+ Public API:
9
+ from headvox import synthesize, Script, get_backend
10
+
11
+ Everything here is original work, released under the MIT License.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from .script import Script, Segment, split_script
16
+ from .backends import TTSBackend, SegmentAudio, WordMark, get_backend, register_backend
17
+ from .synth import synthesize, SynthResult, VoiceSettings
18
+
19
+ __all__ = [
20
+ "Script",
21
+ "Segment",
22
+ "split_script",
23
+ "TTSBackend",
24
+ "SegmentAudio",
25
+ "WordMark",
26
+ "get_backend",
27
+ "register_backend",
28
+ "synthesize",
29
+ "SynthResult",
30
+ "VoiceSettings",
31
+ ]
32
+
33
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,79 @@
1
+ """Pluggable TTS backends.
2
+
3
+ A backend turns a short piece of text into audio bytes plus optional word
4
+ boundaries. Register your own with :func:`register_backend` and select it by
5
+ name on the command line or through :func:`headvox.synthesize`.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Callable, Dict, List, Protocol, runtime_checkable
11
+
12
+
13
+ @dataclass
14
+ class WordMark:
15
+ """A word and where it falls inside a segment's audio (milliseconds)."""
16
+
17
+ text: str
18
+ start_ms: int
19
+ end_ms: int
20
+
21
+
22
+ @dataclass
23
+ class SegmentAudio:
24
+ """Result of synthesizing one segment."""
25
+
26
+ audio: bytes
27
+ audio_format: str # "mp3" or "wav"
28
+ duration_ms: int
29
+ words: List[WordMark] = field(default_factory=list)
30
+
31
+
32
+ @runtime_checkable
33
+ class TTSBackend(Protocol):
34
+ """Interface every backend implements."""
35
+
36
+ name: str
37
+
38
+ def synthesize_segment(self, text: str, settings) -> SegmentAudio: # noqa: D401
39
+ ...
40
+
41
+
42
+ _REGISTRY: Dict[str, Callable[..., TTSBackend]] = {}
43
+
44
+
45
+ def register_backend(name: str, factory: Callable[..., TTSBackend]) -> None:
46
+ _REGISTRY[name.lower()] = factory
47
+
48
+
49
+ def available_backends() -> List[str]:
50
+ return sorted(_REGISTRY)
51
+
52
+
53
+ def get_backend(name: str, **kwargs) -> TTSBackend:
54
+ key = name.lower()
55
+ if key not in _REGISTRY:
56
+ raise ValueError(
57
+ f"unknown backend {name!r}; available: {', '.join(available_backends()) or '(none)'}"
58
+ )
59
+ return _REGISTRY[key](**kwargs)
60
+
61
+
62
+ # Register the backends that ship with HeadVox. Imports are deferred inside the
63
+ # factories so a missing optional dependency only fails when that backend is
64
+ # actually used.
65
+
66
+ def _edge_factory(**kw):
67
+ from .edge import EdgeBackend
68
+
69
+ return EdgeBackend(**kw)
70
+
71
+
72
+ def _sine_factory(**kw):
73
+ from .sine import SineBackend
74
+
75
+ return SineBackend(**kw)
76
+
77
+
78
+ register_backend("edge", _edge_factory)
79
+ register_backend("sine", _sine_factory)
@@ -0,0 +1,73 @@
1
+ """Edge TTS backend.
2
+
3
+ Wraps the `edge-tts` project (Microsoft Edge's online neural voices, free to
4
+ use) behind HeadVox's backend interface. It returns MP3 audio and real
5
+ word-boundary marks, which is what makes tight caption alignment possible.
6
+
7
+ Install the optional dependency::
8
+
9
+ pip install "headvox[edge]"
10
+
11
+ HeadVox does not bundle or re-host any voice model; it calls the same public
12
+ service the `edge-tts` CLI uses.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ from typing import List, Optional
18
+
19
+ from . import SegmentAudio, WordMark
20
+
21
+
22
+ def _fmt_pct(v: float, unit: str = "%") -> str:
23
+ sign = "+" if v >= 0 else "-"
24
+ return f"{sign}{abs(int(round(v)))}{unit}"
25
+
26
+
27
+ class EdgeBackend:
28
+ """TTS backend backed by edge-tts neural voices."""
29
+
30
+ name = "edge"
31
+
32
+ def __init__(self, proxy: Optional[str] = None) -> None:
33
+ try:
34
+ import edge_tts # noqa: F401
35
+ except ImportError as exc: # pragma: no cover - import guard
36
+ raise ImportError(
37
+ "the 'edge' backend needs edge-tts; install with "
38
+ "pip install \"headvox[edge]\""
39
+ ) from exc
40
+ self.proxy = proxy
41
+
42
+ def synthesize_segment(self, text: str, settings) -> SegmentAudio:
43
+ return asyncio.run(self._run(text, settings))
44
+
45
+ async def _run(self, text: str, settings) -> SegmentAudio:
46
+ import edge_tts
47
+
48
+ comm = edge_tts.Communicate(
49
+ text,
50
+ voice=settings.voice or "en-US-EmmaMultilingualNeural",
51
+ rate=_fmt_pct(settings.rate_pct),
52
+ volume=_fmt_pct(settings.volume_pct),
53
+ pitch=_fmt_pct(settings.pitch_hz, "Hz"),
54
+ boundary="WordBoundary",
55
+ proxy=self.proxy,
56
+ )
57
+ audio = bytearray()
58
+ words: List[WordMark] = []
59
+ async for chunk in comm.stream():
60
+ if chunk["type"] == "audio":
61
+ audio += chunk["data"]
62
+ elif chunk["type"] == "WordBoundary":
63
+ start = int(chunk["offset"] / 10_000) # 100-ns ticks -> ms
64
+ dur = int(chunk["duration"] / 10_000)
65
+ words.append(WordMark(chunk["text"], start, start + dur))
66
+ duration_ms = words[-1].end_ms if words else 0
67
+ return SegmentAudio(
68
+ audio=bytes(audio),
69
+ audio_format="mp3",
70
+ duration_ms=duration_ms,
71
+ words=words,
72
+ )
73
+
@@ -0,0 +1,63 @@
1
+ """Offline "sine" backend.
2
+
3
+ Produces a soft tone per word instead of speech. It needs no network and no
4
+ extra packages, so it's what the test suite uses, and it's handy for checking
5
+ pacing, caption layout, and video muxing before spending real TTS calls.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import math
11
+ import struct
12
+ import wave
13
+
14
+ from . import SegmentAudio, WordMark
15
+ from ..script import iter_words
16
+
17
+ SAMPLE_RATE = 24_000
18
+
19
+
20
+ class SineBackend:
21
+ name = "sine"
22
+
23
+ def __init__(self, ms_per_word: int = 260, ms_per_cjk: int = 210, gap_ms: int = 40) -> None:
24
+ self.ms_per_word = ms_per_word
25
+ self.ms_per_cjk = ms_per_cjk
26
+ self.gap_ms = gap_ms
27
+
28
+ def synthesize_segment(self, text: str, settings) -> SegmentAudio:
29
+ speed = max(0.25, 1.0 + settings.rate_pct / 100.0)
30
+ freq = 220.0 * (2 ** (settings.pitch_hz / 1200.0)) if settings.pitch_hz else 220.0
31
+ frames = bytearray()
32
+ words = []
33
+ t = 0
34
+ for tok in iter_words(text):
35
+ if not any(ch.isalnum() for ch in tok):
36
+ continue # punctuation is silent
37
+ base = self.ms_per_cjk if len(tok) == 1 and ord(tok) > 0x2E80 else self.ms_per_word
38
+ dur = int(base / speed)
39
+ frames += _tone(freq, dur)
40
+ frames += _silence(self.gap_ms)
41
+ words.append(WordMark(tok, t, t + dur))
42
+ t += dur + self.gap_ms
43
+ buf = io.BytesIO()
44
+ with wave.open(buf, "wb") as w:
45
+ w.setnchannels(1)
46
+ w.setsampwidth(2)
47
+ w.setframerate(SAMPLE_RATE)
48
+ w.writeframes(bytes(frames))
49
+ return SegmentAudio(buf.getvalue(), "wav", t, words)
50
+
51
+
52
+ def _tone(freq: float, ms: int) -> bytes:
53
+ n = int(SAMPLE_RATE * ms / 1000)
54
+ fade = max(1, int(SAMPLE_RATE * 0.01))
55
+ out = bytearray()
56
+ for i in range(n):
57
+ env = min(1.0, i / fade, (n - i) / fade)
58
+ out += struct.pack("<h", int(6000 * env * math.sin(2 * math.pi * freq * i / SAMPLE_RATE)))
59
+ return bytes(out)
60
+
61
+
62
+ def _silence(ms: int) -> bytes:
63
+ return b"\x00\x00" * int(SAMPLE_RATE * ms / 1000)