Add MOSS music audio captioner

This commit is contained in:
Jaret Burkett committed 2026-09-17 10:45:37 -06:00
1 parent 080873bec9
commit a9e7752178
11 files changed
+1255 -11

No files matched your search

@@ -425,6 +425,14 @@ class AceStepCaptioner(BaseCaptioner):
) )
return item return item
@staticmethod
def _place_inputs(model, inputs):
"""Move a processor batch onto the model; the thinker also reads its
2D pad mask from the model. Called again before every decode pass."""
inputs = inputs.to(model.device).to(model.dtype)
model._pad_mask_2d = inputs.get("attention_mask", None)
return inputs
def _generate( def _generate(
self, self,
model, model,
@@ -434,17 +442,17 @@ class AceStepCaptioner(BaseCaptioner):
retries: tuple = (), retries: tuple = (),
break_loops: bool = False, break_loops: bool = False,
file_path: str = "", file_path: str = "",
max_new_tokens: int = MAX_NEW_TOKENS,
) -> str: ) -> str:
inputs = inputs.to(model.device).to(model.dtype) inputs = self._place_inputs(model, inputs)
# a generate that dies between static-cache creation and its first # a generate that dies between static-cache creation and its first
# forward leaves a half-built cache that breaks every later call # forward leaves a half-built cache that breaks every later call
stale_cache = getattr(model, "_cache", None) stale_cache = getattr(model, "_cache", None)
if stale_cache is not None and not stale_cache.is_initialized: if stale_cache is not None and not stale_cache.is_initialized:
del model._cache del model._cache
model._pad_mask_2d = inputs.get("attention_mask", None)
input_len = inputs["input_ids"].shape[1] input_len = inputs["input_ids"].shape[1]
# greedy decode loops on repeated phrases without a penalty # greedy decode loops on repeated phrases without a penalty
gen_kwargs = {"max_new_tokens": MAX_NEW_TOKENS, "repetition_penalty": repetition_penalty} gen_kwargs = {"max_new_tokens": max_new_tokens, "repetition_penalty": repetition_penalty}
if model.generation_config.cache_implementation == "static": if model.generation_config.cache_implementation == "static":
if input_len + 16 < STATIC_MAX_LENGTH: if input_len + 16 < STATIC_MAX_LENGTH:
from transformers.generation import MaxLengthCriteria, StoppingCriteriaList from transformers.generation import MaxLengthCriteria, StoppingCriteriaList
@@ -453,7 +461,7 @@ class AceStepCaptioner(BaseCaptioner):
"repetition_penalty": repetition_penalty, "repetition_penalty": repetition_penalty,
"max_length": STATIC_MAX_LENGTH, "max_length": STATIC_MAX_LENGTH,
"stopping_criteria": StoppingCriteriaList( "stopping_criteria": StoppingCriteriaList(
[MaxLengthCriteria(max_length=min(input_len + MAX_NEW_TOKENS, STATIC_MAX_LENGTH))] [MaxLengthCriteria(max_length=min(input_len + max_new_tokens, STATIC_MAX_LENGTH))]
), ),
} }
else: else:
@@ -473,7 +481,7 @@ class AceStepCaptioner(BaseCaptioner):
kwargs = dict(gen_kwargs) kwargs = dict(gen_kwargs)
if breaker is not None: if breaker is not None:
kwargs["logits_processor"] = LogitsProcessorList([breaker]) kwargs["logits_processor"] = LogitsProcessorList([breaker])
model._pad_mask_2d = inputs.get("attention_mask", None) self._place_inputs(model, inputs)
generated = model.generate(**inputs, **kwargs) generated = model.generate(**inputs, **kwargs)
return strip_eos(generated[0, input_len:]), breaker return strip_eos(generated[0, input_len:]), breaker
@@ -533,25 +541,29 @@ class AceStepCaptioner(BaseCaptioner):
file_path=file_path, file_path=file_path,
) )
def get_audio_caption(self, inputs) -> str: def get_audio_caption(self, inputs, file_path: str = "") -> str:
if self.caption_config.low_vram and self.model.device != torch.device("cpu"): if self.caption_config.low_vram and self.model.device != torch.device("cpu"):
self.model.to("cpu") self.model.to("cpu")
if self.model2.device == torch.device("cpu"): if self.model2.device == torch.device("cpu"):
self.model2.to(self.device_torch) self.model2.to(self.device_torch)
return self._generate(self.model2, self.processor2, inputs, CAPTIONER_REPETITION_PENALTY) return self._generate(self.model2, self.processor2, inputs, CAPTIONER_REPETITION_PENALTY)
def _transcribe_item(self, item: dict) -> str:
"""Raw transcriber text for a prepped item: '# Languages ... # Lyrics ...'."""
return self.get_audio_lyrics(item["lyrics_inputs"], item["file"])
def _caption_item(self, item: dict) -> str: def _caption_item(self, item: dict) -> str:
if self.caption_config.caption_format == "yue2": if self.caption_config.caption_format == "yue2":
lyrics = clean_lyrics(self.get_audio_lyrics(item["lyrics_inputs"], item["file"])) lyrics = clean_lyrics(self._transcribe_item(item))
if self.caption_config.fixed_caption is not None: if self.caption_config.fixed_caption is not None:
caption = self.caption_config.fixed_caption caption = self.caption_config.fixed_caption
else: else:
caption = self.get_audio_caption(item["caption_inputs"]) caption = self.get_audio_caption(item["caption_inputs"], item["file"])
caption = " ".join(caption.split()) caption = " ".join(caption.split())
return f"{caption}\n[Lyrics]\n{lyrics}" return f"{caption}\n[Lyrics]\n{lyrics}"
analysis = item["analysis"] analysis = item["analysis"]
lyrics = self.get_audio_lyrics(item["lyrics_inputs"], item["file"]) lyrics = self._transcribe_item(item)
language = "en" language = "en"
if "# Languages" in lyrics and "# Lyrics" in lyrics: if "# Languages" in lyrics and "# Lyrics" in lyrics:
@@ -563,7 +575,7 @@ class AceStepCaptioner(BaseCaptioner):
if self.caption_config.fixed_caption is not None: if self.caption_config.fixed_caption is not None:
caption = self.caption_config.fixed_caption caption = self.caption_config.fixed_caption
else: else:
caption = self.get_audio_caption(item["caption_inputs"]) caption = self.get_audio_caption(item["caption_inputs"], item["file"])
output = f"<CAPTION>\n{caption}\n</CAPTION>\n" output = f"<CAPTION>\n{caption}\n</CAPTION>\n"
output += f"<LYRICS>\n{lyrics}\n</LYRICS>\n" output += f"<LYRICS>\n{lyrics}\n</LYRICS>\n"
@@ -0,0 +1,169 @@
"""MOSS-Music captioner: one 8B audio-language model (OpenMOSS-Team/MOSS-Music-8B-Instruct)
does both the lyric transcription and the description that AceStepCaptioner
runs two Qwen2.5-Omni checkpoints for. Output layouts, loop breaking and the
worker pipeline are inherited from AceStepCaptioner."""
import re
from optimum.quanto import freeze
from toolkit.audio.moss_music import HF_REPO, load_moss_music
from toolkit.basic import flush
from toolkit.util.quantize import get_qtype, quantize
from .AceStepCaptioner import (
CAPTIONER_REPETITION_PENALTY,
TRANSCRIBER_REPETITION_PENALTY,
TRANSCRIBER_RETRIES,
AceStepCaptionConfig,
AceStepCaptioner,
analyze_audio,
)
# timestamps are requested on purpose: each line is anchored to the input time
# markers, so decode has to advance through the song instead of looping on a
# repeated phrase. They are stripped from the saved caption.
TRANSCRIBE_PROMPT = "Transcribe the lyrics of this song with timestamps."
# the model writes multi-section essays with timings, chords and lyric quotes
# unless framed as a generator prompt; this keeps it to one plain paragraph
CAPTION_PROMPT = (
"Write a prompt that a text-to-music generator could use to recreate this track. One paragraph, "
"under 80 words: genre, mood, instrumentation, tempo feel, production style, vocal style. "
"No timestamps, section timings, chord names, or lyric quotes."
)
LANGUAGE_PROMPT = (
"What language are the lyrics sung in? Answer with only the two-letter "
"ISO 639-1 language code, or none if there are no lyrics."
)
LANGUAGE_MAX_NEW_TOKENS = 8
# the model writes "[00:18.00 - 00:22.00] line"; a looping decode can run the
# seconds field past 59 ("[00:100.00 - ...]"), so any bracketed run of time
# characters is treated as a timestamp, plus bare mm:ss spans at a line start
_TIME = r"(?:\d{1,2}:)?\d{1,3}:\d{1,3}(?:[.:]\d{1,3})?|\d+(?:\.\d+)?\s*s"
_SPAN = rf"(?:{_TIME})(?:\s*(?:-{{1,3}}>?|–|—|~|to)\s*(?:{_TIME}))?"
_TIME_CHARS = r"[\d:.\s\-–—>~s]*\d[\d:.\s\-–—>~s]*"
TIMESTAMP_RE = re.compile(
rf"\[{_TIME_CHARS}\]|\({_TIME_CHARS}\)|<{_TIME_CHARS}>|^\s*{_SPAN}\s*[:\-–—|]?",
re.MULTILINE,
)
def strip_timestamps(text: str) -> str:
"""Remove time annotations and tidy the line layout they leave behind."""
text = TIMESTAMP_RE.sub("", text)
lines = [" ".join(line.split()) for line in text.splitlines()]
out = []
for line in lines:
if line == "" and (not out or out[-1] == ""):
continue
out.append(line)
return "\n".join(out).strip()
class MossMusicCaptionConfig(AceStepCaptionConfig):
def __init__(self, **kwargs):
kwargs.setdefault("model_name_or_path", HF_REPO)
super().__init__(**kwargs)
self.caption_prompt: str = kwargs.get("caption_prompt") or CAPTION_PROMPT
self.transcribe_prompt: str = kwargs.get("transcribe_prompt") or TRANSCRIBE_PROMPT
# MOSS transcribes the full mix better than a separated vocal stem
self.extract_vocals_before_transcribe = False
# leave the model's "[00:18.00 - 00:22.00]" line times in the saved lyrics
self.keep_timestamps: bool = bool(kwargs.get("keep_timestamps", False))
class MossMusicCaptioner(AceStepCaptioner):
caption_config_class = MossMusicCaptionConfig
caption_config: MossMusicCaptionConfig
def load_model(self):
self.print_and_status_update("Loading MOSS-Music model")
self.model, self.processor = load_moss_music(
self.caption_config.model_name_or_path,
dtype=self.torch_dtype,
device=str(self.device_torch),
)
if self.caption_config.quantize:
self.print_and_status_update("Quantizing MOSS-Music model")
quantize(self.model, weights=get_qtype(self.caption_config.qtype))
freeze(self.model)
flush()
# one model does both jobs; AceStepCaptioner's second slot stays empty
self.model2 = None
self.processor2 = None
flush()
@staticmethod
def _place_inputs(model, inputs):
inputs = inputs.to(model.device)
inputs["audio_data"] = inputs["audio_data"].to(model.dtype)
return inputs
def _make_inputs(self, audio_data, prompt_text: str):
inputs = self.processor(text=prompt_text, audios=[audio_data])
inputs["audio_input_mask"] = inputs["input_ids"] == self.processor.audio_token_id
return inputs
def _prep_file(self, file_path: str) -> dict:
audio_data = self._load_audio(file_path)
is_ace_step = self.caption_config.caption_format == "ace_step"
item = {
"file": file_path,
"analysis": analyze_audio(file_path) if is_ace_step else None,
"lyrics_inputs": self._make_inputs(audio_data, self.caption_config.transcribe_prompt),
}
if is_ace_step:
item["language_inputs"] = self._make_inputs(audio_data, LANGUAGE_PROMPT)
if self.caption_config.fixed_caption is None:
item["caption_inputs"] = self._make_inputs(
audio_data, self.caption_config.caption_prompt
)
return item
def get_audio_lyrics(self, inputs, file_path: str = "") -> str:
text = self._generate(
self.model,
self.processor,
inputs,
TRANSCRIBER_REPETITION_PENALTY,
retries=TRANSCRIBER_RETRIES,
break_loops=True,
file_path=file_path,
)
return text.strip() if self.caption_config.keep_timestamps else strip_timestamps(text)
def get_audio_language(self, inputs) -> str:
answer = self._generate(
self.model,
self.processor,
inputs,
repetition_penalty=1.0,
max_new_tokens=LANGUAGE_MAX_NEW_TOKENS,
)
match = re.search(r"[a-z]{2,3}", answer.lower())
code = match.group(0) if match else ""
# instrumentals answer none; ACE-Step's default language is en
return code if len(code) == 2 and code != "no" else "en"
def _transcribe_item(self, item: dict) -> str:
lyrics = self.get_audio_lyrics(item["lyrics_inputs"], item["file"])
if "language_inputs" not in item:
return lyrics
# the layout AceStepCaptioner._caption_item parses from the ACE transcriber
language = self.get_audio_language(item["language_inputs"])
return f"# Languages\n{language}\n# Lyrics\n{lyrics}"
def get_audio_caption(self, inputs, file_path: str = "") -> str:
# a tag-list prompt can degenerate into a repeated tag; same loop
# breaking as transcription
caption = self._generate(
self.model,
self.processor,
inputs,
CAPTIONER_REPETITION_PENALTY,
retries=TRANSCRIBER_RETRIES,
break_loops=True,
file_path=file_path,
)
return strip_timestamps(caption)
+13
View File
@@ -13,6 +13,18 @@ class AceStepCaptionerExtension(Extension):
return AceStepCaptioner return AceStepCaptioner
class MossMusicCaptionerExtension(Extension):
uid = "MossMusicCaptioner"
name = "MOSS-Music Captioner"
@classmethod
def get_process(cls):
# import your process class here so it is only loaded when needed and return it
from .MossMusicCaptioner import MossMusicCaptioner
return MossMusicCaptioner
class Qwen3VLCaptionerExtension(Extension): class Qwen3VLCaptionerExtension(Extension):
uid = "Qwen3VLCaptioner" uid = "Qwen3VLCaptioner"
name = "Qwen 3VL Captioner" name = "Qwen 3VL Captioner"
@@ -63,6 +75,7 @@ class Ideogram4CaptionerExtension(Extension):
AI_TOOLKIT_EXTENSIONS = [ AI_TOOLKIT_EXTENSIONS = [
AceStepCaptionerExtension, AceStepCaptionerExtension,
MossMusicCaptionerExtension,
Qwen3VLCaptionerExtension, Qwen3VLCaptionerExtension,
Qwen3OmniCaptionerExtension, Qwen3OmniCaptionerExtension,
Qwen25OmniCaptionerExtension, Qwen25OmniCaptionerExtension,
+21
View File
@@ -0,0 +1,21 @@
The code in this directory is adapted from MOSS-Music by the OpenMOSS Team
(https://github.com/OpenMOSS/MOSS-Music), licensed under the Apache License,
Version 2.0. Modifications for ai-toolkit: transformers 5.x compatibility
(WhisperEncoderLayer tensor return, generate input preparation), removal of the
chat-template loader, and direct Qwen2Tokenizer loading.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+35
View File
@@ -0,0 +1,35 @@
import torch
from .configuration import MossMusicConfig, MossMusicEncoderConfig
from .modeling import MossMusicModel
from .processing import MelConfig, MossMusicProcessor
HF_REPO = "OpenMOSS-Team/MOSS-Music-8B-Instruct"
def load_moss_music(
name_or_path: str = HF_REPO,
dtype: torch.dtype = torch.bfloat16,
device="cpu",
enable_time_marker: bool = True,
):
"""Model + processor from a hub id or local checkpoint dir. Time markers
(elapsed seconds interleaved into the audio tokens) are what the model was
trained with and keep long transcriptions from looping; leave them on."""
model = MossMusicModel.from_pretrained(name_or_path, dtype=dtype, device_map=device)
model.eval()
processor = MossMusicProcessor.from_pretrained(
name_or_path, enable_time_marker=enable_time_marker
)
return model, processor
__all__ = [
"HF_REPO",
"MelConfig",
"MossMusicConfig",
"MossMusicEncoderConfig",
"MossMusicModel",
"MossMusicProcessor",
"load_moss_music",
]
+135
View File
@@ -0,0 +1,135 @@
from typing import List, Optional
from dataclasses import dataclass, field
from transformers import PretrainedConfig, Qwen3Config
@dataclass
class MossMusicEncoderConfig:
d_model: int = 1280
output_dim: int = 1280
num_mel_bins: int = 128
encoder_layers: int = 32
encoder_attention_heads: int = 20
encoder_ffn_dim: int = 5120
downsample_rate: int = 8
downsample_hidden_size: int = 480
encoder_attention_window_size: int = 100
max_source_positions: int = 1500
dropout: float = 0.1
attention_dropout: float = 0.1
activation_dropout: float = 0.0
activation_function: str = "gelu"
layer_norm_eps: float = 1e-5
_attn_implementation: str = "eager"
pretrained_path: str = ""
n_window: int = 200
conv_chunksize: int = 64
deepstack_encoder_layer_indexes: List[int] = field(
default_factory=lambda: [8, 16, 24]
)
@classmethod
def from_dict(cls, config_dict):
if config_dict is None:
return cls()
allowed_keys = set(cls.__dataclass_fields__.keys())
filtered = {k: v for k, v in config_dict.items() if k in allowed_keys}
return cls(**filtered)
def to_dict(self):
return {
"d_model": self.d_model,
"output_dim": self.output_dim,
"num_mel_bins": self.num_mel_bins,
"encoder_layers": self.encoder_layers,
"encoder_attention_heads": self.encoder_attention_heads,
"encoder_ffn_dim": self.encoder_ffn_dim,
"downsample_rate": self.downsample_rate,
"downsample_hidden_size": self.downsample_hidden_size,
"encoder_attention_window_size": self.encoder_attention_window_size,
"max_source_positions": self.max_source_positions,
"dropout": self.dropout,
"attention_dropout": self.attention_dropout,
"activation_dropout": self.activation_dropout,
"activation_function": self.activation_function,
"layer_norm_eps": self.layer_norm_eps,
"_attn_implementation": self._attn_implementation,
"pretrained_path": self.pretrained_path,
"n_window": self.n_window,
"conv_chunksize": self.conv_chunksize,
"deepstack_encoder_layer_indexes": list(
self.deepstack_encoder_layer_indexes or []
),
}
class MossMusicConfig(PretrainedConfig):
model_type = "moss_music"
is_composition = True
def __init__(
self,
audio_config=None,
language_config=None,
adapter_hidden_size=8192,
ignore_index=-100,
deepstack_num_inject_layers: Optional[int] = None,
**kwargs,
):
if isinstance(audio_config, dict):
audio_config = MossMusicEncoderConfig.from_dict(audio_config)
elif audio_config is None:
audio_config = MossMusicEncoderConfig()
if isinstance(language_config, dict):
language_config = Qwen3Config(**language_config)
elif language_config is None:
language_config = Qwen3Config()
self.audio_config = audio_config
self.language_config = language_config
self.adapter_hidden_size = adapter_hidden_size
self.ignore_index = ignore_index
self.deepstack_num_inject_layers = deepstack_num_inject_layers
_propagate_keys = {
"num_hidden_layers",
"eos_token_id",
"bos_token_id",
"vocab_size",
"tie_word_embeddings",
}
for key in ("num_hidden_layers", "eos_token_id", "bos_token_id", "vocab_size"):
kwargs.setdefault(key, getattr(language_config, key, None))
kwargs.setdefault("tie_word_embeddings", False)
if hasattr(language_config, "to_dict"):
_lang_keys = set(language_config.to_dict().keys())
for key in list(kwargs.keys()):
if key in _lang_keys and key not in _propagate_keys:
kwargs.pop(key)
super().__init__(**kwargs)
def to_dict(self):
output = super().to_dict()
output["audio_config"] = (
self.audio_config.to_dict()
if hasattr(self.audio_config, "to_dict")
else self.audio_config
)
output["language_config"] = (
self.language_config.to_dict()
if hasattr(self.language_config, "to_dict")
else self.language_config
)
output["adapter_hidden_size"] = self.adapter_hidden_size
output["ignore_index"] = self.ignore_index
output["deepstack_num_inject_layers"] = self.deepstack_num_inject_layers
return output
__all__ = ["MossMusicEncoderConfig", "MossMusicConfig"]
+568
View File
@@ -0,0 +1,568 @@
from typing import Any, List, Optional, Tuple
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers.generation.utils import GenerationMixin
from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
from transformers.modeling_utils import PreTrainedModel
from transformers.models.qwen3.modeling_qwen3 import Qwen3DecoderLayer, Qwen3Model
from transformers.models.whisper.modeling_whisper import WhisperEncoderLayer
from .configuration import MossMusicConfig, MossMusicEncoderConfig
class SinusoidsPositionEmbedding(nn.Module):
def __init__(self, num_positions: int, embedding_dim: int):
super().__init__()
max_timescale = 10000.0
log_timescale_increment = math.log(max_timescale) / (embedding_dim // 2 - 1)
inv_timescales = torch.exp(
-log_timescale_increment * torch.arange(embedding_dim // 2).float()
)
self.register_buffer("inv_timescales", inv_timescales, persistent=False)
def forward(self, seq_len: int, device: torch.device):
scaled_time = torch.arange(
seq_len, device=device, dtype=self.inv_timescales.dtype
).unsqueeze(1) * self.inv_timescales.unsqueeze(0)
sin_emb = torch.sin(scaled_time)
cos_emb = torch.cos(scaled_time)
pos_emb = torch.cat([sin_emb, cos_emb], dim=1)
return pos_emb.unsqueeze(0)
class MossMusicEncoder(nn.Module):
"""Audio encoder with conv-stem downsampling and Whisper transformer layers."""
def __init__(self, config: MossMusicEncoderConfig):
super().__init__()
self.config = config
self.gelu = nn.GELU()
self.conv1 = nn.Conv2d(
1,
config.downsample_hidden_size,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
)
self.conv2 = nn.Conv2d(
config.downsample_hidden_size,
config.downsample_hidden_size,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
)
self.conv3 = nn.Conv2d(
config.downsample_hidden_size,
config.downsample_hidden_size,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
)
# 128 mel bins / 8 = 16 after 3 convs with stride=2
self.stem_proj = nn.Linear(config.downsample_hidden_size * 16, config.d_model)
self.embed_positions = SinusoidsPositionEmbedding(
config.max_source_positions, config.d_model
)
self.layers = nn.ModuleList(
[WhisperEncoderLayer(config) for _ in range(config.encoder_layers)]
)
self.layer_norm = nn.LayerNorm(config.d_model, eps=config.layer_norm_eps)
self.out_proj = (
nn.Linear(config.d_model, config.output_dim, bias=False)
if config.output_dim != config.d_model
else nn.Identity()
)
self.deepstack_encoder_layer_indexes = list(
config.deepstack_encoder_layer_indexes or []
)
self._deepstack_capture_map = {
layer_idx: capture_idx
for capture_idx, layer_idx in enumerate(self.deepstack_encoder_layer_indexes)
}
self.n_window = int(config.n_window)
self.chunk_frames = int(self.n_window * 2)
self.conv_chunksize = int(config.conv_chunksize)
@property
def dtype(self) -> torch.dtype:
return self.conv1.weight.dtype
@staticmethod
def _compute_downsampled_length(lengths: torch.Tensor) -> torch.Tensor:
def conv_out_len(L):
return (L - 1) // 2 + 1
return conv_out_len(conv_out_len(conv_out_len(lengths)))
def _encode_chunk_batch(
self,
input_features: torch.Tensor,
seq_lengths: torch.Tensor,
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
"""Encode a batch of (already padded) chunks through the conv stem and
transformer layers. Returns (last_hidden, ordered_deepstack_hidden_states).
"""
if input_features.dim() == 2:
input_features = input_features.unsqueeze(0)
downsampled_lengths = self._compute_downsampled_length(seq_lengths)
# [B, n_mels, T] -> [B, 1, n_mels, T]
x = input_features.unsqueeze(1)
x = self.gelu(self.conv1(x))
x = self.gelu(self.conv2(x))
x = self.gelu(self.conv3(x))
# [B, C, F, T] -> [B, T, C*F]
x = x.permute(0, 3, 1, 2).contiguous().flatten(2)
x = self.stem_proj(x)
max_len = int(downsampled_lengths.max().item())
if x.size(1) > max_len:
x = x[:, :max_len, :]
positions = self.embed_positions(x.shape[1], x.device)
x = x + positions.to(x.dtype)
padding_mask = (
torch.arange(x.size(1), device=x.device)[None, :]
>= downsampled_lengths[:, None]
)
attention_mask = (1.0 - (~padding_mask).to(dtype=x.dtype)) * torch.finfo(
x.dtype
).min
attention_mask = attention_mask.unsqueeze(1).unsqueeze(1)
deepstack_hidden_states: List[Optional[torch.Tensor]] = [None] * len(
self.deepstack_encoder_layer_indexes
)
for layer_idx, layer in enumerate(self.layers):
x = layer(x, attention_mask)
capture_idx = self._deepstack_capture_map.get(layer_idx)
if capture_idx is not None:
deepstack_hidden_states[capture_idx] = x
x = self.layer_norm(x)
x = self.out_proj(x)
ordered_deepstack_hidden_states = [
h for h in deepstack_hidden_states if h is not None
]
if not isinstance(self.out_proj, nn.Identity):
ordered_deepstack_hidden_states = [
self.out_proj(h) for h in ordered_deepstack_hidden_states
]
return x, ordered_deepstack_hidden_states
def forward(
self,
input_features: torch.Tensor,
feature_lens: Optional[torch.Tensor] = None,
output_deepstack_hidden_states: bool = True,
) -> BaseModelOutputWithPast:
if input_features.dim() == 3:
if feature_lens is None:
feature_lens = torch.full(
(input_features.size(0),),
input_features.size(-1),
dtype=torch.long,
device=input_features.device,
)
else:
feature_lens = feature_lens.to(
device=input_features.device, dtype=torch.long
)
valid_chunks = [
input_features[i, :, : int(feature_lens[i].item())]
for i in range(int(input_features.shape[0]))
]
input_features = torch.cat(valid_chunks, dim=1)
elif input_features.dim() != 2:
raise ValueError(
f"Expected [n_mels, T] or [B, n_mels, T], got {tuple(input_features.shape)}."
)
if feature_lens is None:
feature_lens = torch.tensor(
[int(input_features.shape[1])],
device=input_features.device,
dtype=torch.long,
)
else:
feature_lens = feature_lens.to(
device=input_features.device, dtype=torch.long
)
chunk_frames = int(self.chunk_frames)
chunk_num = torch.ceil(
feature_lens.to(torch.float32) / float(chunk_frames)
).long()
chunk_lengths = torch.full(
(int(chunk_num.sum().item()),),
chunk_frames,
dtype=torch.long,
device=feature_lens.device,
)
tail_chunk_index = F.pad(chunk_num, (1, 0), value=-1).cumsum(0)[1:]
chunk_lengths[tail_chunk_index] = feature_lens % chunk_frames
chunk_lengths[chunk_lengths == 0] = chunk_frames
chunk_list = input_features.T.split(chunk_lengths.tolist(), dim=0)
padded_feature = nn.utils.rnn.pad_sequence(
chunk_list, batch_first=True
).transpose(1, 2)
feature_lens_after_cnn = self._compute_downsampled_length(chunk_lengths)
t_down_max = (
int(feature_lens_after_cnn.max().item())
if feature_lens_after_cnn.numel() > 0
else 0
)
padded_mask_after_cnn = nn.utils.rnn.pad_sequence(
[
torch.ones(int(L.item()), dtype=torch.bool, device=padded_feature.device)
for L in feature_lens_after_cnn
],
batch_first=True,
)
if padded_mask_after_cnn.shape[1] < t_down_max:
padded_mask_after_cnn = F.pad(
padded_mask_after_cnn,
(0, t_down_max - padded_mask_after_cnn.shape[1]),
value=False,
)
num_deepstack = len(self.deepstack_encoder_layer_indexes)
padded_embeds: List[torch.Tensor] = []
deepstack_padded_embeds: List[List[torch.Tensor]] = [
[] for _ in range(num_deepstack)
]
for feat_chunk, len_chunk in zip(
padded_feature.split(self.conv_chunksize, dim=0),
chunk_lengths.split(self.conv_chunksize, dim=0),
):
out, deepstack_outs = self._encode_chunk_batch(feat_chunk, len_chunk)
if out.shape[1] < t_down_max:
out = F.pad(out, (0, 0, 0, t_down_max - out.shape[1]))
padded_embeds.append(out)
if output_deepstack_hidden_states and num_deepstack > 0:
if len(deepstack_outs) != num_deepstack:
raise RuntimeError(
"Deepstack output count does not match configured layer indexes."
)
for capture_idx, ds in enumerate(deepstack_outs):
if ds.shape[1] < t_down_max:
ds = F.pad(ds, (0, 0, 0, t_down_max - ds.shape[1]))
deepstack_padded_embeds[capture_idx].append(ds)
if padded_embeds:
padded_embed = torch.cat(padded_embeds, dim=0)
else:
padded_embed = torch.empty(
(0, t_down_max, self.config.output_dim),
device=padded_feature.device,
)
valid_tokens = padded_embed[padded_mask_after_cnn] # [N_valid, D]
last_hidden_state = valid_tokens.unsqueeze(0) # [1, N_valid, D]
deepstack_states: Optional[Tuple[torch.Tensor, ...]] = None
if output_deepstack_hidden_states and num_deepstack > 0:
collected: List[torch.Tensor] = []
for chunks_list in deepstack_padded_embeds:
if chunks_list:
ds = torch.cat(chunks_list, dim=0)
collected.append(ds[padded_mask_after_cnn].unsqueeze(0))
else:
collected.append(
torch.empty(
(1, 0, self.config.output_dim),
device=padded_feature.device,
dtype=padded_embed.dtype,
)
)
deepstack_states = tuple(collected)
return BaseModelOutputWithPast(
last_hidden_state=last_hidden_state,
hidden_states=deepstack_states,
)
class GatedMLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.gate_proj = nn.Linear(input_size, hidden_size, bias=False)
self.up_proj = nn.Linear(input_size, hidden_size, bias=False)
self.down_proj = nn.Linear(hidden_size, output_size, bias=False)
self.act_fn = nn.SiLU()
def forward(self, x):
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
class MossMusicPreTrainedModel(PreTrainedModel):
config_class = MossMusicConfig
config: MossMusicConfig
base_model_prefix = ""
supports_gradient_checkpointing = True
_no_split_modules = ["Qwen3DecoderLayer"]
_skip_keys_device_placement = ["past_key_values"]
_supports_flash_attn = True
_supports_sdpa = True
_supports_flex_attn = True
_can_compile_fullgraph = False
_supports_attention_backend = True
_can_record_outputs = {"hidden_states": Qwen3DecoderLayer}
class MossMusicModel(MossMusicPreTrainedModel, GenerationMixin):
config_class = MossMusicConfig
_tied_weights_keys: List[str] = []
def __init__(self, config: MossMusicConfig):
super().__init__(config)
self.audio_encoder = MossMusicEncoder(config.audio_config)
self.language_model = Qwen3Model(config.language_config)
self.audio_adapter = GatedMLP(
input_size=config.audio_config.output_dim,
hidden_size=config.adapter_hidden_size,
output_size=config.language_config.hidden_size,
)
deepstack_k = len(
getattr(config.audio_config, "deepstack_encoder_layer_indexes", []) or []
)
if config.deepstack_num_inject_layers is not None:
deepstack_k = min(deepstack_k, int(config.deepstack_num_inject_layers))
self.deepstack_audio_merger_list = nn.ModuleList(
[
GatedMLP(
input_size=config.audio_config.output_dim,
hidden_size=config.adapter_hidden_size,
output_size=config.language_config.hidden_size,
)
for _ in range(deepstack_k)
]
)
self.vocab_size = config.language_config.vocab_size
self.lm_head = nn.Linear(
config.language_config.hidden_size, self.vocab_size, bias=False
)
self.post_init()
def get_input_embeddings(self):
return self.language_model.get_input_embeddings()
def set_input_embeddings(self, value):
self.language_model.set_input_embeddings(value)
def get_output_embeddings(self):
return self.lm_head
def set_output_embeddings(self, new_embeddings):
self.lm_head = new_embeddings
def get_audio_features(self, input_features, feature_lens):
audio_outputs = self.audio_encoder(
input_features=input_features,
feature_lens=feature_lens,
output_deepstack_hidden_states=True,
)
deepstack = (
list(audio_outputs.hidden_states)
if audio_outputs.hidden_states is not None
else None
)
return audio_outputs.last_hidden_state, deepstack
def _apply_deepstack_to_hidden_states(
self,
hidden_states: torch.Tensor,
audio_input_mask: torch.Tensor,
deepstack_embeds: torch.Tensor,
) -> torch.Tensor:
audio_input_mask = audio_input_mask.to(hidden_states.device)
deepstack_embeds = deepstack_embeds.to(hidden_states.device, hidden_states.dtype)
flat = deepstack_embeds.reshape(-1, deepstack_embeds.shape[-1])
hs = hidden_states.clone()
hs[audio_input_mask] = hs[audio_input_mask] + flat
return hs
def _register_llm_deepstack_hooks(
self,
audio_input_mask: torch.Tensor,
deepstack_audio_embeds: List[torch.Tensor],
):
if deepstack_audio_embeds is None or len(deepstack_audio_embeds) == 0:
return []
layers = getattr(self.language_model, "layers", None)
if layers is None:
raise RuntimeError(
"Qwen3Model does not expose `.layers`; cannot register DeepStack hooks."
)
num_inject = len(deepstack_audio_embeds)
handles = []
for layer_idx, layer in enumerate(layers):
if layer_idx >= num_inject:
break
def _make_llm_hook(k: int):
def _hook(_module, _inputs, _output):
if isinstance(_output, (tuple, list)):
hs = _output[0]
new_hs = self._apply_deepstack_to_hidden_states(
hs, audio_input_mask, deepstack_audio_embeds[k]
)
return (new_hs,) + tuple(_output[1:])
else:
return self._apply_deepstack_to_hidden_states(
_output, audio_input_mask, deepstack_audio_embeds[k]
)
return _hook
handles.append(layer.register_forward_hook(_make_llm_hook(layer_idx)))
return handles
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[torch.FloatTensor]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
audio_data: Optional[torch.FloatTensor] = None,
audio_data_seqlens: Optional[torch.Tensor] = None,
audio_input_mask: Optional[torch.Tensor] = None,
**kwargs: Any,
) -> CausalLMOutputWithPast:
# generate() still passes return_dict; transformers 5 models ignore it
kwargs.pop("return_dict", None)
if inputs_embeds is None:
inputs_embeds = self.get_input_embeddings()(input_ids)
hook_handles = []
if audio_data is not None:
if audio_input_mask is None:
raise ValueError(
"audio_input_mask is required when audio_data is provided."
)
audio_embeds, deepstack = self.get_audio_features(audio_data, audio_data_seqlens)
audio_embeds = self.audio_adapter(audio_embeds)
audio_token_count = int(audio_input_mask.to(torch.int32).sum().item())
if audio_token_count != int(audio_embeds.shape[1]):
raise ValueError(
f"Audio token count mismatch: audio_input_mask has {audio_token_count} audio tokens, "
f"but audio_embeds has length {int(audio_embeds.shape[1])}."
)
mask_expanded = audio_input_mask.unsqueeze(-1).expand_as(inputs_embeds)
inputs_embeds = inputs_embeds.clone()
inputs_embeds.masked_scatter_(mask_expanded, audio_embeds)
if deepstack is not None and len(self.deepstack_audio_merger_list) > 0:
deepstack_audio_embeds = []
for i, x in enumerate(deepstack[: len(self.deepstack_audio_merger_list)]):
ds = self.deepstack_audio_merger_list[i](x)
if int(ds.shape[1]) != audio_token_count:
raise ValueError(
f"DeepStack audio seq_len mismatch at index {i}: "
f"expected {audio_token_count}, got {int(ds.shape[1])}."
)
deepstack_audio_embeds.append(ds)
try:
hook_handles = self._register_llm_deepstack_hooks(
audio_input_mask, deepstack_audio_embeds
)
except Exception:
for h in hook_handles:
h.remove()
raise
try:
outputs = self.language_model(
input_ids=None,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
**kwargs,
)
finally:
for h in hook_handles:
h.remove()
hidden_states = outputs[0]
logits = self.lm_head(hidden_states)
loss = None
if labels is not None:
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
loss_fct = nn.CrossEntropyLoss(ignore_index=self.config.ignore_index)
shift_logits = shift_logits.view(-1, self.config.language_config.vocab_size)
shift_labels = shift_labels.view(-1)
shift_labels = shift_labels.to(shift_logits.device)
loss = loss_fct(shift_logits, shift_labels)
return CausalLMOutputWithPast(
loss=loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
def prepare_inputs_for_generation(
self,
input_ids,
next_sequence_length=None,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
is_first_iteration=False,
**kwargs,
):
# audio is encoded once into the prompt embeds; re-sending it on decode
# steps would re-run the encoder against a stale audio_input_mask
if not is_first_iteration:
for key in ("audio_data", "audio_data_seqlens", "audio_input_mask"):
kwargs.pop(key, None)
return super().prepare_inputs_for_generation(
input_ids,
next_sequence_length=next_sequence_length,
past_key_values=past_key_values,
attention_mask=attention_mask,
inputs_embeds=inputs_embeds,
is_first_iteration=is_first_iteration,
**kwargs,
)
__all__ = [
"MossMusicEncoderConfig",
"MossMusicConfig",
"MossMusicModel",
]
+250
View File
@@ -0,0 +1,250 @@
import re
from dataclasses import dataclass
from typing import List, Optional, Sequence, Union
import numpy as np
import torch
from transformers import BatchEncoding, Qwen2Tokenizer
@dataclass
class MelConfig:
mel_sr: int = 16000
mel_dim: int = 128
mel_n_fft: int = 400
mel_hop_length: int = 160
mel_dtype: torch.dtype = torch.bfloat16
class MossMusicProcessor:
"""Builds MOSS-Music inputs: Whisper log-mel features plus a Qwen chat
prompt whose audio span is one placeholder token per 80ms of audio, with
the elapsed seconds written in as digit tokens every 2s (time markers)."""
_AUDIO_SPAN_RE = re.compile(r"<\|audio_bos\|>(?:<\|AUDIO\|>)+<\|audio_eos\|>")
def __init__(
self,
tokenizer,
*,
mel_config: Optional[MelConfig] = None,
enable_time_marker: bool = True,
audio_token_id: int = 151654,
audio_start_id: int = 151669,
audio_end_id: int = 151670,
):
self.tokenizer = tokenizer
self.audio_token_id = int(audio_token_id)
self.audio_start_id = int(audio_start_id)
self.audio_end_id = int(audio_end_id)
self.enable_time_marker = bool(enable_time_marker)
self.config = mel_config or MelConfig()
self._whisper_feature_extractor = None
self._digit_token_ids = {str(d): 15 + d for d in range(10)}
self.audio_tokens_per_second = 12.5
self.time_marker_every_seconds = 2
self.time_marker_every_audio_tokens = int(
self.audio_tokens_per_second * self.time_marker_every_seconds
)
self.model_input_names = [
"input_ids",
"attention_mask",
"audio_data",
"audio_data_seqlens",
]
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
tokenizer_kwargs = {}
for key in ["cache_dir", "revision", "token", "local_files_only"]:
if key in kwargs:
tokenizer_kwargs[key] = kwargs[key]
# the checkpoint's tokenizer_config names Qwen2Tokenizer; loading it
# directly skips AutoTokenizer's remote-code prompt for the auto_map
tokenizer = Qwen2Tokenizer.from_pretrained(
pretrained_model_name_or_path, **tokenizer_kwargs
)
return cls(
tokenizer,
mel_config=kwargs.pop("mel_config", None),
enable_time_marker=kwargs.pop("enable_time_marker", True),
audio_token_id=kwargs.pop("audio_token_id", 151654),
audio_start_id=kwargs.pop("audio_start_id", 151669),
audio_end_id=kwargs.pop("audio_end_id", 151670),
)
@staticmethod
def _conv3_downsample_len(raw_mel_len: int) -> int:
def conv_out_len(length: int) -> int:
return (length - 1) // 2 + 1
return conv_out_len(conv_out_len(conv_out_len(int(raw_mel_len))))
def _get_whisper_feature_extractor(self):
if self._whisper_feature_extractor is not None:
return self._whisper_feature_extractor
from transformers.models.whisper.feature_extraction_whisper import (
WhisperFeatureExtractor,
)
self._whisper_feature_extractor = WhisperFeatureExtractor(
feature_size=int(self.config.mel_dim),
sampling_rate=int(self.config.mel_sr),
hop_length=int(self.config.mel_hop_length),
n_fft=int(self.config.mel_n_fft),
)
return self._whisper_feature_extractor
def _extract_mel(self, audio: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
if isinstance(audio, torch.Tensor):
audio = audio.detach().cpu().numpy()
wav_np = np.ascontiguousarray(audio, dtype=np.float32)
if wav_np.ndim == 2:
wav_np = wav_np[0]
# the raw fbank pass has no 30s chunking, so full songs come through whole
fe = self._get_whisper_feature_extractor()
feats = fe._np_extract_fbank_features(wav_np[None, ...], device="cpu")
return torch.from_numpy(feats[0]).to(dtype=self.config.mel_dtype)
def _get_time_marker_token_ids(self, second: int) -> List[int]:
return [self._digit_token_ids[digit] for digit in str(second)]
def _build_audio_tokens_with_time_markers(self, audio_seq_len: int) -> List[int]:
total_duration_seconds = audio_seq_len / self.audio_tokens_per_second
num_full_seconds = int(total_duration_seconds)
token_ids: List[int] = []
audio_tokens_consumed = 0
for second in range(
self.time_marker_every_seconds,
num_full_seconds + 1,
self.time_marker_every_seconds,
):
marker_pos = (
second // self.time_marker_every_seconds
) * self.time_marker_every_audio_tokens
audio_segment_len = marker_pos - audio_tokens_consumed
if audio_segment_len > 0:
token_ids.extend([self.audio_token_id] * audio_segment_len)
audio_tokens_consumed += audio_segment_len
token_ids.extend(self._get_time_marker_token_ids(second))
remaining = audio_seq_len - audio_tokens_consumed
if remaining > 0:
token_ids.extend([self.audio_token_id] * remaining)
return token_ids
def _build_audio_placeholder_ids(self, num_audio_tokens: int) -> List[int]:
if self.enable_time_marker:
return self._build_audio_tokens_with_time_markers(num_audio_tokens)
return [self.audio_token_id] * num_audio_tokens
def _build_default_prompt(self, text: str, has_audio: bool) -> str:
if has_audio:
return (
"<|im_start|>system\n"
"You are a helpful assistant.<|im_end|>\n"
"<|im_start|>user\n"
"<|audio_bos|><|AUDIO|><|audio_eos|>\n"
f"{text}<|im_end|>\n"
"<|im_start|>assistant\n"
)
return (
"<|im_start|>system\n"
"You are a helpful assistant.<|im_end|>\n"
"<|im_start|>user\n"
f"{text}<|im_end|>\n"
"<|im_start|>assistant\n"
)
def _build_input_from_prompt(self, prompt: str, token_lens: List[int]) -> List[int]:
spans = list(self._AUDIO_SPAN_RE.finditer(prompt))
if len(spans) != len(token_lens):
raise ValueError(
f"Audio placeholder count mismatch: found {len(spans)} spans in text, "
f"but got {len(token_lens)} audio inputs."
)
input_ids: List[int] = []
cursor = 0
for index, match in enumerate(spans):
prefix = prompt[cursor : match.start()]
if prefix:
input_ids.extend(self.tokenizer.encode(prefix, add_special_tokens=False))
input_ids.append(self.audio_start_id)
input_ids.extend(self._build_audio_placeholder_ids(int(token_lens[index])))
input_ids.append(self.audio_end_id)
cursor = match.end()
suffix = prompt[cursor:]
if suffix:
input_ids.extend(self.tokenizer.encode(suffix, add_special_tokens=False))
return input_ids
def __call__(
self,
*,
text: Union[str, Sequence[str]],
audios: Optional[Sequence[Union[np.ndarray, torch.Tensor]]] = None,
audio: Optional[Sequence[Union[np.ndarray, torch.Tensor]]] = None,
return_tensors: str = "pt",
**kwargs,
):
if isinstance(text, (list, tuple)):
if len(text) != 1:
raise ValueError(f"Expected text batch size 1, got {len(text)}")
prompt_text = text[0]
else:
prompt_text = text
audio_list = audios if audios is not None else audio
audio_list = [] if audio_list is None else list(audio_list)
mels: List[torch.Tensor] = []
raw_lengths: List[int] = []
token_lens: List[int] = []
for one_audio in audio_list:
mel = self._extract_mel(one_audio)
raw_len = int(mel.shape[-1])
mels.append(mel)
raw_lengths.append(raw_len)
token_lens.append(self._conv3_downsample_len(raw_len))
if mels:
max_length = max(raw_lengths)
audio_batch = torch.zeros(
(len(mels), self.config.mel_dim, max_length),
dtype=self.config.mel_dtype,
)
for index, mel in enumerate(mels):
audio_batch[index, :, : mel.shape[-1]] = mel
seqlens_tensor = torch.tensor(raw_lengths, dtype=torch.long)
else:
audio_batch = None
seqlens_tensor = None
if self._AUDIO_SPAN_RE.search(prompt_text) is None:
prompt_text = self._build_default_prompt(prompt_text, has_audio=bool(audio_list))
input_ids_list = self._build_input_from_prompt(prompt_text, token_lens)
input_ids_tensor = torch.tensor([input_ids_list], dtype=torch.long)
data = {
"input_ids": input_ids_tensor,
"attention_mask": torch.ones_like(input_ids_tensor),
}
if audio_batch is not None:
data["audio_data"] = audio_batch
data["audio_data_seqlens"] = seqlens_tensor
return BatchEncoding(data=data, tensor_type=return_tensors)
def batch_decode(self, *args, **kwargs):
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
return self.tokenizer.decode(*args, **kwargs)
__all__ = ["MelConfig", "MossMusicProcessor"]
+7
View File
@@ -283,6 +283,13 @@ const CaptionSimpleJob: React.FC<Props> = ({ jobConfig, setJobConfig, gpuIDs, se
onChange={value => setJobConfig(value, 'config.process[0].caption.extract_vocals_before_transcribe')} onChange={value => setJobConfig(value, 'config.process[0].caption.extract_vocals_before_transcribe')}
/> />
)} )}
{additionalSections.includes('caption.keep_timestamps') && (
<Checkbox
label="Keep Lyric Timestamps"
checked={jobConfig.config.process[0].caption.keep_timestamps || false}
onChange={value => setJobConfig(value, 'config.process[0].caption.keep_timestamps')}
/>
)}
{additionalSections.includes('caption.thinking') && ( {additionalSections.includes('caption.thinking') && (
<Checkbox <Checkbox
label="Thinking" label="Thinking"
+34 -1
View File
@@ -1,7 +1,7 @@
import { CloudLora, GroupedSelectOption, SelectOption } from "@/types"; import { CloudLora, GroupedSelectOption, SelectOption } from "@/types";
type CaptionGroup = 'image' | 'music' | 'video' | 'image/video/sound'; type CaptionGroup = 'image' | 'music' | 'video' | 'image/video/sound';
type AdditionalSections = 'caption.model_name_or_path2' | 'caption.caption_prompt' | 'caption.max_res' | 'caption.max_new_tokens' | 'caption.fixed_caption' | 'caption.caption_format' | 'caption.extract_vocals_before_transcribe' | 'caption.thinking' | 'caption.batch_size' | 'caption.layer_offloading'; type AdditionalSections = 'caption.model_name_or_path2' | 'caption.caption_prompt' | 'caption.max_res' | 'caption.max_new_tokens' | 'caption.fixed_caption' | 'caption.caption_format' | 'caption.extract_vocals_before_transcribe' | 'caption.keep_timestamps' | 'caption.thinking' | 'caption.batch_size' | 'caption.layer_offloading';
export interface CaptionOption { export interface CaptionOption {
name: string; name: string;
@@ -84,6 +84,13 @@ Part 2: on the next line write [Lyrics] and then the complete lyrics transcribed
Transcribe only what is actually sung. Be decisive. No preamble, no explanations, no markdown - output only the tag line and the lyrics block.`; Transcribe only what is actually sung. Be decisive. No preamble, no explanations, no markdown - output only the tag line and the lyrics block.`;
// MOSS-Music description prompts. The lyric transcription prompt is fixed in the
// captioner (it asks for timestamps as loop anchors and strips them afterwards).
// Framing the description as a generator prompt is what keeps the model to one
// plain paragraph instead of a sectioned essay with timings and chord names.
const mossMusicCaptionPrompt = "Write a prompt that a text-to-music generator could use to recreate this track. One paragraph, under 80 words: genre, mood, instrumentation, tempo feel, production style, vocal style. No timestamps, section timings, chord names, or lyric quotes.";
const mossMusicTagsPrompt = "Describe this music as a single line of comma-separated style tags: genre, mood, vocal type and delivery, lead instruments, production style, tempo feel, era. Lowercase tags only, no sentences.";
// Editable ADDITIONAL INSTRUCTIONS block injected into the Ideogram system prompt. // Editable ADDITIONAL INSTRUCTIONS block injected into the Ideogram system prompt.
// Users can tweak this for dataset-specific guidance without altering the fixed // Users can tweak this for dataset-specific guidance without altering the fixed
// output contract, element/background rules, or bbox format. // output contract, element/background rules, or bbox format.
@@ -114,6 +121,32 @@ export const captionerTypes: CaptionOption[] = [
'caption.extract_vocals_before_transcribe', 'caption.extract_vocals_before_transcribe',
], ],
}, },
{
name: 'MossMusicCaptioner',
label: 'MOSS-Music',
group: 'music',
defaults: {
'config.process[0].caption.model_name_or_path': ['OpenMOSS-Team/MOSS-Music-8B-Instruct', defaultNameOrPath],
'config.process[0].caption.extensions': [extensionsAudio, defaultExtensions],
'config.process[0].caption.caption_format': ['ace_step', undefined],
'config.process[0].caption.caption_prompt': [mossMusicCaptionPrompt, undefined],
'config.process[0].caption.keep_timestamps': [false, undefined],
'config.process[0].caption.compile': [true, false],
},
name_or_path_options: [
{ value: 'OpenMOSS-Team/MOSS-Music-8B-Instruct', label: 'OpenMOSS-Team/MOSS-Music-8B-Instruct' },
],
captionPrompts: {
'Description (ACE-Step)': mossMusicCaptionPrompt,
'Style tags (YuE2)': mossMusicTagsPrompt,
},
additionalSections: [
'caption.fixed_caption',
'caption.caption_format',
'caption.keep_timestamps',
'caption.caption_prompt',
],
},
{ {
name: 'Qwen3VLCaptioner', name: 'Qwen3VLCaptioner',
label: 'Qwen3-VL', label: 'Qwen3-VL',
+1
View File
@@ -339,6 +339,7 @@ export interface CaptionProcessConfig {
fixed_caption?: string; fixed_caption?: string;
caption_format?: string; caption_format?: string;
extract_vocals_before_transcribe?: boolean; extract_vocals_before_transcribe?: boolean;
keep_timestamps?: boolean;
caption_extension?: string; caption_extension?: string;
thinking?: boolean; thinking?: boolean;
batch_size?: number; batch_size?: number;