Source code for mllm_shap.masking.audio
"""Render audio players: cut or silence absent samples, hide alignment transcripts.
@domain masking
@see [[knowledge/architecture/masking]]
@tested-by [[tests/alignment/test_sgpa_players.py]]
"""
from collections.abc import Sequence
import numpy as np
from ..alignment.io import decode_audio, encode_wav
from ..domain.conversation import (
AudioPart,
ConversationSnapshot,
Message,
TextPart,
Turn,
)
from ..domain.players import PlayerSpan
[docs]
def render_audio(
snapshot: ConversationSnapshot,
spans: Sequence[PlayerSpan],
present: Sequence[bool],
cut: bool,
) -> ConversationSnapshot:
"""Remove (``cut``) or silence the samples of absent players with ``samples``.
Silencing keeps the audio's length and timing; cutting joins what is left.
Spans without ``samples`` and messages without absent audio are untouched.
"""
absent: dict[int, list[tuple[int, int]]] = {}
for span, keep in zip(spans, present, strict=True):
if span.samples is not None and not keep:
absent.setdefault(span.message_index, []).append(span.samples)
if not absent:
return snapshot
turns: list[Turn] = []
index = 0
for turn in snapshot.turns:
messages: list[Message] = []
for message in turn.messages:
ranges = absent.get(index)
messages.append(message if ranges is None else _edit(message, ranges, cut))
index += 1
turns.append(Turn(messages=tuple(messages)))
return ConversationSnapshot.from_turns(tuple(turns))
[docs]
def hide_text(
snapshot: ConversationSnapshot, message_indices: Sequence[int]
) -> ConversationSnapshot:
"""Drop the text parts of the messages at ``message_indices`` (turns flattened)."""
if not message_indices:
return snapshot
hidden = set(message_indices)
turns: list[Turn] = []
index = 0
for turn in snapshot.turns:
messages: list[Message] = []
for message in turn.messages:
if index in hidden:
parts = tuple(p for p in message.content if not isinstance(p, TextPart))
message = Message(
role=message.role, content=parts, speaker=message.speaker
)
messages.append(message)
index += 1
turns.append(Turn(messages=tuple(messages)))
return ConversationSnapshot.from_turns(tuple(turns))
def _edit(message: Message, ranges: list[tuple[int, int]], cut: bool) -> Message:
part = next(p for p in message.content if isinstance(p, AudioPart))
waveform, rate = decode_audio(part.data())
keep = np.ones(len(waveform), dtype=bool)
for start, end in ranges:
keep[start:end] = False
edited = waveform[keep] if cut else np.where(keep, waveform, 0.0)
return message.with_audio(AudioPart(encode_wav(edited, rate), rate))