Spaces:
Sleeping
Sleeping
| """Token vocabulary for chart sequences. | |
| Sequence layout (decoder): | |
| [BOS] [COURSE_c] [LEVEL_l] [DENS_d] ([TIME_t] [NOTE_e])* [EOS] | |
| Conditions may be UNK (condition dropout / unspecified at inference). | |
| """ | |
| from dataclasses import dataclass | |
| # --- audio / windowing constants (single source of truth) --- | |
| SR = 22050 | |
| N_FFT = 2048 | |
| HOP = 256 | |
| N_MELS = 128 | |
| FPS = SR / HOP # 86.1328125 frames/sec | |
| WINDOW = 1728 # frames per training window (~20.06 s) | |
| MAX_TGT = 1536 # max decoder length (prefix + events + eos) | |
| SLOTS = 96 # TJA lattice: slots per 4/4 measure (LCM of 16ths/triplets/32nds) | |
| MEAS_MAX = WINDOW // SLOTS # max whole measures per window in slot mode (18) | |
| SLOTS_PER_BEAT = 24 # meter-general lattice (v3): 3/4 -> 72, 5/4 -> 120 slots | |
| BEATS_MAX = WINDOW // SLOTS_PER_BEAT # 72 beats of token space per window | |
| COURSES = ["easy", "normal", "hard", "oni", "ura"] | |
| N_LEVELS = 12 # levels clamped to 1..12 | |
| N_DENS = 16 | |
| DENS_BUCKET_NPS = 0.75 # bucket width in notes/sec | |
| # canonical note classes; parsed TJA note_type strings are mapped onto these | |
| NOTE_CLASSES = ["don", "ka", "don_big", "ka_big", "roll", "roll_big", "balloon", "end"] | |
| # mapping from strings observed in the dataset -> canonical class | |
| # (observed in taiko-1000-parsed: Don, Ka, DonBig, KaBig, Roll, RollBig, | |
| # Balloon, BalloonAlt, EndOf) | |
| NOTE_TYPE_MAP = { | |
| "Don": "don", | |
| "Ka": "ka", | |
| "DonBig": "don_big", | |
| "KaBig": "ka_big", | |
| "Roll": "roll", | |
| "RollBig": "roll_big", | |
| "Balloon": "balloon", | |
| "BalloonAlt": "balloon", # kusudama, treated as balloon-class span | |
| "EndOf": "end", | |
| } | |
| class Vocab: | |
| pad: int = 0 | |
| eos: int = 1 | |
| bos: int = 2 | |
| unk_cond: int = 3 | |
| def __post_init__(self): | |
| base = 4 | |
| self.course = {c: base + i for i, c in enumerate(COURSES)} | |
| base += len(COURSES) | |
| self.level = {l: base + l - 1 for l in range(1, N_LEVELS + 1)} | |
| base += N_LEVELS | |
| self.dens = {d: base + d for d in range(N_DENS)} | |
| base += N_DENS | |
| self.note = {n: base + i for i, n in enumerate(NOTE_CLASSES)} | |
| base += len(NOTE_CLASSES) | |
| self.time0 = base | |
| base += WINDOW | |
| # Extended condition tokens append after the original token ids. | |
| self.sep = base # separates prev-window context from conditions | |
| self.sib = base + 1 # marks the sibling-chart (easier course) segment | |
| self.style = {s: base + 2 + s for s in range(8)} # charting-intent codes | |
| self.sync = {s: base + 10 + s for s in range(6)} # LHL syncopation bands | |
| # plan-realize tokens: [PLAN] marker + block density (8) + block flags (3) | |
| self.plan = base + 16 | |
| self.pdens = {d: base + 17 + d for d in range(8)} | |
| self.pflag = {f: base + 25 + f for f in range(3)} # 0 none / 1 gap / 2 climax | |
| self.mask = base + 28 # type-infill placeholder | |
| self.cplx = {c: base + 29 + c for c in range(6)} # rhythmic-complexity band | |
| # dual-mode models: explicit output-semantics switch (v3). mode "slot" | |
| # = positions are lattice indices on a supplied grid; "time" = frames. | |
| self.mode = {"slot": base + 35, "time": base + 36} | |
| self.size = base + 37 | |
| self.id2note = {v: k for k, v in self.note.items()} | |
| self.id2course = {v: k for k, v in self.course.items()} | |
| def time(self, frame: int) -> int: | |
| assert 0 <= frame < WINDOW | |
| return self.time0 + frame | |
| def is_time(self, tok: int) -> bool: | |
| return self.time0 <= tok < self.time0 + WINDOW | |
| def is_note(self, tok: int) -> bool: | |
| return tok in self.id2note | |
| def dens_bucket(self, nps: float) -> int: | |
| return min(N_DENS - 1, max(0, int(nps / DENS_BUCKET_NPS))) | |
| VOCAB = Vocab() | |
| SIB_EVENTS = 12 # fixed number of sibling-chart events in the prefix (24 tokens) | |
| PLAN_SLOTS = 5 # fixed number of plan blocks in the prefix (2 tokens each) | |
| def complexity_band(frames): | |
| """IOI-class entropy of a window's hit sequence -> band 0-5 (rhythmic | |
| complexity independent of raw density).""" | |
| import math | |
| hits = sorted(f for f, c in frames if NOTE_CLASSES[c] in ("don","ka","don_big","ka_big")) | |
| if len(hits) < 4: | |
| return 0 | |
| iois = [b - a for a, b in zip(hits, hits[1:]) if b > a] | |
| if not iois: | |
| return 0 | |
| from collections import Counter | |
| cls = Counter(int(round(math.log2(max(i,1)) * 2)) for i in iois) | |
| tot = sum(cls.values()) | |
| ent = -sum((n/tot) * math.log2(n/tot) for n in cls.values()) | |
| return min(5, int(ent / 0.5)) | |
| def encode_window(vocab, course, level, notes, cond_drop=0.0, rng=None, ctx_types=None, | |
| sib_pairs=None, style=None, sync_band=None, plan_slice=None, | |
| complexity=None, mode=None): | |
| """Build a token sequence for one window. | |
| notes: list of (frame_idx, note_class_id) sorted by frame, frame in [0, WINDOW). | |
| ctx_types: optional list of note-class ids from the previous window's tail | |
| (pattern continuity context, v2). Encoded as [BOS] ctx.. [SEP] conds.. | |
| sib_pairs: optional list of (frame, class_id) events from an easier course of | |
| the same song (skeleton hint, easy⊂hard). Fixed SIB_EVENTS slots, | |
| missing slots filled with UNK. Encoded after [SIB]. | |
| Returns (tokens, prefix_len) where loss should be applied after the prefix. | |
| """ | |
| n_hits = sum(1 for _, c in notes if NOTE_CLASSES[c] not in ("end",)) | |
| nps = n_hits / (WINDOW / FPS) | |
| d = vocab.dens_bucket(nps) | |
| def maybe(tok): | |
| if cond_drop > 0 and rng is not None and rng.random() < cond_drop: | |
| return vocab.unk_cond | |
| return tok | |
| lvl = max(1, min(N_LEVELS, level if level and level > 0 else 1)) | |
| seq = [vocab.bos] | |
| if mode is not None: # dual-mode: explicit output-semantics token (never dropped) | |
| seq.append(vocab.mode[mode]) | |
| if ctx_types is not None: # fixed-length, left-padded with UNK (id < 0 = pad) | |
| seq += [vocab.note[NOTE_CLASSES[c]] if c >= 0 else vocab.unk_cond | |
| for c in ctx_types] | |
| seq.append(vocab.sep) | |
| if sib_pairs is not None: | |
| seq.append(vocab.sib) | |
| pairs = list(sib_pairs)[:SIB_EVENTS] | |
| for f, c in pairs: | |
| seq += [vocab.time(int(f)), vocab.note[NOTE_CLASSES[c]]] | |
| seq += [vocab.unk_cond] * (2 * (SIB_EVENTS - len(pairs))) | |
| seq += [ | |
| maybe(vocab.course[course]), | |
| maybe(vocab.level[lvl]), | |
| maybe(vocab.dens[d]), | |
| ] | |
| if style is not None: # charting-intent code (style >= 0; -1 = unknown) | |
| seq.append(maybe(vocab.style[style]) if style >= 0 else vocab.unk_cond) | |
| if sync_band is not None: # LHL syncopation band (groove-intensity control) | |
| seq.append(maybe(vocab.sync[sync_band]) if sync_band >= 0 else vocab.unk_cond) | |
| if complexity is not None: # rhythmic-complexity band (difficulty beyond density) | |
| seq.append(maybe(vocab.cplx[complexity]) if complexity >= 0 else vocab.unk_cond) | |
| if plan_slice is not None: # song-level plan blocks overlapping this window | |
| seq.append(vocab.plan) | |
| blocks = list(plan_slice)[:PLAN_SLOTS] | |
| for d8, fl in blocks: | |
| seq += [maybe(vocab.pdens[min(7, max(0, d8))]), | |
| maybe(vocab.pflag[min(2, max(0, fl))])] | |
| seq += [vocab.unk_cond] * (2 * (PLAN_SLOTS - len(blocks))) | |
| prefix_len = len(seq) | |
| for f, c in notes: | |
| seq.append(vocab.time(int(f))) | |
| seq.append(vocab.note[NOTE_CLASSES[c]]) | |
| seq.append(vocab.eos) | |
| return seq, prefix_len | |
| def decode_tokens(vocab, tokens): | |
| """Token ids -> list of (frame_idx, note_class_name). Ignores malformed pairs.""" | |
| out = [] | |
| cur_t = None | |
| for tok in tokens: | |
| if vocab.is_time(tok): | |
| cur_t = tok - vocab.time0 | |
| elif tok in vocab.id2note and cur_t is not None: | |
| out.append((cur_t, vocab.id2note[tok])) | |
| elif tok == vocab.eos: | |
| break | |
| return out | |