""" LLM Cinema — CUSTOM frontend on gr.Server (Gradio's FastAPI API engine). This is the "off-brand" build: instead of Gradio components, we serve our OWN HTML/CSS/JS cinema and stream frames over Server-Sent Events from FastAPI routes on a gr.Server instance. The film engine (movies + render) is reused as-is. Run: python server_app.py Deploy: HF Space (set app_file: server_app.py). Same CLAUDEMOVIES_LLM_* secrets. """ import html import json import os import re import threading import time import uuid import gradio as gr from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse import movies import render W, H = render.W, render.H HERE = os.path.dirname(__file__) SHOW = os.path.join(HERE, "showcase") # ── content filter (profanity / slurs / hate) ── _BANNED = re.compile(r"\b(" + "|".join([ "fuck", "fucks", "fucking", "fucker", "shit", "bitch", "cunt", "asshole", "bastard", "dick", "piss", "slut", "whore", "nigger", "nigga", "faggot", "fag", "kike", "spic", "chink", "gook", "wetback", "tranny", "retard", "coon", "dyke", "paki", "beaner", "heil hitler", "sieg heil", "white power", "kkk", "gas the jews", ]) + r")\w*", re.I) # \w* catches suffix variants (shitty, fucking, dickhead); leading \b avoids embeds def _blocked(t): return bool(_BANNED.search(t or "")) _MAXLEN = 60 # common prompt-injection / jailbreak patterns — neutralised so nobody can steer the model _INJECT = re.compile( r"(ignore\s+(all\s+|the\s+)?(previous|prior|above|earlier)" r"|disregard\s+(all|the|previous|prior)" r"|system\s+prompt|you\s+are\s+now|act\s+as\s+(a|an|if)" r"|jailbreak|developer\s+mode|override\s+(the|your)" r"|forget\s+(everything|all|the)|new\s+instructions)", re.I) def _clean_concept(s): """Authoritative server-side sanitiser: strip control chars / markup, collapse whitespace, and HARD-cap to 60 chars so a single short film idea is all the model ever receives.""" s = re.sub(r"[\x00-\x1f\x7f]", " ", s or "") # control chars, newlines, tabs s = "".join(ch for ch in s if ch.isprintable()) s = re.sub(r"[<>{}`\\]", "", s) # markup / structural chars s = re.sub(r"\s+", " ", s).strip() return s[:_MAXLEN] def _films(folder): out = {} order = ["knight.json", "paper-boat.json", "rain-cat.json", "troll-dance.json", "snail-race.json", "ghost.json", "bread-dragon.json"] paths = sorted(__import__("glob").glob(os.path.join(folder, "*.json")), key=lambda p: (order.index(os.path.basename(p)) if os.path.basename(p) in order else 99, os.path.basename(p))) for p in paths: try: out[json.load(open(p)).get("title") or os.path.basename(p)[:-5]] = p except Exception: continue return out def grid_to_html(grid, title=""): rows = [] for r in range(H): cells, row, i = grid[r], "", 0 while i < W: col, j, seg = cells[i][1], i, "" while j < W and cells[j][1] == col: seg += cells[j][0] j += 1 hexc = "#%02x%02x%02x" % col if isinstance(col, tuple) else "#7df9a6" esc = html.escape(seg) row += f"{esc}" if seg.strip() else esc i = j rows.append(row) bar = (f"
now_playing: {html.escape(title)}
") return f"{bar}
{chr(10).join(rows)}
" def _tint(grid): """The frame's lit colour and 'energy' — a saturation/brightness-weighted average of the drawn cells, so the audience glow becomes a low-opacity MIRROR of the action on screen. Energy is keyed to how much is lit, so sparse frames (the title/credit cards) glow ~0.""" tr = tg = tb = tw = 0.0 lit = 0 for row in grid: for cell in row: ch = cell[0] if ch == " " or ch == "": continue r, g, b = cell[1] lit += 1 mx, mn = max(r, g, b), min(r, g, b) lum = 0.2126 * r + 0.7152 * g + 0.0722 * b sat = (mx - mn) / mx if mx else 0.0 w = lum * (0.35 + sat) # vivid + bright cells lead the colour tr += r * w; tg += g * w; tb += b * w; tw += w cov = lit / float(W * H) energy = max(0.0, min(1.0, (cov - 0.10) * 6.0)) # cards are sparse -> ~0 -> no glow if tw <= 0: return [150, 180, 235], 0.0 return [int(tr / tw), int(tg / tw), int(tb / tw)], round(energy, 3) def _payload(grid, title=""): tint, energy = _tint(grid) return {"html": grid_to_html(grid, title), "tint": tint, "lvl": energy} def _screen(msg, label="ready"): bar = f"
{html.escape(label)}
" return f"{bar}
{html.escape(msg)}
" def _sse(frames): for fr in frames: payload = fr if isinstance(fr, dict) else {"html": fr} yield "data: " + json.dumps(payload) + "\n\n" yield "data: " + json.dumps({"done": True}) + "\n\n" # ── the custom frontend: a real cinema-hall photo as the backdrop, the ASCII film # projected onto the screen, and all controls docked along the bottom ── PAGE = r""" LLM Cinema
presented by conductor creative labs
LLM CINEMA
▶ LLM CINEMA
×

LLM CINEMA

Cinema is a fine-tuned MiniCPM3-4B — a four-billion-parameter model that writes, draws, and directs original ASCII short films. It invents the story, draws every character, picks the terrain, camera and mood, then stages and plays it frame by frame. No video, no footage — pure imagination, one frame at a time.

How it earns its badges

Small & local

Just 4B parameters — small enough to run on your laptop. Download the off-the-grid build and run it locally on llama.cpp, no cloud needed. Get the local build ↗
Presented by Conductor Creative Labs
conductorailabs.com
""" server = gr.Server(title="LLM Cinema") @server.get("/") def index(): return HTMLResponse(PAGE) @server.get("/static/cinema.png") def bg_image(): return FileResponse(os.path.join(HERE, "static", "cinema.png")) @server.get("/static/ansifont.json") def ansifont(): return FileResponse(os.path.join(HERE, "static", "ansifont.json"), media_type="application/json") @server.get("/static/share.png") def share_image(): return FileResponse(os.path.join(HERE, "static", "share.png"), media_type="image/png") @server.get("/api/gallery") def gallery(): return JSONResponse(list(_films(SHOW).keys())) @server.get("/api/play") def play(name: str): path = _films(SHOW).get(name) if not path: return JSONResponse({"error": "not found"}, status_code=404) spec = json.load(open(path)) def frames(): for grid in render.iter_movie_frames(spec): yield _payload(grid, spec.get("title", "")) time.sleep(movies.FRAME_MS / 1000) return StreamingResponse(_sse(frames()), media_type="text/event-stream") @server.get("/api/make") def make(concept: str): concept = _clean_concept(concept) # sanitise + hard 60-char cap (authoritative) def frames(): if not concept: yield _screen("Type a short film idea to begin.", "ready") return if _blocked(concept) or _INJECT.search(concept): yield _screen("Let's keep it friendly — try a kind, fun film idea.", "blocked") return result = {} def work(): try: result["spec"] = movies.direct(concept) except Exception as e: result["error"] = f"{type(e).__name__}: {e}" th = threading.Thread(target=work, daemon=True) th.start() t0 = time.time() while th.is_alive(): # heartbeats keep the SSE alive while the client plays the opening credits yield {"phase": "writing", "t": int(time.time() - t0)} th.join(timeout=0.7) if result.get("error") or not (result.get("spec") or {}).get("shots"): yield _screen("Could not reach the model. Please try again.", "error") return spec = result["spec"] for grid in render.iter_movie_frames(spec): yield _payload(grid, spec.get("title", "")) time.sleep(movies.FRAME_MS / 1000) return StreamingResponse(_sse(frames()), media_type="text/event-stream") if __name__ == "__main__": # HF Spaces runs this file as a script and injects GRADIO_SERVER_NAME/PORT. server.launch(server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"), server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")))