File size: 9,201 Bytes
fac7535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""

classify_models.py

==================



根据 checkpoint 里的参数名 / 形状,判断 .ckpt 模型属于哪种分离模型架构

(bs_roformer / mel_band_roformer / scnet / bandit / … )。



判据来自 E:\\vs\\pymss-core\\pymss_core\\modules 下的参考实现:



  bs_roformer.py / mel_band_roformer.py   ->  band_split(BandSplit) + mask_estimators(MaskEstimator) + layers(Transformer)

  bands.py  BandSplit       : self.to_features = [Sequential(RMSNorm(gamma), Linear)]

  bands.py  MaskEstimator   : self.to_freqs    = [Sequential(MLP, GLU)]

  transformer.py Attention  : self.norm(gamma) / to_qkv / to_gates / to_out

  scnet.py  SCNet           : self.separation_net / SDlayer / dp_modules / encoder / decoder

  bandit_v2/bandit.py       : self.band_split(norm_fc_modules) / tf_model(seqband) / mask_estim[stem]



bs_roformer 与 mel_band_roformer 的判别点(两者 band_split / mask_estimators 结构相同):

  * BSRoformer.final_norm      = RMSNorm(dim)  -> state_dict 里有 "final_norm.gamma"

  * MelBandRoformer.final_norm  = nn.Identity() -> 没有任何 final_norm 参数

  * BSRoformer 常用 use_shared_bias -> 有 "linear_62_bias_0" / "linear_64_bias_0"

  * freq 带宽分布:BS 默认 DEFAULT_FREQS_PER_BANDS,mel 版本是按 mel 滤波器分带



用法

----

    python classify_models.py                    # 扫描 ./ckpt

    python classify_models.py --dir <ckpt目录>

    python classify_models.py --json out.json

"""
from __future__ import annotations

import argparse
import json
import os
import re
import sys
from collections import defaultdict

import torch

import model_io

HERE = os.path.dirname(os.path.abspath(__file__))

# pymss-core 里 BS-RoFormer 的默认频带划分(单声道)
BS_DEFAULT_FREQS = (2,) * 24 + (4,) * 12 + (12,) * 8 + (24,) * 8 + (48,) * 8 + (128, 129)


def idx_set(pattern: str, keys) -> set[str]:
    out = set()
    for k in keys:
        m = re.search(pattern, k)
        if m:
            out.add(m.group(1))
    return out


def shape_of(sd, pattern: str):
    for k, v in sd.items():
        if re.search(pattern, k):
            return tuple(v.shape)
    return None


def classify(sd: dict) -> dict:
    all_keys = list(sd)
    # 别名键(coreml.source.* / onnx.source.*)保留了原始 PyTorch 名。有些模型
    # (例如 drumsep)的 mask estimator 只以别名形式存在,因此判定时使用全量键。
    keys = all_keys
    info: dict = {}

    # ---- Bandit / Bandit v2 :band_split(norm_fc_modules) + tf_model(seqband) ----
    if any("norm_fc_modules" in k for k in keys) or any(k.startswith("tf_model") for k in keys):
        stems = sorted({m.group(1) for k in keys
                        if (m := re.match(r"mask_estim\.([A-Za-z0-9_]+)\.", k))})
        info.update(
            family="bandit",
            subtype="bandit_v2" if "band_split.norm_fc" in " ".join(keys) else "bandit",
            bands=len(idx_set(r"band_split\.norm_fc_modules\.(\d+)\.", keys)),
            stems=stems,
            stft_nfft=2048,
        )
        return info

    # ---- SCNet ----
    if any(k.startswith(("separation_net", "model.separation_net")) for k in keys) or \
       any("SDlayer" in k for k in keys):
        info.update(family="scnet",
                    encoders=len(idx_set(r"(?:^|[./])encoder\.(\d+)\.", keys)),
                    decoders=len(idx_set(r"(?:^|[./])decoder\.(\d+)\.", keys)))
        return info

    # ---- ConvNeXt U-Net generator(GAN 类降噪器)----
    if any("generator.trainable" in k for k in keys):
        info.update(family="convnext_gan_generator",
                    convnext_blocks=len(idx_set(r"convnext\.(\d+)\.", keys)),
                    u_blocks=len(idx_set(r"u_blocks\.(\d+)\.", keys)),
                    d_blocks=len(idx_set(r"d_blocks\.(\d+)\.", keys)),
                    snake=any(k.endswith(".alpha") for k in keys))
        return info

    # ---- ReDimNet 说话人编码器 ----
    if any(k.startswith("backbone.stage") for k in keys):
        info.update(family="redimnet",
                    stages=len(idx_set(r"backbone\.stage(\d+)\.", keys)))
        return info

    # ---- 小型打分头 ----
    if any(k.startswith(("scorer.", "temporal.")) for k in keys):
        info.update(family="scoring_head",
                    layers=len(idx_set(r"temporal\.layers\.(\d+)\.", keys)))
        return info

    # ---- Roformer 家族 ----
    if any("band_split" in k for k in keys) and \
       any(k.startswith("mask_est") or "mask_estimator" in k for k in keys):
        bands = (idx_set(r"band_split\.(\d+)\.linear\.weight", keys)
                 or idx_set(r"band_split\.to_features\.(\d+)\.", keys))
        n_bands = len(bands)
        # 每带 linear 的 (out_features=dim, in_features=复数频点×声道)
        widths = []
        for k in keys:
            m = re.fullmatch(r"band_split\.(\d+)\.linear\.weight", k)
            if m:
                widths.append((int(m.group(1)), sd[k].shape[0], sd[k].shape[1]))
        if not widths:
            for k in keys:
                m = re.fullmatch(r"band_split\.to_features\.(\d+)\.1\.weight", k)
                if m:
                    widths.append((int(m.group(1)), sd[k].shape[0], sd[k].shape[1]))
        widths.sort()
        per_band = [w // 2 for _, _, w in widths]   # 立体声 → /2 得单声道频点数

        final_norm = any("final_norm" in k for k in keys)
        shared_bias = any("linear_62_bias_0" in k or "linear_64_bias_0" in k for k in keys)
        branch = any(".branch." in k for k in keys)
        depth = len(idx_set(r"^(?:layers|blk)\.(\d+)\.", keys))
        stems_raw = (idx_set(r"(?:mask_estimators?|mask_est)\.(\d+)\.", keys)
                     | idx_set(r"mask_estimators?_(\d+)_", keys))
        heads = shape_of(sd, r"to_gates\.weight$")

        info.update(
            family="bs_roformer" if final_norm else "mel_band_roformer",
            bands=n_bands,
            depth=depth,
            dim=widths[0][1] if widths else None,
            heads=heads[0] if heads else None,
            stems=len(stems_raw),
            final_norm=final_norm,
            shared_bias=shared_bias,
            attention_branch=branch,
            freqs_per_band=per_band,
            freq_layout="BS-DEFAULT(stereo)" if per_band == [2 * f for f in BS_DEFAULT_FREQS]
                        else "BS-DEFAULT(mono)" if per_band == list(BS_DEFAULT_FREQS)
                        else "custom",
        )
        return info

    info.update(family="unknown")
    return info


def describe(info: dict) -> str:
    fam = info.get("family", "?")
    if fam in ("bs_roformer", "mel_band_roformer"):
        bits = [f"{info['bands']} bands", f"dim={info['dim']}", f"depth={info['depth']}",
                f"{info['stems']} stem(s)", f"freq={info.get('freq_layout')}"]
        if info.get("shared_bias"):
            bits.append("shared_bias")
        if info.get("attention_branch"):
            bits.append("attn-branch")
        return "  ".join(bits)
    if fam == "bandit":
        return f"n_bands={info.get('bands')}  stems={info.get('stems')}"
    if fam == "scnet":
        enc, dec = info.get("encoders") or 0, info.get("decoders") or 0
        return f"{enc} enc / {dec} dec" if enc else f"{dec} decoder(s)"
    if fam == "convnext_gan_generator":
        return (f"convnext={info.get('convnext_blocks')} u_blocks={info.get('u_blocks')} "
                f"d_blocks={info.get('d_blocks')} snake={info.get('snake')}")
    if fam == "redimnet":
        return f"stages={info.get('stages')}"
    if fam == "scoring_head":
        return f"temporal_layers={info.get('layers')}"
    return ""


def main() -> int:
    ap = argparse.ArgumentParser(description="识别 ckpt 的分离模型类型")
    ap.add_argument("--dir", default=os.path.join(HERE, "..", "raw"), help="模型目录(safetensors / ckpt)")
    ap.add_argument("--json", default=None, help="把结果另存为 JSON")
    args = ap.parse_args()

    paths = []
    for root, _d, files in os.walk(args.dir):
        for n in files:
            if n.endswith((".ckpt", ".safetensors")):
                paths.append(os.path.join(root, n))
    if not paths:
        print("没有找到 .ckpt / .safetensors")
        return 1

    results = {}
    print(f"{'模型':<34}{'类型':<22}关键参数")
    print("-" * 108)
    for p in sorted(paths):
        sd = model_io.load_any(p)
        info = classify(sd)
        name = os.path.splitext(os.path.basename(p))[0]
        results[name] = {"type": info.get("family"), **{k: v for k, v in info.items() if k != "family"}}
        print(f"{name:<34}{info.get('family', '?'):<22}{describe(info)}")
        del sd

    if args.json:
        with open(args.json, "w", encoding="utf-8") as f:
            json.dump(results, f, indent=2, ensure_ascii=False)
        print(f"\n已写入 {args.json}")
    return 0


if __name__ == "__main__":
    sys.exit(main())