jerryzhou196
Initial chord service
e319a7b unverified
Raw
History Blame Contribute Delete
10.6 kB
"""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