Spaces:
Paused
Paused
File size: 10,569 Bytes
e319a7b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | """MT3 MIDI tokenizer.
Adapted from YourMT3+ (https://github.com/mimbres/YourMT3) and audiocraft_trans.
"""
import difflib
import logging
from collections.abc import Iterable
from muscriptor.tokenizer.notes import (
DRUM_PROGRAM,
SPECIAL_TOKENS,
build_event_vocab,
)
logger = logging.getLogger(__name__)
def get_group_program_map(
instrument_vocabulary: str,
misc_programs: str,
is_mt3: bool = False,
include_drums: bool = False,
) -> dict[int, list[int]]:
if instrument_vocabulary == "ONLY_PIANO":
ret = {0: list(range(128))}
elif instrument_vocabulary == "FULL":
ret = {i: [i] for i in range(128)}
elif instrument_vocabulary == "MT3_MIDI_PLUS":
ret = {
0: list(range(8)),
1: list(range(8, 16)),
2: list(range(16, 24)),
3: list(range(24, 32)),
4: list(range(32, 40)),
5: list(range(40, 56)),
6: list(range(56, 64)),
7: list(range(64, 72)),
8: list(range(72, 80)),
9: list(range(80, 88)),
10: list(range(88, 96)),
11: list(range(100, 102)),
}
elif instrument_vocabulary == "MT3_FULL_PLUS":
ret = {
0: [0, 1, 3, 6, 7],
1: [2, 4, 5],
2: list(range(8, 16)),
3: list(range(16, 24)),
4: [24, 25],
5: [26, 27, 28],
6: [29, 30, 31],
7: [32, 35],
8: [33, 34, 36, 37, 38, 39],
9: [40],
10: [41],
11: [42],
12: [43],
13: [46],
14: [47],
15: [48, 49, 44, 45],
16: [50, 51],
17: [52, 53, 54],
18: [55],
19: [56, 59],
20: [57],
21: [58],
22: [60],
23: [61, 62, 63],
24: [64, 65],
25: [66],
26: [67],
27: [68],
28: [69],
29: [70],
30: [71],
31: list(range(72, 80)),
32: list(range(80, 88)),
33: list(range(88, 96)),
34: [100],
35: [101],
}
elif instrument_vocabulary == "OURS_INSTRUMENT_GROUPS":
ret = {
0: list(range(8)),
1: list(range(24, 32)),
2: list(range(32, 40)),
3: list(range(40, 56)),
4: list(range(56, 64)),
5: list(range(16, 24)) + list(range(64, 80)),
6: list(range(80, 96)),
7: list(range(8, 16)) + list(range(112, 119)),
}
else:
assert False, instrument_vocabulary
if instrument_vocabulary == "MT3_FULL_PLUS" and not is_mt3:
not_assigned = set(range(130)) - set([v for vs in ret.values() for v in vs])
else:
not_assigned = set(range(128)) - set([v for vs in ret.values() for v in vs])
if include_drums:
not_assigned = not_assigned.union({DRUM_PROGRAM})
if misc_programs == "ONE_GROUP":
ret[len(ret)] = list(not_assigned)
elif misc_programs == "SINGLETON_GROUPS":
for p in not_assigned:
ret[len(ret)] = [p]
else:
assert misc_programs == "OMIT", misc_programs
return ret
# Human-readable names for the MT3_FULL_PLUS instrument groups (see
# get_group_program_map). Used by the CLI's --instruments option and the
# web app's /instruments endpoint. The group IDs index the model's learned
# program groups and must not change; only the user-facing names do.
# Notes the model still decodes into an omitted group surface as "program_<n>".
MT3_FULL_PLUS_GROUP_NAMES: dict[str, int] = {
"acoustic_piano": 0,
"electric_piano": 1,
"chromatic_percussion": 2,
"organ": 3,
"acoustic_guitar": 4,
"clean_electric_guitar": 5,
"distorted_electric_guitar": 6,
"acoustic_bass": 7,
"electric_bass": 8,
"violin": 9,
"viola": 10,
"cello": 11,
"contrabass": 12,
"orchestral_harp": 13,
"timpani": 14,
"string_ensemble": 15,
"synth_strings": 16,
"voice": 17,
"orchestra_hit": 18,
"trumpet": 19,
"trombone": 20,
"tuba": 21,
"french_horn": 22,
"brass_section": 23,
"soprano_and_alto_sax": 24,
"tenor_sax": 25,
"baritone_sax": 26,
"oboe": 27,
"english_horn": 28,
"bassoon": 29,
"clarinet": 30,
"flutes": 31,
"synth_lead": 32,
"synth_pad": 33,
"drums": 36,
}
def instrument_group_from_names(names: Iterable[str]) -> str:
"""Map exact instrument group names to the model's conditioning string.
The strict counterpart of :func:`resolve_instrument_names`: every name
must appear verbatim in ``MT3_FULL_PLUS_GROUP_NAMES``. Raises ValueError
listing the unknown names otherwise.
"""
names = list(names)
unknown = [n for n in names if n not in MT3_FULL_PLUS_GROUP_NAMES]
if unknown:
raise ValueError(
f"unknown instrument name(s): {', '.join(map(repr, unknown))}; "
f"valid names: {', '.join(MT3_FULL_PLUS_GROUP_NAMES)}"
)
return " ".join(str(MT3_FULL_PLUS_GROUP_NAMES[n]) for n in names)
def resolve_instrument_names(tokens: Iterable[str]) -> list[str]:
"""Resolve loosely-typed instrument tokens to canonical group names.
Matching is case-insensitive; a token that is not an exact name may be
any substring that matches exactly one group name (``"timp"`` →
``"timpani"``). Raises ValueError when a token is ambiguous (listing the
candidates) or matches nothing (suggesting close spellings).
"""
resolved = []
for token in tokens:
t = token.strip().lower()
if t in MT3_FULL_PLUS_GROUP_NAMES:
resolved.append(t)
continue
hits = [n for n in MT3_FULL_PLUS_GROUP_NAMES if t in n]
if len(hits) == 1:
resolved.append(hits[0])
elif hits:
raise ValueError(
f"ambiguous instrument name {token!r}: "
f"matches {', '.join(hits)}"
)
else:
# Compare against each name AND its underscore-separated words,
# so a typo like "pinao" still surfaces "acoustic_piano".
def closeness(name: str) -> float:
return max(
difflib.SequenceMatcher(None, t, part).ratio()
for part in (name, *name.split("_"))
)
ranked = sorted(MT3_FULL_PLUS_GROUP_NAMES, key=closeness, reverse=True)
suggestions = [n for n in ranked[:3] if closeness(n) >= 0.6]
hint = (
f" — did you mean {', '.join(suggestions)}?"
if suggestions
else ""
)
raise ValueError(f"unknown instrument name {token!r}{hint}")
return resolved
class MT3Tokenizer:
def __init__(
self,
instrument_vocabulary: str = "FULL",
max_shift_steps: int = 1001,
frame_rate: int = 100,
):
self.group_program_map = get_group_program_map(
instrument_vocabulary, misc_programs="SINGLETON_GROUPS", is_mt3=True
)
self.frame_rate = frame_rate
self._vocab = build_event_vocab(max_shift_steps)
self._token_index = {(e.type, e.value): i for i, e in enumerate(self._vocab)}
self.num_tokens = len(self._vocab)
self.eos_id = SPECIAL_TOKENS.index("EOS")
logger.info(f"MT3Tokenizer: {self.num_tokens} tokens")
def tie_section_token_ids(
self, open_note_keys: Iterable[tuple[int, int]]
) -> list[int]:
"""Encode a tie prologue declaring ``open_note_keys`` as sustained.
``open_note_keys`` are the ``(program, pitch)`` pairs of notes still
sounding at a chunk boundary. The layout matches the training encoder
(``note_event2event``): pairs sorted by (program, pitch), each program
token emitted once for its run of pitches, terminated by the ``tie``
token. Teacher-forcing these as the start of a chunk pins the model's
tie section to the notes actually sustained from the previous chunk.
"""
tokens: list[int] = []
program_state: int | None = None
for program, pitch in sorted(open_note_keys):
if program != program_state:
tokens.append(self._token_index[("program", program)])
program_state = program
tokens.append(self._token_index[("pitch", pitch)])
tokens.append(self._token_index[("tie", 0)])
return tokens
def forbidden_token_ids(self, instruments: Iterable[str]) -> list[int]:
"""Token ids that must never be sampled when only ``instruments`` may
appear in the transcription (the hard counterpart of the advisory
instrument_group conditioning).
``instruments`` are exact MT3_FULL_PLUS group names (so this only makes
sense on a tokenizer built with that vocabulary). A ``program`` token is
forbidden unless it decodes to one of the given groups — i.e. it is the
representative (first) program of an allowed group; ``drum`` tokens are
forbidden unless "drums" is listed. Timing, pitch, velocity, tie and
special tokens are never forbidden. Raises ValueError on unknown names.
"""
names = list(instruments)
unknown = [n for n in names if n not in MT3_FULL_PLUS_GROUP_NAMES]
if unknown:
raise ValueError(
f"unknown instrument name(s): {', '.join(map(repr, unknown))}; "
f"valid names: {', '.join(MT3_FULL_PLUS_GROUP_NAMES)}"
)
allow_drums = "drums" in names
# Same representative-program convention as decoding
# (transcription_model._build_instrument_for_program): the model emits
# the first program of a group, so only that program is allowed.
allowed_programs = set()
for name in names:
if name == "drums":
continue
gid = MT3_FULL_PLUS_GROUP_NAMES[name]
if gid in self.group_program_map and self.group_program_map[gid]:
allowed_programs.add(self.group_program_map[gid][0])
forbidden = []
for token_id, event in enumerate(self._vocab):
if event.type == "program" and event.value not in allowed_programs:
forbidden.append(token_id)
elif event.type == "drum" and not allow_drums:
forbidden.append(token_id)
return forbidden
|