Spaces:
Running on Zero
Running on Zero
File size: 15,057 Bytes
a642146 7814c84 a642146 7814c84 530e49b a642146 530e49b a642146 530e49b a642146 530e49b a642146 530e49b 7814c84 530e49b 7814c84 530e49b 7814c84 530e49b 7814c84 530e49b 7814c84 530e49b 7814c84 530e49b | 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 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | """Preload-mode API: the dropdown catalog + pre-aligned segment fetch.
Mirrors the Preload UI for programmatic consumers (e.g. the QuranCaption app):
- ``preload_recitations`` β every released recitation as a
``(reciter, recitation-specifics)`` tuple plus its available chapters. One
call backs the recitation + surah dropdowns.
- ``preload_segments`` β pre-aligned segments (+ optional word timestamps) for a
recitation / chapter / verse range. Output is byte-identical to the Preload
segment-mode JSON download (shared ``build_segment_export`` path), plus a
single directly-fetchable ``audio_url``.
These endpoints are **public / ungated** β Preload data is read-only catalog
content served from the inspector bucket, so they intentionally do NOT call
``enforce_beta_access`` (the Caption app and scripted callers have no OAuth
profile). Everything is a bucket read; no GPU, no session.
"""
from __future__ import annotations
import os
import threading
import time
import gradio as gr
from config import PRELOAD_AUDIO_CATALOG_TTL_S, PRELOAD_CLIP_PAD_S
from src.preload.clip_audio import plan_clip_window
from src.core.usage_logger import (
get_user_id,
log_error,
mark_endpoint_entry,
set_stage,
)
from src.pipeline.exports import build_segment_export
from src.preload import manifest_client, recitation_labels, repo_loader
# ---------------------------------------------------------------------------
# Audio URL β hand back a single directly-fetchable absolute URL.
# ---------------------------------------------------------------------------
def _space_base_url(request: gr.Request | None) -> str:
"""Public base URL of this Space (``https://<host>``), or ``""``.
Prefers the HF-set ``SPACE_HOST`` env var (the canonical ``*.hf.space``
host), falling back to the forwarded/host headers of the incoming request.
"""
host = os.environ.get("SPACE_HOST")
if host:
return f"https://{host.rstrip('/')}"
if request is not None:
try:
headers = request.headers
h = headers.get("x-forwarded-host") or headers.get("host")
proto = headers.get("x-forwarded-proto") or "https"
if h:
return f"{proto}://{h}"
except Exception: # noqa: BLE001 β header access is best-effort
pass
return ""
def _absolute_audio_url(audio_url: str, request: gr.Request | None) -> str:
"""Make a resolved chapter audio URL directly fetchable by external callers.
``repo_loader.resolve_chapter_audio_url`` returns the in-Space bucket-route
path (``/preload-audio/<slug>/<ch>.mp3`` β a public, Range-capable endpoint)
that every released delivery's audio is served through. Relative paths are
absolutized against the Space host; an already-absolute URL is returned
unchanged. There is no separate CDN path β audio is always one route.
"""
if not audio_url:
return ""
if audio_url.startswith(("http://", "https://")):
return audio_url
base = _space_base_url(request)
return f"{base}{audio_url}" if base else audio_url
def _rezero_to_clip(segments, audio_url: str, request: gr.Request | None):
"""Point audio at a range-cut clip and re-zero segment times to its start.
The verse window is derived from the segments' own ``time_from`` /
``time_to`` β the only absolute fields in a segment-mode export (per-word
``start`` / ``end`` are already segment-relative, so they are left alone).
The window is padded by ``PRELOAD_CLIP_PAD_S`` on each side, ``audio_url``
gains ``?start_ms&end_ms`` for the route to cut against, and every segment's
``time_from`` / ``time_to`` is shifted to be relative to the clip start.
Returns ``(clip_audio_url, clip_start_s)``. Falls back to the full chapter
URL with ``clip_start = 0.0`` when there is no audio or no usable timing β
callers should treat ``clip_start`` only as informational metadata.
"""
if not audio_url or not segments:
return _absolute_audio_url(audio_url, request), 0.0
window = plan_clip_window(segments, PRELOAD_CLIP_PAD_S)
if window is None:
return _absolute_audio_url(audio_url, request), 0.0
start_ms, end_ms, clip_start = window
sep = "&" if "?" in audio_url else "?"
clip_url = _absolute_audio_url(
f"{audio_url}{sep}start_ms={start_ms}&end_ms={end_ms}", request,
)
return clip_url, clip_start
# ---------------------------------------------------------------------------
# Endpoint 1 β recitation catalog (drop-downs)
# ---------------------------------------------------------------------------
def preload_recitations(request: gr.Request = None):
"""Return every released recitation with its available chapters.
Shape: ``{"recitations": [{slug, label, reciter, riwayah, style, channel,
source, chapters: [int, ...]}, ...]}``. On catalog failure returns
``{"recitations": [], "error": "..."}``.
"""
mark_endpoint_entry()
set_stage("catalog")
try:
catalog = manifest_client.load_catalog()
details = recitation_labels.build_recitation_details(catalog)
except Exception as e: # noqa: BLE001 β surface as an error payload
log_error(
error_code="catalog_load_failed",
endpoint="preload_recitations",
stage="catalog",
exception=e,
user_id=get_user_id(request) if request else "unknown",
message=f"Preload catalog load failed: {e}",
)
return {"recitations": [], "error": f"Catalog load failed: {e}"}
recitations = []
for row in details:
try:
chapters = repo_loader.chapters_for_delivery(row["slug"])
except Exception: # noqa: BLE001 β a single bad delivery shouldn't 500 the list
chapters = []
recitations.append({**row, "chapters": chapters})
return {"recitations": recitations}
# ---------------------------------------------------------------------------
# Endpoint 2 β pre-aligned segments for a recitation / chapter / verse range
# ---------------------------------------------------------------------------
def _coerce_verse(value, default: int) -> int:
"""Parse a verse bound; non-positive / unparseable falls back to ``default``."""
try:
iv = int(value)
except (TypeError, ValueError):
return default
return iv if iv > 0 else default
def preload_segments(recitation, chapter, verse_from, verse_to,
include_timestamps=True, request: gr.Request = None):
"""Pre-aligned segments for a recitation chapter + verse range.
Verse range defaults to the full chapter (1 β last verse) when
``verse_from`` / ``verse_to`` are missing or β€ 0; bounds are clamped to the
chapter and ordered. ``include_timestamps`` (default True) controls whether
per-word timestamps are emitted. Segments mirror the Preload segment-mode
download byte-for-byte; the envelope adds ``recitation/chapter/verse_from/
verse_to/audio_url``. Errors return ``{"error", "segments": []}``.
"""
mark_endpoint_entry()
set_stage("validate")
slug = (recitation or "").strip() if isinstance(recitation, str) else ""
if not slug:
log_error(
error_code="invalid_recitation", endpoint="preload_segments",
stage="validate", user_id=get_user_id(request) if request else "unknown",
message="recitation (delivery slug) is required",
)
return {"error": "recitation is required", "segments": []}
try:
chapter = int(chapter)
except (TypeError, ValueError):
return {"error": "chapter must be an integer", "segments": []}
surah_info = repo_loader.load_surah_info_local()
meta = surah_info.get(str(chapter))
if not meta:
return {"error": f"Unknown chapter {chapter}", "segments": []}
num_verses = int(meta.get("num_verses") or 1)
lo = _coerce_verse(verse_from, default=1)
hi = _coerce_verse(verse_to, default=num_verses)
lo = max(1, min(lo, num_verses))
hi = max(1, min(hi, num_verses))
if lo > hi:
lo, hi = hi, lo
include_words = True if include_timestamps is None else bool(include_timestamps)
set_stage("build")
try:
pc = repo_loader.build_segment_infos(slug, chapter, lo, hi)
except Exception as e: # noqa: BLE001 β surface as an error payload
log_error(
error_code="preload_build_failed", endpoint="preload_segments",
stage="build", exception=e,
user_id=get_user_id(request) if request else "unknown",
message=f"Preload segment build failed: {e}",
context={"recitation": slug, "chapter": chapter,
"verse_from": lo, "verse_to": hi},
)
return {"error": f"Failed to load segments: {e}", "segments": []}
payload = build_segment_export(
pc.all_segments, include_words=include_words, source="preload",
)
segments = payload.get("segments", []) if payload else []
meta_block = (
payload.get("_meta") if payload
else {"view_mode": "segment", "source": "preload"}
)
# Range-scoped clip: audio_url points at the cut for [verse_from, verse_to]
# and segment times are re-zeroed to the clip start, so the caller downloads
# only the requested verses with timings that already begin at 0.
audio_url, clip_start = _rezero_to_clip(segments, pc.audio_url, request)
return {
"_meta": meta_block,
"recitation": slug,
"chapter": chapter,
"verse_from": lo,
"verse_to": hi,
"audio_url": audio_url,
"clip_start": round(clip_start, 3),
"segments": segments,
}
# ---------------------------------------------------------------------------
# Endpoint 3 β audio-only catalog (full audio set, NON-published reciters)
# ---------------------------------------------------------------------------
# Assembled audio-only payload is TTL-cached: the catalog is ~2k+ deliveries and
# each chapter list is a separate sidecar read, so we build it once per window.
_AUDIO_RECITATIONS_LOCK = threading.Lock()
_AUDIO_RECITATIONS_CACHE: dict | None = None
_AUDIO_RECITATIONS_AT: float = 0.0
def _build_audio_recitations() -> dict:
"""Assemble the audio-only catalog (rows + chapters), reading sidecars in
parallel. Raises on catalog-load failure; the caller maps it to an error."""
catalog = manifest_client.load_catalog()
details = recitation_labels.build_recitation_details(
catalog,
reciters_fn=repo_loader.audio_reciters,
deliveries_fn=repo_loader.audio_deliveries_for_reciter,
)
chapters_by_slug = repo_loader.chapters_for_audio_deliveries(
[row["slug"] for row in details]
)
recitations = [
{**row, "chapters": chapters_by_slug.get(row["slug"], [])} for row in details
]
return {"recitations": recitations}
def preload_audio_recitations(request: gr.Request = None):
"""Return the audio-only recitation catalog with available chapters.
The audio-only set is every ``by_surah`` delivery that has audio on the
bucket but is **not** in the released/segments catalog β so it never
duplicates ``preload_recitations``. These reciters offer downloadable
chapter audio (no reviewed segments). Same row shape as
``preload_recitations``; chapters come from the audio manifest sidecar (not
``segments.json``, which WIP reciters don't have). The assembled payload is
TTL-cached (``PRELOAD_AUDIO_CATALOG_TTL_S``). On catalog failure returns
``{"recitations": [], "error": "..."}``.
"""
global _AUDIO_RECITATIONS_CACHE, _AUDIO_RECITATIONS_AT
mark_endpoint_entry()
set_stage("catalog")
now = time.monotonic()
with _AUDIO_RECITATIONS_LOCK:
if (
_AUDIO_RECITATIONS_CACHE is not None
and (now - _AUDIO_RECITATIONS_AT) < PRELOAD_AUDIO_CATALOG_TTL_S
):
return _AUDIO_RECITATIONS_CACHE
try:
payload = _build_audio_recitations()
except Exception as e: # noqa: BLE001 β surface as an error payload
log_error(
error_code="catalog_load_failed",
endpoint="preload_audio_recitations",
stage="catalog",
exception=e,
user_id=get_user_id(request) if request else "unknown",
message=f"Preload audio catalog load failed: {e}",
)
return {"recitations": [], "error": f"Catalog load failed: {e}"}
with _AUDIO_RECITATIONS_LOCK:
_AUDIO_RECITATIONS_CACHE = payload
_AUDIO_RECITATIONS_AT = time.monotonic()
return payload
# ---------------------------------------------------------------------------
# Endpoint 4 β chapter audio URL for an audio-only recitation (no segments)
# ---------------------------------------------------------------------------
def preload_audio(recitation, chapter, request: gr.Request = None):
"""Directly-fetchable full-chapter audio URL for a recitation / chapter.
Audio-only counterpart of ``preload_segments``: no segments, no clipping β
just the whole-chapter ``audio_url`` (bucket Space-route when the mp3 is on
the mount, else the manifest's CDN URL). Accepts any audio-eligible delivery
(released or audio-only). Errors return ``{"error", "audio_url": ""}``.
"""
mark_endpoint_entry()
set_stage("validate")
slug = (recitation or "").strip() if isinstance(recitation, str) else ""
if not slug:
log_error(
error_code="invalid_recitation", endpoint="preload_audio",
stage="validate", user_id=get_user_id(request) if request else "unknown",
message="recitation (delivery slug) is required",
)
return {"error": "recitation is required", "audio_url": ""}
try:
chapter = int(chapter)
except (TypeError, ValueError):
return {"error": "chapter must be an integer", "audio_url": ""}
if slug not in repo_loader._list_audio_delivery_slugs():
return {"error": f"Unknown or unavailable recitation {slug}", "audio_url": ""}
set_stage("build")
try:
url = repo_loader.resolve_chapter_audio_url(slug, chapter)
except Exception as e: # noqa: BLE001 β surface as an error payload
log_error(
error_code="preload_audio_failed", endpoint="preload_audio",
stage="build", exception=e,
user_id=get_user_id(request) if request else "unknown",
message=f"Preload audio resolve failed: {e}",
context={"recitation": slug, "chapter": chapter},
)
return {"error": f"Failed to resolve audio: {e}", "audio_url": ""}
audio_url = _absolute_audio_url(url, request)
if not audio_url:
return {"error": "No audio available for this recitation/chapter", "audio_url": ""}
return {"recitation": slug, "chapter": chapter, "audio_url": audio_url}
|