[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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 21:10:38 +10:00
parent a15612eed0
commit fb59601cc3
2 changed files with 23 additions and 0 deletions

View File

@ -45,6 +45,7 @@ MEDIA_TYPES = {
SCENES.mkdir(parents=True, exist_ok=True) SCENES.mkdir(parents=True, exist_ok=True)
RENDERS.mkdir(parents=True, exist_ok=True) RENDERS.mkdir(parents=True, exist_ok=True)
HAVE_FFMPEG = shutil.which("ffmpeg") is not None 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 # MODELBEAST proxy (M4) — off unless SCENEGOD_MB=1. Token read from disk at
# request time, never logged. Contract mirrors MESHGOD meshgod/ops.py. # 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") 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) ---- # ---- MODELBEAST proxy (M4, gated) ----
def _mb_post(path: str, data: bytes, content_type: str) -> dict: def _mb_post(path: str, data: bytes, content_type: str) -> dict:
req = urllib.request.Request(MB_HOST + path, data=data, method="POST", req = urllib.request.Request(MB_HOST + path, data=data, method="POST",

View File

@ -176,6 +176,11 @@ def main():
else: else:
print("(render test skipped — ffmpeg/ffprobe not found)") 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) --- # --- MODELBEAST proxy: gating + two-step contract (mock MB, no farm jobs) ---
assert req("POST", "/mb/submit", json.dumps({"operator": "tts"}))[0] == 503 # gated off assert req("POST", "/mb/submit", json.dumps({"operator": "tts"}))[0] == 503 # gated off
n += 1 n += 1