Text Generation
PEFT
Safetensors
English
music
music-generation
chord-generation
symbolic-music
music-transformer
lora
rock
Instructions to use PearlLeeStudio/TheArtist-MusicTransformer-lora-rock with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use PearlLeeStudio/TheArtist-MusicTransformer-lora-rock with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Initial release: TheArtist genre LoRA adapter (F1 base)
Browse files- README.md +97 -0
- adapter_config.json +47 -0
- adapter_model.safetensors +3 -0
- config.json +31 -0
- embedding_extension.pt +3 -0
- model.py +294 -0
- tokenizer.json +364 -0
- tokenizer.py +388 -0
README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
library_name: peft
|
| 4 |
+
base_model: PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80
|
| 5 |
+
tags:
|
| 6 |
+
- music
|
| 7 |
+
- music-generation
|
| 8 |
+
- chord-generation
|
| 9 |
+
- symbolic-music
|
| 10 |
+
- music-transformer
|
| 11 |
+
- lora
|
| 12 |
+
- peft
|
| 13 |
+
- rock
|
| 14 |
+
language:
|
| 15 |
+
- en
|
| 16 |
+
pipeline_tag: text-generation
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# TheArtist Music Transformer — LoRA Adapter (Rock)
|
| 20 |
+
|
| 21 |
+
**LoRA adapter that conditions the F1 base (PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80) toward rock chord progressions.** One of eleven per-genre adapters released alongside the paper *Empirical Study of Pop and Jazz Mix Ratios for Genre-Adaptive Chord Generation* (Lee, 2026).
|
| 22 |
+
|
| 23 |
+
## Adapter summary
|
| 24 |
+
|
| 25 |
+
| Field | Value |
|
| 26 |
+
|---|---|
|
| 27 |
+
| Base model | `PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80` (F1, 25.6M params) |
|
| 28 |
+
| Adapter type | LoRA (Q/K/V projections) |
|
| 29 |
+
| LoRA rank | 8 |
|
| 30 |
+
| LoRA alpha | 16 |
|
| 31 |
+
| LoRA dropout | 0.05 |
|
| 32 |
+
| Target modules | `w_q`, `w_k`, `w_v` |
|
| 33 |
+
| Trainable parameters | ~200K (~0.78% of base) |
|
| 34 |
+
| Adapter file size | ~800 KB |
|
| 35 |
+
| Base vocabulary | 351 tokens (jazz/pop) |
|
| 36 |
+
| Vocabulary extension | +13 genre tokens (`embedding_extension.pt`) |
|
| 37 |
+
| Training epochs | 5 |
|
| 38 |
+
|
| 39 |
+
## Training data
|
| 40 |
+
|
| 41 |
+
48,910 chord-progression sequences in the **rock** subset of the [Chordonomicon dataset](https://huggingface.co/datasets/ailsntua/Chordonomicon). Chordonomicon is licensed CC BY-NC 4.0; see the dataset card for full terms.
|
| 42 |
+
|
| 43 |
+
## Genre character
|
| 44 |
+
|
| 45 |
+
Classic rock and modern rock harmony
|
| 46 |
+
|
| 47 |
+
## License and use
|
| 48 |
+
|
| 49 |
+
The adapter weights are released under **CC BY-NC 4.0** (matching Chordonomicon, the upstream training corpus). Permitted: research, paper replication, portfolio, demo. Not permitted: commercial deployment without separate licensing of upstream data.
|
| 50 |
+
|
| 51 |
+
## Usage
|
| 52 |
+
|
| 53 |
+
```python
|
| 54 |
+
import torch
|
| 55 |
+
from huggingface_hub import hf_hub_download
|
| 56 |
+
from peft import PeftModel
|
| 57 |
+
from model import MusicTransformer
|
| 58 |
+
from tokenizer import ChordTokenizer
|
| 59 |
+
|
| 60 |
+
# 1. Load the F1 base
|
| 61 |
+
base_path = hf_hub_download(
|
| 62 |
+
repo_id="PearlLeeStudio/TheArtist-MusicTransformer-ft-pop80",
|
| 63 |
+
filename="best.pt",
|
| 64 |
+
)
|
| 65 |
+
base_ckpt = torch.load(base_path, map_location="cpu", weights_only=False)
|
| 66 |
+
tokenizer = ChordTokenizer()
|
| 67 |
+
model = MusicTransformer(
|
| 68 |
+
vocab_size=tokenizer.vocab_size,
|
| 69 |
+
d_model=512, n_heads=8, d_ff=2048, n_layers=8,
|
| 70 |
+
max_seq_len=256, dropout=0.0, pad_id=tokenizer.pad_id,
|
| 71 |
+
)
|
| 72 |
+
model.load_state_dict(base_ckpt["model_state_dict"])
|
| 73 |
+
|
| 74 |
+
# 2. Extend the embedding to fit the LoRA's expanded vocabulary
|
| 75 |
+
ext_path = hf_hub_download(repo_id="PearlLeeStudio/TheArtist-MusicTransformer-lora-rock", filename="embedding_extension.pt")
|
| 76 |
+
ext = torch.load(ext_path, map_location="cpu", weights_only=False)
|
| 77 |
+
# (See model/README.md for the apply-extension recipe.)
|
| 78 |
+
|
| 79 |
+
# 3. Apply the LoRA adapter
|
| 80 |
+
adapter_dir = hf_hub_download(repo_id="PearlLeeStudio/TheArtist-MusicTransformer-lora-rock", filename="adapter_model.safetensors")
|
| 81 |
+
model = PeftModel.from_pretrained(model, adapter_dir.rsplit("/", 1)[0])
|
| 82 |
+
model.eval()
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## Citation
|
| 86 |
+
|
| 87 |
+
Preprint: [arXiv:2605.04998](https://arxiv.org/abs/2605.04998).
|
| 88 |
+
|
| 89 |
+
```bibtex
|
| 90 |
+
@misc{lee2026chordmix,
|
| 91 |
+
title = {Empirical Study of Pop and Jazz Mix Ratios for Genre-Adaptive Chord Generation},
|
| 92 |
+
author = {Lee, Jinju},
|
| 93 |
+
year = {2026},
|
| 94 |
+
eprint = {2605.04998},
|
| 95 |
+
archivePrefix = {arXiv}
|
| 96 |
+
}
|
| 97 |
+
```
|
adapter_config.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"alora_invocation_tokens": null,
|
| 3 |
+
"alpha_pattern": {},
|
| 4 |
+
"arrow_config": null,
|
| 5 |
+
"auto_mapping": {
|
| 6 |
+
"base_model_class": "MusicTransformer",
|
| 7 |
+
"parent_library": "model"
|
| 8 |
+
},
|
| 9 |
+
"base_model_name_or_path": null,
|
| 10 |
+
"bias": "none",
|
| 11 |
+
"corda_config": null,
|
| 12 |
+
"ensure_weight_tying": false,
|
| 13 |
+
"eva_config": null,
|
| 14 |
+
"exclude_modules": null,
|
| 15 |
+
"fan_in_fan_out": false,
|
| 16 |
+
"inference_mode": true,
|
| 17 |
+
"init_lora_weights": true,
|
| 18 |
+
"layer_replication": null,
|
| 19 |
+
"layers_pattern": null,
|
| 20 |
+
"layers_to_transform": null,
|
| 21 |
+
"loftq_config": {},
|
| 22 |
+
"lora_alpha": 16,
|
| 23 |
+
"lora_bias": false,
|
| 24 |
+
"lora_dropout": 0.05,
|
| 25 |
+
"lora_ga_config": null,
|
| 26 |
+
"megatron_config": null,
|
| 27 |
+
"megatron_core": "megatron.core",
|
| 28 |
+
"modules_to_save": null,
|
| 29 |
+
"peft_type": "LORA",
|
| 30 |
+
"peft_version": "0.19.1",
|
| 31 |
+
"qalora_group_size": 16,
|
| 32 |
+
"r": 8,
|
| 33 |
+
"rank_pattern": {},
|
| 34 |
+
"revision": null,
|
| 35 |
+
"target_modules": [
|
| 36 |
+
"w_k",
|
| 37 |
+
"w_q",
|
| 38 |
+
"w_v"
|
| 39 |
+
],
|
| 40 |
+
"target_parameters": null,
|
| 41 |
+
"task_type": null,
|
| 42 |
+
"trainable_token_indices": null,
|
| 43 |
+
"use_bdlora": null,
|
| 44 |
+
"use_dora": false,
|
| 45 |
+
"use_qalora": false,
|
| 46 |
+
"use_rslora": false
|
| 47 |
+
}
|
adapter_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8c200d36bce28e4083f5c38425b69ace08e0dcb19788d120162cf67850f28a65
|
| 3 |
+
size 791928
|
config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run_name": "ft_f1_lora_rock",
|
| 3 |
+
"base_checkpoint": "checkpoints/ft_jazz_pop80/best.pt",
|
| 4 |
+
"target_genre": "rock",
|
| 5 |
+
"sources": [
|
| 6 |
+
"chordonomicon_rock"
|
| 7 |
+
],
|
| 8 |
+
"d_model": 512,
|
| 9 |
+
"n_heads": 8,
|
| 10 |
+
"d_ff": 2048,
|
| 11 |
+
"n_layers": 8,
|
| 12 |
+
"max_seq_len": 256,
|
| 13 |
+
"dropout": 0.1,
|
| 14 |
+
"lora_r": 8,
|
| 15 |
+
"lora_alpha": 16,
|
| 16 |
+
"lora_dropout": 0.05,
|
| 17 |
+
"lora_target_modules": [
|
| 18 |
+
"w_q",
|
| 19 |
+
"w_k",
|
| 20 |
+
"w_v"
|
| 21 |
+
],
|
| 22 |
+
"epochs": 5,
|
| 23 |
+
"batch_size": 32,
|
| 24 |
+
"gradient_accumulation_steps": 2,
|
| 25 |
+
"lr": 0.0003,
|
| 26 |
+
"weight_decay": 0.01,
|
| 27 |
+
"warmup_epochs": 1,
|
| 28 |
+
"max_grad_norm": 1.0,
|
| 29 |
+
"use_amp": true,
|
| 30 |
+
"num_workers": 4
|
| 31 |
+
}
|
embedding_extension.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fad35cb4ae81de48fd130b764317c922c015bbd25805e0e2bf677124216851dd
|
| 3 |
+
size 1472800
|
model.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Music Transformer with relative attention for chord generation.
|
| 2 |
+
|
| 3 |
+
Architecture: Transformer decoder (autoregressive) with relative position
|
| 4 |
+
encoding (Shaw et al. 2018, efficient skewing from Huang et al. 2018).
|
| 5 |
+
|
| 6 |
+
Default config (~25M params):
|
| 7 |
+
d_model=512, n_heads=8, d_ff=2048, n_layers=8
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import math
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class RelativeMultiHeadAttention(nn.Module):
|
| 20 |
+
"""Multi-head self-attention with relative position bias."""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
d_model: int,
|
| 25 |
+
n_heads: int,
|
| 26 |
+
max_seq_len: int,
|
| 27 |
+
dropout: float = 0.1,
|
| 28 |
+
) -> None:
|
| 29 |
+
super().__init__()
|
| 30 |
+
assert d_model % n_heads == 0
|
| 31 |
+
self.n_heads = n_heads
|
| 32 |
+
self.d_k = d_model // n_heads
|
| 33 |
+
self.scale = math.sqrt(self.d_k)
|
| 34 |
+
|
| 35 |
+
self.w_q = nn.Linear(d_model, d_model)
|
| 36 |
+
self.w_k = nn.Linear(d_model, d_model)
|
| 37 |
+
self.w_v = nn.Linear(d_model, d_model)
|
| 38 |
+
self.w_o = nn.Linear(d_model, d_model)
|
| 39 |
+
|
| 40 |
+
# Learnable relative position embeddings: positions in [-max_len+1, max_len-1]
|
| 41 |
+
self.max_seq_len = max_seq_len
|
| 42 |
+
self.rel_emb = nn.Embedding(2 * max_seq_len - 1, self.d_k)
|
| 43 |
+
self.dropout = nn.Dropout(dropout)
|
| 44 |
+
|
| 45 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 46 |
+
"""
|
| 47 |
+
Args:
|
| 48 |
+
x: (B, L, D)
|
| 49 |
+
mask: (L, L) bool — True = masked (don't attend)
|
| 50 |
+
Returns:
|
| 51 |
+
(B, L, D)
|
| 52 |
+
"""
|
| 53 |
+
B, L, _ = x.shape
|
| 54 |
+
H, dk = self.n_heads, self.d_k
|
| 55 |
+
|
| 56 |
+
Q = self.w_q(x).view(B, L, H, dk).transpose(1, 2) # (B, H, L, dk)
|
| 57 |
+
K = self.w_k(x).view(B, L, H, dk).transpose(1, 2)
|
| 58 |
+
V = self.w_v(x).view(B, L, H, dk).transpose(1, 2)
|
| 59 |
+
|
| 60 |
+
# Content attention: Q K^T
|
| 61 |
+
content = torch.matmul(Q, K.transpose(-2, -1)) # (B, H, L, L)
|
| 62 |
+
|
| 63 |
+
# Relative position attention: Q R^T via efficient gather
|
| 64 |
+
rel = self._relative_attention(Q, L) # (B, H, L, L)
|
| 65 |
+
|
| 66 |
+
attn = (content + rel) / self.scale
|
| 67 |
+
|
| 68 |
+
if mask is not None:
|
| 69 |
+
attn = attn.masked_fill(mask.unsqueeze(0).unsqueeze(0), float("-inf"))
|
| 70 |
+
|
| 71 |
+
attn = self.dropout(F.softmax(attn, dim=-1))
|
| 72 |
+
out = torch.matmul(attn, V) # (B, H, L, dk)
|
| 73 |
+
out = out.transpose(1, 2).contiguous().view(B, L, -1)
|
| 74 |
+
return self.w_o(out)
|
| 75 |
+
|
| 76 |
+
def _relative_attention(self, Q: torch.Tensor, L: int) -> torch.Tensor:
|
| 77 |
+
"""Compute Q @ R^T using relative position embeddings.
|
| 78 |
+
|
| 79 |
+
Uses the index-gather approach: for each (i, j) pair, the relative
|
| 80 |
+
position is j - i, shifted to a non-negative index.
|
| 81 |
+
"""
|
| 82 |
+
device = Q.device
|
| 83 |
+
# Relative position indices: rel[i,j] = j - i + max_seq_len - 1
|
| 84 |
+
positions = torch.arange(L, device=device)
|
| 85 |
+
rel_idx = positions.unsqueeze(0) - positions.unsqueeze(1) + self.max_seq_len - 1
|
| 86 |
+
rel_idx = rel_idx.clamp(0, 2 * self.max_seq_len - 2)
|
| 87 |
+
|
| 88 |
+
R = self.rel_emb(rel_idx) # (L, L, dk)
|
| 89 |
+
|
| 90 |
+
# Q: (B, H, L, dk) R: (L, L, dk) → need (B, H, L, L)
|
| 91 |
+
# Reshape Q to (B*H, L, dk), bmm with R^T reshaped
|
| 92 |
+
BH = Q.shape[0] * Q.shape[1]
|
| 93 |
+
Q_flat = Q.reshape(BH, L, self.d_k) # (BH, L, dk)
|
| 94 |
+
|
| 95 |
+
# For each query position i, we want dot(Q[i], R[i, :, :]) → (BH, L, L)
|
| 96 |
+
# R: (L, L, dk) → transpose last two → (L, dk, L)
|
| 97 |
+
# Then Q_flat[:, i, :] @ R[i, :, :].T for each i
|
| 98 |
+
# Efficient: einsum
|
| 99 |
+
rel_score = torch.einsum("bld,lsd->bls", Q_flat, R) # (BH, L, L)
|
| 100 |
+
return rel_score.view(Q.shape[0], Q.shape[1], L, L)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class TransformerBlock(nn.Module):
|
| 104 |
+
"""Pre-norm Transformer decoder block."""
|
| 105 |
+
|
| 106 |
+
def __init__(
|
| 107 |
+
self,
|
| 108 |
+
d_model: int,
|
| 109 |
+
n_heads: int,
|
| 110 |
+
d_ff: int,
|
| 111 |
+
max_seq_len: int,
|
| 112 |
+
dropout: float = 0.1,
|
| 113 |
+
) -> None:
|
| 114 |
+
super().__init__()
|
| 115 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 116 |
+
self.attn = RelativeMultiHeadAttention(d_model, n_heads, max_seq_len, dropout)
|
| 117 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 118 |
+
self.ffn = nn.Sequential(
|
| 119 |
+
nn.Linear(d_model, d_ff),
|
| 120 |
+
nn.GELU(),
|
| 121 |
+
nn.Dropout(dropout),
|
| 122 |
+
nn.Linear(d_ff, d_model),
|
| 123 |
+
nn.Dropout(dropout),
|
| 124 |
+
)
|
| 125 |
+
self.drop = nn.Dropout(dropout)
|
| 126 |
+
|
| 127 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 128 |
+
x = x + self.drop(self.attn(self.norm1(x), mask))
|
| 129 |
+
x = x + self.ffn(self.norm2(x))
|
| 130 |
+
return x
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class MusicTransformer(nn.Module):
|
| 134 |
+
"""Autoregressive Music Transformer for chord generation."""
|
| 135 |
+
|
| 136 |
+
def __init__(
|
| 137 |
+
self,
|
| 138 |
+
vocab_size: int,
|
| 139 |
+
d_model: int = 512,
|
| 140 |
+
n_heads: int = 8,
|
| 141 |
+
d_ff: int = 2048,
|
| 142 |
+
n_layers: int = 8,
|
| 143 |
+
max_seq_len: int = 512,
|
| 144 |
+
dropout: float = 0.1,
|
| 145 |
+
pad_id: int = 0,
|
| 146 |
+
) -> None:
|
| 147 |
+
super().__init__()
|
| 148 |
+
self.d_model = d_model
|
| 149 |
+
self.max_seq_len = max_seq_len
|
| 150 |
+
self.pad_id = pad_id
|
| 151 |
+
|
| 152 |
+
self.token_emb = nn.Embedding(vocab_size, d_model, padding_idx=pad_id)
|
| 153 |
+
self.drop = nn.Dropout(dropout)
|
| 154 |
+
|
| 155 |
+
self.layers = nn.ModuleList([
|
| 156 |
+
TransformerBlock(d_model, n_heads, d_ff, max_seq_len, dropout)
|
| 157 |
+
for _ in range(n_layers)
|
| 158 |
+
])
|
| 159 |
+
|
| 160 |
+
self.norm = nn.LayerNorm(d_model)
|
| 161 |
+
self.out_proj = nn.Linear(d_model, vocab_size, bias=False)
|
| 162 |
+
|
| 163 |
+
# Weight tying (embedding ↔ output projection)
|
| 164 |
+
self.out_proj.weight = self.token_emb.weight
|
| 165 |
+
|
| 166 |
+
self._init_weights()
|
| 167 |
+
|
| 168 |
+
def _init_weights(self) -> None:
|
| 169 |
+
for name, p in self.named_parameters():
|
| 170 |
+
if p.dim() > 1 and "token_emb" not in name:
|
| 171 |
+
nn.init.xavier_uniform_(p)
|
| 172 |
+
# Embedding std=1/sqrt(d_model) so that after *sqrt(d_model) scaling
|
| 173 |
+
# inputs have unit variance, and weight-tied output logits stay small
|
| 174 |
+
nn.init.normal_(self.token_emb.weight, mean=0.0, std=self.d_model ** -0.5)
|
| 175 |
+
|
| 176 |
+
@staticmethod
|
| 177 |
+
def _causal_mask(L: int, device: torch.device) -> torch.Tensor:
|
| 178 |
+
"""Upper-triangular causal mask (True = masked)."""
|
| 179 |
+
return torch.triu(torch.ones(L, L, device=device, dtype=torch.bool), diagonal=1)
|
| 180 |
+
|
| 181 |
+
def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 182 |
+
"""
|
| 183 |
+
Args:
|
| 184 |
+
input_ids: (B, L) token IDs
|
| 185 |
+
Returns:
|
| 186 |
+
logits: (B, L, vocab_size)
|
| 187 |
+
"""
|
| 188 |
+
B, L = input_ids.shape
|
| 189 |
+
x = self.token_emb(input_ids) * math.sqrt(self.d_model)
|
| 190 |
+
x = self.drop(x)
|
| 191 |
+
|
| 192 |
+
mask = self._causal_mask(L, input_ids.device)
|
| 193 |
+
for layer in self.layers:
|
| 194 |
+
x = layer(x, mask)
|
| 195 |
+
|
| 196 |
+
return self.out_proj(self.norm(x))
|
| 197 |
+
|
| 198 |
+
def count_parameters(self) -> int:
|
| 199 |
+
return sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 200 |
+
|
| 201 |
+
@torch.no_grad()
|
| 202 |
+
def generate(
|
| 203 |
+
self,
|
| 204 |
+
prompt_ids: torch.Tensor,
|
| 205 |
+
max_new_tokens: int = 64,
|
| 206 |
+
temperature: float = 1.0,
|
| 207 |
+
top_k: int = 0,
|
| 208 |
+
top_p: float = 0.9,
|
| 209 |
+
eos_id: int = 2,
|
| 210 |
+
repetition_penalty: float = 1.0,
|
| 211 |
+
no_repeat_ngram_size: int = 0,
|
| 212 |
+
ignore_repeat_token_ids: set[int] | None = None,
|
| 213 |
+
) -> torch.Tensor:
|
| 214 |
+
"""Autoregressive generation from a prompt.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
prompt_ids: (1, L) token IDs including [BOS] and context.
|
| 218 |
+
max_new_tokens: maximum tokens to generate.
|
| 219 |
+
temperature: sampling temperature (lower = more deterministic).
|
| 220 |
+
top_k: keep only top-k logits (0 = disabled).
|
| 221 |
+
top_p: nucleus sampling threshold.
|
| 222 |
+
eos_id: stop token.
|
| 223 |
+
repetition_penalty: divide logits of previously-seen tokens by
|
| 224 |
+
this factor (HF convention). > 1.0 discourages repeats.
|
| 225 |
+
1.0 disables. Typical: 1.2–1.5.
|
| 226 |
+
no_repeat_ngram_size: ban candidate tokens that would complete
|
| 227 |
+
an n-gram already present in the current sequence (n =
|
| 228 |
+
this value). 0 disables. Typical: 3 for chord sequences.
|
| 229 |
+
ignore_repeat_token_ids: token ids exempt from the two repetition
|
| 230 |
+
controls above — e.g. [BAR] or other separators that
|
| 231 |
+
*should* recur. If None, no exemptions.
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
(1, L') full sequence including prompt and generated tokens.
|
| 235 |
+
"""
|
| 236 |
+
self.eval()
|
| 237 |
+
ids = prompt_ids.clone()
|
| 238 |
+
exempt = ignore_repeat_token_ids or set()
|
| 239 |
+
|
| 240 |
+
for _ in range(max_new_tokens):
|
| 241 |
+
ctx = ids[:, -self.max_seq_len :]
|
| 242 |
+
logits = self(ctx)[:, -1, :] / max(temperature, 1e-8)
|
| 243 |
+
|
| 244 |
+
# Repetition penalty (HuggingFace-style): scale already-seen token
|
| 245 |
+
# logits so they are less attractive. Positive logits get divided,
|
| 246 |
+
# negative logits get multiplied (stays "less attractive" either sign).
|
| 247 |
+
if repetition_penalty != 1.0:
|
| 248 |
+
seen = set(ids[0].tolist()) - exempt
|
| 249 |
+
if seen:
|
| 250 |
+
idx = torch.tensor(list(seen), device=logits.device, dtype=torch.long)
|
| 251 |
+
vals = logits[0, idx]
|
| 252 |
+
vals = torch.where(
|
| 253 |
+
vals > 0,
|
| 254 |
+
vals / repetition_penalty,
|
| 255 |
+
vals * repetition_penalty,
|
| 256 |
+
)
|
| 257 |
+
logits[0, idx] = vals
|
| 258 |
+
|
| 259 |
+
# No-repeat n-gram: block any candidate token that would complete
|
| 260 |
+
# an n-gram already present earlier in the sequence.
|
| 261 |
+
if no_repeat_ngram_size > 0 and ids.shape[1] >= no_repeat_ngram_size:
|
| 262 |
+
n = no_repeat_ngram_size
|
| 263 |
+
seq = ids[0].tolist()
|
| 264 |
+
prefix = tuple(seq[-(n - 1):]) if n > 1 else ()
|
| 265 |
+
banned: set[int] = set()
|
| 266 |
+
for i in range(len(seq) - n + 1):
|
| 267 |
+
if tuple(seq[i : i + n - 1]) == prefix:
|
| 268 |
+
banned.add(seq[i + n - 1])
|
| 269 |
+
banned -= exempt
|
| 270 |
+
if banned:
|
| 271 |
+
bidx = torch.tensor(list(banned), device=logits.device, dtype=torch.long)
|
| 272 |
+
logits[0, bidx] = float("-inf")
|
| 273 |
+
|
| 274 |
+
# Top-k
|
| 275 |
+
if top_k > 0:
|
| 276 |
+
topk_vals, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
| 277 |
+
logits[logits < topk_vals[:, -1:]] = float("-inf")
|
| 278 |
+
|
| 279 |
+
# Top-p (nucleus)
|
| 280 |
+
if 0 < top_p < 1.0:
|
| 281 |
+
sorted_logits, sorted_idx = torch.sort(logits, descending=True)
|
| 282 |
+
cum_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
|
| 283 |
+
remove = cum_probs - F.softmax(sorted_logits, dim=-1) > top_p
|
| 284 |
+
sorted_logits[remove] = float("-inf")
|
| 285 |
+
logits = sorted_logits.scatter(1, sorted_idx, sorted_logits)
|
| 286 |
+
|
| 287 |
+
probs = F.softmax(logits, dim=-1)
|
| 288 |
+
next_id = torch.multinomial(probs, num_samples=1)
|
| 289 |
+
ids = torch.cat([ids, next_id], dim=-1)
|
| 290 |
+
|
| 291 |
+
if (next_id == eos_id).all():
|
| 292 |
+
break
|
| 293 |
+
|
| 294 |
+
return ids
|
tokenizer.json
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"token2id": {
|
| 3 |
+
"[PAD]": 0,
|
| 4 |
+
"[BOS]": 1,
|
| 5 |
+
"[EOS]": 2,
|
| 6 |
+
"[BAR]": 3,
|
| 7 |
+
"[KEY:Cmaj]": 4,
|
| 8 |
+
"[KEY:Dbmaj]": 5,
|
| 9 |
+
"[KEY:Dmaj]": 6,
|
| 10 |
+
"[KEY:Ebmaj]": 7,
|
| 11 |
+
"[KEY:Emaj]": 8,
|
| 12 |
+
"[KEY:Fmaj]": 9,
|
| 13 |
+
"[KEY:F#maj]": 10,
|
| 14 |
+
"[KEY:Gmaj]": 11,
|
| 15 |
+
"[KEY:Abmaj]": 12,
|
| 16 |
+
"[KEY:Amaj]": 13,
|
| 17 |
+
"[KEY:Bbmaj]": 14,
|
| 18 |
+
"[KEY:Bmaj]": 15,
|
| 19 |
+
"[KEY:Cmin]": 16,
|
| 20 |
+
"[KEY:Dbmin]": 17,
|
| 21 |
+
"[KEY:Dmin]": 18,
|
| 22 |
+
"[KEY:Ebmin]": 19,
|
| 23 |
+
"[KEY:Emin]": 20,
|
| 24 |
+
"[KEY:Fmin]": 21,
|
| 25 |
+
"[KEY:F#min]": 22,
|
| 26 |
+
"[KEY:Gmin]": 23,
|
| 27 |
+
"[KEY:Abmin]": 24,
|
| 28 |
+
"[KEY:Amin]": 25,
|
| 29 |
+
"[KEY:Bbmin]": 26,
|
| 30 |
+
"[KEY:Bmin]": 27,
|
| 31 |
+
"[TIME:4/4]": 28,
|
| 32 |
+
"[TIME:3/4]": 29,
|
| 33 |
+
"[TIME:6/8]": 30,
|
| 34 |
+
"[TIME:2/4]": 31,
|
| 35 |
+
"[TIME:5/4]": 32,
|
| 36 |
+
"[GENRE:jazz]": 33,
|
| 37 |
+
"[GENRE:pop]": 34,
|
| 38 |
+
"[GENRE:rock]": 35,
|
| 39 |
+
"[GENRE:blues]": 36,
|
| 40 |
+
"[GENRE:bossa]": 37,
|
| 41 |
+
"[GENRE:none]": 38,
|
| 42 |
+
"Cmaj": 39,
|
| 43 |
+
"Cm": 40,
|
| 44 |
+
"C7": 41,
|
| 45 |
+
"Cmaj7": 42,
|
| 46 |
+
"Cm7": 43,
|
| 47 |
+
"Cm7b5": 44,
|
| 48 |
+
"Cdim7": 45,
|
| 49 |
+
"Cdim": 46,
|
| 50 |
+
"Caug": 47,
|
| 51 |
+
"Csus4": 48,
|
| 52 |
+
"Csus2": 49,
|
| 53 |
+
"C6": 50,
|
| 54 |
+
"Cm6": 51,
|
| 55 |
+
"C9": 52,
|
| 56 |
+
"Cm9": 53,
|
| 57 |
+
"Cmaj9": 54,
|
| 58 |
+
"C11": 55,
|
| 59 |
+
"Cm11": 56,
|
| 60 |
+
"C13": 57,
|
| 61 |
+
"Cm13": 58,
|
| 62 |
+
"Cadd9": 59,
|
| 63 |
+
"CmMaj7": 60,
|
| 64 |
+
"C7b9": 61,
|
| 65 |
+
"C7#9": 62,
|
| 66 |
+
"C7#11": 63,
|
| 67 |
+
"C7b13": 64,
|
| 68 |
+
"Dbmaj": 65,
|
| 69 |
+
"Dbm": 66,
|
| 70 |
+
"Db7": 67,
|
| 71 |
+
"Dbmaj7": 68,
|
| 72 |
+
"Dbm7": 69,
|
| 73 |
+
"Dbm7b5": 70,
|
| 74 |
+
"Dbdim7": 71,
|
| 75 |
+
"Dbdim": 72,
|
| 76 |
+
"Dbaug": 73,
|
| 77 |
+
"Dbsus4": 74,
|
| 78 |
+
"Dbsus2": 75,
|
| 79 |
+
"Db6": 76,
|
| 80 |
+
"Dbm6": 77,
|
| 81 |
+
"Db9": 78,
|
| 82 |
+
"Dbm9": 79,
|
| 83 |
+
"Dbmaj9": 80,
|
| 84 |
+
"Db11": 81,
|
| 85 |
+
"Dbm11": 82,
|
| 86 |
+
"Db13": 83,
|
| 87 |
+
"Dbm13": 84,
|
| 88 |
+
"Dbadd9": 85,
|
| 89 |
+
"DbmMaj7": 86,
|
| 90 |
+
"Db7b9": 87,
|
| 91 |
+
"Db7#9": 88,
|
| 92 |
+
"Db7#11": 89,
|
| 93 |
+
"Db7b13": 90,
|
| 94 |
+
"Dmaj": 91,
|
| 95 |
+
"Dm": 92,
|
| 96 |
+
"D7": 93,
|
| 97 |
+
"Dmaj7": 94,
|
| 98 |
+
"Dm7": 95,
|
| 99 |
+
"Dm7b5": 96,
|
| 100 |
+
"Ddim7": 97,
|
| 101 |
+
"Ddim": 98,
|
| 102 |
+
"Daug": 99,
|
| 103 |
+
"Dsus4": 100,
|
| 104 |
+
"Dsus2": 101,
|
| 105 |
+
"D6": 102,
|
| 106 |
+
"Dm6": 103,
|
| 107 |
+
"D9": 104,
|
| 108 |
+
"Dm9": 105,
|
| 109 |
+
"Dmaj9": 106,
|
| 110 |
+
"D11": 107,
|
| 111 |
+
"Dm11": 108,
|
| 112 |
+
"D13": 109,
|
| 113 |
+
"Dm13": 110,
|
| 114 |
+
"Dadd9": 111,
|
| 115 |
+
"DmMaj7": 112,
|
| 116 |
+
"D7b9": 113,
|
| 117 |
+
"D7#9": 114,
|
| 118 |
+
"D7#11": 115,
|
| 119 |
+
"D7b13": 116,
|
| 120 |
+
"Ebmaj": 117,
|
| 121 |
+
"Ebm": 118,
|
| 122 |
+
"Eb7": 119,
|
| 123 |
+
"Ebmaj7": 120,
|
| 124 |
+
"Ebm7": 121,
|
| 125 |
+
"Ebm7b5": 122,
|
| 126 |
+
"Ebdim7": 123,
|
| 127 |
+
"Ebdim": 124,
|
| 128 |
+
"Ebaug": 125,
|
| 129 |
+
"Ebsus4": 126,
|
| 130 |
+
"Ebsus2": 127,
|
| 131 |
+
"Eb6": 128,
|
| 132 |
+
"Ebm6": 129,
|
| 133 |
+
"Eb9": 130,
|
| 134 |
+
"Ebm9": 131,
|
| 135 |
+
"Ebmaj9": 132,
|
| 136 |
+
"Eb11": 133,
|
| 137 |
+
"Ebm11": 134,
|
| 138 |
+
"Eb13": 135,
|
| 139 |
+
"Ebm13": 136,
|
| 140 |
+
"Ebadd9": 137,
|
| 141 |
+
"EbmMaj7": 138,
|
| 142 |
+
"Eb7b9": 139,
|
| 143 |
+
"Eb7#9": 140,
|
| 144 |
+
"Eb7#11": 141,
|
| 145 |
+
"Eb7b13": 142,
|
| 146 |
+
"Emaj": 143,
|
| 147 |
+
"Em": 144,
|
| 148 |
+
"E7": 145,
|
| 149 |
+
"Emaj7": 146,
|
| 150 |
+
"Em7": 147,
|
| 151 |
+
"Em7b5": 148,
|
| 152 |
+
"Edim7": 149,
|
| 153 |
+
"Edim": 150,
|
| 154 |
+
"Eaug": 151,
|
| 155 |
+
"Esus4": 152,
|
| 156 |
+
"Esus2": 153,
|
| 157 |
+
"E6": 154,
|
| 158 |
+
"Em6": 155,
|
| 159 |
+
"E9": 156,
|
| 160 |
+
"Em9": 157,
|
| 161 |
+
"Emaj9": 158,
|
| 162 |
+
"E11": 159,
|
| 163 |
+
"Em11": 160,
|
| 164 |
+
"E13": 161,
|
| 165 |
+
"Em13": 162,
|
| 166 |
+
"Eadd9": 163,
|
| 167 |
+
"EmMaj7": 164,
|
| 168 |
+
"E7b9": 165,
|
| 169 |
+
"E7#9": 166,
|
| 170 |
+
"E7#11": 167,
|
| 171 |
+
"E7b13": 168,
|
| 172 |
+
"Fmaj": 169,
|
| 173 |
+
"Fm": 170,
|
| 174 |
+
"F7": 171,
|
| 175 |
+
"Fmaj7": 172,
|
| 176 |
+
"Fm7": 173,
|
| 177 |
+
"Fm7b5": 174,
|
| 178 |
+
"Fdim7": 175,
|
| 179 |
+
"Fdim": 176,
|
| 180 |
+
"Faug": 177,
|
| 181 |
+
"Fsus4": 178,
|
| 182 |
+
"Fsus2": 179,
|
| 183 |
+
"F6": 180,
|
| 184 |
+
"Fm6": 181,
|
| 185 |
+
"F9": 182,
|
| 186 |
+
"Fm9": 183,
|
| 187 |
+
"Fmaj9": 184,
|
| 188 |
+
"F11": 185,
|
| 189 |
+
"Fm11": 186,
|
| 190 |
+
"F13": 187,
|
| 191 |
+
"Fm13": 188,
|
| 192 |
+
"Fadd9": 189,
|
| 193 |
+
"FmMaj7": 190,
|
| 194 |
+
"F7b9": 191,
|
| 195 |
+
"F7#9": 192,
|
| 196 |
+
"F7#11": 193,
|
| 197 |
+
"F7b13": 194,
|
| 198 |
+
"F#maj": 195,
|
| 199 |
+
"F#m": 196,
|
| 200 |
+
"F#7": 197,
|
| 201 |
+
"F#maj7": 198,
|
| 202 |
+
"F#m7": 199,
|
| 203 |
+
"F#m7b5": 200,
|
| 204 |
+
"F#dim7": 201,
|
| 205 |
+
"F#dim": 202,
|
| 206 |
+
"F#aug": 203,
|
| 207 |
+
"F#sus4": 204,
|
| 208 |
+
"F#sus2": 205,
|
| 209 |
+
"F#6": 206,
|
| 210 |
+
"F#m6": 207,
|
| 211 |
+
"F#9": 208,
|
| 212 |
+
"F#m9": 209,
|
| 213 |
+
"F#maj9": 210,
|
| 214 |
+
"F#11": 211,
|
| 215 |
+
"F#m11": 212,
|
| 216 |
+
"F#13": 213,
|
| 217 |
+
"F#m13": 214,
|
| 218 |
+
"F#add9": 215,
|
| 219 |
+
"F#mMaj7": 216,
|
| 220 |
+
"F#7b9": 217,
|
| 221 |
+
"F#7#9": 218,
|
| 222 |
+
"F#7#11": 219,
|
| 223 |
+
"F#7b13": 220,
|
| 224 |
+
"Gmaj": 221,
|
| 225 |
+
"Gm": 222,
|
| 226 |
+
"G7": 223,
|
| 227 |
+
"Gmaj7": 224,
|
| 228 |
+
"Gm7": 225,
|
| 229 |
+
"Gm7b5": 226,
|
| 230 |
+
"Gdim7": 227,
|
| 231 |
+
"Gdim": 228,
|
| 232 |
+
"Gaug": 229,
|
| 233 |
+
"Gsus4": 230,
|
| 234 |
+
"Gsus2": 231,
|
| 235 |
+
"G6": 232,
|
| 236 |
+
"Gm6": 233,
|
| 237 |
+
"G9": 234,
|
| 238 |
+
"Gm9": 235,
|
| 239 |
+
"Gmaj9": 236,
|
| 240 |
+
"G11": 237,
|
| 241 |
+
"Gm11": 238,
|
| 242 |
+
"G13": 239,
|
| 243 |
+
"Gm13": 240,
|
| 244 |
+
"Gadd9": 241,
|
| 245 |
+
"GmMaj7": 242,
|
| 246 |
+
"G7b9": 243,
|
| 247 |
+
"G7#9": 244,
|
| 248 |
+
"G7#11": 245,
|
| 249 |
+
"G7b13": 246,
|
| 250 |
+
"Abmaj": 247,
|
| 251 |
+
"Abm": 248,
|
| 252 |
+
"Ab7": 249,
|
| 253 |
+
"Abmaj7": 250,
|
| 254 |
+
"Abm7": 251,
|
| 255 |
+
"Abm7b5": 252,
|
| 256 |
+
"Abdim7": 253,
|
| 257 |
+
"Abdim": 254,
|
| 258 |
+
"Abaug": 255,
|
| 259 |
+
"Absus4": 256,
|
| 260 |
+
"Absus2": 257,
|
| 261 |
+
"Ab6": 258,
|
| 262 |
+
"Abm6": 259,
|
| 263 |
+
"Ab9": 260,
|
| 264 |
+
"Abm9": 261,
|
| 265 |
+
"Abmaj9": 262,
|
| 266 |
+
"Ab11": 263,
|
| 267 |
+
"Abm11": 264,
|
| 268 |
+
"Ab13": 265,
|
| 269 |
+
"Abm13": 266,
|
| 270 |
+
"Abadd9": 267,
|
| 271 |
+
"AbmMaj7": 268,
|
| 272 |
+
"Ab7b9": 269,
|
| 273 |
+
"Ab7#9": 270,
|
| 274 |
+
"Ab7#11": 271,
|
| 275 |
+
"Ab7b13": 272,
|
| 276 |
+
"Amaj": 273,
|
| 277 |
+
"Am": 274,
|
| 278 |
+
"A7": 275,
|
| 279 |
+
"Amaj7": 276,
|
| 280 |
+
"Am7": 277,
|
| 281 |
+
"Am7b5": 278,
|
| 282 |
+
"Adim7": 279,
|
| 283 |
+
"Adim": 280,
|
| 284 |
+
"Aaug": 281,
|
| 285 |
+
"Asus4": 282,
|
| 286 |
+
"Asus2": 283,
|
| 287 |
+
"A6": 284,
|
| 288 |
+
"Am6": 285,
|
| 289 |
+
"A9": 286,
|
| 290 |
+
"Am9": 287,
|
| 291 |
+
"Amaj9": 288,
|
| 292 |
+
"A11": 289,
|
| 293 |
+
"Am11": 290,
|
| 294 |
+
"A13": 291,
|
| 295 |
+
"Am13": 292,
|
| 296 |
+
"Aadd9": 293,
|
| 297 |
+
"AmMaj7": 294,
|
| 298 |
+
"A7b9": 295,
|
| 299 |
+
"A7#9": 296,
|
| 300 |
+
"A7#11": 297,
|
| 301 |
+
"A7b13": 298,
|
| 302 |
+
"Bbmaj": 299,
|
| 303 |
+
"Bbm": 300,
|
| 304 |
+
"Bb7": 301,
|
| 305 |
+
"Bbmaj7": 302,
|
| 306 |
+
"Bbm7": 303,
|
| 307 |
+
"Bbm7b5": 304,
|
| 308 |
+
"Bbdim7": 305,
|
| 309 |
+
"Bbdim": 306,
|
| 310 |
+
"Bbaug": 307,
|
| 311 |
+
"Bbsus4": 308,
|
| 312 |
+
"Bbsus2": 309,
|
| 313 |
+
"Bb6": 310,
|
| 314 |
+
"Bbm6": 311,
|
| 315 |
+
"Bb9": 312,
|
| 316 |
+
"Bbm9": 313,
|
| 317 |
+
"Bbmaj9": 314,
|
| 318 |
+
"Bb11": 315,
|
| 319 |
+
"Bbm11": 316,
|
| 320 |
+
"Bb13": 317,
|
| 321 |
+
"Bbm13": 318,
|
| 322 |
+
"Bbadd9": 319,
|
| 323 |
+
"BbmMaj7": 320,
|
| 324 |
+
"Bb7b9": 321,
|
| 325 |
+
"Bb7#9": 322,
|
| 326 |
+
"Bb7#11": 323,
|
| 327 |
+
"Bb7b13": 324,
|
| 328 |
+
"Bmaj": 325,
|
| 329 |
+
"Bm": 326,
|
| 330 |
+
"B7": 327,
|
| 331 |
+
"Bmaj7": 328,
|
| 332 |
+
"Bm7": 329,
|
| 333 |
+
"Bm7b5": 330,
|
| 334 |
+
"Bdim7": 331,
|
| 335 |
+
"Bdim": 332,
|
| 336 |
+
"Baug": 333,
|
| 337 |
+
"Bsus4": 334,
|
| 338 |
+
"Bsus2": 335,
|
| 339 |
+
"B6": 336,
|
| 340 |
+
"Bm6": 337,
|
| 341 |
+
"B9": 338,
|
| 342 |
+
"Bm9": 339,
|
| 343 |
+
"Bmaj9": 340,
|
| 344 |
+
"B11": 341,
|
| 345 |
+
"Bm11": 342,
|
| 346 |
+
"B13": 343,
|
| 347 |
+
"Bm13": 344,
|
| 348 |
+
"Badd9": 345,
|
| 349 |
+
"BmMaj7": 346,
|
| 350 |
+
"B7b9": 347,
|
| 351 |
+
"B7#9": 348,
|
| 352 |
+
"B7#11": 349,
|
| 353 |
+
"B7b13": 350,
|
| 354 |
+
"[GENRE:country]": 351,
|
| 355 |
+
"[GENRE:rnb_soul]": 352,
|
| 356 |
+
"[GENRE:hip_hop]": 353,
|
| 357 |
+
"[GENRE:electronic]": 354,
|
| 358 |
+
"[GENRE:funk]": 355,
|
| 359 |
+
"[GENRE:folk]": 356,
|
| 360 |
+
"[GENRE:gospel]": 357,
|
| 361 |
+
"[GENRE:classical]": 358
|
| 362 |
+
},
|
| 363 |
+
"vocab_size": 359
|
| 364 |
+
}
|
tokenizer.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chord sequence tokenizer for Music Transformer training.
|
| 2 |
+
|
| 3 |
+
Vocabulary (~350 tokens):
|
| 4 |
+
[PAD]=0, [BOS]=1, [EOS]=2, [BAR]=3
|
| 5 |
+
[KEY:Cmaj] ... [KEY:Bmin] (24 keys)
|
| 6 |
+
[TIME:4/4] ... [TIME:5/4] (5 time sigs)
|
| 7 |
+
[GENRE:jazz] ... [GENRE:none] (6 genres)
|
| 8 |
+
Cmaj, Cm, C7, ... B7b13 (12 roots x 26 qualities = 312 chords)
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
# Canonical root names (jazz convention: prefer flats)
|
| 17 |
+
ROOTS = ["C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
|
| 18 |
+
|
| 19 |
+
# Root name aliases for normalization
|
| 20 |
+
ROOT_ALIASES: dict[str, str] = {
|
| 21 |
+
"C#": "Db", "D#": "Eb", "E#": "F", "Fb": "E",
|
| 22 |
+
"G#": "Ab", "A#": "Bb", "B#": "C", "Cb": "B",
|
| 23 |
+
"Gb": "F#",
|
| 24 |
+
# Lowercase
|
| 25 |
+
"c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "a": "A", "b": "B",
|
| 26 |
+
"c#": "Db", "db": "Db", "d#": "Eb", "eb": "Eb",
|
| 27 |
+
"f#": "F#", "gb": "F#", "g#": "Ab", "ab": "Ab",
|
| 28 |
+
"a#": "Bb", "bb": "Bb", "cb": "B", "fb": "E",
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
# Chord qualities in our vocabulary
|
| 32 |
+
QUALITIES = [
|
| 33 |
+
"maj", "m", "7", "maj7", "m7", "m7b5", "dim7", "dim", "aug",
|
| 34 |
+
"sus4", "sus2", "6", "m6", "9", "m9", "maj9", "11", "m11",
|
| 35 |
+
"13", "m13", "add9", "mMaj7", "7b9", "7#9", "7#11", "7b13",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
# Quality alias mapping → canonical quality
|
| 39 |
+
_QUALITY_ALIASES: dict[str, str] = {
|
| 40 |
+
# Major
|
| 41 |
+
"major": "maj", "M": "maj",
|
| 42 |
+
# Minor
|
| 43 |
+
"min": "m", "minor": "m", "-": "m", "mi": "m",
|
| 44 |
+
# Dominant 7
|
| 45 |
+
"dom7": "7", "dom": "7",
|
| 46 |
+
# Major 7
|
| 47 |
+
"^7": "maj7", "M7": "maj7", "Maj7": "maj7", "major7": "maj7",
|
| 48 |
+
"j7": "maj7", "^": "maj7", "delta": "maj7",
|
| 49 |
+
# Minor 7
|
| 50 |
+
"min7": "m7", "-7": "m7", "mi7": "m7",
|
| 51 |
+
# Half-diminished
|
| 52 |
+
"hdim7": "m7b5", "hdim": "m7b5", "h7": "m7b5",
|
| 53 |
+
"%7": "m7b5", "%": "m7b5",
|
| 54 |
+
# Diminished
|
| 55 |
+
"o": "dim", "o7": "dim7",
|
| 56 |
+
# Augmented
|
| 57 |
+
"+": "aug",
|
| 58 |
+
# Suspended
|
| 59 |
+
"sus": "sus4",
|
| 60 |
+
# 6th
|
| 61 |
+
"min6": "m6", "-6": "m6",
|
| 62 |
+
# 9th
|
| 63 |
+
"min9": "m9", "-9": "m9", "M9": "maj9", "^9": "maj9", "Maj9": "maj9",
|
| 64 |
+
# 11th
|
| 65 |
+
"min11": "m11", "-11": "m11",
|
| 66 |
+
# 13th
|
| 67 |
+
"min13": "m13", "-13": "m13",
|
| 68 |
+
# Minor-major 7
|
| 69 |
+
"minmaj7": "mMaj7", "-^7": "mMaj7", "mM7": "mMaj7",
|
| 70 |
+
# Altered dominants
|
| 71 |
+
"7alt": "7b9",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# Keys and metadata
|
| 75 |
+
MAJOR_KEYS = [f"{r}maj" for r in ROOTS]
|
| 76 |
+
MINOR_KEYS = [f"{r}min" for r in ROOTS]
|
| 77 |
+
ALL_KEYS = MAJOR_KEYS + MINOR_KEYS
|
| 78 |
+
TIME_SIGS = ["4/4", "3/4", "6/8", "2/4", "5/4"]
|
| 79 |
+
GENRES = ["jazz", "pop", "rock", "blues", "bossa"]
|
| 80 |
+
|
| 81 |
+
EXTRA_GENRES = [
|
| 82 |
+
"country", "rnb_soul", "hip_hop", "electronic",
|
| 83 |
+
"funk", "folk", "gospel", "classical",
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class ChordTokenizer:
|
| 88 |
+
"""Deterministic tokenizer for chord sequences."""
|
| 89 |
+
|
| 90 |
+
PAD = 0
|
| 91 |
+
BOS = 1
|
| 92 |
+
EOS = 2
|
| 93 |
+
BAR = 3
|
| 94 |
+
|
| 95 |
+
def __init__(self, include_extra_genres: bool = False) -> None:
|
| 96 |
+
self.include_extra_genres = include_extra_genres
|
| 97 |
+
self.token2id: dict[str, int] = {}
|
| 98 |
+
self.id2token: dict[int, str] = {}
|
| 99 |
+
self._build_vocab()
|
| 100 |
+
|
| 101 |
+
# ------------------------------------------------------------------
|
| 102 |
+
# Vocab construction
|
| 103 |
+
# ------------------------------------------------------------------
|
| 104 |
+
|
| 105 |
+
def _build_vocab(self) -> None:
|
| 106 |
+
tokens: list[str] = ["[PAD]", "[BOS]", "[EOS]", "[BAR]"]
|
| 107 |
+
for key in ALL_KEYS:
|
| 108 |
+
tokens.append(f"[KEY:{key}]")
|
| 109 |
+
for ts in TIME_SIGS:
|
| 110 |
+
tokens.append(f"[TIME:{ts}]")
|
| 111 |
+
for genre in GENRES:
|
| 112 |
+
tokens.append(f"[GENRE:{genre}]")
|
| 113 |
+
tokens.append("[GENRE:none]")
|
| 114 |
+
for root in ROOTS:
|
| 115 |
+
for quality in QUALITIES:
|
| 116 |
+
tokens.append(f"{root}{quality}")
|
| 117 |
+
if self.include_extra_genres:
|
| 118 |
+
for genre in EXTRA_GENRES:
|
| 119 |
+
tokens.append(f"[GENRE:{genre}]")
|
| 120 |
+
for i, tok in enumerate(tokens):
|
| 121 |
+
self.token2id[tok] = i
|
| 122 |
+
self.id2token[i] = tok
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def vocab_size(self) -> int:
|
| 126 |
+
return len(self.token2id)
|
| 127 |
+
|
| 128 |
+
@property
|
| 129 |
+
def pad_id(self) -> int:
|
| 130 |
+
return self.PAD
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def bos_id(self) -> int:
|
| 134 |
+
return self.BOS
|
| 135 |
+
|
| 136 |
+
@property
|
| 137 |
+
def eos_id(self) -> int:
|
| 138 |
+
return self.EOS
|
| 139 |
+
|
| 140 |
+
@property
|
| 141 |
+
def bar_id(self) -> int:
|
| 142 |
+
return self.BAR
|
| 143 |
+
|
| 144 |
+
# ------------------------------------------------------------------
|
| 145 |
+
# Encoding helpers
|
| 146 |
+
# ------------------------------------------------------------------
|
| 147 |
+
|
| 148 |
+
def encode_chord(self, chord_str: str) -> int | None:
|
| 149 |
+
token = self.normalize_chord(chord_str)
|
| 150 |
+
return self.token2id.get(token) if token else None
|
| 151 |
+
|
| 152 |
+
def encode_key(self, key_str: str) -> int | None:
|
| 153 |
+
return self.token2id.get(f"[KEY:{key_str}]")
|
| 154 |
+
|
| 155 |
+
def encode_time_sig(self, ts: str) -> int | None:
|
| 156 |
+
return self.token2id.get(f"[TIME:{ts}]")
|
| 157 |
+
|
| 158 |
+
def encode_genre(self, genre: str) -> int | None:
|
| 159 |
+
return self.token2id.get(f"[GENRE:{genre}]")
|
| 160 |
+
|
| 161 |
+
def encode_sequence(self, song: dict) -> list[int]:
|
| 162 |
+
"""Encode a unified song dict to a token-ID sequence.
|
| 163 |
+
|
| 164 |
+
Expected *song* format::
|
| 165 |
+
|
| 166 |
+
{
|
| 167 |
+
"key": "Cmaj",
|
| 168 |
+
"time_signature": "4/4",
|
| 169 |
+
"genre": "jazz",
|
| 170 |
+
"bars": [["Cmaj7", "Am7"], ["Dm7", "G7"], ...]
|
| 171 |
+
}
|
| 172 |
+
"""
|
| 173 |
+
ids: list[int] = [self.BOS]
|
| 174 |
+
|
| 175 |
+
kid = self.encode_key(song.get("key", "Cmaj"))
|
| 176 |
+
if kid is not None:
|
| 177 |
+
ids.append(kid)
|
| 178 |
+
|
| 179 |
+
tid = self.encode_time_sig(song.get("time_signature", "4/4"))
|
| 180 |
+
if tid is not None:
|
| 181 |
+
ids.append(tid)
|
| 182 |
+
|
| 183 |
+
gid = self.encode_genre(song.get("genre", "none"))
|
| 184 |
+
if gid is not None:
|
| 185 |
+
ids.append(gid)
|
| 186 |
+
|
| 187 |
+
for bar in song.get("bars", []):
|
| 188 |
+
ids.append(self.BAR)
|
| 189 |
+
for chord in bar:
|
| 190 |
+
cid = self.encode_chord(chord)
|
| 191 |
+
if cid is not None:
|
| 192 |
+
ids.append(cid)
|
| 193 |
+
|
| 194 |
+
ids.append(self.EOS)
|
| 195 |
+
return ids
|
| 196 |
+
|
| 197 |
+
def decode(self, ids: list[int]) -> list[str]:
|
| 198 |
+
return [self.id2token.get(i, "[UNK]") for i in ids]
|
| 199 |
+
|
| 200 |
+
# ------------------------------------------------------------------
|
| 201 |
+
# Chord normalization
|
| 202 |
+
# ------------------------------------------------------------------
|
| 203 |
+
|
| 204 |
+
@staticmethod
|
| 205 |
+
def normalize_root(root: str) -> str | None:
|
| 206 |
+
"""Normalize a root note name to canonical form."""
|
| 207 |
+
if root in ROOTS:
|
| 208 |
+
return root
|
| 209 |
+
if root in ROOT_ALIASES:
|
| 210 |
+
return ROOT_ALIASES[root]
|
| 211 |
+
# Try capitalize first letter
|
| 212 |
+
cap = root[0].upper() + root[1:] if len(root) > 1 else root.upper()
|
| 213 |
+
if cap in ROOTS:
|
| 214 |
+
return cap
|
| 215 |
+
if cap in ROOT_ALIASES:
|
| 216 |
+
return ROOT_ALIASES[cap]
|
| 217 |
+
return None
|
| 218 |
+
|
| 219 |
+
@staticmethod
|
| 220 |
+
def normalize_chord(chord_str: str) -> str | None:
|
| 221 |
+
"""Normalize any chord notation to ``{Root}{quality}`` in our vocab."""
|
| 222 |
+
if not chord_str or chord_str in (
|
| 223 |
+
"N", "NC", "N.C.", "X", "x",
|
| 224 |
+
"pause", "silence", "&pause", "end",
|
| 225 |
+
):
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
# Strip slash-chord bass
|
| 229 |
+
if "/" in chord_str:
|
| 230 |
+
chord_str = chord_str.split("/")[0]
|
| 231 |
+
|
| 232 |
+
# Billboard colon format Root:Quality
|
| 233 |
+
if ":" in chord_str:
|
| 234 |
+
root_part, qual_part = chord_str.split(":", 1)
|
| 235 |
+
# qual_part may also have /bass — already stripped above
|
| 236 |
+
else:
|
| 237 |
+
root_part = chord_str[0]
|
| 238 |
+
qual_part = chord_str[1:]
|
| 239 |
+
if qual_part and qual_part[0] in ("b", "#"):
|
| 240 |
+
root_part += qual_part[0]
|
| 241 |
+
qual_part = qual_part[1:]
|
| 242 |
+
|
| 243 |
+
norm_root = ChordTokenizer.normalize_root(root_part)
|
| 244 |
+
if norm_root is None:
|
| 245 |
+
return None
|
| 246 |
+
|
| 247 |
+
quality = ChordTokenizer._normalize_quality(qual_part)
|
| 248 |
+
if quality is None or quality not in QUALITIES:
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
return f"{norm_root}{quality}"
|
| 252 |
+
|
| 253 |
+
@staticmethod
|
| 254 |
+
def _normalize_quality(q: str) -> str | None:
|
| 255 |
+
"""Map various quality notations to our canonical set."""
|
| 256 |
+
if not q:
|
| 257 |
+
return "maj"
|
| 258 |
+
|
| 259 |
+
# Direct hit
|
| 260 |
+
if q in QUALITIES:
|
| 261 |
+
return q
|
| 262 |
+
|
| 263 |
+
# Alias table
|
| 264 |
+
if q in _QUALITY_ALIASES:
|
| 265 |
+
return _QUALITY_ALIASES[q]
|
| 266 |
+
|
| 267 |
+
# Case-insensitive alias search
|
| 268 |
+
for alias, canon in _QUALITY_ALIASES.items():
|
| 269 |
+
if q.lower() == alias.lower():
|
| 270 |
+
return canon
|
| 271 |
+
|
| 272 |
+
# ---- Heuristic fallbacks for unusual notations ----
|
| 273 |
+
|
| 274 |
+
# WJazzD altered dominants: "79b" → 7b9, "79#" → 7#9, etc.
|
| 275 |
+
if q.startswith("7"):
|
| 276 |
+
tail = q[1:]
|
| 277 |
+
if "b9" in tail or "9b" in tail:
|
| 278 |
+
return "7b9"
|
| 279 |
+
if "#9" in tail or "9#" in tail:
|
| 280 |
+
return "7#9"
|
| 281 |
+
if "#11" in tail or "11#" in tail:
|
| 282 |
+
return "7#11"
|
| 283 |
+
if "b13" in tail or "13b" in tail:
|
| 284 |
+
return "7b13"
|
| 285 |
+
|
| 286 |
+
# Compound minor qualities
|
| 287 |
+
if q.startswith("m") or q.startswith("-"):
|
| 288 |
+
inner = q.lstrip("m").lstrip("-")
|
| 289 |
+
if "7" in inner and ("b5" in inner or "b5" in q):
|
| 290 |
+
return "m7b5"
|
| 291 |
+
if "7" in inner:
|
| 292 |
+
return "m7"
|
| 293 |
+
if "9" in inner:
|
| 294 |
+
return "m9"
|
| 295 |
+
if "11" in inner:
|
| 296 |
+
return "m11"
|
| 297 |
+
if "13" in inner:
|
| 298 |
+
return "m13"
|
| 299 |
+
if "6" in inner:
|
| 300 |
+
return "m6"
|
| 301 |
+
return "m"
|
| 302 |
+
|
| 303 |
+
# Bare numbers
|
| 304 |
+
if q in ("7",):
|
| 305 |
+
return "7"
|
| 306 |
+
if q in ("9",):
|
| 307 |
+
return "9"
|
| 308 |
+
if q in ("6",):
|
| 309 |
+
return "6"
|
| 310 |
+
if q in ("11",):
|
| 311 |
+
return "11"
|
| 312 |
+
if q in ("13",):
|
| 313 |
+
return "13"
|
| 314 |
+
|
| 315 |
+
# If nothing matched, approximate as major
|
| 316 |
+
return "maj"
|
| 317 |
+
|
| 318 |
+
# ------------------------------------------------------------------
|
| 319 |
+
# Transposition
|
| 320 |
+
# ------------------------------------------------------------------
|
| 321 |
+
|
| 322 |
+
def transpose_chord_token(self, token: str, semitones: int) -> str | None:
|
| 323 |
+
"""Transpose a chord token string by *semitones*."""
|
| 324 |
+
if token.startswith("["):
|
| 325 |
+
return None
|
| 326 |
+
root = token[0]
|
| 327 |
+
rest = token[1:]
|
| 328 |
+
if rest and rest[0] in ("b", "#"):
|
| 329 |
+
root += rest[0]
|
| 330 |
+
rest = rest[1:]
|
| 331 |
+
norm_root = self.normalize_root(root)
|
| 332 |
+
if norm_root is None:
|
| 333 |
+
return None
|
| 334 |
+
new_root = ROOTS[(ROOTS.index(norm_root) + semitones) % 12]
|
| 335 |
+
return f"{new_root}{rest}"
|
| 336 |
+
|
| 337 |
+
def transpose_key_token(self, token: str, semitones: int) -> str:
|
| 338 |
+
"""Transpose a key token like ``[KEY:Cmaj]``."""
|
| 339 |
+
inner = token[5:-1] # strip [KEY: and ]
|
| 340 |
+
if inner.endswith("maj"):
|
| 341 |
+
root, mode = inner[:-3], "maj"
|
| 342 |
+
elif inner.endswith("min"):
|
| 343 |
+
root, mode = inner[:-3], "min"
|
| 344 |
+
else:
|
| 345 |
+
return token
|
| 346 |
+
norm = self.normalize_root(root)
|
| 347 |
+
if norm is None:
|
| 348 |
+
return token
|
| 349 |
+
new_root = ROOTS[(ROOTS.index(norm) + semitones) % 12]
|
| 350 |
+
return f"[KEY:{new_root}{mode}]"
|
| 351 |
+
|
| 352 |
+
def transpose_sequence(self, ids: list[int], semitones: int) -> list[int]:
|
| 353 |
+
"""Transpose every chord & key token in *ids* by *semitones*."""
|
| 354 |
+
if semitones % 12 == 0:
|
| 355 |
+
return list(ids)
|
| 356 |
+
out: list[int] = []
|
| 357 |
+
for tid in ids:
|
| 358 |
+
tok = self.id2token.get(tid)
|
| 359 |
+
if tok is None:
|
| 360 |
+
out.append(tid)
|
| 361 |
+
elif tok.startswith("[KEY:"):
|
| 362 |
+
new = self.transpose_key_token(tok, semitones)
|
| 363 |
+
out.append(self.token2id.get(new, tid))
|
| 364 |
+
elif tok.startswith("[") or tid <= self.BAR:
|
| 365 |
+
out.append(tid)
|
| 366 |
+
else:
|
| 367 |
+
new = self.transpose_chord_token(tok, semitones)
|
| 368 |
+
out.append(self.token2id[new] if new and new in self.token2id else tid)
|
| 369 |
+
return out
|
| 370 |
+
|
| 371 |
+
# ------------------------------------------------------------------
|
| 372 |
+
# Persistence
|
| 373 |
+
# ------------------------------------------------------------------
|
| 374 |
+
|
| 375 |
+
def save(self, path: str | Path) -> None:
|
| 376 |
+
Path(path).write_text(json.dumps({
|
| 377 |
+
"token2id": self.token2id,
|
| 378 |
+
"vocab_size": self.vocab_size,
|
| 379 |
+
}, indent=2, ensure_ascii=False))
|
| 380 |
+
|
| 381 |
+
@classmethod
|
| 382 |
+
def load(cls, path: str | Path) -> ChordTokenizer:
|
| 383 |
+
tok = cls()
|
| 384 |
+
data = json.loads(Path(path).read_text())
|
| 385 |
+
assert data["vocab_size"] == tok.vocab_size, (
|
| 386 |
+
f"Vocab mismatch: file={data['vocab_size']}, current={tok.vocab_size}"
|
| 387 |
+
)
|
| 388 |
+
return tok
|