135 lines
5.4 KiB
Python
Raw Permalink Normal View History

# LIB · TTS 配音模块 + 自动字幕生成
# D180 · 2026-07-10
"""Edge-TTS 配音引擎 · 多角色音色 · 自动SRT字幕"""
import asyncio, edge_tts, os, json, shutil
# 角色音色表 · 从 engines/tts-engine.py 加载(单一真相源)
try:
import sys, os as _os
_engine_dir = _os.path.join(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), "engines")
if _engine_dir not in sys.path: sys.path.insert(0, _engine_dir)
from tts_engine import DEFAULT_VOICES as _ENGINE_VOICES
VOICES = {name: cfg["voice"] for name, cfg in _ENGINE_VOICES.items()}
except Exception:
VOICES = {
"苏白": "zh-CN-XiaoxiaoNeural",
"诸葛风": "zh-CN-YunxiNeural",
"萧灵汐": "zh-CN-XiaoyiNeural",
"王执事": "zh-CN-YunyangNeural",
"林昊": "zh-CN-YunxiNeural",
"旁白": "zh-CN-YunyangNeural",
"默认女": "zh-CN-XiaoxiaoNeural",
"默认男": "zh-CN-YunxiNeural",
}
FFMPEG = shutil.which("ffmpeg") or os.path.expanduser("~/.workbuddy/binaries/ffmpeg.exe")
def tts(text, output_path, voice="zh-CN-YunxiNeural", rate="+0%"):
"""生成语音 mp3返回时间戳用于字幕"""
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
words = []
async def _run():
comm = edge_tts.Communicate(text, voice, rate=rate)
sub = edge_tts.SubMaker()
with open(output_path, "wb") as f:
async for chunk in comm.stream():
if chunk["type"] == "audio":
f.write(chunk["data"])
elif chunk["type"] == "SentenceBoundary":
words.append({
"text": chunk["text"],
"start": chunk["offset"] / 10000000,
"end": (chunk["offset"] + chunk["duration"]) / 10000000
})
asyncio.run(_run())
return {"path": output_path, "words": words} if os.path.exists(output_path) else None
def tts_for_character(text, character, output_path, rate="+0%"):
"""按角色名查音色生成"""
voice = VOICES.get(character, "zh-CN-YunxiNeural")
return tts(text, output_path, voice, rate)
def words_to_srt(words, output_path, offset=0):
"""句时间戳 → SRT 字幕文件"""
if not words:
return None
lines = []
for i, w in enumerate(words):
def fmt(t):
ms = int((offset + t) * 1000)
h = ms // 3600000; ms %= 3600000
m = ms // 60000; ms %= 60000
s = ms // 1000; ms %= 1000
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
lines.append(f"{i+1}")
lines.append(f"{fmt(w['start'])} --> {fmt(w['end'])}")
lines.append(w["text"])
lines.append("")
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
return output_path
def burn_subtitle(video_path, srt_path, output_path):
"""烧录字幕到视频"""
if not os.path.exists(FFMPEG):
return f"ffmpeg not found at {FFMPEG}"
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
import subprocess
srt_esc = srt_path.replace("\\", "/").replace(":", "\\:")
r = subprocess.run([
FFMPEG, "-y",
"-i", video_path,
"-vf", f"subtitles='{srt_esc}':force_style='Fontsize=24,Alignment=2,BorderStyle=3,Outline=1,Shadow=1'",
"-c:a", "copy",
output_path
], capture_output=True, text=True)
return output_path if r.returncode == 0 else r.stderr[:300]
def mix_audio_video(video_path, audio_path, output_path, video_vol=0.3):
"""FFmpeg 合成视频+音频"""
if not os.path.exists(FFMPEG):
return f"ffmpeg not found at {FFMPEG}"
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
import subprocess
r = subprocess.run([
FFMPEG, "-y",
"-i", video_path,
"-i", audio_path,
"-filter_complex", f"[0:a]volume={video_vol}[va];[1:a]volume=1.0[aa];[va][aa]amix=inputs=2:duration=first",
"-c:v", "copy",
output_path
], capture_output=True, text=True)
return output_path if r.returncode == 0 else r.stderr
def add_subtitle(video_path, subtitle_text, output_path):
"""FFmpeg 添加字幕"""
if not os.path.exists(FFMPEG):
return f"ffmpeg not found at {FFMPEG}"
import subprocess
r = subprocess.run([
FFMPEG, "-y",
"-i", video_path,
"-vf", f"drawtext=text='{subtitle_text}':fontsize=28:fontcolor=white:borderw=2:bordercolor=black:x=(w-text_w)/2:y=h-120",
"-c:a", "copy",
output_path
], capture_output=True, text=True)
return output_path if r.returncode == 0 else r.stderr
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python tts.py tts <text> <output> [voice]")
print(" python tts.py char <text> <character> <output>")
print(" python tts.py mix <video> <audio> <output>")
print(f"\nVoices: {list(VOICES.keys())}")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "tts":
voice = sys.argv[4] if len(sys.argv) > 4 else "zh-CN-YunxiNeural"
print(tts(sys.argv[2], sys.argv[3], voice) or "FAILED")
elif cmd == "char":
print(tts_for_character(sys.argv[2], sys.argv[3], sys.argv[4]) or "FAILED")
elif cmd == "mix":
print(mix_audio_video(sys.argv[2], sys.argv[3], sys.argv[4]) or "FAILED")