D144: 配音+字幕管线开发·Agent可直接调用
- 新增 engines/tts-engine.py:Edge-TTS配音引擎,支持多角色音色配置 - 新增 engines/subtitle-renderer.py:字幕渲染引擎,SRT→PNG序列 - 更新 ENTRY.hdlp:工具链增加 tts-engine.py 和 subtitle-renderer.py - 依赖:pip install edge-tts pysrt cairosvg Pillow - 路径:video-ai-system/engines/tts-engine.py
This commit is contained in:
parent
c80d804bf9
commit
1eaa2b532f
@ -178,9 +178,11 @@ video-ai-system/
|
||||
| video-composer.js (FFmpeg拼接) | 自研 | ✅ (溶解过渡·faststart) |
|
||||
| image-api-adapter.js (Seedream生图) | 自研 | ✅ (苏白人设图已生成) |
|
||||
| svg-to-png.py (SVG→PNG渲染) | 自研 | ✅ (D144·CairoSVG·解决FFmpeg无SVG解码器) |
|
||||
| tts-engine.py (Edge-TTS配音) | MIT | ✅ (D144·中文语音·多角色音色配置) |
|
||||
| subtitle-renderer.py (字幕渲染) | 自研 | ⚠️ (PNG序列渲染·FFmpeg合成待完善) |
|
||||
| MoviePy v2.x | MIT | ⏳ 待评估 |
|
||||
| WhisperX + Subaligner | MIT | ⏳ |
|
||||
| Edge-TTS (一期) → GPT-SoVITS MIT (二期) | MIT | ⏳ |
|
||||
| GPT-SoVITS (二期音色克隆) | MIT | ⏳ |
|
||||
|
||||
## D135 八坑炼规矩 · cc-014
|
||||
|
||||
|
||||
319
video-ai-system/engines/subtitle-renderer.py
Normal file
319
video-ai-system/engines/subtitle-renderer.py
Normal file
@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Subtitle Renderer · 字幕渲染引擎
|
||||
=============================
|
||||
将 SRT 字幕文件渲染为 PNG 序列,再通过 FFmpeg 合成到视频中。
|
||||
|
||||
依赖:
|
||||
pip install cairosvg Pillow pysrt
|
||||
|
||||
用法:
|
||||
# 基本用法(SRT → PNG 序列)
|
||||
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/
|
||||
|
||||
# 指定视频尺寸(PNG 宽度匹配视频)
|
||||
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/ --width 1080 --height 1920
|
||||
|
||||
# 渲染后直接合成到视频
|
||||
python subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4
|
||||
|
||||
# 自定义字幕样式
|
||||
python subtitle-renderer.py --srt input.srt --font-size 48 --font-color white --bg-color black --position bottom
|
||||
|
||||
# 作为模块导入
|
||||
from subtitle_renderer import render_subtitles
|
||||
render_subtitles("input.srt", "./subtitles-png/")
|
||||
|
||||
字幕样式配置:
|
||||
--font-size : 字体大小(默认 36)
|
||||
--font-color : 字体颜色(默认 white)
|
||||
--bg-color : 背景色(默认 semi-transparent black)
|
||||
--position : 位置(top / middle / bottom,默认 bottom)
|
||||
--margin-bottom : 底部边距(默认 100px)
|
||||
|
||||
路径:
|
||||
video-ai-system/engines/subtitle-renderer.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import pysrt
|
||||
except ImportError:
|
||||
print("[ERROR] 缺少依赖:pysrt")
|
||||
print("请先安装:pip install pysrt")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import cairosvg
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ImportError:
|
||||
print("[ERROR] 缺少依赖:cairosvg Pillow")
|
||||
print("请先安装:pip install cairosvg Pillow")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 默认字幕样式
|
||||
DEFAULT_STYLE = {
|
||||
"font_size": 36,
|
||||
"font_color": "white",
|
||||
"bg_color": "rgba(0, 0, 0, 0.6)",
|
||||
"position": "bottom", # top / middle / bottom
|
||||
"margin_bottom": 100,
|
||||
"margin_horizontal": 60,
|
||||
"stroke_color": "black",
|
||||
"stroke_width": 2,
|
||||
"video_width": 1080,
|
||||
"video_height": 1920,
|
||||
}
|
||||
|
||||
|
||||
def render_subtitle_png(
|
||||
text: str,
|
||||
output_path: str,
|
||||
width: int = 1080,
|
||||
height: int = 200,
|
||||
style: dict = None
|
||||
) -> bool:
|
||||
"""
|
||||
渲染单条字幕为 PNG(带背景)
|
||||
|
||||
:param text: 字幕文本
|
||||
:param output_path: 输出 PNG 路径
|
||||
:param width: PNG 宽度(匹配视频宽度)
|
||||
:param height: PNG 高度
|
||||
:param style: 字幕样式字典
|
||||
:return: 是否成功
|
||||
"""
|
||||
if style is None:
|
||||
style = DEFAULT_STYLE
|
||||
|
||||
try:
|
||||
# 创建透明背景 PNG
|
||||
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# 字体(使用系统字体)
|
||||
font_size = style.get("font_size", 36)
|
||||
try:
|
||||
font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", font_size)
|
||||
except:
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# 计算文本尺寸
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# 居中位置
|
||||
x = (width - text_width) // 2
|
||||
y = (height - text_height) // 2
|
||||
|
||||
# 绘制背景(半透明黑底)
|
||||
bg_padding = 20
|
||||
bg_x1 = x - bg_padding
|
||||
bg_y1 = y - bg_padding
|
||||
bg_x2 = x + text_width + bg_padding
|
||||
bg_y2 = y + text_height + bg_padding
|
||||
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=(0, 0, 0, 160))
|
||||
|
||||
# 绘制描边
|
||||
stroke_width = style.get("stroke_width", 2)
|
||||
stroke_color = style.get("stroke_color", "black")
|
||||
for offset in range(-stroke_width, stroke_width + 1):
|
||||
draw.text((x + offset, y), text, font=font, fill=stroke_color)
|
||||
draw.text((x, y + offset), text, font=font, fill=stroke_color)
|
||||
|
||||
# 绘制主文本
|
||||
font_color = style.get("font_color", "white")
|
||||
draw.text((x, y), text, font=font, fill=font_color)
|
||||
|
||||
# 保存
|
||||
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
||||
img.save(output_path, "PNG")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 渲染字幕失败:{e}")
|
||||
return False
|
||||
|
||||
|
||||
def render_subtitles(
|
||||
srt_path: str,
|
||||
output_dir: str,
|
||||
style: dict = None
|
||||
) -> dict:
|
||||
"""
|
||||
渲染 SRT 字幕为 PNG 序列
|
||||
|
||||
:param srt_path: SRT 文件路径
|
||||
:param output_dir: 输出目录
|
||||
:param style: 字幕样式字典
|
||||
:return: {idx: {"png": png_path, "start": start_time, "end": end_time, "text": text}}
|
||||
"""
|
||||
if not os.path.isfile(srt_path):
|
||||
print(f"[ERROR] SRT 文件不存在:{srt_path}")
|
||||
return {}
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 加载 SRT
|
||||
subs = pysrt.open(srt_path, encoding="utf-8")
|
||||
print(f"[INFO] 找到 {len(subs)} 条字幕,开始渲染 PNG 序列...")
|
||||
|
||||
results = {}
|
||||
for sub in subs:
|
||||
idx = str(sub.index).zfill(4)
|
||||
text = sub.text.strip()
|
||||
start_time = sub.start.ordinal # 毫秒
|
||||
end_time = sub.end.ordinal
|
||||
|
||||
# 渲染 PNG
|
||||
png_path = os.path.join(output_dir, f"{idx}.png")
|
||||
video_width = style.get("video_width", 1080) if style else 1080
|
||||
png_height = style.get("font_size", 36) * 3 if style else 108
|
||||
|
||||
ok = render_subtitle_png(text, png_path, width=video_width, height=png_height, style=style)
|
||||
if ok:
|
||||
results[idx] = {
|
||||
"png": png_path,
|
||||
"start": start_time,
|
||||
"end": end_time,
|
||||
"text": text
|
||||
}
|
||||
|
||||
print(f"[OK] 字幕 PNG 序列渲染完成:{len(results)}/{len(subs)} 成功")
|
||||
return results
|
||||
|
||||
|
||||
def burn_subtitles_to_video(
|
||||
video_path: str,
|
||||
subtitles: dict,
|
||||
output_path: str,
|
||||
video_width: int = 1080,
|
||||
video_height: int = 1920
|
||||
) -> bool:
|
||||
"""
|
||||
将 PNG 字幕序列合成到视频中(使用 FFmpeg overlay 滤镜)
|
||||
|
||||
:param video_path: 输入视频路径
|
||||
:param subtitles: render_subtitles 返回的字典
|
||||
:param output_path: 输出视频路径
|
||||
:param video_width: 视频宽度
|
||||
:param video_height: 视频高度
|
||||
:return: 是否成功
|
||||
"""
|
||||
if not subtitles:
|
||||
print("[ERROR] 没有字幕数据")
|
||||
return False
|
||||
|
||||
# 生成 FFmpeg 复杂滤镜表达式
|
||||
# 思路:为每个字幕 PNG 创建带有时序的 overlay
|
||||
# 简化版:使用 subtitles 滤镜(需要 FFmpeg 编译时启用)
|
||||
#
|
||||
# 由于本机 FFmpeg 无 subtitles 滤镜,改用 drawtext 方案
|
||||
# 如果 drawtext 也不可用,则生成带透明度的 PNG 序列,用 overlay 滤镜
|
||||
|
||||
print("[INFO] 生成字幕叠加滤镜脚本...")
|
||||
|
||||
# 简化方案:生成所有字幕 PNG 后,用 FFmpeg 的 overlay 滤镜
|
||||
# 为每个时间段启用对应的 PNG
|
||||
# 这需要用 FFmpeg 的 `enable` 表达式
|
||||
|
||||
filter_complex = []
|
||||
input_count = 1 # 第0个输入是视频
|
||||
|
||||
for idx, sub in subtitles.items():
|
||||
png_path = sub["png"]
|
||||
start_sec = sub["start"] / 1000.0
|
||||
end_sec = sub["end"] / 1000.0
|
||||
|
||||
# 添加 PNG 输入
|
||||
filter_complex.append(f"[1:v]scale={video_width}:-1[png{idx}]")
|
||||
# 简化:直接用一条命令处理所有字幕(实际应该用动态叠加)
|
||||
|
||||
input_count += 1
|
||||
|
||||
# 实际实现:用一条简单的 FFmpeg 命令测试
|
||||
# 这里先输出一个简化版:把第一条字幕叠加上去
|
||||
first_png = list(subtitles.values())[0]["png"]
|
||||
|
||||
cmd = (
|
||||
f'ffmpeg -i "{video_path}" -i "{first_png}" '
|
||||
f'-filter_complex "[0:v][1:v] overlay=0:(H-h) [out]" '
|
||||
f'-map "[out]" -c:a copy "{output_path}" -y'
|
||||
)
|
||||
|
||||
print(f"[INFO] FFmpeg 命令:{cmd}")
|
||||
|
||||
# 实际应该用更准确的方案,这里先输出架构
|
||||
print(f"\n[INFO] 字幕合成需要更复杂的 FFmpeg 滤镜表达式。")
|
||||
print(f"[INFO] 建议方案:")
|
||||
print(f" 1. 使用 FFmpeg 的 `drawtext` 滤镜(需要重新编译 FFmpeg)")
|
||||
print(f" 2. 或使用专业字幕工具(如 Aegisub)生成 ASS,再用 FFmpeg 烧录")
|
||||
print(f"\n[INFO] 当前已生成字幕 PNG 序列在:{os.path.dirname(first_png)}")
|
||||
print(f"[INFO] 可手动用 FFmpeg 或视频编辑软件合成。")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Subtitle Renderer · 字幕渲染引擎",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
python subtitle-renderer.py --srt input.srt --output-dir ./subtitles-png/
|
||||
python subtitle-renderer.py --srt input.srt --video input.mp4 --output output.mp4
|
||||
python subtitle-renderer.py --srt input.srt --font-size 48 --position bottom
|
||||
"""
|
||||
)
|
||||
parser.add_argument("--srt", required=True, help="SRT 字幕文件路径")
|
||||
parser.add_argument("--output-dir", help="PNG 序列输出目录")
|
||||
parser.add_argument("--video", help="输入视频路径(可选,用于直接合成)")
|
||||
parser.add_argument("--output", help="输出视频路径(配合 --video 使用)")
|
||||
parser.add_argument("--font-size", type=int, default=36, help="字体大小")
|
||||
parser.add_argument("--font-color", default="white", help="字体颜色")
|
||||
parser.add_argument("--bg-color", default="rgba(0,0,0,0.6)", help="背景色")
|
||||
parser.add_argument("--position", default="bottom", choices=["top", "middle", "bottom"], help="字幕位置")
|
||||
parser.add_argument("--video-width", type=int, default=1080, help="视频宽度")
|
||||
parser.add_argument("--video-height", type=int, default=1920, help="视频高度")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 构建样式字典
|
||||
style = {
|
||||
"font_size": args.font_size,
|
||||
"font_color": args.font_color,
|
||||
"bg_color": args.bg_color,
|
||||
"position": args.position,
|
||||
"video_width": args.video_width,
|
||||
"video_height": args.video_height,
|
||||
}
|
||||
|
||||
# 渲染 PNG 序列
|
||||
output_dir = args.output_dir or "./subtitles-png/"
|
||||
subtitles = render_subtitles(args.srt, output_dir, style)
|
||||
|
||||
if not subtitles:
|
||||
sys.exit(1)
|
||||
|
||||
# 如果指定了视频,则合成
|
||||
if args.video and args.output:
|
||||
burn_subtitles_to_video(args.video, subtitles, args.output, args.video_width, args.video_height)
|
||||
|
||||
print(f"\n[OK] 字幕 PNG 序列已生成:{output_dir}")
|
||||
print(f"[INFO] 共 {len(subtitles)} 条字幕")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
311
video-ai-system/engines/tts-engine.py
Normal file
311
video-ai-system/engines/tts-engine.py
Normal file
@ -0,0 +1,311 @@
|
||||
#!/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()
|
||||
Loading…
x
Reference in New Issue
Block a user