Improve speed of sage sheet decoder with yue2. Recover from tracks that fail
This commit is contained in:
1 parent
ce64e65c39
commit
410206e41c
7 files changed
+499
-4
No files matched your search
@@ -41,6 +41,22 @@ class _Attention(nn.Module):
|
||||
b, _, l, _ = out.shape
|
||||
return self.out_proj(out.transpose(1, 2).reshape(b, l, -1)), (k, v)
|
||||
|
||||
# static-cache path: explicit casts to the projection dtype (no-ops under autocast with fp32 weights)
|
||||
def kv(self, src):
|
||||
src = src.to(self.k_proj.weight.dtype)
|
||||
return self._split(self.k_proj(src)), self._split(self.v_proj(src))
|
||||
|
||||
def attend(self, x, k, v, mask):
|
||||
q = self._split(self.q_proj(x.to(self.q_proj.weight.dtype))).to(k.dtype)
|
||||
if mask is not None and q.shape[2] == 1:
|
||||
# single-query + mask sends SDPA to the mem-efficient kernel, which tiles over every cached key (~0.3 ms); by hand it is ~20 us
|
||||
scores = torch.matmul(q.float(), k.float().transpose(-1, -2)) * (self.head_dim**-0.5)
|
||||
out = torch.matmul(scores.masked_fill(~mask, float("-inf")).softmax(-1).to(v.dtype), v)
|
||||
else:
|
||||
out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
|
||||
b, _, l, _ = out.shape
|
||||
return self.out_proj(out.transpose(1, 2).reshape(b, l, -1).to(self.out_proj.weight.dtype))
|
||||
|
||||
|
||||
class _Layer(nn.Module):
|
||||
def __init__(self, dim: int, heads: int, ffn: int):
|
||||
@@ -63,6 +79,37 @@ class _Layer(nn.Module):
|
||||
x = self.final_layer_norm(x + self.fc2(F.gelu(self.fc1(x))))
|
||||
return x, self_kv + cross_kv
|
||||
|
||||
def step_static(self, x, cache, i, write_idx, mask):
|
||||
k, v = self.self_attn.kv(x)
|
||||
cache.k[i].index_copy_(2, write_idx, k.to(cache.k.dtype))
|
||||
cache.v[i].index_copy_(2, write_idx, v.to(cache.v.dtype))
|
||||
x = self.self_attn_layer_norm(x + self.self_attn.attend(x, cache.k[i], cache.v[i], mask))
|
||||
x = self.encoder_attn_layer_norm(x + self.encoder_attn.attend(x, cache.ck[i], cache.cv[i], None))
|
||||
return self.final_layer_norm(x + self.fc2(F.gelu(self.fc1(x.to(self.fc1.weight.dtype)))))
|
||||
|
||||
|
||||
class StaticCache:
|
||||
"""Preallocated batch-1 KV buffers so a decode step has no growing tensors and can be CUDA-graph replayed.
|
||||
``pos`` (device tensor) is the number of cached tokens; the captured step reads and bumps it in place."""
|
||||
|
||||
def __init__(self, decoder: "ScoreDecoder", max_len: int, memory_len: int, device, dtype):
|
||||
attn = decoder.layers[0].self_attn
|
||||
n, h, d = len(decoder.layers), attn.heads, attn.head_dim
|
||||
self.max_len = max_len
|
||||
self.k = torch.zeros(n, 1, h, max_len, d, device=device, dtype=dtype)
|
||||
self.v = torch.zeros_like(self.k)
|
||||
self.ck = torch.zeros(n, 1, h, memory_len, d, device=device, dtype=dtype)
|
||||
self.cv = torch.zeros_like(self.ck)
|
||||
self.pos = torch.zeros(1, dtype=torch.long, device=device)
|
||||
self.arange = torch.arange(max_len, device=device)
|
||||
|
||||
def fill_cross(self, decoder: "ScoreDecoder", memory):
|
||||
for i, layer in enumerate(decoder.layers):
|
||||
k, v = layer.encoder_attn.kv(memory)
|
||||
self.ck[i].copy_(k)
|
||||
self.cv[i].copy_(v)
|
||||
self.pos.zero_()
|
||||
|
||||
|
||||
class ScoreDecoder(nn.Module):
|
||||
"""Drop-in for the BartDecoder the upstream model builds: ``ScoreDecoder(bart_config, embed_tokens=...)``."""
|
||||
@@ -79,6 +126,26 @@ class ScoreDecoder(nn.Module):
|
||||
def gradient_checkpointing_disable(self):
|
||||
pass
|
||||
|
||||
def prefill_static(self, input_ids, cache: StaticCache):
|
||||
"""[1, P] into an empty cache; returns hidden [1, P, dim]."""
|
||||
p = input_ids.shape[1]
|
||||
idx = torch.arange(p, device=input_ids.device)
|
||||
x = self.layernorm_embedding(self.embed_tokens(input_ids) + self.embed_positions(idx + POSITION_OFFSET)[None])
|
||||
mask = (cache.arange[None, :] <= idx[:, None])[None, None]
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = layer.step_static(x, cache, i, idx, mask)
|
||||
cache.pos.fill_(p)
|
||||
return x
|
||||
|
||||
def step_static(self, input_ids, cache: StaticCache):
|
||||
"""[1, 1] at position ``cache.pos``; graph-capturable (no host reads)."""
|
||||
x = self.layernorm_embedding(self.embed_tokens(input_ids) + self.embed_positions(cache.pos + POSITION_OFFSET)[None])
|
||||
mask = (cache.arange <= cache.pos)[None, None, None, :]
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = layer.step_static(x, cache, i, cache.pos, mask)
|
||||
cache.pos += 1
|
||||
return x
|
||||
|
||||
def forward(self, input_ids, attention_mask=None, encoder_hidden_states=None, encoder_attention_mask=None,
|
||||
past_key_values=None, use_cache=False, output_hidden_states=False, return_dict=True, **_):
|
||||
b, l = input_ids.shape
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Launch-light replacement for SheetSage2's ``constrained_prompt_generate``.
|
||||
|
||||
Upstream decodes one token per Python iteration: a growing KV cache re-concatenated every step,
|
||||
the grammar mask rebuilt from ~10 slice kernels, and a host sync per token, which leaves the
|
||||
6-layer / 512-wide decoder at ~5 ms per token on an idle CPU and far worse under dataloader
|
||||
load. Here the grammar (``PromptGrammarState``) is tabulated once into ``allowed[state, token]``
|
||||
and ``next_state[state, token_type]`` lookups, the decoder runs on a ``StaticCache``, and the
|
||||
whole per-token step (mask, argmax, state update, decoder forward for the chosen token) is one
|
||||
CUDA graph replayed with a host sync every ``SYNC_EVERY`` tokens. Batch 1 only (what upstream's
|
||||
``constrained_prompt_generate`` accepts); anything else falls back to the upstream function.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from .sheetsage_decoder import StaticCache
|
||||
|
||||
SYNC_EVERY = 32
|
||||
TYPE_NAMES = ("subbeat_shift", "time", "meter", "eighth_position", "structure", "key", "chord_full", "pitch", "duration")
|
||||
EOS_TYPE = len(TYPE_NAMES)
|
||||
OTHER_TYPE = EOS_TYPE + 1
|
||||
INCOMPLETE = (None, "rhythm_after_meter", "melody_after_pitch")
|
||||
N_STATES = 2 * 2 * 5 * 7 * 3
|
||||
NO_STOP = 1 << 40
|
||||
|
||||
|
||||
def state_index(state) -> int:
|
||||
"""PromptGrammarState -> row; ``allowed``/``update`` only read these fields (shift_run only as < 4)."""
|
||||
key = (int(state.payload_count > 0), int(state.in_shift), min(int(state.shift_run), 4), int(state.last_field_index) + 1, INCOMPLETE.index(state.incomplete))
|
||||
return (((key[0] * 2 + key[1]) * 5 + key[2]) * 7 + key[3]) * 3 + key[4]
|
||||
|
||||
|
||||
def build_tables(tokenizer, state_cls):
|
||||
"""Enumerate the whole grammar key space: allowed [S, V] bool, next_state [S, n_types] long, token type [V] long."""
|
||||
attrs = [{"chord_full": "full_chord"}.get(n, n) for n in TYPE_NAMES]
|
||||
ranges = [(getattr(tokenizer, f"{a}_token_start"), getattr(tokenizer, f"{a}_token_end")) for a in attrs]
|
||||
type_of = torch.full((tokenizer.n_tokens,), OTHER_TYPE, dtype=torch.long)
|
||||
for t, (s, e) in enumerate(ranges):
|
||||
type_of[s:e] = t
|
||||
type_of[tokenizer.eos_token] = EOS_TYPE
|
||||
allowed = torch.zeros(N_STATES, tokenizer.n_tokens, dtype=torch.bool)
|
||||
next_state = torch.arange(N_STATES, dtype=torch.long)[:, None].repeat(1, OTHER_TYPE + 1)
|
||||
for payload in (0, 1):
|
||||
for in_shift in (False, True):
|
||||
for run in range(5):
|
||||
for lfi in range(-1, 6):
|
||||
for inc in INCOMPLETE:
|
||||
st = state_cls(tokenizer)
|
||||
st.payload_count, st.in_shift, st.shift_run, st.last_field_index, st.incomplete = payload, in_shift, run, lfi, inc
|
||||
i = state_index(st)
|
||||
allowed[i] = st.allowed(torch.device("cpu"))
|
||||
for t, (s, e) in enumerate(ranges):
|
||||
if s < e:
|
||||
nxt = copy.copy(st)
|
||||
nxt.update(s)
|
||||
next_state[i, t] = state_index(nxt)
|
||||
return allowed, next_state, type_of
|
||||
|
||||
|
||||
def stop_time_id(stop_seconds: Optional[float], time_hz: int) -> int:
|
||||
"""Smallest time id with ``id / time_hz >= stop`` under float division, as upstream compares."""
|
||||
if stop_seconds is None:
|
||||
return NO_STOP
|
||||
t = max(0, math.ceil(float(stop_seconds) * time_hz))
|
||||
while t > 0 and (t - 1) / time_hz >= stop_seconds:
|
||||
t -= 1
|
||||
while t / time_hz < stop_seconds:
|
||||
t += 1
|
||||
return t
|
||||
|
||||
|
||||
class FastConstrainedGenerate:
|
||||
"""Callable with upstream's ``constrained_prompt_generate`` signature; ``upstream`` handles unsupported calls."""
|
||||
|
||||
def __init__(self, upstream):
|
||||
self.upstream = upstream
|
||||
self.model = None
|
||||
self.key = None
|
||||
self.graph = None
|
||||
|
||||
def __call__(self, model, audio, prompts, max_sequence_length, prefix_tokens=None, autocast_dtype=torch.bfloat16,
|
||||
stop_time_seconds=None, progress_callback=None, memory=None, step_callback=None):
|
||||
if step_callback is not None or audio.device.type != "cuda" or audio.shape[0] != 1:
|
||||
return self.upstream(model, audio, prompts, max_sequence_length, prefix_tokens=prefix_tokens, autocast_dtype=autocast_dtype,
|
||||
stop_time_seconds=stop_time_seconds, progress_callback=progress_callback, memory=memory, step_callback=step_callback)
|
||||
tokenizer = model.tokenizer
|
||||
prefix = list(prefix_tokens) if prefix_tokens is not None else tokenizer.prompt_prefix(prompts)
|
||||
if not prefix or prefix[0] != tokenizer.sos_token:
|
||||
raise ValueError("generation prefix must begin with <|sos|>")
|
||||
if prefix[-1] == tokenizer.eos_token:
|
||||
prefix = prefix[:-1]
|
||||
max_len = int(max_sequence_length)
|
||||
if len(prefix) >= max_len:
|
||||
return torch.tensor(prefix + [tokenizer.eos_token], dtype=torch.long)
|
||||
autocast = torch.autocast(device_type="cuda", dtype=autocast_dtype, cache_enabled=False) if autocast_dtype is not None else torch.autocast(device_type="cuda", enabled=False)
|
||||
if memory is None:
|
||||
with autocast:
|
||||
memory = model.encode(audio)
|
||||
self._ensure_built(model, audio.device, max_len, memory.shape[1], autocast_dtype)
|
||||
|
||||
state = self._state_cls(tokenizer)
|
||||
for token in prefix[prefix.index(tokenizer.out_token) + 1 :]:
|
||||
state.update(token)
|
||||
self.cache.fill_cross(self.dec, memory)
|
||||
hidden = self.dec.prefill_static(torch.tensor([prefix], dtype=torch.long, device=audio.device), self.cache)
|
||||
self.logits.copy_(self._project(hidden))
|
||||
self.state.fill_(state_index(state))
|
||||
self.stop_id.fill_(stop_time_id(stop_time_seconds, tokenizer.time_hz))
|
||||
self.gen_count.zero_()
|
||||
self.finish_pos.fill_(-1)
|
||||
self.done.zero_()
|
||||
|
||||
budget = max_len - len(prefix)
|
||||
steps = 0
|
||||
while steps < budget:
|
||||
for _ in range(min(SYNC_EVERY, budget - steps)):
|
||||
self.graph.replay()
|
||||
steps += 1
|
||||
if progress_callback is not None:
|
||||
progress_callback(len(prefix) + steps)
|
||||
if self.done.item():
|
||||
break
|
||||
n = int(self.finish_pos.item()) + 1 if self.done.item() else steps
|
||||
generated = self.out[:n].tolist()
|
||||
if any(int(self.type_of[t]) == OTHER_TYPE for t in generated):
|
||||
raise RuntimeError("Unexpected prompt token type in constrained generation")
|
||||
tokens = prefix + generated
|
||||
if tokens[-1] != tokenizer.eos_token:
|
||||
tokens.append(tokenizer.eos_token)
|
||||
return torch.tensor(tokens, dtype=torch.long)
|
||||
|
||||
def _project(self, hidden):
|
||||
"""last position of [1, L, dim] -> fp32 logits [1, V]"""
|
||||
return torch.nn.functional.linear(hidden[:, -1].to(self.proj_w.dtype), self.proj_w).float()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _ensure_built(self, model, device, max_len, memory_len, autocast_dtype):
|
||||
key = (id(model), str(device), max_len, memory_len, autocast_dtype)
|
||||
if self.key == key and self.graph is not None:
|
||||
return
|
||||
import importlib
|
||||
|
||||
gen = importlib.import_module(type(model).__module__.rsplit(".", 1)[0] + ".generation_sheetsage2")
|
||||
self._state_cls = gen.PromptGrammarState
|
||||
self.model, self.key = model, key
|
||||
tok = model.tokenizer
|
||||
allowed, next_state, type_of = build_tables(tok, self._state_cls)
|
||||
self.allowed, self.next_state = allowed.to(device), next_state.to(device)
|
||||
self.type_of = type_of # host copy for the post-check
|
||||
self.type_of_dev = type_of.to(device)
|
||||
cache_len = max_len + SYNC_EVERY # replays may overshoot the budget by < SYNC_EVERY after finishing
|
||||
self.cache = StaticCache(model.decoder, cache_len, memory_len, device, autocast_dtype or torch.float32)
|
||||
self.logits = torch.zeros(1, tok.n_tokens, device=device)
|
||||
self.state = torch.zeros(1, dtype=torch.long, device=device)
|
||||
self.stop_id = torch.zeros(1, dtype=torch.long, device=device)
|
||||
self.gen_count = torch.zeros(1, dtype=torch.long, device=device)
|
||||
self.finish_pos = torch.full((1,), -1, dtype=torch.long, device=device)
|
||||
self.done = torch.zeros(1, dtype=torch.bool, device=device)
|
||||
self.out = torch.zeros(cache_len, dtype=torch.long, device=device)
|
||||
self.cur = torch.zeros(1, 1, dtype=torch.long, device=device)
|
||||
self.eos = tok.eos_token
|
||||
self.time_start = tok.time_token_start
|
||||
# bf16 copies of the decoder linears and the tied projection table: what autocast would cast on every step
|
||||
dtype = autocast_dtype or torch.float32
|
||||
self.dec = copy.deepcopy(model.decoder, memo={id(model.decoder.embed_tokens): model.decoder.embed_tokens})
|
||||
for m in self.dec.modules():
|
||||
if isinstance(m, torch.nn.Linear):
|
||||
m.to(dtype)
|
||||
self.proj_w = model.output_projection.weight.detach().to(dtype)
|
||||
|
||||
def step():
|
||||
masked = self.logits.masked_fill(~self.allowed[self.state], float("-inf"))
|
||||
tok_id = masked.argmax(-1) # [1]
|
||||
ttype = self.type_of_dev[tok_id]
|
||||
stop_hit = (ttype == 1) & ((tok_id - self.time_start) >= self.stop_id)
|
||||
finished = (tok_id == self.eos) | stop_hit
|
||||
self.out.index_copy_(0, self.gen_count, tok_id)
|
||||
self.finish_pos.copy_(torch.where(finished & (self.finish_pos < 0), self.gen_count, self.finish_pos))
|
||||
self.done.logical_or_(finished)
|
||||
self.gen_count += 1
|
||||
self.state.copy_(self.next_state[self.state, ttype])
|
||||
self.cur.copy_(tok_id[:, None])
|
||||
self.logits.copy_(self._project(self.dec.step_static(self.cur, self.cache)))
|
||||
|
||||
# warm up on a side stream (cuBLAS workspaces, autocast paths), then capture
|
||||
s = torch.cuda.Stream(device)
|
||||
s.wait_stream(torch.cuda.current_stream(device))
|
||||
with torch.cuda.stream(s):
|
||||
for _ in range(3):
|
||||
step()
|
||||
torch.cuda.current_stream(device).wait_stream(s)
|
||||
self.graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(self.graph, stream=s):
|
||||
step()
|
||||
torch.cuda.synchronize(device)
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Lenient ABC rebuild for SheetSage2 transcriptions whose strict notation export failed.
|
||||
|
||||
Upstream's ``export_result`` rejects the whole sheet on any inconsistency in the decoded events
|
||||
(non-consecutive beat IDs in a bar, a note shorter than a subbeat, overlapping quantized notes,
|
||||
a chord change shorter than the grid, an unparsable chord symbol). The decoded events are still
|
||||
mostly right, so this rebuilds the same inputs with the offending rows repaired or dropped and
|
||||
runs upstream's own ``generate_abc_from_data`` again, so the result is still validated ABC.
|
||||
A quality gate refuses sheets that needed more than ``MAX_REPAIR_FRACTION`` of beats or notes touched.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
MAX_REPAIR_FRACTION = 0.2
|
||||
|
||||
|
||||
class AbcRepairError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _modules(model):
|
||||
pkg = type(model).__module__.rsplit(".", 1)[0]
|
||||
return importlib.import_module(pkg + ".notation_sheetsage2"), importlib.import_module(pkg + ".exports_sheetsage2"), importlib.import_module(pkg + ".midi_sheetsage2")
|
||||
|
||||
|
||||
def _beat_rows(events) -> Tuple[List[list], int]:
|
||||
"""exports.rhythm_rows without raising on off-grid eighth positions; returns rows + skipped count."""
|
||||
from fractions import Fraction
|
||||
|
||||
rows, meter, skipped = [], None, 0
|
||||
for event in events:
|
||||
rhythm = event["values"].get("rhythm", {})
|
||||
meter = rhythm.get("meter", meter)
|
||||
eighth = rhythm.get("eighth_position")
|
||||
if eighth is None or meter is None:
|
||||
continue
|
||||
position = Fraction(int(eighth) * int(meter[1]), 8)
|
||||
if position.denominator != 1 or not 0 <= position < meter[0]:
|
||||
skipped += 1
|
||||
continue
|
||||
rows.append([float(event["time"]), int(position) + 1, int(meter[0]), int(meter[1])])
|
||||
return rows, skipped
|
||||
|
||||
|
||||
def _repair_beats(rows: List[list]) -> Tuple[List[list], int]:
|
||||
"""Strictly increasing times; consecutive beat IDs inside each bar. A repeated ID within half a beat
|
||||
period is a duplicated row (dropped); any other non-increase starts a new bar; skipped IDs are filled."""
|
||||
if len(rows) < 2:
|
||||
raise AbcRepairError("fewer than two decoded beats")
|
||||
period = float(np.median(np.diff([r[0] for r in rows]))) if len(rows) > 2 else 0.5
|
||||
out, changed = [], 0
|
||||
for row in rows:
|
||||
if out and row[0] <= out[-1][0]:
|
||||
changed += 1
|
||||
continue
|
||||
if out and row[1] == out[-1][1] and row[0] - out[-1][0] < 0.5 * period:
|
||||
changed += 1
|
||||
continue
|
||||
if out:
|
||||
expected = 1 if row[1] <= out[-1][1] else out[-1][1] + 1
|
||||
if expected > row[2]: # ran past the declared numerator: the model missed a downbeat
|
||||
expected = 1
|
||||
if expected != row[1]:
|
||||
changed += 1
|
||||
row = [row[0], expected, row[2], row[3]]
|
||||
out.append(row)
|
||||
if all(r[1] != 1 for r in out):
|
||||
raise AbcRepairError("no downbeat in the decoded beats")
|
||||
return out, changed
|
||||
|
||||
|
||||
def _extend_beats(rows, duration, notes):
|
||||
"""Same tail extension as export_result: continue the final tempo to the audio/last note end."""
|
||||
period = float(np.median(np.diff([b[0] for b in rows[-9:]])))
|
||||
if period <= 0:
|
||||
raise AbcRepairError("decoded beats must increase in time")
|
||||
beats = [list(b) for b in rows]
|
||||
end = max(duration, max((n[1] for n in notes), default=0))
|
||||
while beats[-1][0] < end - 1e-6:
|
||||
prev = beats[-1]
|
||||
beats.append([prev[0] + period, prev[1] % prev[2] + 1, prev[2], prev[3]])
|
||||
return beats
|
||||
|
||||
|
||||
def _merge_short_intervals(rows, subbeat_times, quantize) -> Tuple[List[list], int]:
|
||||
"""Rows that quantize to zero subbeats are absorbed into the previous row (or the next, for the first)."""
|
||||
out, merged = [], 0
|
||||
for row in rows:
|
||||
start_t, end_t = quantize(row[0], subbeat_times), quantize(row[1], subbeat_times)
|
||||
if end_t <= start_t:
|
||||
merged += 1
|
||||
if out:
|
||||
out[-1][1] = max(out[-1][1], row[1])
|
||||
continue
|
||||
if out and out[-1][1] > row[0]: # keep contiguity after a merge
|
||||
row = [out[-1][1], row[1], row[2]]
|
||||
if row[1] <= row[0]:
|
||||
merged += 1
|
||||
continue
|
||||
out.append(list(row))
|
||||
return out, merged
|
||||
|
||||
|
||||
def _filter_notes(notes, subbeat_times, notation) -> Tuple[List[list], int]:
|
||||
"""Drop notes that quantize to nothing or land on subbeats another note of the same track already holds."""
|
||||
boundaries = notation._subbeat_boundaries(subbeat_times)
|
||||
kept, dropped = [], 0
|
||||
for track in (0, 1):
|
||||
taken = np.zeros(len(subbeat_times), dtype=bool)
|
||||
for note in sorted((n for n in notes if n[3] == track), key=lambda n: (n[0], n[1], n[2])):
|
||||
start_t = max(0, min(int(np.searchsorted(boundaries, note[0])), len(taken) - 1))
|
||||
end_t = max(0, min(int(np.searchsorted(boundaries, note[1])), len(taken) - 1))
|
||||
if end_t <= start_t or taken[start_t:end_t].any():
|
||||
dropped += 1
|
||||
continue
|
||||
taken[start_t:end_t] = True
|
||||
kept.append(list(note))
|
||||
return sorted(kept), dropped
|
||||
|
||||
|
||||
def repair_abc(model, result: dict, melody_only: bool = False) -> Tuple[str, str]:
|
||||
"""Rebuild ABC for a transcribe() result whose ``abc`` is None. Returns (abc, summary) or raises AbcRepairError."""
|
||||
notation, exports, midi_mod = _modules(model)
|
||||
events = result["events"]
|
||||
duration = float(result["duration_seconds"])
|
||||
notes = []
|
||||
for event in events:
|
||||
start = float(event["time"])
|
||||
for note in event["values"].get("melody", ()):
|
||||
end = min(duration, float(note["end_time"]))
|
||||
if end > start:
|
||||
notes.append([start, end, int(note["pitch"]), int(note["track"])])
|
||||
notes.sort()
|
||||
clean, _ = exports.notation_notes(notes)
|
||||
|
||||
rows, skipped = _beat_rows(events)
|
||||
rows, changed = _repair_beats(rows)
|
||||
beats = _extend_beats(rows, duration, clean)
|
||||
n_beats = len(rows) + skipped
|
||||
if (changed + skipped) > MAX_REPAIR_FRACTION * n_beats:
|
||||
raise AbcRepairError(f"{changed + skipped}/{n_beats} beat rows needed repair")
|
||||
|
||||
parsed = notation._parse_beats(notation._row_entries([[str(x) for x in b] for b in beats], "beats"), "beats")
|
||||
measures, _ = notation.infer_measures(parsed, meter_conflict="infer")
|
||||
subbeat_times, _, _ = notation._build_grid(parsed, measures)
|
||||
|
||||
intervals = {}
|
||||
merged = 0
|
||||
for field in ("chord", "key", "structure"):
|
||||
rows = exports.interval_rows(events, field, duration)
|
||||
rows = [[max(beats[0][0], a), min(beats[-1][0], b), v] for a, b, v in rows if b > beats[0][0] and a < beats[-1][0]]
|
||||
if field != "structure":
|
||||
rows, m = _merge_short_intervals(rows, subbeat_times, notation._quantize_time)
|
||||
merged += m
|
||||
intervals[field] = rows
|
||||
if not intervals["key"]:
|
||||
raise AbcRepairError("no key was decoded")
|
||||
bad_chords = 0
|
||||
for row in intervals["chord"]:
|
||||
try:
|
||||
notation.chord_symbol_to_abc(row[2])
|
||||
except notation.AbcRebuildError:
|
||||
row[2] = "N"
|
||||
bad_chords += 1
|
||||
keys = []
|
||||
for row in intervals["key"]:
|
||||
try:
|
||||
notation.key_symbol_to_abc(row[2])
|
||||
keys.append(row)
|
||||
except notation.AbcRebuildError:
|
||||
if keys:
|
||||
keys[-1][1] = row[1]
|
||||
if not keys:
|
||||
raise AbcRepairError("no usable key")
|
||||
intervals["key"] = keys
|
||||
|
||||
kept, dropped = _filter_notes(clean, subbeat_times, notation)
|
||||
if dropped > MAX_REPAIR_FRACTION * max(1, len(clean)):
|
||||
raise AbcRepairError(f"{dropped}/{len(clean)} notes could not be placed on the beat grid")
|
||||
|
||||
try:
|
||||
text, _ = notation.generate_abc_from_data(
|
||||
midi_mod.midi_bytes(exports._midi(kept)), beats, intervals["chord"], intervals["key"], intervals["structure"], melody_only=melody_only
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise AbcRepairError(f"rebuild still failed: {exc}") from exc
|
||||
summary = f"beats repaired {changed + skipped}/{n_beats}, notes dropped {dropped}/{len(clean)}, intervals merged {merged}, chords blanked {bad_chords}"
|
||||
return text, summary
|
||||
@@ -15,6 +15,8 @@ import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from toolkit.basic import UnusableFileError
|
||||
|
||||
from .model import ABC_END, ABC_START, CODEC_SIZE, EOD, INSTRUCTIONS, MUSIC_START
|
||||
|
||||
MERT_REPO = "m-a-p/MERT-v2-FullSong"
|
||||
@@ -107,6 +109,7 @@ class SheetSage2Transcriber:
|
||||
from .shims import install_shims
|
||||
|
||||
install_shims() # mir_eval.chord / pretty_midi stand-ins unless the real packages exist
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from safetensors.torch import load_file
|
||||
@@ -114,11 +117,16 @@ class SheetSage2Transcriber:
|
||||
from transformers.dynamic_module_utils import get_class_from_dynamic_module
|
||||
|
||||
from .sheetsage_decoder import ScoreDecoder
|
||||
from .sheetsage_generate import FastConstrainedGenerate
|
||||
|
||||
cfg = AutoConfig.from_pretrained(repo, trust_remote_code=True)
|
||||
cfg.weights_format = "merged" # the Comfy file carries the LoRA already folded into the encoder
|
||||
cls = get_class_from_dynamic_module(cfg.auto_map["AutoModel"], repo)
|
||||
sys.modules[cls.__module__].BartDecoder = ScoreDecoder # upstream targets transformers 4.45's BartDecoder
|
||||
pipeline = importlib.import_module(cls.__module__.rsplit(".", 1)[0] + ".pipeline_sheetsage2")
|
||||
if not isinstance(pipeline.constrained_prompt_generate, FastConstrainedGenerate):
|
||||
# per-window decode loop: static cache + CUDA graph instead of upstream's per-token Python loop
|
||||
pipeline.constrained_prompt_generate = FastConstrainedGenerate(pipeline.constrained_prompt_generate)
|
||||
cls.post_init = lambda self: None # transformers-5 tied-weight bookkeeping rejects upstream's list; weights load explicitly
|
||||
model = cls(cfg)
|
||||
sd = {k: v.to(torch.float32) for k, v in load_file(weights_path).items()}
|
||||
@@ -130,6 +138,7 @@ class SheetSage2Transcriber:
|
||||
self.model = model.eval()
|
||||
self.model.requires_grad_(False)
|
||||
_rebuild_rotary(self.model)
|
||||
self.warnings: List[str] = [] # repair notes for the caller to print with the track path
|
||||
|
||||
def to(self, device):
|
||||
self.model.to(device)
|
||||
@@ -142,11 +151,19 @@ class SheetSage2Transcriber:
|
||||
@torch.no_grad()
|
||||
def transcribe(self, waveform: torch.Tensor, sample_rate: int, cot: str = "full") -> str:
|
||||
"""waveform [C, samples] -> ABC text (``cot`` "full" or "melody")."""
|
||||
from .sheetsage_repair import AbcRepairError, repair_abc
|
||||
|
||||
mono = waveform.float().mean(0) if waveform.dim() == 2 else waveform.float()
|
||||
result = self.model.transcribe(mono.cpu(), sampling_rate=sample_rate, melody_only=cot == "melody")
|
||||
abc = result.get("abc") or ""
|
||||
if not abc.strip():
|
||||
raise RuntimeError(f"SheetSage2 produced no ABC: {result.get('abc_error') or 'unknown reason'}")
|
||||
# strict export rejected the sheet: rebuild it with the bad rows repaired, else the cacher drops the track
|
||||
error = result.get("abc_error") or "unknown reason"
|
||||
try:
|
||||
abc, summary = repair_abc(self.model, result, melody_only=cot == "melody")
|
||||
except AbcRepairError as exc:
|
||||
raise UnusableFileError(f"SheetSage2 produced no ABC: {error}; repair failed: {exc}") from exc
|
||||
self.warnings.append(f"SheetSage2 ABC repaired ({error}): {summary}")
|
||||
return abc
|
||||
|
||||
|
||||
|
||||
@@ -265,6 +265,13 @@ class YuE2AudioModel(BaseAudioModel):
|
||||
self.transcriber = SheetSage2Transcriber(resolve_named_file(self.sheetsage_path, component="sheetsage2"))
|
||||
return self.transcriber
|
||||
|
||||
def pop_encode_warnings(self) -> List[str]:
|
||||
"""Repair notes from the last encode_audio call; the latent cacher prints them with the file path."""
|
||||
if self.transcriber is None:
|
||||
return []
|
||||
warnings, self.transcriber.warnings = self.transcriber.warnings, []
|
||||
return warnings
|
||||
|
||||
def get_transformer_block_names(self) -> Optional[List[str]]:
|
||||
return ["model.layers"]
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ import os
|
||||
import torch
|
||||
|
||||
|
||||
class UnusableFileError(Exception):
|
||||
"""A model raises this from encode when a dataset file cannot be used (e.g. no sheet could be transcribed);
|
||||
the latent cacher drops the file instead of failing the job."""
|
||||
|
||||
|
||||
def value_map(inputs, min_in, max_in, min_out, max_out):
|
||||
return (inputs - min_in) * (max_out - min_out) / (max_in - min_in) + min_out
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from tqdm import tqdm
|
||||
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection, SiglipImageProcessor
|
||||
|
||||
from toolkit.audio.preserve_pitch import time_stretch_preserve_pitch
|
||||
from toolkit.basic import flush, value_map
|
||||
from toolkit.basic import UnusableFileError, flush, value_map
|
||||
from toolkit.buckets import get_bucket_for_image_size, get_resolution
|
||||
from toolkit.config_modules import ControlTypes
|
||||
from toolkit.control_generator import ControlGenerator
|
||||
@@ -2005,7 +2005,16 @@ class LatentCachingMixin:
|
||||
if needs_encode and not did_move:
|
||||
self.sd.set_device_state_preset('cache_latents')
|
||||
did_move = True
|
||||
self._cache_one_latent(file_item, latent_path, cached_state_dict, needs_encode, to_disk, to_memory)
|
||||
try:
|
||||
self._cache_one_latent(file_item, latent_path, cached_state_dict, needs_encode, to_disk, to_memory)
|
||||
except UnusableFileError as e:
|
||||
pbar.write(f"Skipping unusable file {file_item.path}: {e}")
|
||||
failed_items.append(file_item)
|
||||
pbar.update(1)
|
||||
continue
|
||||
# models may report per-file repairs (e.g. a patched sheet); print them on their own line with the path
|
||||
for warning in getattr(self.sd, 'pop_encode_warnings', lambda: [])():
|
||||
pbar.write(f"{file_item.path}: {warning}")
|
||||
file_item.is_latent_cached = True
|
||||
i += 1
|
||||
pbar.update(1)
|
||||
@@ -2014,7 +2023,7 @@ class LatentCachingMixin:
|
||||
pbar.close()
|
||||
|
||||
if failed_items:
|
||||
print_acc(f"Removed {len(failed_items)} files from the dataset that failed to load")
|
||||
print_acc(f"Removed {len(failed_items)} files from the dataset that failed to load or were unusable")
|
||||
self._remove_file_items(failed_items)
|
||||
|
||||
# restore device state
|
||||
@@ -2116,6 +2125,8 @@ class LatentCachingMixin:
|
||||
for k, v in latent.extras.items():
|
||||
if torch.is_tensor(v):
|
||||
state_dict[f'{DISK_PREFIX}{k}'] = v.clone().detach().cpu()
|
||||
except UnusableFileError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print_acc(f"Error processing image: {file_item.path}")
|
||||
print_acc(f"Error: {str(e)}")
|
||||
|
||||
Reference in new issue
Block a user