75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
# LIB · TTS 配音模块
|
|
# D180 · 2026-07-10
|
|
"""Edge-TTS 配音引擎 · 多角色音色"""
|
|
import asyncio, edge_tts, os
|
|
|
|
# 角色音色表
|
|
VOICES = {
|
|
"林昊": "zh-CN-YunxiNeural", # 青年男声·迷茫虚弱
|
|
"旁白": "zh-CN-YunyangNeural", # 新闻男声
|
|
"默认女": "zh-CN-XiaoxiaoNeural", # 女声
|
|
"默认男": "zh-CN-YunxiNeural",
|
|
}
|
|
|
|
FFMPEG = 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)
|
|
async def _run():
|
|
await edge_tts.Communicate(text, voice, rate=rate).save(output_path)
|
|
asyncio.run(_run())
|
|
return output_path 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 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")
|