|
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 |
2 | 3 | import json |
3 | | -from gtts import gTTS |
| 4 | +import edge_tts |
4 | 5 | from mutagen.mp3 import MP3 |
5 | 6 |
|
| 7 | +# en-US-GuyNeural = neutral male US voice (Silicon Valley style) |
| 8 | +VOICE = "en-US-GuyNeural" |
| 9 | +RATE = "+0%" # natural pace |
| 10 | + |
6 | 11 | SEGMENTS = [ |
7 | | - {"id": "intro", "text": "Introducing ebuild. EoS Embedded Build System."}, |
| 12 | + {"id": "intro", "text": "Introducing ebuild. The embedded build system for EoS."}, |
8 | 13 | {"id": "f1", "text": "Feature one. Cross-Compilation Toolchains. Target ARM, RISC-V, and x86 from a single host with automatic toolchain management."}, |
9 | 14 | {"id": "f2", "text": "Feature two. Incremental Builds. Content-addressed caching rebuilds only changed targets, cutting build times by 90 percent."}, |
10 | 15 | {"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."} |
13 | 18 | ] |
14 | 19 |
|
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