更新 3, 2, 1 的声音
This commit is contained in:
Binary file not shown.
+94
-32
@@ -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.
Reference in New Issue
Block a user