| |
| """v2 Stage 4a — re-derive the Transkun ONNX export + frozen buffers + TDD fixtures. |
| |
| Boundary: featuresBatch -> S (backbone + inner-product CRF scorer). The mel front end (audio -> |
| featuresBatch) and the Viterbi decode (S -> intervals) are ported to C# (4b/4c); this script also emits |
| their reference fixtures. All outputs are written raw little-endian + a manifest.json for trivial C# reads. |
| """ |
| import json, math, os, sys |
| import numpy as np |
| import torch |
|
|
| TK_DIR = "/private/tmp/claude-501/-Users-lawls-Development-TuesdayCrowd-Projects-audio-claudio/37748a9b-31e0-48a1-896e-7a25c1faf008/scratchpad/transkun-env/lib/python3.14/site-packages/transkun" |
| OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + "/artifacts" |
| os.makedirs(OUT, exist_ok=True) |
| torch.manual_seed(0) |
| np.random.seed(0) |
|
|
| manifest = {} |
| def save(name, arr, dtype): |
| a = np.ascontiguousarray(arr).astype(dtype) |
| fn = name + (".f32" if dtype == "<f4" else ".i32") |
| a.tofile(os.path.join(OUT, fn)) |
| manifest[name] = {"file": fn, "shape": list(a.shape), "dtype": "f32" if dtype == "<f4" else "i32"} |
| print(f" saved {name:22} shape={list(a.shape)} dtype={manifest[name]['dtype']}") |
|
|
| |
| print("[1] loading model from 2.0.conf + 2.0.pt") |
| import moduleconf |
| conf_path = os.path.join(TK_DIR, "pretrained", "2.0.conf") |
| pt_path = os.path.join(TK_DIR, "pretrained", "2.0.pt") |
| confManager = moduleconf.parseFromFile(conf_path) |
| TransKun = confManager["Model"].module.TransKun |
| conf = confManager["Model"].config |
| checkpoint = torch.load(pt_path, map_location="cpu") |
| model = TransKun(conf=conf) |
| key = "best_state_dict" if "best_state_dict" in checkpoint else "state_dict" |
| missing = model.load_state_dict(checkpoint[key], strict=False) |
| model.eval(); torch.set_grad_enabled(False) |
| print(f" loaded ({key}); missing={len(missing.missing_keys)} unexpected={len(missing.unexpected_keys)}") |
| print(f" fs={model.fs} windowSize={model.windowSize} hopSize={model.hopSize} " |
| f"nSym={len(model.targetMIDIPitch)} params={sum(p.numel() for p in model.parameters())/1e6:.2f}M") |
|
|
| |
| class ExportWrapper(torch.nn.Module): |
| def __init__(self, m): |
| super().__init__() |
| self.backbone = m.backbone |
| self.scorer = m.scorer |
| self.register_buffer("outputIndices", torch.tensor(m.targetMIDIPitch)) |
| def forward(self, featuresBatch): |
| ctx = self.backbone(featuresBatch, outputIndices=self.outputIndices) |
| S_batch, _ = self.scorer(ctx) |
| return S_batch.flatten(-2, -1) |
|
|
| wrapper = ExportWrapper(model).eval() |
| onnx_path = os.path.join(OUT, "model.onnx") |
| T0 = 64 |
| feat_example = torch.randn(1, T0, 229, 6) |
|
|
| |
| |
| |
| import torch.nn.functional as F |
| _orig_sdpa = F.scaled_dot_product_attention |
| def _sdpa_4d(q, k, v, *a, **kw): |
| if q.ndim == 5: |
| B, X, H, L, D = q.shape |
| rs = lambda t: t.reshape(B * X, H, L, D) |
| return _orig_sdpa(rs(q), rs(k), rs(v), *a, **kw).reshape(B, X, H, L, D) |
| return _orig_sdpa(q, k, v, *a, **kw) |
| F.scaled_dot_product_attention = _sdpa_4d |
| torch.nn.functional.scaled_dot_product_attention = _sdpa_4d |
|
|
| print(f"[2] exporting featuresBatch{list(feat_example.shape)} -> S, opset 17 (SDPA reshaped 5D->4D)") |
| try: |
| torch.onnx.export( |
| wrapper, (feat_example,), onnx_path, opset_version=17, |
| input_names=["featuresBatch"], output_names=["S"], |
| dynamic_axes={"featuresBatch": {1: "T"}, "S": {0: "T", 1: "T"}}) |
| print(f" export OK: {os.path.getsize(onnx_path)/1e6:.1f} MB (+ external .data)") |
| except Exception as e: |
| print(f" STOCK EXPORT FAILED: {type(e).__name__}: {e}\n (would apply eye-multiply patch)") |
| raise |
|
|
| |
| import onnx |
| m_full = onnx.load(onnx_path) |
| single_path = os.path.join(OUT, "transkun.onnx") |
| onnx.save(m_full, single_path, save_as_external_data=False) |
| os.remove(onnx_path) |
| if os.path.exists(onnx_path + ".data"): |
| os.remove(onnx_path + ".data") |
| print(f" consolidated -> transkun.onnx {os.path.getsize(single_path)/1e6:.1f} MB (single file)") |
|
|
| |
| print("[3] validating single-file ONNX vs PyTorch") |
| import onnxruntime as ort |
| sess = ort.InferenceSession(single_path, providers=["CPUExecutionProvider"]) |
| def check(feat, tag): |
| s_torch = wrapper(feat).numpy() |
| s_onnx = sess.run(None, {"featuresBatch": feat.numpy()})[0] |
| corr = np.corrcoef(s_torch.ravel(), s_onnx.ravel())[0, 1] |
| denom = np.abs(s_torch).max() + 1e-9 |
| relerr = np.abs(s_torch - s_onnx).max() / denom |
| print(f" {tag:14} shape={list(s_onnx.shape)} corr={corr:.6f} maxRelErr={relerr:.2e}") |
| return corr, relerr, s_onnx |
| check(feat_example, "random T=64") |
| check(torch.randn(1, 100, 229, 6), "random T=100") |
|
|
| |
| print("[4] extracting frozen buffers") |
| fe = model.framewiseFeatureExtractor |
| freq2mels = fe.freq2mels.numpy() |
| win = fe.spectrogramExtractor.win |
| wins = torch.cat([win.unsqueeze(0), fe.spectrogramExtractor.winGen.get().t()], dim=0).numpy() |
| save("freq2mels", freq2mels, "<f4") |
| save("windows", wins, "<f4") |
| save("symbols", np.array(model.targetMIDIPitch), "<i4") |
| params = { |
| "fs": int(model.fs), "windowSize": int(model.windowSize), "hopSize": int(model.hopSize), |
| "nMels": int(fe.outputDim), "nWindows": int(wins.shape[0]), "eps": float(fe.eps), |
| "fMin": 30.0, "fMax": 8000.0, "rfftBins": int(model.windowSize // 2 + 1), |
| "segmentSizeSeconds": 16.0, "segmentHopSeconds": 8.0, "nSymbols": len(model.targetMIDIPitch), |
| } |
| print(" params:", params) |
|
|
| |
| print("[5] ref3b (mel front end reference)") |
| fs, hop, wsz, eps, nmel = model.fs, model.hopSize, model.windowSize, fe.eps, fe.outputDim |
| |
| |
| import wave |
| wf = wave.open("/Users/lawls/Development/TuesdayCrowd/Projects/audio-claudio/fixtures/golden/two-bar.wav") |
| assert wf.getframerate() == fs and wf.getnchannels() == 1 and wf.getsampwidth() == 2 |
| nread = int(1.5 * fs) |
| audio = (np.frombuffer(wf.readframes(nread), dtype="<i2").astype(np.float32) / 32768.0) |
| wf.close() |
|
|
| def make_frame(x, hopSize, windowSize): |
| n = x.shape[-1] |
| nFrame = math.ceil(n / hopSize) + 1 |
| lPad = windowSize // 2 |
| rPad = (nFrame - 1) * hopSize + windowSize // 2 - n |
| xp = torch.nn.functional.pad(torch.tensor(x), (lPad, rPad)) |
| return xp.unfold(-1, windowSize, hopSize) |
|
|
| frames = make_frame(audio, hop, wsz).unsqueeze(0).unsqueeze(0) |
| mean = frames.mean(dim=[1, 2, 3], keepdim=True) |
| std = frames.std(dim=[1, 2, 3], keepdim=True) |
| framesN = (frames - mean) / (std + 1e-8) |
| features = fe(framesN).contiguous() |
| features = features.view(1, *features.shape[-3:]) |
| save("ref3b_audio", audio, "<f4") |
| save("ref3b_features", features.squeeze(0).numpy(), "<f4") |
| print(f" audio={len(audio)} samples -> features {list(features.shape)}") |
|
|
| |
| print("[6] ref3c (Viterbi decode reference)") |
| from transkun.CRF.NeuralSemiCRFInterval import viterbiBackward |
| S_real = wrapper(features).squeeze() |
| T = S_real.shape[0] |
| noise = torch.zeros(T - 1, S_real.shape[2]) |
| intervals_real = viterbiBackward(S_real, noise, None) |
| save("ref3c_S", S_real.numpy(), "<f4") |
| json.dump({str(k): v for k, v in enumerate(intervals_real)}, |
| open(os.path.join(OUT, "ref3c_intervals.json"), "w")) |
| nnotes = sum(len(v) for v in intervals_real) |
| print(f" real S {list(S_real.shape)} -> {nnotes} intervals across 90 tracks") |
|
|
| |
| |
| Tsyn, nSym = 6, 90 |
| Ssyn = torch.full((Tsyn, Tsyn, nSym), -5.0) |
| Ssyn[3, 1, 5] = 4.0 |
| Ssyn[5, 5, 5] = 2.0 |
| Ssyn[4, 0, 10] = 6.0 |
| Ssyn[2, 2, 20] = 3.0 |
| noise_syn = torch.zeros(Tsyn - 1, nSym) |
| intervals_syn = viterbiBackward(Ssyn, noise_syn, None) |
| save("ref3c_syn_S", Ssyn.numpy(), "<f4") |
| json.dump({str(k): v for k, v in enumerate(intervals_syn)}, |
| open(os.path.join(OUT, "ref3c_syn_intervals.json"), "w")) |
| print(f" synthetic S {list(Ssyn.shape)} -> t5={intervals_syn[5]} t10={intervals_syn[10]} t20={intervals_syn[20]}") |
|
|
| |
| |
| forced = [0] * nSym |
| forced[5] = 4 |
| intervals_forced = viterbiBackward(Ssyn, noise_syn, forced) |
| json.dump({"forcedStartPos": forced, "intervals": {str(k): v for k, v in enumerate(intervals_forced)}}, |
| open(os.path.join(OUT, "ref3c_forced_intervals.json"), "w")) |
| print(f" forced(t5->4) -> t5={intervals_forced[5]} (interval (1,3) skipped)") |
|
|
| |
| json.dump(manifest, open(os.path.join(OUT, "manifest.json"), "w"), indent=2) |
| json.dump(params, open(os.path.join(OUT, "params.json"), "w"), indent=2) |
| print(f"[7] wrote manifest.json + params.json to {OUT}") |
| print("DONE") |
|
|