File size: 20,874 Bytes
9b965b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
"""Codecfake W2V2-AASIST (Xie et al., arXiv 2405.04880) — inference port.

Source: https://github.com/xieyuankun/Codecfake @ 72c32e2840eef95fbeade349425fa9c5391fa11e (NO LICENSE).
  model.py: GraphAttentionLayer, HtrgGraphAttentionLayer, GraphPool, Residual_block, W2VAASIST
  generate_score.py / preprocess.py: pad_dataset (first 64600 samples, repeat-tile), HF
  Wav2Vec2FeatureExtractor (do_normalize) + frozen facebook/wav2vec2-xls-r-300m, hidden_states[5]
  -> unsqueeze(0) -> transpose(2,3) = (B, 1, 1024, T) -> W2VAASIST -> (last_hidden, logits).

Edits vs the source (inference-only, maths unchanged):
  * removed `print(x.shape)` in W2VAASIST.forward and the unused imports (pytorch_model_summary, ...);
  * Residual_block: the source computes bn1->selu for non-first blocks and then applies conv1 to `x`,
    discarding that result; the dead computation is dropped (identical output; bn1 weights kept so the
    checkpoint loads strictly);
  * the frozen XLS-R front-end is truncated to its first 5 transformer layers: hidden_states[5] of the
    24-layer HF model is the output of layer index 4 *before* any final LayerNorm, so `XLSRLayer5`
    runs feature encoder -> feature projection -> pos-conv -> layers[0..4] and returns that tensor
    (bit-identical to the full model's hidden_states[5], verified in implementation-notes.md);
  * the HF feature-extractor normalisation is reproduced per row in torch (zero mean, unit population
    variance, eps 1e-7), so a whole batch is normalised on the GPU;
  * checkpoints are pickled whole modules (torch.save(model)); `load_pickled_state` unpickles them with an
    allow-list unpickler that maps the source's `model.*` classes to a bare nn.Module stub (no source
    code runs) and returns the module's state_dict.
"""
from __future__ import annotations

import pickle
from typing import Union

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

WINDOW = 64600
XLSR_REPO = "facebook/wav2vec2-xls-r-300m"
XLSR_REV = "1a640f32ac3e39899438a2931f9924c02f080a54"
N_LAYERS = 5  # hidden_states[5]


def pad_repeat(x: np.ndarray, cut: int = WINDOW) -> np.ndarray:
    """Source pad_dataset: first `cut` samples; shorter clips tiled (int(cut/len)+1 repeats) and cropped."""
    n = x.shape[0]
    if n >= cut:
        return x[:cut]
    return np.tile(x, int(cut / n) + 1)[:cut]


def normalize(x: torch.Tensor) -> torch.Tensor:
    """Wav2Vec2FeatureExtractor zero_mean_unit_var_norm, per row: (x - mean) / sqrt(var + 1e-7)."""
    m = x.mean(dim=-1, keepdim=True)
    v = x.var(dim=-1, keepdim=True, unbiased=False)
    return (x - m) / torch.sqrt(v + 1e-7)


# ----------------------------------------------------------------------------- XLS-R front-end
class XLSRLayer5(nn.Module):
    """Frozen XLS-R 300M, first 5 transformer layers; returns HF hidden_states[5] (B, T, 1024)."""

    def __init__(self, w2v):
        super().__init__()
        self.feature_extractor = w2v.feature_extractor
        self.feature_projection = w2v.feature_projection
        self.pos_conv_embed = w2v.encoder.pos_conv_embed
        self.layers = nn.ModuleList(list(w2v.encoder.layers)[:N_LAYERS])

    def forward(self, wav: torch.Tensor) -> torch.Tensor:
        h = self.feature_extractor(wav).transpose(1, 2)
        h, _ = self.feature_projection(h)
        h = h + self.pos_conv_embed(h)
        for layer in self.layers:
            h = layer(h, attention_mask=None)[0]
        return h


def build_xlsr(local_dir: str | None = None) -> XLSRLayer5:
    """facebook/wav2vec2-xls-r-300m @ XLSR_REV, loaded with only N_LAYERS transformer layers (asserted)."""
    from transformers import Wav2Vec2Config, Wav2Vec2Model
    cfg = Wav2Vec2Config.from_pretrained(XLSR_REPO, revision=XLSR_REV)
    assert cfg.do_stable_layer_norm and cfg.num_hidden_layers == 24 and cfg.hidden_size == 1024
    cfg.num_hidden_layers = N_LAYERS
    w2v, info = Wav2Vec2Model.from_pretrained(XLSR_REPO, revision=XLSR_REV, config=cfg,
                                             attn_implementation="eager", output_loading_info=True)
    assert not info["missing_keys"], info["missing_keys"]
    unexp = [k.removeprefix("wav2vec2.") for k in info["unexpected_keys"]]
    bad = [k for k in unexp  # allowed: dropped layers 5..23 + the pre-training-only heads
           if not (k.startswith(("encoder.layers.", "quantizer.", "project_q.", "project_hid."))
                   and not any(k.startswith(f"encoder.layers.{i}.") for i in range(N_LAYERS)))]
    assert sum(k.startswith("encoder.layers.") for k in unexp) == 16 * (24 - N_LAYERS)
    assert not bad, bad
    return XLSRLayer5(w2v)


# ----------------------------------------------------------------------------- AASIST head (model.py)
class GraphAttentionLayer(nn.Module):
    def __init__(self, in_dim, out_dim, **kwargs):
        super().__init__()
        self.att_proj = nn.Linear(in_dim, out_dim)
        self.att_weight = self._init_new_params(out_dim, 1)
        self.proj_with_att = nn.Linear(in_dim, out_dim)
        self.proj_without_att = nn.Linear(in_dim, out_dim)
        self.bn = nn.BatchNorm1d(out_dim)
        self.input_drop = nn.Dropout(p=0.2)
        self.act = nn.SELU(inplace=True)
        self.temp = kwargs.get("temperature", 1.)

    def forward(self, x):
        x = self.input_drop(x)
        att_map = self._derive_att_map(x)
        x = self._project(x, att_map)
        x = self._apply_BN(x)
        return self.act(x)

    def _pairwise_mul_nodes(self, x):
        nb_nodes = x.size(1)
        x = x.unsqueeze(2).expand(-1, -1, nb_nodes, -1)
        return x * x.transpose(1, 2)

    def _derive_att_map(self, x):
        att_map = self._pairwise_mul_nodes(x)
        att_map = torch.tanh(self.att_proj(att_map))
        att_map = torch.matmul(att_map, self.att_weight)
        att_map = att_map / self.temp
        return F.softmax(att_map, dim=-2)

    def _project(self, x, att_map):
        return self.proj_with_att(torch.matmul(att_map.squeeze(-1), x)) + self.proj_without_att(x)

    def _apply_BN(self, x):
        org_size = x.size()
        return self.bn(x.view(-1, org_size[-1])).view(org_size)

    def _init_new_params(self, *size):
        out = nn.Parameter(torch.FloatTensor(*size))
        nn.init.xavier_normal_(out)
        return out


class HtrgGraphAttentionLayer(nn.Module):
    def __init__(self, in_dim, out_dim, **kwargs):
        super().__init__()
        self.proj_type1 = nn.Linear(in_dim, in_dim)
        self.proj_type2 = nn.Linear(in_dim, in_dim)
        self.att_proj = nn.Linear(in_dim, out_dim)
        self.att_projM = nn.Linear(in_dim, out_dim)
        self.att_weight11 = self._init_new_params(out_dim, 1)
        self.att_weight22 = self._init_new_params(out_dim, 1)
        self.att_weight12 = self._init_new_params(out_dim, 1)
        self.att_weightM = self._init_new_params(out_dim, 1)
        self.proj_with_att = nn.Linear(in_dim, out_dim)
        self.proj_without_att = nn.Linear(in_dim, out_dim)
        self.proj_with_attM = nn.Linear(in_dim, out_dim)
        self.proj_without_attM = nn.Linear(in_dim, out_dim)
        self.bn = nn.BatchNorm1d(out_dim)
        self.input_drop = nn.Dropout(p=0.2)
        self.act = nn.SELU(inplace=True)
        self.temp = kwargs.get("temperature", 1.)

    def forward(self, x1, x2, master=None):
        num_type1 = x1.size(1)
        num_type2 = x2.size(1)
        x1 = self.proj_type1(x1)
        x2 = self.proj_type2(x2)
        x = torch.cat([x1, x2], dim=1)
        if master is None:
            master = torch.mean(x, dim=1, keepdim=True)
        x = self.input_drop(x)
        att_map = self._derive_att_map(x, num_type1, num_type2)
        master = self._update_master(x, master)
        x = self._project(x, att_map)
        x = self._apply_BN(x)
        x = self.act(x)
        x1 = x.narrow(1, 0, num_type1)
        x2 = x.narrow(1, num_type1, num_type2)
        return x1, x2, master

    def _update_master(self, x, master):
        att_map = self._derive_att_map_master(x, master)
        return self._project_master(x, master, att_map)

    def _pairwise_mul_nodes(self, x):
        nb_nodes = x.size(1)
        x = x.unsqueeze(2).expand(-1, -1, nb_nodes, -1)
        return x * x.transpose(1, 2)

    def _derive_att_map_master(self, x, master):
        att_map = x * master
        att_map = torch.tanh(self.att_projM(att_map))
        att_map = torch.matmul(att_map, self.att_weightM)
        att_map = att_map / self.temp
        return F.softmax(att_map, dim=-2)

    def _derive_att_map(self, x, num_type1, num_type2):
        att_map = self._pairwise_mul_nodes(x)
        att_map = torch.tanh(self.att_proj(att_map))
        att_board = torch.zeros_like(att_map[:, :, :, 0]).unsqueeze(-1)
        att_board[:, :num_type1, :num_type1, :] = torch.matmul(
            att_map[:, :num_type1, :num_type1, :], self.att_weight11)
        att_board[:, num_type1:, num_type1:, :] = torch.matmul(
            att_map[:, num_type1:, num_type1:, :], self.att_weight22)
        att_board[:, :num_type1, num_type1:, :] = torch.matmul(
            att_map[:, :num_type1, num_type1:, :], self.att_weight12)
        att_board[:, num_type1:, :num_type1, :] = torch.matmul(
            att_map[:, num_type1:, :num_type1, :], self.att_weight12)
        att_map = att_board / self.temp
        return F.softmax(att_map, dim=-2)

    def _project(self, x, att_map):
        return self.proj_with_att(torch.matmul(att_map.squeeze(-1), x)) + self.proj_without_att(x)

    def _project_master(self, x, master, att_map):
        x1 = self.proj_with_attM(torch.matmul(att_map.squeeze(-1).unsqueeze(1), x))
        return x1 + self.proj_without_attM(master)

    def _apply_BN(self, x):
        org_size = x.size()
        return self.bn(x.view(-1, org_size[-1])).view(org_size)

    def _init_new_params(self, *size):
        out = nn.Parameter(torch.FloatTensor(*size))
        nn.init.xavier_normal_(out)
        return out


class GraphPool(nn.Module):
    def __init__(self, k: float, in_dim: int, p: Union[float, int]):
        super().__init__()
        self.k = k
        self.sigmoid = nn.Sigmoid()
        self.proj = nn.Linear(in_dim, 1)
        self.drop = nn.Dropout(p=p) if p > 0 else nn.Identity()
        self.in_dim = in_dim

    def forward(self, h):
        scores = self.sigmoid(self.proj(self.drop(h)))
        return self.top_k_graph(scores, h, self.k)

    def top_k_graph(self, scores, h, k):
        _, n_nodes, n_feat = h.size()
        n_nodes = max(int(n_nodes * k), 1)
        _, idx = torch.topk(scores, n_nodes, dim=1)
        idx = idx.expand(-1, -1, n_feat)
        h = h * scores
        return torch.gather(h, 1, idx)


class Residual_block(nn.Module):
    def __init__(self, nb_filts, first=False):
        super().__init__()
        self.first = first
        if not self.first:
            self.bn1 = nn.BatchNorm2d(num_features=nb_filts[0])
        self.conv1 = nn.Conv2d(nb_filts[0], nb_filts[1], kernel_size=(2, 3), padding=(1, 1), stride=1)
        self.selu = nn.SELU(inplace=True)
        self.bn2 = nn.BatchNorm2d(num_features=nb_filts[1])
        self.conv2 = nn.Conv2d(nb_filts[1], nb_filts[1], kernel_size=(2, 3), padding=(0, 1), stride=1)
        if nb_filts[0] != nb_filts[1]:
            self.downsample = True
            self.conv_downsample = nn.Conv2d(nb_filts[0], nb_filts[1], padding=(0, 1), kernel_size=(1, 3), stride=1)
        else:
            self.downsample = False

    def forward(self, x):
        identity = x
        # source: bn1/selu computed for non-first blocks, but conv1 is applied to x (kept verbatim; the
        # inplace SELU acts on bn1's fresh output, so x is untouched)
        out = self.conv1(x)
        out = self.bn2(out)
        out = self.selu(out)
        out = self.conv2(out)
        if self.downsample:
            identity = self.conv_downsample(identity)
        out += identity
        return out


class W2VAASIST(nn.Module):
    def __init__(self):
        super().__init__()
        filts = [128, [1, 32], [32, 32], [32, 64], [64, 64]]
        gat_dims = [64, 32]
        pool_ratios = [0.5, 0.5, 0.5, 0.5]
        temperatures = [2.0, 2.0, 100.0, 100.0]
        self.first_bn = nn.BatchNorm2d(num_features=1)
        self.first_bn1 = nn.BatchNorm2d(num_features=64)
        self.drop = nn.Dropout(0.5, inplace=True)
        self.drop_way = nn.Dropout(0.2, inplace=True)
        self.selu = nn.SELU(inplace=True)
        self.encoder = nn.Sequential(
            nn.Sequential(Residual_block(nb_filts=filts[1], first=True)),
            nn.Sequential(Residual_block(nb_filts=filts[2])),
            nn.Sequential(Residual_block(nb_filts=filts[3])),
            nn.Sequential(Residual_block(nb_filts=filts[4])),
            nn.Sequential(Residual_block(nb_filts=filts[4])),
            nn.Sequential(Residual_block(nb_filts=filts[4])))
        self.LL = nn.Linear(1024, 128)
        self.attention = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=(1, 1)),
            nn.SELU(inplace=True),
            nn.BatchNorm2d(128),
            nn.Conv2d(128, 64, kernel_size=(1, 1)),
        )
        self.pos_S = nn.Parameter(torch.randn(1, 42, filts[-1][-1]))
        self.master1 = nn.Parameter(torch.randn(1, 1, gat_dims[0]))
        self.master2 = nn.Parameter(torch.randn(1, 1, gat_dims[0]))
        self.GAT_layer_S = GraphAttentionLayer(filts[-1][-1], gat_dims[0], temperature=temperatures[0])
        self.GAT_layer_T = GraphAttentionLayer(filts[-1][-1], gat_dims[0], temperature=temperatures[1])
        self.HtrgGAT_layer_ST11 = HtrgGraphAttentionLayer(gat_dims[0], gat_dims[1], temperature=temperatures[2])
        self.HtrgGAT_layer_ST12 = HtrgGraphAttentionLayer(gat_dims[1], gat_dims[1], temperature=temperatures[2])
        self.HtrgGAT_layer_ST21 = HtrgGraphAttentionLayer(gat_dims[0], gat_dims[1], temperature=temperatures[2])
        self.HtrgGAT_layer_ST22 = HtrgGraphAttentionLayer(gat_dims[1], gat_dims[1], temperature=temperatures[2])
        self.pool_S = GraphPool(pool_ratios[0], gat_dims[0], 0.3)
        self.pool_T = GraphPool(pool_ratios[1], gat_dims[0], 0.3)
        self.pool_hS1 = GraphPool(pool_ratios[2], gat_dims[1], 0.3)
        self.pool_hT1 = GraphPool(pool_ratios[2], gat_dims[1], 0.3)
        self.pool_hS2 = GraphPool(pool_ratios[2], gat_dims[1], 0.3)
        self.pool_hT2 = GraphPool(pool_ratios[2], gat_dims[1], 0.3)
        self.out_layer = nn.Linear(5 * gat_dims[1], 2)

    def forward(self, x):
        x = x.squeeze(dim=1)
        x = x.transpose(1, 2)
        x = self.LL(x)
        x = x.transpose(1, 2)
        x = x.unsqueeze(dim=1)
        x = F.max_pool2d(x, (3, 3))
        x = self.first_bn(x)
        x = self.selu(x)
        x = self.encoder(x)
        x = self.first_bn1(x)
        x = self.selu(x)
        w = self.attention(x)
        w1 = F.softmax(w, dim=-1)
        m = torch.sum(x * w1, dim=-1)
        e_S = m.transpose(1, 2) + self.pos_S
        gat_S = self.GAT_layer_S(e_S)
        out_S = self.pool_S(gat_S)
        w2 = F.softmax(w, dim=-2)
        m1 = torch.sum(x * w2, dim=-2)
        e_T = m1.transpose(1, 2)
        gat_T = self.GAT_layer_T(e_T)
        out_T = self.pool_T(gat_T)
        master1 = self.master1.expand(x.size(0), -1, -1)
        master2 = self.master2.expand(x.size(0), -1, -1)
        out_T1, out_S1, master1 = self.HtrgGAT_layer_ST11(out_T, out_S, master=self.master1)
        out_S1 = self.pool_hS1(out_S1)
        out_T1 = self.pool_hT1(out_T1)
        out_T_aug, out_S_aug, master_aug = self.HtrgGAT_layer_ST12(out_T1, out_S1, master=master1)
        out_T1 = out_T1 + out_T_aug
        out_S1 = out_S1 + out_S_aug
        master1 = master1 + master_aug
        out_T2, out_S2, master2 = self.HtrgGAT_layer_ST21(out_T, out_S, master=self.master2)
        out_S2 = self.pool_hS2(out_S2)
        out_T2 = self.pool_hT2(out_T2)
        out_T_aug, out_S_aug, master_aug = self.HtrgGAT_layer_ST22(out_T2, out_S2, master=master2)
        out_T2 = out_T2 + out_T_aug
        out_S2 = out_S2 + out_S_aug
        master2 = master2 + master_aug
        out_T1 = self.drop_way(out_T1)
        out_T2 = self.drop_way(out_T2)
        out_S1 = self.drop_way(out_S1)
        out_S2 = self.drop_way(out_S2)
        master1 = self.drop_way(master1)
        master2 = self.drop_way(master2)
        out_T = torch.max(out_T1, out_T2)
        out_S = torch.max(out_S1, out_S2)
        master = torch.max(master1, master2)
        T_max, _ = torch.max(torch.abs(out_T), dim=1)
        T_avg = torch.mean(out_T, dim=1)
        S_max, _ = torch.max(torch.abs(out_S), dim=1)
        S_avg = torch.mean(out_S, dim=1)
        last_hidden = torch.cat([T_max, T_avg, S_max, S_avg, master.squeeze(1)], dim=1)
        last_hidden = self.drop(last_hidden)
        output = self.out_layer(last_hidden)
        return last_hidden, output


class CodecfakeNet(nn.Module):
    """wav (B, 64600) -> per-row normalisation -> XLS-R hidden_states[5] -> W2VAASIST logits (B, 2)."""

    def __init__(self, xlsr: XLSRLayer5, head: W2VAASIST):
        super().__init__()
        self.xlsr = xlsr
        self.head = head

    def forward(self, wav: torch.Tensor) -> torch.Tensor:
        h = self.xlsr(normalize(wav))                    # (B, T, 1024)
        h = h.unsqueeze(1).transpose(2, 3)               # (B, 1, 1024, T), as generate_score.py
        return self.head(h)[1]


# ----------------------------------------------------------------------------- checkpoint conversion
class _Stub(nn.Module):
    """Stand-in for the source's model.* classes while unpickling (state only, no source code)."""

    def forward(self, *a, **k):  # pragma: no cover
        raise RuntimeError("stub")


_ALLOWED = {
    ("collections", "OrderedDict"), ("builtins", "set"), ("__builtin__", "set"),
    ("torch", "FloatStorage"), ("torch", "LongStorage"),
    ("torch._utils", "_rebuild_parameter"), ("torch._utils", "_rebuild_tensor_v2"),
}
_SOURCE_CLASSES = {"W2VAASIST", "GraphAttentionLayer", "HtrgGraphAttentionLayer", "GraphPool", "Residual_block"}


class _AllowListUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == "model" and name in _SOURCE_CLASSES:
            return _Stub
        if module.startswith("torch.nn.modules.") or (module, name) in _ALLOWED:
            return super().find_class(module, name)
        raise pickle.UnpicklingError(f"blocked global {module}.{name}")


class _PickleModule:
    Unpickler = _AllowListUnpickler
    load = staticmethod(pickle.load)
    __name__ = "codecfake_allowlist_pickle"


def load_pickled_state(path: str) -> dict:
    """state_dict of the source's torch.save(W2VAASIST) file, via the allow-list unpickler."""
    mod = torch.load(path, map_location="cpu", weights_only=False, pickle_module=_PickleModule)
    assert isinstance(mod, _Stub), type(mod)
    return {k: v.detach().clone().contiguous() for k, v in mod.state_dict().items()}


# ----------------------------------------------------------------------------- weight acquisition
SRC_REPO = "xieyuankun/Codecfake"
SRC_REV = "72c32e2840eef95fbeade349425fa9c5391fa11e"
CKPT_SHA256 = {
    "cotrain_w2v2aasist_CSAM": "86876e2c0eb98d6e60ecf2f1139112206adf47b1562aee72530d0c22e04c644a",
    "codec_w2v2aasist": "5a606d1fbd578dbd214471cead283c236f4204c54d602e945924b3594f8c25a3",
}


def ckpt_url(sub: str) -> str:
    return f"https://raw.githubusercontent.com/{SRC_REPO}/{SRC_REV}/pretrained_model/{sub}/anti-spoofing_feat_model.pt"


def head_state(sub: str, model_dir: str) -> dict:
    """W2VAASIST state_dict for pretrained_model/<sub>. No upstream license -> not re-hosted: on first use the
    authors' pickled module is downloaded from the commit-pinned GitHub raw URL (sha256-checked) into
    ../_ckpt_dl/xieyuankun__Codecfake/<rev>/, converted once with the allow-list unpickler, and cached as
    <model_dir>/head_<sub>.safetensors (loaded on every later run)."""
    import hashlib
    import os
    import urllib.request
    from safetensors.torch import load_file, save_file
    cache = os.path.join(model_dir, f"head_{sub}.safetensors")
    if not os.path.exists(cache):
        pt = os.path.join(model_dir, "..", "_ckpt_dl", "xieyuankun__Codecfake", SRC_REV, "pretrained_model",
                          sub, "anti-spoofing_feat_model.pt")
        if not os.path.exists(pt):
            os.makedirs(os.path.dirname(pt), exist_ok=True)
            urllib.request.urlretrieve(ckpt_url(sub), pt + ".part")
            os.replace(pt + ".part", pt)
        digest = hashlib.sha256(open(pt, "rb").read()).hexdigest()
        if digest != CKPT_SHA256[sub]:
            raise RuntimeError(f"{pt}: sha256 {digest} != pinned {CKPT_SHA256[sub]}")
        save_file(load_pickled_state(pt), cache + ".part", metadata={"source": ckpt_url(sub), "sha256": digest})
        os.replace(cache + ".part", cache)
    return load_file(cache)