312 lines
9.4 KiB
Python
312 lines
9.4 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
TTS Engine · Edge-TTS 配音引擎
|
|||
|
|
==================================
|
|||
|
|
为视频AI系统提供文本转语音能力,支持多角色音色配置。
|
|||
|
|
|
|||
|
|
依赖:
|
|||
|
|
pip install edge-tts
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
# 基本用法(默认中文女声)
|
|||
|
|
python tts-engine.py --text "你好世界" --output output.mp3
|
|||
|
|
|
|||
|
|
# 指定角色(从配置文件读取音色)
|
|||
|
|
python tts-engine.py --text "未来的天下第一宗!" --character "苏白" --output su-bai.mp3
|
|||
|
|
|
|||
|
|
# 指定语音(Edge-TTS 语音名)
|
|||
|
|
python tts-engine.py --text "Hello World" --voice "en-US-JennyNeural" --output hello.mp3
|
|||
|
|
|
|||
|
|
# 调整语速/音调/音量
|
|||
|
|
python tts-engine.py --text "你好" --rate "+20%" --pitch "+5Hz" --volume "+10%" --output output.mp3
|
|||
|
|
|
|||
|
|
# 批量生成(从SRT字幕文件)
|
|||
|
|
python tts-engine.py --srt input.srt --output-dir ./audio/ --character "苏白"
|
|||
|
|
|
|||
|
|
# 作为模块导入
|
|||
|
|
from tts_engine import generate_speech
|
|||
|
|
generate_speech("你好世界", "output.mp3", voice="zh-CN-XiaoxiaoNeural")
|
|||
|
|
|
|||
|
|
角色音色配置:
|
|||
|
|
video-ai-system/config/voices.json
|
|||
|
|
|
|||
|
|
路径:
|
|||
|
|
video-ai-system/engines/tts-engine.py
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import edge_tts
|
|||
|
|
except ImportError:
|
|||
|
|
print("[ERROR] 缺少依赖:edge-tts")
|
|||
|
|
print("请先安装:pip install edge-tts")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 默认角色音色配置
|
|||
|
|
DEFAULT_VOICES = {
|
|||
|
|
"苏白": {
|
|||
|
|
"voice": "zh-CN-XiaoxiaoNeural", # 阳光少年音
|
|||
|
|
"rate": "+5%",
|
|||
|
|
"pitch": "+0Hz"
|
|||
|
|
},
|
|||
|
|
"诸葛风": {
|
|||
|
|
"voice": "zh-CN-YunxiNeural", # 沉稳男声
|
|||
|
|
"rate": "+0%",
|
|||
|
|
"pitch": "-5Hz"
|
|||
|
|
},
|
|||
|
|
"萧灵汐": {
|
|||
|
|
"voice": "zh-CN-XiaoyiNeural", # 清冷女声
|
|||
|
|
"rate": "+0%",
|
|||
|
|
"pitch": "+0Hz"
|
|||
|
|
},
|
|||
|
|
"王执事": {
|
|||
|
|
"voice": "zh-CN-YunyangNeural", # 中年男声
|
|||
|
|
"rate": "+0%",
|
|||
|
|
"pitch": "-10Hz"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_voice_config(config_path: str = None) -> dict:
|
|||
|
|
"""加载角色音色配置文件"""
|
|||
|
|
if config_path and os.path.isfile(config_path):
|
|||
|
|
try:
|
|||
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|||
|
|
return json.load(f)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[WARN] 无法读取配置文件 {config_path}:{e}")
|
|||
|
|
print("[INFO] 使用默认音色配置")
|
|||
|
|
return DEFAULT_VOICES
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def generate_speech_async(
|
|||
|
|
text: str,
|
|||
|
|
output_path: str,
|
|||
|
|
voice: str = "zh-CN-XiaoxiaoNeural",
|
|||
|
|
rate: str = "+0%",
|
|||
|
|
pitch: str = "+0Hz",
|
|||
|
|
volume: str = "+0%"
|
|||
|
|
) -> bool:
|
|||
|
|
"""
|
|||
|
|
异步生成语音(Edge-TTS)
|
|||
|
|
|
|||
|
|
:param text: 要合成的文本
|
|||
|
|
:param output_path: 输出音频文件路径
|
|||
|
|
:param voice: Edge-TTS 语音名
|
|||
|
|
:param rate: 语速(如 "+20%"、"-10%")
|
|||
|
|
:param pitch: 音调(如 "+5Hz"、"-10Hz")
|
|||
|
|
:param volume: 音量(如 "+10%"、"-5%")
|
|||
|
|
:return: 是否成功
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 确保输出目录存在
|
|||
|
|
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|||
|
|
|
|||
|
|
communicate = edge_tts.Communicate(
|
|||
|
|
text, voice,
|
|||
|
|
rate=rate, pitch=pitch, volume=volume
|
|||
|
|
)
|
|||
|
|
await communicate.save(output_path)
|
|||
|
|
|
|||
|
|
# 验证输出文件
|
|||
|
|
if os.path.isfile(output_path):
|
|||
|
|
file_size = os.path.getsize(output_path)
|
|||
|
|
print(f"[OK] 生成语音:{output_path} ({file_size // 1024} KB)")
|
|||
|
|
print(f" 语音:{voice} | 语速:{rate} | 音调:{pitch}")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
print(f"[ERROR] 输出文件未生成:{output_path}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[ERROR] 生成语音失败:{e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_speech(
|
|||
|
|
text: str,
|
|||
|
|
output_path: str,
|
|||
|
|
voice: str = "zh-CN-XiaoxiaoNeural",
|
|||
|
|
rate: str = "+0%",
|
|||
|
|
pitch: str = "+0Hz",
|
|||
|
|
volume: str = "+0%"
|
|||
|
|
) -> bool:
|
|||
|
|
"""
|
|||
|
|
同步包装器(供外部调用)
|
|||
|
|
|
|||
|
|
:param text: 要合成的文本
|
|||
|
|
:param output_path: 输出音频文件路径
|
|||
|
|
:param voice: Edge-TTS 语音名
|
|||
|
|
:param rate: 语速
|
|||
|
|
:param pitch: 音调
|
|||
|
|
:param volume: 音量
|
|||
|
|
:return: 是否成功
|
|||
|
|
"""
|
|||
|
|
return asyncio.run(generate_speech_async(text, output_path, voice, rate, pitch, volume))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_by_character(
|
|||
|
|
text: str,
|
|||
|
|
output_path: str,
|
|||
|
|
character: str,
|
|||
|
|
config: dict = None
|
|||
|
|
) -> bool:
|
|||
|
|
"""
|
|||
|
|
按角色名生成语音(自动读取音色配置)
|
|||
|
|
|
|||
|
|
:param text: 要合成的文本
|
|||
|
|
:param output_path: 输出音频文件路径
|
|||
|
|
:param character: 角色名(如 "苏白")
|
|||
|
|
:param config: 音色配置字典(可选,默认加载 DEFAULT_VOICES)
|
|||
|
|
:return: 是否成功
|
|||
|
|
"""
|
|||
|
|
if config is None:
|
|||
|
|
config = load_voice_config()
|
|||
|
|
|
|||
|
|
if character not in config:
|
|||
|
|
print(f"[WARN] 角色 '{character}' 未配置音色,使用默认音色")
|
|||
|
|
return generate_speech(text, output_path)
|
|||
|
|
|
|||
|
|
voice_config = config[character]
|
|||
|
|
return generate_speech(
|
|||
|
|
text, output_path,
|
|||
|
|
voice=voice_config.get("voice", "zh-CN-XiaoxiaoNeural"),
|
|||
|
|
rate=voice_config.get("rate", "+0%"),
|
|||
|
|
pitch=voice_config.get("pitch", "+0Hz")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def process_srt(srt_path: str, output_dir: str, character: str = None, config: dict = None):
|
|||
|
|
"""
|
|||
|
|
从SRT字幕文件批量生成语音
|
|||
|
|
|
|||
|
|
:param srt_path: SRT 文件路径
|
|||
|
|
:param output_dir: 输出目录
|
|||
|
|
:param character: 角色名(所有台词用同一音色)
|
|||
|
|
:param config: 音色配置字典
|
|||
|
|
"""
|
|||
|
|
if not os.path.isfile(srt_path):
|
|||
|
|
print(f"[ERROR] SRT 文件不存在:{srt_path}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
os.makedirs(output_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 简单SRT解析(按空行分割)
|
|||
|
|
with open(srt_path, "r", encoding="utf-8") as f:
|
|||
|
|
content = f.read()
|
|||
|
|
|
|||
|
|
blocks = content.strip().split("\n\n")
|
|||
|
|
print(f"[INFO] 找到 {len(blocks)} 条字幕,开始生成语音...")
|
|||
|
|
|
|||
|
|
success = 0
|
|||
|
|
for block in blocks:
|
|||
|
|
lines = block.strip().split("\n")
|
|||
|
|
if len(lines) < 3:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
idx = lines[0].strip()
|
|||
|
|
# timeine = lines[1].strip() # 暂不使用时序
|
|||
|
|
text = " ".join(lines[2:]).strip()
|
|||
|
|
|
|||
|
|
if not text:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
output_path = os.path.join(output_dir, f"{idx.zfill(4)}.mp3")
|
|||
|
|
|
|||
|
|
if character:
|
|||
|
|
ok = generate_by_character(text, output_path, character, config)
|
|||
|
|
else:
|
|||
|
|
ok = generate_speech(text, output_path)
|
|||
|
|
|
|||
|
|
if ok:
|
|||
|
|
success += 1
|
|||
|
|
|
|||
|
|
print(f"\n[INFO] SRT批量生成完成:{success}/{len(blocks)} 成功")
|
|||
|
|
return success == len(blocks)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def list_voices():
|
|||
|
|
"""列出所有可用的Edge-TTS语音"""
|
|||
|
|
print("[INFO] 正在获取可用语音列表...\n")
|
|||
|
|
asyncio.run(_list_voices_async())
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _list_voices_async():
|
|||
|
|
"""异步列出语音"""
|
|||
|
|
voices = await edge_tts.list_voices()
|
|||
|
|
print("中文语音:")
|
|||
|
|
for v in voices:
|
|||
|
|
if v["Locale"].startswith("zh-"):
|
|||
|
|
print(f" {v['ShortName']:30s} - {v['FriendlyName']}")
|
|||
|
|
print(f"\n共 {len([v for v in voices if v['Locale'].startswith('zh-')])} 个中文语音")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(
|
|||
|
|
description="TTS Engine · Edge-TTS 配音引擎",
|
|||
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|||
|
|
epilog="""
|
|||
|
|
示例:
|
|||
|
|
python tts-engine.py --text "你好世界" --output output.mp3
|
|||
|
|
python tts-engine.py --text "未来的天下第一宗!" --character "苏白" --output su-bai.mp3
|
|||
|
|
python tts-engine.py --text "Hello" --voice "en-US-JennyNeural" --output hello.mp3
|
|||
|
|
python tts-engine.py --srt input.srt --output-dir ./audio/ --character "苏白"
|
|||
|
|
python tts-engine.py --list-voices
|
|||
|
|
"""
|
|||
|
|
)
|
|||
|
|
parser.add_argument("--text", help="要合成的文本")
|
|||
|
|
parser.add_argument("--output", help="输出音频文件路径")
|
|||
|
|
parser.add_argument("--voice", default="zh-CN-XiaoxiaoNeural", help="Edge-TTS 语音名")
|
|||
|
|
parser.add_argument("--character", help="角色名(从配置文件读取音色)")
|
|||
|
|
parser.add_argument("--rate", default="+0%", help="语速(如 +20%%、-10%%)")
|
|||
|
|
parser.add_argument("--pitch", default="+0Hz", help="音调(如 +5Hz、-10Hz)")
|
|||
|
|
parser.add_argument("--volume", default="+0%", help="音量(如 +10%%、-5%%)")
|
|||
|
|
parser.add_argument("--srt", help="从SRT字幕文件批量生成")
|
|||
|
|
parser.add_argument("--output-dir", help="批量生成时的输出目录")
|
|||
|
|
parser.add_argument("--config", help="角色音色配置文件路径")
|
|||
|
|
parser.add_argument("--list-voices", action="store_true", help="列出所有可用语音")
|
|||
|
|
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
if args.list_voices:
|
|||
|
|
list_voices()
|
|||
|
|
sys.exit(0)
|
|||
|
|
|
|||
|
|
if args.srt:
|
|||
|
|
# 批量模式
|
|||
|
|
if not args.output_dir:
|
|||
|
|
print("[ERROR] 批量模式需要指定 --output-dir")
|
|||
|
|
sys.exit(1)
|
|||
|
|
config = load_voice_config(args.config)
|
|||
|
|
ok = process_srt(args.srt, args.output_dir, args.character, config)
|
|||
|
|
sys.exit(0 if ok else 1)
|
|||
|
|
|
|||
|
|
if not args.text or not args.output:
|
|||
|
|
print("[ERROR] 需要指定 --text 和 --output")
|
|||
|
|
parser.print_help()
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 单文件模式
|
|||
|
|
if args.character:
|
|||
|
|
config = load_voice_config(args.config)
|
|||
|
|
ok = generate_by_character(args.text, args.output, args.character, config)
|
|||
|
|
else:
|
|||
|
|
ok = generate_speech(
|
|||
|
|
args.text, args.output,
|
|||
|
|
voice=args.voice, rate=args.rate,
|
|||
|
|
pitch=args.pitch, volume=args.volume
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
sys.exit(0 if ok else 1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|