SlavaYakubov commited on
Commit
ffa767f
·
verified ·
1 Parent(s): 23e5552

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()