Spaces:
Paused
Paused
File size: 5,029 Bytes
397b4ac 994d3d9 397b4ac 994d3d9 397b4ac 994d3d9 397b4ac b714046 397b4ac b714046 60d60e5 397b4ac 994d3d9 397b4ac 4798cd4 397b4ac 994d3d9 397b4ac 994d3d9 397b4ac 994d3d9 397b4ac 994d3d9 397b4ac | 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 | """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,
}
|