Spaces:
Paused
Paused
Merge pull request #8 from MomsVoiceAI/fix/answer-audio
Browse filesfix: Answer audio - use Mimi streaming decoder + flash-attn
- app.py +15 -4
- inference_lfm.py +52 -4
app.py
CHANGED
|
@@ -420,10 +420,21 @@ print("[MomsVoice] Creating stock voice profile (vivian)...")
|
|
| 420 |
_ensure_stock_voice_profile()
|
| 421 |
print("[MomsVoice] Stock voice profile ready.")
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
|
| 429 |
# Gradio Application Core setup
|
|
|
|
| 420 |
_ensure_stock_voice_profile()
|
| 421 |
print("[MomsVoice] Stock voice profile ready.")
|
| 422 |
|
| 423 |
+
# Load LFM Q&A model asynchronously — users typically start with Play tab,
|
| 424 |
+
# so LFM can load in the background while they browse stories.
|
| 425 |
+
import threading
|
| 426 |
+
|
| 427 |
+
def _load_lfm_background():
|
| 428 |
+
from inference_lfm import get_lfm_model, warmup_lfm
|
| 429 |
+
print("[MomsVoice] (background) Loading LFM2.5-Audio-1.5B Q&A model...")
|
| 430 |
+
get_lfm_model()
|
| 431 |
+
print("[MomsVoice] (background) LFM loaded. Running warmup generation...")
|
| 432 |
+
warmup_lfm()
|
| 433 |
+
print("[MomsVoice] (background) LFM Q&A model ready (compiled + warmed up).")
|
| 434 |
+
|
| 435 |
+
_lfm_thread = threading.Thread(target=_load_lfm_background, daemon=True)
|
| 436 |
+
_lfm_thread.start()
|
| 437 |
+
print("[MomsVoice] LFM loading in background thread. App starting now.")
|
| 438 |
|
| 439 |
|
| 440 |
# Gradio Application Core setup
|
inference_lfm.py
CHANGED
|
@@ -70,6 +70,16 @@ def _module_device(module, fallback: torch.device) -> torch.device:
|
|
| 70 |
return getattr(module, "device", fallback)
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
def get_lfm_model():
|
| 74 |
"""Load LFM2.5-Audio-1.5B model. Cached after first call."""
|
| 75 |
global _processor, _model
|
|
@@ -98,21 +108,59 @@ def get_lfm_model():
|
|
| 98 |
if moved_model is not None:
|
| 99 |
_model = moved_model
|
| 100 |
_model = _model.eval()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# Force lazy Mimi construction after the processor is on the target device,
|
| 102 |
# and fail early if the streaming decoder cannot run there.
|
| 103 |
mimi = _processor.mimi.eval()
|
| 104 |
-
|
| 105 |
-
with torch.no_grad(), mimi.streaming(1):
|
| 106 |
-
mimi.decode(torch.randint(0, 2048, (1, 8, 1), device=device))
|
| 107 |
logger.info("LFM2.5-Audio loaded on %s.", _module_device(_model, device))
|
| 108 |
return _processor, _model
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
def answer_question_audio(
|
| 112 |
question_audio_path: str | None = None,
|
| 113 |
question_text: str | None = None,
|
| 114 |
story_context: str = "",
|
| 115 |
-
max_new_tokens: int =
|
| 116 |
) -> tuple[str, np.ndarray | None, int]:
|
| 117 |
"""
|
| 118 |
Answer a question about the story using LFM2.5-Audio end-to-end.
|
|
|
|
| 70 |
return getattr(module, "device", fallback)
|
| 71 |
|
| 72 |
|
| 73 |
+
def _reset_mimi_streaming_state(mimi) -> None:
|
| 74 |
+
"""Best-effort reset of Mimi's internal streaming buffers."""
|
| 75 |
+
for attr in ("_streaming_state", "_states"):
|
| 76 |
+
if hasattr(mimi, attr):
|
| 77 |
+
try:
|
| 78 |
+
setattr(mimi, attr, None)
|
| 79 |
+
except Exception:
|
| 80 |
+
pass
|
| 81 |
+
|
| 82 |
+
|
| 83 |
def get_lfm_model():
|
| 84 |
"""Load LFM2.5-Audio-1.5B model. Cached after first call."""
|
| 85 |
global _processor, _model
|
|
|
|
| 108 |
if moved_model is not None:
|
| 109 |
_model = moved_model
|
| 110 |
_model = _model.eval()
|
| 111 |
+
# torch.compile for inference speedup on Linux/CUDA (skip on Windows)
|
| 112 |
+
if device.type == "cuda" and os.name != "nt":
|
| 113 |
+
try:
|
| 114 |
+
_model = torch.compile(_model, mode="reduce-overhead", dynamic=True)
|
| 115 |
+
logger.info("torch.compile applied to LFM model (reduce-overhead, dynamic).")
|
| 116 |
+
except Exception as exc:
|
| 117 |
+
logger.warning("torch.compile failed, using eager mode: %s", exc)
|
| 118 |
# Force lazy Mimi construction after the processor is on the target device,
|
| 119 |
# and fail early if the streaming decoder cannot run there.
|
| 120 |
mimi = _processor.mimi.eval()
|
| 121 |
+
_reset_mimi_streaming_state(mimi)
|
|
|
|
|
|
|
| 122 |
logger.info("LFM2.5-Audio loaded on %s.", _module_device(_model, device))
|
| 123 |
return _processor, _model
|
| 124 |
|
| 125 |
|
| 126 |
+
def warmup_lfm():
|
| 127 |
+
"""Run a dummy generation to trigger torch.compile and warm CUDA graphs.
|
| 128 |
+
Call once at startup after get_lfm_model() to eliminate first-query latency.
|
| 129 |
+
"""
|
| 130 |
+
try:
|
| 131 |
+
processor, model = get_lfm_model()
|
| 132 |
+
from liquid_audio import ChatState
|
| 133 |
+
device = _module_device(model, _select_device())
|
| 134 |
+
dtype = next(model.parameters()).dtype
|
| 135 |
+
mimi = processor.mimi.eval()
|
| 136 |
+
|
| 137 |
+
_reset_mimi_streaming_state(mimi)
|
| 138 |
+
with GPU_INFERENCE_LOCK, torch.no_grad():
|
| 139 |
+
chat = ChatState(processor, dtype=dtype)
|
| 140 |
+
chat.new_turn("system")
|
| 141 |
+
chat.add_text("You are a helpful assistant.")
|
| 142 |
+
chat.end_turn()
|
| 143 |
+
chat.new_turn("user")
|
| 144 |
+
chat.add_text("Hi")
|
| 145 |
+
chat.end_turn()
|
| 146 |
+
chat.new_turn("assistant")
|
| 147 |
+
|
| 148 |
+
with mimi.streaming(1):
|
| 149 |
+
for t in model.generate_interleaved(
|
| 150 |
+
**chat, max_new_tokens=10, audio_temperature=1.0, audio_top_k=4,
|
| 151 |
+
):
|
| 152 |
+
pass # Just trigger compilation, discard output
|
| 153 |
+
_reset_mimi_streaming_state(mimi)
|
| 154 |
+
logger.info("LFM warmup complete — torch.compile graphs cached.")
|
| 155 |
+
except Exception as exc:
|
| 156 |
+
logger.warning("LFM warmup failed (non-fatal): %s", exc)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
def answer_question_audio(
|
| 160 |
question_audio_path: str | None = None,
|
| 161 |
question_text: str | None = None,
|
| 162 |
story_context: str = "",
|
| 163 |
+
max_new_tokens: int = 100,
|
| 164 |
) -> tuple[str, np.ndarray | None, int]:
|
| 165 |
"""
|
| 166 |
Answer a question about the story using LFM2.5-Audio end-to-end.
|