cang-ying/engines/tts-engine.py

312 lines
9.4 KiB
Python
Raw Normal View History

#!/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()