更新 3, 2, 1 的声音

This commit is contained in:
xsl
2026-09-27 10:38:28 +08:00
parent 7155f23213
commit ca76128ac0
3 changed files with 94 additions and 32 deletions
Binary file not shown.
+94 -32
View File
@@ -112,7 +112,69 @@ def to_mp3(wav_path: Path, mp3_path: Path) -> None:
)
def normalize_clip(wav: np.ndarray, sr: int) -> np.ndarray:
wav = trim_edges(np.asarray(wav, dtype=np.float32).reshape(-1), sr)
peak = float(np.max(np.abs(wav)))
if peak < 0.05:
raise RuntimeError(f"peak too low ({peak:.4f})")
wav = wav / peak * PEAK_NORM
dur = len(wav) / sr
if dur < 0.35:
wav = np.concatenate([wav, np.zeros(int((0.35 - dur) * sr), dtype=np.float32)])
return wav
def write_mp3(name: str, wav: np.ndarray, sr: int) -> None:
wav_path = STAGING / f"{name}.wav"
mp3_path = OUT_DIR / f"{name}.mp3"
sf.write(str(wav_path), wav, sr)
to_mp3(wav_path, mp3_path)
dur = len(wav) / sr
print(f"[ok] {name}.mp3 {dur:.2f}s {mp3_path.stat().st_size} bytes", flush=True)
def gen_countdown(model) -> tuple[np.ndarray, int]:
"""倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。
与提示音共用同一 speaker + instruct;句间留约 0.35s,总长约 2.5s。
"""
digits = ["三", "二", "一"]
# 同批带上一条短提示作音色锚点,丢弃锚点音频
texts = ["保持不动", *digits]
wavs, sr = model.generate_custom_voice(
text=texts,
language=["Chinese"] * len(texts),
speaker=[SPEAKER] * len(texts),
instruct=[INSTRUCT] * len(texts),
max_new_tokens=2048,
)
gap = np.zeros(int(0.35 * sr), dtype=np.float32)
parts: list[np.ndarray] = []
for i, digit in enumerate(digits):
clip = normalize_clip(wavs[i + 1], sr)
parts.append(clip)
if i < len(digits) - 1:
parts.append(gap)
print(f" countdown digit {digit}: {len(clip) / sr:.2f}s", flush=True)
return np.concatenate(parts), sr
def main() -> int:
import argparse
ap = argparse.ArgumentParser()
ap.add_argument(
"--only",
default="",
help="逗号分隔的 id,只生成这些;空则全量",
)
args = ap.parse_args()
only = {x.strip() for x in args.only.split(",") if x.strip()}
todo = [(n, t) for n, t in LINES if not only or n in only]
if only and not todo:
print(f"unknown --only ids: {sorted(only)}", flush=True)
return 1
OUT_DIR.mkdir(parents=True, exist_ok=True)
STAGING.mkdir(parents=True, exist_ok=True)
@@ -123,46 +185,46 @@ def main() -> int:
dtype=torch.bfloat16,
)
names = [n for n, _ in LINES]
texts = [t for _, t in LINES]
print(f"generating {len(names)} lines with speaker={SPEAKER} ...", flush=True)
wavs, sr = model.generate_custom_voice(
text=texts,
language=["Chinese"] * len(names),
speaker=[SPEAKER] * len(names),
instruct=[INSTRUCT] * len(names),
max_new_tokens=2048,
)
# 倒计时走专用拼接路径,其余整句生成
normal = [(n, t) for n, t in todo if n != "countdown-321"]
do_countdown = any(n == "countdown-321" for n, _ in todo)
manifest_ids: list[str] = []
for name, wav in zip(names, wavs):
wav = np.asarray(wav, dtype=np.float32).reshape(-1)
wav = trim_edges(wav, sr)
if normal:
names = [n for n, _ in normal]
texts = [t for _, t in normal]
print(f"generating {len(names)} lines with speaker={SPEAKER} ...", flush=True)
wavs, sr = model.generate_custom_voice(
text=texts,
language=["Chinese"] * len(names),
speaker=[SPEAKER] * len(names),
instruct=[INSTRUCT] * len(names),
max_new_tokens=2048,
)
for name, wav in zip(names, wavs):
try:
write_mp3(name, normalize_clip(wav, sr), sr)
except RuntimeError as e:
print(f"[FAIL] {name}: {e}", flush=True)
return 1
if do_countdown:
print(f"generating countdown-321 (digit stitch, speaker={SPEAKER}) ...", flush=True)
wav, sr = gen_countdown(model)
# 整段再峰值归一一次,保证与提示音响度一致
peak = float(np.max(np.abs(wav)))
if peak < 0.05:
print(f"[FAIL] {name}: peak too low ({peak:.4f})", flush=True)
return 1
wav = wav / peak * PEAK_NORM
dur = len(wav) / sr
if dur < 0.35:
pad = np.zeros(int((0.35 - dur) * sr), dtype=np.float32)
wav = np.concatenate([wav, pad])
dur = len(wav) / sr
write_mp3("countdown-321", wav, sr)
wav_path = STAGING / f"{name}.wav"
mp3_path = OUT_DIR / f"{name}.mp3"
sf.write(str(wav_path), wav, sr)
to_mp3(wav_path, mp3_path)
size = mp3_path.stat().st_size
print(f"[ok] {name}.mp3 {dur:.2f}s {size} bytes", flush=True)
manifest_ids.append(name)
manifest = {"files": manifest_ids}
# 清单始终覆盖文档全量已落地文件(缺文件则跳过写入名)
existing = [
n for n, _ in LINES if (OUT_DIR / f"{n}.mp3").is_file()
]
manifest = {"files": existing}
(OUT_DIR / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"wrote {OUT_DIR / 'manifest.json'} ({len(manifest_ids)} files)", flush=True)
print(f"wrote {OUT_DIR / 'manifest.json'} ({len(existing)} files)", flush=True)
return 0
Binary file not shown.