Download scripts/synth_eval.py from ehabnegm/s2pro-egy: direct link, hf CLI and curl.
- Browser
- Download file 7.07 kB
-
https://huggingface.co/ehabnegm/s2pro-egy/resolve/main/scripts/synth_eval.py
- Command line
-
hf download hf://ehabnegm/s2pro-egy/scripts/synth_eval.py
-
curl -L -o synth_eval.py https://huggingface.co/ehabnegm/s2pro-egy/resolve/main/scripts/synth_eval.py
7.07 kB
| #!/usr/bin/env python3 | |
| """Batch-synthesize the Egyptian eval set with s2-pro (+optional LoRA ckpt). | |
| Loads the text2semantic model + codec ONCE, then synthesizes every sentence in | |
| --sentences (jsonl: {id, text}) using a fixed reference voice, writing | |
| <outdir>/<id>.wav plus timing.jsonl with generation stats (RTF etc). | |
| Usage: | |
| python synth_eval.py --outdir /opt/work/eval/baseline | |
| python synth_eval.py --outdir /opt/work/eval/step_2000 \ | |
| --lora-ckpt results/s2pro_egy_lora/checkpoints/step_000002000.ckpt \ | |
| --lora-config r_32_egy | |
| """ | |
| import argparse | |
| import json | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| from loguru import logger | |
| from fish_speech.models.text2semantic.inference import ( | |
| encode_audio, | |
| generate_long, | |
| init_model, | |
| load_codec_model, | |
| ) | |
| CKPT = Path("/opt/work/checkpoints/s2-pro") | |
| def load_lora(model, ckpt_path: str, lora_config_name: str, lora_filter: str = "all"): | |
| from hydra import compose, initialize_config_dir | |
| from hydra.utils import instantiate | |
| from fish_speech.models.text2semantic.lora import setup_lora | |
| cfg_dir = str( | |
| Path("/opt/work/fish-speech/fish_speech/configs/lora").resolve() | |
| ) | |
| with initialize_config_dir(version_base="1.3", config_dir=cfg_dir): | |
| lora_cfg = instantiate(compose(config_name=lora_config_name)) | |
| setup_lora(model, lora_cfg) | |
| sd = torch.load(ckpt_path, map_location="cpu", weights_only=False) | |
| if "state_dict" in sd: | |
| sd = sd["state_dict"] | |
| sd = {k.removeprefix("model."): v for k, v in sd.items()} | |
| if lora_filter == "slow": | |
| sd = {k: v for k, v in sd.items() if ".fast_" not in k and not k.startswith("fast_")} | |
| logger.info(f"ablation slow-only: {len(sd)} tensors kept") | |
| elif lora_filter == "fast": | |
| sd = {k: v for k, v in sd.items() if ".fast_" in k or k.startswith("fast_")} | |
| logger.info(f"ablation fast-only: {len(sd)} tensors kept") | |
| err = model.load_state_dict(sd, strict=False) | |
| n_lora = sum(1 for k in sd if "lora" in k) | |
| assert n_lora > 0, "no lora keys in checkpoint!" | |
| logger.info(f"Loaded {n_lora} LoRA tensors; missing={len(err.missing_keys)} (expected: base weights)") | |
| if err.unexpected_keys: | |
| logger.warning(f"Unexpected keys: {err.unexpected_keys[:5]}") | |
| return model | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--sentences", default="/opt/work/scripts/eval_sentences.jsonl") | |
| ap.add_argument("--outdir", required=True) | |
| ap.add_argument("--lora-ckpt", default=None) | |
| ap.add_argument("--lora-config", default="r_32_egy") | |
| ap.add_argument("--ref-audio", default="/opt/work/eval/ref_voice.wav") | |
| ap.add_argument("--ref-text-file", default="/opt/work/eval/ref_voice.txt") | |
| ap.add_argument("--temperature", type=float, default=0.8) | |
| ap.add_argument("--top-p", type=float, default=0.8) | |
| ap.add_argument("--seed", type=int, default=42) | |
| ap.add_argument("--skip-existing", action="store_true", | |
| help="only synthesize ids whose wav is missing; merge timing") | |
| ap.add_argument("--lora-filter", choices=["all", "slow", "fast"], default="all", | |
| help="ablation: load only slow-transformer or only fast-transformer LoRA weights") | |
| ap.add_argument("--limit", type=int, default=0, help="synth only first N sentences") | |
| args = ap.parse_args() | |
| outdir = Path(args.outdir) | |
| outdir.mkdir(parents=True, exist_ok=True) | |
| device = "cuda" | |
| precision = torch.bfloat16 | |
| sentences = [ | |
| json.loads(l) | |
| for l in Path(args.sentences).read_text(encoding="utf-8").splitlines() | |
| if l.strip() | |
| ] | |
| old_timing = {} | |
| tj = outdir / "timing.jsonl" | |
| if args.skip_existing and tj.exists(): | |
| for l in tj.read_text(encoding="utf-8").splitlines(): | |
| if l.strip(): | |
| r = json.loads(l) | |
| old_timing[r["id"]] = r | |
| before = len(sentences) | |
| sentences = [s for s in sentences | |
| if not (outdir / f"{s['id']}.wav").exists()] | |
| logger.info(f"skip-existing: {before - len(sentences)} kept, {len(sentences)} to synth") | |
| if not sentences: | |
| logger.info("nothing to do") | |
| return | |
| if args.limit: | |
| sentences = sentences[: args.limit] | |
| logger.info("Loading text2semantic model...") | |
| model, decode_one_token = init_model(CKPT, device, precision, compile=False) | |
| if args.lora_ckpt: | |
| model = load_lora(model, args.lora_ckpt, args.lora_config, args.lora_filter) | |
| model = model.to(device=device, dtype=precision).eval() | |
| with torch.device(device): | |
| model.setup_caches( | |
| max_batch_size=1, | |
| max_seq_len=model.config.max_seq_len, | |
| dtype=next(model.parameters()).dtype, | |
| ) | |
| logger.info("Loading codec...") | |
| codec = load_codec_model(CKPT / "codec.pth", device, precision) | |
| ref_text = Path(args.ref_text_file).read_text(encoding="utf-8").strip() | |
| ref_tokens = encode_audio(args.ref_audio, codec, device).cpu() | |
| results = [] | |
| for s in sentences: | |
| torch.manual_seed(args.seed) | |
| torch.cuda.manual_seed(args.seed) | |
| t0 = time.time() | |
| gen = generate_long( | |
| model=model, | |
| device=device, | |
| decode_one_token=decode_one_token, | |
| text=s["text"], | |
| num_samples=1, | |
| max_new_tokens=0, | |
| top_p=args.top_p, | |
| top_k=30, | |
| temperature=args.temperature, | |
| compile=False, | |
| iterative_prompt=True, | |
| chunk_length=300, | |
| prompt_text=[ref_text], | |
| prompt_tokens=[ref_tokens], | |
| ) | |
| codes = [] | |
| for r in gen: | |
| if r.action == "sample": | |
| codes.append(r.codes) | |
| gen_s = time.time() - t0 | |
| if not codes: | |
| logger.error(f"{s['id']}: NO CODES GENERATED") | |
| results.append({"id": s["id"], "error": "no_codes"}) | |
| continue | |
| merged = torch.cat(codes, dim=1).to(device) | |
| with torch.no_grad(): | |
| fake = codec.from_indices(merged.unsqueeze(0)) | |
| wav = fake[0, 0].float().cpu().numpy() | |
| dur = len(wav) / codec.sample_rate | |
| sf.write(outdir / f"{s['id']}.wav", wav, codec.sample_rate) | |
| rtf = gen_s / max(dur, 1e-6) | |
| results.append( | |
| {"id": s["id"], "text": s["text"], "gen_s": round(gen_s, 2), | |
| "dur_s": round(dur, 2), "rtf": round(rtf, 2)} | |
| ) | |
| logger.info(f"{s['id']}: {dur:.1f}s audio in {gen_s:.1f}s (RTF {rtf:.2f})") | |
| merged = {**old_timing, **{r["id"]: r for r in results}} | |
| with open(outdir / "timing.jsonl", "w", encoding="utf-8") as f: | |
| for r in merged.values(): | |
| f.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| ok = [r for r in results if "rtf" in r] | |
| if ok: | |
| logger.info( | |
| f"DONE {len(ok)}/{len(results)} ok; mean RTF " | |
| f"{sum(r['rtf'] for r in ok)/len(ok):.2f}" | |
| ) | |
| if __name__ == "__main__": | |
| main() | |