add standalone apollo_m.py + working usage example
Browse files- README.md +10 -11
- apollo_m.py +257 -0
README.md
CHANGED
|
@@ -74,24 +74,23 @@ Not intended as a replacement for composers, and not evaluated for any use beyon
|
|
| 74 |
|
| 75 |
## How to use
|
| 76 |
|
| 77 |
-
The model is a custom PyTorch architecture (not a `transformers` model).
|
| 78 |
|
| 79 |
```python
|
| 80 |
-
|
|
|
|
| 81 |
from huggingface_hub import hf_hub_download
|
| 82 |
-
from safetensors.torch import load_file
|
| 83 |
-
|
| 84 |
repo = "viknesh100/Apollo-M"
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
```
|
| 91 |
|
| 92 |
-
Generation uses grammar-constrained sampling: at each step only tokens that keep the sequence valid are allowed (`BAR` then `POS` then `PITCH` then `DUR`, increasing positions inside a bar, exactly the requested number of bars), so every output parses to valid MIDI. Defaults: temperature 0.9, nucleus p 0.92.
|
| 93 |
|
| 94 |
-
A small rule-based parser turns
|
| 95 |
|
| 96 |
## Files
|
| 97 |
|
|
|
|
| 74 |
|
| 75 |
## How to use
|
| 76 |
|
| 77 |
+
The model is a custom PyTorch architecture (not a `transformers` model). Everything needed (architecture, vocab, prompt parser, constrained decoder, MIDI writer) is in `apollo_m.py` in this repo.
|
| 78 |
|
| 79 |
```python
|
| 80 |
+
# pip install torch huggingface_hub safetensors pretty_midi
|
| 81 |
+
import sys, os
|
| 82 |
from huggingface_hub import hf_hub_download
|
|
|
|
|
|
|
| 83 |
repo = "viknesh100/Apollo-M"
|
| 84 |
+
sys.path.insert(0, os.path.dirname(hf_hub_download(repo, "apollo_m.py"))) # add token="hf_..." if the repo is private
|
| 85 |
+
from apollo_m import load
|
| 86 |
+
|
| 87 |
+
m = load(repo)
|
| 88 |
+
m.make("8 bars in D minor at 90 bpm, sad piano melody", n=3, seed=1) # writes MIDI files to ./apollo_out/
|
| 89 |
```
|
| 90 |
|
| 91 |
+
Generation uses grammar-constrained sampling: at each step only tokens that keep the sequence valid are allowed (`BAR` then `POS` then `PITCH` then `DUR`, increasing positions inside a bar, exactly the requested number of bars), so every output parses to valid MIDI. Defaults: temperature 0.9, nucleus p 0.92. Override fields directly if you prefer, e.g. `m.make("8 bars", key=("A","minor"), bpm=100, program=40)`.
|
| 92 |
|
| 93 |
+
A small rule-based parser turns the prompt into header tokens (key, bars, bpm or tempo words, time signature, genre and mood words from `labels.json`, instrument words). The model itself does not read free text.
|
| 94 |
|
| 95 |
## Files
|
| 96 |
|
apollo_m.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Apollo-M: standalone inference. Load the final model from the Hub and turn a text prompt into MIDI.
|
| 2 |
+
from apollo_m import load
|
| 3 |
+
m = load(); m.make("8 bars in D minor at 90 bpm, sad piano melody", n=3, seed=1)
|
| 4 |
+
"""
|
| 5 |
+
import os, re, json, math, time
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch, torch.nn as nn, torch.nn.functional as F
|
| 8 |
+
import pretty_midi
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
REPO, SEQ_LEN = "viknesh100/Apollo-M", 1024
|
| 12 |
+
|
| 13 |
+
# ---------------- vocab (identical to training) ----------------
|
| 14 |
+
NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
|
| 15 |
+
TS_LIST = [(4,4),(3,4),(2,4),(6,8),(12,8)]
|
| 16 |
+
STEPS_PER_BAR = {ts: ts[0]*16//ts[1] for ts in TS_LIST}
|
| 17 |
+
BASE_FIELDS = ["TEMPO","TS","KEY","BARS","DENS","REG"]
|
| 18 |
+
NAMES = ["PAD","BOS","EOS","BAR"]
|
| 19 |
+
NAMES += [f"POS_{i}" for i in range(32)]
|
| 20 |
+
NAMES += [f"PITCH_{i}" for i in range(128)]
|
| 21 |
+
NAMES += [f"DUR_{i}" for i in range(1,33)]
|
| 22 |
+
NAMES += [f"TEMPO_{i}" for i in range(24)]
|
| 23 |
+
NAMES += [f"TS_{a}_{b}" for a,b in TS_LIST]
|
| 24 |
+
NAMES += [f"KEY_{i}" for i in range(24)]
|
| 25 |
+
NAMES += [f"BARS_{i}" for i in range(1,17)]
|
| 26 |
+
NAMES += [f"DENS_{i}" for i in range(8)]
|
| 27 |
+
NAMES += [f"REG_{i}" for i in range(8)]
|
| 28 |
+
NAMES += [f"UNK_{f}" for f in BASE_FIELDS]
|
| 29 |
+
NAMES += [f"EXTRA_{i}" for i in range(64)]
|
| 30 |
+
while len(NAMES) % 8: NAMES.append(f"PADV_{len(NAMES)}")
|
| 31 |
+
T = {n:i for i,n in enumerate(NAMES)}; VOCAB = len(NAMES)
|
| 32 |
+
BOS, EOS, BAR_ID = T["BOS"], T["EOS"], T["BAR"]
|
| 33 |
+
POS0, PITCH0, DUR1 = T["POS_0"], T["PITCH_0"], T["DUR_1"]
|
| 34 |
+
FIELDS = BASE_FIELDS + ["GENRE","MOOD","INSTR"]
|
| 35 |
+
FIELD_BASE = {"TEMPO":T["TEMPO_0"],"TS":T["TS_4_4"],"KEY":T["KEY_0"],"BARS":T["BARS_1"],"DENS":T["DENS_0"],"REG":T["REG_0"],
|
| 36 |
+
"GENRE":T["EXTRA_0"],"MOOD":T["EXTRA_24"],"INSTR":T["EXTRA_40"]}
|
| 37 |
+
FIELD_N = {"TEMPO":24,"TS":len(TS_LIST),"KEY":24,"BARS":16,"DENS":8,"REG":8,"GENRE":24,"MOOD":16,"INSTR":16}
|
| 38 |
+
UNK_IDS = [T[f"UNK_{f}"] for f in BASE_FIELDS] + [T["EXTRA_56"], T["EXTRA_57"], T["EXTRA_58"]]
|
| 39 |
+
HEADER_LEN = 1 + len(FIELDS)
|
| 40 |
+
|
| 41 |
+
# ---------------- model (identical to training) ----------------
|
| 42 |
+
class RMSNorm(nn.Module):
|
| 43 |
+
def __init__(s, d, eps=1e-5): super().__init__(); s.w = nn.Parameter(torch.ones(d)); s.eps = eps
|
| 44 |
+
def forward(s, x):
|
| 45 |
+
xf = x.float(); return (xf*torch.rsqrt(xf.pow(2).mean(-1,keepdim=True)+s.eps)).type_as(x)*s.w.to(x.dtype)
|
| 46 |
+
|
| 47 |
+
def rope_cache(T_, hd, base=10000.0):
|
| 48 |
+
inv = 1.0/(base**(torch.arange(0,hd,2).float()/hd)); f = torch.outer(torch.arange(T_).float(), inv)
|
| 49 |
+
return f.cos(), f.sin()
|
| 50 |
+
def apply_rope(x, cos, sin):
|
| 51 |
+
x1, x2 = x[...,::2], x[...,1::2]; c, s_ = cos[:x.size(2)].to(x.dtype), sin[:x.size(2)].to(x.dtype)
|
| 52 |
+
return torch.stack([x1*c-x2*s_, x1*s_+x2*c], -1).flatten(-2)
|
| 53 |
+
|
| 54 |
+
class Block(nn.Module):
|
| 55 |
+
def __init__(s, c):
|
| 56 |
+
super().__init__(); d = c["d_model"]; s.h = c["n_head"]; s.dp = c["dropout"]
|
| 57 |
+
s.n1, s.n2 = RMSNorm(d), RMSNorm(d)
|
| 58 |
+
s.qkv = nn.Linear(d, 3*d, bias=False); s.o = nn.Linear(d, d, bias=False)
|
| 59 |
+
s.w1 = nn.Linear(d, c["d_ff"], bias=False); s.w3 = nn.Linear(d, c["d_ff"], bias=False); s.w2 = nn.Linear(c["d_ff"], d, bias=False)
|
| 60 |
+
s.drop = nn.Dropout(c["dropout"])
|
| 61 |
+
def forward(s, x, cos, sin):
|
| 62 |
+
B,T_,C = x.shape
|
| 63 |
+
q,k,v = s.qkv(s.n1(x)).view(B,T_,3,s.h,C//s.h).permute(2,0,3,1,4)
|
| 64 |
+
q, k = apply_rope(q,cos,sin), apply_rope(k,cos,sin)
|
| 65 |
+
y = F.scaled_dot_product_attention(q,k,v,is_causal=True,dropout_p=s.dp if s.training else 0.0)
|
| 66 |
+
x = x + s.drop(s.o(y.transpose(1,2).reshape(B,T_,C)))
|
| 67 |
+
h = s.n2(x); return x + s.drop(s.w2(F.silu(s.w1(h))*s.w3(h)))
|
| 68 |
+
|
| 69 |
+
class GPT(nn.Module):
|
| 70 |
+
def __init__(s, c):
|
| 71 |
+
super().__init__(); s.c = c
|
| 72 |
+
s.emb = nn.Embedding(c["vocab"], c["d_model"]); s.blocks = nn.ModuleList(Block(c) for _ in range(c["n_layer"]))
|
| 73 |
+
s.norm = RMSNorm(c["d_model"]); s.head = nn.Linear(c["d_model"], c["vocab"], bias=False); s.head.weight = s.emb.weight
|
| 74 |
+
cos, sin = rope_cache(c["max_len"], c["d_model"]//c["n_head"])
|
| 75 |
+
s.register_buffer("cos", cos, persistent=False); s.register_buffer("sin", sin, persistent=False)
|
| 76 |
+
def forward(s, idx):
|
| 77 |
+
x = s.emb(idx)
|
| 78 |
+
for b in s.blocks: x = b(x, s.cos, s.sin)
|
| 79 |
+
return s.head(s.norm(x))
|
| 80 |
+
|
| 81 |
+
# ---------------- MIDI codec ----------------
|
| 82 |
+
def decode(toks):
|
| 83 |
+
toks = [int(t) for t in toks]; notes, bar, pos, pitch = [], -1, None, None
|
| 84 |
+
for t in toks[HEADER_LEN:]:
|
| 85 |
+
if t == EOS: break
|
| 86 |
+
if t == BAR_ID: bar += 1; pos = pitch = None
|
| 87 |
+
elif POS0 <= t < POS0+32: pos = t-POS0
|
| 88 |
+
elif PITCH0 <= t < PITCH0+128: pitch = t-PITCH0
|
| 89 |
+
elif DUR1 <= t < DUR1+32:
|
| 90 |
+
if bar >= 0 and pos is not None and pitch is not None: notes.append((bar,pos,pitch,t-DUR1+1))
|
| 91 |
+
pos = pitch = None
|
| 92 |
+
return notes
|
| 93 |
+
|
| 94 |
+
def to_midi(toks, path, ts=(4,4), bpm=120, program=0):
|
| 95 |
+
spb = STEPS_PER_BAR[ts]; notes = sorted(decode(toks))
|
| 96 |
+
pm = pretty_midi.PrettyMIDI(initial_tempo=bpm)
|
| 97 |
+
pm.time_signature_changes.append(pretty_midi.TimeSignature(ts[0], ts[1], 0))
|
| 98 |
+
inst = pretty_midi.Instrument(program=int(program)); sec = 60.0/bpm/4
|
| 99 |
+
for i,(b,p,pi,d) in enumerate(notes):
|
| 100 |
+
s = b*spb+p
|
| 101 |
+
if i+1 < len(notes): d = min(d, notes[i+1][0]*spb+notes[i+1][1]-s)
|
| 102 |
+
inst.notes.append(pretty_midi.Note(velocity=90, pitch=pi, start=s*sec, end=(s+max(1,d))*sec))
|
| 103 |
+
pm.instruments.append(inst); pm.write(str(path)); return len(notes)
|
| 104 |
+
|
| 105 |
+
def detect_key(pcs):
|
| 106 |
+
maj = np.array([6.35,2.23,3.48,2.33,4.38,4.09,2.52,5.19,2.39,3.66,2.29,2.88])
|
| 107 |
+
mnr = np.array([6.33,2.68,3.52,5.38,2.60,3.53,2.54,4.75,3.98,2.69,3.34,3.17])
|
| 108 |
+
if pcs.std() == 0: return 0
|
| 109 |
+
best = (-9, 0)
|
| 110 |
+
for mode, prof in enumerate((maj, mnr)):
|
| 111 |
+
for t in range(12):
|
| 112 |
+
c = np.corrcoef(pcs, np.roll(prof, t))[0,1]
|
| 113 |
+
if c > best[0]: best = (c, mode*12+t)
|
| 114 |
+
return best[1]
|
| 115 |
+
|
| 116 |
+
# ---------------- prompt parser (rule-based) ----------------
|
| 117 |
+
_PC = {"C":0,"D":2,"E":4,"F":5,"G":7,"A":9,"B":11}
|
| 118 |
+
def _key_from(letter, acc, mode):
|
| 119 |
+
pc = (_PC[letter.upper()] + (1 if acc in ("#","♯","sharp") else -1 if acc in ("b","♭","flat") else 0)) % 12
|
| 120 |
+
return (NOTE_NAMES[pc], mode)
|
| 121 |
+
def parse_key(t):
|
| 122 |
+
m = re.search(r"\b([A-Ga-g])\s*(#|♯|b|♭|sharp|flat)?\s*(major|minor|maj|min)\b", t, re.I)
|
| 123 |
+
if m: return _key_from(m.group(1), (m.group(2) or "").lower(), "minor" if m.group(3).lower().startswith("min") else "major")
|
| 124 |
+
m = re.search(r"\b([A-G])(#|♯|b|♭)?m\b", t)
|
| 125 |
+
if m: return _key_from(m.group(1), m.group(2) or "", "minor")
|
| 126 |
+
m = re.search(r"\bkey\s*(?:of|:)?\s*([A-G])(#|♯|b|♭)?(?![a-z#])", t)
|
| 127 |
+
if m: return _key_from(m.group(1), m.group(2) or "", "major")
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
TEMPO_WORDS = [("very slow",55),("very fast",170),("slow",70),("ballad",70),("adagio",65),("andante",85),("moderate",105),
|
| 131 |
+
("medium",105),("mid-tempo",105),("upbeat",125),("allegro",130),("fast",140),("presto",170)]
|
| 132 |
+
INSTR_WORDS = {"piano":0,"electric piano":4,"harpsichord":6,"vibraphone":11,"marimba":12,"organ":19,"accordion":21,"harmonica":22,
|
| 133 |
+
"acoustic guitar":24,"nylon guitar":24,"steel guitar":25,"guitar":25,"electric guitar":27,"bass":33,
|
| 134 |
+
"violin":40,"viola":41,"cello":42,"strings":48,"harp":46,"choir":52,"voice":53,"trumpet":56,"trombone":57,"horn":60,"brass":61,
|
| 135 |
+
"saxophone":65,"sax":65,"oboe":68,"clarinet":71,"flute":73,"recorder":74,"pan flute":75,"synth lead":80,"synth":80,
|
| 136 |
+
"pad":88,"banjo":105,"sitar":104,"bell":14,"bells":14}
|
| 137 |
+
ALIASES = [("hip hop","hiphop"),("new age","newage"),("easy listening","easylistening"),("lo-fi","lofi"),("r&b","rnb")]
|
| 138 |
+
|
| 139 |
+
class Apollo:
|
| 140 |
+
def __init__(self, model, labels, dev): self.model, self.labels, self.dev = model, labels, dev
|
| 141 |
+
|
| 142 |
+
def parse(self, text):
|
| 143 |
+
low = " " + text.lower() + " "
|
| 144 |
+
for a, b in ALIASES: low = low.replace(a, b)
|
| 145 |
+
spec = dict(bpm=None, ts=(4,4), key=parse_key(" " + text + " "), bars=8, dens=None, reg=None, genre=None, mood=None, program=None)
|
| 146 |
+
m = re.search(r"\b(\d{1,2})\s*-?\s*bars?\b", low)
|
| 147 |
+
if m: spec["bars"] = int(np.clip(int(m.group(1)), 1, 16))
|
| 148 |
+
m = re.search(r"\b(\d{2,3})\s*(?:bpm|beats per minute)\b", low) or re.search(r"\btempo\s*(?:of|:|=)?\s*(\d{2,3})\b", low)
|
| 149 |
+
if m: spec["bpm"] = int(np.clip(int(m.group(1)), 40, 279))
|
| 150 |
+
else:
|
| 151 |
+
for w, v in TEMPO_WORDS:
|
| 152 |
+
if re.search(rf"\b{re.escape(w)}\b", low): spec["bpm"] = v; break
|
| 153 |
+
m = re.search(r"\b(\d{1,2})\s*/\s*(\d{1,2})\b", low)
|
| 154 |
+
if m and (int(m.group(1)), int(m.group(2))) in TS_LIST: spec["ts"] = (int(m.group(1)), int(m.group(2)))
|
| 155 |
+
elif "waltz" in low: spec["ts"] = (3,4)
|
| 156 |
+
for w, v in (("sparse",1),("simple",2),("busy",6),("dense",6)):
|
| 157 |
+
if re.search(rf"\b{w}\b", low): spec["dens"] = v; break
|
| 158 |
+
if "low register" in low: spec["reg"] = 1
|
| 159 |
+
elif "high register" in low: spec["reg"] = 6
|
| 160 |
+
for g in sorted(self.labels["genre"], key=len, reverse=True):
|
| 161 |
+
if re.search(rf"\b{re.escape(g)}\b", low): spec["genre"] = g; break
|
| 162 |
+
for md in sorted(self.labels["mood"], key=len, reverse=True):
|
| 163 |
+
if re.search(rf"\b{re.escape(md)}\b", low): spec["mood"] = md; break
|
| 164 |
+
for w in sorted(INSTR_WORDS, key=len, reverse=True):
|
| 165 |
+
if re.search(rf"\b{re.escape(w)}\b", low): spec["program"] = INSTR_WORDS[w]; break
|
| 166 |
+
return spec
|
| 167 |
+
|
| 168 |
+
@torch.no_grad()
|
| 169 |
+
def generate(self, bpm=None, ts=(4,4), key=None, bars=8, dens=None, reg=None, genre=None, mood=None, program=None,
|
| 170 |
+
temp=0.9, top_p=0.92, seed=None, max_new=1500):
|
| 171 |
+
dev = self.dev
|
| 172 |
+
g = torch.Generator(device=dev); g.manual_seed(seed if seed is not None else int(time.time()))
|
| 173 |
+
spb = STEPS_PER_BAR[ts]
|
| 174 |
+
hdr = [BOS,
|
| 175 |
+
UNK_IDS[0] if bpm is None else FIELD_BASE["TEMPO"]+int(np.clip((bpm-40)//10,0,23)),
|
| 176 |
+
FIELD_BASE["TS"]+TS_LIST.index(ts),
|
| 177 |
+
UNK_IDS[2] if key is None else FIELD_BASE["KEY"]+(12 if key[1]=="minor" else 0)+NOTE_NAMES.index(key[0]),
|
| 178 |
+
(FIELD_BASE["BARS"]+bars-1) if bars else UNK_IDS[3],
|
| 179 |
+
UNK_IDS[4] if dens is None else FIELD_BASE["DENS"]+dens,
|
| 180 |
+
UNK_IDS[5] if reg is None else FIELD_BASE["REG"]+reg,
|
| 181 |
+
UNK_IDS[6] if genre is None else FIELD_BASE["GENRE"]+self.labels["genre"].index(genre),
|
| 182 |
+
UNK_IDS[7] if mood is None else FIELD_BASE["MOOD"]+self.labels["mood"].index(mood),
|
| 183 |
+
UNK_IDS[8] if program is None else FIELD_BASE["INSTR"]+int(program)//8]
|
| 184 |
+
lo_b, hi_b = (bars, bars) if bars else (2, 16)
|
| 185 |
+
out = list(hdr); state, bars_done, last_pos = "start", 0, -1
|
| 186 |
+
for _ in range(max_new):
|
| 187 |
+
x = torch.tensor([out[-SEQ_LEN:]], device=dev)
|
| 188 |
+
logits = self.model(x)[0,-1].float()
|
| 189 |
+
allow = torch.zeros(VOCAB, dtype=torch.bool, device=dev)
|
| 190 |
+
if state == "start": allow[BAR_ID] = True
|
| 191 |
+
elif state == "pos": allow[PITCH0:PITCH0+128] = True
|
| 192 |
+
elif state == "pitch": allow[DUR1:DUR1+32] = True
|
| 193 |
+
else:
|
| 194 |
+
if last_pos+1 < spb: allow[POS0+last_pos+1:POS0+spb] = True
|
| 195 |
+
if bars_done < hi_b: allow[BAR_ID] = True
|
| 196 |
+
if bars_done >= lo_b and last_pos >= 0: allow[EOS] = True
|
| 197 |
+
if not allow.any(): allow[EOS] = True
|
| 198 |
+
l = logits.masked_fill(~allow, float("-inf"))/temp
|
| 199 |
+
p = torch.softmax(l, -1); sp, si = p.sort(descending=True)
|
| 200 |
+
sp[(sp.cumsum(0)-sp) > top_p] = 0; sp /= sp.sum()
|
| 201 |
+
tok = int(si[torch.multinomial(sp, 1, generator=g)]); out.append(tok)
|
| 202 |
+
if tok == EOS: break
|
| 203 |
+
if tok == BAR_ID: bars_done += 1; last_pos = -1; state = "free"
|
| 204 |
+
elif POS0 <= tok < POS0+32: last_pos = tok-POS0; state = "pos"
|
| 205 |
+
elif PITCH0 <= tok < PITCH0+128: state = "pitch"
|
| 206 |
+
else: state = "free"
|
| 207 |
+
if out[-1] != EOS: out.append(EOS)
|
| 208 |
+
return out
|
| 209 |
+
|
| 210 |
+
def make(self, prompt, n=3, seed=None, outdir="apollo_out", temp=0.9, top_p=0.92, **override):
|
| 211 |
+
"""prompt -> n MIDI files. Extra kwargs override parsed fields, e.g. bpm=100, key=("D","minor"), program=40."""
|
| 212 |
+
spec = self.parse(prompt); spec.update(override)
|
| 213 |
+
print("parsed:", {k: v for k, v in spec.items() if v is not None})
|
| 214 |
+
Path(outdir).mkdir(parents=True, exist_ok=True)
|
| 215 |
+
slug = re.sub(r"[^a-z0-9]+", "_", prompt.lower()).strip("_")[:40] or "melody"
|
| 216 |
+
base = seed if seed is not None else int(time.time()) % 100000
|
| 217 |
+
paths = []
|
| 218 |
+
for i in range(n):
|
| 219 |
+
toks = self.generate(temp=temp, top_p=top_p, seed=base+i, **spec)
|
| 220 |
+
path = f"{outdir}/{slug}_{base+i}.mid"
|
| 221 |
+
nn_ = to_midi(toks, path, ts=spec["ts"], bpm=spec["bpm"] or 120, program=spec["program"] if spec["program"] is not None else 0)
|
| 222 |
+
info = f"{nn_} notes"
|
| 223 |
+
notes = decode(toks)
|
| 224 |
+
if len(notes) >= 8:
|
| 225 |
+
pcs = np.zeros(12)
|
| 226 |
+
for _, _, p_, d in notes: pcs[p_ % 12] += d
|
| 227 |
+
k = detect_key(pcs); info += f" | detected key {NOTE_NAMES[k % 12]} {'minor' if k >= 12 else 'major'}"
|
| 228 |
+
print(f" seed {base+i}: {path} | {info}"); paths.append(path)
|
| 229 |
+
return paths
|
| 230 |
+
|
| 231 |
+
# ---------------- loader ----------------
|
| 232 |
+
def _token(token=None):
|
| 233 |
+
if token: return token
|
| 234 |
+
try:
|
| 235 |
+
from google.colab import userdata
|
| 236 |
+
return userdata.get("HF_TOKEN")
|
| 237 |
+
except Exception:
|
| 238 |
+
return os.environ.get("HF_TOKEN") # None is fine for a public repo
|
| 239 |
+
|
| 240 |
+
def load(repo=REPO, device=None, token=None):
|
| 241 |
+
from huggingface_hub import hf_hub_download
|
| 242 |
+
from safetensors.torch import load_file
|
| 243 |
+
tok = _token(token); dev = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 244 |
+
get = lambda f: hf_hub_download(repo, f, token=tok)
|
| 245 |
+
cfg = json.load(open(get("config.json")))["cfg"]
|
| 246 |
+
assert json.load(open(get("vocab.json"))) == NAMES, "vocab.json in the repo differs from the vocab in this file"
|
| 247 |
+
labels = json.load(open(get("labels.json")))
|
| 248 |
+
model = GPT(cfg)
|
| 249 |
+
missing, unexpected = model.load_state_dict({k: v.float() for k, v in load_file(get("model.safetensors")).items()}, strict=False)
|
| 250 |
+
assert set(missing) <= {"head.weight"} and not unexpected, f"weights do not match the architecture: missing={missing} unexpected={unexpected}"
|
| 251 |
+
model = (model.half() if dev == "cuda" else model.float()).to(dev).eval()
|
| 252 |
+
print(f"loaded {repo} on {dev} | {sum(p.numel() for p in model.parameters())/1e6:.1f}M params")
|
| 253 |
+
return Apollo(model, labels, dev)
|
| 254 |
+
|
| 255 |
+
if __name__ == "__main__":
|
| 256 |
+
import sys
|
| 257 |
+
load().make(" ".join(sys.argv[1:]) or "8 bars in D minor at 90 bpm, sad piano melody", n=3, seed=1)
|