#!/usr/bin/env python3 """Capture the demo's canned API responses from a running STT server. Development tool, never shipped. Hand-writing fixtures for two dozen endpoints guesses their shapes wrong and drifts silently as the API moves; recording them from a real server gets the shapes right and can be re-run when something changes. Everything captured passes through stt.demo_redact first, because a real server's responses carry the operator's home directory, the machine's hostname, the paired machine's address and any SMB credentials. Read the residual-flag report it prints before committing the result. Usage: .venv/bin/python3 scripts/record_demo_fixtures.py --base-url http://127.0.0.1:8080 .venv/bin/python3 scripts/record_demo_fixtures.py --dry-run # print, don't write """ from __future__ import annotations import argparse import json import os import socket import sys import urllib.error import urllib.request sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from stt import demo_redact # noqa: E402 # GET endpoints whose real responses the demo serves back. Only the families the demo # intercepts — anything the demo lets through to the real server has no fixture. ENDPOINTS = ( "/api/models/list", "/api/models/manager", "/api/models/local", "/api/models/nllb-list", "/api/models/nllb-status", "/api/models/madlad-list", "/api/models/gguf-list", "/api/models/faster-whisper/list", "/api/models/tts-list", "/api/models/panns-status", "/api/models/silero-vad-status", "/api/models/download-status", "/api/tts/status", "/api/tts/voices", "/api/tts/models", "/api/translation/settings", "/api/translation/status", "/api/audio-devices", "/api/file-mover/status", "/api/tunnel/status", "/api/translate/llm-prompt", ) HEADER = '''"""Canned API responses the demo serves in place of the real backends. GENERATED by scripts/record_demo_fixtures.py — do not edit by hand; re-record instead. Captured from a real server so the shapes are right, then passed through stt.demo_redact to remove the recording machine's identity. """ from __future__ import annotations from typing import Any, Dict ''' def fetch(base_url: str, path: str, timeout: float = 15.0): """The endpoint's JSON, or None if it is not one this server serves.""" try: with urllib.request.urlopen(base_url.rstrip("/") + path, timeout=timeout) as response: body = response.read().decode("utf-8") except urllib.error.HTTPError as exc: print(f" {path}: HTTP {exc.code} — skipped") return None except (urllib.error.URLError, OSError, TimeoutError) as exc: print(f" {path}: unreachable ({exc}) — skipped") return None try: return json.loads(body) except ValueError: print(f" {path}: not JSON — skipped") return None def render(fixtures) -> str: body = json.dumps(fixtures, indent=4, ensure_ascii=False, sort_keys=True) return (HEADER + "RESPONSES: Dict[str, Any] = " + body.replace(": true", ": True").replace(": false", ": False") .replace(": null", ": None") + "\n") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--base-url", default="http://127.0.0.1:8080", help="a running STT server to record from") parser.add_argument("--out", default=os.path.join("stt", "demo_fixtures.py")) parser.add_argument("--hostname", default=socket.gethostname(), help="the recording machine's hostname, replaced wherever it appears") parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() print(f"Recording from {args.base_url} (hostname {args.hostname!r})") fixtures = {} for path in ENDPOINTS: payload = fetch(args.base_url, path) if payload is None: continue fixtures[path] = demo_redact.redact(payload, hostname=args.hostname) print(f" {path}: captured") if not fixtures: print("Nothing captured — is the server running and reachable?") return 1 flags = [] for path, payload in fixtures.items(): flags.extend(f"{path} {flag}" for flag in demo_redact.residual_flags(payload, hostname=args.hostname)) print(f"\n{len(fixtures)} endpoints captured.") if flags: print(f"\n!! {len(flags)} residual flags — review before committing:") for flag in flags: print(f" {flag}") else: print("No residual flags.") rendered = render(fixtures) if args.dry_run: print(f"\n--dry-run: would write {len(rendered)} bytes to {args.out}") return 0 with open(args.out, "w", encoding="utf-8") as handle: handle.write(rendered) print(f"\nWrote {args.out} ({len(rendered)} bytes)") return 0 if __name__ == "__main__": raise SystemExit(main())