"""Celebrity Deathmatch — Gradio frontend (HuggingFace Space). Upload two fighters -> FIGHT! runs Stage 1 (fight script) + Stage 2 (keyframe reel). The opt-in "Animate" button runs Stage 3 (chained, captioned fight video). Runs fully on CPU with DEATHMATCH_MOCK=1 (canned fight, placeholder frames). """ from __future__ import annotations import json import os import tempfile import gradio as gr from model_runtime import ( API_URL, MOCK, BackendError, animate, generate_fightcard, generate_keyframes, health, ) ARENAS = [ "Classic Deathmatch ring", "Rooftop at night", "Abandoned shopping mall", "Volcano lair", "Suburban backyard", "Neon Tokyo street", ] STYLES = ["Claymation", "Stop-motion", "Cartoon", "Comic book"] CSS = """ @import url('https://fonts.googleapis.com/css2?family=Anton&family=DM+Sans:wght@400;600;800&family=JetBrains+Mono:wght@600&display=swap'); :root { --dm-bg: #0b0608; --dm-panel: rgba(24,12,16,0.86); --dm-red: #e0303c; --dm-gold: #f2b030; --dm-text: #f4ede0; --dm-muted: #b09088; } body, .gradio-container { background: radial-gradient(circle at 15% 8%, rgba(224,48,60,0.18), transparent 32%), radial-gradient(circle at 85% 4%, rgba(242,176,48,0.12), transparent 34%), linear-gradient(160deg, #0b0608 0%, #140a0d 60%, #1c0e12 100%) !important; color: var(--dm-text) !important; font-family: 'DM Sans', sans-serif !important; } .gradio-container { max-width: 1180px !important; margin: 0 auto !important; padding: 22px 20px 44px !important; } .dm-disclaimer { margin: 0 0 16px; padding: 9px 14px; border: 1px dashed rgba(242,176,48,0.45); border-radius: 12px; background: rgba(242,176,48,0.07); color: var(--dm-gold); font-family: 'JetBrains Mono', monospace; font-size: 11.5px; letter-spacing: 0.04em; text-align: center; } .dm-hero { position: relative; overflow: hidden; padding: 30px 32px; border-radius: 26px; border: 1px solid rgba(224,48,60,0.3); background: linear-gradient(120deg, rgba(28,12,16,0.95), rgba(12,6,8,0.9)), radial-gradient(circle at 88% 20%, rgba(224,48,60,0.28), transparent 40%); box-shadow: 0 22px 80px rgba(0,0,0,0.5); } .dm-hero h1 { margin: 0; font-family: 'Anton', sans-serif; font-size: clamp(46px, 8vw, 92px); line-height: 0.9; letter-spacing: 0.01em; text-transform: uppercase; background: linear-gradient(90deg, #ffd27a, var(--dm-red) 60%, #ff5060); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .dm-hero p { margin: 12px 0 0; color: var(--dm-muted); font-size: 16px; max-width: 640px; } .dm-status { margin: 14px 0 4px; padding: 11px 14px; border-radius: 14px; font-size: 12px; font-family: 'JetBrains Mono', monospace; border: 1px solid rgba(255,255,255,0.1); background: rgba(10,6,8,0.7); color: var(--dm-muted); } .dm-status.ok { border-color: rgba(60,200,110,0.4); } .dm-status.mock { border-color: rgba(242,176,48,0.45); color: var(--dm-gold); } .dm-status.fail { border-color: rgba(224,48,60,0.5); color: #ff8a8a; } .tape { display: grid; grid-template-columns: 1fr auto 1fr; gap: 14px; align-items: stretch; margin-top: 6px; } .tape-card { padding: 18px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.09); background: var(--dm-panel); box-shadow: 0 14px 44px rgba(0,0,0,0.34); } .tape-card.b { border-color: rgba(64,160,200,0.28); } .tape-card .nm { font-family: 'Anton', sans-serif; font-size: 30px; text-transform: uppercase; color: var(--dm-text); } .tape-card .tag { color: var(--dm-muted); font-size: 13px; margin: 6px 0 14px; } .tape-card .mv { color: var(--dm-gold); font-size: 13px; font-family: 'JetBrains Mono', monospace; margin-bottom: 14px; } .statrow { display:flex; align-items:center; gap:10px; margin:7px 0; font-size:11px; font-family:'JetBrains Mono',monospace; color:var(--dm-muted); } .statrow .lab { width: 96px; text-transform: uppercase; } .bar { flex:1; height:9px; border-radius:99px; background: rgba(255,255,255,0.08); overflow:hidden; } .bar > span { display:block; height:100%; background: linear-gradient(90deg, var(--dm-gold), var(--dm-red)); } .vs { display:flex; align-items:center; font-family:'Anton',sans-serif; font-size:40px; color:var(--dm-red); } .winner-banner { margin-top: 4px; padding: 22px; border-radius: 22px; text-align: center; border: 1px solid rgba(242,176,48,0.5); background: radial-gradient(circle at 50% 0%, rgba(242,176,48,0.22), transparent 60%), var(--dm-panel); } .winner-banner .w { font-family:'Anton',sans-serif; font-size: 56px; text-transform: uppercase; background: linear-gradient(90deg,#ffd27a,var(--dm-red)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; } .winner-banner .r { color: var(--dm-muted); font-size: 14px; margin-top: 6px; } button.primary, .dm-primary button { border: 0 !important; border-radius: 999px !important; background: linear-gradient(135deg, #ffd27a, var(--dm-red) 55%, #a01820) !important; color: #1a0608 !important; font-family:'Anton',sans-serif !important; letter-spacing:0.06em !important; font-size: 18px !important; text-transform: uppercase !important; box-shadow: 0 14px 40px rgba(224,48,60,0.32) !important; } .dm-commentary { padding: 16px 18px; border-radius: 18px; border: 1px solid rgba(255,255,255,0.08); background: var(--dm-panel); } label, .block label { color: var(--dm-muted) !important; font-family:'JetBrains Mono',monospace !important; font-size: 11px !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; } """ def _hero() -> str: return ("

Celebrity
Deathmatch

" "

Upload two fighters. Our AI ring director books the brawl, " "renders the claymation reel, and crowns a winner. Then hit " "Animate for the full fight.

") def _status() -> str: h = health() s = h.get("status") if s == "mock": return ("
● MOCK MODE — canned fight, no GPU. " "Set DEATHMATCH_MOCK=0 + deploy Modal for real models.
") if s == "ok": return (f"
● BACKEND ONLINE / {h.get('stage1','?')} / " f"{h.get('stage2','?')}
") return (f"
● BACKEND UNREACHABLE / {API_URL} / " "deploy Modal or run with DEATHMATCH_MOCK=1
") def _statbar(label: str, value: int) -> str: pct = max(0, min(100, int(value) * 10)) return (f"
{label}" f"" f"{int(value)}/10
") def _fighter_card(f: dict, side: str) -> str: stats = f.get("stats", {}) bars = "".join(_statbar(k, stats.get(k, 5)) for k in ("power", "speed", "showmanship")) return (f"
" f"
{f.get('name','?')}
" f"
{f.get('persona','')}
" f"
★ {f.get('signature_move','')}
" f"{bars}
") def _tale_html(card: dict) -> str: return ("
" + _fighter_card(card["fighter_a"], "a") + "
VS
" + _fighter_card(card["fighter_b"], "b") + "
") def _commentary_md(card: dict) -> str: lines = ["### Ringside commentary"] for beat in card["beats"]: lines.append(f"**Round {beat['id']} — {beat['title']}** \n" f"_{beat['commentary']}_") return "\n\n".join(lines) def _winner_html(card: dict) -> str: w = card["fighter_a"] if str(card.get("winner", "A")).upper() == "A" else card["fighter_b"] return (f"
WINNER BY KNOCKOUT
" f"
{w.get('name','?')}
" f"
{card.get('winner_reason','')}
") def run_fight(image_a, image_b, storyline, arena, style): if image_a is None or image_b is None: raise gr.Error("Upload a photo for BOTH fighters first.") try: card = generate_fightcard(image_a, image_b, storyline, arena, style) frames = generate_keyframes(card, style) except (RuntimeError, BackendError) as e: raise gr.Error(f"Fight booking failed: {e}") gallery = [(img, f"Round {beat['id']}: {beat['title']}") for img, beat in zip(frames, card["beats"])] return (card, frames, _tale_html(card), gallery, _commentary_md(card), gr.update(interactive=True), gr.update(selected="brawl")) def run_animate(card, frames, style): if not card or not frames: raise gr.Error("Book a fight first (press FIGHT!).") try: path = animate(card, frames, style) except (RuntimeError, BackendError) as e: raise gr.Error(f"Animation failed: {e}") return path, _winner_html(card), gr.update(selected="knockout") def export_card(card): if not card: raise gr.Error("Nothing to export yet.") fd, path = tempfile.mkstemp(prefix="deathmatch_fightcard_", suffix=".json") with os.fdopen(fd, "w") as f: json.dump(card, f, indent=2, ensure_ascii=False) return path with gr.Blocks(title="Celebrity Deathmatch") as demo: gr.HTML("
⚠ PARODY — AI-generated claymation caricatures " "of public figures for comedic effect. Not real. No real people were harmed.
") gr.HTML(_hero()) gr.HTML(_status()) card_state = gr.State(None) frames_state = gr.State(None) with gr.Tabs(selected="tape") as tabs: with gr.Tab("01 Tale of the Tape", id="tape"): with gr.Row(): img_a = gr.Image(label="Fighter A", type="pil") img_b = gr.Image(label="Fighter B", type="pil") with gr.Row(): storyline = gr.Textbox(label="Backstory / beef (optional)", max_lines=2, placeholder="e.g. They feuded over a sampled hook") arena = gr.Dropdown(ARENAS, value=ARENAS[0], label="Arena") style = gr.Dropdown(STYLES, value=STYLES[0], label="Visual style") fight_btn = gr.Button("FIGHT!", variant="primary", elem_classes=["dm-primary"]) tale = gr.HTML() with gr.Tab("02 The Brawl", id="brawl"): gr.Markdown("### The keyframe reel") reel = gr.Gallery(label="Fight reel", columns=5, height=320, object_fit="cover") commentary = gr.Markdown() animate_btn = gr.Button("🎬 Animate the full fight", variant="primary", interactive=False, elem_classes=["dm-primary"]) gr.Markdown("_Animate chains the keyframes into one continuous, " "caption-burned fight clip (Stage 3 — opt-in, GPU-heavy)._") with gr.Tab("03 Knockout", id="knockout"): winner = gr.HTML() fight_video = gr.Video(label="The fight", autoplay=True) export_btn = gr.Button("Download fight card (JSON)") export_file = gr.File(label="Fight card") fight_btn.click( run_fight, [img_a, img_b, storyline, arena, style], [card_state, frames_state, tale, reel, commentary, animate_btn, tabs], ) animate_btn.click( run_animate, [card_state, frames_state, style], [fight_video, winner, tabs], ) export_btn.click(export_card, [card_state], [export_file]) demo_a = os.path.join(os.path.dirname(__file__), "examples", "fighter_a.png") demo_b = os.path.join(os.path.dirname(__file__), "examples", "fighter_b.png") if os.path.exists(demo_a) and os.path.exists(demo_b): gr.Examples(examples=[[demo_a, demo_b, "", ARENAS[0], STYLES[0]]], inputs=[img_a, img_b, storyline, arena, style], label="Try a demo matchup") if __name__ == "__main__": demo.launch(max_file_size="10mb", ssr_mode=False, css=CSS)