Support Yue2 music model. (#16250)

This commit is contained in:
comfyanonymous
2026-09-11 19:34:44 -04:00
committed by GitHub
parent 1d91a82dc6
commit b058ec6528
15 changed files with 2216 additions and 8 deletions
+22 -2
View File
@@ -1,10 +1,13 @@
from .wav2vec2 import Wav2Vec2Model
from .whisper import WhisperLargeV3
from .sheetsage2 import SheetSage2
from .sheetsage2_abc import events_to_abc
import comfy.model_management
import comfy.ops
import comfy.utils
import logging
import torchaudio
import torch
class AudioEncoderModel():
@@ -13,6 +16,9 @@ class AudioEncoderModel():
offload_device = comfy.model_management.text_encoder_offload_device()
self.dtype = comfy.model_management.text_encoder_dtype(self.load_device)
model_type = config.pop("model_type")
self.model_sample_rate = config.pop("model_sample_rate", 16000)
if model_type == "sheetsage2":
self.dtype = torch.bfloat16 if comfy.model_management.should_use_bf16(self.load_device) else torch.float32
model_config = dict(config)
model_config.update({
"dtype": self.dtype,
@@ -24,9 +30,10 @@ class AudioEncoderModel():
self.model = Wav2Vec2Model(**model_config)
elif model_type == "whisper3":
self.model = WhisperLargeV3(**model_config)
elif model_type == "sheetsage2":
self.model = SheetSage2(**model_config)
self.model.eval()
self.patcher = comfy.model_patcher.CoreModelPatcher(self.model, load_device=self.load_device, offload_device=offload_device)
self.model_sample_rate = 16000
comfy.model_management.archive_model_dtypes(self.model)
def load_sd(self, sd):
@@ -46,6 +53,17 @@ class AudioEncoderModel():
return outputs
class SheetSage2AudioEncoder(AudioEncoderModel):
def generate_abc(self, audio, sample_rate, melody_only=True):
audio = torchaudio.functional.resample(audio.float().mean(dim=1), sample_rate, self.model_sample_rate)
comfy.model_management.load_model_gpu(self.patcher)
scores = []
for waveform in audio:
events = self.model.transcribe(waveform[None].to(self.load_device))
scores.append(events_to_abc(events, waveform.shape[-1] / self.model_sample_rate, melody_only=melody_only))
return scores
def load_audio_encoder_from_sd(sd, prefix=""):
sd = comfy.utils.state_dict_prefix_replace(sd, {"wav2vec2.": ""})
if "encoder.layer_norm.bias" in sd: #wav2vec2
@@ -79,10 +97,12 @@ def load_audio_encoder_from_sd(sd, prefix=""):
config = {
"model_type": "whisper3",
}
elif "encoder.feature_extractor.mel_mean" in sd and "decoder.layernorm_embedding.weight" in sd and "layer_weight" in sd:
config = {"model_type": "sheetsage2", "model_sample_rate": 24000}
else:
raise RuntimeError("ERROR: audio encoder not supported.")
audio_encoder = AudioEncoderModel(config)
audio_encoder = SheetSage2AudioEncoder(config) if config["model_type"] == "sheetsage2" else AudioEncoderModel(config)
m, u = audio_encoder.load_sd(sd)
if len(m) > 0:
logging.warning("missing audio encoder: {}".format(m))
+182
View File
@@ -0,0 +1,182 @@
"""MERT-v2 ConvNeXt/Conformer audio encoder."""
import torch
from torch import nn
from torch.nn import functional as F
import comfy.ops
import comfy.quant_ops
from comfy.ldm.modules.attention import optimized_attention_for_device
class MelFrontend(nn.Module):
def __init__(self, device=None):
super().__init__()
self.register_buffer("mel_mean", torch.empty(128, device=device, dtype=torch.float32))
self.register_buffer("mel_std", torch.empty(128, device=device, dtype=torch.float32))
self.spectrogram = nn.Module()
self.spectrogram.register_buffer("window", torch.empty(2048, device=device, dtype=torch.float32))
self.mel_scale = nn.Module()
self.mel_scale.register_buffer("fb", torch.empty(1025, 128, device=device, dtype=torch.float32))
def forward(self, waveform):
window = comfy.ops.cast_to_input(self.spectrogram.window, waveform)
spectrum = torch.stft(waveform, n_fft=2048, hop_length=240, win_length=2048,
window=window, return_complex=True).abs().square()
mel = spectrum.transpose(-1, -2) @ comfy.ops.cast_to_input(self.mel_scale.fb, waveform)
mel = 10.0 * mel.clamp_min(1e-10).log10()
mean = comfy.ops.cast_to_input(self.mel_mean, waveform)
std = comfy.ops.cast_to_input(self.mel_std, waveform)
return (mel[:, :-1] - mean) / std.clamp_min(1e-5)
class Transpose(nn.Module):
def forward(self, x):
return x.transpose(1, 2)
class GlobalResponseNorm(nn.Module):
def __init__(self, dim, device=None, dtype=None):
super().__init__()
self.weight = nn.Parameter(torch.empty(1, 1, dim, device=device, dtype=dtype))
self.bias = nn.Parameter(torch.empty(1, 1, dim, device=device, dtype=dtype))
def forward(self, x):
magnitude = torch.linalg.vector_norm(x, dim=1, keepdim=True)
normalized = magnitude / (magnitude.mean(dim=-1, keepdim=True) + 1e-6)
weight = comfy.ops.cast_to_input(self.weight, x)
bias = comfy.ops.cast_to_input(self.bias, x)
return weight * (x * normalized) + bias + x
class ConvNextLayer(nn.Module):
def __init__(self, dim, device=None, dtype=None, operations=None):
super().__init__()
self.depthwise_block = nn.Sequential(
Transpose(), operations.Conv1d(dim, dim, 7, padding=3, groups=dim, device=device, dtype=dtype), Transpose(),
)
self.pointwise_block = nn.Sequential(
operations.LayerNorm(dim, eps=1e-6, device=device, dtype=dtype),
operations.Linear(dim, 4 * dim, device=device, dtype=dtype), nn.GELU(),
GlobalResponseNorm(4 * dim, device=device, dtype=dtype),
operations.Linear(4 * dim, dim, device=device, dtype=dtype),
)
def forward(self, x):
return x + self.pointwise_block(self.depthwise_block(x))
class ConvNextBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride, depth, device=None, dtype=None, operations=None):
super().__init__()
self.resampling_layer = nn.Identity()
if in_channels != out_channels or stride > 1:
self.resampling_layer = nn.Sequential(
operations.LayerNorm(in_channels, eps=1e-6, device=device, dtype=dtype), Transpose(),
operations.Conv1d(in_channels, out_channels, 2, stride=stride, device=device, dtype=dtype), Transpose(),
)
self.convnext_layers = nn.Sequential(*[
ConvNextLayer(out_channels, device=device, dtype=dtype, operations=operations) for _ in range(depth)
])
def forward(self, x):
return self.convnext_layers(self.resampling_layer(x))
class Attention(nn.Module):
def __init__(self, dim, heads, device=None, dtype=None, operations=None):
super().__init__()
self.heads = heads
self.query_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.key_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.value_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.out_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
def forward(self, x, positions, attention):
shape = (x.shape[0], x.shape[1], self.heads, -1)
q = self.query_proj(x).reshape(shape).transpose(1, 2)
k = self.key_proj(x).reshape(shape).transpose(1, 2)
v = self.value_proj(x).reshape(shape).transpose(1, 2)
q, k = comfy.quant_ops.ck.apply_rope_split_half(q, k, positions)
return self.out_proj(attention(q, k, v, self.heads, skip_reshape=True))
class FeedForward(nn.Module):
def __init__(self, dim, intermediate, device=None, dtype=None, operations=None):
super().__init__()
self.w_1 = operations.Linear(dim, intermediate, device=device, dtype=dtype)
self.w_2 = operations.Linear(intermediate, dim, device=device, dtype=dtype)
def forward(self, x):
return self.w_2(F.gelu(self.w_1(x)))
class ConvolutionModule(nn.Module):
def __init__(self, dim, device=None, dtype=None, operations=None):
super().__init__()
self.layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.conv_block = nn.Sequential(
Transpose(), operations.Conv1d(dim, dim * 2, 1, bias=False, device=device, dtype=dtype), nn.GLU(dim=1),
operations.Conv1d(dim, dim, 31, padding=15, groups=dim, bias=False, device=device, dtype=dtype),
nn.Sequential(Transpose(), operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype), Transpose()),
nn.GELU(), operations.Conv1d(dim, dim, 1, bias=False, device=device, dtype=dtype), Transpose(),
)
def forward(self, x):
return self.conv_block(self.layer_norm(x))
class ConformerBlock(nn.Module):
def __init__(self, dim, intermediate, heads, device=None, dtype=None, operations=None):
super().__init__()
self.ffn1_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.ffn1 = FeedForward(dim, intermediate, device=device, dtype=dtype, operations=operations)
self.attn_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.attn = Attention(dim, heads, device=device, dtype=dtype, operations=operations)
self.conv_module = ConvolutionModule(dim, device=device, dtype=dtype, operations=operations)
self.ffn2_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.ffn2 = FeedForward(dim, intermediate, device=device, dtype=dtype, operations=operations)
self.final_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
def forward(self, x, positions, attention):
x = x + 0.5 * self.ffn1(self.ffn1_layer_norm(x))
x = x + self.attn(self.attn_layer_norm(x), positions, attention)
x = x + self.conv_module(x)
x = x + 0.5 * self.ffn2(self.ffn2_layer_norm(x))
return self.final_layer_norm(x)
class MERT2(nn.Module):
def __init__(self, dim=1024, intermediate=4096, heads=16, layers=24, channels=(128, 512, 1024),
depths=(3, 4, 5), device=None, dtype=None, operations=None):
super().__init__()
self.head_dim = dim // heads
self.feature_extractor = MelFrontend(device=device)
channels = (128, *channels)
self.subsampling_module = nn.Sequential(*[
ConvNextBlock(channels[i], channels[i + 1], (1, 2, 2)[i], depths[i], device=device, dtype=dtype, operations=operations)
for i in range(3)
])
self.layers = nn.ModuleList([
ConformerBlock(dim, intermediate, heads, device=device, dtype=dtype, operations=operations) for _ in range(layers)
])
def position_embeddings(self, x):
inverse = 1.0 / (10000 ** (torch.arange(0, self.head_dim, 2, device=x.device, dtype=torch.float32) / self.head_dim))
angles = torch.arange(x.shape[1], device=x.device, dtype=torch.float32)[:, None] * inverse
cos, sin = angles.cos().to(x.dtype), angles.sin().to(x.dtype)
return torch.stack((cos, -sin, sin, cos), dim=-1).reshape(1, 1, x.shape[1], -1, 2, 2).float()
def forward(self, mel, layer_weight, output_hidden_states=False):
x = self.subsampling_module(mel)
weights = comfy.ops.cast_to_input(layer_weight, x).softmax(dim=0)
mixed = x * weights[0]
states = [x] if output_hidden_states else None
positions = self.position_embeddings(x)
attention = optimized_attention_for_device(x.device)
for weight, layer in zip(weights[1:], self.layers):
x = layer(x, positions, attention)
mixed = mixed + x * weight
if output_hidden_states:
states.append(x)
return mixed, states
+537
View File
@@ -0,0 +1,537 @@
"""SheetSage2 audio-to-score generation with the released event vocabulary."""
import copy
import logging
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
import comfy.model_management
import comfy.model_prefetch
import comfy.ops
import comfy.utils
from comfy.audio_encoders.mert2 import MERT2
from comfy.ldm.modules.attention import optimized_attention_for_device
from comfy.text_encoders.llama import FixedKV
class DecoderAttention(nn.Module):
def __init__(self, dim, heads, device=None, dtype=None, operations=None):
super().__init__()
self.heads = heads
self.q_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.k_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.v_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
self.out_proj = operations.Linear(dim, dim, device=device, dtype=dtype)
def project(self, x, projection):
return projection(x).reshape(x.shape[0], x.shape[1], self.heads, -1).transpose(1, 2)
def forward(self, x, attention, mask=None, cache=None, memory=None):
q = self.project(x, self.q_proj)
if memory is not None:
k, v = memory
else:
k, v = self.project(x, self.k_proj), self.project(x, self.v_proj)
length = x.shape[1]
if isinstance(cache, FixedKV):
key, value = k.transpose(1, 2), v.transpose(1, 2)
if length == 1 and cache.index > 0:
position = cache.position.view(-1, 1, 1, 1).expand_as(key)
cache.key.scatter_(1, position, key)
cache.value.scatter_(1, position, value)
valid = torch.arange(cache.key.shape[1], device=x.device)[None] < cache.seqlen[:, None]
mask = torch.zeros(valid.shape, device=x.device, dtype=x.dtype).masked_fill_(~valid, -torch.inf)[:, None, None]
out = attention(q, cache.key.transpose(1, 2), cache.value.transpose(1, 2), self.heads, mask=mask, skip_reshape=True)
return self.out_proj(out), cache
cache.key[:, :length].copy_(key)
cache.value[:, :length].copy_(value)
elif cache is not None:
key, value, index = cache
key[:, :, index:index + length].copy_(k)
value[:, :, index:index + length].copy_(v)
k, v = key[:, :, :index + length], value[:, :, :index + length]
cache = key, value, index + length
out = attention(q, k, v, self.heads, mask=mask, skip_reshape=True)
return self.out_proj(out), cache
class DecoderLayer(nn.Module):
def __init__(self, dim, intermediate, heads, device=None, dtype=None, operations=None):
super().__init__()
self.self_attn = DecoderAttention(dim, heads, device=device, dtype=dtype, operations=operations)
self.self_attn_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.encoder_attn = DecoderAttention(dim, heads, device=device, dtype=dtype, operations=operations)
self.encoder_attn_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.fc1 = operations.Linear(dim, intermediate, device=device, dtype=dtype)
self.fc2 = operations.Linear(intermediate, dim, device=device, dtype=dtype)
self.final_layer_norm = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
def forward(self, x, attention, mask, cache):
self_cache, memory = cache
out, self_cache = self.self_attn(x, attention, mask=mask, cache=self_cache)
x = self.self_attn_layer_norm(x + out)
out, _ = self.encoder_attn(x, attention, memory=memory)
x = self.encoder_attn_layer_norm(x + out)
return self.final_layer_norm(x + self.fc2(F.gelu(self.fc1(x)))), (self_cache, memory)
class Decoder(nn.Module):
def __init__(self, dim, intermediate, heads, layers, max_tokens, device=None, dtype=None, operations=None):
super().__init__()
self.embed_positions = operations.Embedding(max_tokens + 2, dim, device=device, dtype=dtype)
self.layernorm_embedding = operations.LayerNorm(dim, eps=1e-5, device=device, dtype=dtype)
self.layers = nn.ModuleList([
DecoderLayer(dim, intermediate, heads, device=device, dtype=dtype, operations=operations) for _ in range(layers)
])
def forward(self, x, positions, cache):
length = x.shape[1]
fixed = isinstance(cache[0][0], FixedKV)
index = cache[0][0].index if fixed else cache[0][0][2]
mask = None
if length > 1:
mask = torch.full((length, index + length), -torch.inf, device=x.device, dtype=x.dtype).triu_(index + 1)
x = self.layernorm_embedding(x + self.embed_positions(positions, out_dtype=x.dtype))
graph = fixed and length == 1 and index > 0
if graph:
x = x.clone()
attention = optimized_attention_for_device(x.device, mask=mask is not None or graph, small_input=True)
queue = comfy.model_prefetch.make_prefetch_queue(list(self.layers), x.device, {"prefetch_dynamic_vbars": True})
for i, layer in enumerate(self.layers):
if fixed:
cache[i][0].prepare(length)
def core():
nonlocal x
out, cache[i] = layer(x, attention, mask, cache[i])
if graph:
x.copy_(out)
else:
x = out
comfy.model_prefetch.prefetch_queue_pop(queue, x.device, layer, x.dtype, core=core,
enable_graph=graph, malloc_scope="block")
if fixed:
cache[i][0].advance(length)
comfy.model_prefetch.prefetch_queue_pop(queue, x.device, None, malloc_scope="block")
return x
class SheetSage2(nn.Module):
def __init__(self, dim=512, intermediate=2048, heads=8, layers=6, max_tokens=5120,
mert_config=None, device=None, dtype=None, operations=None):
super().__init__()
mert_config = {} if mert_config is None else mert_config
self.dtype = dtype
self.max_tokens = max_tokens
self.encoder = MERT2(**mert_config, device=device, dtype=dtype, operations=operations)
self.layer_weight = nn.Parameter(torch.empty(len(self.encoder.layers) + 1, device=device, dtype=dtype))
self.encoder_projection = operations.Linear(mert_config.get("dim", 1024), dim, device=device, dtype=dtype)
self.tokenizer = ScoreTokenizer()
self.token_embedding = operations.Embedding(self.tokenizer.n_tokens, dim, device=device, dtype=dtype)
self.decoder = Decoder(dim, intermediate, heads, layers, max_tokens, device=device, dtype=dtype, operations=operations)
self.output_projection = operations.Linear(dim, self.tokenizer.n_tokens, bias=False, device=device, dtype=dtype)
def get_dynamic_vram__units(self):
return list(self.decoder.layers), []
def encode(self, waveform, output_hidden_states=False):
# The released encoder attends to the entire 300-second window, including its padding.
waveform = F.pad(waveform, (0, max(0, 300 * 24000 - waveform.shape[-1])))
mel = self.encoder.feature_extractor(waveform.float()).to(self.dtype)
mixed, states = self.encoder(mel, self.layer_weight, output_hidden_states=output_hidden_states)
return self.encoder_projection(mixed), states
def forward(self, audio):
return self.encode(audio.mean(dim=1), output_hidden_states=True)
def init_cache(self, memory):
batch, _, dim = memory.shape
fixed = comfy.model_prefetch.malloc_graph_enabled(memory.device)
cache = []
for layer in self.decoder.layers:
heads = layer.self_attn.heads
shape = (batch, self.max_tokens, heads, dim // heads) if fixed else (batch, heads, self.max_tokens, dim // heads)
# Fixed attention includes masked future slots, whose values must remain finite.
key = torch.zeros(shape, device=memory.device, dtype=memory.dtype) if fixed else torch.empty(shape, device=memory.device, dtype=memory.dtype)
value = torch.zeros_like(key) if fixed else torch.empty_like(key)
if fixed:
self_cache = FixedKV(key, value, 0, torch.empty(batch, device=memory.device, dtype=torch.long),
torch.zeros(batch, device=memory.device, dtype=torch.int32))
else:
self_cache = key, value, 0
cross = layer.encoder_attn
cache.append((self_cache, (cross.project(memory, cross.k_proj), cross.project(memory, cross.v_proj))))
return cache
def decode(self, ids, positions, cache):
x = self.token_embedding(ids, out_dtype=self.dtype)
return self.output_projection(self.decoder(x, positions, cache)[:, -1:])
def generate_tokens(self, memory, stop_seconds, prefix=None):
tokenizer = self.tokenizer
tokens = tokenizer.prompt_prefix() if prefix is None else list(prefix)
state = PromptGrammarState(tokenizer)
for token in tokens[tokens.index(tokenizer.out_token) + 1:]:
state.update(token)
cache = self.init_cache(memory)
device = memory.device
ids = torch.tensor([tokens], device=device, dtype=torch.long)
positions = torch.arange(2, len(tokens) + 2, device=device)[None]
logits = self.decode(ids, positions, cache)
ids = torch.empty((1, 1), device=device, dtype=torch.long)
positions = torch.full((1, 1), len(tokens) + 2, device=device, dtype=torch.long)
fixed = isinstance(cache[0][0], FixedKV)
progress = comfy.utils.ProgressBar(self.max_tokens - len(tokens))
try:
for step in comfy.utils.model_trange(self.max_tokens - len(tokens), desc="SheetSage2 transcription", unit="token"):
comfy.model_management.throw_exception_if_processing_interrupted()
scores = logits[0, -1].float().masked_fill(~state.allowed(device), -torch.inf)
next_id = scores.argmax()
token = next_id.item()
tokens.append(token)
progress.update_absolute(step + 1)
if state.update(token):
break
if tokenizer.time_token_start <= token < tokenizer.time_token_end and tokenizer.token_to_time_id(token) / tokenizer.time_hz >= stop_seconds:
tokens.append(tokenizer.eos_token)
break
if len(tokens) == self.max_tokens:
logging.warning("SheetSage2 reached its token limit; the transcription may be incomplete.")
tokens.append(tokenizer.eos_token)
break
ids.copy_(next_id)
if fixed:
comfy.model_prefetch.malloc_graph_begin(device)
logits.copy_(self.decode(ids, positions, cache))
if fixed:
comfy.model_prefetch.malloc_graph_end()
positions.add_(1)
finally:
comfy.model_prefetch.cleanup_prefetch_queues()
return tokens
def transcribe(self, waveform):
duration = waveform.shape[-1] / 24000
stitched = []
for window in sliding_window_plan(duration):
comfy.model_management.throw_exception_if_processing_interrupted()
start = window["start"]
prefix, base = overlap_prefix(stitched, self.tokenizer, start, window["prefix_end"])
if prefix is not None and len(prefix) >= self.max_tokens - 128:
raise ValueError("SheetSage2 overlap fills the token context; transcribe shorter audio sections.")
segment = waveform[:, round(start * 24000):round(window["end"] * 24000)]
memory, _ = self.encode(segment)
stop = window["generation_stop"] if window["generation_stop"] is not None else min(duration - start, 300.0)
tokens = self.generate_tokens(memory, stop, prefix)
decoded = self.tokenizer.decode_sequence(tokens)
lookup = event_time_map(decoded, 300.0)
stitched.extend(stitched_window_events(decoded, lookup, start, window["accept_start"],
window["accept_end"], duration, global_subbeat_base=base))
stitched.sort(key=lambda event: (event["time"], event["global_subbeat"]))
return stitched
CHROMATIC_SHARPS = ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
STRUCTURE_LABELS = (
"silence", "intro", "outro", "verse", "chorus", "bridge", "pre-chorus", "post-chorus", "interlude",
"fade-out", "loop", "rap", "preshot", "irregular", "instrumental", "intro and verse", "pre-chorus and chorus",
"verse and pre-chorus", "solo", "theme", "development", "variation", "pre-outro",
)
DURATION_TEMPLATES = (1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096)
EVENT_FIELDS = ("timestamp", "rhythm", "structure", "key", "chord", "melody")
FIELD_TO_INDEX = {name: index for index, name in enumerate(EVENT_FIELDS)}
class ScoreTokenizer:
pad_token, sos_token, eos_token, out_token = 0, 1, 2, 3
time_hz = 100
def __init__(self):
self.meter_pairs = tuple((numerator, denominator) for numerator in range(1, 33) for denominator in (1, 2, 4, 8, 16, 32))
self.full_chord_labels = ["N"]
inversions = {"maj": ("/2", "/3", "/5"), "min": ("/2", "/b3", "/5"),
"maj7": ("/3", "/5", "/7"), "min7": ("/b3", "/5", "/b7"), "7": ("/3", "/5", "/b7")}
for quality in ("maj", "min", "dim", "aug", "maj7", "min7", "7", "hdim7", "dim7", "minmaj7", "sus2", "sus4", "sus4(b7)", "maj6", "min6"):
for root in CHROMATIC_SHARPS:
self.full_chord_labels.extend(f"{root}:{quality}{inversion}" for inversion in (*inversions.get(quality, ()), ""))
offset = 260
self.ranges = []
for name, count in (("subbeat_shift", 257), ("time", 30000), ("meter", 192), ("eighth_position", 256),
("structure", len(STRUCTURE_LABELS)), ("key", 24), ("majmin_chord", 25),
("full_chord", len(self.full_chord_labels)), ("pitch", 256), ("duration", len(DURATION_TEMPLATES))):
setattr(self, f"{name}_token_start", offset)
setattr(self, f"{name}_token_end", offset + count)
self.ranges.append((name.replace("full_chord", "chord_full").replace("majmin_chord", "chord_majmin"), offset, offset + count))
offset += count
self.n_tokens = offset
def prompt_prefix(self):
return [self.sos_token, 4, 5, 6, 7, 9, 11, self.out_token]
def token_type(self, token):
for name, start, end in self.ranges:
if start <= token < end:
return name
return {0: "pad", 1: "sos", 2: "eos", 3: "out"}.get(token, "prompt")
def token_to_time_id(self, token):
return token - self.time_token_start
def decode_field(self, field, tokens):
if field == "timestamp":
return self.token_to_time_id(tokens[0]) / self.time_hz
if field == "rhythm":
rhythm = {}
for token in tokens:
if self.token_type(token) == "meter":
rhythm["meter"] = self.meter_pairs[token - self.meter_token_start]
else:
rhythm["eighth_position"] = token - self.eighth_position_token_start
return rhythm
if field == "structure":
return STRUCTURE_LABELS[tokens[0] - self.structure_token_start]
if field == "key":
index = tokens[0] - self.key_token_start
return f"{CHROMATIC_SHARPS[index % 12]}:{'minor' if index >= 12 else 'major'}"
if field == "chord":
return self.full_chord_labels[tokens[0] - self.full_chord_token_start]
notes, index = [], 0
while index < len(tokens):
pitch = tokens[index] - self.pitch_token_start
index += 1
duration = 0
if index < len(tokens) and self.token_type(tokens[index]) == "duration":
duration = tokens[index] - self.duration_token_start
index += 1
notes.append({"pitch": pitch % 128, "track": int(pitch >= 128),
"duration_bin": duration, "duration_steps": DURATION_TEMPLATES[duration]})
return notes
def decode_sequence(self, tokens):
fields = {"time": "timestamp", "meter": "rhythm", "eighth_position": "rhythm", "structure": "structure",
"key": "key", "chord_full": "chord", "pitch": "melody", "duration": "melody"}
position, subbeat, events = tokens.index(self.out_token) + 1, 0, []
while position < len(tokens) and tokens[position] != self.eos_token:
if self.token_type(tokens[position]) != "subbeat_shift":
raise ValueError("SheetSage2 produced an event without a beat position.")
while position < len(tokens) and self.token_type(tokens[position]) == "subbeat_shift":
subbeat += tokens[position] - self.subbeat_shift_token_start
position += 1
payload = {}
while position < len(tokens) and self.token_type(tokens[position]) not in ("subbeat_shift", "eos"):
token = tokens[position]
payload.setdefault(fields[self.token_type(token)], []).append(token)
position += 1
if payload:
events.append({"subbeat": subbeat, "tokens_by_field": payload,
"values": {field: self.decode_field(field, values) for field, values in payload.items()}})
return {"events": events}
def encode_events(self, events):
tokens, previous = self.prompt_prefix(), 0
for event in events:
shift = event["subbeat"] - previous
while shift > 256:
tokens.append(self.subbeat_shift_token_end - 1)
shift -= 256
tokens.append(self.subbeat_shift_token_start + shift)
previous = event["subbeat"]
for field in EVENT_FIELDS:
tokens.extend(event["tokens_by_field"].get(field, ()))
return tokens
def sliding_window_plan(duration, window_seconds=300.0, overlap_seconds=200.0, lookahead_seconds=100.0):
hop = window_seconds - overlap_seconds
start, accepted = 0.0, 0.0
result = []
while True:
last = start + window_seconds >= duration - 1e-6
accept_end = duration if last else start + window_seconds - lookahead_seconds
result.append(dict(start=start, end=min(duration, start + window_seconds),
accept_start=accepted, accept_end=accept_end, prefix_end=accepted,
generation_stop=None if last else window_seconds - lookahead_seconds))
if last:
return result
accepted = accept_end
start = min(start + hop, duration - window_seconds)
def overlap_prefix(stitched, tokenizer, start, prefix_end):
events = [event for event in stitched if start - 1e-4 <= event["time"] < prefix_end - 1e-4]
events.sort(key=lambda event: (event["global_subbeat"], event["time"]))
first = next((i for i, event in enumerate(events) if "timestamp" in event["values"] or "rhythm" in event["values"]), None)
if first is None:
return None, 0
events = copy.deepcopy(events[first:])
base = events[0]["global_subbeat"]
context = {}
for event in stitched:
if event["time"] > events[0]["time"] + 1e-6:
continue
for field in ("structure", "key", "chord"):
if event["tokens_by_field"].get(field):
context[field] = event["tokens_by_field"][field]
for token in event["tokens_by_field"].get("rhythm", ()):
if tokenizer.token_type(token) == "meter":
context["meter"] = token
for event in events:
event["subbeat"] = max(0, event["global_subbeat"] - base)
if "timestamp" in event["tokens_by_field"]:
time_id = min(29999, max(0, round((event["time"] - start) * tokenizer.time_hz)))
event["tokens_by_field"]["timestamp"] = [tokenizer.time_token_start + time_id]
first_fields = events[0]["tokens_by_field"]
for field in ("structure", "key", "chord"):
if field not in first_fields and field in context:
first_fields[field] = list(context[field])
rhythm = first_fields.get("rhythm", [])
if any(tokenizer.token_type(token) == "eighth_position" for token in rhythm) and not any(tokenizer.token_type(token) == "meter" for token in rhythm) and "meter" in context:
first_fields["rhythm"] = [context["meter"], *rhythm]
return tokenizer.encode_events(events), base
def event_time_map(decoded, target_seconds):
anchors = sorted({event["subbeat"]: event["values"]["timestamp"] for event in decoded["events"] if "timestamp" in event["values"]}.items())
if not anchors:
return lambda step: min(target_seconds, max(0.0, step * 0.125))
steps, times = np.asarray(anchors, dtype=np.float64).T
period = float(np.median(np.diff(times) / np.maximum(np.diff(steps), 1))) if len(anchors) > 1 else 0.125
if not np.isfinite(period) or period <= 0:
period = 0.125
def lookup(step):
if step <= steps[0]:
return float(np.clip(times[0] + (step - steps[0]) * period, 0, target_seconds))
if step >= steps[-1]:
return float(np.clip(times[-1] + (step - steps[-1]) * period, 0, target_seconds))
return float(np.interp(step, steps, times))
return lookup
def stitched_window_events(decoded, lookup, start, accept_start, accept_end, duration, global_subbeat_base=0):
accepted = []
for source in decoded["events"]:
time = start + lookup(source["subbeat"])
if time < accept_start - 1e-4 or time >= accept_end - 1e-4 or time >= duration - 1e-4:
continue
event = copy.deepcopy(source)
event["time"] = float(np.clip(time, 0, duration))
event["global_subbeat"] = global_subbeat_base + event["subbeat"]
if "timestamp" in event["values"]:
event["values"]["timestamp"] = event["time"]
for note in event["values"].get("melody", ()):
note["end_time"] = min(duration, max(event["time"] + 0.04, start + lookup(event["subbeat"] + note["duration_steps"])))
accepted.append(event)
return accepted
class PromptGrammarState:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
self.generated_events = 0
self.in_shift = True
self.shift_run = 0
self.payload_count = 0
self.last_field_index = -1
self.incomplete = None
def _allow_field_starts(self, allowed):
tokenizer = self.tokenizer
if self.last_field_index < FIELD_TO_INDEX["timestamp"]:
allowed[tokenizer.time_token_start : tokenizer.time_token_end] = True
if self.last_field_index < FIELD_TO_INDEX["rhythm"]:
allowed[tokenizer.meter_token_start : tokenizer.meter_token_end] = True
allowed[
tokenizer.eighth_position_token_start : tokenizer.eighth_position_token_end
] = True
if self.last_field_index < FIELD_TO_INDEX["structure"]:
allowed[
tokenizer.structure_token_start : tokenizer.structure_token_end
] = True
if self.last_field_index < FIELD_TO_INDEX["key"]:
allowed[tokenizer.key_token_start : tokenizer.key_token_end] = True
if self.last_field_index < FIELD_TO_INDEX["chord"]:
allowed[
tokenizer.full_chord_token_start : tokenizer.full_chord_token_end
] = True
if self.last_field_index <= FIELD_TO_INDEX["melody"]:
allowed[tokenizer.pitch_token_start : tokenizer.pitch_token_end] = True
def allowed(self, device):
tokenizer = self.tokenizer
allowed = torch.zeros(tokenizer.n_tokens, dtype=torch.bool, device=device)
can_end = self.payload_count > 0
if can_end:
allowed[tokenizer.eos_token] = True
if self.payload_count > 0 or self.in_shift:
if self.shift_run < 4:
allowed[
tokenizer.subbeat_shift_token_start : tokenizer.subbeat_shift_token_end
] = True
if self.incomplete == "rhythm_after_meter":
allowed[
tokenizer.eighth_position_token_start : tokenizer.eighth_position_token_end
] = True
return allowed
if self.incomplete == "melody_after_pitch":
allowed[tokenizer.duration_token_start : tokenizer.duration_token_end] = True
allowed[tokenizer.pitch_token_start : tokenizer.pitch_token_end] = True
return allowed
self._allow_field_starts(allowed)
return allowed
def update(self, token):
tokenizer = self.tokenizer
token = int(token)
token_type = tokenizer.token_type(token)
if token == tokenizer.eos_token:
return True
if token_type == "subbeat_shift":
if not self.in_shift and self.payload_count > 0:
self.generated_events += 1
self.payload_count = 0
self.last_field_index = -1
self.incomplete = None
self.in_shift = True
self.shift_run += 1
return False
self.in_shift = False
self.shift_run = 0
self.payload_count += 1
if token_type == "time":
self.last_field_index = FIELD_TO_INDEX["timestamp"]
self.incomplete = None
elif token_type == "meter":
self.last_field_index = FIELD_TO_INDEX["rhythm"]
self.incomplete = "rhythm_after_meter"
elif token_type == "eighth_position":
self.last_field_index = FIELD_TO_INDEX["rhythm"]
self.incomplete = None
elif token_type == "structure":
self.last_field_index = FIELD_TO_INDEX["structure"]
self.incomplete = None
elif token_type == "key":
self.last_field_index = FIELD_TO_INDEX["key"]
self.incomplete = None
elif token_type == "chord_full":
self.last_field_index = FIELD_TO_INDEX["chord"]
self.incomplete = None
elif token_type == "pitch":
self.last_field_index = FIELD_TO_INDEX["melody"]
self.incomplete = "melody_after_pitch"
elif token_type == "duration":
self.last_field_index = FIELD_TO_INDEX["melody"]
self.incomplete = None
else:
raise RuntimeError(f"Unexpected prompt token type {token_type!r}")
return False
+921
View File
@@ -0,0 +1,921 @@
"""SheetSage2 timed events to two-voice ABC, adapted from m-a-p/SheetSage2."""
from __future__ import annotations
import math
import re
from collections import Counter
from dataclasses import dataclass, replace
from fractions import Fraction
from typing import Sequence
import numpy as np
_SUPPORTED_DURATION_UNITS = frozenset({1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48})
_MUSIC_ELEMENT_RE = re.compile(
r'"(?P<quoted>[^"]*)"'
r"|\[K:(?P<key>[^\]]+)\]"
r"|(?P<note>[_=^]*[A-Ga-gz][,']*)(?P<duration>\d*)(?P<tie>-?)"
)
def interval_rows(events, field, duration):
rows = [[event["time"], duration, event["values"][field]] for event in events if field in event["values"]]
for previous, current in zip(rows, rows[1:]):
previous[1] = current[0]
return [row for row in rows if row[1] > row[0]]
def events_to_abc(events, duration, melody_only=True):
beats, meter = [], None
notes = {"Vocal": [], "Ins": []}
for event in events:
rhythm = event["values"].get("rhythm", {})
meter = rhythm.get("meter", meter)
eighth = rhythm.get("eighth_position")
if eighth is not None and meter is not None:
position = Fraction(eighth * meter[1], 8)
if position.denominator != 1 or not 0 <= position < meter[0]:
raise BeatGridError(f"Beat position {eighth} is outside the decoded {meter[0]}/{meter[1]} grid.")
beats.append(BeatEvent(event["time"], int(position) + 1, meter[0], meter[1]))
for note in event["values"].get("melody", ()):
end = min(duration, note["end_time"])
if end > event["time"]:
notes[VOICE_IDS[note["track"]]].append([event["time"], end, note["pitch"]])
if len(beats) < 2:
raise BeatGridError("SheetSage2 needs at least two decoded beats to produce ABC.")
if any(current.time <= previous.time for previous, current in zip(beats, beats[1:])):
raise BeatGridError("SheetSage2 decoded beat times are not increasing.")
period = float(np.median(np.diff([beat.time for beat in beats[-9:]])))
while beats[-1].time < duration - 1e-6:
previous = beats[-1]
beats.append(BeatEvent(previous.time + period, previous.beat_id % previous.declared_numerator + 1,
previous.declared_numerator, previous.denominator))
intervals = {}
for field in ("key", "structure", "chord"):
intervals[field] = [[max(beats[0].time, start), min(beats[-1].time, end), value]
for start, end, value in interval_rows(events, field, duration)
if end > beats[0].time and start < beats[-1].time]
if not intervals["key"]:
raise AbcRebuildError("SheetSage2 did not decode a key for the ABC score.")
keys = [(start, end, key_symbol_to_abc(key)) for start, end, key in intervals["key"]]
measures, diagnostics = infer_measures(beats)
times, quarters, denominators = _build_grid(beats, measures)
voices = {}
for voice, track in notes.items():
track.sort(key=lambda note: (note[0], note[2], note[1]))
for previous, current in zip(track, track[1:]):
if previous[1] > current[0] + 1e-6:
previous[1] = current[0]
voices[voice] = _notes_to_arr([note for note in track if note[1] > note[0] + 1e-6], times, voice)
score = RebuiltAbcScore(
beats=beats, measures=measures, subbeat_times=times, subbeat_quarters=quarters,
subbeat_denominators=denominators,
key_arr=_fill_intervals(keys, times, default=keys[0][2], dtype="<U16"),
chord_arr=np.full(len(times), "N", dtype="<U64") if melody_only else _fill_intervals(intervals["chord"], times, default="N", dtype="<U64"),
structure_events=_structure_events(intervals["structure"], times), voice_arrs=voices, diagnostics=diagnostics,
)
return score_to_abc(score)
SUBBEAT_DIVISION = 4
VOICE_IDS = ("Vocal", "Ins")
NO_CHORDS = frozenset({"N", "X", "?"})
class AbcRebuildError(ValueError):
"""Base class for deterministic reconstruction failures."""
class BeatGridError(AbcRebuildError):
pass
class ChordSymbolError(AbcRebuildError):
pass
class MelodyVoiceError(AbcRebuildError):
pass
@dataclass(frozen=True)
class BeatEvent:
time: float
beat_id: int
declared_numerator: int
denominator: int
@dataclass(frozen=True)
class Measure:
index: int
start_beat: int
end_beat: int
numerator: int
denominator: int
pickup: bool = False
partial: bool = False
inferred: bool = False
notated_numerator: int | None = None
notated_denominator: int | None = None
pad_before: bool = False
@property
def beat_count(self) -> int:
return self.end_beat - self.start_beat
@property
def start_t(self) -> int:
return self.start_beat * SUBBEAT_DIVISION
@property
def end_t(self) -> int:
return self.end_beat * SUBBEAT_DIVISION
@property
def abc_numerator(self) -> int:
return self.notated_numerator or self.numerator
@property
def abc_denominator(self) -> int:
return self.notated_denominator or self.denominator
@dataclass
class RebuiltAbcScore:
beats: list[BeatEvent]
measures: list[Measure]
subbeat_times: np.ndarray
subbeat_quarters: np.ndarray
subbeat_denominators: np.ndarray
key_arr: np.ndarray
chord_arr: np.ndarray
structure_events: list[tuple[int, str]]
voice_arrs: dict[str, np.ndarray]
diagnostics: list[str]
subbeat_div: int = SUBBEAT_DIVISION
@dataclass
class MeasureGroup:
measures: list[Measure]
structure_labels: list[str]
meter_changed: bool
key_changed: bool
_QUALITY_TO_ABC = {
"maj": "",
"min": "m",
"dim": "dim",
"aug": "aug",
"7": "7",
"maj7": "maj7",
"min7": "m7",
"dim7": "dim7",
"hdim7": "m7b5",
"sus4": "sus4",
"sus2": "sus2",
"maj6": "6",
"min6": "m6",
"sus4(b7)": "7sus4",
# abc2midi and SymMusic both accept the parenthesized major seventh.
# Common aliases such as mmaj7/mM7 trigger abc2midi diagnostics.
"minmaj7": "m(maj7)",
}
_NATURAL_PITCH_CLASS = {
"C": 0,
"D": 2,
"E": 4,
"F": 5,
"G": 7,
"A": 9,
"B": 11,
}
_LETTERS = "CDEFGAB"
_SHARP_PITCH_NAMES = ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")
_FLAT_PITCH_NAMES = ("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B")
_ROOT_RE = re.compile(r"^(?P<letter>[A-G])(?P<accidental>#{0,2}|b{0,2})$")
_BASS_DEGREE_RE = re.compile(r"^(?P<accidental>#{0,2}|b{0,2})(?P<degree>[1-9]|1[0-3])$")
_KEY_SIGNATURE_ACCIDENTALS = {
"C": 0,
"G": 1,
"D": 2,
"A": 3,
"E": 4,
"B": 5,
"F#": 6,
"C#": 7,
"F": -1,
"Bb": -2,
"Eb": -3,
"Ab": -4,
"Db": -5,
"Gb": -6,
"Cb": -7,
"Am": 0,
"Em": 1,
"Bm": 2,
"F#m": 3,
"C#m": 4,
"G#m": 5,
"D#m": 6,
"A#m": 7,
"Dm": -1,
"Gm": -2,
"Cm": -3,
"Fm": -4,
"Bbm": -5,
"Ebm": -6,
"Abm": -7,
}
_KEY_RELATIVE_PITCH_NAMES = {
7: ("B#", "C#", "C##", "D#", "D##", "E#", "F#", "F##", "G#", "G##", "A#", "B"),
6: ("B#", "C#", "C##", "D#", "E", "E#", "F#", "F##", "G#", "G##", "A#", "B"),
5: ("B#", "C#", "C##", "D#", "E", "E#", "F#", "F##", "G#", "A", "A#", "B"),
4: ("B#", "C#", "D", "D#", "E", "E#", "F#", "F##", "G#", "A", "A#", "B"),
3: ("B#", "C#", "D", "D#", "E", "E#", "F#", "G", "G#", "A", "A#", "B"),
2: ("C", "C#", "D", "D#", "E", "E#", "F#", "G", "G#", "A", "A#", "B"),
1: ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"),
0: ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "Bb", "B"),
-1: ("C", "C#", "D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B"),
-2: ("C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"),
-3: ("C", "Db", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"),
-4: ("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"),
-5: ("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "Cb"),
-6: ("C", "Db", "D", "Eb", "Fb", "F", "Gb", "G", "Ab", "A", "Bb", "Cb"),
-7: ("C", "Db", "D", "Eb", "Fb", "F", "Gb", "G", "Ab", "Bbb", "Bb", "Cb"),
}
def _mode_with_first_tiebreak(values: Sequence[int]) -> int:
counts = Counter(values)
maximum = max(counts.values())
return next(value for value in values if counts[value] == maximum)
def infer_measures(beats: Sequence[BeatEvent]) -> tuple[list[Measure], list[str]]:
"""Infer self-consistent measures from actual downbeat boundaries."""
downbeat_indices = [index for index, beat in enumerate(beats) if beat.beat_id == 1]
if not downbeat_indices:
raise BeatGridError("No downbeat (beat ID 1) exists in the beat lab")
spans: list[tuple[int, int, bool, bool]] = []
if downbeat_indices[0] > 0:
spans.append((0, downbeat_indices[0], True, False))
spans.extend(
(start, end, False, False)
for start, end in zip(downbeat_indices, downbeat_indices[1:])
)
if downbeat_indices[-1] < len(beats) - 1:
# Exported beat labs use their last row as the score end boundary. If
# that row is not a downbeat, the final bar is intentionally truncated.
spans.append((downbeat_indices[-1], len(beats) - 1, False, True))
if not spans:
raise BeatGridError("No positive-length measure exists between downbeats")
diagnostics = []
measures = []
for measure_index, (start, end, pickup, partial) in enumerate(spans):
events = list(beats[start:end])
# Downbeat spans still define measures when the model skips a beat ID.
beat_count = len(events)
denominators = [event.denominator for event in events]
denominator = _mode_with_first_tiebreak(denominators)
declared_numerators = [event.declared_numerator for event in events]
declared_numerator = _mode_with_first_tiebreak(declared_numerators)
numerator_conflict = any(value != beat_count for value in declared_numerators)
denominator_conflict = any(value != denominator for value in denominators)
pad_final_partial = (
partial
and len(set(declared_numerators)) == 1
and not denominator_conflict
and declared_numerator >= beat_count
)
inferred = pickup or partial or numerator_conflict or denominator_conflict
if pad_final_partial and declared_numerator > beat_count:
diagnostics.append(
f"measure {measure_index}: padded final {beat_count}/{denominator} span "
f"to declared {declared_numerator}/{denominator} with trailing rest"
)
elif numerator_conflict:
diagnostics.append(
f"measure {measure_index}: inferred {beat_count}/{denominator} from downbeat span; "
f"declared numerators were {declared_numerators}"
)
if denominator_conflict:
diagnostics.append(
f"measure {measure_index}: placed denominator {denominator} at the measure boundary; "
f"row declarations were {denominators}"
)
measures.append(
Measure(
index=measure_index,
start_beat=start,
end_beat=end,
numerator=beat_count,
denominator=denominator,
pickup=pickup,
partial=partial,
inferred=inferred,
notated_numerator=(
declared_numerator if pad_final_partial else beat_count
),
)
)
if len(measures) >= 2:
first = measures[0]
following = measures[1]
first_duration = first.numerator / first.denominator
following_duration = (
following.abc_numerator / following.abc_denominator
)
if first_duration < following_duration:
measures[0] = replace(
first,
inferred=True,
notated_numerator=following.abc_numerator,
notated_denominator=following.abc_denominator,
pad_before=True,
)
diagnostics.append(
f"measure 0: padded leading {first.numerator}/{first.denominator} span "
f"to {following.abc_numerator}/{following.abc_denominator} "
f"with preceding rest"
)
return measures, diagnostics
def _build_grid(beats: Sequence[BeatEvent], measures: Sequence[Measure]):
interval_denominators = np.zeros(len(beats) - 1, dtype=np.int32)
for measure in measures:
interval_denominators[measure.start_beat:measure.end_beat] = measure.denominator
if np.any(interval_denominators == 0):
raise BeatGridError("Downbeat spans do not cover every beat interval")
subbeat_times = []
subbeat_denominators = []
quarter_positions = [0.0]
current_quarter = 0.0
for index in range(len(beats) - 1):
start = beats[index].time
end = beats[index + 1].time
denominator = int(interval_denominators[index])
times = np.linspace(start, end, SUBBEAT_DIVISION + 1)[:-1]
subbeat_times.extend(float(value) for value in times)
subbeat_denominators.extend([denominator] * SUBBEAT_DIVISION)
quarter_step = 4.0 / denominator / SUBBEAT_DIVISION
for _ in range(SUBBEAT_DIVISION):
current_quarter += quarter_step
quarter_positions.append(current_quarter)
subbeat_times.append(beats[-1].time)
subbeat_denominators.append(int(interval_denominators[-1]))
return (
np.asarray(subbeat_times, dtype=np.float64),
np.asarray(quarter_positions, dtype=np.float64),
np.asarray(subbeat_denominators, dtype=np.int32),
)
def _subbeat_boundaries(subbeat_times: np.ndarray) -> np.ndarray:
return (subbeat_times[:-1] + subbeat_times[1:]) / 2
def _quantize_time(time: float, subbeat_times: np.ndarray) -> int:
return int(np.searchsorted(_subbeat_boundaries(subbeat_times), float(time)))
def _fill_intervals(rows, subbeat_times, *, default, dtype):
result = np.full(len(subbeat_times), default, dtype=dtype)
for start, end, value in rows:
start_t = _quantize_time(start, subbeat_times)
end_t = _quantize_time(end, subbeat_times)
start_t = max(0, min(start_t, len(result) - 1))
end_t = max(0, min(end_t, len(result) - 1))
if start_t == end_t == len(result) - 1:
continue
if end_t <= start_t:
raise AbcRebuildError(
f"Interval {start:.6f}-{end:.6f} ({value}) is shorter than the ABC subbeat grid"
)
result[start_t:end_t] = value
if len(result) > 1:
result[-1] = result[-2]
return result
def _structure_events(rows, subbeat_times):
events = []
for start, _, label in rows:
t = _quantize_time(start, subbeat_times)
t = max(0, min(t, len(subbeat_times) - 1))
events.append((t, label))
return events
def _notes_to_arr(notes, subbeat_times, voice_id):
result = np.zeros(len(subbeat_times), dtype=np.int32)
boundaries = _subbeat_boundaries(subbeat_times)
for note in sorted(notes, key=lambda item: (item[0], item[1], item[2])):
start_t = int(np.searchsorted(boundaries, note[0]))
end_t = int(np.searchsorted(boundaries, note[1]))
start_t = max(0, min(start_t, len(result) - 1))
end_t = max(0, min(end_t, len(result) - 1))
if start_t == end_t == len(result) - 1:
continue
if end_t <= start_t:
raise MelodyVoiceError(
f"{voice_id}: note pitch={note[2]} at {note[0]:.6f}-{note[1]:.6f} "
"cannot be represented on the decoded subbeat grid"
)
if np.any(result[start_t:end_t] != 0):
raise MelodyVoiceError(
f"{voice_id}: overlapping quantized melody notes at subbeats {start_t}:{end_t}"
)
sustain = note[2] * 2 + 2
result[start_t:end_t] = sustain
result[start_t] = sustain + 1
return result
def _pitch_class(root: str) -> tuple[int, str, str]:
match = _ROOT_RE.fullmatch(root)
if match is None:
raise ChordSymbolError(f"Invalid pitch spelling {root!r}")
letter = match.group("letter")
accidental = match.group("accidental")
offset = accidental.count("#") - accidental.count("b")
return (_NATURAL_PITCH_CLASS[letter] + offset) % 12, letter, accidental
def portable_pitch_name(root: str, *, preserve_double: bool = False) -> str:
pitch_class, _, accidental = _pitch_class(root)
if preserve_double or len(accidental) <= 1:
return root
names = _SHARP_PITCH_NAMES if accidental.startswith("#") else _FLAT_PITCH_NAMES
return names[pitch_class]
def _bass_degree_to_pitch(root: str, degree_text: str) -> str:
if _ROOT_RE.fullmatch(degree_text):
return portable_pitch_name(degree_text, preserve_double=True)
match = _BASS_DEGREE_RE.fullmatch(degree_text)
if match is None:
raise ChordSymbolError(f"Invalid chord bass degree {degree_text!r}")
root_pc, root_letter, root_accidental = _pitch_class(root)
degree = int(match.group("degree"))
degree_accidental = match.group("accidental")
scale_semitones = (0, 2, 4, 5, 7, 9, 11)
interval = scale_semitones[(degree - 1) % 7] + 12 * ((degree - 1) // 7)
interval += degree_accidental.count("#") - degree_accidental.count("b")
target_pc = (root_pc + interval) % 12
target_letter_index = (_LETTERS.index(root_letter) + degree - 1) % 7
target_letter = _LETTERS[target_letter_index]
natural_pc = _NATURAL_PITCH_CLASS[target_letter]
difference = (target_pc - natural_pc + 6) % 12 - 6
if difference in {-2, -1, 0, 1, 2}:
accidental = {-2: "bb", -1: "b", 0: "", 1: "#", 2: "##"}[difference]
return target_letter + accidental
names = _SHARP_PITCH_NAMES if "#" in (root_accidental + degree_accidental) else _FLAT_PITCH_NAMES
return names[target_pc]
def chord_symbol_to_abc(chord: str) -> str | None:
chord = chord.strip()
if chord in NO_CHORDS:
return None
if ":" not in chord:
raise ChordSymbolError(f"Chord {chord!r} is missing the ':' quality separator")
root, descriptor = chord.split(":", 1)
if "/" in descriptor:
quality, bass_degree = descriptor.split("/", 1)
else:
quality, bass_degree = descriptor, None
if quality not in _QUALITY_TO_ABC:
raise ChordSymbolError(
f"Unsupported chord quality {quality!r} in {chord!r}; refusing to rewrite it as major"
)
chord_root = portable_pitch_name(root, preserve_double=True)
text = chord_root + _QUALITY_TO_ABC[quality]
if bass_degree:
text += "/" + _bass_degree_to_pitch(root, bass_degree)
return text
def key_symbol_to_abc(key: str) -> str:
key = key.strip()
if ":" in key:
root, mode = key.split(":", 1)
if mode not in {"major", "minor"}:
raise AbcRebuildError(f"Unsupported key mode {mode!r} in {key!r}")
elif key.endswith("m"):
root, mode = key[:-1], "minor"
else:
root, mode = key, "major"
root_pc, _, accidental = _pitch_class(root)
candidate = portable_pitch_name(root) + ("m" if mode == "minor" else "")
if candidate in _KEY_SIGNATURE_ACCIDENTALS:
return candidate
names = _FLAT_PITCH_NAMES if "b" in accidental else _SHARP_PITCH_NAMES
candidate = names[root_pc] + ("m" if mode == "minor" else "")
if candidate not in _KEY_SIGNATURE_ACCIDENTALS:
fallback_names = _SHARP_PITCH_NAMES if names is _FLAT_PITCH_NAMES else _FLAT_PITCH_NAMES
candidate = fallback_names[root_pc] + ("m" if mode == "minor" else "")
if candidate not in _KEY_SIGNATURE_ACCIDENTALS:
raise AbcRebuildError(f"Cannot encode portable ABC key for {key!r}")
return candidate
def get_key_accidentals(key: str) -> list[int]:
try:
count = _KEY_SIGNATURE_ACCIDENTALS[key]
except KeyError as exc:
raise AbcRebuildError(f"Unsupported ABC key signature {key!r}") from exc
accidentals = [0] * 7
order = "FCGDAEB" if count > 0 else "BEADGCF"
for letter in order[:abs(count)]:
accidentals[_LETTERS.index(letter)] = 1 if count > 0 else -1
return accidentals
def note_to_abc(note: int, key_accidentals: Sequence[int], measure_accidentals: dict) -> str:
"""Use key-relative spelling and write only bar-state changes.
The two target parsers propagate an accidental to the same note letter in
every octave until the next barline. ``measure_accidentals`` is therefore
keyed by letter and reset by the caller for every bar (and after an inline
key change). This preserves pitches across parsers while still omitting
repeated accidental marks. The key-relative spelling can use double
accidentals in remote keys; MIDI G is F## in G# minor, for example.
"""
accidental_count = sum(key_accidentals)
try:
pitch_name = _KEY_RELATIVE_PITCH_NAMES[accidental_count][note % 12]
except KeyError as exc:
raise AbcRebuildError(
f"Unsupported key signature accidental count {accidental_count}"
) from exc
letter = pitch_name[0]
accidental = pitch_name[1:]
accidental_number = {"": 0, "#": 1, "##": 2, "b": -1, "bb": -2}[accidental]
octave = (note - 60) // 12
# Cb and B# cross the MIDI octave boundary even though their written note
# letter does not.
if note % 12 == 11 and accidental_number == -1:
octave += 1
elif note % 12 == 0 and accidental_number == 1:
octave -= 1
scale_index = _LETTERS.index(letter)
current_accidental = measure_accidentals.get(
scale_index,
key_accidentals[scale_index],
)
accidental_text = ""
if current_accidental != accidental_number:
measure_accidentals[scale_index] = accidental_number
accidental_text = {-2: "__", -1: "_", 0: "=", 1: "^", 2: "^^"}[
accidental_number
]
if octave > 0:
letter = letter.lower()
if octave > 1:
letter += "'" * (octave - 1)
elif octave < 0:
letter += "," * abs(octave)
return accidental_text + letter
def abc_unit_denominator(score: RebuiltAbcScore) -> int:
values = [
denominator * score.subbeat_div
for measure in score.measures
for denominator in (measure.denominator, measure.abc_denominator)
]
denominator = math.lcm(*values)
if denominator > 1024:
raise AbcRebuildError(f"Required ABC unit length 1/{denominator} is unreasonably small")
return denominator
def _measure_actual_units(measure: Measure, unit_denominator: int) -> int:
return measure.numerator * unit_denominator // measure.denominator
def _measure_abc_units(measure: Measure, unit_denominator: int) -> int:
return measure.abc_numerator * unit_denominator // measure.abc_denominator
def _measure_padding_units(measure: Measure, unit_denominator: int) -> int:
return (
_measure_abc_units(measure, unit_denominator)
- _measure_actual_units(measure, unit_denominator)
)
def _duration_units(score: RebuiltAbcScore, start_t: int, end_t: int, unit_denominator: int) -> int:
units = 0
for denominator in score.subbeat_denominators[start_t:end_t]:
divisor = int(denominator) * score.subbeat_div
if unit_denominator % divisor:
raise AbcRebuildError(
f"ABC L:1/{unit_denominator} cannot express a 1/{divisor} subbeat exactly"
)
units += unit_denominator // divisor
return units
def estimate_tempo(score: RebuiltAbcScore) -> float:
seconds = score.subbeat_times[-1] - score.subbeat_times[0]
quarter_notes = score.subbeat_quarters[-1] - score.subbeat_quarters[0]
if seconds <= 0 or quarter_notes <= 0:
raise AbcRebuildError("Cannot estimate tempo from a zero-duration score")
return float(quarter_notes / seconds * 60.0)
def _continues_pitch(value: int, next_value: int) -> bool:
if value <= 0:
return False
pitch = value // 2 - 1
return next_value == pitch * 2 + 2
def _same_note_segment(value: int, next_value: int) -> bool:
if value == 0:
return next_value == 0
pitch = value // 2 - 1
return next_value == pitch * 2 + 2
def _split_duration_units(duration: int) -> list[int]:
"""Split a duration into values accepted by strict music parsers."""
if duration <= 0:
raise AbcRebuildError(f"Cannot serialize non-positive duration {duration}")
result = []
remaining = int(duration)
while remaining:
if remaining in _SUPPORTED_DURATION_UNITS:
result.append(remaining)
break
candidates = [
value
for value in _SUPPORTED_DURATION_UNITS
if value < remaining
]
if not candidates:
raise AbcRebuildError(
f"Duration {duration} cannot be split into representable ABC values"
)
chunk = max(candidates)
result.append(chunk)
remaining -= chunk
return result
def _duration_text(duration: int) -> str:
return "" if duration == 1 else str(duration)
def _render_duration_tokens(
prefix: str,
note_text: str,
duration: int,
*,
tie_out: bool,
) -> list[str]:
chunks = _split_duration_units(duration)
tokens = []
for index, chunk in enumerate(chunks):
continues = note_text != "z" and (
index + 1 < len(chunks) or tie_out
)
tokens.append(
(prefix if index == 0 else "")
+ note_text
+ _duration_text(chunk)
+ ("-" if continues else "")
)
return tokens
def _render_voice_measure(
score: RebuiltAbcScore,
voice_id: str,
measure: Measure,
unit_denominator: int,
) -> str:
voice = score.voice_arrs[voice_id]
show_chords = voice_id == "Vocal"
measure_accidentals = {}
current_key = str(score.key_arr[measure.start_t])
key_accidentals = get_key_accidentals(current_key)
parts = []
padding = _measure_padding_units(measure, unit_denominator)
if padding < 0:
raise AbcRebuildError(
f"Measure {measure.index}: notated meter is shorter than its decoded span"
)
leading_padding = padding if measure.pad_before else 0
trailing_padding = 0 if measure.pad_before else padding
t = measure.start_t
while t < measure.end_t:
change_points = [measure.end_t]
for probe in range(t + 1, measure.end_t):
if not _same_note_segment(int(voice[t]), int(voice[probe])):
change_points.append(probe)
break
for probe in range(t + 1, measure.end_t):
if score.key_arr[probe] != score.key_arr[probe - 1]:
change_points.append(probe)
break
if show_chords:
for probe in range(t + 1, measure.end_t):
if score.chord_arr[probe] != score.chord_arr[probe - 1]:
change_points.append(probe)
break
next_t = min(change_points)
prefix = ""
key = str(score.key_arr[t])
if t > measure.start_t and key != current_key:
current_key = key
key_accidentals = get_key_accidentals(current_key)
measure_accidentals = {}
prefix += f"[K:{current_key}]"
if show_chords and (t == measure.start_t or score.chord_arr[t] != score.chord_arr[t - 1]):
chord = str(score.chord_arr[t])
chord_text = chord_symbol_to_abc(chord)
if chord_text is not None:
prefix += f'"{chord_text}"'
value = int(voice[t])
if value == 0:
note_text = "z"
else:
note_text = note_to_abc(value // 2 - 1, key_accidentals, measure_accidentals)
duration = _duration_units(score, t, next_t, unit_denominator)
if t == measure.start_t and leading_padding:
if value == 0 and not prefix:
duration += leading_padding
else:
parts.extend(
_render_duration_tokens(
"",
"z",
leading_padding,
tie_out=False,
)
)
leading_padding = 0
if value == 0 and next_t == measure.end_t and trailing_padding:
duration += trailing_padding
trailing_padding = 0
if duration <= 0:
raise AbcRebuildError(f"Non-positive ABC duration at subbeats {t}:{next_t}")
tie_out = (
value > 0
and next_t < len(voice)
and _continues_pitch(value, int(voice[next_t]))
)
parts.extend(
_render_duration_tokens(
prefix,
note_text,
duration,
tie_out=tie_out,
)
)
t = next_t
if leading_padding:
raise AbcRebuildError(
f"Measure {measure.index}: leading rest padding was not serialized"
)
if trailing_padding:
parts.extend(
_render_duration_tokens(
"",
"z",
trailing_padding,
tie_out=False,
)
)
return "".join(parts)
def _is_compressible_full_rest(rendered_measure: str) -> bool:
"""Whether a rendered measure can be losslessly replaced by ABC ``Z``."""
cursor = 0
saw_note = False
for match in _MUSIC_ELEMENT_RE.finditer(rendered_measure):
if rendered_measure[cursor:match.start()]:
return False
cursor = match.end()
if match.group("quoted") is not None or match.group("key") is not None:
return False
saw_note = True
if match.group("note") != "z" or match.group("tie"):
return False
return saw_note and cursor == len(rendered_measure)
def _render_voice_group(
score: RebuiltAbcScore,
voice_id: str,
measures: list[Measure],
unit_denominator: int,
) -> str:
rendered = [
_render_voice_measure(
score,
voice_id,
measure,
unit_denominator,
)
for measure in measures
]
parts = []
index = 0
while index < len(rendered):
if not _is_compressible_full_rest(rendered[index]):
parts.append(rendered[index] + "|")
index += 1
continue
end = index + 1
while (
end < len(rendered)
and _is_compressible_full_rest(rendered[end])
):
end += 1
count = end - index
parts.append("Z" + (str(count) if count > 1 else "") + "|")
index = end
return "".join(parts)
def _sanitize_structure_label(value: str) -> str:
return " ".join(str(value).split())
def _measure_groups(score: RebuiltAbcScore) -> list[MeasureGroup]:
first_measure = score.measures[0]
active_meter = (
first_measure.abc_numerator,
first_measure.abc_denominator,
)
active_key = str(score.key_arr[first_measure.start_t])
active_structure = ""
groups: list[MeasureGroup] = []
for measure in score.measures:
meter = (measure.abc_numerator, measure.abc_denominator)
key = str(score.key_arr[measure.start_t])
meter_changed = meter != active_meter
key_changed = key != active_key
new_structure_labels = []
for t, label in score.structure_events:
if not measure.start_t <= t < measure.end_t:
continue
clean_label = _sanitize_structure_label(label)
if clean_label and clean_label != active_structure:
new_structure_labels.append(clean_label)
active_structure = clean_label
start_group = (
not groups
or len(groups[-1].measures) >= 4
or meter_changed
or key_changed
or bool(new_structure_labels)
)
if start_group:
groups.append(
MeasureGroup(
measures=[measure],
structure_labels=new_structure_labels,
meter_changed=meter_changed,
key_changed=key_changed,
)
)
else:
groups[-1].measures.append(measure)
active_meter = meter
active_key = str(score.key_arr[measure.end_t - 1])
return groups
def score_to_abc(score: RebuiltAbcScore) -> str:
unit_denominator = abc_unit_denominator(score)
first_measure = score.measures[0]
first_key = str(score.key_arr[first_measure.start_t])
lines = [
"X:1",
"T:",
f"M:{first_measure.abc_numerator}/{first_measure.abc_denominator}",
f"L:1/{unit_denominator}",
f"Q:1/4={int(round(estimate_tempo(score)))}",
'V: Vocal clef=treble name="Vocal Melody" snm="Vocal"',
'V: Ins clef=treble name="Ins Melody" snm="Inst."',
f"K:{first_key}",
]
for group in _measure_groups(score):
lines.extend(f"% {label}" for label in group.structure_labels)
first_group_measure = group.measures[0]
for voice_id in VOICE_IDS:
lines.append(f"V: {voice_id}")
if group.meter_changed:
lines.append(
f"M:{first_group_measure.abc_numerator}/"
f"{first_group_measure.abc_denominator}"
)
if group.key_changed:
lines.append(
f"K:{score.key_arr[first_group_measure.start_t]}"
)
lines.append(
_render_voice_group(
score,
voice_id,
group.measures,
unit_denominator,
)
)
text = "\n".join(lines) + "\n"
return text
+6
View File
@@ -1024,6 +1024,12 @@ class ACEAudio15(LatentFormat):
latent_dimensions = 1
temporal_downscale_ratio = 1764
class YuE2(LatentFormat):
latent_channels = 64
latent_dimensions = 1
temporal_downscale_ratio = 1920
class MiniMaxMusic3(LatentFormat):
latent_channels = 128
latent_dimensions = 1
+8 -4
View File
@@ -157,7 +157,7 @@ class DecoderBlock(nn.Module):
else:
upsample_layer = WNConvTranspose1d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=2*stride, stride=stride, padding=math.ceil(stride/2))
kernel_size=2*stride, stride=stride, padding=math.ceil(stride/2), output_padding=stride % 2)
self.layers = nn.Sequential(
get_activation("snake" if use_snake else "elu", antialias=antialias_activation, channels=in_channels),
@@ -261,16 +261,20 @@ class AudioOobleckVAE(nn.Module):
use_snake=True,
antialias_activation=False,
use_nearest_upsample=False,
final_tanh=False):
final_tanh=False,
sample_latent=True):
super().__init__()
self.encoder = OobleckEncoder(in_channels, channels, latent_dim * 2, c_mults, strides, use_snake, antialias_activation)
self.decoder = OobleckDecoder(in_channels, channels, latent_dim, c_mults, strides, use_snake, antialias_activation,
use_nearest_upsample=use_nearest_upsample, final_tanh=final_tanh)
self.bottleneck = VAEBottleneck()
self.sample_latent = sample_latent
def encode(self, x):
return self.bottleneck.encode(self.encoder(x))
encoded = self.encoder(x)
if not self.sample_latent:
return encoded.chunk(2, dim=1)[0]
return self.bottleneck.encode(encoded)
def decode(self, x):
return self.decoder(self.bottleneck.decode(x))
+87
View File
@@ -0,0 +1,87 @@
"""YuE2 acoustic transformer. Adapted from M·A·P YuE2 (Apache-2.0)."""
import torch
from torch import nn
import comfy.model_management
import comfy.model_prefetch
import comfy.ops
from comfy.ldm.modules.attention import optimized_attention_for_device
from comfy.ldm.modules.diffusionmodules.util import timestep_embedding
from comfy.text_encoders.llama import Qwen3_8BConfig, RMSNorm, TransformerBlock, precompute_freqs_cis
def model_config(**overrides):
return Qwen3_8BConfig(**{
"vocab_size": 184704, "hidden_size": 2048, "intermediate_size": 6144,
"num_hidden_layers": 28, "num_attention_heads": 16, "num_key_value_heads": 8,
"max_position_embeddings": 24576, "merged_qkv": True, "merged_mlp": True,
**overrides,
})
class TimestepEmbedder(nn.Module):
def __init__(self, hidden_size, dtype, device, operations):
super().__init__()
self.mlp = nn.Sequential(
operations.Linear(256, hidden_size, dtype=dtype, device=device),
nn.SiLU(),
operations.Linear(hidden_size, hidden_size, dtype=dtype, device=device),
)
def forward(self, t, dtype):
return self.mlp(timestep_embedding(t, 256).to(dtype))
class AudioPositionEmbedding(nn.Module):
def __init__(self, frames, hidden_size, dtype, device):
super().__init__()
self.register_buffer("pe", torch.empty(frames, hidden_size, dtype=dtype, device=device))
def forward(self, length, x):
return comfy.ops.cast_to_input(self.pe[:length], x)
class YuE2(nn.Module):
def __init__(self, dtype=None, device=None, operations=None, **kwargs):
super().__init__()
self.dtype = dtype
self.config = model_config(**kwargs.get("config", {}))
config = self.config
self.model = nn.Module()
self.model.layers = nn.ModuleList([
TransformerBlock(config, i, device=device, dtype=dtype, ops=operations)
for i in range(config.num_hidden_layers)
])
self.model.norm = RMSNorm(config.hidden_size, config.rms_norm_eps, device=device, dtype=dtype)
self.vae2llm = operations.Linear(64, config.hidden_size, dtype=dtype, device=device)
self.llm2vae = operations.Linear(config.hidden_size, 64, dtype=dtype, device=device)
self.time_embedder = TimestepEmbedder(config.hidden_size, dtype, device, operations)
self.latent_pos_embed = AudioPositionEmbedding(config.max_position_embeddings, config.hidden_size, dtype, device)
def forward(self, x, timestep, context, yue2_chunks, transformer_options={}, **kwargs):
batch, channels, frames = x.shape
if frames != yue2_chunks[-1][1]:
raise ValueError("YuE2 latent duration must match the seconds output of YuE2 Text Encode.")
config = self.config
time = self.time_embedder(timestep.to(x.dtype), x.dtype)[:, None]
output = torch.empty_like(x)
attention = optimized_attention_for_device(x.device)
for start, end, kv_start, kv_end in yue2_chunks:
comfy.model_management.throw_exception_if_processing_interrupted()
ar_length = kv_end - kv_start
length = end - start + 2
state = torch.nn.functional.pad(x[..., start:end].transpose(1, 2), (0, 0, 1, 1))
state = self.vae2llm(state) + time + self.latent_pos_embed(length, x)[None]
positions = torch.arange(ar_length, ar_length + length, device=x.device)[None]
rope = precompute_freqs_cis(config.head_dim, positions, config.rope_theta, device=x.device)
prefix = context[:, kv_start:kv_end].reshape(batch, ar_length, config.num_hidden_layers, 2, config.num_key_value_heads, config.head_dim)
prefix = prefix.permute(2, 3, 0, 4, 1, 5)
prefetch = comfy.model_prefetch.make_prefetch_queue(list(self.model.layers), x.device, transformer_options)
for index, layer in enumerate(self.model.layers):
comfy.model_prefetch.prefetch_queue_pop(prefetch, x.device, layer, state.dtype)
state, _ = layer(state, freqs_cis=rope, optimized_attention=attention,
past_key_value=(prefix[index, 0], prefix[index, 1], ar_length))
comfy.model_prefetch.prefetch_queue_pop(prefetch, x.device, None)
output[..., start:end] = self.llm2vae(self.model.norm(state))[:, 1:-1].transpose(1, 2)
return output
+20
View File
@@ -23,6 +23,7 @@ import logging
import comfy.ldm.lightricks.av_model
import comfy.ldm.minimax.model
import comfy.ldm.minimax_music.dit
import comfy.ldm.yue2.model
import comfy.nested_tensor
import comfy.ldm.lightricks.symmetric_patchifier
import comfy.context_windows
@@ -2551,6 +2552,25 @@ class ACEStep15(BaseModel):
out['refer_audio'] = comfy.conds.CONDRegular(refer_audio)
return out
class YuE2(BaseModel):
def __init__(self, model_config, model_type=ModelType.FLOW, device=None):
super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.yue2.model.YuE2)
def extra_conds(self, **kwargs):
out = super().extra_conds(**kwargs)
context = kwargs["cross_attn"].to(device=kwargs["device"], dtype=self.get_dtype_inference())
out["c_crossattn"] = comfy.conds.CONDRegular(context)
out["yue2_chunks"] = comfy.conds.CONDConstant(kwargs["yue2_chunks"])
return out
def extra_conds_shapes(self, **kwargs):
return {"c_crossattn": kwargs["cross_attn"].shape}
def memory_required(self, input_shape, cond_shapes={}):
context_size = sum(math.prod(shape) for shape in cond_shapes.get("c_crossattn", []))
return super().memory_required(input_shape, cond_shapes) + context_size * comfy.model_management.dtype_size(self.get_dtype_inference())
class MiniMaxMusic3(BaseModel):
def __init__(self, model_config, model_type=ModelType.FLOW, device=None):
super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.minimax_music.dit.MiniMaxMusic3DiT)
+6
View File
@@ -1152,6 +1152,12 @@ def detect_unet_config(state_dict, key_prefix, metadata=None):
"text_dim": 4096,
}
if all(key_prefix + key in state_dict for key in (
"vae2llm.weight", "llm2vae.weight", "latent_pos_embed.pe",
"model.layers.0.self_attn.qkv_proj.weight", "time_embedder.mlp.0.weight",
)):
return {"audio_model": "yue2"}
if '{}input_blocks.0.0.weight'.format(key_prefix) not in state_dict_keys:
return None
+16 -1
View File
@@ -79,6 +79,7 @@ import comfy.text_encoders.qwen35
import comfy.text_encoders.qwen3vl
import comfy.text_encoders.minimax
import comfy.text_encoders.minimax_music
import comfy.text_encoders.yue2
import comfy.ldm.minimax.vae
import comfy.ldm.minimax.audio_vae
import comfy.text_encoders.boogu
@@ -695,6 +696,7 @@ class VAE:
decoder_config={'target': "comfy.ldm.modules.diffusionmodules.model.Decoder", 'params': decoder_ddconfig if decoder_ddconfig is not None else ddconfig})
elif "decoder.layers.1.layers.0.beta" in sd:
config = {}
yue2_vae = "decoder.layers.6.layers.1.weight_v" in sd or "decoder.layers.6.layers.1.parametrizations.weight.original1" in sd
param_key = None
self.upscale_ratio = 2048
self.downscale_ratio = 2048
@@ -709,6 +711,9 @@ class VAE:
self.upscale_ratio = 1920
self.downscale_ratio = 1920
if yue2_vae:
config.update(channels=64, c_mults=[1, 2, 4, 8, 16, 32], strides=[2, 2, 4, 4, 5, 6],
sample_latent=False)
self.first_stage_model = AudioOobleckVAE(**config)
self.memory_used_encode = lambda shape, dtype: (1000 * shape[2]) * model_management.dtype_size(dtype)
self.memory_used_decode = lambda shape, dtype: (1000 * shape[2] * 2048) * model_management.dtype_size(dtype)
@@ -720,6 +725,10 @@ class VAE:
self.process_input = lambda audio: audio
self.working_dtypes = [torch.float16, torch.bfloat16, torch.float32]
self.disable_offload = True
if yue2_vae:
self.audio_sample_rate = 48000
self.upscale_ratio = self.downscale_ratio = 1920
self.memory_used_decode = lambda shape, dtype: (1500 * shape[-1] * 1920) * model_management.dtype_size(dtype)
elif "blocks.2.blocks.3.stack.5.weight" in sd or "decoder.blocks.2.blocks.3.stack.5.weight" in sd or "layers.4.layers.1.attn_block.attn.qkv.weight" in sd or "encoder.layers.4.layers.1.attn_block.attn.qkv.weight" in sd: #genmo mochi vae
if "blocks.2.blocks.3.stack.5.weight" in sd:
sd = comfy.utils.state_dict_prefix_replace(sd, {"": "decoder."})
@@ -1552,6 +1561,7 @@ class CLIPType(Enum):
JOYIMAGE = 33
MAGE = 34
MINIMAX = 35
YUE2 = 36
@@ -1741,7 +1751,12 @@ def load_text_encoder_state_dicts(state_dicts=[], embedding_directory=None, clip
clip_target.params = {}
if len(clip_data) == 1:
te_model = detect_te_model(clip_data[0])
if clip_type == CLIPType.MINIMAX and "model.audio_decoder.projection.weight" in clip_data[0]:
if clip_type == CLIPType.YUE2 and "yue2_tokenizer_json" in clip_data[0]:
tokenizer_data["yue2_tokenizer_json"] = clip_data[0].pop("yue2_tokenizer_json")
detect = comfy.text_encoders.hunyuan_video.llama_detect(clip_data[0])
clip_target.clip = comfy.text_encoders.yue2.te(**detect)
clip_target.tokenizer = comfy.text_encoders.yue2.YuE2Tokenizer
elif clip_type == CLIPType.MINIMAX and "model.audio_decoder.projection.weight" in clip_data[0]:
tokenizer_data["tokenizer_json"] = clip_data[0].pop("tokenizer_json", None)
quant = comfy.utils.detect_layer_quantization(clip_data[0], "")
if quant is not None:
+23
View File
@@ -33,6 +33,7 @@ import comfy.text_encoders.mage_flow
import comfy.text_encoders.joyimage
import comfy.text_encoders.anima
import comfy.text_encoders.ace15
import comfy.text_encoders.yue2
import comfy.text_encoders.longcat_image
import comfy.text_encoders.ernie
import comfy.text_encoders.cogvideo
@@ -2265,6 +2266,27 @@ class ACEStep15(supported_models_base.BASE):
return supported_models_base.ClipTarget(comfy.text_encoders.ace15.ACE15Tokenizer, comfy.text_encoders.ace15.te(**detect))
class YuE2(supported_models_base.BASE):
unet_config = {"audio_model": "yue2"}
unet_extra_config = {}
latent_format = latent_formats.YuE2
supported_inference_dtypes = [torch.bfloat16, torch.float32]
sampling_settings = {"multiplier": 1.0}
memory_usage_factor = 4.0
vae_key_prefix = ["vae."]
text_encoder_key_prefix = ["text_encoders."]
def get_model(self, state_dict, prefix="", device=None):
return model_base.YuE2(self, device=device)
def model_type(self, state_dict, prefix=""):
return model_base.ModelType.FLOW
def clip_target(self, state_dict={}):
detect = comfy.text_encoders.hunyuan_video.llama_detect(state_dict, self.text_encoder_key_prefix[0])
return supported_models_base.ClipTarget(comfy.text_encoders.yue2.YuE2Tokenizer, comfy.text_encoders.yue2.te(**detect))
class MiniMaxMusic3(supported_models_base.BASE):
unet_config = {
"audio_model": "minimax_music3",
@@ -2583,6 +2605,7 @@ models = [
ACEStep,
ACEStep15,
MiniMaxMusic3,
YuE2,
Omnigen2,
Boogu,
MageFlow,
+266
View File
@@ -0,0 +1,266 @@
"""YuE2 score/semantic generation and acoustic prefix conditioning."""
import logging
import torch
from tokenizers import Tokenizer
import comfy.model_management
import comfy.model_prefetch
import comfy.ops
import comfy.utils
from comfy.ldm.yue2.model import model_config
from comfy.text_encoders.llama import FixedKV, Llama2_
EOD = 151643
ABC_START, ABC_END = 151847, 151848
MUSIC_START, MUSIC_END = 151851, 151852
CODEC_OFFSET, CODEC_SIZE = 151853, 32768
CONTEXT = 24576
FRAMES_PER_SECOND = 25
INSTRUCTIONS = {
"off": "Generate music with codec tokens from the given conditions.",
"melody": "Generate a melody-only ABC transcription without chord symbols, then generate music with codec tokens from the given conditions.",
"full": "Generate a chord-annotated ABC transcription, then generate music with codec tokens from the given conditions.",
}
def distribution(logits, history, step, phase, temperature, top_p, top_k, repetition_penalty, penalty_window, min_tokens, legacy_off=False):
scores = logits.clone() if legacy_off else logits.float().clone()
end = ABC_END if phase == "abc" else MUSIC_END
allowed = torch.full_like(scores, -torch.inf)
if phase == "abc":
allowed[..., :EOD] = 0
else:
allowed[..., CODEC_OFFSET:CODEC_OFFSET + CODEC_SIZE] = 0
allowed[..., end] = 0
scores += allowed
if step < min_tokens:
scores[..., end] = -torch.inf
if repetition_penalty != 1.0 and history:
recent = torch.tensor([history[-penalty_window:]], dtype=torch.long, device=scores.device)
counts = torch.zeros_like(scores)
counts.scatter_add_(-1, recent, torch.ones_like(recent, dtype=scores.dtype))
penalty = repetition_penalty ** counts
scores = torch.where(scores < 0, scores * penalty, scores / penalty)
if temperature == 0:
return scores
scores /= temperature
threshold = scores.topk(min(top_k, scores.shape[-1])).values[..., -1, None]
scores.masked_fill_(scores < threshold, -torch.inf)
if top_p < 1:
values, indices = scores.sort(descending=True)
probabilities = values.softmax(-1)
removed = probabilities.cumsum(-1) - probabilities > top_p
removed[..., :3 if legacy_off else 1] = False
values.masked_fill_(removed, -torch.inf)
scores = values.scatter(-1, indices, values)
return scores
def chunk_ranges(frames, prefix_tokens, context=CONTEXT):
size = (context - prefix_tokens - 3) // 2
if frames < 1 or size < 1:
raise ValueError("YuE2 needs music tokens and enough context for at least one acoustic frame.")
return [(start, min(start + size, frames)) for start in range(0, frames, size)]
class YuE2Tokenizer:
def __init__(self, embedding_directory=None, tokenizer_data={}):
data = tokenizer_data["yue2_tokenizer_json"]
if torch.is_tensor(data):
data = data.numpy().tobytes()
self.tokenizer_json = data
self.tokenizer = Tokenizer.from_str(data.decode("utf-8"))
def tokenize_with_weights(self, text, return_word_ids=False, **kwargs):
cot = kwargs.get("cot", "full")
prompt = f"{INSTRUCTIONS[cot]}\n[Tags]\n{text}\n[Lyrics]\n{kwargs.get('lyrics', '')}\n"
return {
"prefix": [EOD] + self.tokenizer.encode(prompt).ids + [ABC_START],
"negative": [EOD] + self.tokenizer.encode(INSTRUCTIONS[cot]).ids,
"abc_ids": self.tokenizer.encode(kwargs.get("abc", "")).ids,
"cot": cot,
"seed": kwargs.get("seed", 0),
"max_tokens": kwargs.get("max_tokens", 9000),
"temperature": kwargs.get("temperature", 1.0),
"top_p": kwargs.get("top_p", 0.95),
"top_k": kwargs.get("top_k", 100),
"repetition_penalty": kwargs.get("repetition_penalty", 1.2),
"cfg_scale": kwargs.get("cfg_scale", 1.01 if cot == "off" else 1.0),
}
def state_dict(self):
return {"yue2_tokenizer_json": torch.frombuffer(bytearray(self.tokenizer_json), dtype=torch.uint8)}
def decode(self, ids, skip_special_tokens=True):
return self.tokenizer.decode(ids, skip_special_tokens=skip_special_tokens)
class YuE2TEModel(torch.nn.Module):
def __init__(self, device="cpu", dtype=None, model_options={}, config=None):
super().__init__()
self.config = model_config(**{"fixed_kv": True, **(config or {})})
operations = model_options.get("custom_operations", comfy.ops.manual_cast)
quant = model_options.get("quantization_metadata")
if quant is not None and "custom_operations" not in model_options:
operations = comfy.ops.mixed_precision_ops(quant, dtype)
self.model = Llama2_(self.config, device=device, dtype=dtype, ops=operations)
self.model.prefetch_dynamic_vbars = True
self.model.graph_dynamic_vbar_blocks = True
self.dtypes = {dtype}
self.execution_device = device
def get_dynamic_vram__units(self):
return self.model.get_dynamic_vram__units()
def set_clip_options(self, options):
self.execution_device = options.get("execution_device", self.execution_device)
def reset_clip_options(self):
pass
def load_sd(self, state_dict):
return self.load_state_dict(state_dict, strict=False, assign=getattr(self, "can_assign_sd", False))
def memory_estimation_function(self, tokens, device=None):
config = self.config
abc_length = 0 if tokens["cot"] == "off" else len(tokens["abc_ids"])
length = min(config.max_position_embeddings, len(tokens["prefix"]) + abc_length + tokens["max_tokens"] + 2)
branches = 1 if tokens["cfg_scale"] == 1.0 else 2
dtype = torch.bfloat16 if comfy.model_management.should_use_bf16(device) else torch.float32
cache = branches * 2 * config.num_hidden_layers * config.num_key_value_heads * config.head_dim * length
prefill = branches * (length * length + length * (config.intermediate_size * 3 + config.hidden_size * 8))
return (cache + prefill) * comfy.model_management.dtype_size(dtype)
def _prefill(self, prefixes, capacity, dtype):
length = max(map(len, prefixes))
ids = torch.tensor([[0] * (length - len(prefix)) + prefix for prefix in prefixes], device=self.execution_device, dtype=torch.long)
mask = positions = None
if any(len(prefix) != length for prefix in prefixes):
mask = torch.ones((len(prefixes), capacity), device=self.execution_device, dtype=torch.long)
for index, prefix in enumerate(prefixes):
mask[index, :length - len(prefix)] = 0
positions = mask[:, :length].cumsum(-1).sub_(1).clamp_min_(0)
cache = self.model.init_kv_cache(len(prefixes), capacity, self.execution_device, dtype)
output = self.model(ids, attention_mask=mask[:, :length] if mask is not None else None,
position_ids=positions, past_key_values=cache, dtype=dtype)
return self.model.lm_head(output[0][:, -1]), output[2], mask
def _generate(self, prefix, seed, max_tokens, phase, dtype, negative=None, cfg_scale=1.0, legacy_off=False, **sampling):
if max(len(prefix), len(negative or [])) + max_tokens > self.config.max_position_embeddings:
raise ValueError("YuE2 prompt plus generation budget exceeds the model context; reduce the token budget or prompt length.")
device = self.execution_device
rng_device = device if torch.device(device).type != "mps" else "cpu"
generator = torch.Generator(device=rng_device).manual_seed(seed)
prefixes = [prefix] if cfg_scale == 1.0 else [prefix, negative]
prefix_length = max(map(len, prefixes))
logits, cache, mask = self._prefill(prefixes, prefix_length + max_tokens, dtype)
fixed_kv = isinstance(cache[0], FixedKV)
decode_tokens = torch.empty((len(prefixes), 1), device=device, dtype=torch.long)
positions = torch.tensor([[len(p)] for p in prefixes], device=device, dtype=torch.long)
history = []
end = ABC_END if phase == "abc" else MUSIC_END
progress = comfy.utils.ProgressBar(max_tokens)
try:
for step in comfy.utils.model_trange(max_tokens, desc="YuE2 ABC sampling" if phase == "abc" else "YuE2 music sampling", unit="token"):
comfy.model_management.throw_exception_if_processing_interrupted()
guided = logits if cfg_scale == 1.0 else logits[1:] + cfg_scale * (logits[:1] - logits[1:])
scores = distribution(guided, history, step, phase, legacy_off=legacy_off, **sampling)
if sampling["temperature"] == 0:
next_id = scores.argmax(-1, keepdim=True)
else:
probabilities = scores.softmax(-1).to(rng_device)
next_id = torch.multinomial(probabilities, 1, generator=generator).to(device)
decode_tokens.copy_(next_id)
token = next_id.item()
progress.update_absolute(step + 1)
if token == end:
return history, False
history.append(token)
if step + 1 < max_tokens:
# Keep decode allocations stable; sampling has a changing history window.
if fixed_kv:
comfy.model_prefetch.malloc_graph_begin(device)
output = self.model(decode_tokens, past_key_values=cache, dtype=dtype, position_ids=positions,
attention_mask=mask[:, :prefix_length + step + 1] if mask is not None and not fixed_kv else None)
logits.copy_(self.model.lm_head(output[0][:, -1]))
cache = output[2]
del output
if fixed_kv:
comfy.model_prefetch.malloc_graph_end()
positions.add_(1)
finally:
# Each phase has different KV buffers and may change the CFG batch size.
comfy.model_prefetch.cleanup_prefetch_queues()
logging.warning("YuE2 %s reached its token budget; increase the limit for a complete result.", phase)
return history, True
def _acoustic_conditioning(self, prefix, tokens, dtype):
config = self.config
ranges = chunk_ranges(len(tokens), len(prefix), config.max_position_embeddings)
total = sum(len(prefix) + end - start + 1 for start, end in ranges)
# A normal [batch, tokens, features] conditioning tensor, with each layer's KV in features.
output = torch.empty((1, total, config.num_hidden_layers, 2, config.num_key_value_heads, config.head_dim),
device=comfy.model_management.intermediate_device(), dtype=dtype)
chunks = []
offset = 0
for start, end in ranges:
comfy.model_management.throw_exception_if_processing_interrupted()
ids = prefix + tokens[start:end] + [MUSIC_END]
_, cache, _ = self._prefill([ids], len(ids), dtype)
for index, kv in enumerate(cache):
if isinstance(kv, FixedKV):
key, value = kv.key, kv.value
else:
key, value, _ = kv
key, value = key.transpose(1, 2), value.transpose(1, 2)
output[:, offset:offset + len(ids), index, 0].copy_(key)
output[:, offset:offset + len(ids), index, 1].copy_(value)
chunks.append((start, end, offset, offset + len(ids)))
offset += len(ids)
del cache
return output.flatten(2), tuple(chunks)
def generate(self, tokens, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.95, repetition_penalty=1.0, seed=None, **kwargs):
dtype = torch.bfloat16 if comfy.model_management.should_use_bf16(self.execution_device) else torch.float32
ids, _ = self._generate(
tokens["prefix"], tokens["seed"] if seed is None else seed, max_length, "abc", dtype,
temperature=temperature if do_sample else 0, top_p=top_p, top_k=top_k,
repetition_penalty=repetition_penalty, penalty_window=100, min_tokens=min(32, max_length),
)
return ids
def encode_token_weights(self, tokens):
device = self.execution_device
dtype = torch.bfloat16 if comfy.model_management.should_use_bf16(device) else torch.float32
prefix = tokens["prefix"]
abc_ids = tokens["abc_ids"]
cot = tokens["cot"]
if cot == "off":
abc_ids = []
prefix = prefix + abc_ids + [ABC_END, MUSIC_START]
negative = tokens["negative"] + ([MUSIC_START] if cot == "off" else [ABC_START] + abc_ids + [ABC_END, MUSIC_START])
semantic, semantic_truncated = self._generate(
prefix, tokens["seed"], tokens["max_tokens"], "semantic", dtype,
negative=negative, cfg_scale=tokens["cfg_scale"], legacy_off=cot == "off",
temperature=tokens["temperature"], top_p=tokens["top_p"], top_k=tokens["top_k"],
repetition_penalty=tokens["repetition_penalty"], penalty_window=50,
min_tokens=min(200, tokens["max_tokens"]),
)
conditioning, chunks = self._acoustic_conditioning(prefix, semantic, dtype)
return conditioning, None, {
"yue2_chunks": chunks, "yue2_abc_ids": abc_ids, "yue2_frames": len(semantic),
"yue2_truncated": semantic_truncated,
}
def te(dtype_llama=None, llama_quantization_metadata=None):
class YuE2TEModel_(YuE2TEModel):
def __init__(self, device="cpu", dtype=None, model_options={}):
dtype = comfy.model_management.pick_weight_dtype(dtype_llama, dtype, device)
if llama_quantization_metadata is not None:
model_options = {**model_options, "quantization_metadata": llama_quantization_metadata}
super().__init__(device=device, dtype=dtype, model_options=model_options)
return YuE2TEModel_
+22
View File
@@ -50,12 +50,34 @@ class AudioEncoderEncode(io.ComfyNode):
return io.NodeOutput(output)
class SheetSage2AudioToABC(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SheetSage2AudioToABC",
display_name="SheetSage2 Audio to ABC",
category="model/conditioning/yue2",
description="Transcribes vocal and instrumental melodies from music into ABC notation. Connect abc output to YuE2 Generate Music node and use the matching mode.",
inputs=[
io.AudioEncoder.Input("audio_encoder"),
io.Audio.Input("audio"),
io.Combo.Input("mode", options=["melody", "full"], tooltip="full: generates melody and chords; melody: generates melody only, recommended for covers."),
],
outputs=[io.String.Output(display_name="abc", is_output_list=True)],
)
@classmethod
def execute(cls, audio_encoder, audio, mode):
return io.NodeOutput(audio_encoder.generate_abc(audio["waveform"], audio["sample_rate"], melody_only=mode == "melody"))
class AudioEncoder(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
AudioEncoderLoader,
AudioEncoderEncode,
SheetSage2AudioToABC,
]
+98
View File
@@ -0,0 +1,98 @@
import torch
from typing_extensions import override
import comfy.model_management
from comfy.text_encoders.yue2 import FRAMES_PER_SECOND
from comfy_api.latest import ComfyExtension, io
class YuE2GenerateABC(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="YuE2GenerateABC",
display_name="YuE2 Generate ABC",
category="model/conditioning/yue2",
description="Generates the ABC notation of a song from style and lyrics. Connect abc to YuE2 Generate Music node.",
inputs=[
io.Clip.Input("clip"),
io.String.Input("style", multiline=True, dynamic_prompts=True),
io.String.Input("lyrics", multiline=True, dynamic_prompts=True),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True),
io.Combo.Input("mode", options=["full", "melody"], tooltip="full: generates melody and chords; melody: generates melody only, recommended for covers."),
io.Int.Input("max_abc_tokens", default=8192, min=1, max=20000, advanced=True),
],
outputs=[io.String.Output(display_name="abc")],
)
@classmethod
def execute(cls, clip, style, lyrics, seed, mode, max_abc_tokens):
tokens = clip.tokenize(style, lyrics=lyrics, cot=mode, seed=seed, max_tokens=max_abc_tokens)
ids = clip.generate(tokens, max_length=max_abc_tokens, temperature=0.7, top_p=0.9, top_k=30, repetition_penalty=1.005, seed=seed)
return io.NodeOutput(clip.decode(ids))
class YuE2GenerateMusic(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="YuE2GenerateMusic",
display_name="YuE2 Generate Music",
category="model/conditioning/yue2",
description="Generates music tokens and acoustic conditioning from style, lyrics, and an ABC notation. Provide the generated seconds to the Empty YuE2 Latent Audio node. An empty ABC input ignores the selected mode.",
inputs=[
io.Clip.Input("clip"),
io.String.Input("style", multiline=True, dynamic_prompts=True),
io.String.Input("lyrics", multiline=True, dynamic_prompts=True),
io.String.Input("abc", default="", multiline=True, tooltip="Connect the ABC generator or supply an edited score. Leave empty to use off mode automatically."),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff, control_after_generate=True),
io.Combo.Input("mode", options=["full", "melody"], tooltip="full: generates melody and chords; melody: generates melody only, recommended for covers."),
io.Float.Input("max_duration", default=360.0, min=0.04, max=360.0, step=0.04, tooltip="Maximum duration; generation can stop earlier. The release uses a 360-second budget."),
io.Float.Input("temperature", default=1.0, min=0.0, max=5.0, step=0.05, advanced=True),
io.Float.Input("top_p", default=0.95, min=0.01, max=1.0, step=0.01, advanced=True),
io.Int.Input("top_k", default=100, min=1, max=32768, advanced=True),
io.Float.Input("repetition_penalty", default=1.2, min=0.01, max=10.0, step=0.01, advanced=True),
],
outputs=[io.Conditioning.Output(), io.Float.Output(display_name="seconds")],
)
@classmethod
def execute(cls, clip, style, lyrics, seed, mode, max_duration, temperature, top_p, top_k, repetition_penalty, abc=""):
if not abc.strip():
mode = "off"
tokens = clip.tokenize(style, lyrics=lyrics, cot=mode, seed=seed, abc=abc,
max_tokens=max(1, round(max_duration * FRAMES_PER_SECOND)),
temperature=temperature, top_p=top_p, top_k=top_k, repetition_penalty=repetition_penalty)
conditioning = clip.encode_from_tokens_scheduled(tokens)
return io.NodeOutput(conditioning, conditioning[0][1]["yue2_frames"] / FRAMES_PER_SECOND)
class EmptyYuE2LatentAudio(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="EmptyYuE2LatentAudio",
display_name="Empty YuE2 Latent Audio",
category="model/latent/yue2",
inputs=[
io.Float.Input("seconds", default=120.0, min=0.04, max=1000.0, step=0.04),
io.Int.Input("batch_size", default=1, min=1, max=4096),
],
outputs=[io.Latent.Output()],
)
@classmethod
def execute(cls, seconds, batch_size):
latent = torch.zeros((batch_size, 64, max(1, round(seconds * FRAMES_PER_SECOND))),
device=comfy.model_management.intermediate_device(), dtype=comfy.model_management.intermediate_dtype())
return io.NodeOutput({"samples": latent, "type": "audio", "downscale_ratio_temporal": 1920})
class YuE2Extension(ComfyExtension):
@override
async def get_node_list(self):
return [YuE2GenerateABC, YuE2GenerateMusic, EmptyYuE2LatentAudio]
async def comfy_entrypoint():
return YuE2Extension()
+2 -1
View File
@@ -1009,7 +1009,7 @@ class CLIPLoader:
@classmethod
def INPUT_TYPES(s):
return {"required": { "clip_name": (folder_paths.get_filename_list("text_encoders"), ),
"type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit", "ideogram4", "boogu", "krea2", "joyimage", "mage", "minimax"], ),
"type": (["stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan", "hidream", "chroma", "ace", "omnigen2", "qwen_image", "hunyuan_image", "flux2", "ovis", "longcat_image", "cogvideox", "lens", "pixeldit", "ideogram4", "boogu", "krea2", "joyimage", "mage", "minimax", "yue2"], ),
},
"optional": {
"device": (["default", "cpu"], {"advanced": True}),
@@ -2458,6 +2458,7 @@ async def init_builtin_extra_nodes():
"nodes_lt_upsampler.py",
"nodes_lt_audio.py",
"nodes_minimax_music.py",
"nodes_yue2.py",
"nodes_minimax_h3.py",
"nodes_lt.py",
"nodes_lt_keyframes.py",