Spaces:
Paused
Paused
File size: 12,180 Bytes
c5bd25e 373219a c5bd25e d397f1a fbb26dd 994d3d9 4798cd4 d397f1a 5bb5f05 d397f1a c5bd25e 397b4ac c5bd25e 994d3d9 c5bd25e d397f1a c5bd25e fbb26dd c5bd25e 994d3d9 5bb5f05 994d3d9 4798cd4 871cd89 4798cd4 871cd89 4798cd4 871cd89 c5bd25e 994d3d9 5bb5f05 994d3d9 5bb5f05 994d3d9 5bb5f05 4798cd4 994d3d9 c5bd25e 871cd89 4798cd4 871cd89 4798cd4 871cd89 4798cd4 871cd89 4798cd4 871cd89 c5bd25e 942f441 c5bd25e 5bb5f05 c5bd25e 5bb5f05 942f441 5bb5f05 942f441 5bb5f05 4798cd4 5bb5f05 4798cd4 942f441 4798cd4 5bb5f05 942f441 5bb5f05 4798cd4 fbb26dd 4798cd4 fbb26dd 4798cd4 5bb5f05 4798cd4 5bb5f05 4798cd4 5bb5f05 4798cd4 5bb5f05 4798cd4 5bb5f05 4798cd4 c5bd25e 4798cd4 c5bd25e 4798cd4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | """
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
|