MOSS-Transcribe-Diarize / processing_moss_transcribe_diarize.py
zylin12's picture
Upload MOSS-Transcribe-Diarize pretrained model files
0844c4a verified
Raw
History Blame Contribute Delete
11 kB
"""Processor for MOSS-Transcribe-Diarize audio-text inference."""
from __future__ import annotations
from typing import Optional, Union
import numpy as np
import torch
from transformers.feature_extraction_utils import BatchFeature
from transformers.processing_utils import ProcessingKwargs, ProcessorMixin, Unpack
AUDIO_PAD_TOKEN = "<|audio_pad|>"
AUDIO_START_TOKEN = "<|audio_start|>"
AUDIO_END_TOKEN = "<|audio_end|>"
WHISPER_ENCODER_STRIDE = 2
class MossTranscribeDiarizeProcessorKwargs(ProcessingKwargs, total=False):
_defaults = {
"common_kwargs": {
"return_tensors": "pt",
},
"audio_kwargs": {},
}
def _audio_to_numpy(audio: Union[np.ndarray, torch.Tensor]) -> np.ndarray:
if torch.is_tensor(audio):
audio = audio.detach().cpu().numpy()
audio = np.asarray(audio, dtype=np.float32)
if audio.ndim > 1:
audio = np.squeeze(audio)
if audio.ndim == 0:
audio = audio.reshape(1)
if audio.ndim != 1:
raise ValueError(f"Expected mono audio with shape (num_samples,), got shape {audio.shape}.")
if audio.shape[0] == 0:
raise ValueError("Audio must contain at least one sample.")
return audio
def _pad_or_trim_audio(audio: np.ndarray, length: int) -> np.ndarray:
if audio.shape[0] > length:
audio = audio[:length]
elif audio.shape[0] < length:
audio = np.pad(audio, (0, length - audio.shape[0]))
return audio.astype(np.float32, copy=False)
def _compute_audio_token_length(num_samples: int, feature_extractor, audio_merge_size: int) -> int:
stride = int(feature_extractor.hop_length) * WHISPER_ENCODER_STRIDE * int(audio_merge_size)
return (int(num_samples) - 1) // stride + 1
def _chunk_audio(
feature_extractor,
audio: Union[np.ndarray, torch.Tensor],
audio_merge_size: int,
) -> tuple[np.ndarray, list[int]]:
audio = _audio_to_numpy(audio)
n_samples = int(feature_extractor.n_samples)
chunks, token_lengths = [], []
for start in range(0, audio.shape[0], n_samples):
chunk = audio[start : start + n_samples]
token_lengths.append(_compute_audio_token_length(chunk.shape[0], feature_extractor, audio_merge_size))
chunks.append(_pad_or_trim_audio(chunk, n_samples))
return np.stack(chunks), token_lengths
def _audios_to_input_features(
feature_extractor,
audios: list[Union[np.ndarray, torch.Tensor]],
*,
audio_merge_size: int,
feature_extractor_kwargs: Optional[dict] = None,
) -> tuple[torch.Tensor, torch.LongTensor, torch.LongTensor]:
feature_batches, feature_lengths, chunk_mapping = [], [], []
feature_extractor_kwargs = dict(feature_extractor_kwargs or {})
feature_extractor_kwargs.update(
{
"sampling_rate": int(feature_extractor.sampling_rate),
"padding": "max_length",
"return_tensors": "pt",
}
)
for audio_idx, audio in enumerate(audios):
chunks, token_lengths = _chunk_audio(feature_extractor, audio, audio_merge_size)
features = feature_extractor(
list(chunks),
**feature_extractor_kwargs,
)["input_features"]
feature_batches.append(features)
feature_lengths.extend(token_lengths)
chunk_mapping.extend([audio_idx] * len(token_lengths))
if feature_batches:
input_features = torch.cat(feature_batches, dim=0)
else:
input_features = torch.empty(
(0, int(feature_extractor.feature_size), int(feature_extractor.nb_max_frames)),
)
length_device = input_features.device
return (
input_features,
torch.tensor(feature_lengths, dtype=torch.long, device=length_device),
torch.tensor(chunk_mapping, dtype=torch.long, device=length_device),
)
class MossTranscribeDiarizeProcessor(ProcessorMixin):
"""Build MOSS-Transcribe-Diarize model inputs from text prompts and raw waveforms.
The model consumes log-mel ``input_features``. This processor owns the raw
waveform preprocessing, audio placeholder expansion, and optional numeric
time anchors inside the audio span.
"""
attributes = ["feature_extractor", "tokenizer"]
feature_extractor_class = "AutoFeatureExtractor"
tokenizer_class = "AutoTokenizer"
model_input_names = [
"input_ids",
"attention_mask",
"input_features",
"audio_feature_lengths",
"audio_chunk_mapping",
]
def __init__(
self,
feature_extractor=None,
tokenizer=None,
audio_tokens_per_second: float = 12.5,
audio_merge_size: int = 4,
time_marker_every_seconds: int = 2,
enable_time_marker: bool = True,
chat_template: Optional[str] = None,
):
if feature_extractor is None:
raise ValueError("MossTranscribeDiarizeProcessor requires a feature_extractor.")
if tokenizer is None:
raise ValueError("MossTranscribeDiarizeProcessor requires a tokenizer.")
super().__init__(feature_extractor, tokenizer, chat_template=chat_template)
self.audio_tokens_per_second = audio_tokens_per_second
self.audio_merge_size = int(audio_merge_size)
self.time_marker_every_seconds = time_marker_every_seconds
self.enable_time_marker = enable_time_marker
self.audio_token = AUDIO_PAD_TOKEN if not hasattr(tokenizer, "audio_token") else tokenizer.audio_token
self.audio_start_token = (
AUDIO_START_TOKEN if not hasattr(tokenizer, "audio_start_token") else tokenizer.audio_start_token
)
self.audio_end_token = AUDIO_END_TOKEN if not hasattr(tokenizer, "audio_end_token") else tokenizer.audio_end_token
resolved_audio_token_id = tokenizer.convert_tokens_to_ids(self.audio_token)
if resolved_audio_token_id is None:
raise ValueError(f"Tokenizer is missing required audio placeholder token {self.audio_token!r}.")
self.audio_token_id = int(resolved_audio_token_id)
self.digit_token_ids = self._get_digit_token_ids()
def _get_digit_token_ids(self) -> dict[str, int]:
digit_token_ids = {}
for digit in "0123456789":
ids = self.tokenizer.encode(digit, add_special_tokens=False)
if len(ids) != 1:
raise ValueError(f"Digit {digit!r} is not a single token: {ids}")
digit_token_ids[digit] = int(ids[0])
return digit_token_ids
def _audio_span_ids(self, audio_seq_len: int) -> list[int]:
audio_seq_len = int(audio_seq_len)
if not self.enable_time_marker or audio_seq_len <= 0 or self.time_marker_every_seconds <= 0:
return [self.audio_token_id] * max(audio_seq_len, 0)
tokens_per_marker = int(self.audio_tokens_per_second * self.time_marker_every_seconds)
if tokens_per_marker <= 0:
return [self.audio_token_id] * audio_seq_len
duration = audio_seq_len / float(self.audio_tokens_per_second)
output, consumed = [], 0
for sec in range(self.time_marker_every_seconds, int(duration) + 1, self.time_marker_every_seconds):
pos = (sec // self.time_marker_every_seconds) * tokens_per_marker
segment_len = pos - consumed
if segment_len > 0:
output.extend([self.audio_token_id] * segment_len)
consumed += segment_len
marker_ids = [self.digit_token_ids[digit] for digit in str(sec)]
output.extend(marker_ids)
remainder = audio_seq_len - consumed
if remainder > 0:
output.extend([self.audio_token_id] * remainder)
return output
def _expand_audio_token(self, text: str, num_audio_tokens: int, max_length: int) -> list[int]:
audio_ids = self._audio_span_ids(num_audio_tokens)
audio_token_count = text.count(self.audio_token)
if audio_token_count != 1:
raise ValueError(
f"Expected exactly one {self.audio_token!r} token per text sample, got {audio_token_count}."
)
before_audio, after_audio = text.split(self.audio_token, maxsplit=1)
before_ids = self.tokenizer.encode(before_audio, add_special_tokens=False)
after_ids = self.tokenizer.encode(after_audio, add_special_tokens=False)
input_ids = before_ids + audio_ids + after_ids
if len(input_ids) > max_length:
raise ValueError(f"Prompt/audio sequence exceeds max_length={max_length}")
return input_ids
def __call__(
self,
text: Union[str, list[str]],
audio,
*,
max_length: int = 131072,
**kwargs: Unpack[MossTranscribeDiarizeProcessorKwargs],
) -> BatchFeature:
return_tensors = kwargs.pop("return_tensors", "pt")
output_kwargs = self._merge_kwargs(
MossTranscribeDiarizeProcessorKwargs,
tokenizer_init_kwargs=self.tokenizer.init_kwargs,
**kwargs,
)
if isinstance(text, str):
texts = [text]
else:
texts = list(text)
audios = audio if isinstance(audio, list) else [audio]
if len(texts) != len(audios):
raise ValueError(f"Expected one audio per text prompt, got {len(audios)} audios and {len(texts)} prompts.")
input_features, audio_feature_lengths, audio_chunk_mapping = _audios_to_input_features(
self.feature_extractor,
audios,
audio_merge_size=self.audio_merge_size,
feature_extractor_kwargs=output_kwargs["audio_kwargs"],
)
audio_token_counts = torch.zeros(len(audios), dtype=torch.long, device=audio_feature_lengths.device)
audio_token_counts.scatter_add_(0, audio_chunk_mapping, audio_feature_lengths)
encoded = [
self._expand_audio_token(prompt, int(num_audio_tokens.item()), max_length)
for prompt, num_audio_tokens in zip(texts, audio_token_counts)
]
max_seq_len = max(len(ids) for ids in encoded)
pad_token_id = self.tokenizer.pad_token_id
if pad_token_id is None:
pad_token_id = self.tokenizer.eos_token_id or 0
input_ids, attention_mask = [], []
for ids in encoded:
pad_len = max_seq_len - len(ids)
input_ids.append(ids + [pad_token_id] * pad_len)
attention_mask.append([1] * len(ids) + [0] * pad_len)
target_device = input_features.device
data = {
"input_ids": torch.tensor(input_ids, dtype=torch.long, device=target_device),
"attention_mask": torch.tensor(attention_mask, dtype=torch.long, device=target_device),
"input_features": input_features,
"audio_feature_lengths": audio_feature_lengths,
"audio_chunk_mapping": audio_chunk_mapping,
}
return BatchFeature(data=data, tensor_type=return_tensors)
__all__ = ["MossTranscribeDiarizeProcessor"]