Spaces:
Paused
Paused
| """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, | |
| } | |