216 lines
8.6 KiB
Python
216 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
VoxCPM嘉宾语音生成脚本 - 第八章:韩信的入场券
|
||
功能:为四位嘉宾(Graham、Dmitri、Amita、穆罕默德)生成语音
|
||
"""
|
||
import os
|
||
import sys
|
||
import soundfile as sf
|
||
import numpy as np
|
||
import time
|
||
|
||
# 设置路径
|
||
WORKSPACE = "/root/tts"
|
||
VOXCPM_DIR = os.path.join(WORKSPACE, "VoxCPM")
|
||
OUTPUT_DIR = os.path.join(WORKSPACE, "podcast_audios", "chapter8_voxcpm")
|
||
REFERENCE_DIR = os.path.join(WORKSPACE, "hosts")
|
||
|
||
# 确保目录存在
|
||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||
print(f"✅ 输出目录创建成功: {OUTPUT_DIR}")
|
||
|
||
# 添加VoxCPM到Python路径
|
||
sys.path.insert(0, os.path.join(VOXCPM_DIR, "src"))
|
||
print(f"✅ 添加VoxCPM路径: {os.path.join(VOXCPM_DIR, 'src')}")
|
||
|
||
# 导入VoxCPM
|
||
from voxcpm.core import VoxCPM
|
||
|
||
# 模型路径
|
||
LOCAL_MODEL_PATH = os.path.join(VOXCPM_DIR, "models", "openbmb__VoxCPM1.5")
|
||
if not os.path.exists(LOCAL_MODEL_PATH):
|
||
LOCAL_MODEL_PATH = os.path.join(VOXCPM_DIR, "models", "VoxCPM1.5")
|
||
if not os.path.exists(LOCAL_MODEL_PATH):
|
||
print(f"❌ 找不到模型路径")
|
||
sys.exit(1)
|
||
print(f"✅ 模型路径: {LOCAL_MODEL_PATH}")
|
||
|
||
# 嘉宾配置
|
||
GUESTS = {
|
||
"graham": {
|
||
"name": "Graham Cox",
|
||
"reference_file": None, # 使用默认音色
|
||
"description": "Palo Alto科技巨头CMO,技术乐观主义者",
|
||
"dialogues": [
|
||
{
|
||
"id": "tech_gap",
|
||
"text": "等等,主持人,我觉得你漏掉了一个关键变量——技术代差。2003年伊拉克战争,美军只用42天就推翻了萨达姆。2001年阿富汗,美军用精确制导炸弹摧毁了所有塔利班据点。这说明什么?战争形态已经变了。你还在用冷战思维分析地缘政治?不好意思,在这个时代,芯片比坦克好使,代码比航母管用。",
|
||
"filename": "graham_tech_gap.wav"
|
||
},
|
||
{
|
||
"id": "tom_clancy",
|
||
"text": "哦!说到这个,我必须提一下《熊与龙》!2000年出版,预言了中俄联合对抗美国。当时所有人都在笑,说这是科幻小说。结果呢?2022年俄乌战争,中俄真的无上限了!这就是为什么我收集了60本签名版——克兰西是地缘政治界的先知!",
|
||
"filename": "graham_tom_clancy.wav"
|
||
}
|
||
]
|
||
},
|
||
"dmitri": {
|
||
"name": "Dmitri Volkov",
|
||
"reference_file": None, # 使用默认音色
|
||
"description": "莫斯科国际关系学院副教授,能源地缘政治专家",
|
||
"dialogues": [
|
||
{
|
||
"id": "energy_ace",
|
||
"text": "主持人,我同意技术很重要,但让我补充一点——能源才是终极王牌。2006年天然气涨价,欧洲人是怎么颤抖的?中国能成为世界工厂,恰恰是因为俄罗斯的能源支撑。西伯利亚的天然气管道,才是真正的入场券。没有俄罗斯的能源,中国凭什么24小时开工?",
|
||
"filename": "dmitri_energy_ace.wav"
|
||
},
|
||
{
|
||
"id": "russia_pain",
|
||
"text": "因为你没打过真正的仗,年轻人。俄罗斯在车臣打了两场仗,死了2万人,才学会什么叫持久战。中国选择忍,不是怂,是聪明。等你的航母掉头去阿富汗,我就可以闷声发大财。这就是战略耐心。",
|
||
"filename": "dmitri_russia_pain.wav"
|
||
}
|
||
]
|
||
},
|
||
"amita": {
|
||
"name": "Amita Sharma",
|
||
"reference_file": None, # 使用默认音色
|
||
"description": "孟买政策研究中心高级研究员,印度视角",
|
||
"dialogues": [
|
||
{
|
||
"id": "india_alternative",
|
||
"text": "等一下,两位。你们说的世界工厂,好像默认了中国模式是唯一的。但让我提醒一下——2008年之后,班加罗尔正在崛起。印度的软件外包,墨西哥的近岸制造,越南的流水线...世界工厂不只有一个。主持人,你为什么只讲中国?",
|
||
"filename": "amita_india_alternative.wav"
|
||
}
|
||
]
|
||
},
|
||
"mohammed": {
|
||
"name": "穆罕默德 Al-Fayed",
|
||
"reference_file": None, # 使用默认音色
|
||
"description": "开罗大学政治学教授,中东问题专家",
|
||
"dialogues": [
|
||
{
|
||
"id": "factory_trap",
|
||
"text": "各位说的都很好,但我想问一个更根本的问题——世界工厂这个概念,本身是不是一个陷阱?中国用70%的外贸依存度换来了什么?换来了美国航母可以随时切断马六甲海峡。换来了鸡蛋放在一个篮子里的风险。主持人,你管这叫入场券?我倒觉得这像是一张——请君入瓮的请帖。",
|
||
"filename": "mohammed_factory_trap.wav"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
# 初始化模型
|
||
print(f"\n🚀 开始初始化VoxCPM模型...")
|
||
start_time = time.time()
|
||
|
||
try:
|
||
model = VoxCPM(
|
||
voxcpm_model_path=LOCAL_MODEL_PATH,
|
||
enable_denoiser=False,
|
||
optimize=False
|
||
)
|
||
print(f"✅ 模型初始化完成,耗时: {time.time()-start_time:.2f} 秒")
|
||
except Exception as e:
|
||
print(f"❌ 模型初始化失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
# 生成所有嘉宾的语音
|
||
print(f"\n🎙️ 开始生成嘉宾语音...")
|
||
total_start = time.time()
|
||
|
||
for guest_id, guest_info in GUESTS.items():
|
||
print(f"\n{'='*60}")
|
||
print(f"嘉宾: {guest_info['name']}")
|
||
print(f"描述: {guest_info['description']}")
|
||
print(f"{'='*60}")
|
||
|
||
for dialogue in guest_info['dialogues']:
|
||
print(f"\n📄 生成对话: {dialogue['id']}")
|
||
print(f"文本: {dialogue['text'][:50]}...")
|
||
|
||
dialogue_start = time.time()
|
||
|
||
try:
|
||
# 生成音频
|
||
audio = model.generate(
|
||
text=dialogue['text'],
|
||
prompt_wav_path=guest_info['reference_file'],
|
||
prompt_text=None,
|
||
cfg_value=2.0,
|
||
inference_timesteps=20,
|
||
normalize=True,
|
||
denoise=False,
|
||
retry_badcase=True
|
||
)
|
||
|
||
# 保存音频
|
||
output_file = os.path.join(OUTPUT_DIR, dialogue['filename'])
|
||
sf.write(output_file, audio, model.tts_model.sample_rate)
|
||
|
||
# 验证
|
||
if os.path.exists(output_file):
|
||
file_size = os.path.getsize(output_file)
|
||
duration = len(audio) / model.tts_model.sample_rate
|
||
print(f"✅ 生成成功!")
|
||
print(f" 文件: {output_file}")
|
||
print(f" 大小: {file_size} 字节")
|
||
print(f" 时长: {duration:.2f} 秒")
|
||
print(f" 耗时: {time.time()-dialogue_start:.2f} 秒")
|
||
else:
|
||
print(f"❌ 保存失败")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 生成失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 生成主持人语音
|
||
print(f"\n{'='*60}")
|
||
print(f"主持人: Sonia")
|
||
print(f"{'='*60}")
|
||
|
||
host_dialogue = {
|
||
"id": "host_intro",
|
||
"text": "1999年5月8日,贝尔格莱德的火光中,三位中国记者的生命,换来的是什么?是广东南海流水线上,MADE IN CHINA标签的加速缝制。两年后,同样是这群年轻人,在大学操场上疯狂嘶吼:I enjoy losing face! 这不是精神分裂,这是——卧薪尝胆。",
|
||
"filename": "host_intro.wav"
|
||
}
|
||
|
||
print(f"\n📄 生成主持人介绍")
|
||
print(f"文本: {host_dialogue['text'][:50]}...")
|
||
|
||
try:
|
||
audio = model.generate(
|
||
text=host_dialogue['text'],
|
||
prompt_wav_path=None,
|
||
prompt_text=None,
|
||
cfg_value=2.0,
|
||
inference_timesteps=20,
|
||
normalize=True,
|
||
denoise=False
|
||
)
|
||
|
||
output_file = os.path.join(OUTPUT_DIR, host_dialogue['filename'])
|
||
sf.write(output_file, audio, model.tts_model.sample_rate)
|
||
|
||
if os.path.exists(output_file):
|
||
print(f"✅ 主持人语音生成成功!")
|
||
print(f" 文件: {output_file}")
|
||
else:
|
||
print(f"❌ 主持人语音保存失败")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 主持人语音生成失败: {e}")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"🎉 所有语音生成完成!")
|
||
print(f"总耗时: {time.time()-total_start:.2f} 秒")
|
||
print(f"输出目录: {OUTPUT_DIR}")
|
||
print(f"{'='*60}")
|
||
|
||
# 列出所有生成的文件
|
||
print(f"\n📋 生成的文件列表:")
|
||
for file in os.listdir(OUTPUT_DIR):
|
||
if file.endswith('.wav'):
|
||
file_path = os.path.join(OUTPUT_DIR, file)
|
||
size = os.path.getsize(file_path)
|
||
print(f" - {file} ({size} 字节)") |