from math import inf from logitbias import logitbias_simplify, escape_to_zero from logitbias_json import logitbias_eval_all def mask_range(logits, low, high, outside=False): if outside: logits[:low] = -inf logits[high + 1 :] = -inf else: logits[low : high + 1] = -inf class LogitProcessor: def reset(self): pass def __call__(self, ys, logits): raise NotImplementedError() class SyntaxChecker(LogitProcessor): def __init__( self, vocab, min_polyphony, max_polyphony, allow_new_track=False, allow_end=False, allow_description_anchor=False, ): super().__init__() self.vocab = vocab if min_polyphony is not None: if min_polyphony < 0: raise ValueError("min_polyphony must be >= 0") if min_polyphony > vocab.polyphony_max: raise ValueError("min_polyphony must be <= vocab.polyphony_max") if max_polyphony is not None and max_polyphony < min_polyphony: raise ValueError("max_polyphony must be >= min_polyphony") if min_polyphony < 2: min_polyphony = None if max_polyphony is not None: max_polyphony = min(max_polyphony, vocab.polyphony_max) else: max_polyphony = vocab.polyphony_max self.min_polyphony = min_polyphony self.max_polyphony = max_polyphony self.allow_new_track = allow_new_track self.allow_end = allow_end self.allow_description_anchor = allow_description_anchor def __call__(self, ys, logits): assert ys.shape[0] > 0 if ys.shape[0] >= 2: # no key symbols after header mask_range( logits, self.vocab.keys_least_index, self.vocab.keys_greatest_index, ) expect_duration = False allow_polyphony = False logits[self.vocab.pad.index] = -inf logits[self.vocab.begin.index] = -inf if not self.allow_end: logits[self.vocab.end.index] = -inf if not self.allow_new_track: logits[self.vocab.new_track.index] = -inf if not self.allow_description_anchor: logits[self.vocab.description_anchor.index] = -inf lastsym = ys[-1, 0].item() # polyphony can be after header, new_track, description_anchor, or rest was_rest = ys.shape[0] > 1 and self.vocab.is_rest(ys[-2, 0].item()) allow_polyphony = ( self.vocab.is_begin(lastsym) or self.vocab.is_new_track(lastsym) or self.vocab.is_key(lastsym) or self.vocab.is_description_anchor(lastsym) or was_rest ) # duration always and only follows pitch or rest expect_duration = self.vocab.is_pitch(lastsym) or self.vocab.is_rest(lastsym) mask_range( logits, self.vocab.durations_least_index, self.vocab.durations_greatest_index, outside=expect_duration, ) if self.vocab.is_polyphony(lastsym): mask_range( logits, self.vocab.pitch_least_index, self.vocab.pitch_greatest_index, outside=True, ) # polyphony constraints next_polyphony = ys[-1, 8].item() num_over_limit = next_polyphony - self.max_polyphony num_over_commitment = -1 if allow_polyphony: # mask polyphony symbols that would exceed max_polyphony given current polyphony mask_range( logits, self.vocab.polyphony_least_index + max(1, -num_over_limit) - 1, self.vocab.polyphony_greatest_index, ) # enforce minimum polyphony if self.min_polyphony is not None: mask_range( logits, self.vocab.polyphony_least_index + self.min_polyphony - 2, self.vocab.polyphony_greatest_index, outside=True, ) # but do not mask rests! logits[self.vocab.rest.index] = 0 # TODO fail fast here if infeasible else: mask_range( logits, self.vocab.polyphony_least_index, self.vocab.polyphony_greatest_index, ) # if already committed to a polyphony, figure out how many more notes are allowed committed_polyphony = 1 chord_size = 0 for i in range(ys.shape[0] - 1, -1, -1): sym = ys[i, 0].item() if self.vocab.is_polyphony(sym): committed_polyphony = self.vocab.symbols[sym].value break if self.vocab.is_pitch(sym): chord_size += 1 if self.vocab.is_rest(sym): break num_over_commitment = chord_size - committed_polyphony # only one rest in a row if was_rest: logits[self.vocab.rest.index] = -inf if (num_over_limit >= 0 or num_over_commitment >= 0) and not expect_duration: # we are at max polyphony, so prevent new pitches mask_range( logits, self.vocab.pitch_least_index, self.vocab.pitch_greatest_index, ) if num_over_limit >= 0 and self.vocab.is_rest(lastsym): # if generating a rest duration, mask durations too short to allow generation of a pitch # (since the pitch would exceed max_polyphony) note_end_times = [] p = next_polyphony t = self.vocab.get_tensor_subbeat(ys[-1], next=True).item() for j in range(2, ys.shape[0] + 1): sym = ys[-j, 0].item() nextsym = ys[-j + 1, 0].item() if self.vocab.is_pitch(sym) and self.vocab.is_duration(nextsym): start_subbeat = self.vocab.get_tensor_subbeat(ys[-j]).item() end_subbeat = start_subbeat + self.vocab.symbols[nextsym].value if end_subbeat > t: note_end_times.append(end_subbeat) p -= 1 if p == 0: break note_end_times.sort() free_up_polyphony = min( ( num_over_limit + 0 if self.min_polyphony is None else (self.min_polyphony - 1) ), len(note_end_times) - 1, ) dt = note_end_times[free_up_polyphony] - t # brain teaser mask_range( logits, self.vocab.durations_least_index, ( self.vocab.durations[dt].index - 1 if dt <= self.vocab.duration_max else self.vocab.durations_greatest_index ), # this fails faster if we're already infeasible ) class BiasFormer(LogitProcessor): def __init__(self, vocab, bias, count=None): super().__init__() self.vocab = vocab self.bias = bias self.count = count self.env = {} self.last_seq_len = 0 self.pitch_idx_vals = [ ( (min(v.index for v in vs.values()), max(v.index for v in vs.values())), p, ) for p, vs in vocab.pitches.items() ] self.polyphony_idx_vals = [(p.index, p.value) for p in vocab.polyphony.values()] self.duration_idx_vals = [ (p.index, p.value / vocab.quantize_divisions) for p in vocab.durations.values() ] def reset(self): self.env = {} self.last_seq_len = 0 self.count = None def update_env(self, ys): if ys.shape[0] < self.last_seq_len: self.env = {} self.last_seq_len = 0 elif ys.shape[0] == self.last_seq_len: return env = self.env pitches = env.get("pitches", []) lengths = env.get("lengths", []) start = env.get("start", True) subbeat = env.get("subbeat", -1) for i in range(self.last_seq_len, ys.shape[0]): lastval = ys[i - 1, 0].item() if i > 0 else -1 val = ys[i, 0].item() val_subbeat = self.vocab.get_tensor_subbeat(ys[i]).item() if self.vocab.is_begin(val) or self.vocab.is_new_track(val): start = True subbeat = -1 continue if not (self.vocab.is_key(val) or self.vocab.is_description_anchor(val)): start = False if ( subbeat < val_subbeat and self.vocab.is_duration(val) and self.vocab.is_pitch(lastval) ): subbeat = val_subbeat pitches.append(self.vocab.symbols[lastval].value) lengths.append( self.vocab.symbols[val].value / self.vocab.quantize_divisions ) self.last_seq_len = ys.shape[0] self.env = { "start": start, "pitches": pitches, "lengths": lengths, "subbeat": subbeat, # for own use "index": self.count if self.count is not None else 0, "beat": self.vocab.get_tensor_subbeat(ys[-1], next=True).item() / self.vocab.quantize_divisions, } def apply_parametric_bias_to_range( self, logits, env, variable, range_values, range_start, range_end ): if (logits[range_start : range_end + 1] == -inf).all(): return parametric_bias = logitbias_simplify(self.bias, env, variable) if callable(parametric_bias): for idx, val in range_values: delta = parametric_bias(val) if isinstance(idx, tuple): logits[idx[0] : idx[1] + 1] += delta else: logits[idx] += delta elif parametric_bias != 0: logits[range_start : range_end + 1,] += parametric_bias def __call__(self, ys, logits): self.update_env(ys) env = self.env lastsym = ys[-1, 0].item() if ys.shape[1] > 0 else -1 was_rest = self.vocab.is_rest(lastsym) env["length"] = escape_to_zero env["polyphony"] = escape_to_zero env["pitch"] = escape_to_zero env["rest"] = False self.apply_parametric_bias_to_range( logits, env, "pitch", self.pitch_idx_vals, self.vocab.pitch_least_index, self.vocab.pitch_greatest_index, ) self.apply_parametric_bias_to_range( logits, env, "polyphony", self.polyphony_idx_vals, self.vocab.polyphony_least_index, self.vocab.polyphony_greatest_index, ) env["rest"] = was_rest self.apply_parametric_bias_to_range( logits, env, "length", self.duration_idx_vals, self.vocab.durations_least_index, self.vocab.durations_greatest_index, ) env["rest"] = True logits[self.vocab.rest.index] += logitbias_eval_all(self.bias, env) class SearchException(Exception): pass