Source code for mllm_shap.backends.transformers.audio
"""Model inputs for audio language models through a Hugging Face processor.
@domain backends
@see [[knowledge/architecture/backends]]
@tested-by [[tests/backends/test_transformers_audio.py]]
"""
from typing import Any
import numpy as np
from numpy.typing import NDArray
from ...alignment.io import decode_audio
from ...domain.conversation import AudioPart, ConversationSnapshot, Message
from .._chat import chat_role, message_chat_text
from .chat import render_prompt
[docs]
def processor_inputs(processor: Any, snapshot: ConversationSnapshot) -> Any:
"""Encode ``snapshot`` with its audio for ``processor``.
A processor whose chat template mentions audio gets structured audio and text
parts; otherwise each audio part becomes ``processor.audio_token`` in the
rendered prompt and the waveforms go to ``processor(text=..., audio=...)``.
Audio parts come before the message text, and an audio part left empty by
masking is dropped.
"""
rate = _sampling_rate(processor)
audio = {
id(message): [w for w in _waveforms(message, rate) if w.size]
for message in snapshot.messages()
}
if _template_has_audio(processor):
conversation = [
{"role": chat_role(m.role), "content": _parts(m, audio[id(m)])}
for m in snapshot.messages()
]
return processor.apply_chat_template(
conversation,
tokenize=True,
return_dict=True,
add_generation_prompt=True,
return_tensors="pt",
)
messages = [
{
"role": chat_role(m.role),
"content": processor.audio_token * len(audio[id(m)]) + message_chat_text(m),
}
for m in snapshot.messages()
]
waveforms = [w for m in snapshot.messages() for w in audio[id(m)]]
return processor(
text=render_prompt(processor.tokenizer, messages),
audio=waveforms or None,
return_tensors="pt",
)
def _parts(message: Message, waveforms: list[NDArray[np.float32]]) -> list[Any]:
parts: list[Any] = [{"type": "audio", "audio": w} for w in waveforms]
text = message_chat_text(message)
if text:
parts.append({"type": "text", "text": text})
return parts
def _waveforms(message: Message, rate: int) -> list[NDArray[np.float32]]:
return [
decode_audio(part.data(), rate)[0]
for part in message.content
if isinstance(part, AudioPart)
]
def _sampling_rate(processor: Any) -> int:
# Processors name their audio component differently (Granite: audio_processor).
extractor: Any = getattr(processor, "feature_extractor", None) or getattr(
processor, "audio_processor", None
)
return int(extractor.sampling_rate)
def _template_has_audio(processor: Any) -> bool:
templates = (
getattr(processor, "chat_template", None),
getattr(processor.tokenizer, "chat_template", None),
)
return any(isinstance(t, str) and "audio" in t for t in templates)