LFG-3 / processing_lfg3.py
glenn2's picture
Add LFG-3 code and configuration
7e5c0ea verified
Raw
History Blame Contribute Delete
5.79 kB
from __future__ import annotations
import numpy as np
import torch
from transformers import Gemma4Processor
from transformers.feature_extraction_utils import BatchFeature
from .parakeet_projector import PARAKEET_NAME, valid_frames_for
TARGET_SR = 16000
def _load_audio(item, target_sr: int = TARGET_SR):
if isinstance(item, dict):
arr = np.asarray(item.get("array"), dtype="float32")
sr = int(item.get("sampling_rate") or target_sr)
elif isinstance(item, (str, bytes)):
import librosa
arr, sr = librosa.load(item, sr=target_sr, mono=True)
else:
arr, sr = np.asarray(item, dtype="float32"), target_sr
if arr.ndim > 1:
arr = arr.mean(axis=1)
if sr != target_sr:
import librosa
arr = librosa.resample(arr.astype("float32"), orig_sr=sr,
target_sr=target_sr)
return arr.astype("float32")
def _collect_audio(messages):
out = []
for m in messages:
content = m.get("content")
if not isinstance(content, (list, tuple)):
continue
for part in content:
if isinstance(part, dict) and part.get("type") in ("audio",
"input_audio"):
out.append(part.get("audio", part.get("url")))
return out
class LFG3Processor(Gemma4Processor):
parakeet_name = PARAKEET_NAME
def _feature_extractor(self):
if getattr(self, "_parakeet_fe", None) is None:
from transformers import AutoFeatureExtractor
self._parakeet_fe = AutoFeatureExtractor.from_pretrained(
self.parakeet_name
)
return self._parakeet_fe
def _scaffold_ids(self):
tok = self.tokenizer
ids = tuple(tok.convert_tokens_to_ids(t)
for t in ("<|audio|>", "<|audio>", "<audio|>"))
if any(i is None or i == tok.unk_token_id for i in ids):
raise ValueError(f"audio scaffold tokens missing from vocab: {ids}")
return ids
def _batch_features(self, clips):
fe = self._feature_extractor()
per_clip, lengths = [], []
for clip in clips:
out = fe(clip, sampling_rate=TARGET_SR, return_tensors="pt")
feats = out["input_features"][0]
mask = out.get("attention_mask")
n = int(mask[0].sum()) if mask is not None else feats.shape[0]
per_clip.append(feats[:n])
lengths.append(n)
width = max(lengths)
padded = torch.zeros((len(per_clip), width, per_clip[0].shape[-1]),
dtype=per_clip[0].dtype)
mask = torch.zeros((len(per_clip), width), dtype=torch.long)
for i, (feats, n) in enumerate(zip(per_clip, lengths)):
padded[i, :n] = feats
mask[i, :n] = 1
return {"input_features": padded, "encoder_attention_mask": mask}
def _splice(self, text: str, clip) -> tuple[list[int], int]:
ids = self.tokenizer(text, add_special_tokens=False)["input_ids"]
if clip is None:
return ids, 0
marker_id, boa_id, eoa_id = self._scaffold_ids()
n = valid_frames_for(len(clip), TARGET_SR)
try:
pos = ids.index(marker_id)
except ValueError as exc:
raise ValueError(
"audio supplied but no <|audio|> marker in the rendered "
"prompt; include a {'type': 'audio'} part in the message"
) from exc
return ids[:pos] + [boa_id] + [marker_id] * n + [eoa_id] + ids[pos + 1:], n
def apply_chat_template(
self,
conversations,
tokenize: bool = True,
return_dict: bool = True,
return_tensors: str = "pt",
padding: bool = False,
add_generation_prompt: bool = True,
enable_thinking: bool = False,
audio=None,
**kwargs,
):
render = super().apply_chat_template
batched = bool(conversations) and isinstance(conversations[0],
(list, tuple))
convs = list(conversations) if batched else [conversations]
texts = [render(c, tokenize=False,
add_generation_prompt=add_generation_prompt,
enable_thinking=enable_thinking, **kwargs)
for c in convs]
if not tokenize:
return texts if batched else texts[0]
rows, clips, frames = [], [], []
for conv, text in zip(convs, texts):
found = _collect_audio(conv) if audio is None else list(audio)
if len(found) > 1:
raise ValueError("one audio clip per conversation is supported; "
f"got {len(found)}")
clip = _load_audio(found[0]) if found else None
ids, n = self._splice(text, clip)
rows.append(ids)
if clip is not None:
clips.append(clip)
frames.append(n)
pad_id = self.tokenizer.pad_token_id or 0
width = max(len(r) for r in rows)
input_ids = [[pad_id] * (width - len(r)) + r for r in rows]
attn = [[0] * (width - len(r)) + [1] * len(r) for r in rows]
data = {
"input_ids": torch.tensor(input_ids, dtype=torch.long),
"attention_mask": torch.tensor(attn, dtype=torch.long),
}
if clips:
if len(clips) != len(rows):
raise ValueError("every conversation in a batch must carry "
"audio, or none of them may")
data.update(self._batch_features(clips))
data["valid_frames"] = torch.tensor(frames, dtype=torch.long)
return BatchFeature(data=data)