vedmistry commited on
Commit
bb92a2a
·
0 Parent(s):

initial BACHI deployment

Browse files
Files changed (11) hide show
  1. Dockerfile +20 -0
  2. README.md +30 -0
  3. app.py +236 -0
  4. dataset.py +382 -0
  5. models/HT.py +386 -0
  6. models/__init__.py +0 -0
  7. models/components.py +339 -0
  8. models/model.py +686 -0
  9. models/variants.py +654 -0
  10. packages.txt +1 -0
  11. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY packages.txt /app/packages.txt
6
+ RUN apt-get update && \
7
+ apt-get install -y --no-install-recommends git gcc libc6-dev && \
8
+ xargs apt-get install -y --no-install-recommends < /app/packages.txt && \
9
+ rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt /app/requirements.txt
12
+ RUN pip install --no-cache-dir gradio==5.28.0
13
+ RUN pip install --no-cache-dir -r /app/requirements.txt
14
+
15
+ COPY . /app
16
+
17
+ ENV PORT=7860
18
+ EXPOSE 7860
19
+
20
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Bachi
3
+ emoji: 🎵
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # BACHI — HARP Endpoint
12
+
13
+ Deploys [BACHI](https://github.com/AndyWeasley2004/BACHI_Chord_Recognition) as a [PyHARP](https://github.com/TEAMuP-dev/pyharp) endpoint.
14
+
15
+ ## What it does
16
+
17
+ Automatic chord recognition from symbolic music scores. Outputs beat-aligned chord labels (root, quality, bass).
18
+
19
+ ## Inputs
20
+
21
+ - MIDI or MusicXML file (.mid, .midi, .musicxml, .mxl, .xml)
22
+ - Model type: Classical or Pop
23
+
24
+ ## Note
25
+
26
+ Model is trained on piano data only and supports MIDI pitch range 21-108.
27
+
28
+ ## License
29
+
30
+ MIT
app.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import math
4
+ import tempfile
5
+ import pickle
6
+ from pathlib import Path
7
+
8
+ import torch
9
+ import numpy as np
10
+ import yaml
11
+ import gradio as gr
12
+ from pyharp import ModelCard, build_endpoint
13
+ from huggingface_hub import hf_hub_download
14
+
15
+ sys.path.insert(0, str(Path(__file__).parent))
16
+ from dataset import load_vocabs
17
+ from models.variants import build_model
18
+
19
+ os.environ["TOKENIZERS_PARALLELISM"] = "true"
20
+
21
+ REPO_ID = "Itsuki-music/BACHI_Chord_Recognition"
22
+ CHECKPOINT_NAMES = {
23
+ "Classical": "classical_film_kdec",
24
+ "Pop": "pop909_film_kdec",
25
+ }
26
+ CHECKPOINT_FILES = ["best_model.pt", "config.yaml", "vocab.pkl"]
27
+
28
+ loaded_models = {}
29
+
30
+ def get_model(model_type: str):
31
+ if model_type not in loaded_models:
32
+ folder = CHECKPOINT_NAMES[model_type]
33
+ ckpt_dir = Path(__file__).parent / "ckpts" / folder
34
+ ckpt_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ for fname in CHECKPOINT_FILES:
37
+ dest = ckpt_dir / fname
38
+ if not dest.exists():
39
+ print(f"Downloading {folder}/{fname}...", flush=True)
40
+ hf_hub_download(
41
+ repo_id=REPO_ID,
42
+ filename=f"{folder}/{fname}",
43
+ repo_type="dataset",
44
+ local_dir=Path(__file__).parent / "ckpts",
45
+ )
46
+ print(f"Downloaded {fname}.", flush=True)
47
+
48
+ with open(ckpt_dir / "config.yaml", "r") as f:
49
+ config = yaml.safe_load(f)
50
+
51
+ vocabs = load_vocabs(str(ckpt_dir / "vocab.pkl"))
52
+ use_key = (
53
+ bool(config.get("use_key", False))
54
+ or bool(config["training"].get("use_key", False))
55
+ or bool(config["model"].get("use_key", False))
56
+ )
57
+ experiment = config["experiment"]
58
+
59
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
60
+ print(f"Loading {model_type} model...", flush=True)
61
+ model = build_model(experiment, config["model"], vocabs, use_key=use_key).to(device)
62
+ model.load_state_dict(
63
+ torch.load(ckpt_dir / "best_model.pt", map_location=device, weights_only=True)
64
+ )
65
+ model.eval()
66
+ print(f"{model_type} model loaded.", flush=True)
67
+
68
+ loaded_models[model_type] = (model, config, vocabs, use_key, device)
69
+
70
+ return loaded_models[model_type]
71
+
72
+
73
+ def extract_pianoroll(score_path: Path, resolution: int = 12):
74
+ import miditoolkit
75
+ from music21 import converter, note as m21_note, chord as m21_chord
76
+
77
+ notes_data = []
78
+ suffix = score_path.suffix.lower()
79
+
80
+ if suffix in {".mid", ".midi"}:
81
+ midi = miditoolkit.MidiFile(str(score_path))
82
+ tpb = midi.ticks_per_beat or 480
83
+ for inst in midi.instruments:
84
+ if inst.is_drum:
85
+ continue
86
+ for n in inst.notes:
87
+ notes_data.append((n.pitch, n.start / tpb, n.end / tpb))
88
+ else:
89
+ sc = converter.parse(str(score_path))
90
+ parts = list(sc.parts) if sc.hasPartLikeStreams() else [sc]
91
+ for part in parts:
92
+ inst = part.getInstrument()
93
+ if inst:
94
+ classes = inst.classes if hasattr(inst, "classes") else []
95
+ if "Percussion" in classes or "Unpitched" in classes:
96
+ continue
97
+ for el in part.flat.notes:
98
+ dur = float(el.quarterLength)
99
+ start = float(el.offset)
100
+ if isinstance(el, m21_note.Note):
101
+ notes_data.append((el.pitch.midi, start, start + dur))
102
+ elif isinstance(el, m21_chord.Chord):
103
+ for p in el.pitches:
104
+ notes_data.append((p.midi, start, start + dur))
105
+
106
+ if not notes_data:
107
+ return None
108
+
109
+ notes_data.sort(key=lambda x: x[1])
110
+ last_end = max(nd[2] for nd in notes_data)
111
+ total_frames = math.ceil(last_end * resolution)
112
+ pianoroll = np.zeros((88, total_frames), dtype=np.int8)
113
+ for midi_pitch, start_b, end_b in notes_data:
114
+ row = midi_pitch - 21
115
+ if not (0 <= row < 88):
116
+ continue
117
+ s_f = max(0, math.floor(start_b * resolution))
118
+ e_f = min(total_frames, math.ceil(end_b * resolution))
119
+ pianoroll[row, s_f:e_f] = 1
120
+
121
+ return pianoroll
122
+
123
+
124
+ def predict_piece(pianoroll, model, config, vocabs, use_key, device):
125
+ beat_resolution = config["model"]["beat_resolution"]
126
+ label_resolution = config["model"]["label_resolution"]
127
+ segment_len = config["model"]["n_beats"] * beat_resolution
128
+ pr_to_label_ratio = beat_resolution // label_resolution
129
+ comps_eval = ["root", "quality", "bass"] + (["key"] if use_key else [])
130
+
131
+ n_frames = pianoroll.shape[0]
132
+ segments, masks = [], []
133
+ for i in range(0, n_frames, segment_len):
134
+ seg = pianoroll[i : i + segment_len]
135
+ orig_len = seg.shape[0]
136
+ if orig_len < segment_len:
137
+ seg = torch.cat([seg, torch.zeros(segment_len - orig_len, seg.shape[1])], dim=0)
138
+ mask = torch.ones(segment_len, dtype=torch.bool)
139
+ if orig_len < segment_len:
140
+ mask[orig_len:] = False
141
+ segments.append(seg)
142
+ masks.append(mask)
143
+
144
+ piece_preds = {k: [] for k in comps_eval + ["boundary"]}
145
+ for i in range(0, len(segments), 16):
146
+ batch_segs = torch.stack(segments[i : i + 16]).to(device)
147
+ batch_masks = torch.stack(masks[i : i + 16]).to(device)
148
+ with torch.no_grad():
149
+ out = model.forward_infer(batch_segs, src_key_padding_mask=~batch_masks)
150
+ for k in comps_eval + ["boundary"]:
151
+ if k in out:
152
+ piece_preds[k].append(out[k].detach().cpu())
153
+
154
+ n_target = math.ceil(n_frames / pr_to_label_ratio)
155
+ piece_pred_ids = {}
156
+ for k, parts in piece_preds.items():
157
+ if not parts:
158
+ continue
159
+ cat = torch.cat([p.reshape(-1) for p in parts], dim=0)
160
+ piece_pred_ids[k] = cat[:n_target] if k == "boundary" else cat[:n_target].long()
161
+
162
+ inv_root = {v: k for k, v in vocabs["root"].items()}
163
+ inv_qual = {v: k for k, v in vocabs["quality"].items()}
164
+ inv_bass = {v: k for k, v in vocabs["bass"].items()}
165
+
166
+ valid_len = len(piece_pred_ids.get("root", []))
167
+ if valid_len == 0:
168
+ return "No predictions generated."
169
+
170
+ r_seq = piece_pred_ids["root"][:valid_len].tolist()
171
+ q_seq = piece_pred_ids["quality"][:valid_len].tolist()
172
+ b_seq = piece_pred_ids["bass"][:valid_len].tolist()
173
+
174
+ time_per_token = 1.0 / max(1, config["model"]["label_resolution"])
175
+ merged = []
176
+ cur_r, cur_q, cur_b = r_seq[0], q_seq[0], b_seq[0]
177
+ cur_start = 0
178
+ for t in range(1, valid_len):
179
+ if r_seq[t] != cur_r or q_seq[t] != cur_q or b_seq[t] != cur_b:
180
+ label = f"{inv_root.get(cur_r, str(cur_r))}_{inv_qual.get(cur_q, str(cur_q))}_{inv_bass.get(cur_b, str(cur_b))}"
181
+ merged.append(f"{cur_start * time_per_token:.2f} {label}")
182
+ cur_r, cur_q, cur_b = r_seq[t], q_seq[t], b_seq[t]
183
+ cur_start = t
184
+ label = f"{inv_root.get(cur_r, str(cur_r))}_{inv_qual.get(cur_q, str(cur_q))}_{inv_bass.get(cur_b, str(cur_b))}"
185
+ merged.append(f"{cur_start * time_per_token:.2f} {label}")
186
+
187
+ return "\n".join(merged)
188
+
189
+
190
+ model_card = ModelCard(
191
+ name="BACHI Chord Recognition",
192
+ description="Automatic chord recognition from symbolic music scores (MIDI or MusicXML). Outputs beat-aligned chord labels.",
193
+ author="Mingyang Yao, Ke Chen, Shlomo Dubnov, Taylor Berg-Kirkpatrick",
194
+ tags=["chord-recognition", "symbolic-music", "midi", "musicxml"],
195
+ )
196
+
197
+
198
+ def process_fn(input_file: str, model_type: str) -> str:
199
+ print(f"Processing with {model_type} model...", flush=True)
200
+ model, config, vocabs, use_key, device = get_model(model_type)
201
+
202
+ score_path = Path(input_file)
203
+ pianoroll_np = extract_pianoroll(score_path, resolution=config["model"]["beat_resolution"])
204
+ if pianoroll_np is None:
205
+ return "Error: Could not extract notes from the input file."
206
+
207
+ pianoroll = torch.from_numpy(pianoroll_np.T).float()
208
+ result = predict_piece(pianoroll, model, config, vocabs, use_key, device)
209
+ print("Done.", flush=True)
210
+ return result
211
+
212
+
213
+ with gr.Blocks() as demo:
214
+ input_components = [
215
+ gr.File(
216
+ label="Input Score (.mid, .midi, .musicxml, .mxl, .xml)",
217
+ file_types=[".mid", ".midi", ".musicxml", ".mxl", ".xml"],
218
+ ),
219
+ gr.Dropdown(
220
+ choices=["Classical", "Pop"],
221
+ value="Classical",
222
+ label="Model Type",
223
+ ),
224
+ ]
225
+ output_components = [
226
+ gr.Textbox(label="Chord Predictions", lines=20),
227
+ ]
228
+ app = build_endpoint(
229
+ model_card=model_card,
230
+ input_components=input_components,
231
+ output_components=output_components,
232
+ process_fn=process_fn,
233
+ )
234
+
235
+ print("Launching Gradio...", flush=True)
236
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True, pwa=True)
dataset.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import torch
4
+ from torch.utils.data import Dataset, random_split
5
+ import numpy as np
6
+ import pickle
7
+ from typing import List, Dict, Tuple, Optional, Any
8
+ import math
9
+ import torch.nn.functional as F
10
+ import random
11
+ from collections import defaultdict
12
+
13
+
14
+ def load_vocabs(vocab_path: str) -> Dict[str, Any]:
15
+ """Loads vocabularies and augments with per-component PAD/NONE indices.
16
+
17
+ For 3-part prediction, only `root`, `quality`, and `bass` are loaded.
18
+ """
19
+ with open(vocab_path, 'rb') as f:
20
+ data = pickle.load(f)
21
+
22
+ root_map = data['root_to_idx']
23
+ pad_token = 'PAD'
24
+ none_tokens = ['N', 'None'] # allow either spelling in source vocabs
25
+ bass_map = root_map
26
+
27
+ # Only keep the three chord parts for prediction
28
+ vocabs = {
29
+ 'root': root_map,
30
+ 'quality': data['quality_to_idx'],
31
+ 'bass': bass_map,
32
+ 'key': data['key_to_idx'],
33
+ }
34
+
35
+ # Global root PAD index (back-compat)
36
+ vocabs['pad_idx'] = root_map[pad_token]
37
+
38
+ # Add per-component PAD and NONE indices
39
+ for comp, comp_map in list(vocabs.items()):
40
+ if comp == 'pad_idx':
41
+ continue
42
+ # per-component PAD index (must exist)
43
+ comp_pad_idx = comp_map.get(pad_token)
44
+ if comp_pad_idx is None:
45
+ raise ValueError(f"Component '{comp}' vocab lacks PAD token")
46
+ vocabs[f'{comp}_pad_idx'] = comp_pad_idx
47
+
48
+ # NONE index preference: N > None > PAD
49
+ none_idx = None
50
+ for tok in none_tokens:
51
+ if tok in comp_map:
52
+ none_idx = comp_map[tok]
53
+ break
54
+ if none_idx is None:
55
+ none_idx = comp_pad_idx
56
+ vocabs[f'{comp}_none_idx'] = none_idx
57
+
58
+ return vocabs
59
+
60
+
61
+ class PianoRollDataset(Dataset):
62
+ """Dataset for piano roll representation."""
63
+ pad_idx = -1 # Will be updated in __init__
64
+ def __init__(
65
+ self,
66
+ data_root: str,
67
+ config: dict,
68
+ vocabs: Dict[str, Any],
69
+ split: str = 'train',
70
+ use_augmentation: bool = False,
71
+ use_key: bool = False,
72
+ ):
73
+ self.data_root = data_root
74
+ self.config = config
75
+ self.n_beats = self.config['n_beats']
76
+ self.split = split
77
+ self.use_augmentation = use_augmentation
78
+ self.use_key = use_key
79
+ self.beat_resolution = self.config['beat_resolution']
80
+ self.label_resolution = self.config['label_resolution']
81
+ self.pr_to_label_ratio = self.beat_resolution // self.label_resolution
82
+
83
+ self.vocabs = vocabs
84
+ self.pad_idx = self.vocabs['pad_idx']
85
+
86
+ self.chord_components = ['root', 'quality', 'bass']
87
+ self.label_indices_map = {'root': 0, 'quality': 1, 'bass': 2}
88
+ if self.use_key:
89
+ self.chord_components.append('key')
90
+ self.label_indices_map['key'] = 3
91
+
92
+ # --- Lengths in pianoroll-frame resolution ---
93
+ self.max_len = self.n_beats * self.beat_resolution
94
+
95
+ for comp in self.chord_components:
96
+ setattr(self, f'{comp}_vocab', self.vocabs[comp])
97
+ setattr(self, f'{comp}_none_idx', self.vocabs[f'{comp}_none_idx'])
98
+
99
+ suffix = 'shift0.npz' if not self.use_augmentation else '.npz'
100
+ # print(f"Loading {suffix} files from {data_root}")
101
+ self.file_list = sorted([
102
+ os.path.join(data_root, f)
103
+ for f in os.listdir(data_root) if f.endswith(suffix)
104
+ ])
105
+
106
+ def __len__(self) -> int:
107
+ return len(self.file_list)
108
+
109
+ def __getitem__(self, idx: int) -> Optional[Dict[str, torch.Tensor]]:
110
+ filepath = self.file_list[idx]
111
+ with np.load(filepath, allow_pickle=True) as data:
112
+ pianoroll_full = torch.from_numpy(data['pianoroll'].T).float()
113
+ labels_full = data['labels']
114
+ boundaries_full = data['boundaries']
115
+
116
+ pianoroll = pianoroll_full
117
+ labels = labels_full
118
+
119
+ # --- Create ground truth chord tensor from labels (map to per-component vocab indices) ---
120
+ target_indices = {}
121
+
122
+ for comp in self.chord_components:
123
+ vocab = getattr(self, f'{comp}_vocab')
124
+ none_idx = getattr(self, f'{comp}_none_idx')
125
+ label_col_idx = self.label_indices_map[comp]
126
+ col = labels[:, label_col_idx]
127
+ mapped_tensor = None
128
+ # If labels are already integer indices within range, accept directly
129
+ try:
130
+ if np.issubdtype(col.dtype, np.integer):
131
+ col_int = col.astype(np.int64)
132
+ if col_int.min(initial=0) >= 0 and col_int.max(initial=0) < len(vocab):
133
+ mapped_tensor = torch.from_numpy(col_int)
134
+ except Exception:
135
+ mapped_tensor = None
136
+ # Otherwise map string/mixed labels through vocab with fallback to none_idx
137
+ if mapped_tensor is None:
138
+ try:
139
+ col_list = col.astype(str).tolist()
140
+ except Exception:
141
+ col_list = [str(x) for x in col.tolist()]
142
+ mapped = [vocab.get(lbl, none_idx) for lbl in col_list]
143
+ mapped_tensor = torch.tensor(mapped, dtype=torch.long)
144
+ target_indices[comp] = mapped_tensor.long()
145
+
146
+ # --- Load pre-computed boundary flag ---
147
+ boundary_flag = torch.from_numpy(boundaries_full.astype(np.float32))
148
+
149
+ if self.split == 'train':
150
+ return self._get_train_item(pianoroll, target_indices, boundary_flag)
151
+ else: # 'val' or 'test'
152
+ piece_name = _get_piece_name(filepath)
153
+ # Build accurate targets from labels for evaluation
154
+ return self._get_eval_item(pianoroll, labels, boundary_flag, piece_name)
155
+
156
+ def _sample_stratified_start(self, X: int) -> int:
157
+ """
158
+ Sample s ∈ {0..X} with P(s) ∝ 1 + beta * (s/X).
159
+ Implemented as a mixture of Uniform and 'linear-in-s' discrete law.
160
+ Exact, O(1), numerically stable.
161
+
162
+ beta ∈ [0,2]. beta=0 -> uniform; beta=1 -> mild late tilt (good default).
163
+ """
164
+ if X <= 0:
165
+ return 0
166
+
167
+ beta = float(getattr(self, 'stratify_beta', 1.0))
168
+
169
+ # Mixture weights: P = a * Uniform + (1-a) * Linear(s)
170
+ a = 1.0 - beta / 2.0 # ∈ [0,1]
171
+ if np.random.rand() < a:
172
+ # Uniform over 0..X
173
+ return int(np.random.randint(0, X + 1))
174
+ else:
175
+ # Sample from Q(s) ∝ s over {0..X} (i.e., s=0 has weight 0).
176
+ # Do it by inverting triangular numbers over 1..X.
177
+ M = X * (X + 1) // 2 # sum_{s=1}^X s
178
+ r = np.random.randint(1, M + 1) # 1..M inclusive
179
+ s = int((math.isqrt(1 + 8 * r) - 1) // 2) # floor((sqrt(1+8r)-1)/2)
180
+ # Numerical guard (rare when r hits exact triangle): clamp
181
+ if s > X:
182
+ s = X
183
+ return s
184
+
185
+ def _get_train_item(self, pianoroll, target_indices, boundary_flag):
186
+ n_pr_frames = pianoroll.shape[0]
187
+ # start with at least half of window size and convert to label frames
188
+ max_start_label_frame = (n_pr_frames - self.max_len // 2) // self.pr_to_label_ratio
189
+ target_max_len = self.max_len // self.pr_to_label_ratio
190
+
191
+ # Stratified start over 0..max_start_label_frame (tilt to late positions)
192
+ start_label_frame = self._sample_stratified_start(max_start_label_frame)
193
+ start_pr_frame = start_label_frame * self.pr_to_label_ratio
194
+
195
+ # --- slice & pad encoder input ---
196
+ pr_segment = pianoroll[start_pr_frame : start_pr_frame + self.max_len]
197
+ pr_pad_amount = self.max_len - pr_segment.shape[0]
198
+ if pr_pad_amount > 0:
199
+ # keep dtype/device consistent with pr_segment
200
+ pr_pad = pr_segment.new_zeros((pr_pad_amount, pr_segment.shape[1]))
201
+ pr_segment = torch.cat([pr_segment, pr_pad], dim=0)
202
+
203
+ # --- slice targets at label resolution ---
204
+ target_start = start_label_frame
205
+ target_segs = {}
206
+ for comp in self.chord_components:
207
+ target_segs[comp] = target_indices[comp][target_start : target_start + target_max_len]
208
+ boundary_seg = boundary_flag[target_start : target_start + target_max_len]
209
+
210
+ # --- masks & padding for targets ---
211
+ current_target_len = target_segs[self.chord_components[0]].shape[0]
212
+ target_mask = torch.zeros(target_max_len, dtype=torch.bool)
213
+ target_mask[:current_target_len] = True
214
+
215
+ # expand target mask to encoder (frame) mask
216
+ encoder_mask = target_mask.repeat_interleave(self.pr_to_label_ratio)
217
+ if pr_pad_amount > 0:
218
+ encoder_mask[-pr_pad_amount:] = False
219
+
220
+ target_pad_amount = target_max_len - current_target_len
221
+ if target_pad_amount > 0:
222
+ for comp in self.chord_components:
223
+ comp_none_idx = getattr(self, f'{comp}_none_idx')
224
+ pad_tensor = torch.full((target_pad_amount,), comp_none_idx, dtype=torch.long)
225
+ target_segs[comp] = torch.cat([target_segs[comp], pad_tensor])
226
+
227
+ boundary_pad = torch.zeros(target_pad_amount, dtype=boundary_seg.dtype)
228
+ boundary_seg = torch.cat([boundary_seg, boundary_pad])
229
+
230
+ item = {
231
+ 'encoder_input': pr_segment,
232
+ 'target_boundary': boundary_seg,
233
+ 'mask': target_mask,
234
+ 'encoder_mask': encoder_mask,
235
+ }
236
+ for comp in self.chord_components:
237
+ item[f'target_{comp}'] = target_segs[comp]
238
+
239
+ return item
240
+
241
+ def _get_eval_item(self, pianoroll, labels, boundary_flag, piece_name):
242
+ # Reconstruct per-component target indices directly from the label matrix
243
+ n_label_frames = labels.shape[0]
244
+ target_indices = {}
245
+ for comp in self.chord_components:
246
+ vocab = getattr(self, f'{comp}_vocab')
247
+ none_idx = getattr(self, f'{comp}_none_idx')
248
+ label_col_idx = self.label_indices_map[comp]
249
+ # Extract the column for this component; handle types robustly
250
+ col = labels[:, label_col_idx]
251
+ mapped_tensor = None
252
+ # Case 1: already integer indices
253
+ try:
254
+ if np.issubdtype(col.dtype, np.integer):
255
+ col_int = col.astype(np.int64)
256
+ # If values look like valid indices, accept directly; otherwise fallback to mapping
257
+ if col_int.min(initial=0) >= 0 and col_int.max(initial=0) < len(vocab):
258
+ mapped_tensor = torch.from_numpy(col_int)
259
+ except Exception:
260
+ mapped_tensor = None
261
+ # Case 2: map from labels (strings or mixed types) to indices
262
+ if mapped_tensor is None:
263
+ try:
264
+ col_list = col.astype(str).tolist()
265
+ except Exception:
266
+ col_list = [str(x) for x in col.tolist()]
267
+ mapped = [vocab.get(lbl, none_idx) for lbl in col_list]
268
+ mapped_tensor = torch.tensor(mapped, dtype=torch.long)
269
+ target_indices[comp] = mapped_tensor.long()
270
+
271
+ mask = torch.ones(n_label_frames, dtype=torch.bool)
272
+ encoder_mask = torch.ones(pianoroll.shape[0], dtype=torch.bool)
273
+ item = {
274
+ 'piece_name': piece_name,
275
+ 'encoder_input': pianoroll,
276
+ 'target_boundary': boundary_flag,
277
+ 'mask': mask,
278
+ 'encoder_mask': encoder_mask,
279
+ }
280
+ for comp in self.chord_components:
281
+ item[f'target_{comp}'] = target_indices[comp]
282
+ return item
283
+
284
+ def get_vocab_sizes(self) -> Dict[str, int]:
285
+ sizes = {comp: len(self.vocabs[comp]) for comp in self.chord_components}
286
+ return sizes
287
+
288
+ def get_pad_idx(self) -> int:
289
+ return self.pad_idx
290
+
291
+
292
+ def _get_piece_name(filename: str) -> str:
293
+ """Extracts the base piece name from a filename by splitting on '_shift'."""
294
+ base_filename = os.path.basename(filename)
295
+ if '_shift' in base_filename:
296
+ piece_name = base_filename.split('_shift')[0]
297
+ else:
298
+ piece_name = base_filename
299
+ return piece_name
300
+
301
+
302
+ def create_datasets(
303
+ data_root: str,
304
+ config: dict,
305
+ vocabs: Dict[str, Any],
306
+ seed: int = 42,
307
+ ) -> Tuple[Dataset, Dataset]:
308
+ """
309
+ Create train and validation datasets with group-based splitting.
310
+ This ensures that all augmentations of a piece belong to the same split.
311
+ """
312
+ full_dataset = PianoRollDataset(
313
+ data_root=data_root,
314
+ config=config,
315
+ vocabs=vocabs,
316
+ split='train', # split does not matter here
317
+ use_augmentation=config['use_augmentation'],
318
+ use_key=config['use_key'],
319
+ )
320
+
321
+ # Group files by piece name
322
+ piece_files = defaultdict(list)
323
+ for f in full_dataset.file_list:
324
+ piece_name = _get_piece_name(f)
325
+ piece_files[piece_name].append(f)
326
+
327
+ unique_pieces = sorted(list(piece_files.keys()))
328
+
329
+ # Shuffle for random split
330
+ random.seed(seed)
331
+ random.shuffle(unique_pieces)
332
+
333
+ # Split unique pieces (90% train, 10% validation)
334
+ train_size = int(0.9 * len(unique_pieces))
335
+ train_pieces = unique_pieces[:train_size]
336
+ val_pieces = unique_pieces[train_size:]
337
+
338
+ # Get file lists for each split, only use shift0.npz for validation
339
+ train_files = [file for piece in train_pieces for file in piece_files[piece]]
340
+ if config['use_augmentation']:
341
+ val_files = [file for piece in val_pieces for file in piece_files[piece] if file.endswith('shift0.npz')]
342
+ else:
343
+ val_files = [file for piece in val_pieces for file in piece_files[piece]]
344
+ print(f"Train files: {len(train_files)}, Val files: {len(val_files)}")
345
+
346
+ # Create datasets for each split with the correct file list
347
+ train_dataset = PianoRollDataset(data_root, config, vocabs, 'train', use_key=config['use_key'])
348
+ train_dataset.file_list = train_files
349
+
350
+ val_dataset = PianoRollDataset(data_root, config, vocabs, 'val', use_key=config['use_key'])
351
+ val_dataset.file_list = val_files
352
+
353
+ json.dump(sorted([_get_piece_name(file) for file in val_files]),
354
+ open('val_files_unique.json', 'w'), indent=2)
355
+
356
+ return train_dataset, val_dataset
357
+
358
+
359
+ def collate_fn(batch):
360
+ """
361
+ Collate function that filters out empty or invalid samples.
362
+ For training, it uses default collate.
363
+ For evaluation (variable length), it handles padding if needed, but typically used with batch_size=1.
364
+ """
365
+ batch = [item for item in batch if item is not None]
366
+ if not batch:
367
+ return {}
368
+
369
+ # If batch contains only a single sample, simply return that sample's dict.
370
+ # This is handy for evaluation where we usually set batch_size = 1 and do
371
+ # not need the extra list wrapper.
372
+ if len(batch) == 1 and 'piece_name' in batch[0]:
373
+ return batch[0]
374
+
375
+ # For training batches (fixed-length segments) every sample has the same
376
+ # sequence length, so the default PyTorch collate works fine.
377
+ if 'encoder_input' in batch[0] and batch[0]['encoder_input'].shape[0] == batch[-1]['encoder_input'].shape[0]:
378
+ return torch.utils.data.dataloader.default_collate(batch)
379
+
380
+ # Otherwise we have variable-length sequences – fall back to returning the
381
+ # list so the caller can deal with padding/iteration manually.
382
+ return batch
models/HT.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+ from typing import Dict, Optional, Tuple
6
+
7
+ def get_relative_position_encoding(n_steps, d_model, max_dist=10):
8
+ """
9
+ Generates relative positional encodings, similar to Transformer-XL.
10
+ """
11
+ vocab_size = 2 * max_dist + 1
12
+ position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
13
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
14
+
15
+ pe = torch.zeros(vocab_size, d_model)
16
+ pe[:, 0::2] = torch.sin(position * div_term)
17
+ pe[:, 1::2] = torch.cos(position * div_term)
18
+
19
+ range_vec = torch.arange(n_steps)
20
+ distance_mat = range_vec[None, :] - range_vec[:, None]
21
+ distance_mat_clipped = torch.clamp(distance_mat, -max_dist, max_dist)
22
+ final_mat = distance_mat_clipped + max_dist
23
+
24
+ embeddings = F.embedding(final_mat.long(), pe)
25
+ return embeddings
26
+
27
+ class RelativeMultiHeadAttention(nn.Module):
28
+ """
29
+ Multi-Head Attention with relative positional encoding, inspired by Transformer-XL and HTv2.
30
+ """
31
+ def __init__(self, d_model, n_heads, dropout=0.1, max_dist=10):
32
+ super().__init__()
33
+ assert d_model % n_heads == 0
34
+ self.d_model = d_model
35
+ self.n_heads = n_heads
36
+ self.d_head = d_model // n_heads
37
+ self.max_dist = max_dist
38
+
39
+ self.w_q = nn.Linear(d_model, d_model)
40
+ self.w_k = nn.Linear(d_model, d_model)
41
+ self.w_v = nn.Linear(d_model, d_model)
42
+ self.w_o = nn.Linear(d_model, d_model)
43
+
44
+ self.u_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
45
+ self.v_bias = nn.Parameter(torch.randn(self.n_heads, self.d_head))
46
+ self.w_r = nn.Linear(d_model, d_model)
47
+
48
+ self.dropout = nn.Dropout(dropout)
49
+ self.layer_norm = nn.LayerNorm(d_model)
50
+
51
+ def forward(self, q, k, v, pos_emb, key_padding_mask=None, attn_mask=None, values_provided=False):
52
+ batch_size, seq_len_q, _ = q.size()
53
+ seq_len_k = k.size(1)
54
+
55
+ residual = q
56
+
57
+ q = self.w_q(q).view(batch_size, seq_len_q, self.n_heads, self.d_head)
58
+ k = self.w_k(k).view(batch_size, seq_len_k, self.n_heads, self.d_head)
59
+ v_transformed = self.w_v(v).view(batch_size, seq_len_k, self.n_heads, self.d_head)
60
+
61
+ q_with_u = (q + self.u_bias).transpose(1, 2) # (B, h, T_q, d_h)
62
+ q_with_v = (q + self.v_bias).transpose(1, 2) # (B, h, T_q, d_h)
63
+ k = k.transpose(1, 2) # (B, h, T_k, d_h)
64
+ v_transformed = v_transformed.transpose(1, 2) # (B, h, T_k, d_h)
65
+
66
+ pos_emb = self.w_r(pos_emb).view(seq_len_q, seq_len_k, self.n_heads, self.d_head)
67
+ pos_emb = pos_emb.permute(2, 0, 3, 1) # (h, T_q, d_h, T_k)
68
+
69
+ # Content-based addressing
70
+ ac = torch.matmul(q_with_u, k.transpose(-2, -1)) # (B, h, T_q, T_k)
71
+
72
+ # Position-based addressing
73
+ q_for_bd = q_with_v.permute(1, 2, 0, 3) # (h, T_q, B, d_h)
74
+ bd_t = torch.matmul(q_for_bd, pos_emb) # (h, T_q, B, T_k)
75
+ bd = bd_t.permute(2, 0, 1, 3) # (B, h, T_q, T_k)
76
+
77
+ attn_score = (ac + bd) / math.sqrt(self.d_head)
78
+
79
+ # Apply key and (optional) attention masks
80
+ if key_padding_mask is not None:
81
+ attn_score = attn_score.masked_fill(key_padding_mask.unsqueeze(1).unsqueeze(2), float('-inf'))
82
+
83
+ if attn_mask is not None:
84
+ attn_score = attn_score.masked_fill(attn_mask.unsqueeze(0), float('-inf'))
85
+
86
+ # ------------------------------------------------------------------
87
+ # Guard against rows where *all* keys were masked (e.g., an attention
88
+ # block that consists purely of padding tokens). A softmax over a
89
+ # vector of \(-\infty\) would otherwise produce NaNs. We detect such
90
+ # rows and set their scores to zero, ensuring a well-defined softmax
91
+ # (which will yield a uniform distribution) before later resetting
92
+ # their weights to zero.
93
+ # ------------------------------------------------------------------
94
+ all_masked = torch.isinf(attn_score) & (attn_score < 0) # True where -inf
95
+ all_masked = all_masked.all(dim=-1, keepdim=True) # (B, h, T_q, 1)
96
+
97
+ # For rows with all_masked == True, replace -inf with 0 so that the
98
+ # subsequent softmax does not generate NaNs.
99
+ attn_score = attn_score.masked_fill(all_masked, 0.0)
100
+
101
+ attn_weights = F.softmax(attn_score, dim=-1) # (B, h, T_q, T_k)
102
+
103
+ # After the softmax, zero-out rows that correspond to fully-padded
104
+ # queries so they don’t influence the output.
105
+ attn_weights = attn_weights.masked_fill(all_masked, 0.0)
106
+ attn_weights = self.dropout(attn_weights)
107
+
108
+ attn_output = torch.matmul(attn_weights, v_transformed) # (B, h, T_q, d_h)
109
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len_q, self.d_model)
110
+
111
+ attn_output = self.w_o(attn_output)
112
+ attn_output = self.dropout(attn_output)
113
+
114
+ if values_provided:
115
+ output = v + attn_output
116
+ else:
117
+ output = residual + attn_output
118
+
119
+ return self.layer_norm(output), attn_weights
120
+
121
+ class IntraBlockMHA(nn.Module):
122
+ def __init__(self, d_model, n_heads, dropout, max_dist=3):
123
+ super().__init__()
124
+ self.mha = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist=max_dist)
125
+
126
+ def forward(self, x, n_blocks, key_padding_mask=None):
127
+ batch_size, seq_len, _ = x.shape
128
+ block_len = seq_len // n_blocks
129
+
130
+ x = x.reshape(batch_size * n_blocks, block_len, -1)
131
+
132
+ mask = None
133
+ if key_padding_mask is not None:
134
+ mask = key_padding_mask.reshape(batch_size * n_blocks, block_len)
135
+
136
+ pos_emb = get_relative_position_encoding(block_len, x.size(-1), self.mha.max_dist).to(x.device)
137
+
138
+ x, _ = self.mha(x, x, x, pos_emb, key_padding_mask=mask)
139
+ return x.reshape(batch_size, seq_len, -1)
140
+
141
+ class ConvFFN(nn.Module):
142
+ def __init__(self, d_model, dropout=0.1):
143
+ super().__init__()
144
+ # piano roll resolution: 4 -> 12 (3x)
145
+ # kernel size: 3 -> 9 (3x)
146
+ self.conv1 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
147
+ self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=9, padding=4)
148
+ self.dropout = nn.Dropout(dropout)
149
+ self.norm = nn.LayerNorm(d_model)
150
+
151
+ def forward(self, x):
152
+ residual = x
153
+ x = x.transpose(1, 2)
154
+ x = F.relu(self.conv1(x))
155
+ x = self.dropout(x)
156
+ x = self.conv2(x)
157
+ x = self.dropout(x)
158
+ x = x.transpose(1, 2)
159
+ x = x + residual
160
+ return self.norm(x)
161
+
162
+ class TransformerLayer(nn.Module):
163
+ def __init__(self, d_model, n_heads, dropout, max_dist, use_conv_ffn=True):
164
+ super().__init__()
165
+ self.self_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
166
+ self.cross_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
167
+ self.pos_attn = RelativeMultiHeadAttention(d_model, n_heads, dropout, max_dist)
168
+ if use_conv_ffn:
169
+ self.ffn = ConvFFN(d_model, dropout)
170
+ else: # Fallback to original FFN if needed
171
+ self.ffn = nn.Sequential(
172
+ nn.Linear(d_model, d_model * 4),
173
+ nn.ReLU(),
174
+ nn.Dropout(dropout),
175
+ nn.Linear(d_model * 4, d_model),
176
+ nn.Dropout(dropout)
177
+ )
178
+ self.norm = nn.LayerNorm(d_model)
179
+
180
+ self.use_conv_ffn = use_conv_ffn
181
+
182
+ def forward(self, dec_input, enc_output, pos_emb, dec_pos_emb,
183
+ tgt_mask=None, memory_mask=None,
184
+ tgt_key_padding_mask=None, memory_key_padding_mask=None):
185
+ # Self-attention
186
+ dec_output, _ = self.self_attn(dec_input, dec_input, dec_input, pos_emb, key_padding_mask=tgt_key_padding_mask)
187
+
188
+ # Positional Attention
189
+ dec_output = self.pos_attn(dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
190
+ dec_pos_emb.unsqueeze(0).repeat(dec_input.size(0), 1, 1),
191
+ dec_output, pos_emb=pos_emb,
192
+ key_padding_mask=tgt_key_padding_mask, values_provided=True)[0]
193
+
194
+ # Cross-attention
195
+ dec_output, _ = self.cross_attn(dec_output, enc_output, enc_output, pos_emb, key_padding_mask=memory_key_padding_mask)
196
+
197
+ # FFN
198
+ if self.use_conv_ffn:
199
+ dec_output = self.ffn(dec_output)
200
+ else:
201
+ dec_output = self.norm(dec_output + self.ffn(dec_output))
202
+
203
+ return dec_output
204
+
205
+ class PositionalEncoding(nn.Module):
206
+ """Injects some information about the relative or absolute position of the tokens in the sequence."""
207
+ def __init__(self, d_model, dropout=0.1, max_len=5000):
208
+ super(PositionalEncoding, self).__init__()
209
+ self.dropout = nn.Dropout(p=dropout)
210
+
211
+ pe = torch.zeros(max_len, d_model)
212
+ position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
213
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
214
+ pe[:, 0::2] = torch.sin(position * div_term)
215
+ pe[:, 1::2] = torch.cos(position * div_term)
216
+ pe = pe.unsqueeze(0)
217
+ self.register_buffer('pe', pe)
218
+
219
+ def forward(self, x):
220
+ """
221
+ Args:
222
+ x: Tensor, shape [batch_size, seq_len, d_model]
223
+ """
224
+ x = x + self.pe[:, :x.size(1), :]
225
+ return self.dropout(x)
226
+
227
+ class BinaryRound(torch.autograd.Function):
228
+ """
229
+ Rounds a tensor whose values are in [0,1] to a tensor with values in {0, 1},
230
+ using the straight through estimator for the gradient.
231
+ """
232
+ @staticmethod
233
+ def forward(ctx, input):
234
+ return torch.round(input).to(input.dtype)
235
+
236
+ @staticmethod
237
+ def backward(ctx, grad_output):
238
+ return grad_output
239
+
240
+ class HarmonyTransformer(nn.Module):
241
+ def __init__(self, config: Dict, vocab_sizes: Dict[str, int]):
242
+ super().__init__()
243
+ self.input_size = config['input_size']
244
+ self.d_model = config['d_model']
245
+ self.n_layers = config['n_layers']
246
+ self.n_heads = config['n_heads']
247
+ self.dropout_rate = config['dropout']
248
+ self.train_boundary = config['train_boundary']
249
+ self.slope = config.get('slope', 1.0)
250
+ self.max_len = config['n_beats'] * config['beat_resolution']
251
+ self.config = config
252
+
253
+ # Embeddings
254
+ self.enc_input_embed = nn.Linear(self.input_size, self.d_model)
255
+ self.dec_input_embed = nn.Linear(self.input_size, self.d_model)
256
+
257
+ self.pos_encoder = PositionalEncoding(self.d_model, self.dropout_rate, self.max_len)
258
+ self.register_buffer('pos_emb', get_relative_position_encoding(self.max_len, self.d_model, self.max_len - 1))
259
+
260
+ self.enc_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
261
+ self.dec_intra_block_mha = IntraBlockMHA(self.d_model, self.n_heads, self.dropout_rate, max_dist=3)
262
+
263
+ # Encoder
264
+ self.encoder_layers = nn.ModuleList([
265
+ RelativeMultiHeadAttention(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
266
+ for _ in range(self.n_layers)
267
+ ])
268
+ self.encoder_ffns = nn.ModuleList([ConvFFN(self.d_model, self.dropout_rate) for _ in range(self.n_layers)])
269
+ self.enc_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
270
+
271
+ # Decoder
272
+ self.decoder_layers = nn.ModuleList([
273
+ TransformerLayer(self.d_model, self.n_heads, self.dropout_rate, self.max_len-1)
274
+ for _ in range(self.n_layers)
275
+ ])
276
+ self.dec_weights = nn.Parameter(torch.zeros(self.n_layers + 1))
277
+
278
+ # Chord Change Prediction
279
+ self.chord_change_predictor = nn.Linear(self.d_model, 1)
280
+
281
+ # Output layers
282
+ self.root_predictor = nn.Linear(self.d_model, vocab_sizes['root'])
283
+ self.quality_predictor = nn.Linear(self.d_model, vocab_sizes['quality'])
284
+ self.bass_predictor = nn.Linear(self.d_model, vocab_sizes['bass'])
285
+
286
+ self._reset_parameters()
287
+
288
+ def _reset_parameters(self):
289
+ for p in self.parameters():
290
+ if p.dim() > 1:
291
+ nn.init.xavier_uniform_(p)
292
+
293
+ def chord_block_compression(self, hidden_states, chord_changes):
294
+ """Compress hidden states according to chord changes."""
295
+ block_ids = torch.cumsum(chord_changes, dim=1) - chord_changes[:, 0].unsqueeze(1)
296
+ # Ensure integer dtype for one-hot encoding
297
+ block_ids = block_ids.long()
298
+
299
+ max_blocks = (torch.max(block_ids).item() + 1) if block_ids.numel() > 0 else 1
300
+ one_hot_ids = F.one_hot(block_ids, num_classes=max_blocks).float() # (B, S, M)
301
+
302
+ summed_states = torch.bmm(one_hot_ids.transpose(1, 2), hidden_states) # (B, M, H)
303
+ block_counts = one_hot_ids.sum(dim=1).unsqueeze(-1).clamp(min=1)
304
+
305
+ mean_states = summed_states / block_counts
306
+ # block_ids already of integer dtype
307
+ return mean_states, block_ids
308
+
309
+ def decode_compressed_sequences(self, compressed_sequences, block_ids):
310
+ """Decode chord sequences according to chords_pred and block_ids."""
311
+ return torch.gather(compressed_sequences, 1, block_ids.unsqueeze(-1).expand(-1, -1, compressed_sequences.size(-1)))
312
+
313
+ def forward(self, src: torch.Tensor, src_key_padding_mask: torch.Tensor) -> Dict[str, torch.Tensor]:
314
+ """
315
+ Args:
316
+ src: (batch_size, seq_len, input_size)
317
+ src_key_padding_mask: (batch_size, seq_len), True for valid, False for pad
318
+ Returns:
319
+ Dictionary of predictions.
320
+ """
321
+ # --- Encoder ---
322
+ enc_output = self.pos_encoder(self.enc_input_embed(src))
323
+
324
+ # Intra-block MHA
325
+ enc_output = self.enc_intra_block_mha(enc_output, n_blocks=self.max_len//4, key_padding_mask=src_key_padding_mask)
326
+
327
+ # Main encoder layers
328
+ enc_layer_outputs = [enc_output]
329
+ for i in range(self.n_layers):
330
+ enc_output, _ = self.encoder_layers[i](enc_output, enc_output, enc_output,
331
+ self.pos_emb, key_padding_mask=src_key_padding_mask)
332
+ enc_output = self.encoder_ffns[i](enc_output)
333
+ enc_layer_outputs.append(enc_output)
334
+
335
+ # Weighted sum of encoder layers
336
+ enc_weights = F.softmax(self.enc_weights, dim=0)
337
+ enc_output = torch.stack(enc_layer_outputs, dim=-1) # (B, S, H, L+1)
338
+ enc_output = (enc_output * enc_weights).sum(dim=-1) # (B, S, H)
339
+
340
+ # --- Chord‐change prediction ---
341
+ boundary_logits = self.chord_change_predictor(enc_output) # (B, S, 1)
342
+ chord_change_prob = torch.sigmoid(self.slope * boundary_logits)
343
+ chord_change_pred = BinaryRound.apply(chord_change_prob).squeeze(-1) # (B, S) in {0,1}
344
+
345
+ # --- Decoder input embedding with regionalization ---
346
+ dec_input_embed = self.dec_input_embed(src)
347
+ dec_input_embed = F.dropout(dec_input_embed, p=self.dropout_rate, training=self.training)
348
+ dec_input_embed = self.dec_intra_block_mha(dec_input_embed, n_blocks=self.max_len // 4, key_padding_mask=src_key_padding_mask)
349
+
350
+ # Compress by predicted chord boundaries and expand back
351
+ dec_input_embed_reg, block_ids = self.chord_block_compression(dec_input_embed, chord_change_pred.long())
352
+ dec_input_embed_reg = self.decode_compressed_sequences(dec_input_embed_reg, block_ids)
353
+
354
+ # Combine embeddings
355
+ dec_input_embed = dec_input_embed + dec_input_embed_reg + enc_output
356
+
357
+ # Positional encoding
358
+ dec_input_embed = self.pos_encoder(dec_input_embed)
359
+ dec_pos_emb = self.pos_encoder.pe[:, :dec_input_embed.size(1), :].squeeze(0)
360
+
361
+ # --- Decoder layers with layer weighting ---
362
+ dec_layer_outputs = [dec_input_embed]
363
+ dec_output = dec_input_embed
364
+ for i in range(self.n_layers):
365
+ dec_output = self.decoder_layers[i](dec_output, enc_output, self.pos_emb, dec_pos_emb,
366
+ tgt_key_padding_mask=src_key_padding_mask,
367
+ memory_key_padding_mask=src_key_padding_mask)
368
+ dec_layer_outputs.append(dec_output)
369
+
370
+ dec_weights = F.softmax(self.dec_weights, dim=0)
371
+ dec_output = torch.stack(dec_layer_outputs, dim=-1) # (B, S, H, L+1)
372
+ dec_output = (dec_output * dec_weights).sum(dim=-1)
373
+
374
+ # --- Output Projections ---
375
+ root_logits = self.root_predictor(dec_output)
376
+ quality_logits = self.quality_predictor(dec_output)
377
+ bass_logits = self.bass_predictor(dec_output)
378
+
379
+ preds = {
380
+ 'root': root_logits,
381
+ 'quality': quality_logits,
382
+ 'bass': bass_logits,
383
+ 'boundary': boundary_logits
384
+ }
385
+
386
+ return preds
models/__init__.py ADDED
File without changes
models/components.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Any, Dict, Optional, Tuple
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+
8
+ class PatchEmbedding(nn.Module):
9
+ def __init__(self, d_model: int, frames_per_patch: int = 6, expansion: int = 2):
10
+ super().__init__()
11
+ self.d_model = d_model
12
+ self.frames_per_patch = frames_per_patch
13
+ # Frame embedding (collapse pitch dim)
14
+ self.conv2d = nn.Conv2d(
15
+ in_channels=1,
16
+ out_channels=d_model,
17
+ kernel_size=(88, 1),
18
+ stride=(1, 1),
19
+ padding=(0, 0),
20
+ )
21
+ self.norm_frame = nn.LayerNorm(d_model)
22
+ # anti-aliasing conv on time axis
23
+ self.aa = nn.Conv1d(d_model, d_model, kernel_size=3, stride=1,
24
+ padding=1, groups=d_model, bias=False)
25
+
26
+ # Late temporal pooling (downsample frames -> patches)
27
+ self.glu_conv = nn.Conv1d(
28
+ in_channels=d_model,
29
+ out_channels=d_model * expansion * 2,
30
+ kernel_size=frames_per_patch,
31
+ stride=frames_per_patch,
32
+ padding=0,
33
+ bias=True,
34
+ )
35
+ self.project = nn.Conv1d(
36
+ in_channels=d_model * expansion,
37
+ out_channels=d_model,
38
+ kernel_size=1,
39
+ )
40
+ self.norm_temporal = nn.LayerNorm(d_model)
41
+
42
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
43
+ # x: (B, 1, 88, T)
44
+ x = self.conv2d(x) # (B, C, 1, T)
45
+ x = x.squeeze(2).transpose(1, 2) # (B, T, C)
46
+ x = self.norm_frame(x)
47
+
48
+ # anti-aliased and temporal pooling
49
+ x = x.transpose(1, 2) # (B, C, T)
50
+ x = self.aa(x) # (B, C, T)
51
+ v, g = self.glu_conv(x).chunk(2, dim=1)
52
+ x = self.project(v * torch.sigmoid(g)) # (B, C, T//k)
53
+ x = x.transpose(1, 2) # (B, T//k, C)
54
+ return self.norm_temporal(x)
55
+
56
+
57
+ def downsample_key_padding_mask(mask: Optional[torch.Tensor], frames_per_patch: int) -> Optional[torch.Tensor]:
58
+ if mask is None:
59
+ return None
60
+ bsz, total_len = mask.shape
61
+ if total_len < frames_per_patch:
62
+ return mask.new_zeros((bsz, 0), dtype=mask.dtype)
63
+ out_len = total_len // frames_per_patch
64
+ trimmed = mask[:, : out_len * frames_per_patch]
65
+ grouped = trimmed.view(bsz, out_len, frames_per_patch)
66
+ return grouped.all(dim=-1)
67
+
68
+
69
+ class RelativePositionBias(nn.Module):
70
+ def __init__(self, num_heads: int, max_distance: int) -> None:
71
+ super().__init__()
72
+ if max_distance < 1:
73
+ raise ValueError("max_distance must be >= 1")
74
+ self.num_heads = num_heads
75
+ self.max_distance = max_distance
76
+ self.bias = nn.Parameter(torch.zeros(2 * max_distance - 1, num_heads))
77
+
78
+ def forward(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
79
+ pos = torch.arange(seq_len, device=device)
80
+ rel = pos[:, None] - pos[None, :]
81
+ rel = rel.clamp(-self.max_distance + 1, self.max_distance - 1)
82
+ rel = rel + self.max_distance - 1
83
+ bias = self.bias[rel]
84
+ return bias.permute(2, 0, 1).to(dtype=dtype)
85
+
86
+
87
+ class RelativeTransformerEncoderLayer(nn.Module):
88
+ def __init__(
89
+ self,
90
+ d_model: int,
91
+ nhead: int,
92
+ dim_feedforward: int,
93
+ dropout: float,
94
+ activation: str,
95
+ ) -> None:
96
+ super().__init__()
97
+ if d_model % nhead != 0:
98
+ raise ValueError("d_model must be divisible by nhead")
99
+ self.d_model = d_model
100
+ self.nhead = nhead
101
+ self.head_dim = d_model // nhead
102
+
103
+ self.qkv_proj = nn.Linear(d_model, 3 * d_model)
104
+ self.attn_dropout = nn.Dropout(dropout)
105
+ self.out_proj = nn.Linear(d_model, d_model)
106
+ self.resid_dropout = nn.Dropout(dropout)
107
+
108
+ self.norm1 = nn.LayerNorm(d_model)
109
+ self.norm2 = nn.LayerNorm(d_model)
110
+
111
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
112
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
113
+ self.ff_dropout = nn.Dropout(dropout)
114
+
115
+ if activation == "gelu":
116
+ self.activation_fn = F.gelu
117
+ elif activation == "relu":
118
+ self.activation_fn = F.relu
119
+ else:
120
+ raise ValueError(f"unsupported activation: {activation}")
121
+
122
+ def forward(
123
+ self,
124
+ src: torch.Tensor,
125
+ src_mask: Optional[torch.Tensor] = None,
126
+ src_key_padding_mask: Optional[torch.Tensor] = None,
127
+ attn_bias: Optional[torch.Tensor] = None,
128
+ ) -> torch.Tensor:
129
+ bsz, seq_len, _ = src.size()
130
+ qkv = self.qkv_proj(src)
131
+ q, k, v = qkv.chunk(3, dim=-1)
132
+ q = q.view(bsz, seq_len, self.nhead, self.head_dim)
133
+ k = k.view(bsz, seq_len, self.nhead, self.head_dim)
134
+ v = v.view(bsz, seq_len, self.nhead, self.head_dim)
135
+
136
+ attn_scores = torch.einsum("bthd,bshd->bhts", q, k) / math.sqrt(self.head_dim)
137
+
138
+ if src_mask is not None:
139
+ if src_mask.dtype == torch.bool:
140
+ attn_scores = attn_scores.masked_fill(src_mask.unsqueeze(0), float("-inf"))
141
+ else:
142
+ attn_scores = attn_scores + src_mask.unsqueeze(0)
143
+
144
+ if src_key_padding_mask is not None:
145
+ key_mask = src_key_padding_mask.unsqueeze(1).unsqueeze(2)
146
+ attn_scores = attn_scores.masked_fill(key_mask, float("-inf"))
147
+
148
+ if attn_bias is not None:
149
+ if attn_bias.dim() == 3:
150
+ attn_scores = attn_scores + attn_bias.unsqueeze(0)
151
+ elif attn_bias.dim() == 4:
152
+ attn_scores = attn_scores + attn_bias
153
+ else:
154
+ raise ValueError("attn_bias must be 3D or 4D tensor")
155
+
156
+ attn_weights = F.softmax(attn_scores, dim=-1)
157
+ attn_weights = self.attn_dropout(attn_weights)
158
+ context = torch.einsum("bhts,bshd->bthd", attn_weights, v)
159
+ context = context.contiguous().view(bsz, seq_len, self.d_model)
160
+ attn_out = self.out_proj(context)
161
+ src = self.norm1(src + self.resid_dropout(attn_out))
162
+
163
+ ff_out = self.linear2(self.ff_dropout(self.activation_fn(self.linear1(src))))
164
+ src = self.norm2(src + self.resid_dropout(ff_out))
165
+ return src
166
+
167
+
168
+ class RelativeTransformerEncoder(nn.Module):
169
+ def __init__(
170
+ self,
171
+ num_layers: int,
172
+ d_model: int,
173
+ nhead: int,
174
+ dim_feedforward: int,
175
+ dropout: float,
176
+ activation: str,
177
+ relative_position_bias: Optional[RelativePositionBias],
178
+ ) -> None:
179
+ super().__init__()
180
+ self.layers = nn.ModuleList(
181
+ [
182
+ RelativeTransformerEncoderLayer(
183
+ d_model=d_model,
184
+ nhead=nhead,
185
+ dim_feedforward=dim_feedforward,
186
+ dropout=dropout,
187
+ activation=activation,
188
+ )
189
+ for _ in range(num_layers)
190
+ ]
191
+ )
192
+ self.norm = nn.LayerNorm(d_model)
193
+ self.rpb = relative_position_bias
194
+
195
+ def forward(
196
+ self,
197
+ src: torch.Tensor,
198
+ src_key_padding_mask: Optional[torch.Tensor],
199
+ ) -> torch.Tensor:
200
+ output = src
201
+ if self.rpb is not None:
202
+ attn_bias = self.rpb(src.size(1), device=src.device, dtype=src.dtype)
203
+ else:
204
+ attn_bias = None
205
+
206
+ for layer in self.layers:
207
+ output = layer(
208
+ output,
209
+ src_key_padding_mask=src_key_padding_mask,
210
+ attn_bias=attn_bias,
211
+ )
212
+
213
+ output = self.norm(output)
214
+ return output
215
+
216
+
217
+ class ChordProjectionHead(nn.Module):
218
+ def __init__(self, d_model: int, vocab_sizes: Dict[str, int]) -> None:
219
+ super().__init__()
220
+ self.boundary_head = nn.Sequential(
221
+ nn.Linear(d_model, d_model // 2),
222
+ nn.GELU(),
223
+ nn.Linear(d_model // 2, 1),
224
+ )
225
+
226
+ projection_heads: Dict[str, nn.Module] = {}
227
+ for name, size in vocab_sizes.items():
228
+ projection_heads[name] = nn.Sequential(
229
+ nn.Linear(d_model, d_model // 2),
230
+ nn.GELU(),
231
+ nn.Linear(d_model // 2, size),
232
+ )
233
+ self.projection_heads = nn.ModuleDict(projection_heads)
234
+
235
+ def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
236
+ boundary_logits = self.boundary_head(x).squeeze(-1)
237
+ outputs: Dict[str, torch.Tensor] = {"boundary": boundary_logits}
238
+ for comp, head in self.projection_heads.items():
239
+ outputs[comp] = head(x)
240
+ return outputs
241
+
242
+
243
+ class KTokenDecoderLayer(nn.Module):
244
+ def __init__(self, d_model: int, nhead: int, mlp_ratio: int, dropout: float) -> None:
245
+ super().__init__()
246
+ if d_model % nhead != 0:
247
+ raise ValueError("d_model must be divisible by nhead")
248
+ self.d_model = d_model
249
+ self.nhead = nhead
250
+ self.head_dim = d_model // nhead
251
+
252
+ self.sa_qkv = nn.Linear(d_model, 3 * d_model)
253
+ self.sa_out = nn.Linear(d_model, d_model)
254
+ self.sa_ln = nn.LayerNorm(d_model)
255
+ self.sa_drop = nn.Dropout(dropout)
256
+
257
+ self.ca_q = nn.Linear(d_model, d_model)
258
+ self.ca_kv = nn.Linear(d_model, 2 * d_model)
259
+ self.ca_out = nn.Linear(d_model, d_model)
260
+ self.ca_ln = nn.LayerNorm(d_model)
261
+ self.ca_drop = nn.Dropout(dropout)
262
+
263
+ hidden = d_model * mlp_ratio
264
+ self.ff_ln = nn.LayerNorm(d_model)
265
+ self.ff = nn.Sequential(
266
+ nn.Linear(d_model, hidden),
267
+ nn.GELU(),
268
+ nn.Dropout(dropout),
269
+ nn.Linear(hidden, d_model),
270
+ )
271
+ self.ff_drop = nn.Dropout(dropout)
272
+
273
+ def _attn(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
274
+ attn = torch.einsum("nlhd,nshd->nhls", q, k) / math.sqrt(q.size(-1))
275
+ attn = torch.softmax(attn, dim=-1)
276
+ ctx = torch.einsum("nhls,nshd->nlhd", attn, v)
277
+ ctx = ctx.contiguous().view(q.size(0), q.size(1), -1)
278
+ return ctx
279
+
280
+ def forward(self, x_tokens: torch.Tensor, context: torch.Tensor) -> torch.Tensor:
281
+ bsz, length, k_tokens, d_model = x_tokens.shape
282
+ c_len = context.size(2)
283
+ batch_tokens = bsz * length
284
+
285
+ x = x_tokens.view(batch_tokens, k_tokens, d_model)
286
+ c = context.view(batch_tokens, c_len, d_model)
287
+
288
+ x_norm = self.sa_ln(x)
289
+ qkv = self.sa_qkv(x_norm)
290
+ q, k, v = qkv.chunk(3, dim=-1)
291
+ q = q.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
292
+ k = k.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
293
+ v = v.view(batch_tokens, k_tokens, self.nhead, self.head_dim)
294
+ sa_ctx = self._attn(q, k, v)
295
+ x = x + self.sa_drop(self.sa_out(sa_ctx))
296
+
297
+ x_norm = self.ca_ln(x)
298
+ q = self.ca_q(x_norm).view(batch_tokens, k_tokens, self.nhead, self.head_dim)
299
+ kv = self.ca_kv(c)
300
+ k, v = kv.chunk(2, dim=-1)
301
+ k = k.view(batch_tokens, c_len, self.nhead, self.head_dim)
302
+ v = v.view(batch_tokens, c_len, self.nhead, self.head_dim)
303
+ ca_ctx = self._attn(q, k, v)
304
+ x = x + self.ca_drop(self.ca_out(ca_ctx))
305
+
306
+ x_norm = self.ff_ln(x)
307
+ x = x + self.ff_drop(self.ff(x_norm))
308
+
309
+ return x.view(bsz, length, k_tokens, d_model)
310
+
311
+
312
+ def build_rpb(config: Dict[str, Any]) -> RelativePositionBias:
313
+ return RelativePositionBias(
314
+ num_heads=config["n_head"],
315
+ max_distance=config["n_beats"] * config["label_resolution"],
316
+ )
317
+
318
+
319
+ def build_encoder(config: Dict[str, Any]) -> RelativeTransformerEncoder:
320
+ rpb = build_rpb(config)
321
+ return RelativeTransformerEncoder(
322
+ num_layers=config["num_encoder_layers"],
323
+ d_model=config["d_model"],
324
+ nhead=config["n_head"],
325
+ dim_feedforward=config["dim_feedforward"],
326
+ dropout=config["dropout"],
327
+ activation="gelu",
328
+ relative_position_bias=rpb,
329
+ )
330
+
331
+
332
+ def build_patch_embedding(config: Dict[str, Any]) -> PatchEmbedding:
333
+ return PatchEmbedding(
334
+ d_model=config["d_model"],
335
+ frames_per_patch=config["frames_per_patch"],
336
+ expansion=2,
337
+ )
338
+
339
+
models/model.py ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+ from typing import Dict, Any, Optional
6
+
7
+
8
+ class PatchEmbedding(nn.Module):
9
+ def __init__(self, d_model: int, frames_per_patch: int = 6, expansion: int = 2):
10
+ super().__init__()
11
+ self.d_model = d_model
12
+ self.frames_per_patch = frames_per_patch
13
+ # Frame embedding (collapse pitch dim)
14
+ self.conv2d = nn.Conv2d(
15
+ in_channels=1,
16
+ out_channels=d_model,
17
+ kernel_size=(88, 1),
18
+ stride=(1, 1),
19
+ padding=(0, 0),
20
+ )
21
+ self.norm_frame = nn.LayerNorm(d_model)
22
+ # anti-aliasing conv on time axis
23
+ self.aa = nn.Conv1d(d_model, d_model, kernel_size=3, stride=1,
24
+ padding=1, groups=d_model, bias=False)
25
+
26
+ # Late temporal pooling (downsample frames -> patches)
27
+ self.glu_conv = nn.Conv1d(
28
+ in_channels=d_model,
29
+ out_channels=d_model * expansion * 2,
30
+ kernel_size=frames_per_patch,
31
+ stride=frames_per_patch,
32
+ padding=0,
33
+ bias=True,
34
+ )
35
+ self.project = nn.Conv1d(
36
+ in_channels=d_model * expansion,
37
+ out_channels=d_model,
38
+ kernel_size=1,
39
+ )
40
+ self.norm_temporal = nn.LayerNorm(d_model)
41
+
42
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
43
+ # x: (B, 1, 88, T)
44
+ x = self.conv2d(x) # (B, C, 1, T)
45
+ x = x.squeeze(2).transpose(1, 2) # (B, T, C)
46
+ x = self.norm_frame(x)
47
+
48
+ # anti-aliased and temporal pooling
49
+ x = x.transpose(1, 2) # (B, C, T)
50
+ x = self.aa(x) # (B, C, T)
51
+ v, g = self.glu_conv(x).chunk(2, dim=1)
52
+ x = self.project(v * torch.sigmoid(g)) # (B, C, T//k)
53
+ x = x.transpose(1, 2) # (B, T//k, C)
54
+ return self.norm_temporal(x)
55
+
56
+
57
+ class PositionalEncoding(nn.Module):
58
+ def __init__(self, d_model, dropout=0.1, max_len=5000):
59
+ super(PositionalEncoding, self).__init__()
60
+ self.dropout = nn.Dropout(p=dropout)
61
+
62
+ pe = torch.zeros(max_len, d_model)
63
+ position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
64
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
65
+ pe[:, 0::2] = torch.sin(position * div_term)
66
+ pe[:, 1::2] = torch.cos(position * div_term)
67
+ pe = pe.unsqueeze(0).transpose(0, 1)
68
+ self.register_buffer('pe', pe)
69
+
70
+ def forward(self, x):
71
+ x = x.transpose(0, 1)
72
+ x = x + self.pe[:x.size(0), :]
73
+ x = self.dropout(x)
74
+ return x.transpose(0, 1)
75
+
76
+
77
+ class RelativeTransformerEncoderLayer(nn.Module):
78
+ def __init__(self, d_model: int, nhead: int, dim_feedforward: int, dropout: float, activation: str = 'gelu'):
79
+ super().__init__()
80
+ if d_model % nhead != 0:
81
+ raise ValueError("d_model must be divisible by nhead.")
82
+ self.d_model = d_model
83
+ self.nhead = nhead
84
+ self.head_dim = d_model // nhead
85
+
86
+ self.qkv_proj = nn.Linear(d_model, 3 * d_model)
87
+ self.attn_dropout = nn.Dropout(dropout)
88
+ self.out_proj = nn.Linear(d_model, d_model)
89
+ self.resid_dropout = nn.Dropout(dropout)
90
+
91
+ self.norm1 = nn.LayerNorm(d_model)
92
+ self.norm2 = nn.LayerNorm(d_model)
93
+
94
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
95
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
96
+ self.ff_dropout = nn.Dropout(dropout)
97
+
98
+ if activation == 'gelu':
99
+ self.activation_fn = F.gelu
100
+ elif activation == 'relu':
101
+ self.activation_fn = F.relu
102
+ else:
103
+ raise ValueError(f"Unsupported activation: {activation}")
104
+
105
+ def forward(
106
+ self,
107
+ src: torch.Tensor,
108
+ src_mask: Optional[torch.Tensor] = None,
109
+ src_key_padding_mask: Optional[torch.Tensor] = None,
110
+ attn_bias: Optional[torch.Tensor] = None,
111
+ ) -> torch.Tensor:
112
+ # src: (B, T_new, C)
113
+ bsz, seq_len_new, _ = src.size()
114
+
115
+ qkv = self.qkv_proj(src) # (B, T_new, 3*C)
116
+ q, k, v = qkv.chunk(3, dim=-1)
117
+ q = q.view(bsz, seq_len_new, self.nhead, self.head_dim)
118
+ k_all = k.view(bsz, seq_len_new, self.nhead, self.head_dim)
119
+ v_all = v.view(bsz, seq_len_new, self.nhead, self.head_dim)
120
+
121
+ # Attention: queries are only for current tokens; keys include past+current
122
+ attn_scores = torch.einsum('bthd,bshd->bhts', q, k_all) / math.sqrt(self.head_dim) # (B, H, T_new, T_total)
123
+
124
+ # Additive or boolean mask over attention logits
125
+ if src_mask is not None:
126
+ if src_mask.dtype == torch.bool:
127
+ attn_scores = attn_scores.masked_fill(src_mask.unsqueeze(0), float('-inf'))
128
+ else:
129
+ attn_scores = attn_scores + src_mask.unsqueeze(0)
130
+
131
+ # Key padding mask
132
+ if src_key_padding_mask is not None:
133
+ key_mask = src_key_padding_mask.unsqueeze(1).unsqueeze(2) # (B,1,1,T)
134
+ attn_scores = attn_scores.masked_fill(key_mask, float('-inf'))
135
+
136
+ if attn_bias is not None:
137
+ # Support 3D (H, T, T) or 4D (B, H, T, T)
138
+ if attn_bias.dim() == 3:
139
+ attn_scores = attn_scores + attn_bias.unsqueeze(0)
140
+ elif attn_bias.dim() == 4:
141
+ attn_scores = attn_scores + attn_bias
142
+ else:
143
+ raise ValueError("attn_bias must be 3D or 4D tensor if provided")
144
+
145
+ attn_weights = F.softmax(attn_scores, dim=-1)
146
+ attn_weights = self.attn_dropout(attn_weights)
147
+
148
+ context = torch.einsum('bhts,bshd->bthd', attn_weights, v_all) # (B, T_new, H, D)
149
+ context = context.contiguous().view(bsz, seq_len_new, self.d_model) # (B, T_new, C)
150
+ attn_out = self.out_proj(context)
151
+
152
+ src = src + self.resid_dropout(attn_out)
153
+ src = self.norm1(src)
154
+
155
+ ff = self.linear2(self.ff_dropout(self.activation_fn(self.linear1(src))))
156
+ src = src + self.resid_dropout(ff)
157
+ src = self.norm2(src)
158
+
159
+ return src
160
+
161
+ def downsample_key_padding_mask(mask: torch.Tensor, frames_per_patch: int) -> torch.Tensor:
162
+ # mask: (B, T) where True denotes padding.
163
+ bsz, total_len = mask.shape
164
+ if total_len < frames_per_patch:
165
+ # No valid output tokens from temporal pooling
166
+ return mask.new_ones((bsz, 0), dtype=mask.dtype)
167
+ out_len = total_len // frames_per_patch
168
+ trimmed = mask[:, :out_len * frames_per_patch]
169
+ grouped = trimmed.view(bsz, out_len, frames_per_patch)
170
+ return grouped.all(dim=-1)
171
+
172
+
173
+ class RelativePositionBias(nn.Module):
174
+ def __init__(self, num_heads: int, max_distance: int):
175
+ super().__init__()
176
+ if max_distance < 1:
177
+ raise ValueError("max_distance must be >= 1")
178
+ self.max_distance = max_distance
179
+ self.num_heads = num_heads
180
+ # Table over relative distances in [-max_distance+1, max_distance-1]
181
+ self.bias = nn.Parameter(torch.zeros(2 * max_distance - 1, num_heads))
182
+
183
+ def forward(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
184
+ # Compute clipped relative position indices
185
+ pos = torch.arange(seq_len, device=device)
186
+ rel = pos[:, None] - pos[None, :] # (T, T)
187
+ rel = rel.clamp(-self.max_distance + 1, self.max_distance - 1)
188
+ rel = rel + self.max_distance - 1 # shift to [0, 2*max_distance-2]
189
+ bias = self.bias[rel] # (T, T, H)
190
+ return bias.permute(2, 0, 1).to(dtype=dtype) # (H, T, T)
191
+
192
+
193
+ class RelativeTransformerEncoder(nn.Module):
194
+ def __init__(self, num_layers: int, d_model: int, nhead: int, dim_feedforward: int,
195
+ dropout: float, activation: str = 'gelu', relative_position_bias = None):
196
+ super().__init__()
197
+ self.layers = nn.ModuleList([
198
+ RelativeTransformerEncoderLayer(
199
+ d_model=d_model,
200
+ nhead=nhead,
201
+ dim_feedforward=dim_feedforward,
202
+ dropout=dropout,
203
+ activation=activation,
204
+ ) for _ in range(num_layers)
205
+ ])
206
+ self.norm = nn.LayerNorm(d_model)
207
+ self.rpb = relative_position_bias
208
+ self.nhead = nhead
209
+
210
+ def forward(
211
+ self,
212
+ src: torch.Tensor,
213
+ src_key_padding_mask: Optional[torch.Tensor] = None,
214
+ ) -> torch.Tensor:
215
+ output = src
216
+
217
+ if self.rpb is not None:
218
+ attn_bias = self.rpb(src.size(1), device=src.device, dtype=src.dtype)
219
+ else:
220
+ attn_bias = None
221
+
222
+ for mod in self.layers:
223
+ output = mod(
224
+ output,
225
+ src_key_padding_mask=src_key_padding_mask,
226
+ attn_bias=attn_bias,
227
+ )
228
+
229
+ output = self.norm(output)
230
+ return output
231
+
232
+
233
+ class ChordDecomposeProjection(nn.Module):
234
+ def __init__(self, d_model: int, vocab_sizes: Dict[str, int]):
235
+ super().__init__()
236
+ self.d_model = d_model
237
+ self.vocab_sizes = vocab_sizes
238
+ self.boundary_head = nn.Sequential(
239
+ nn.Linear(d_model, d_model // 2),
240
+ nn.GELU(),
241
+ nn.Linear(d_model // 2, 1),
242
+ )
243
+
244
+ self.projection_heads = nn.ModuleDict()
245
+ for comp, size in self.vocab_sizes.items():
246
+ self.projection_heads[comp] = nn.Sequential(
247
+ nn.Linear(d_model, d_model // 2),
248
+ nn.GELU(),
249
+ nn.Linear(d_model // 2, size),
250
+ )
251
+
252
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
253
+ boundary_logits = self.boundary_head(x)
254
+
255
+ output = {'boundary': boundary_logits.squeeze(-1)}
256
+ for comp, head in self.projection_heads.items():
257
+ output[comp] = head(x)
258
+
259
+ return output
260
+
261
+
262
+ class ChordRecognitionModel(nn.Module):
263
+ def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
264
+ super().__init__()
265
+ self.config = model_config
266
+ self.vocab_sizes = vocab_sizes
267
+ self.d_model = self.config['d_model']
268
+ # Encoder: shared patch embedding and relative transformer (unchanged)
269
+ self.embedding = PatchEmbedding(
270
+ d_model=self.d_model,
271
+ frames_per_patch=self.config['frames_per_patch'],
272
+ expansion=2,
273
+ )
274
+
275
+ self.input_dropout = nn.Dropout(self.config['dropout'])
276
+
277
+ rpb = RelativePositionBias(
278
+ num_heads=self.config['n_head'],
279
+ max_distance=self.config['n_beats'] * self.config['label_resolution']
280
+ )
281
+ self.relative_transformer_encoder = RelativeTransformerEncoder(
282
+ num_layers=self.config['num_encoder_layers'],
283
+ d_model=self.d_model,
284
+ nhead=self.config['n_head'],
285
+ dim_feedforward=self.config['dim_feedforward'],
286
+ dropout=self.config['dropout'],
287
+ activation='gelu',
288
+ relative_position_bias=rpb,
289
+ )
290
+
291
+ # Boundary head, smoother, and FiLM gating
292
+ d_b = max(1, self.d_model // 4)
293
+ k_b = int(self.config.get('boundary_kernel', 5))
294
+ self.boundary_head = nn.Linear(self.d_model, 1)
295
+ self.boundary_smoother = nn.Conv1d(
296
+ in_channels=1,
297
+ out_channels=1,
298
+ kernel_size=k_b,
299
+ padding=k_b // 2,
300
+ groups=1,
301
+ bias=True,
302
+ )
303
+ self.boundary_e0 = nn.Parameter(torch.zeros(d_b))
304
+ self.boundary_e1 = nn.Parameter(torch.randn(d_b) * 0.02)
305
+ # Optional key context (3-part setting). Infer size from vocab_sizes if provided
306
+ self.Vq = int(vocab_sizes.get('quality', 0))
307
+ self.Vr = int(vocab_sizes.get('root', 0))
308
+ self.Vb = int(vocab_sizes.get('bass', 0))
309
+
310
+ # FiLM layers take boundary embedding
311
+ self.film_ln_in = nn.LayerNorm(self.d_model + d_b)
312
+ self.film_ln_h = nn.LayerNorm(self.d_model)
313
+ self.film_mlp = nn.Linear(self.d_model + d_b, 2 * self.d_model)
314
+
315
+ # Triple-token decoder: embeddings and heads
316
+ self.mask_id_q = int(self.config.get('mask_id_q', self.Vq))
317
+ self.mask_id_r = int(self.config.get('mask_id_r', self.Vr))
318
+ self.mask_id_b = int(self.config.get('mask_id_b', self.Vb))
319
+ self.emb_q = nn.Embedding(self.Vq + 1, self.d_model)
320
+ self.emb_r = nn.Embedding(self.Vr + 1, self.d_model)
321
+ self.emb_b = nn.Embedding(self.Vb + 1, self.d_model)
322
+
323
+ dec_heads = int(self.config.get('dec_heads', 4))
324
+ dec_mlp_ratio = int(self.config.get('dec_mlp_ratio', 4))
325
+ dec_layers = int(self.config.get('dec_layers', 1))
326
+ dec_dropout = float(self.config.get('dec_dropout', 0.1))
327
+ self.window_radius = int(self.config.get('window_radius', 2))
328
+ self.decoder_layers = nn.ModuleList([
329
+ KTokenDecoderLayer(
330
+ d_model=self.d_model,
331
+ nhead=dec_heads,
332
+ mlp_ratio=dec_mlp_ratio,
333
+ dropout=dec_dropout,
334
+ ) for _ in range(dec_layers)
335
+ ])
336
+ self.dec_norm = nn.LayerNorm(self.d_model)
337
+ self.head_q = nn.Linear(self.d_model, self.Vq)
338
+ self.head_r = nn.Linear(self.d_model, self.Vr)
339
+ self.head_b = nn.Linear(self.d_model, self.Vb)
340
+
341
+ # Legacy decompose projection for compatibility when training in decompose mode
342
+ self.chord_decompose_projection = ChordDecomposeProjection(self.d_model, self.vocab_sizes)
343
+
344
+ def forward(self, encoder_input: torch.Tensor,
345
+ src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
346
+ H, _ = self._encode(encoder_input, src_key_padding_mask)
347
+ out = self.chord_decompose_projection(H)
348
+ return out
349
+
350
+ # ===== Utilities =====
351
+ def _encode(self, encoder_input: torch.Tensor,
352
+ src_key_padding_mask: Optional[torch.Tensor]) -> (torch.Tensor, torch.Tensor):
353
+ x = self.embedding(encoder_input)
354
+ mask_down = None
355
+ if src_key_padding_mask is not None:
356
+ mask_down = downsample_key_padding_mask(src_key_padding_mask, self.config['frames_per_patch'])
357
+ x = self.input_dropout(x)
358
+ H = self.relative_transformer_encoder(x, src_key_padding_mask=mask_down)
359
+ boundary_logits = self.boundary_head(H).squeeze(-1)
360
+ return H, boundary_logits
361
+
362
+ def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
363
+ b = boundary_logits.unsqueeze(1)
364
+ smoothed = self.boundary_smoother(b)
365
+ return torch.sigmoid(smoothed.squeeze(1))
366
+
367
+ def _apply_film(self, H: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
368
+ B, T, D = H.shape
369
+ e0 = self.boundary_e0.view(1, 1, -1).expand(B, T, -1)
370
+ e1 = self.boundary_e1.view(1, 1, -1).expand(B, T, -1)
371
+ # soft embedding for boundary
372
+ eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
373
+ film_in = torch.cat([H, eb], dim=-1)
374
+
375
+ # layer norm and linear projection
376
+ film_in = self.film_ln_in(film_in)
377
+ gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
378
+ Z = self.film_ln_h(H) * (1.0 + gamma) + beta
379
+ return Z
380
+
381
+ def _build_local_windows(self, H: torch.Tensor, radius: int) -> torch.Tensor:
382
+ x = H.transpose(1, 2)
383
+ padded = F.pad(x, (radius, radius), mode='replicate')
384
+ win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
385
+ win = win.permute(0, 2, 3, 1).contiguous()
386
+ return win
387
+
388
+ def _build_context(self, H: torch.Tensor, Z: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
389
+ local = self._build_local_windows(H, self.window_radius)
390
+ z = Z.unsqueeze(2)
391
+ parts = [z, local]
392
+ C = torch.cat(parts, dim=2)
393
+ return C
394
+
395
+ def _embed_tokens(self, ids_q: torch.Tensor, ids_r: torch.Tensor, ids_b: torch.Tensor) -> torch.Tensor:
396
+ xq = self.emb_q(ids_q)
397
+ xr = self.emb_r(ids_r)
398
+ xb = self.emb_b(ids_b)
399
+ X = torch.stack([xq, xr, xb], dim=2)
400
+ return X
401
+
402
+ def _run_decoder(self, X: torch.Tensor, C: torch.Tensor):
403
+ x = X
404
+ for layer in self.decoder_layers:
405
+ x = layer(x, C)
406
+ x = self.dec_norm(x)
407
+ xq = x[:, :, 0, :]
408
+ xr = x[:, :, 1, :]
409
+ xb = x[:, :, 2, :]
410
+ logits_q = self.head_q(xq)
411
+ logits_r = self.head_r(xr)
412
+ logits_b = self.head_b(xb)
413
+ return logits_q, logits_r, logits_b
414
+
415
+ # ===== Training forward =====
416
+ def forward_train(self, encoder_input: torch.Tensor,
417
+ targets: Dict[str, torch.Tensor],
418
+ src_key_padding_mask: Optional[torch.Tensor] = None,
419
+ target_mask: Optional[torch.Tensor] = None) -> Dict[str, Any]:
420
+ device = encoder_input.device
421
+ H, boundary_logits = self._encode(encoder_input, src_key_padding_mask)
422
+ # No key prediction/context in training
423
+ b_soft = self._smooth_boundary(boundary_logits)
424
+ Z = self._apply_film(H, b_soft)
425
+ C = self._build_context(H, Z, b_soft)
426
+
427
+ tgt_q = targets['quality']
428
+ tgt_r = targets['root']
429
+ tgt_b = targets['bass']
430
+ B, T = tgt_q.shape
431
+ if target_mask is None:
432
+ target_mask = torch.ones(B, T, dtype=torch.bool, device=device)
433
+
434
+ # mask n slots randomly per (B,T) across 3 slots [q,r,b]
435
+ k_rand = torch.randint(1, 4, (B, T), device=device)
436
+ rand_scores = torch.rand(B, T, 3, device=device)
437
+ top_vals, top_idx = torch.topk(rand_scores, k=3, dim=-1)
438
+ mask_slots = torch.zeros(B, T, 3, dtype=torch.bool, device=device)
439
+ # enable first k indices per position
440
+ for kk in range(1, 4):
441
+ sel = (k_rand == kk)
442
+ if sel.any():
443
+ idx_sel = top_idx[sel][:, :kk]
444
+ row = mask_slots[sel]
445
+ if idx_sel.numel() > 0:
446
+ row.scatter_(dim=1, index=idx_sel, value=True)
447
+ mask_slots[sel] = row
448
+
449
+ ids_q = tgt_q.clone()
450
+ ids_r = tgt_r.clone()
451
+ ids_b = tgt_b.clone()
452
+ ids_q[mask_slots[:, :, 0]] = self.mask_id_q
453
+ ids_r[mask_slots[:, :, 1]] = self.mask_id_r
454
+ ids_b[mask_slots[:, :, 2]] = self.mask_id_b
455
+
456
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
457
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
458
+
459
+ def ce_masked(logits: Optional[torch.Tensor], target: torch.Tensor, slot_mask: torch.Tensor) -> torch.Tensor:
460
+ # Build supervision mask and safe targets to avoid CUDA asserts from out-of-range labels
461
+ m = slot_mask & target_mask # (B,T) supervised locations
462
+ num_classes = logits.size(-1)
463
+ safe_target = torch.where(
464
+ m,
465
+ target.clamp(min=0, max=num_classes - 1),
466
+ torch.zeros_like(target)
467
+ )
468
+ ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction='none')
469
+ denom = m.float().sum().clamp(min=1.0)
470
+ return (ce * m.float()).sum() / denom
471
+
472
+ loss_q = ce_masked(logits_q, tgt_q, mask_slots[:, :, 0])
473
+ loss_r = ce_masked(logits_r, tgt_r, mask_slots[:, :, 1])
474
+ loss_b = ce_masked(logits_b, tgt_b, mask_slots[:, :, 2])
475
+
476
+ bce = F.binary_cross_entropy_with_logits(boundary_logits,
477
+ targets['boundary'].to(boundary_logits.dtype),
478
+ pos_weight=torch.tensor(2.0, device=device),
479
+ reduction='none')
480
+ loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
481
+ total_loss = loss_q + loss_r + loss_b + loss_boundary * 3
482
+
483
+ with torch.no_grad():
484
+ stats = {}
485
+ for name, logits, target, m in [
486
+ ('quality', logits_q, tgt_q, mask_slots[:, :, 0]),
487
+ ('root', logits_r, tgt_r, mask_slots[:, :, 1]),
488
+ ('bass', logits_b, tgt_b, mask_slots[:, :, 2]),
489
+ ]:
490
+ if logits is None:
491
+ stats[f'acc_{name}'] = 0.0
492
+ stats[f'conf_{name}'] = 0.0
493
+ stats[f'ece_{name}'] = 0.0
494
+ else:
495
+ pred = logits.argmax(dim=-1)
496
+ sel = (m & target_mask)
497
+ denom = sel.float().sum().clamp(min=1.0)
498
+ acc = (pred[sel] == target[sel]).float().sum() / denom
499
+ prob = logits.float().softmax(dim=-1)
500
+ conf = prob.max(dim=-1).values
501
+ mean_conf = conf[sel].sum() / denom
502
+ # simple ECE
503
+ ece = torch.tensor(0.0, device=device)
504
+ bins = torch.linspace(0, 1, steps=11, device=device)
505
+ conf_flat = conf[sel]
506
+ pred_flat = pred[sel]
507
+ tgt_flat = target[sel]
508
+ for i in range(10):
509
+ lo, hi = bins[i], bins[i+1]
510
+ mask_bin = (conf_flat >= lo) & (conf_flat < hi if i < 9 else conf_flat <= hi)
511
+ if mask_bin.sum() > 0:
512
+ acc_bin = (pred_flat[mask_bin] == tgt_flat[mask_bin]).float().mean()
513
+ conf_bin = conf_flat[mask_bin].mean()
514
+ ece = ece + (mask_bin.float().mean() * (acc_bin - conf_bin).abs())
515
+ stats[f'acc_{name}'] = acc.item()
516
+ stats[f'conf_{name}'] = mean_conf.item()
517
+ stats[f'ece_{name}'] = ece.item()
518
+
519
+ return {
520
+ 'loss': total_loss,
521
+ 'loss_map': {
522
+ 'quality': loss_q,
523
+ 'root': loss_r,
524
+ 'bass': loss_b,
525
+ 'boundary': loss_boundary,
526
+ },
527
+ 'logits': {
528
+ 'quality': logits_q,
529
+ 'root': logits_r,
530
+ 'bass': logits_b,
531
+ },
532
+ 'mask_slots': mask_slots, # (B,T,3) bool in order [q,r,b]
533
+ 'boundary_logits': boundary_logits,
534
+ 'stats': stats,
535
+ }
536
+
537
+ # ===== Inference forward =====
538
+ def forward_infer(self, encoder_input: torch.Tensor,
539
+ src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
540
+ device = encoder_input.device
541
+ H, boundary_logits = self._encode(encoder_input, src_key_padding_mask)
542
+ # No key prediction/context in inference
543
+ b_soft = self._smooth_boundary(boundary_logits)
544
+ Z = self._apply_film(H, b_soft)
545
+ C = self._build_context(H, Z, b_soft)
546
+
547
+ B, T, _ = H.shape
548
+ ids_q = torch.full((B, T), self.mask_id_q, dtype=torch.long, device=device)
549
+ ids_r = torch.full((B, T), self.mask_id_r, dtype=torch.long, device=device)
550
+ ids_b = torch.full((B, T), self.mask_id_b, dtype=torch.long, device=device)
551
+ filled_q = torch.zeros((B, T), dtype=torch.bool, device=device)
552
+ filled_r = torch.zeros((B, T), dtype=torch.bool, device=device)
553
+ filled_b = torch.zeros((B, T), dtype=torch.bool, device=device)
554
+
555
+ # Track decode order per time step: 0=quality, 1=root, 2=bass
556
+ decode_order = torch.full((B, T, 3), -1, dtype=torch.long, device=device)
557
+ order_pos = 0
558
+
559
+ for step in (3, 2, 1):
560
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
561
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
562
+ pq = logits_q.softmax(dim=-1)
563
+ pr = logits_r.softmax(dim=-1)
564
+ pb = logits_b.softmax(dim=-1)
565
+ conf_q = pq.max(dim=-1).values
566
+ conf_r = pr.max(dim=-1).values
567
+ conf_b = pb.max(dim=-1).values
568
+ conf_q = conf_q.masked_fill(filled_q, float('-inf'))
569
+ conf_r = conf_r.masked_fill(filled_r, float('-inf'))
570
+ conf_b = conf_b.masked_fill(filled_b, float('-inf'))
571
+ conf = torch.stack([conf_q, conf_r, conf_b], dim=-1)
572
+ take_slot = conf.argmax(dim=-1)
573
+
574
+ # record order at this step
575
+ decode_order[:, :, order_pos] = take_slot
576
+ order_pos += 1
577
+
578
+ pred_q = logits_q.argmax(dim=-1)
579
+ commit_q = (take_slot == 0) | ((step == 1) & (~filled_q))
580
+ ids_q[commit_q] = pred_q[commit_q]
581
+ filled_q = filled_q | commit_q
582
+
583
+ pred_r = logits_r.argmax(dim=-1)
584
+ commit_r = (take_slot == 1) | ((step == 1) & (~filled_r))
585
+ ids_r[commit_r] = pred_r[commit_r]
586
+ filled_r = filled_r | commit_r
587
+
588
+ pred_b = logits_b.argmax(dim=-1)
589
+ commit_b = (take_slot == 2) | ((step == 1) & (~filled_b))
590
+ ids_b[commit_b] = pred_b[commit_b]
591
+ filled_b = filled_b | commit_b
592
+
593
+ # final confidences
594
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
595
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
596
+ conf_q = logits_q.softmax(dim=-1).max(dim=-1).values
597
+ conf_r = logits_r.softmax(dim=-1).max(dim=-1).values
598
+ conf_b = logits_b.softmax(dim=-1).max(dim=-1).values
599
+
600
+ return {
601
+ 'quality': ids_q,
602
+ 'root': ids_r,
603
+ 'bass': ids_b,
604
+ 'conf_quality': conf_q,
605
+ 'conf_root': conf_r,
606
+ 'conf_bass': conf_b,
607
+ 'boundary': boundary_logits,
608
+ 'decode_order': decode_order,
609
+ }
610
+
611
+
612
+ class KTokenDecoderLayer(nn.Module):
613
+ def __init__(self, d_model: int, nhead: int, mlp_ratio: int, dropout: float):
614
+ super().__init__()
615
+ self.d_model = d_model
616
+ self.nhead = nhead
617
+ self.head_dim = d_model // nhead
618
+ if self.head_dim * nhead != d_model:
619
+ raise ValueError("d_model must be divisible by nhead")
620
+
621
+ # self-attention over K tokens
622
+ self.sa_qkv = nn.Linear(d_model, 3 * d_model)
623
+ self.sa_out = nn.Linear(d_model, d_model)
624
+ self.sa_ln = nn.LayerNorm(d_model)
625
+ self.sa_drop = nn.Dropout(dropout)
626
+
627
+ # cross-attention to context
628
+ self.ca_q = nn.Linear(d_model, d_model)
629
+ self.ca_kv = nn.Linear(d_model, 2 * d_model)
630
+ self.ca_out = nn.Linear(d_model, d_model)
631
+ self.ca_ln = nn.LayerNorm(d_model)
632
+ self.ca_drop = nn.Dropout(dropout)
633
+
634
+ # ffn
635
+ hidden = d_model * mlp_ratio
636
+ self.ff_ln = nn.LayerNorm(d_model)
637
+ self.ff = nn.Sequential(
638
+ nn.Linear(d_model, hidden),
639
+ nn.GELU(),
640
+ nn.Dropout(dropout),
641
+ nn.Linear(hidden, d_model),
642
+ )
643
+ self.ff_drop = nn.Dropout(dropout)
644
+
645
+ def _attn(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
646
+ # q,k,v: (N, L, H, D)
647
+ attn = torch.einsum('nlhd,nshd->nhls', q, k) / math.sqrt(q.size(-1)) # (N,H,L,S)
648
+ attn = torch.softmax(attn, dim=-1)
649
+ ctx = torch.einsum('nhls,nshd->nlhd', attn, v) # (N,L,H,D)
650
+ ctx = ctx.contiguous().view(q.size(0), q.size(1), -1) # (N,L,C)
651
+ return ctx
652
+
653
+ def forward(self, X: torch.Tensor, C: torch.Tensor) -> torch.Tensor:
654
+ # X: (B,T,K,D), C: (B,T,Lc,D)
655
+ B, T, K, D = X.shape
656
+ Lc = C.size(2)
657
+ N = B * T
658
+ # reshape
659
+ x = X.view(N, K, D)
660
+ c = C.view(N, Lc, D)
661
+
662
+ # self-attn (over K)
663
+ x_norm = self.sa_ln(x)
664
+ qkv = self.sa_qkv(x_norm)
665
+ q, k, v = qkv.chunk(3, dim=-1)
666
+ q = q.view(N, K, self.nhead, self.head_dim)
667
+ k = k.view(N, K, self.nhead, self.head_dim)
668
+ v = v.view(N, K, self.nhead, self.head_dim)
669
+ sa_ctx = self._attn(q, k, v) # (N,K,C)
670
+ x = x + self.sa_drop(self.sa_out(sa_ctx))
671
+
672
+ # cross-attn (queries = tokens, keys/values = context)
673
+ x_norm = self.ca_ln(x)
674
+ q = self.ca_q(x_norm).view(N, K, self.nhead, self.head_dim)
675
+ kv = self.ca_kv(c)
676
+ k, v = kv.chunk(2, dim=-1)
677
+ k = k.view(N, Lc, self.nhead, self.head_dim)
678
+ v = v.view(N, Lc, self.nhead, self.head_dim)
679
+ ca_ctx = self._attn(q, k, v) # (N,K,C)
680
+ x = x + self.ca_drop(self.ca_out(ca_ctx))
681
+
682
+ # ffn
683
+ x_norm = self.ff_ln(x)
684
+ x = x + self.ff_drop(self.ff(x_norm))
685
+
686
+ return x.view(B, T, K, D)
models/variants.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Dict, Any, Optional, Tuple
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+
8
+ from .components import (
9
+ downsample_key_padding_mask,
10
+ KTokenDecoderLayer,
11
+ ChordProjectionHead,
12
+ build_encoder,
13
+ build_patch_embedding,
14
+ )
15
+
16
+
17
+ def _to_patch_input(x_bt88: torch.Tensor) -> torch.Tensor:
18
+ # Accepts (B, T, 88) -> returns (B, 1, 88, T)
19
+ return x_bt88.transpose(1, 2).unsqueeze(1).contiguous()
20
+
21
+ class BaseEncoder(nn.Module):
22
+ def __init__(self, model_config: Dict[str, Any]):
23
+ super().__init__()
24
+ self.config = model_config
25
+ self.d_model = self.config["d_model"]
26
+ self.embedding = build_patch_embedding(self.config)
27
+ self.input_dropout = nn.Dropout(self.config["dropout"])
28
+ self.encoder = build_encoder(self.config)
29
+ self.boundary_head = nn.Linear(self.d_model, 1)
30
+
31
+ def encode(self, encoder_input_bt88: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
32
+ x_pr = _to_patch_input(encoder_input_bt88)
33
+ x = self.embedding(x_pr)
34
+ mask_down = None
35
+ if src_key_padding_mask is not None:
36
+ mask_down = downsample_key_padding_mask(src_key_padding_mask, self.config["frames_per_patch"])
37
+ x = self.input_dropout(x)
38
+ h = self.encoder(x, src_key_padding_mask=mask_down)
39
+ boundary_logits = self.boundary_head(h).squeeze(-1)
40
+ return h, boundary_logits
41
+
42
+
43
+ class BaselineLinearModel(nn.Module):
44
+ """PatchEmbedding + TransformerEncoder + linear heads (baseline)."""
45
+
46
+ def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
47
+ super().__init__()
48
+ self.config = model_config
49
+ self.encoder = BaseEncoder(model_config)
50
+ self.proj = ChordProjectionHead(model_config["d_model"], vocab_sizes)
51
+ self.use_key = ('key' in vocab_sizes)
52
+
53
+ def forward_train(
54
+ self,
55
+ encoder_input: torch.Tensor,
56
+ targets: Dict[str, torch.Tensor],
57
+ src_key_padding_mask: Optional[torch.Tensor] = None,
58
+ target_mask: Optional[torch.Tensor] = None,
59
+ vocabs: Optional[Dict[str, Any]] = None,
60
+ ) -> Dict[str, Any]:
61
+ device = encoder_input.device
62
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
63
+ outputs = self.proj(h)
64
+
65
+ bsz, t_len = targets["quality"].shape
66
+ if target_mask is None:
67
+ target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
68
+
69
+ def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
70
+ num_classes = logits.size(-1)
71
+ comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
72
+ valid = mask & (target != comp_pad)
73
+ safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
74
+ ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
75
+ denom = valid.float().sum().clamp(min=1.0)
76
+ return (ce * valid.float()).sum() / denom
77
+
78
+ loss_q = ce_masked(outputs["quality"], targets["quality"], target_mask, "quality")
79
+ loss_r = ce_masked(outputs["root"], targets["root"], target_mask, "root")
80
+ loss_b = ce_masked(outputs["bass"], targets["bass"], target_mask, "bass")
81
+ if self.use_key and ("key" in outputs) and ("key" in targets):
82
+ loss_k = ce_masked(outputs["key"], targets["key"], target_mask, "key")
83
+ else:
84
+ loss_k = torch.tensor(0.0, device=device)
85
+ bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
86
+ loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
87
+ total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
88
+
89
+ with torch.no_grad():
90
+ logits = {k: v for k, v in outputs.items() if k in ("quality", "root", "bass")}
91
+ return {
92
+ "loss": total_loss,
93
+ "loss_map": {
94
+ "quality": loss_q,
95
+ "root": loss_r,
96
+ "bass": loss_b,
97
+ "key": loss_k,
98
+ "boundary": loss_boundary,
99
+ },
100
+ "logits": logits,
101
+ "boundary_logits": boundary_logits,
102
+ }
103
+
104
+ def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
105
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
106
+ outputs = self.proj(h)
107
+ comps = ["quality","root","bass"] + (["key"] if self.use_key and ("key" in outputs) else [])
108
+ ids = {k: outputs[k].argmax(dim=-1) for k in comps}
109
+ conf = {f"conf_{k}": outputs[k].softmax(dim=-1).max(dim=-1).values for k in comps}
110
+ ids.update(conf)
111
+ ids["boundary"] = boundary_logits
112
+ return ids
113
+
114
+
115
+ class FiLMContextLinearModel(nn.Module):
116
+ """FiLM injection + local context window + per-component context projector (linear heads)."""
117
+
118
+ def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
119
+ super().__init__()
120
+ self.config = model_config
121
+ self.encoder = BaseEncoder(model_config)
122
+ d_model = model_config["d_model"]
123
+ self.window_radius = int(model_config["window_radius"]) # Lc = 2R+1 + 1(Z)
124
+
125
+ # FiLM parameters (copy of CR-model style)
126
+ self.d_b = max(1, d_model // 4)
127
+ k_b = int(model_config["boundary_kernel"])
128
+ self.boundary_smoother = nn.Conv1d(1, 1, kernel_size=k_b, padding=k_b // 2, bias=True)
129
+ self.boundary_e0 = nn.Parameter(torch.zeros(self.d_b))
130
+ self.boundary_e1 = nn.Parameter(torch.randn(self.d_b) * 0.02)
131
+
132
+ self.film_ln_in = nn.LayerNorm(d_model + self.d_b)
133
+ self.film_ln_h = nn.LayerNorm(d_model)
134
+ self.film_mlp = nn.Linear(d_model + self.d_b, 2 * d_model)
135
+
136
+ # Per-component context heads
137
+ self.comp_names = ["quality", "root", "bass"]
138
+ if 'key' in vocab_sizes:
139
+ self.comp_names.append('key')
140
+ self.vocab_sizes = vocab_sizes
141
+ self.attn_mlp = nn.ModuleDict()
142
+ self.comp_proj = nn.ModuleDict()
143
+ for comp in self.comp_names:
144
+ self.attn_mlp[comp] = nn.Sequential(
145
+ nn.Linear(model_config["d_model"], model_config["d_model"] // 2),
146
+ nn.GELU(),
147
+ nn.Linear(model_config["d_model"] // 2, 1), # score per context token
148
+ )
149
+ self.comp_proj[comp] = nn.Sequential(
150
+ nn.Linear(model_config["d_model"], model_config["d_model"]),
151
+ nn.GELU(),
152
+ nn.Linear(model_config["d_model"], self.vocab_sizes[comp]),
153
+ )
154
+
155
+ def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
156
+ b = boundary_logits.unsqueeze(1)
157
+ smoothed = self.boundary_smoother(b)
158
+ return torch.sigmoid(smoothed.squeeze(1))
159
+
160
+ def _apply_film(self, h: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
161
+ bsz, t_len, d_model = h.shape
162
+ e0 = self.boundary_e0.view(1, 1, -1).expand(bsz, t_len, -1)
163
+ e1 = self.boundary_e1.view(1, 1, -1).expand(bsz, t_len, -1)
164
+ eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
165
+ film_in = torch.cat([h, eb], dim=-1)
166
+ film_in = self.film_ln_in(film_in)
167
+ gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
168
+ z = self.film_ln_h(h) * (1.0 + gamma) + beta
169
+ return z
170
+
171
+ def _build_local_windows(self, h: torch.Tensor, radius: int) -> torch.Tensor:
172
+ x = h.transpose(1, 2)
173
+ padded = F.pad(x, (radius, radius), mode="replicate")
174
+ win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
175
+ win = win.permute(0, 2, 3, 1).contiguous()
176
+ return win
177
+
178
+ def _build_context(self, h: torch.Tensor, z: torch.Tensor, b_soft: torch.Tensor) -> torch.Tensor:
179
+ local = self._build_local_windows(h, self.window_radius)
180
+ z_exp = z.unsqueeze(2) # (B,T,1,D)
181
+ c = torch.cat([z_exp, local], dim=2) # (B,T,Lc,D)
182
+ return c
183
+
184
+ def _context_logits(self, c: torch.Tensor, comp: str) -> torch.Tensor:
185
+ # c: (B,T,L,D)
186
+ scores = self.attn_mlp[comp](c) # (B,T,L,1)
187
+ attn = torch.softmax(scores, dim=2)
188
+ pooled = (attn * c).sum(dim=2) # (B,T,D)
189
+ logits = self.comp_proj[comp](pooled) # (B,T,V)
190
+ return logits
191
+
192
+ def forward_train(
193
+ self,
194
+ encoder_input: torch.Tensor,
195
+ targets: Dict[str, torch.Tensor],
196
+ src_key_padding_mask: Optional[torch.Tensor] = None,
197
+ target_mask: Optional[torch.Tensor] = None,
198
+ vocabs: Optional[Dict[str, Any]] = None,
199
+ ) -> Dict[str, Any]:
200
+ device = encoder_input.device
201
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
202
+ b_soft = self._smooth_boundary(boundary_logits)
203
+ z = self._apply_film(h, b_soft)
204
+ c = self._build_context(h, z, b_soft)
205
+
206
+ logits = {comp: self._context_logits(c, comp) for comp in self.comp_names}
207
+ bsz, t_len = targets["quality"].shape
208
+ if target_mask is None:
209
+ target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
210
+
211
+ def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
212
+ num_classes = logits.size(-1)
213
+ comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
214
+ valid = mask & (target != comp_pad)
215
+ safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
216
+ ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
217
+ denom = valid.float().sum().clamp(min=1.0)
218
+ return (ce * valid.float()).sum() / denom
219
+
220
+ loss_q = ce_masked(logits["quality"], targets["quality"], target_mask, "quality")
221
+ loss_r = ce_masked(logits["root"], targets["root"], target_mask, "root")
222
+ loss_b = ce_masked(logits["bass"], targets["bass"], target_mask, "bass")
223
+ loss_k = torch.tensor(0.0, device=device)
224
+ if 'key' in self.comp_names and ('key' in targets):
225
+ loss_k = ce_masked(logits['key'], targets['key'], target_mask, 'key')
226
+ bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
227
+ loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
228
+ total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
229
+
230
+ return {
231
+ "loss": total_loss,
232
+ "loss_map": {
233
+ "quality": loss_q,
234
+ "root": loss_r,
235
+ "bass": loss_b,
236
+ "key": loss_k,
237
+ "boundary": loss_boundary,
238
+ },
239
+ "logits": logits,
240
+ "boundary_logits": boundary_logits,
241
+ }
242
+
243
+ def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
244
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
245
+ b_soft = self._smooth_boundary(boundary_logits)
246
+ z = self._apply_film(h, b_soft)
247
+ c = self._build_context(h, z, b_soft)
248
+ logits = {comp: self._context_logits(c, comp) for comp in self.comp_names}
249
+ ids = {k: logits[k].argmax(dim=-1) for k in self.comp_names}
250
+ conf = {f"conf_{k}": logits[k].softmax(dim=-1).max(dim=-1).values for k in self.comp_names}
251
+ ids.update(conf)
252
+ ids["boundary"] = boundary_logits
253
+ return ids
254
+
255
+
256
+ class ChordRecognitionModelWrapper(nn.Module):
257
+ """A thin wrapper around scripts.model.ChordRecognitionModel to normalize inputs."""
258
+
259
+ def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
260
+ super().__init__()
261
+ from .model import ChordRecognitionModel as CR
262
+
263
+ self.inner = CR(model_config, vocab_sizes)
264
+
265
+ def forward_train(
266
+ self,
267
+ encoder_input: torch.Tensor,
268
+ targets: Dict[str, torch.Tensor],
269
+ src_key_padding_mask: Optional[torch.Tensor] = None,
270
+ target_mask: Optional[torch.Tensor] = None,
271
+ vocabs: Optional[Dict[str, Any]] = None,
272
+ ) -> Dict[str, Any]:
273
+ x = _to_patch_input(encoder_input)
274
+ return self.inner.forward_train(x, targets, src_key_padding_mask, target_mask)
275
+
276
+ def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
277
+ x = _to_patch_input(encoder_input)
278
+ return self.inner.forward_infer(x, src_key_padding_mask)
279
+
280
+
281
+ class FiLMKTokenKeyModel(nn.Module):
282
+ """FiLM + KToken decoder with key-conditioned FiLM injection (key used as auxiliary condition only)."""
283
+
284
+ def __init__(self, model_config: Dict[str, Any], vocab_sizes: Dict[str, int], key_vocab_size: int):
285
+ super().__init__()
286
+ self.config = model_config
287
+ self.vocab_sizes = vocab_sizes
288
+ d_model = model_config["d_model"]
289
+ self.encoder = BaseEncoder(model_config)
290
+
291
+ # FiLM
292
+ self.d_b = max(1, d_model // 4)
293
+ k_b = int(model_config["boundary_kernel"])
294
+ self.boundary_smoother = nn.Conv1d(1, 1, kernel_size=k_b, padding=k_b // 2, bias=True)
295
+ self.boundary_e0 = nn.Parameter(torch.zeros(self.d_b))
296
+ self.boundary_e1 = nn.Parameter(torch.randn(self.d_b) * 0.02)
297
+
298
+ # Key soft embedding
299
+ self.d_k = max(1, d_model // 8)
300
+ self.key_embed = nn.Embedding(key_vocab_size, self.d_k)
301
+ self.key_head = nn.Linear(d_model, key_vocab_size)
302
+
303
+ # FiLM projection with concatenated boundary/key embeddings
304
+ self.film_ln_in = nn.LayerNorm(d_model + self.d_b + self.d_k)
305
+ self.film_ln_h = nn.LayerNorm(d_model)
306
+ self.film_mlp = nn.Linear(d_model + self.d_b + self.d_k, 2 * d_model)
307
+
308
+ # Decoder (same as CR model)
309
+ self.mask_id_q = self.vocab_sizes["quality"]
310
+ self.mask_id_r = self.vocab_sizes["root"]
311
+ self.mask_id_b = self.vocab_sizes["bass"]
312
+ self.emb_q = nn.Embedding(self.vocab_sizes["quality"] + 1, d_model)
313
+ self.emb_r = nn.Embedding(self.vocab_sizes["root"] + 1, d_model)
314
+ self.emb_b = nn.Embedding(self.vocab_sizes["bass"] + 1, d_model)
315
+
316
+ dec_heads = int(model_config["dec_heads"])
317
+ dec_mlp_ratio = int(model_config["dec_mlp_ratio"])
318
+ dec_layers = int(model_config["dec_layers"])
319
+ dec_dropout = float(model_config["dec_dropout"])
320
+ self.window_radius = int(model_config["window_radius"])
321
+ self.decoder_layers = nn.ModuleList(
322
+ [
323
+ KTokenDecoderLayer(
324
+ d_model=d_model,
325
+ nhead=dec_heads,
326
+ mlp_ratio=dec_mlp_ratio,
327
+ dropout=dec_dropout,
328
+ )
329
+ for _ in range(dec_layers)
330
+ ]
331
+ )
332
+ self.dec_norm = nn.LayerNorm(d_model)
333
+ self.head_q = nn.Linear(d_model, self.vocab_sizes["quality"])
334
+ self.head_r = nn.Linear(d_model, self.vocab_sizes["root"])
335
+ self.head_b = nn.Linear(d_model, self.vocab_sizes["bass"])
336
+ self.use_key = ("key" in self.vocab_sizes)
337
+
338
+ def _smooth_boundary(self, boundary_logits: torch.Tensor) -> torch.Tensor:
339
+ b = boundary_logits.unsqueeze(1)
340
+ smoothed = self.boundary_smoother(b)
341
+ return torch.sigmoid(smoothed.squeeze(1))
342
+
343
+ def _apply_film(self, h: torch.Tensor, b_soft: torch.Tensor, key_soft: torch.Tensor) -> torch.Tensor:
344
+ bsz, t_len, d_model = h.shape
345
+ # boundary embedding
346
+ e0 = self.boundary_e0.to(h.dtype).view(1, 1, -1).expand(bsz, t_len, -1)
347
+ e1 = self.boundary_e1.to(h.dtype).view(1, 1, -1).expand(bsz, t_len, -1)
348
+ eb = b_soft.unsqueeze(-1) * e1 + (1.0 - b_soft).unsqueeze(-1) * e0
349
+ # key soft embedding via expectation over embeddings
350
+ key_indices = torch.arange(self.key_embed.num_embeddings, device=h.device)
351
+ key_emb_table = self.key_embed(key_indices) # (V_k, d_k)
352
+ key_emb_table = key_emb_table.to(dtype=key_soft.dtype)
353
+ ek = key_soft @ key_emb_table # (B,T,d_k)
354
+
355
+ film_in = torch.cat([h, eb, ek], dim=-1)
356
+ film_in = self.film_ln_in(film_in)
357
+ gamma, beta = self.film_mlp(film_in).chunk(2, dim=-1)
358
+ z = self.film_ln_h(h) * (1.0 + gamma) + beta
359
+ return z
360
+
361
+ def _build_local_windows(self, h: torch.Tensor, radius: int) -> torch.Tensor:
362
+ x = h.transpose(1, 2)
363
+ padded = F.pad(x, (radius, radius), mode="replicate")
364
+ win = padded.unfold(dimension=2, size=2 * radius + 1, step=1)
365
+ win = win.permute(0, 2, 3, 1).contiguous()
366
+ return win
367
+
368
+ def _build_context(self, h: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
369
+ local = self._build_local_windows(h, self.window_radius)
370
+ z_exp = z.unsqueeze(2)
371
+ c = torch.cat([z_exp, local], dim=2)
372
+ return c
373
+
374
+ def _embed_tokens(self, ids_q: torch.Tensor, ids_r: torch.Tensor, ids_b: torch.Tensor) -> torch.Tensor:
375
+ xq = self.emb_q(ids_q)
376
+ xr = self.emb_r(ids_r)
377
+ xb = self.emb_b(ids_b)
378
+ x = torch.stack([xq, xr, xb], dim=2)
379
+ return x
380
+
381
+ def _run_decoder(self, X: torch.Tensor, C: torch.Tensor):
382
+ x = X
383
+ for layer in self.decoder_layers:
384
+ x = layer(x, C)
385
+ x = self.dec_norm(x)
386
+ xq = x[:, :, 0, :]
387
+ xr = x[:, :, 1, :]
388
+ xb = x[:, :, 2, :]
389
+ logits_q = self.head_q(xq)
390
+ logits_r = self.head_r(xr)
391
+ logits_b = self.head_b(xb)
392
+ return logits_q, logits_r, logits_b
393
+
394
+ def forward_train(
395
+ self,
396
+ encoder_input: torch.Tensor,
397
+ targets: Dict[str, torch.Tensor],
398
+ src_key_padding_mask: Optional[torch.Tensor] = None,
399
+ target_mask: Optional[torch.Tensor] = None,
400
+ vocabs: Optional[Dict[str, Any]] = None,
401
+ ) -> Dict[str, Any]:
402
+ device = encoder_input.device
403
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
404
+ b_soft = self._smooth_boundary(boundary_logits)
405
+
406
+ key_logits = self.key_head(h)
407
+ key_soft = key_logits.softmax(dim=-1)
408
+
409
+ z = self._apply_film(h, b_soft, key_soft)
410
+ C = self._build_context(h, z)
411
+
412
+ tgt_q = targets["quality"]
413
+ tgt_r = targets["root"]
414
+ tgt_b = targets["bass"]
415
+ bsz, t_len = tgt_q.shape
416
+ if target_mask is None:
417
+ target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
418
+
419
+ # Random mask-filling per CR model
420
+ k_rand = torch.randint(1, 4, (bsz, t_len), device=device)
421
+ rand_scores = torch.rand(bsz, t_len, 3, device=device)
422
+ top_vals, top_idx = torch.topk(rand_scores, k=3, dim=-1)
423
+ mask_slots = torch.zeros(bsz, t_len, 3, dtype=torch.bool, device=device)
424
+ for kk in range(1, 4):
425
+ sel = (k_rand == kk)
426
+ if sel.any():
427
+ idx_sel = top_idx[sel][:, :kk]
428
+ row = mask_slots[sel]
429
+ if idx_sel.numel() > 0:
430
+ row.scatter_(dim=1, index=idx_sel, value=True)
431
+ mask_slots[sel] = row
432
+
433
+ ids_q = tgt_q.clone()
434
+ ids_r = tgt_r.clone()
435
+ ids_b = tgt_b.clone()
436
+ ids_q[mask_slots[:, :, 0]] = self.mask_id_q
437
+ ids_r[mask_slots[:, :, 1]] = self.mask_id_r
438
+ ids_b[mask_slots[:, :, 2]] = self.mask_id_b
439
+
440
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
441
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
442
+
443
+ def ce_masked(logits: torch.Tensor, target: torch.Tensor, slot_mask: torch.Tensor) -> torch.Tensor:
444
+ m = slot_mask & target_mask
445
+ num_classes = logits.size(-1)
446
+ safe_target = torch.where(m, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
447
+ ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
448
+ denom = m.float().sum().clamp(min=1.0)
449
+ return (ce * m.float()).sum() / denom
450
+
451
+ loss_q = ce_masked(logits_q, tgt_q, mask_slots[:, :, 0])
452
+ loss_r = ce_masked(logits_r, tgt_r, mask_slots[:, :, 1])
453
+ loss_b = ce_masked(logits_b, tgt_b, mask_slots[:, :, 2])
454
+
455
+ # Optional key cross-entropy (auxiliary, weight 1.0)
456
+ loss_k = torch.tensor(0.0, device=device)
457
+ if self.use_key and ("key" in targets):
458
+ key_logits = self.key_head(h)
459
+ num_classes_k = key_logits.size(-1)
460
+ safe_k = targets["key"].clamp(min=0, max=num_classes_k - 1)
461
+ loss_k = F.cross_entropy(key_logits.transpose(1, 2), safe_k, reduction="none")
462
+ denom_k = target_mask.float().sum().clamp(min=1.0)
463
+ loss_k = (loss_k * target_mask.float()).sum() / denom_k
464
+
465
+ bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
466
+ loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
467
+ total_loss = loss_q + loss_r + loss_b + loss_k + loss_boundary * 3.0
468
+
469
+ return {
470
+ "loss": total_loss,
471
+ "loss_map": {
472
+ "quality": loss_q,
473
+ "root": loss_r,
474
+ "bass": loss_b,
475
+ "key": loss_k,
476
+ "boundary": loss_boundary,
477
+ },
478
+ "logits": {
479
+ "quality": logits_q,
480
+ "root": logits_r,
481
+ "bass": logits_b,
482
+ },
483
+ "mask_slots": mask_slots,
484
+ "boundary_logits": boundary_logits,
485
+ }
486
+
487
+ def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
488
+ device = encoder_input.device
489
+ h, boundary_logits = self.encoder.encode(encoder_input, src_key_padding_mask)
490
+ b_soft = self._smooth_boundary(boundary_logits)
491
+ key_logits = self.key_head(h)
492
+ key_soft = key_logits.softmax(dim=-1)
493
+ z = self._apply_film(h, b_soft, key_soft)
494
+ C = self._build_context(h, z)
495
+
496
+ bsz, t_len, _ = h.shape
497
+ ids_q = torch.full((bsz, t_len), self.mask_id_q, dtype=torch.long, device=device)
498
+ ids_r = torch.full((bsz, t_len), self.mask_id_r, dtype=torch.long, device=device)
499
+ ids_b = torch.full((bsz, t_len), self.mask_id_b, dtype=torch.long, device=device)
500
+ filled_q = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
501
+ filled_r = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
502
+ filled_b = torch.zeros((bsz, t_len), dtype=torch.bool, device=device)
503
+
504
+ for step in (3, 2, 1):
505
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
506
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
507
+ pq = logits_q.softmax(dim=-1)
508
+ pr = logits_r.softmax(dim=-1)
509
+ pb = logits_b.softmax(dim=-1)
510
+ conf_q = pq.max(dim=-1).values
511
+ conf_r = pr.max(dim=-1).values
512
+ conf_b = pb.max(dim=-1).values
513
+ conf_q = conf_q.masked_fill(filled_q, float("-inf"))
514
+ conf_r = conf_r.masked_fill(filled_r, float("-inf"))
515
+ conf_b = conf_b.masked_fill(filled_b, float("-inf"))
516
+ conf = torch.stack([conf_q, conf_r, conf_b], dim=-1)
517
+ take_slot = conf.argmax(dim=-1)
518
+
519
+ pred_q = logits_q.argmax(dim=-1)
520
+ commit_q = (take_slot == 0) | ((step == 1) & (~filled_q))
521
+ ids_q[commit_q] = pred_q[commit_q]
522
+ filled_q = filled_q | commit_q
523
+
524
+ pred_r = logits_r.argmax(dim=-1)
525
+ commit_r = (take_slot == 1) | ((step == 1) & (~filled_r))
526
+ ids_r[commit_r] = pred_r[commit_r]
527
+ filled_r = filled_r | commit_r
528
+
529
+ pred_b = logits_b.argmax(dim=-1)
530
+ commit_b = (take_slot == 2) | ((step == 1) & (~filled_b))
531
+ ids_b[commit_b] = pred_b[commit_b]
532
+ filled_b = filled_b | commit_b
533
+
534
+ X = self._embed_tokens(ids_q, ids_r, ids_b)
535
+ logits_q, logits_r, logits_b = self._run_decoder(X, C)
536
+ conf_q = logits_q.softmax(dim=-1).max(dim=-1).values
537
+ conf_r = logits_r.softmax(dim=-1).max(dim=-1).values
538
+ conf_b = logits_b.softmax(dim=-1).max(dim=-1).values
539
+ return {
540
+ "quality": ids_q,
541
+ "root": ids_r,
542
+ "bass": ids_b,
543
+ "conf_quality": conf_q,
544
+ "conf_root": conf_r,
545
+ "conf_bass": conf_b,
546
+ "boundary": boundary_logits,
547
+ }
548
+
549
+
550
+ class HTAdapter(nn.Module):
551
+ """Adapter to unify HT with the common training/eval interface."""
552
+
553
+ def __init__(self, ht_config: Dict[str, Any], vocab_sizes: Dict[str, int]):
554
+ super().__init__()
555
+ from .HT import HarmonyTransformer
556
+
557
+ self.inner = HarmonyTransformer(ht_config, vocab_sizes)
558
+ self.vocab_sizes = vocab_sizes
559
+
560
+ def forward_train(
561
+ self,
562
+ encoder_input: torch.Tensor,
563
+ targets: Dict[str, torch.Tensor],
564
+ src_key_padding_mask: Optional[torch.Tensor] = None,
565
+ target_mask: Optional[torch.Tensor] = None,
566
+ vocabs: Optional[Dict[str, Any]] = None,
567
+ ) -> Dict[str, Any]:
568
+ device = encoder_input.device
569
+ out = self.inner(encoder_input, src_key_padding_mask)
570
+ logits_q = out["quality"]
571
+ logits_r = out["root"]
572
+ logits_b = out["bass"]
573
+ boundary_logits = out["boundary"].squeeze(-1)
574
+
575
+ bsz, t_len, _ = logits_q.shape
576
+ if target_mask is None:
577
+ target_mask = torch.ones(bsz, t_len, dtype=torch.bool, device=device)
578
+
579
+ def ce_masked(logits: torch.Tensor, target: torch.Tensor, mask: torch.Tensor, comp_name: str) -> torch.Tensor:
580
+ num_classes = logits.size(-1)
581
+ comp_pad = vocabs.get(f"{comp_name}_pad_idx", vocabs["pad_idx"]) if vocabs is not None else 0
582
+ valid = mask & (target != comp_pad)
583
+ safe_target = torch.where(valid, target.clamp(min=0, max=num_classes - 1), torch.zeros_like(target))
584
+ ce = F.cross_entropy(logits.transpose(1, 2), safe_target, reduction="none")
585
+ denom = valid.float().sum().clamp(min=1.0)
586
+ return (ce * valid.float()).sum() / denom
587
+
588
+ loss_q = ce_masked(logits_q, targets["quality"], target_mask, "quality")
589
+ loss_r = ce_masked(logits_r, targets["root"], target_mask, "root")
590
+ loss_b = ce_masked(logits_b, targets["bass"], target_mask, "bass")
591
+ bce = F.binary_cross_entropy_with_logits(boundary_logits, targets["boundary"].to(boundary_logits.dtype), reduction="none")
592
+ loss_boundary = (bce * target_mask.float()).sum() / target_mask.float().sum().clamp(min=1.0)
593
+ total_loss = loss_q + loss_r + loss_b + 3.0 * loss_boundary
594
+
595
+ return {
596
+ "loss": total_loss,
597
+ "loss_map": {
598
+ "quality": loss_q,
599
+ "root": loss_r,
600
+ "bass": loss_b,
601
+ "boundary": loss_boundary,
602
+ },
603
+ "logits": {
604
+ "quality": logits_q,
605
+ "root": logits_r,
606
+ "bass": logits_b,
607
+ },
608
+ "boundary_logits": boundary_logits,
609
+ }
610
+
611
+ def forward_infer(self, encoder_input: torch.Tensor, src_key_padding_mask: Optional[torch.Tensor] = None) -> Dict[str, torch.Tensor]:
612
+ out = self.inner(encoder_input, src_key_padding_mask)
613
+ ids = {
614
+ "quality": out["quality"].argmax(dim=-1),
615
+ "root": out["root"].argmax(dim=-1),
616
+ "bass": out["bass"].argmax(dim=-1),
617
+ "boundary": out["boundary"].squeeze(-1),
618
+ }
619
+ return ids
620
+
621
+
622
+ def build_model(experiment: str, model_config: Dict[str, Any], vocabs: Dict[str, Any], use_key: bool = False) -> nn.Module:
623
+ # Build vocab sizes for components (include key optionally)
624
+ chord_components = ["root", "quality", "bass"] + (["key"] if use_key and ("key" in vocabs) else [])
625
+ vocab_sizes = {comp: len(vocabs[comp]) for comp in chord_components}
626
+
627
+ exp = experiment.lower()
628
+ if exp == "baseline":
629
+ return BaselineLinearModel(model_config, vocab_sizes)
630
+ if exp == "film_ctx":
631
+ return FiLMContextLinearModel(model_config, vocab_sizes)
632
+ if exp == "film_kdec":
633
+ return ChordRecognitionModelWrapper(model_config, vocab_sizes)
634
+ if exp == "film_kdec_key":
635
+ key_vocab_size = len(vocabs["key"]) if ("key" in vocabs) else 24
636
+ return FiLMKTokenKeyModel(model_config, vocab_sizes, key_vocab_size)
637
+ if exp == "ht":
638
+ # The HT config expects specific keys; reuse model_config where possible
639
+ ht_cfg = {
640
+ "input_size": model_config["input_size"],
641
+ "d_model": model_config["d_model"],
642
+ "n_layers": model_config["num_encoder_layers"],
643
+ "n_heads": model_config["n_head"],
644
+ "dropout": model_config["dropout"],
645
+ "train_boundary": True,
646
+ "slope": 1.0,
647
+ "n_beats": model_config["n_beats"],
648
+ "beat_resolution": model_config["beat_resolution"],
649
+ }
650
+ return HTAdapter(ht_cfg, vocab_sizes)
651
+
652
+ raise ValueError(f"Unknown experiment '{experiment}'")
653
+
654
+
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0
2
+ gradio==5.28.0
3
+ torch
4
+ torchaudio
5
+ numpy
6
+ pyyaml
7
+ music21
8
+ miditoolkit
9
+ huggingface_hub
10
+ tqdm