lawls commited on
Commit
b445981
·
verified ·
1 Parent(s): 0675035

v2.0.0 — Transkun transformer-only ONNX export + decode spec (from audio-claudio)

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ freq2mels.f32 filter=lfs diff=lfs merge=lfs -text
37
+ ref3b_audio.f32 filter=lfs diff=lfs merge=lfs -text
38
+ ref3b_features.f32 filter=lfs diff=lfs merge=lfs -text
39
+ ref3c_S.f32 filter=lfs diff=lfs merge=lfs -text
DECODE_SPEC.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Transkun — self-contained ONNX export (v2 Stage 4)
2
+
3
+ A transformer-only ONNX export of **Transkun** (Yujia Yan's Neural Semi-CRF piano transcriber, 0.984
4
+ MAESTRO F1), plus the frozen front-end buffers and the decode spec needed to run it **in-process with no
5
+ Python/torch at runtime** behind `audio-claudio`'s `ITranscriber` port.
6
+
7
+ - **Upstream:** <https://github.com/Yujia-Yan/Skipping-The-Frame-Level> — Yujia Yan, Frank Cwitkowitz,
8
+ Zhiyao Duan. Package `transkun` 2.0.1, checkpoint `pretrained/2.0.pt`.
9
+ - **License:** MIT (© 2021 Yujia Yan) — see [`LICENSE.transkun`](LICENSE.transkun). audio-claudio is UNLICENSE;
10
+ MIT is compatible.
11
+ - This is a **transformer-only export + a decode spec**, not a drop-in `.onnx` transcriber: the mel front
12
+ end and the semi-CRF Viterbi decode are reimplemented in C# (Stages 4b/4c), because `torch.fft.rfft` and
13
+ the custom semi-CRF backtracking are not ONNX-exportable.
14
+
15
+ ## What the ONNX computes
16
+
17
+ `transkun.onnx` maps **`featuresBatch` → (`S`, `ctx`)**:
18
+
19
+ - input `featuresBatch` `[nBatch, T, 229, 6]` — log-mel features (229 mel bins × 6 windows), `T` dynamic.
20
+ - output `S` `[T, T, nBatch*90]` — the semi-CRF pairwise interval scores. `S[e, b, k]` scores a note on
21
+ track `k` spanning frames `b→e` (diagonal `e==b` = a single-frame note). The 90 tracks are
22
+ `symbols = [-64, -67, 21..108]`: index 0 = sustain pedal (CC64), 1 = soft pedal (CC67), 2–89 = MIDI
23
+ 21–108. (The `S_skip` "no-event" score is provably 0 and is hardcoded in C#.)
24
+ - output `ctx` `[90, T, 256]` (Stage 4e) — the backbone features, gathered at decoded interval endpoints to
25
+ drive the attribute heads. `S` is byte-for-byte the same as the S-only 4a export (corr 1.0).
26
+
27
+ **`transkun-heads.onnx`** (Stage 4e) maps the gathered interval features **`attr` `[N, 768]`**
28
+ (`[ctx_a, ctx_b, ctx_a·ctx_b]`) → **`velLogits` `[N, 128]`** (`velocityPredictor`; velocity = argmax) and
29
+ **`ofRaw` `[N, 4]`** (`refinedOFPredictor`: two sub-frame onset/offset value logits → a ContinuousBernoulli
30
+ mean in `[-0.5, 0.5]` frames, + two presence logits). This adds real velocity + sub-frame timing on top of
31
+ the frame-level decode — validated note-identical to the native CLI (velocity exact, onsets ~1 ms).
32
+
33
+ Two ops needed care in export (see `export_transkun.py`): the backbone's **5-D `scaled_dot_product_attention`**
34
+ is reshaped to 4-D (a mathematical identity — SDPA batches all but the last two dims) because the ONNX
35
+ exporter only supports 4-D SDPA. `diag_embed` exported cleanly on this stack (torch 2.13 / onnx 1.22 /
36
+ onnxruntime 1.27, opset 17), contrary to an earlier assumption. Validated **`corr = 1.000000`,
37
+ maxRelErr ≈ 5e-6** vs PyTorch on random and dynamic-`T` inputs.
38
+
39
+ ## Files
40
+
41
+ | File | What |
42
+ |---|---|
43
+ | `transkun.onnx` | the main export `featuresBatch → (S, ctx)` (opset 17, weights inlined, ~53 MB) |
44
+ | `transkun-heads.onnx` | the velocity + onset/offset attribute heads `attr → (velLogits, ofRaw)` (~3.4 MB) |
45
+ | `export_transkun_heads.py` | Stage-4e regeneration (main graph with `ctx` + the heads) |
46
+ | `freq2mels.f32` `[2049, 229]` | mel filterbank (`torchaudio.melscale_fbanks`, 30–8000 Hz) — Stage 4b |
47
+ | `windows.f32` `[6, 4096]` | analysis windows (row 0 Hann, rows 1–5 learned Gaussian) — Stage 4b |
48
+ | `symbols.i32` `[90]` | the track→symbol map `[-64, -67, 21..108]` |
49
+ | `params.json` | fs 44100, windowSize 4096, hopSize 1024, nMels 229, eps 1e-5, segment 16 s / hop 8 s |
50
+ | `ref3b_audio.f32`, `ref3b_features.f32` | Stage-4b TDD fixture: 1.5 s of `two-bar.wav` → its `featuresBatch` `[66,229,6]` |
51
+ | `ref3c_S.f32`, `ref3c_intervals.json` | Stage-4c TDD fixture: a real model `S` `[66,66,90]` → Viterbi intervals |
52
+ | `ref3c_syn_S.f32`, `ref3c_syn_intervals.json` | hand-built multi-track `S` `[6,6,90]` → known intervals |
53
+ | `ref3c_forced_intervals.json` | the same synthetic `S` with a `forcedStartPos` (Stage-4d stitching) |
54
+ | `manifest.json` | shape/dtype/file for every raw `.f32`/`.i32` array (raw little-endian) |
55
+ | `export_transkun.py` | the regeneration script (needs the transkun venv; see below) |
56
+
57
+ ## Regenerating
58
+
59
+ Not needed for the build (everything above is committed). To reproduce, in a venv with
60
+ `transkun==2.0.1`, `torch`, `onnx`, `onnxruntime`, `numpy`:
61
+
62
+ ```
63
+ python export_transkun.py <output-dir>
64
+ ```
65
+
66
+ It loads the model **from `2.0.conf`** (not class defaults — `baseSize=64`, `nHead=8`), wraps
67
+ `backbone + scorer`, exports, validates against PyTorch, extracts the buffers, and regenerates the ref
68
+ fixtures. Deterministic (seed 0).
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Yujia Yan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: onnxruntime
4
+ tags:
5
+ - audio
6
+ - music
7
+ - piano-transcription
8
+ - amt
9
+ - onnx
10
+ - transkun
11
+ - semi-crf
12
+ pipeline_tag: audio-to-audio
13
+ ---
14
+
15
+ # Transkun — transformer-only ONNX export + decode spec
16
+
17
+ A **self-contained ONNX export of [Transkun](https://github.com/Yujia-Yan/Skipping-The-Frame-Level)**
18
+ (Yujia Yan's Neural Semi-CRF piano transcriber, 0.984 MAESTRO note F1) that runs the full model **in-process
19
+ with no Python/PyTorch at runtime**. This is **not** a drop-in `.onnx` transcriber: `torch.fft.rfft` and the
20
+ custom semi-CRF backtracking are not ONNX-exportable, so the mel front end and the Viterbi decode are provided
21
+ as a **documented decode spec** + a **reference decoder**.
22
+
23
+ > **Attribution.** The model, weights and architecture are the work of **Yujia Yan, Frank Cwitkowitz and
24
+ > Zhiyao Duan** (*"Skipping the Frame-Level: Event-Based Piano Transcription with Neural Semi-CRFs"*, NeurIPS
25
+ > 2021). This is an independent export + decode spec, not affiliated with or endorsed by the authors. Upstream:
26
+ > <https://github.com/Yujia-Yan/Skipping-The-Frame-Level>. License: **MIT** (© 2021 Yujia Yan).
27
+
28
+ ## What's in the package
29
+
30
+ | File | Role |
31
+ |---|---|
32
+ | `transkun.onnx` (~53 MB, opset 17) | `featuresBatch [1,T,229,6] → (S [T,T,90], ctx [90,T,256])` — the transformer + semi-CRF scorer + backbone features |
33
+ | `transkun-heads.onnx` (~3.4 MB) | `attr [N,768] → (velLogits [N,128], ofRaw [N,4])` — velocity + sub-frame onset/offset heads |
34
+ | `freq2mels.f32 [2049,229]`, `windows.f32 [6,4096]`, `symbols.i32 [90]`, `params.json` | frozen front-end constants |
35
+ | `LICENSE.transkun` | upstream MIT license |
36
+ | `export_transkun.py`, `export_transkun_heads.py` | regeneration scripts (need the `transkun` PyTorch package) |
37
+
38
+ The **decode spec** (`DECODE_SPEC.md`) documents the mel front end, the `S` layout, the 90-track
39
+ symbol map (`[-64, -67, 21..108]` = sustain/soft pedal + MIDI 21–108), the semi-CRF `viterbiBackward`, the
40
+ 16 s/8 s segment stitching, and the attribute heads (velocity = argmax; `ofValue` = ContinuousBernoulli mean).
41
+
42
+ ## Reference decoder + validation
43
+
44
+ The reference decoder is the C# implementation in **[audio-claudio](https://github.com/TuesdayCrowd/audio-claudio)**
45
+ (mel front end, `SemiCrfViterbi`, `TranskunTranscriber`). It is validated **note-identical to the native
46
+ `transkun` CLI (PyTorch)**: on the test clips it reaches **100% note-level F1 at ±25 ms** with **exact
47
+ velocity** on every note — the export + decode spec reproduce the reference implementation, not merely
48
+ approximate it.
49
+
50
+ ## Pipeline (how to run)
51
+
52
+ ```
53
+ audio (mono, 44.1 kHz)
54
+ → mel front end (framing 4096/1024, 6 windows, rfft ortho, freq2mels, log-norm) → featuresBatch
55
+ → transkun.onnx → (S, ctx)
56
+ → semi-CRF viterbiBackward(S) → per-track note intervals, over 16 s/8 s stitched segments
57
+ → gather ctx at interval endpoints → transkun-heads.onnx → velocity + sub-frame onset/offset
58
+ → notes (+ sustain/soft pedal from tracks 0/1)
59
+ ```
60
+
61
+ See the repo's decode spec and `TranskunTranscriber` for the exact arithmetic (segment padding, `forcedStartPos`
62
+ carry, merge, `resolveOverlapping`).
export_transkun.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """v2 Stage 4a — re-derive the Transkun ONNX export + frozen buffers + TDD fixtures.
3
+
4
+ Boundary: featuresBatch -> S (backbone + inner-product CRF scorer). The mel front end (audio ->
5
+ featuresBatch) and the Viterbi decode (S -> intervals) are ported to C# (4b/4c); this script also emits
6
+ their reference fixtures. All outputs are written raw little-endian + a manifest.json for trivial C# reads.
7
+ """
8
+ import json, math, os, sys
9
+ import numpy as np
10
+ import torch
11
+
12
+ 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"
13
+ OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + "/artifacts"
14
+ os.makedirs(OUT, exist_ok=True)
15
+ torch.manual_seed(0)
16
+ np.random.seed(0)
17
+
18
+ manifest = {}
19
+ def save(name, arr, dtype):
20
+ a = np.ascontiguousarray(arr).astype(dtype)
21
+ fn = name + (".f32" if dtype == "<f4" else ".i32")
22
+ a.tofile(os.path.join(OUT, fn))
23
+ manifest[name] = {"file": fn, "shape": list(a.shape), "dtype": "f32" if dtype == "<f4" else "i32"}
24
+ print(f" saved {name:22} shape={list(a.shape)} dtype={manifest[name]['dtype']}")
25
+
26
+ # ---------------------------------------------------------------- 1. load model from the .conf ----------
27
+ print("[1] loading model from 2.0.conf + 2.0.pt")
28
+ import moduleconf
29
+ conf_path = os.path.join(TK_DIR, "pretrained", "2.0.conf")
30
+ pt_path = os.path.join(TK_DIR, "pretrained", "2.0.pt")
31
+ confManager = moduleconf.parseFromFile(conf_path)
32
+ TransKun = confManager["Model"].module.TransKun
33
+ conf = confManager["Model"].config
34
+ checkpoint = torch.load(pt_path, map_location="cpu")
35
+ model = TransKun(conf=conf)
36
+ key = "best_state_dict" if "best_state_dict" in checkpoint else "state_dict"
37
+ missing = model.load_state_dict(checkpoint[key], strict=False)
38
+ model.eval(); torch.set_grad_enabled(False)
39
+ print(f" loaded ({key}); missing={len(missing.missing_keys)} unexpected={len(missing.unexpected_keys)}")
40
+ print(f" fs={model.fs} windowSize={model.windowSize} hopSize={model.hopSize} "
41
+ f"nSym={len(model.targetMIDIPitch)} params={sum(p.numel() for p in model.parameters())/1e6:.2f}M")
42
+
43
+ # ---------------------------------------------------------------- 2. export wrapper: featuresBatch -> S -
44
+ class ExportWrapper(torch.nn.Module):
45
+ def __init__(self, m):
46
+ super().__init__()
47
+ self.backbone = m.backbone
48
+ self.scorer = m.scorer
49
+ self.register_buffer("outputIndices", torch.tensor(m.targetMIDIPitch))
50
+ def forward(self, featuresBatch): # [nBatch, T, 229, 6]
51
+ ctx = self.backbone(featuresBatch, outputIndices=self.outputIndices)
52
+ S_batch, _ = self.scorer(ctx) # S_batch: [T, T, nBatch, 90]; skip is provably 0
53
+ return S_batch.flatten(-2, -1) # [T, T, nBatch*90]
54
+
55
+ wrapper = ExportWrapper(model).eval()
56
+ onnx_path = os.path.join(OUT, "model.onnx")
57
+ T0 = 64
58
+ feat_example = torch.randn(1, T0, 229, 6)
59
+
60
+ # The backbone's axial attention calls SDPA on 5-D q/k/v ([B, T', H, L, D]); the ONNX exporter's SDPA only
61
+ # supports 4-D. SDPA treats every dim before the last two as batch, so collapsing the two leading dims to
62
+ # one 4-D batch is a mathematical identity (validated by corr below). Patch for the duration of the export.
63
+ import torch.nn.functional as F
64
+ _orig_sdpa = F.scaled_dot_product_attention
65
+ def _sdpa_4d(q, k, v, *a, **kw):
66
+ if q.ndim == 5:
67
+ B, X, H, L, D = q.shape
68
+ rs = lambda t: t.reshape(B * X, H, L, D)
69
+ return _orig_sdpa(rs(q), rs(k), rs(v), *a, **kw).reshape(B, X, H, L, D)
70
+ return _orig_sdpa(q, k, v, *a, **kw)
71
+ F.scaled_dot_product_attention = _sdpa_4d
72
+ torch.nn.functional.scaled_dot_product_attention = _sdpa_4d
73
+
74
+ print(f"[2] exporting featuresBatch{list(feat_example.shape)} -> S, opset 17 (SDPA reshaped 5D->4D)")
75
+ try:
76
+ torch.onnx.export(
77
+ wrapper, (feat_example,), onnx_path, opset_version=17,
78
+ input_names=["featuresBatch"], output_names=["S"],
79
+ dynamic_axes={"featuresBatch": {1: "T"}, "S": {0: "T", 1: "T"}})
80
+ print(f" export OK: {os.path.getsize(onnx_path)/1e6:.1f} MB (+ external .data)")
81
+ except Exception as e:
82
+ print(f" STOCK EXPORT FAILED: {type(e).__name__}: {e}\n (would apply eye-multiply patch)")
83
+ raise
84
+
85
+ # Consolidate the external-data weights into ONE self-contained .onnx for committing.
86
+ import onnx
87
+ m_full = onnx.load(onnx_path) # pulls in model.onnx.data
88
+ single_path = os.path.join(OUT, "transkun.onnx")
89
+ onnx.save(m_full, single_path, save_as_external_data=False)
90
+ os.remove(onnx_path)
91
+ if os.path.exists(onnx_path + ".data"):
92
+ os.remove(onnx_path + ".data")
93
+ print(f" consolidated -> transkun.onnx {os.path.getsize(single_path)/1e6:.1f} MB (single file)")
94
+
95
+ # ---------------------------------------------------------------- 3. validate ONNX == PyTorch -----------
96
+ print("[3] validating single-file ONNX vs PyTorch")
97
+ import onnxruntime as ort
98
+ sess = ort.InferenceSession(single_path, providers=["CPUExecutionProvider"])
99
+ def check(feat, tag):
100
+ s_torch = wrapper(feat).numpy()
101
+ s_onnx = sess.run(None, {"featuresBatch": feat.numpy()})[0]
102
+ corr = np.corrcoef(s_torch.ravel(), s_onnx.ravel())[0, 1]
103
+ denom = np.abs(s_torch).max() + 1e-9
104
+ relerr = np.abs(s_torch - s_onnx).max() / denom
105
+ print(f" {tag:14} shape={list(s_onnx.shape)} corr={corr:.6f} maxRelErr={relerr:.2e}")
106
+ return corr, relerr, s_onnx
107
+ check(feat_example, "random T=64")
108
+ check(torch.randn(1, 100, 229, 6), "random T=100") # dynamic-T sanity
109
+
110
+ # ---------------------------------------------------------------- 4. frozen buffers ---------------------
111
+ print("[4] extracting frozen buffers")
112
+ fe = model.framewiseFeatureExtractor
113
+ freq2mels = fe.freq2mels.numpy() # [2049, 229]
114
+ win = fe.spectrogramExtractor.win # [4096] Hann
115
+ wins = torch.cat([win.unsqueeze(0), fe.spectrogramExtractor.winGen.get().t()], dim=0).numpy() # [6, 4096]
116
+ save("freq2mels", freq2mels, "<f4")
117
+ save("windows", wins, "<f4")
118
+ save("symbols", np.array(model.targetMIDIPitch), "<i4")
119
+ params = {
120
+ "fs": int(model.fs), "windowSize": int(model.windowSize), "hopSize": int(model.hopSize),
121
+ "nMels": int(fe.outputDim), "nWindows": int(wins.shape[0]), "eps": float(fe.eps),
122
+ "fMin": 30.0, "fMax": 8000.0, "rfftBins": int(model.windowSize // 2 + 1),
123
+ "segmentSizeSeconds": 16.0, "segmentHopSeconds": 8.0, "nSymbols": len(model.targetMIDIPitch),
124
+ }
125
+ print(" params:", params)
126
+
127
+ # ---------------------------------------------------------------- 5. ref3b: audio -> featuresBatch ------
128
+ print("[5] ref3b (mel front end reference)")
129
+ fs, hop, wsz, eps, nmel = model.fs, model.hopSize, model.windowSize, fe.eps, fe.outputDim
130
+ # The first 1.5 s of the committed two-bar MeltySynth piano render (44100 Hz mono int16) — a real piano
131
+ # signal (attack + timbre) that fires the MAESTRO-trained model, fully reproducible from a committed WAV.
132
+ import wave
133
+ wf = wave.open("/Users/lawls/Development/TuesdayCrowd/Projects/audio-claudio/fixtures/golden/two-bar.wav")
134
+ assert wf.getframerate() == fs and wf.getnchannels() == 1 and wf.getsampwidth() == 2
135
+ nread = int(1.5 * fs)
136
+ audio = (np.frombuffer(wf.readframes(nread), dtype="<i2").astype(np.float32) / 32768.0)
137
+ wf.close()
138
+
139
+ def make_frame(x, hopSize, windowSize): # mirrors Util.makeFrame (leftPaddingHalfFrame=True)
140
+ n = x.shape[-1]
141
+ nFrame = math.ceil(n / hopSize) + 1
142
+ lPad = windowSize // 2
143
+ rPad = (nFrame - 1) * hopSize + windowSize // 2 - n
144
+ xp = torch.nn.functional.pad(torch.tensor(x), (lPad, rPad))
145
+ return xp.unfold(-1, windowSize, hopSize) # [nFrame, windowSize]
146
+
147
+ frames = make_frame(audio, hop, wsz).unsqueeze(0).unsqueeze(0) # [1,1,nFrame,windowSize] (nBatch,nChan,..)
148
+ mean = frames.mean(dim=[1, 2, 3], keepdim=True)
149
+ std = frames.std(dim=[1, 2, 3], keepdim=True)
150
+ framesN = (frames - mean) / (std + 1e-8)
151
+ features = fe(framesN).contiguous() # [1,1,nFrame,229,6]
152
+ features = features.view(1, *features.shape[-3:]) # [1,nFrame,229,6]
153
+ save("ref3b_audio", audio, "<f4")
154
+ save("ref3b_features", features.squeeze(0).numpy(), "<f4") # [nFrame,229,6]
155
+ print(f" audio={len(audio)} samples -> features {list(features.shape)}")
156
+
157
+ # ---------------------------------------------------------------- 6. ref3c: S -> intervals --------------
158
+ print("[6] ref3c (Viterbi decode reference)")
159
+ from transkun.CRF.NeuralSemiCRFInterval import viterbiBackward
160
+ S_real = wrapper(features).squeeze() # [nFrame,nFrame,90] (nBatch=1 -> flatten gives 90)
161
+ T = S_real.shape[0]
162
+ noise = torch.zeros(T - 1, S_real.shape[2])
163
+ intervals_real = viterbiBackward(S_real, noise, None) # per-track list of (begin,end)
164
+ save("ref3c_S", S_real.numpy(), "<f4")
165
+ json.dump({str(k): v for k, v in enumerate(intervals_real)},
166
+ open(os.path.join(OUT, "ref3c_intervals.json"), "w"))
167
+ nnotes = sum(len(v) for v in intervals_real)
168
+ print(f" real S {list(S_real.shape)} -> {nnotes} intervals across 90 tracks")
169
+
170
+ # Hand-checkable synthetic S, multi-track (score[end, begin, track]): track 5 = interval (1,3) + singleton
171
+ # (5,5); track 10 = interval (0,4); track 20 = singleton (2,2). Decoded with default forcedStartPos.
172
+ Tsyn, nSym = 6, 90
173
+ Ssyn = torch.full((Tsyn, Tsyn, nSym), -5.0)
174
+ Ssyn[3, 1, 5] = 4.0
175
+ Ssyn[5, 5, 5] = 2.0
176
+ Ssyn[4, 0, 10] = 6.0
177
+ Ssyn[2, 2, 20] = 3.0
178
+ noise_syn = torch.zeros(Tsyn - 1, nSym)
179
+ intervals_syn = viterbiBackward(Ssyn, noise_syn, None)
180
+ save("ref3c_syn_S", Ssyn.numpy(), "<f4")
181
+ json.dump({str(k): v for k, v in enumerate(intervals_syn)},
182
+ open(os.path.join(OUT, "ref3c_syn_intervals.json"), "w"))
183
+ print(f" synthetic S {list(Ssyn.shape)} -> t5={intervals_syn[5]} t10={intervals_syn[10]} t20={intervals_syn[20]}")
184
+
185
+ # A forcedStartPos case (used by 4d segment stitching): same S, but track 5 forced to start at frame 4,
186
+ # so its interval (1,3) is skipped and only the singleton (5,5) survives.
187
+ forced = [0] * nSym
188
+ forced[5] = 4
189
+ intervals_forced = viterbiBackward(Ssyn, noise_syn, forced)
190
+ json.dump({"forcedStartPos": forced, "intervals": {str(k): v for k, v in enumerate(intervals_forced)}},
191
+ open(os.path.join(OUT, "ref3c_forced_intervals.json"), "w"))
192
+ print(f" forced(t5->4) -> t5={intervals_forced[5]} (interval (1,3) skipped)")
193
+
194
+ # ---------------------------------------------------------------- 7. write manifest + params ------------
195
+ json.dump(manifest, open(os.path.join(OUT, "manifest.json"), "w"), indent=2)
196
+ json.dump(params, open(os.path.join(OUT, "params.json"), "w"), indent=2)
197
+ print(f"[7] wrote manifest.json + params.json to {OUT}")
198
+ print("DONE")
export_transkun_heads.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """v2 Stage 4e — re-export the main graph to ALSO output ctx, and export the two attribute heads
3
+ (velocityPredictor, refinedOFPredictor) so the C# engine can add real velocity + sub-frame onset/offset
4
+ refinement. Overwrites transkun.onnx (S is byte-for-byte the same; ctx is an added output) and adds
5
+ transkun-heads.onnx. Deterministic (seed 0)."""
6
+ import json, os, sys
7
+ import numpy as np
8
+ import torch
9
+
10
+ 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"
11
+ OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + "/artifacts4e"
12
+ os.makedirs(OUT, exist_ok=True)
13
+ torch.manual_seed(0); np.random.seed(0)
14
+
15
+ print("[1] load model from 2.0.conf")
16
+ import moduleconf
17
+ cm = moduleconf.parseFromFile(os.path.join(TK_DIR, "pretrained", "2.0.conf"))
18
+ model = cm["Model"].module.TransKun(conf=cm["Model"].config)
19
+ ckpt = torch.load(os.path.join(TK_DIR, "pretrained", "2.0.pt"), map_location="cpu")
20
+ model.load_state_dict(ckpt["best_state_dict" if "best_state_dict" in ckpt else "state_dict"], strict=False)
21
+ model.eval(); torch.set_grad_enabled(False)
22
+
23
+ # SDPA 5D->4D (identity) so the backbone exports; same as the 4a export.
24
+ import torch.nn.functional as F
25
+ _sdpa = F.scaled_dot_product_attention
26
+ def _sdpa4d(q, k, v, *a, **kw):
27
+ if q.ndim == 5:
28
+ B, X, H, L, D = q.shape
29
+ r = lambda t: t.reshape(B * X, H, L, D)
30
+ return _sdpa(r(q), r(k), r(v), *a, **kw).reshape(B, X, H, L, D)
31
+ return _sdpa(q, k, v, *a, **kw)
32
+ F.scaled_dot_product_attention = _sdpa4d
33
+ torch.nn.functional.scaled_dot_product_attention = _sdpa4d
34
+
35
+ import onnx, onnxruntime as ort
36
+ def consolidate(split_path, single_path):
37
+ m = onnx.load(split_path)
38
+ onnx.save(m, single_path, save_as_external_data=False)
39
+ os.remove(split_path)
40
+ if os.path.exists(split_path + ".data"):
41
+ os.remove(split_path + ".data")
42
+
43
+ # ---- main graph: featuresBatch -> (S, ctx) --------------------------------------------------------------
44
+ print("[2] export main graph featuresBatch -> (S, ctx)")
45
+ class MainWrapper(torch.nn.Module):
46
+ def __init__(self, m):
47
+ super().__init__()
48
+ self.backbone = m.backbone
49
+ self.scorer = m.scorer
50
+ self.register_buffer("outputIndices", torch.tensor(m.targetMIDIPitch))
51
+ def forward(self, featuresBatch):
52
+ ctx = self.backbone(featuresBatch, outputIndices=self.outputIndices) # [1,90,T,256]
53
+ S_batch, _ = self.scorer(ctx)
54
+ S = S_batch.flatten(-2, -1) # [T,T,90]
55
+ return S, ctx.squeeze(0) # ctx -> [90,T,256]
56
+
57
+ main = MainWrapper(model).eval()
58
+ feat = torch.randn(1, 64, 229, 6)
59
+ split = os.path.join(OUT, "_main.onnx")
60
+ torch.onnx.export(main, (feat,), split, opset_version=17,
61
+ input_names=["featuresBatch"], output_names=["S", "ctx"],
62
+ dynamic_axes={"featuresBatch": {1: "T"}, "S": {0: "T", 1: "T"}, "ctx": {1: "T"}})
63
+ main_path = os.path.join(OUT, "transkun.onnx")
64
+ consolidate(split, main_path)
65
+ print(f" transkun.onnx {os.path.getsize(main_path)/1e6:.1f} MB")
66
+
67
+ sess = ort.InferenceSession(main_path, providers=["CPUExecutionProvider"])
68
+ S_t, ctx_t = main(feat)
69
+ S_o, ctx_o = sess.run(None, {"featuresBatch": feat.numpy()})
70
+ print(f" S corr={np.corrcoef(S_t.numpy().ravel(), S_o.ravel())[0,1]:.6f} shape={list(S_o.shape)}")
71
+ print(f" ctx corr={np.corrcoef(ctx_t.numpy().ravel(), ctx_o.ravel())[0,1]:.6f} shape={list(ctx_o.shape)}")
72
+
73
+ # ---- heads: attr[N,768] -> (velLogits[N,128], ofRaw[N,4]) -----------------------------------------------
74
+ print("[3] export heads attr -> (velLogits, ofRaw)")
75
+ class HeadWrapper(torch.nn.Module):
76
+ def __init__(self, m):
77
+ super().__init__()
78
+ self.vel = m.velocityPredictor
79
+ self.of = m.refinedOFPredictor
80
+ def forward(self, attr):
81
+ return self.vel(attr), self.of(attr)
82
+
83
+ heads = HeadWrapper(model).eval()
84
+ attr = torch.randn(7, 768)
85
+ hsplit = os.path.join(OUT, "_heads.onnx")
86
+ torch.onnx.export(heads, (attr,), hsplit, opset_version=17,
87
+ input_names=["attr"], output_names=["velLogits", "ofRaw"],
88
+ dynamic_axes={"attr": {0: "N"}, "velLogits": {0: "N"}, "ofRaw": {0: "N"}})
89
+ heads_path = os.path.join(OUT, "transkun-heads.onnx")
90
+ consolidate(hsplit, heads_path)
91
+ print(f" transkun-heads.onnx {os.path.getsize(heads_path)/1e3:.0f} KB")
92
+
93
+ hsess = ort.InferenceSession(heads_path, providers=["CPUExecutionProvider"])
94
+ v_t, o_t = heads(attr)
95
+ v_o, o_o = hsess.run(None, {"attr": attr.numpy()})
96
+ print(f" velLogits corr={np.corrcoef(v_t.numpy().ravel(), v_o.ravel())[0,1]:.6f} shape={list(v_o.shape)}")
97
+ print(f" ofRaw corr={np.corrcoef(o_t.numpy().ravel(), o_o.ravel())[0,1]:.6f} shape={list(o_o.shape)}")
98
+
99
+ print(f"[4] wrote {main_path} + {heads_path}")
100
+ print("DONE")
freq2mels.f32 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:449f2a5269cde41f6705cfde7157240ce7c8930f20b50909d57e633395bbf0f4
3
+ size 1876884
manifest.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "freq2mels": {
3
+ "file": "freq2mels.f32",
4
+ "shape": [
5
+ 2049,
6
+ 229
7
+ ],
8
+ "dtype": "f32"
9
+ },
10
+ "windows": {
11
+ "file": "windows.f32",
12
+ "shape": [
13
+ 6,
14
+ 4096
15
+ ],
16
+ "dtype": "f32"
17
+ },
18
+ "symbols": {
19
+ "file": "symbols.i32",
20
+ "shape": [
21
+ 90
22
+ ],
23
+ "dtype": "i32"
24
+ },
25
+ "ref3b_audio": {
26
+ "file": "ref3b_audio.f32",
27
+ "shape": [
28
+ 66150
29
+ ],
30
+ "dtype": "f32"
31
+ },
32
+ "ref3b_features": {
33
+ "file": "ref3b_features.f32",
34
+ "shape": [
35
+ 66,
36
+ 229,
37
+ 6
38
+ ],
39
+ "dtype": "f32"
40
+ },
41
+ "ref3c_S": {
42
+ "file": "ref3c_S.f32",
43
+ "shape": [
44
+ 66,
45
+ 66,
46
+ 90
47
+ ],
48
+ "dtype": "f32"
49
+ },
50
+ "ref3c_syn_S": {
51
+ "file": "ref3c_syn_S.f32",
52
+ "shape": [
53
+ 6,
54
+ 6,
55
+ 90
56
+ ],
57
+ "dtype": "f32"
58
+ }
59
+ }
params.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fs": 44100,
3
+ "windowSize": 4096,
4
+ "hopSize": 1024,
5
+ "nMels": 229,
6
+ "nWindows": 6,
7
+ "eps": 1e-05,
8
+ "fMin": 30.0,
9
+ "fMax": 8000.0,
10
+ "rfftBins": 2049,
11
+ "segmentSizeSeconds": 16.0,
12
+ "segmentHopSeconds": 8.0,
13
+ "nSymbols": 90
14
+ }
ref3b_audio.f32 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:805fabae442dc3262a3d28fe670f541ad157d6c72d3a9ce5e9403661eca6d587
3
+ size 264600
ref3b_features.f32 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca43f2677f53ba60d175e0c401b163be66ef70932e303c5d03e7e444c7c99620
3
+ size 362736
ref3c_S.f32 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2cbcdcb7d1567dbf29c48d2f9cef420a1de61d8d819b4ebfc921639bc9c9a0e
3
+ size 1568160
ref3c_forced_intervals.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"forcedStartPos": [0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "intervals": {"0": [], "1": [], "2": [], "3": [], "4": [], "5": [[5, 5]], "6": [], "7": [], "8": [], "9": [], "10": [[0, 4]], "11": [], "12": [], "13": [], "14": [], "15": [], "16": [], "17": [], "18": [], "19": [], "20": [[2, 2]], "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": []}}
ref3c_intervals.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"0": [], "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": [[43, 63]], "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": []}
ref3c_syn_S.f32 ADDED
Binary file (13 kB). View file
 
ref3c_syn_intervals.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"0": [], "1": [], "2": [], "3": [], "4": [], "5": [[1, 3], [5, 5]], "6": [], "7": [], "8": [], "9": [], "10": [[0, 4]], "11": [], "12": [], "13": [], "14": [], "15": [], "16": [], "17": [], "18": [], "19": [], "20": [[2, 2]], "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": []}
symbols.i32 ADDED
Binary file (360 Bytes). View file
 
transkun-heads.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f609f53fbb4df0a2165f941aff1d5a1c1063ba40aaa699365448dca5d9f3544d
3
+ size 3422740
transkun.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d6960ecbce5c3d51051ebc1ebdeec4d3018ea6cfd360fb1ae96359aa7a075a3
3
+ size 53391226
windows.f32 ADDED
Binary file (98.3 kB). View file