Source code for mllm_shap.backends.mock.audio
"""Deterministic mock backend that hears the voiced runs of audio parts.
@domain backends
@see [[knowledge/architecture/backends]]
@tested-by [[tests/backends/test_mock_audio.py]]
"""
import hashlib
import io
import wave
import numpy as np
from numpy.typing import NDArray
from ...alignment.io import decode_audio
from ...domain.config import GenerationConfig
from ...domain.conversation import AudioPart, ConversationSnapshot, TextPart
from ...domain.generation import GenerationRecord
from ...value.generation_records import generation_record_from_text
from .._chat import message_chat_text
from .backend import MockBackend
SILENCE = 0.01
"""Largest absolute amplitude (of full scale 1) that counts as silence."""
MIN_GAP_S = 0.01
"""Shortest silence that separates two voiced runs; shorter dips (zero crossings)
stay inside one run."""
[docs]
class MockAudioBackend(MockBackend):
"""Echo text words and answer one token per voiced run of every audio part.
A run's token hashes its 16-bit samples, so silencing or cutting a run drops
exactly its token and leaves the others unchanged. 16-bit PCM WAV decodes
with the standard library; other formats need the ``audio`` extra.
"""
[docs]
async def generate(
self,
snapshot: ConversationSnapshot,
generation: GenerationConfig,
) -> GenerationRecord:
"""Return the heard words of ``snapshot``, at most ``max_new_tokens``."""
words: list[str] = []
for message in snapshot.messages():
words.extend(message_chat_text(message).split())
for part in message.content:
if not isinstance(part, TextPart):
words.extend(_run_tokens(*_waveform(part)))
return generation_record_from_text(" ".join(words[: generation.max_new_tokens]))
def _waveform(part: AudioPart) -> tuple[NDArray[np.float32], int]:
data = part.data()
try:
with wave.open(io.BytesIO(data)) as reader:
width, channels = reader.getsampwidth(), reader.getnchannels()
rate = reader.getframerate()
frames = reader.readframes(reader.getnframes())
except (wave.Error, EOFError):
return decode_audio(data)
if width != 2:
return decode_audio(data)
samples = np.frombuffer(frames, dtype="<i2").reshape(-1, channels)
return (samples.mean(axis=1) / 32768).astype(np.float32), rate
def _run_tokens(waveform: NDArray[np.float32], rate: int) -> list[str]:
loud = np.flatnonzero(np.abs(waveform) > SILENCE)
if loud.size == 0:
return []
breaks = np.flatnonzero(np.diff(loud) > max(1, int(MIN_GAP_S * rate)))
starts = np.r_[loud[0], loud[breaks + 1]]
ends = np.r_[loud[breaks], loud[-1]] + 1
tokens: list[str] = []
for start, end in zip(starts, ends, strict=True):
scaled = np.clip(np.round(waveform[start:end] * 32768), -32768, 32767)
pcm = scaled.astype("<i2").tobytes()
tokens.append(hashlib.sha256(pcm).hexdigest()[:4])
return tokens