Spaces:
Paused
Paused
| """Small shared utilities: logging, JSON extraction, retries, ffprobe.""" | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import re | |
| import subprocess | |
| import time | |
| from typing import Any, Callable, Optional | |
| log = logging.getLogger("reelstudio") | |
| if not log.handlers: | |
| _h = logging.StreamHandler() | |
| _h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) | |
| log.addHandler(_h) | |
| log.setLevel(logging.INFO) | |
| # --------------------------------------------------------------------------- | |
| # JSON extraction (LLMs love markdown fences) | |
| # --------------------------------------------------------------------------- | |
| _FENCE_RE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL) | |
| def extract_json(text: str) -> Any: | |
| """Parse JSON out of an LLM response, stripping markdown fences. | |
| Tries, in order: fenced block, the raw text, and the largest | |
| {...} / [...] slice. Raises ValueError if nothing parses. | |
| """ | |
| candidates = [] | |
| m = _FENCE_RE.search(text) | |
| if m: | |
| candidates.append(m.group(1)) | |
| candidates.append(text.strip()) | |
| for opener, closer in (("{", "}"), ("[", "]")): | |
| start, end = text.find(opener), text.rfind(closer) | |
| if start != -1 and end > start: | |
| candidates.append(text[start : end + 1]) | |
| for c in candidates: | |
| try: | |
| return json.loads(c) | |
| except (json.JSONDecodeError, TypeError): | |
| continue | |
| raise ValueError(f"No parseable JSON in LLM response: {text[:200]!r}") | |
| # --------------------------------------------------------------------------- | |
| # Retry with exponential backoff | |
| # --------------------------------------------------------------------------- | |
| class HardAPIError(Exception): | |
| """Non-retryable API failure (400/401/403/404 class).""" | |
| def retry_backoff( | |
| fn: Callable[[], Any], | |
| attempts: int = 4, | |
| base_delay: float = 2.0, | |
| retry_on: tuple = (Exception,), | |
| label: str = "call", | |
| ) -> Any: | |
| """Run ``fn`` with exponential backoff. HardAPIError is never retried.""" | |
| last: Optional[Exception] = None | |
| for i in range(attempts): | |
| try: | |
| return fn() | |
| except HardAPIError: | |
| raise | |
| except retry_on as e: # noqa: PERF203 | |
| last = e | |
| delay = base_delay * (2**i) | |
| log.warning("%s failed (attempt %d/%d): %s — retrying in %.1fs", label, i + 1, attempts, e, delay) | |
| if i < attempts - 1: | |
| time.sleep(delay) | |
| raise last # type: ignore[misc] | |
| # --------------------------------------------------------------------------- | |
| # FFmpeg helpers | |
| # --------------------------------------------------------------------------- | |
| def run_ffmpeg(args: list[str], label: str = "ffmpeg") -> None: | |
| """Run an ffmpeg command, raising with stderr tail on failure.""" | |
| cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", *args] | |
| proc = subprocess.run(cmd, capture_output=True, text=True) | |
| if proc.returncode != 0: | |
| tail = (proc.stderr or "")[-2000:] | |
| raise RuntimeError(f"{label} failed (exit {proc.returncode}): {tail}") | |
| def ffprobe_duration(path: str) -> float: | |
| """True media duration in seconds via ffprobe.""" | |
| proc = subprocess.run( | |
| [ | |
| "ffprobe", "-v", "error", | |
| "-show_entries", "format=duration", | |
| "-of", "default=noprint_wrappers=1:nokey=1", | |
| path, | |
| ], | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if proc.returncode != 0: | |
| raise RuntimeError(f"ffprobe failed for {path}: {proc.stderr[-500:]}") | |
| return float(proc.stdout.strip()) | |
| def slugify(text: str, max_len: int = 40) -> str: | |
| s = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") | |
| return s[:max_len] or "untitled" | |