从github迁移过来

This commit is contained in:
xsl
2026-05-18 18:39:49 +08:00
commit 9c5d9e586c
89 changed files with 9035 additions and 0 deletions
+327
View File
@@ -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": { # 复用 xiaomeispeed +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()