从github迁移过来
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
《林夏》TTS 批量生产脚本
|
||||
用法:
|
||||
conda activate cosyvoice
|
||||
cd /home/xsl/blind
|
||||
python audio/batch_tts.py [--role mom] [--test]
|
||||
|
||||
参数:
|
||||
--role ROLE 只生产指定角色(不指定则全部)
|
||||
--test 只合成第一条,用于快速验证环境
|
||||
--dry-run 只打印任务列表,不实际合成
|
||||
"""
|
||||
import os, sys, time, argparse, warnings
|
||||
import torch
|
||||
import soundfile as sf # torchaudio nightly requires torchcodec; use soundfile instead
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
COSYVOICE_DIR = "/home/xsl/tools/CosyVoice"
|
||||
PROJECT_DIR = "/home/xsl/blind"
|
||||
REF_DIR = f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref"
|
||||
OUT_DIR = f"{PROJECT_DIR}/audio/00_raw/tts"
|
||||
|
||||
sys.path.insert(0, COSYVOICE_DIR)
|
||||
sys.path.insert(0, f"{COSYVOICE_DIR}/third_party/Matcha-TTS")
|
||||
os.chdir(COSYVOICE_DIR)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 角色配置(对应 SOP §4.2)
|
||||
# ref_wav / ref_text 在参考音频备齐前先留空
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
ROLES = {
|
||||
"mom": {
|
||||
"speed": 0.92,
|
||||
"ref_wav": f"{REF_DIR}/mom_ref.wav",
|
||||
"ref_text": "", # ← 参考音频对应文字,备齐后填入
|
||||
},
|
||||
"azhe": {
|
||||
"speed": 1.05,
|
||||
"ref_wav": f"{REF_DIR}/azhe_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"xiaomei": {
|
||||
"speed": 1.10,
|
||||
"ref_wav": f"{REF_DIR}/xiaomei_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"xiaomei_drunk": { # 醉语音用同参考,speed 调慢
|
||||
"speed": 0.95,
|
||||
"ref_wav": f"{REF_DIR}/xiaomei_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"zhounan": {
|
||||
"speed": 0.95,
|
||||
"ref_wav": f"{REF_DIR}/zhounan_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"hr": {
|
||||
"speed": 1.00,
|
||||
"ref_wav": f"{REF_DIR}/hr_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"anan": {
|
||||
"speed": 1.15,
|
||||
"ref_wav": f"{REF_DIR}/anan_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"dorm_a": {
|
||||
"speed": 1.10,
|
||||
"ref_wav": f"{REF_DIR}/dorm_a_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"dorm_b": {
|
||||
"speed": 1.00,
|
||||
"ref_wav": f"{REF_DIR}/dorm_b_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"dorm_c": {
|
||||
"speed": 0.95,
|
||||
"ref_wav": f"{REF_DIR}/dorm_c_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"delivery": {
|
||||
"speed": 1.20,
|
||||
"ref_wav": f"{REF_DIR}/delivery_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
"self_3y": { # 复用 xiaomei,speed +0.05
|
||||
"speed": 1.15,
|
||||
"ref_wav": f"{REF_DIR}/xiaomei_ref.wav",
|
||||
"ref_text": "",
|
||||
},
|
||||
# system 用预训练音色,不在这里
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# 台词表(对应林夏.md §3 + §5)
|
||||
# 格式: (输出文件前缀, 角色key, 台词)
|
||||
# 长戏(≥30s)已按 SOP §5.3 拆分为分段
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
LINES = [
|
||||
# ── 01 · 21:03 妈妈电话 ──
|
||||
("lv11_2103_mom_call_01",
|
||||
"mom",
|
||||
"今晚煮了你爱吃的排骨,给你拍张照啊。下周五回来一趟,你高中物理老师的儿子,挺好的。"),
|
||||
|
||||
# ── 02 · 21:15 小美三连 ──
|
||||
("lv11_2115_xiaomei_voice_01", "xiaomei", "在吗"),
|
||||
("lv11_2115_xiaomei_voice_02", "xiaomei", "你被甩了我都听说了"),
|
||||
("lv11_2115_xiaomei_voice_03", "xiaomei", "明天晚上喝酒,海底捞旁边那家,不许放鸽子"),
|
||||
|
||||
# ── 03 · 21:20 外卖小哥 ──
|
||||
("lv11_2120_delivery_call_01",
|
||||
"delivery",
|
||||
"你好你的麻辣烫到了,放门口可以吗?"),
|
||||
|
||||
# ── 04 · 21:30 阿哲语音 ──
|
||||
("lv11_2130_azhe_voice_01",
|
||||
"azhe",
|
||||
"嗯……夏夏。我那个AirPods充电盒,好像落你抽屉里了。"
|
||||
"你方便的时候帮我寄一下吗,工位还要用。地址我等下发你。"),
|
||||
|
||||
# ── 05 · 21:45 HR 王姐 ──
|
||||
("lv11_2145_hr_voice_01",
|
||||
"hr",
|
||||
"夏夏啊,离职流程你下周三之前办完哈,公积金那边记得跟进。"
|
||||
"辛苦啦,外面有合适的工作我帮你留意。"),
|
||||
|
||||
# ── 06 · 22:00 安安 ──
|
||||
("lv11_2200_anan_voice_01",
|
||||
"anan",
|
||||
"夏夏!我在helens!要不要过来!我请客!"),
|
||||
|
||||
# ── 07 · 22:15 寝室群 ──
|
||||
("lv11_2215_dorm_a_voice_01", "dorm_a", "哎你们看,我今天升职了!"),
|
||||
("lv11_2215_dorm_b_voice_01", "dorm_b", "真的啊!恭喜恭喜!太厉害了!"),
|
||||
("lv11_2215_dorm_c_voice_01", "dorm_c", "厉害!那要请客啊!"),
|
||||
("lv11_2215_dorm_a_voice_02", "dorm_a", "哈哈哈必须的!夏夏你怎么不说话呀"),
|
||||
("lv11_2215_dorm_b_voice_02", "dorm_b", "夏夏在忙吧,别打扰她了"),
|
||||
|
||||
# ── 09 · 22:45 小美醉语音(分 4 段)──
|
||||
("lv11_2245_xiaomei_voice_01_seg1",
|
||||
"xiaomei_drunk",
|
||||
"夏夏夏夏夏。你睡了吗?我跟你说,我跟你说啊……今天我喝多了……"),
|
||||
("lv11_2245_xiaomei_voice_01_seg2",
|
||||
"xiaomei_drunk",
|
||||
"你别嫌我烦哈,我跟你说……其实啊……阿哲他,大三那年,他来追过我。"
|
||||
"我没答应他。我那时候就觉得他这人,呵,差点意思。"),
|
||||
("lv11_2245_xiaomei_voice_01_seg3",
|
||||
"xiaomei_drunk",
|
||||
"你跟他在一起的时候,我没说,因为说了也没意义嘛对吧。"
|
||||
"现在他甩了你,我才敢说。"),
|
||||
("lv11_2245_xiaomei_voice_01_seg4",
|
||||
"xiaomei_drunk",
|
||||
"夏夏,你听到了吗?你别恨我啊。……喂?算了。"),
|
||||
|
||||
# ── 10 · 23:00 妈妈文字 ──
|
||||
("lv11_2300_mom_text_01", "mom", "睡了吗?"),
|
||||
|
||||
# ── 11 · 23:15 阿哲电话 ──
|
||||
("lv11_2315_azhe_call_01", "azhe", "我能过去拿吗?就5分钟"),
|
||||
|
||||
# ── 12 · 23:30 周南第一条 ──
|
||||
("lv11_2330_zhounan_voice_01",
|
||||
"zhounan",
|
||||
"林夏,是我,周南。你应该不记得我了吧,初三坐你后桌那个"),
|
||||
|
||||
# ── 13 · 23:45 周南长讲述(4 段)──
|
||||
("lv11_2345_zhounan_voice_01",
|
||||
"zhounan",
|
||||
"其实我一直想找机会跟你说声谢谢。初三那年你借我笔记,那次考试我才没挂科。"),
|
||||
("lv11_2345_zhounan_voice_02",
|
||||
"zhounan",
|
||||
"后来高考,我没考好。复读了一年,去了一个二本。然后就回老家了,没什么好说的。"),
|
||||
("lv11_2345_zhounan_voice_03",
|
||||
"zhounan",
|
||||
"今年第一次来北京出差。来之前我想着,要不要找你。找了很久,才在一个校友群里看到你。"
|
||||
"你现在过得怎么样?"),
|
||||
("lv11_2345_zhounan_voice_04",
|
||||
"zhounan",
|
||||
"今天我生日。本来就想跟你说一声,对不起这么晚。"),
|
||||
|
||||
# ── 14 · 00:15 妈妈电话 ──
|
||||
("lv11_0015_mom_call_01",
|
||||
"mom",
|
||||
"夏夏,你不接电话我有点担心。妈妈知道你工作忙,你早点睡。"),
|
||||
|
||||
# ── 16 · 02:00 妈妈长语音(分 8 段)──
|
||||
("lv11_0200_mom_voice_01_seg1",
|
||||
"mom",
|
||||
"夏夏。妈妈知道你今天电话不接。妈妈不催你,就是想跟你说一件事。"),
|
||||
("lv11_0200_mom_voice_01_seg2",
|
||||
"mom",
|
||||
"你24了对吧。妈妈24那年——你应该没听我说过——"),
|
||||
("lv11_0200_mom_voice_01_seg3",
|
||||
"mom",
|
||||
"那时候妈妈在广州,给一个香港老板做秘书,1995年。"
|
||||
"那年我也被分手过。也被开除过。一模一样的春天。"),
|
||||
("lv11_0200_mom_voice_01_seg4",
|
||||
"mom",
|
||||
"那年我哭得特别难看。后来呢?后来就没什么后来了。"
|
||||
"我回了老家,认识了你爸,生了你。"),
|
||||
("lv11_0200_mom_voice_01_seg5",
|
||||
"mom",
|
||||
"我跟你说这个不是想跟你说什么都会过去——"
|
||||
"我想跟你说,那年夏天我没死,是因为我外婆每天晚上都给我打一个电话。"),
|
||||
("lv11_0200_mom_voice_01_seg6",
|
||||
"mom",
|
||||
"她不说什么,就问我今天吃饭了没。"),
|
||||
("lv11_0200_mom_voice_01_seg7",
|
||||
"mom",
|
||||
"我现在打给你,你不接也没事。妈妈给你发个语音,你什么时候想听就听。你不用回我。"),
|
||||
("lv11_0200_mom_voice_01_seg8",
|
||||
"mom",
|
||||
"你今天接了好多电话吧。妈妈不打扰你了。晚安。"),
|
||||
|
||||
# ── self-3y 三版(台词待编剧定稿)──
|
||||
# ("lv11_0030_self-3y_voice_01", "self_3y", "(明朗版台词)"),
|
||||
# ("lv11_0030_self-3y_voice_02", "self_3y", "(迷茫版台词)"),
|
||||
# ("lv11_0030_self-3y_voice_03", "self_3y", "(温柔版台词)"),
|
||||
]
|
||||
|
||||
|
||||
def load_model():
|
||||
from cosyvoice.cli.cosyvoice import CosyVoice2
|
||||
print("加载 CosyVoice2 模型...")
|
||||
model = CosyVoice2("pretrained_models/CosyVoice2-0.5B")
|
||||
print(f"模型就绪,采样率 {model.sample_rate}Hz")
|
||||
return model
|
||||
|
||||
|
||||
def load_ref(role_cfg):
|
||||
from cosyvoice.utils.file_utils import load_wav
|
||||
wav_path = role_cfg["ref_wav"]
|
||||
if not os.path.exists(wav_path):
|
||||
return None, None
|
||||
wav = load_wav(wav_path, 16000)
|
||||
# 裁到 30s 以内
|
||||
max_samples = 16000 * 30
|
||||
if wav.shape[1] > max_samples:
|
||||
wav = wav[:, :max_samples]
|
||||
return wav, role_cfg["ref_text"]
|
||||
|
||||
|
||||
def synthesize(model, filename, role_key, text):
|
||||
role_cfg = ROLES[role_key]
|
||||
ref_wav, ref_text = load_ref(role_cfg)
|
||||
|
||||
out_path = f"{OUT_DIR}/{filename}_v1.wav"
|
||||
if os.path.exists(out_path):
|
||||
print(f" [跳过,已存在] {filename}")
|
||||
return True
|
||||
|
||||
if ref_wav is None:
|
||||
print(f" [跳过,缺参考音频] {role_key}: {role_cfg['ref_wav']}")
|
||||
return False
|
||||
|
||||
if not ref_text.strip():
|
||||
print(f" [跳过,ref_text 未填] {role_key}")
|
||||
return False
|
||||
|
||||
t0 = time.time()
|
||||
chunks = []
|
||||
for chunk in model.inference_zero_shot(
|
||||
tts_text=text,
|
||||
prompt_text=ref_text,
|
||||
prompt_speech_16k=ref_wav,
|
||||
stream=False,
|
||||
speed=role_cfg["speed"],
|
||||
):
|
||||
chunks.append(chunk["tts_speech"])
|
||||
|
||||
if not chunks:
|
||||
print(f" [失败,无输出] {filename}")
|
||||
return False
|
||||
|
||||
audio = torch.concat(chunks, dim=1)
|
||||
duration = audio.shape[1] / model.sample_rate
|
||||
sf.write(out_path, audio.squeeze().cpu().numpy(), model.sample_rate)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
rtf = elapsed / duration if duration > 0 else 0
|
||||
print(f" ✓ {filename} {duration:.1f}s 音频 {elapsed:.1f}s 耗时 RTF={rtf:.3f}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--role", help="只合成指定角色")
|
||||
parser.add_argument("--test", action="store_true", help="只合成第一条")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只打印任务")
|
||||
args = parser.parse_args()
|
||||
|
||||
lines = LINES
|
||||
if args.role:
|
||||
lines = [(f, r, t) for f, r, t in lines if r == args.role or r.startswith(args.role)]
|
||||
if args.test:
|
||||
lines = lines[:1]
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 《林夏》TTS 批量合成 共 {len(lines)} 条")
|
||||
print(f"{'='*60}")
|
||||
|
||||
if args.dry_run:
|
||||
for f, r, t in lines:
|
||||
ref_exists = "✓" if os.path.exists(ROLES.get(r, {}).get("ref_wav", "")) else "✗缺参考"
|
||||
print(f" [{ref_exists}] {f} [{r}] {t[:30]}...")
|
||||
return
|
||||
|
||||
model = load_model()
|
||||
ok, skip, fail = 0, 0, 0
|
||||
for i, (filename, role_key, text) in enumerate(lines, 1):
|
||||
print(f"\n[{i}/{len(lines)}] {filename}")
|
||||
result = synthesize(model, filename, role_key, text)
|
||||
if result is True: ok += 1
|
||||
elif result is None: skip += 1
|
||||
else: fail += 1
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 完成:{ok} 条 跳过:{skip} 条 失败:{fail} 条")
|
||||
print(f" 输出目录:{OUT_DIR}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user