Huggingface_Hack / qa_flow.py
sshtel's picture
perf: improve LFM2.5-Audio inference quality and reliability
4798cd4
Raw
History Blame Contribute Delete
5.03 kB
"""Server-side Q&A flow helpers for the Gradio Ask interaction."""
from __future__ import annotations
import logging
import os
import threading
import time
import uuid
from pathlib import Path
from typing import Callable, Iterable, Protocol
logger = logging.getLogger(__name__)
DEFAULT_SAMPLE_RATE = 24000
_RECENT_AUDIO_LOCK = threading.Lock()
_RECENT_AUDIO_SUBMISSIONS: dict[str, float] = {}
_DUPLICATE_WINDOW_SEC = 30.0
_GENERATED_QA_MAX_FILES = int(os.environ.get("MOMSVOICE_QA_AUDIO_MAX_FILES", "30"))
class AudioWriter(Protocol):
def __call__(self, path: Path, waveform, sample_rate: int) -> None:
...
def _normalize_audio_path(audio_path: str | Path | None) -> str:
if not audio_path:
return ""
try:
path = Path(audio_path).resolve()
stat = path.stat()
return f"{path}:{stat.st_size}:{stat.st_mtime_ns}"
except OSError:
return str(audio_path)
def claim_audio_submission(audio_path: str | Path | None) -> bool:
"""Return False when the same recorded audio was already claimed recently."""
normalized = _normalize_audio_path(audio_path)
if not normalized:
return True
now = time.monotonic()
cutoff = now - _DUPLICATE_WINDOW_SEC
with _RECENT_AUDIO_LOCK:
stale = [
key for key, seen_at in _RECENT_AUDIO_SUBMISSIONS.items()
if seen_at < cutoff
]
for key in stale:
_RECENT_AUDIO_SUBMISSIONS.pop(key, None)
if normalized in _RECENT_AUDIO_SUBMISSIONS:
return False
_RECENT_AUDIO_SUBMISSIONS[normalized] = now
return True
def release_audio_submission(audio_path: str | Path | None) -> None:
"""Allow a recording to be submitted again, used after failed generation."""
normalized = _normalize_audio_path(audio_path)
if not normalized:
return
with _RECENT_AUDIO_LOCK:
_RECENT_AUDIO_SUBMISSIONS.pop(normalized, None)
def _default_audio_writer(path: Path, waveform, sample_rate: int) -> None:
import soundfile as sf
import numpy as np
wav = np.asarray(waveform, dtype=np.float32)
# Clip to [-1, 1] before writing as PCM_16 for universal compatibility
wav = np.clip(wav, -1.0, 1.0)
sf.write(str(path), wav, sample_rate, subtype='PCM_16')
def _prune_generated_answers(output_dir: Path) -> None:
try:
files = sorted(
output_dir.glob("qa_answer_*.wav"),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
except OSError:
return
for stale in files[_GENERATED_QA_MAX_FILES:]:
try:
stale.unlink()
except OSError:
continue
def build_qa_response(
*,
question_text: str | None,
question_audio_path: str | Path | None,
paragraphs: Iterable[str] | None,
output_dir: Path,
answer_fn: Callable[..., tuple[str, object | None, int]],
audio_writer: AudioWriter = _default_audio_writer,
max_new_tokens: int = 512,
) -> dict[str, object]:
"""Run Q&A and return display-ready data without importing Gradio."""
q_txt = (question_text or "").strip()
audio_path = str(question_audio_path) if question_audio_path else None
has_audio = bool(audio_path)
if not q_txt and not has_audio:
return {
"ok": False,
"answer_text": "Please type a question or record one with the microphone.",
"display_question": "",
"audio_path": None,
"error": None,
}
story_context = "\n\n".join(paragraphs or [])
error = None
try:
answer_text, waveform, sr = answer_fn(
question_audio_path=audio_path if has_audio else None,
question_text=q_txt if q_txt and not has_audio else None,
story_context=story_context,
max_new_tokens=max_new_tokens,
)
if not answer_text:
answer_text = "Hmm, I'm not sure about that! Let's keep listening to find out."
except Exception as exc:
logger.exception("LFM Q&A failed: %s", exc)
error = type(exc).__name__
answer_text = (
"Oops, I couldn't think of an answer right now. Let's keep reading! "
f"({type(exc).__name__})"
)
waveform = None
sr = DEFAULT_SAMPLE_RATE
answer_audio_path = None
if waveform is not None:
output_dir.mkdir(parents=True, exist_ok=True)
answer_audio_path = output_dir / f"qa_answer_{uuid.uuid4().hex[:8]}.wav"
try:
audio_writer(answer_audio_path, waveform, sr)
_prune_generated_answers(output_dir)
except Exception as exc:
logger.exception("Failed to write answer audio: %s", exc)
error = error or type(exc).__name__
answer_audio_path = None
return {
"ok": True,
"answer_text": answer_text,
"display_question": q_txt or "(audio question)",
"audio_path": answer_audio_path,
"error": error,
}