Delete .ipynb_checkpoints/infer-checkpoint.py with huggingface_hub
Browse files
.ipynb_checkpoints/infer-checkpoint.py
DELETED
|
@@ -1,168 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Standalone inference script for the best sdclap CLAP model (qwen3 text encoder + Whisper-large-v3
|
| 4 |
-
last-layer audio encoder, joint_dim=256, mlp3_residual projection heads). Computes a single
|
| 5 |
-
cosine-similarity score between one audio clip and one text description of how it was spoken.
|
| 6 |
-
|
| 7 |
-
This script is self-contained on purpose — it does NOT import anything from the main sdclap
|
| 8 |
-
repository (`scripts/neural_network/...`); the minimal model/embedding logic it needs is
|
| 9 |
-
reimplemented inline below, so it only depends on the packages in `requirements.txt` plus a
|
| 10 |
-
checkpoint file from `checkpoints/`.
|
| 11 |
-
|
| 12 |
-
The checkpoint (~11MB) contains ONLY the trained projection heads + gender embedding table +
|
| 13 |
-
logit_scale/bias — NOT the frozen pretrained text/audio encoders, which are downloaded from
|
| 14 |
-
HuggingFace on first run (Qwen/Qwen3-Embedding-4B ~8GB, openai/whisper-large-v3 ~3GB).
|
| 15 |
-
|
| 16 |
-
Usage (venv with requirements.txt installed):
|
| 17 |
-
python infer.py --audio clip.wav --text "тихо прошептала, едва сдерживая слёзы"
|
| 18 |
-
python infer.py --audio clip.wav --text "..." --gender male --checkpoint checkpoints/qwen3_whisper_last_seed2_best.pt
|
| 19 |
-
"""
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import argparse
|
| 23 |
-
|
| 24 |
-
import numpy as np
|
| 25 |
-
import torch
|
| 26 |
-
import torch.nn as nn
|
| 27 |
-
import torch.nn.functional as F
|
| 28 |
-
|
| 29 |
-
GENDER_TO_ID = {"male": 0, "female": 1, "unknown": 2}
|
| 30 |
-
TEXT_ENCODER_NAME = "Qwen/Qwen3-Embedding-4B"
|
| 31 |
-
AUDIO_ENCODER_NAME = "openai/whisper-large-v3"
|
| 32 |
-
WHISPER_LAYER_IDX = 32 # last encoder layer of whisper-large-v3 ("whisper_large_v3_l32")
|
| 33 |
-
WHISPER_ENCODER_FPS = 50.0
|
| 34 |
-
WHISPER_MAX_FRAMES = 1500
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# --- minimal ClapModel (inference-only path: no VIB, no FiLM, no n_vectors>1, no attention-pool —
|
| 38 |
-
# none of these are used by the qwen3_whisper_last checkpoints published here) --------------------
|
| 39 |
-
|
| 40 |
-
class _ResidualBlock(nn.Module):
|
| 41 |
-
def __init__(self, dim: int, dropout: float) -> None:
|
| 42 |
-
super().__init__()
|
| 43 |
-
self.block = nn.Sequential(
|
| 44 |
-
nn.Linear(dim, dim), nn.LayerNorm(dim), nn.GELU(), nn.Dropout(dropout),
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 48 |
-
return x + self.block(x)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
class ProjectionHead(nn.Module):
|
| 52 |
-
"""mlp3_residual architecture only — matches head_arch used by qwen3_whisper_last."""
|
| 53 |
-
|
| 54 |
-
def __init__(self, in_dim: int, hidden_dim: int, out_dim: int, dropout: float) -> None:
|
| 55 |
-
super().__init__()
|
| 56 |
-
self.backbone = nn.Sequential(
|
| 57 |
-
nn.Linear(in_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.GELU(), nn.Dropout(dropout),
|
| 58 |
-
_ResidualBlock(hidden_dim, dropout),
|
| 59 |
-
)
|
| 60 |
-
self.out_head = nn.Linear(hidden_dim, out_dim)
|
| 61 |
-
|
| 62 |
-
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 63 |
-
return self.out_head(self.backbone(x))
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
class ClapModel(nn.Module):
|
| 67 |
-
def __init__(self, text_dim: int, audio_dim: int, joint_dim: int, hidden_dim: int,
|
| 68 |
-
gender_dim: int, dropout: float) -> None:
|
| 69 |
-
super().__init__()
|
| 70 |
-
self.gender_embedding = nn.Embedding(3, gender_dim)
|
| 71 |
-
self.text_head = ProjectionHead(text_dim + gender_dim, hidden_dim, joint_dim, dropout)
|
| 72 |
-
self.audio_head = ProjectionHead(audio_dim, hidden_dim, joint_dim, dropout)
|
| 73 |
-
|
| 74 |
-
def encode_text(self, text_emb: torch.Tensor, gender_id: torch.Tensor) -> torch.Tensor:
|
| 75 |
-
gender_vec = self.gender_embedding(gender_id)
|
| 76 |
-
x = torch.cat([text_emb, gender_vec], dim=-1)
|
| 77 |
-
return F.normalize(self.text_head(x), dim=-1)
|
| 78 |
-
|
| 79 |
-
def encode_audio(self, audio_emb: torch.Tensor) -> torch.Tensor:
|
| 80 |
-
return F.normalize(self.audio_head(audio_emb), dim=-1)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def load_clap_model(checkpoint_path: str, device: str) -> ClapModel:
|
| 84 |
-
ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 85 |
-
sd = ckpt["model"]
|
| 86 |
-
args = ckpt["args"]
|
| 87 |
-
text_dim = sd["text_head.backbone.0.weight"].shape[1] - args["gender_dim"]
|
| 88 |
-
audio_dim = sd["audio_head.backbone.0.weight"].shape[1]
|
| 89 |
-
model = ClapModel(
|
| 90 |
-
text_dim=text_dim, audio_dim=audio_dim, joint_dim=args["joint_dim"],
|
| 91 |
-
hidden_dim=args["hidden_dim"], gender_dim=args["gender_dim"], dropout=args["dropout"],
|
| 92 |
-
)
|
| 93 |
-
missing, unexpected = model.load_state_dict(sd, strict=False)
|
| 94 |
-
unexpected = [k for k in unexpected if k not in ("logit_scale", "logit_bias")]
|
| 95 |
-
if missing or unexpected:
|
| 96 |
-
raise RuntimeError(f"checkpoint key mismatch: missing={missing}, unexpected={unexpected}")
|
| 97 |
-
model.to(device).eval()
|
| 98 |
-
return model
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# --- raw text -> qwen3 embedding (2560-dim, L2-normalized) ----------------------------------------
|
| 102 |
-
|
| 103 |
-
def embed_text(text: str, device: str) -> torch.Tensor:
|
| 104 |
-
from sentence_transformers import SentenceTransformer
|
| 105 |
-
|
| 106 |
-
kwargs = {
|
| 107 |
-
"model_kwargs": {"dtype": torch.bfloat16 if device == "cuda" else torch.float32},
|
| 108 |
-
"processor_kwargs": {"padding_side": "left"},
|
| 109 |
-
}
|
| 110 |
-
encoder = SentenceTransformer(TEXT_ENCODER_NAME, device=device, **kwargs)
|
| 111 |
-
emb = encoder.encode([text], normalize_embeddings=True, show_progress_bar=False)
|
| 112 |
-
return torch.from_numpy(np.asarray(emb, dtype=np.float32)).to(device)
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
# --- raw audio -> whisper-large-v3 last-layer embedding (1280-dim, masked mean-pool) --------------
|
| 116 |
-
|
| 117 |
-
def embed_audio(audio_path: str, device: str) -> torch.Tensor:
|
| 118 |
-
import librosa
|
| 119 |
-
from transformers import WhisperFeatureExtractor, WhisperModel
|
| 120 |
-
|
| 121 |
-
extractor = WhisperFeatureExtractor.from_pretrained(AUDIO_ENCODER_NAME)
|
| 122 |
-
model = WhisperModel.from_pretrained(AUDIO_ENCODER_NAME, dtype=torch.float16).to(device).eval()
|
| 123 |
-
encoder = model.encoder
|
| 124 |
-
target_sr = extractor.sampling_rate
|
| 125 |
-
|
| 126 |
-
wave, sr = librosa.load(audio_path, sr=None, mono=True)
|
| 127 |
-
if sr != target_sr:
|
| 128 |
-
wave = librosa.resample(wave.astype(np.float32), orig_sr=sr, target_sr=target_sr)
|
| 129 |
-
duration_s = len(wave) / target_sr
|
| 130 |
-
n_real = min(WHISPER_MAX_FRAMES, max(1, round(duration_s * WHISPER_ENCODER_FPS)))
|
| 131 |
-
|
| 132 |
-
inputs = extractor(wave, sampling_rate=target_sr, return_tensors="pt")
|
| 133 |
-
input_features = inputs["input_features"].to(device=device, dtype=torch.float16)
|
| 134 |
-
with torch.no_grad():
|
| 135 |
-
out = encoder(input_features, output_hidden_states=True)
|
| 136 |
-
hs = out.hidden_states[WHISPER_LAYER_IDX][:, :n_real, :]
|
| 137 |
-
audio_vec = hs.mean(dim=1)[0].float()
|
| 138 |
-
return audio_vec.unsqueeze(0).to(device)
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
def main() -> None:
|
| 142 |
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 143 |
-
ap.add_argument("--audio", required=True, help="path to an audio file (wav/mp3/flac/...)")
|
| 144 |
-
ap.add_argument("--text", required=True, help="Russian description of how the speech sounds")
|
| 145 |
-
ap.add_argument("--gender", default="unknown", choices=list(GENDER_TO_ID),
|
| 146 |
-
help="reader gender to condition on — 'unknown' matches the paper's zero-shot "
|
| 147 |
-
"evaluation setup on external datasets (default)")
|
| 148 |
-
ap.add_argument("--checkpoint", default="checkpoints/qwen3_whisper_last_seed0_best.pt",
|
| 149 |
-
help="any of the 5 published seeds are equivalent for this purpose")
|
| 150 |
-
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 151 |
-
args = ap.parse_args()
|
| 152 |
-
|
| 153 |
-
model = load_clap_model(args.checkpoint, args.device)
|
| 154 |
-
|
| 155 |
-
text_emb = embed_text(args.text, args.device)
|
| 156 |
-
audio_emb = embed_audio(args.audio, args.device)
|
| 157 |
-
|
| 158 |
-
gender_id = torch.tensor([GENDER_TO_ID[args.gender]], dtype=torch.long, device=args.device)
|
| 159 |
-
with torch.no_grad():
|
| 160 |
-
text_z = model.encode_text(text_emb, gender_id)
|
| 161 |
-
audio_z = model.encode_audio(audio_emb)
|
| 162 |
-
similarity = (text_z * audio_z).sum(dim=-1).item()
|
| 163 |
-
|
| 164 |
-
print(f"cosine similarity: {similarity:.4f}")
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
if __name__ == "__main__":
|
| 168 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|