Загрузка данных
"""Извлечение TitaNet-эмбеддингов скользящим окном по аудиозаписям.
Запуск (в окружении с NeMo):
python extract_titanet.py /path/to/wav_dir /path/to/out_dir
python extract_titanet.py /path/to/wav_dir /path/to/out_dir --device cuda --csv
Вход: папка с .wav (любой sr/каналы — приводятся к 16 кГц моно).
Выход на каждую запись:
<name>.npz : embeddings (M,192) L2-норм., starts_s (M,), ends_s (M,), meta
<name>.csv : (опц., --csv) строки "<name>_NNNN, v1 v2 ... v192" — как ivectors.csv
"""
from __future__ import annotations
import argparse
from pathlib import Path
import librosa
import numpy as np
import torch
import nemo.collections.asr as nemo_asr
SR = 16000 # TitaNet требует строго 16 кГц моно
def l2_normalize(x: np.ndarray, eps: float = 1e-10) -> np.ndarray:
return x / np.maximum(np.linalg.norm(x, axis=-1, keepdims=True), eps)
def load_model(device: str):
model = nemo_asr.models.EncDecSpeakerLabelModel.from_pretrained(
"nvidia/speakerverification_en_titanet_large"
)
model.eval()
return model.to(device)
@torch.no_grad()
def window_embeddings(
wav_path: Path, model, win_s: float, hop_s: float, device: str
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""-> (starts_s (M,), ends_s (M,), embeddings (M,192) L2-норм.)."""
audio, _ = librosa.load(str(wav_path), sr=SR, mono=True)
win, hop = int(win_s * SR), int(hop_s * SR)
if len(audio) < win: # запись короче окна — одно окно по всей записи
starts = [0]
else:
starts = list(range(0, len(audio) - win + 1, hop))
starts_s, ends_s, embs = [], [], []
for start in starts:
chunk = audio[start:start + win]
sig = torch.tensor(chunk, dtype=torch.float32, device=device).unsqueeze(0)
length = torch.tensor([sig.shape[1]], dtype=torch.int64, device=device)
_, emb = model.forward(input_signal=sig, input_signal_length=length)
embs.append(emb.squeeze(0).cpu().numpy())
starts_s.append(start / SR)
ends_s.append((start + len(chunk)) / SR)
return (
np.asarray(starts_s, dtype=np.float64),
np.asarray(ends_s, dtype=np.float64),
l2_normalize(np.asarray(embs, dtype=np.float32)),
)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("wav_dir", help="папка с .wav")
ap.add_argument("out_dir", help="куда писать эмбеддинги")
ap.add_argument("--win", type=float, default=1.5, help="окно, сек")
ap.add_argument("--hop", type=float, default=0.5, help="шаг, сек")
ap.add_argument("--device", default="cpu", help="cpu или cuda")
ap.add_argument("--csv", action="store_true", help="дополнительно писать .csv")
args = ap.parse_args()
out = Path(args.out_dir)
out.mkdir(parents=True, exist_ok=True)
wavs = sorted(Path(args.wav_dir).glob("*.wav"))
if not wavs:
raise SystemExit(f"в {args.wav_dir} не найдено .wav")
print(f"найдено записей: {len(wavs)} | device={args.device} "
f"win={args.win}s hop={args.hop}s")
model = load_model(args.device)
for wav in wavs:
starts_s, ends_s, embs = window_embeddings(
wav, model, args.win, args.hop, args.device
)
np.savez(
out / f"{wav.stem}.npz",
embeddings=embs, starts_s=starts_s, ends_s=ends_s,
meta=dict(model="titanet_large", dim=int(embs.shape[1]),
win_s=args.win, hop_s=args.hop, sr=SR),
)
if args.csv:
lines = [
f"{wav.stem}_{i:04d}," + " ".join(f"{v:.6f}" for v in embs[i])
for i in range(embs.shape[0])
]
(out / f"{wav.stem}.csv").write_text(
"filename,i-vector\n" + "\n".join(lines)
)
print(f"{wav.name}: {embs.shape[0]} окон × {embs.shape[1]} -> {wav.stem}.npz")
print(f"\nГотово. Эмбеддинги в {out.resolve()}/")
if __name__ == "__main__":
main()