Skip to content

Commit 226a081

Browse files
committed
feat: switch TTS to edge-tts en-US-GuyNeural (neutral US accent)
1 parent 1ea55e1 commit 226a081

2 files changed

Lines changed: 47 additions & 37 deletions

File tree

.github/workflows/video-build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: |
3333
sudo apt-get update
3434
sudo apt-get install -y ffmpeg libcairo2-dev libpango1.0-dev
35-
pip install manim==0.18.1 gTTS mutagen
35+
pip install manim==0.18.1 edge-tts mutagen
3636
3737
- name: Generate narration audio (per-segment)
3838
run: |
@@ -73,3 +73,4 @@ jobs:
7373
fail_on_unmatched_files: false
7474
env:
7575
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76+

promo/generate_audio.py

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,54 @@
1-
"""Generate per-segment narration audio and output durations for Manim sync."""
1+
"""Generate per-segment narration using edge-tts (US English neural voice)."""
2+
import asyncio
23
import json
3-
from gtts import gTTS
4+
import edge_tts
45
from mutagen.mp3 import MP3
56

7+
# en-US-GuyNeural = neutral male US voice (Silicon Valley style)
8+
VOICE = "en-US-GuyNeural"
9+
RATE = "+0%" # natural pace
10+
611
SEGMENTS = [
7-
{"id": "intro", "text": "Introducing ebuild. EoS Embedded Build System."},
12+
{"id": "intro", "text": "Introducing ebuild. The embedded build system for EoS."},
813
{"id": "f1", "text": "Feature one. Cross-Compilation Toolchains. Target ARM, RISC-V, and x86 from a single host with automatic toolchain management."},
914
{"id": "f2", "text": "Feature two. Incremental Builds. Content-addressed caching rebuilds only changed targets, cutting build times by 90 percent."},
1015
{"id": "f3", "text": "Feature three. Dependency Resolution. DAG-based resolver handles diamond dependencies and circular includes automatically."},
11-
{"id": "arch", "text": "Under the hood, ebuild is built with C, Make, CMake, LLVM. The architecture flows from Parser, to Resolver, to Scheduler, to Compiler, to Linker."},
12-
{"id": "cta", "text": "ebuild. Open source and production ready. Visit github dot com slash embeddedos-org slash ebuild."},
16+
{"id": "arch", "text": "Under the hood, ebuild is built with C, Make, CMake, and LLVM. The architecture flows from Parser, to Resolver, to Scheduler, to Compiler, to Linker."},
17+
{"id": "cta", "text": "ebuild. Open source and production ready. Visit github dot com slash embeddedos-org slash ebuild."}
1318
]
1419

15-
durations = {}
16-
audio_files = []
17-
18-
for seg in SEGMENTS:
19-
filename = f"seg_{seg['id']}.mp3"
20-
tts = gTTS(text=seg["text"], lang="en", slow=False)
21-
tts.save(filename)
22-
dur = MP3(filename).info.length
23-
durations[seg["id"]] = round(dur + 0.5, 1) # add 0.5s padding
24-
audio_files.append(filename)
25-
print(f" {seg['id']}: {dur:.1f}s -> padded {durations[seg['id']]}s")
26-
27-
# Write durations JSON for Manim to read
28-
with open("durations.json", "w") as f:
29-
json.dump(durations, f, indent=2)
30-
31-
# Concatenate all segments into single narration.mp3
32-
import subprocess
33-
list_file = "concat_list.txt"
34-
with open(list_file, "w") as f:
35-
for af in audio_files:
36-
f.write(f"file '{af}'\n")
37-
38-
subprocess.run([
39-
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
40-
"-i", list_file, "-c", "copy", "narration.mp3"
41-
], check=True)
42-
43-
total = sum(durations.values())
44-
print(f"\nTotal narration: {total:.1f}s")
45-
print(f"Durations: {json.dumps(durations)}")
20+
21+
async def generate():
22+
durations = {}
23+
audio_files = []
24+
25+
for seg in SEGMENTS:
26+
filename = f"seg_{seg['id']}.mp3"
27+
communicate = edge_tts.Communicate(seg["text"], VOICE, rate=RATE)
28+
await communicate.save(filename)
29+
dur = MP3(filename).info.length
30+
durations[seg["id"]] = round(dur + 0.5, 1)
31+
audio_files.append(filename)
32+
print(f" {seg['id']}: {dur:.1f}s -> padded {durations[seg['id']]}s")
33+
34+
with open("durations.json", "w") as f:
35+
json.dump(durations, f, indent=2)
36+
37+
# Concatenate
38+
import subprocess
39+
with open("concat_list.txt", "w") as f:
40+
for af in audio_files:
41+
f.write(f"file '{af}'\n")
42+
43+
subprocess.run([
44+
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
45+
"-i", "concat_list.txt", "-c", "copy", "narration.mp3"
46+
], check=True)
47+
48+
total = sum(durations.values())
49+
print(f"\nVoice: {VOICE}")
50+
print(f"Total narration: {total:.1f}s")
51+
print(f"Durations: {json.dumps(durations)}")
52+
53+
54+
asyncio.run(generate())

0 commit comments

Comments
 (0)