From fb59601cc349d591da238ab1f918f6856a06fa72 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 21:10:38 +1000 Subject: [PATCH] [laneC] /rhubarb viseme endpoint (ffmpeg-style 503 gating) GET /rhubarb?path= -> rhubarb -f json -> mouthCues for Lane A's viseme lane; 503 when the binary is absent (not installed on ultra yet). Path-guarded. test_server.py: +503 gating assert (9 groups green). Co-Authored-By: Claude Opus 4.8 --- scenegod/server.py | 18 ++++++++++++++++++ scripts/test_server.py | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/scenegod/server.py b/scenegod/server.py index 6613ae6..85a58cd 100644 --- a/scenegod/server.py +++ b/scenegod/server.py @@ -45,6 +45,7 @@ MEDIA_TYPES = { SCENES.mkdir(parents=True, exist_ok=True) RENDERS.mkdir(parents=True, exist_ok=True) HAVE_FFMPEG = shutil.which("ffmpeg") is not None +HAVE_RHUBARB = shutil.which("rhubarb") is not None # MODELBEAST proxy (M4) — off unless SCENEGOD_MB=1. Token read from disk at # request time, never logged. Contract mirrors MESHGOD meshgod/ops.py. @@ -347,6 +348,23 @@ def render_out(rid: str): return FileResponse(out, media_type="video/mp4") +# ---- viseme lip-sync (M4) — feeds Lane A's viseme lane ---- +@app.get("/rhubarb") +def rhubarb(path: str): + """Rhubarb Lip Sync on an asset audio file -> {metadata, mouthCues:[...]}. + 503 if rhubarb absent (same pattern as ffmpeg). ponytail: synchronous — + VO clips are short; make it a render-style session if that ever bites.""" + if not HAVE_RHUBARB: + raise HTTPException(503, "rhubarb not installed (see github.com/DanielSWolf/rhubarb-lip-sync)") + f = safe_under(ASSETS, path) + if not f.is_file(): + raise HTTPException(404, "not found") + p = subprocess.run(["rhubarb", "-f", "json", str(f)], capture_output=True, text=True) + if p.returncode != 0: + raise HTTPException(500, f"rhubarb failed: {p.stderr[-500:]}") + return json.loads(p.stdout) + + # ---- MODELBEAST proxy (M4, gated) ---- def _mb_post(path: str, data: bytes, content_type: str) -> dict: req = urllib.request.Request(MB_HOST + path, data=data, method="POST", diff --git a/scripts/test_server.py b/scripts/test_server.py index 80811ab..adc540c 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -176,6 +176,11 @@ def main(): else: print("(render test skipped — ffmpeg/ffprobe not found)") + # --- rhubarb: 503 when the binary is absent (happy path needs rhubarb) --- + if not shutil.which("rhubarb"): + assert req("GET", "/rhubarb?path=audio/x.wav")[0] == 503 + n += 1 + # --- MODELBEAST proxy: gating + two-step contract (mock MB, no farm jobs) --- assert req("POST", "/mb/submit", json.dumps({"operator": "tts"}))[0] == 503 # gated off n += 1