TinyNarrator / scripts /live_smoke.py
cvpfus
Implement missing model runtime polish
35f2e3b
Raw
History Blame Contribute Delete
8.52 kB
"""Live model smoke tests for Tiny Narrator.
Run against a running local app to verify configured model endpoints:
python scripts/live_smoke.py --base-url http://127.0.0.1:7860
Checks:
- /api/runtime-status: confirms reader brain, speech, vision, image generation status
- /api/describe-image: confirms MiniCPM-V-4.6 image description path
- /api/generate-image: confirms Modal Klein image generation path
Exit codes:
0 - all configured checks passed or skipped
1 - one or more configured checks failed
Secrets (API keys, tokens) are never printed.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
def _get_json(url: str, timeout: int = 30) -> dict:
"""Fetch a JSON GET endpoint and return the parsed payload."""
request = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def _post_json(url: str, payload: dict, timeout: int = 60) -> dict:
"""POST JSON to an endpoint and return the parsed response."""
body = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def _format_elapsed(ms: float) -> str:
if ms < 1000:
return f"{round(ms)} ms"
return f"{ms / 1000:.2f} s"
def _status_label(status: str) -> str:
return {
"online": "PASS",
"fallback-ready": "SKIP",
"fallback": "SKIP",
"placeholder-ready": "SKIP",
"offline": "FAIL",
}.get(status, "SKIP")
def _role_status(runtime_status: dict[str, dict], role: str) -> str:
return str(runtime_status.get(role, {}).get("status", "unknown"))
def _role_is_configured(runtime_status: dict[str, dict], role: str) -> bool:
details = runtime_status.get(role, {})
if details.get("configured") is True:
return True
return bool(details.get("endpoint") or details.get("base_url"))
def check_runtime_status(base_url: str) -> dict[str, dict]:
"""Fetch runtime status and return per-role metadata."""
start = time.perf_counter()
try:
payload = _get_json(f"{base_url}/api/runtime-status")
elapsed = (time.perf_counter() - start) * 1000
roles = {
"reader_brain": payload.get("reader_brain", {}),
"vision": payload.get("vision", {}),
"speech": payload.get("speech", {}),
"image_generation": payload.get("image_generation", {}),
}
labels = ", ".join(f"{role}={_role_status(roles, role)}" for role in roles)
print(f" runtime-status {_format_elapsed(elapsed):>10} {labels}")
return roles
except (OSError, urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
elapsed = (time.perf_counter() - start) * 1000
print(f" runtime-status {_format_elapsed(elapsed):>10} FAIL ({exc.__class__.__name__})")
return {}
def check_describe_image(base_url: str, runtime_status: dict[str, dict]) -> bool:
"""Smoke test /api/describe-image for a known article image."""
vision_status = _role_status(runtime_status, "vision")
vision_configured = _role_is_configured(runtime_status, "vision")
if not vision_configured and vision_status in {"fallback-ready", "unknown"}:
print(f" describe-image {'SKIP':>10} vision not configured")
return True
if vision_configured and vision_status != "online":
print(f" describe-image {'FAIL':>10} vision configured but status={vision_status}")
return False
start = time.perf_counter()
try:
payload = _post_json(
f"{base_url}/api/describe-image",
{"image_id": "desk-reader", "caption": "article reader"},
timeout=90,
)
elapsed = (time.perf_counter() - start) * 1000
runtime = payload.get("runtime", "unknown")
alt_text = payload.get("alt_text", "")
truncated = alt_text[:80] + ("..." if len(alt_text) > 80 else "") if alt_text else "(empty)"
if runtime == "minicpm-v4.6":
print(f" describe-image {_format_elapsed(elapsed):>10} PASS runtime={runtime} alt={truncated}")
return True
elif runtime == "fallback":
print(f" describe-image {_format_elapsed(elapsed):>10} FAIL runtime={runtime} (expected live)")
return False
else:
print(f" describe-image {_format_elapsed(elapsed):>10} FAIL runtime={runtime} (unexpected)")
return False
except (OSError, urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
elapsed = (time.perf_counter() - start) * 1000
print(f" describe-image {_format_elapsed(elapsed):>10} FAIL ({exc.__class__.__name__})")
return False
def check_generate_image(base_url: str, runtime_status: dict[str, dict]) -> bool:
"""Smoke test /api/generate-image with a deterministic prompt."""
image_status = _role_status(runtime_status, "image_generation")
image_configured = _role_is_configured(runtime_status, "image_generation")
if not image_configured and image_status in {"fallback-ready", "unknown"}:
print(f" generate-image {'SKIP':>10} Klein not configured")
return True
if image_configured and image_status != "online":
print(f" generate-image {'FAIL':>10} Klein configured but status={image_status}")
return False
start = time.perf_counter()
try:
payload = _post_json(
f"{base_url}/api/generate-image",
{"prompt": "smoke test accessible article thumbnail", "seed": 42},
timeout=180,
)
elapsed = (time.perf_counter() - start) * 1000
runtime = payload.get("runtime", "unknown")
image_url = payload.get("image_url", "")
if runtime == "modal-klein":
print(f" generate-image {_format_elapsed(elapsed):>10} PASS runtime={runtime} url={image_url[:60]}")
return True
elif runtime == "fallback":
print(f" generate-image {_format_elapsed(elapsed):>10} FAIL runtime={runtime} (expected live Klein)")
return False
else:
print(f" generate-image {_format_elapsed(elapsed):>10} FAIL runtime={runtime} (unexpected)")
return False
except (OSError, urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
elapsed = (time.perf_counter() - start) * 1000
print(f" generate-image {_format_elapsed(elapsed):>10} FAIL ({exc.__class__.__name__})")
return False
def main() -> None:
parser = argparse.ArgumentParser(description="Tiny Narrator live model smoke tests")
parser.add_argument("--base-url", default="http://127.0.0.1:7860", help="App base URL (default: http://127.0.0.1:7860)")
args = parser.parse_args()
base_url = args.base_url.rstrip("/")
overall_start = time.perf_counter()
print(f"Tiny Narrator live smoke tests — {base_url}")
print()
print("Checks:")
runtime_status = check_runtime_status(base_url)
if not runtime_status:
print()
print("Could not reach the app. Is it running?")
sys.exit(1)
vision_configured = _role_is_configured(runtime_status, "vision")
klein_configured = _role_is_configured(runtime_status, "image_generation")
describe_ok = check_describe_image(base_url, runtime_status)
generate_ok = check_generate_image(base_url, runtime_status)
overall_elapsed = (time.perf_counter() - overall_start) * 1000
print()
failures = []
if vision_configured and not describe_ok:
failures.append("describe-image")
if klein_configured and not generate_ok:
failures.append("generate-image")
if failures:
print(f"FAILED: {', '.join(failures)} ({_format_elapsed(overall_elapsed)} total)")
sys.exit(1)
else:
configured_count = sum(1 for v in [vision_configured, klein_configured] if v)
skipped_count = 2 - configured_count
print(f"DONE: {configured_count} passed, {skipped_count} skipped ({_format_elapsed(overall_elapsed)} total)")
sys.exit(0)
if __name__ == "__main__":
main()