Huggingface_Hack / inference_lfm.py
sshtel's picture
fix prompt for short answer
942f441
Raw
History Blame Contribute Delete
12.2 kB
"""
LFM2.5-Audio-1.5B inference module — end-to-end audio Q&A.
Replaces the 3-model pipeline (Whisper ASR + Qwen Q&A + Qwen TTS) with a single
multimodal model that accepts audio/text input and produces audio+text output.
"""
from __future__ import annotations
import logging
import os
import re
import threading
import time
if os.name == "nt":
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1") # Windows typically lacks Triton for torch.compile
import torch
import numpy as np
from runtime_config import GPU_INFERENCE_LOCK
logger = logging.getLogger(__name__)
_processor = None
_model = None
_model_lock = threading.Lock()
HF_REPO = "LiquidAI/LFM2.5-Audio-1.5B"
SAMPLE_RATE = 24000 # Mimi codec native rate (confirmed in official demo + library constants)
# Matches special boundary tokens like <|text_end|>, <|audio_start|> that leak into decoded text
_SPECIAL_TOKEN_RE = re.compile(r"<\|[^|>]+\|>")
def _select_device() -> torch.device:
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
def _select_dtype() -> torch.dtype:
if not torch.cuda.is_available():
return torch.float32
cap = torch.cuda.get_device_capability()
return torch.bfloat16 if cap[0] >= 8 else torch.float16
def _move_module(module, device: torch.device, dtype: torch.dtype):
if not hasattr(module, "to"):
return module
try:
return module.to(device=device, dtype=dtype)
except TypeError:
try:
return module.to(device)
except Exception:
return module
except Exception:
return module
def _first_parameter_device(module, fallback: torch.device) -> torch.device:
try:
return next(module.parameters()).device
except Exception:
try:
return next(module.buffers()).device
except Exception:
return fallback
def _module_device(module, fallback: torch.device) -> torch.device:
try:
return next(module.parameters()).device
except Exception:
return getattr(module, "device", fallback)
def _assemble_waveform(wav_chunks: list) -> np.ndarray | None:
"""Concatenate Mimi output chunks, normalize to peak 0.9, and return float32 array."""
if not wav_chunks:
return None
try:
waveform = torch.cat(wav_chunks, dim=-1).float().numpy().squeeze()
except Exception as exc:
logger.warning("Waveform assembly failed: %s", exc)
return None
if waveform.ndim == 0 or waveform.size == 0:
return None
peak = float(np.abs(waveform).max())
logger.info("Waveform peak amplitude: %.6f (samples: %d)", peak, waveform.size)
if peak == 0.0:
logger.warning("Waveform is all zeros — model generated silence")
return None
return waveform * (0.9 / peak)
def get_model_status() -> dict:
"""Return current model load state, device, dtype, and GPU memory."""
status: dict = {
"model_loaded": _model is not None,
"repo": HF_REPO,
"device": None,
"dtype": None,
"gpu_name": None,
"gpu_memory_used_gb": None,
"gpu_memory_reserved_gb": None,
}
if _model is not None:
device = _module_device(_model, _select_device())
status["device"] = str(device)
try:
status["dtype"] = str(next(_model.parameters()).dtype).replace("torch.", "")
except Exception:
pass
if device.type == "cuda":
try:
status["gpu_name"] = torch.cuda.get_device_name(device)
status["gpu_memory_used_gb"] = round(torch.cuda.memory_allocated(device) / 1e9, 2)
status["gpu_memory_reserved_gb"] = round(torch.cuda.memory_reserved(device) / 1e9, 2)
except Exception:
pass
return status
def get_lfm_model():
"""Load LFM2.5-Audio-1.5B model. Cached after first call."""
global _processor, _model
if _model is None:
with _model_lock:
if _model is None:
from liquid_audio import LFM2AudioModel, LFM2AudioProcessor
device = _select_device()
dtype = _select_dtype()
logger.info("Loading %s on %s (%s)...", HF_REPO, device, dtype)
_processor = LFM2AudioProcessor.from_pretrained(HF_REPO, device=device).eval()
try:
_model = LFM2AudioModel.from_pretrained(
HF_REPO,
dtype=dtype if device.type == "cuda" else torch.float32,
device=device,
).eval()
except TypeError:
_model = LFM2AudioModel.from_pretrained(HF_REPO).eval()
moved_processor = _move_module(_processor, device, dtype)
if moved_processor is not None:
_processor = moved_processor
moved_model = _move_module(_model, device, dtype)
if moved_model is not None:
_model = moved_model
_model = _model.eval()
# Force lazy Mimi construction after the processor is on the target device,
# and fail early if the streaming decoder cannot run there.
mimi = _processor.mimi.eval()
if device.type == "cuda":
with torch.no_grad(), mimi.streaming(1):
mimi.decode(torch.randint(0, 2048, (1, 8, 1), device=device))
logger.info("LFM2.5-Audio loaded on %s.", _module_device(_model, device))
return _processor, _model
def warmup_lfm():
"""Run a dummy generation to warm the model after loading.
Call once at startup after get_lfm_model() to eliminate first-query latency.
"""
try:
from liquid_audio import ChatState
processor, model = get_lfm_model()
device = _module_device(model, _select_device())
dtype = next(model.parameters()).dtype
with GPU_INFERENCE_LOCK, torch.no_grad():
chat = ChatState(processor, dtype=dtype)
chat.new_turn("system")
chat.add_text("Respond with interleaved text and audio.")
chat.end_turn()
chat.new_turn("user")
chat.add_text("Hi")
chat.end_turn()
chat.new_turn("assistant")
for _ in model.generate_interleaved(
**chat, max_new_tokens=10, audio_temperature=1.0, audio_top_k=4,
):
pass # Just trigger warmup, discard output
logger.info("LFM warmup complete.")
except Exception as exc:
logger.warning("LFM warmup failed (non-fatal): %s", exc)
def answer_question_audio(
question_audio_path: str | None = None,
question_text: str | None = None,
story_context: str = "",
max_new_tokens: int = 150,
) -> tuple[str, np.ndarray | None, int]:
"""
Answer a question about the story using LFM2.5-Audio end-to-end.
Accepts either audio input (child's voice) or text input.
Returns (answer_text, audio_waveform_or_None, sample_rate).
"""
from liquid_audio import ChatState
processor, model = get_lfm_model()
device = _module_device(model, _select_device())
dtype = next(model.parameters()).dtype
with GPU_INFERENCE_LOCK, torch.no_grad():
chat = ChatState(processor, dtype=dtype)
# System prompt: format requirement + brevity constraint
chat.new_turn("system")
chat.add_text(
"Respond with interleaved text and audio. "
"Give a short, direct answer in 1-2 sentences. Do not repeat the question."
)
chat.end_turn()
# User turn — story context as text prefix, then audio or text question
chat.new_turn("user")
if story_context:
chat.add_text(
f"Story context:\n{story_context[:2000]}\n\n"
"Based only on the story above, answer briefly."
)
if question_audio_path:
import librosa
wav_np, sr = librosa.load(question_audio_path, sr=16000, mono=True)
wav = torch.from_numpy(wav_np).unsqueeze(0).to(device)
chat.add_audio(wav, sr)
elif question_text:
chat.add_text(question_text)
else:
return "Please ask a question!", None, SAMPLE_RATE
# Closing constraint so the model sees it immediately before generating
chat.add_text("Answer in 1-2 sentences only.")
chat.end_turn()
chat.new_turn("assistant")
text_out: list[torch.Tensor] = []
audio_out: list[torch.Tensor] = []
text_token_count = 0
t0 = time.perf_counter()
for t in model.generate_interleaved(
**chat,
max_new_tokens=max_new_tokens,
audio_temperature=1.0,
audio_top_k=4,
):
if t.numel() == 1:
text_token_count += 1
text_out.append(t)
elif t.numel() == 8:
audio_out.append(t)
gen_time = time.perf_counter() - t0
audio_frame_count = max(0, len(audio_out) - 1) # last frame is EOS
logger.info(
"Generated: %d text tokens, %d audio frames (%.1f sec), %.1f s wall",
text_token_count, audio_frame_count, audio_frame_count / 12.5, gen_time,
)
# Decode text — strip interleaved boundary special tokens that leak through
answer_text = ""
if text_out:
raw_text = "".join(processor.text.decode(t.detach().cpu()) for t in text_out)
answer_text = _SPECIAL_TOKEN_RE.sub("", raw_text).strip()
# Decode audio via processor.decode (drops EOS frame)
waveform = None
if len(audio_out) > 1:
audio_codes = torch.stack(audio_out[:-1], dim=1).unsqueeze(0) # (1, 8, N)
raw = processor.decode(audio_codes).cpu().float()
waveform = raw[0].numpy()
peak = float(np.abs(waveform).max())
if peak > 0:
waveform = waveform * (0.9 / peak)
logger.info("Decoded audio: %.2f sec, peak %.4f", len(waveform) / SAMPLE_RATE, peak)
return answer_text, waveform, SAMPLE_RATE
def text_to_audio_lfm(
text: str,
max_new_tokens: int = 1024,
) -> tuple[np.ndarray | None, int]:
"""Convert text to audio using LFM2.5-Audio in TTS mode.
Feeds the text as a user message with a "read aloud" system prompt,
then collects only the audio tokens from generate_interleaved.
Returns (waveform_or_None, sample_rate).
"""
from liquid_audio import ChatState
if not text.strip():
return None, SAMPLE_RATE
processor, model = get_lfm_model()
device = _module_device(model, _select_device())
dtype = next(model.parameters()).dtype
with GPU_INFERENCE_LOCK, torch.no_grad():
chat = ChatState(processor, dtype=dtype)
chat.new_turn("system")
chat.add_text("Read the following text aloud clearly and naturally.")
chat.end_turn()
chat.new_turn("user")
chat.add_text(text)
chat.end_turn()
chat.new_turn("assistant")
wav_chunks = []
audio_frame_count = 0
mimi = processor.mimi.eval()
mimi_device = _first_parameter_device(mimi, device)
with mimi.streaming(1):
for t in model.generate_interleaved(
**chat,
max_new_tokens=max_new_tokens,
audio_temperature=1.0,
audio_top_k=4,
):
if t.numel() == 8:
if (t == 2048).any():
continue
audio_frame_count += 1
try:
wav_chunk = mimi.decode(t[None, :, None].to(device=mimi_device, dtype=torch.long))[0]
wav_chunks.append(wav_chunk.cpu())
except Exception as exc:
logger.warning("TTS decode skipped frame: %s", exc)
logger.info("TTS: %d audio frames (%.1f sec)", audio_frame_count, audio_frame_count / 12.5)
return _assemble_waveform(wav_chunks), SAMPLE_RATE