Spaces:
Sleeping
Sleeping
Deploy deathmatch from d218214
Browse files- .gitattributes +4 -0
- README.md +87 -20
- app.py +78 -6
- examples/README.md +38 -0
- examples/britney.jpg +3 -0
- examples/eminem.jpg +0 -0
- examples/keanu.jpg +0 -0
- examples/musk.jpg +3 -0
- examples/scarlett.jpg +3 -0
- examples/zuckerberg.jpg +3 -0
.gitattributes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
examples/britney.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
examples/musk.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
examples/scarlett.jpg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
examples/zuckerberg.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -6,35 +6,102 @@ colorTo: yellow
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "6.0.0"
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
a fight script with round-by-round announcer commentary, a rendered keyframe
|
| 16 |
-
reel, a declared winner β and an optional **Animate** button that chains it into
|
| 17 |
-
one continuous fight video with the commentary burned in as captions.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
| 1 | Fight director (reads both photos) | MiniCPM-V-2_6 (OpenBMB) | A10G |
|
| 27 |
-
| 2 | Claymation keyframes | FLUX.1-schnell (BFL) | L40S |
|
| 28 |
-
| 3 | Imageβvideo fight clip (opt-in) | LTX-Video (Lightricks) | H100 |
|
| 29 |
|
| 30 |
-
##
|
| 31 |
|
| 32 |
-
- **
|
| 33 |
-
- **
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
```sh
|
| 39 |
DEATHMATCH_MOCK=1 python app.py
|
| 40 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "6.0.0"
|
| 8 |
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
short_description: Two photos in, a claymation celebrity death match out
|
| 11 |
+
tags:
|
| 12 |
+
- thousand-token-wood
|
| 13 |
+
- off-brand
|
| 14 |
+
- best-demo
|
| 15 |
+
- text-to-video
|
| 16 |
+
- image-generation
|
| 17 |
+
- text-to-speech
|
| 18 |
+
- gradio
|
| 19 |
+
- modal
|
| 20 |
+
models:
|
| 21 |
+
- openbmb/MiniCPM-V-2_6
|
| 22 |
+
- black-forest-labs/FLUX.1-schnell
|
| 23 |
+
- Lightricks/LTX-Video
|
| 24 |
+
- Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign
|
| 25 |
---
|
| 26 |
|
| 27 |
+

|
| 28 |
+

|
| 29 |
+

|
| 30 |
+

|
| 31 |
+

|
| 32 |
+

|
| 33 |
|
| 34 |
+
# π₯ Celebrity Deathmatch
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
**Upload two photos. Our AI ring director books the brawl** β a claymation fight
|
| 37 |
+
script, a rendered keyframe reel, a declared winner β then turns it into one
|
| 38 |
+
continuous fight video with **two ring announcers screaming over the action** and
|
| 39 |
+
the crowd going wild.
|
| 40 |
|
| 41 |
+
It's MTV's *Celebrity Deathmatch* as an AI-native toy: pure spectacle, zero
|
| 42 |
+
practical value, maximum fun. (That's the [Thousand Token Wood](#) track in one
|
| 43 |
+
sentence.)
|
| 44 |
|
| 45 |
+
> β οΈ **Parody.** Every visual is an AI-generated claymation **caricature** of a
|
| 46 |
+
> public figure, for comedic effect. Not real. No real people were harmed.
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
## βΆοΈ See it in action
|
| 49 |
|
| 50 |
+
- **Demo video:** _TODO β paste your YouTube / uploaded video link here_
|
| 51 |
+
- **Social post:** _TODO β paste your launch post link here_
|
| 52 |
+
|
| 53 |
+
## Why it's worth a look
|
| 54 |
+
|
| 55 |
+
- **ποΈ Two-announcer voiceover, not a silent clip.** Every beat is called by
|
| 56 |
+
**Nick** (dry, sarcastic) and **Johnny** (loud, over-excited) β two *designed*
|
| 57 |
+
voices from Qwen3-TTS VoiceDesign β mixed over a bell, a crowd murmur bed, and a
|
| 58 |
+
winner roar. The fight has a soundtrack, like the real show.
|
| 59 |
+
- **π¨ Off-brand UI.** No default Gradio look: a custom claymation-fight art
|
| 60 |
+
direction β Anton display type, fire-and-clay palette, tale-of-the-tape stat
|
| 61 |
+
bars, animated winner banner.
|
| 62 |
+
- **π§± Real caricatures, real stakes.** MiniCPM *reads both photos*, invents a
|
| 63 |
+
fighting persona, signature move, and stat line for each, then choreographs a
|
| 64 |
+
5-beat arc and picks a winner.
|
| 65 |
+
|
| 66 |
+
## How it works
|
| 67 |
+
|
| 68 |
+
A four-model pipeline β **every model under the hackathon's 32B cap**:
|
| 69 |
+
|
| 70 |
+
| Stage | Role | Model | Params |
|
| 71 |
+
|------|------|-------|--------|
|
| 72 |
+
| 1 | Fight director β reads **both** photos β fight card JSON | **MiniCPM-V-2_6** (OpenBMB) | 8B |
|
| 73 |
+
| 2 | Claymation keyframe reel | **FLUX.1-schnell** (BFL) | 12B |
|
| 74 |
+
| 3 | Keyframes β continuous fight video (opt-in) | **LTX-Video** (Lightricks) | 2B |
|
| 75 |
+
| 3 | Two-announcer voiceover | **Qwen3-TTS VoiceDesign** (Qwen) | 2B |
|
| 76 |
+
|
| 77 |
+
```
|
| 78 |
+
photo A + photo B
|
| 79 |
+
βββΆ Stage 1 MiniCPM-V-2_6 β fight card (fighters, 5 beats, 2-announcer commentary, winner)
|
| 80 |
+
βββΆ Stage 2 FLUX.1-schnell β 5 claymation keyframes
|
| 81 |
+
βββΆ Stage 3 (Animate) LTX-Video β chained clips
|
| 82 |
+
+ Qwen3-TTS β Nick/Johnny voiceover + crowd SFX + burned-in captions
|
| 83 |
+
β one MP4
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
The reel (Stages 1β2) is the fast default; **Animate** (Stage 3) is opt-in
|
| 87 |
+
because it's the GPU-heavy step.
|
| 88 |
+
|
| 89 |
+
## Tech
|
| 90 |
+
|
| 91 |
+
- **Frontend:** this Gradio 6 Space (custom CSS, no template look).
|
| 92 |
+
- **Backend:** two Modal GPU apps β `deathmatch` (MiniCPM A10G + FLUX L40S +
|
| 93 |
+
ASGI gateway) and `deathmatch-video` (ComfyUI + LTX on H100, Qwen3-TTS on A10G).
|
| 94 |
+
- **Audio/video post:** pure `ffmpeg` β xfade stitch, fit-to-beat TTS mixing,
|
| 95 |
+
synthesized crowd SFX, burned-in captions.
|
| 96 |
+
- Wired to the backend via `DEATHMATCH_API_URL` (set automatically on deploy).
|
| 97 |
+
|
| 98 |
+
## Run it locally (no GPU)
|
| 99 |
|
| 100 |
```sh
|
| 101 |
DEATHMATCH_MOCK=1 python app.py
|
| 102 |
```
|
| 103 |
+
|
| 104 |
+
Mock mode exercises the **entire UX** on CPU β canned fight card, placeholder
|
| 105 |
+
keyframe reel, and a stand-in fight video β so you can see the whole flow without
|
| 106 |
+
burning a single GPU minute.
|
| 107 |
+
</content>
|
app.py
CHANGED
|
@@ -111,6 +111,51 @@ button.primary, .dm-primary button {
|
|
| 111 |
}
|
| 112 |
label, .block label { color: var(--dm-muted) !important; font-family:'JetBrains Mono',monospace !important;
|
| 113 |
font-size: 11px !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
"""
|
| 115 |
|
| 116 |
|
|
@@ -311,12 +356,39 @@ with gr.Blocks(title="Celebrity Deathmatch") as demo:
|
|
| 311 |
)
|
| 312 |
export_btn.click(export_card, [card_state], [export_file])
|
| 313 |
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
if __name__ == "__main__":
|
|
|
|
| 111 |
}
|
| 112 |
label, .block label { color: var(--dm-muted) !important; font-family:'JetBrains Mono',monospace !important;
|
| 113 |
font-size: 11px !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; }
|
| 114 |
+
|
| 115 |
+
/* βββββββββ de-Gradio: hide framework chrome, theme the native controls so the
|
| 116 |
+
Space stops looking like a Gradio template (Off-Brand) βββββββββ */
|
| 117 |
+
footer, .gradio-container footer { display: none !important; }
|
| 118 |
+
.gradio-container { border: none !important; box-shadow: none !important; }
|
| 119 |
+
/* Let the page background show through default component panels */
|
| 120 |
+
.block, .form, .gr-group, .gr-box { background: transparent !important; border: none !important; box-shadow: none !important; }
|
| 121 |
+
|
| 122 |
+
/* Tabs β claymation chips, not default Gradio tabs */
|
| 123 |
+
.tab-nav, .tabs > .tab-nav { border-bottom: none !important; gap: 8px !important; margin-bottom: 14px !important; }
|
| 124 |
+
.tab-nav button {
|
| 125 |
+
background: rgba(10,6,8,0.6) !important; color: var(--dm-muted) !important;
|
| 126 |
+
border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 12px !important;
|
| 127 |
+
font-family: 'Anton', sans-serif !important; letter-spacing: 0.05em !important;
|
| 128 |
+
text-transform: uppercase !important; padding: 8px 16px !important;
|
| 129 |
+
}
|
| 130 |
+
.tab-nav button.selected {
|
| 131 |
+
color: #1a0608 !important; border-color: transparent !important;
|
| 132 |
+
background: linear-gradient(135deg, var(--dm-gold), var(--dm-red)) !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Native inputs β dark and on-theme */
|
| 136 |
+
input[type=text], textarea, select, .gr-input, .wrap input, .multiselect input {
|
| 137 |
+
background: rgba(10,6,8,0.66) !important; color: var(--dm-text) !important;
|
| 138 |
+
border: 1px solid rgba(255,255,255,0.12) !important; border-radius: 12px !important;
|
| 139 |
+
}
|
| 140 |
+
input::placeholder, textarea::placeholder { color: rgba(176,144,136,0.6) !important; }
|
| 141 |
+
|
| 142 |
+
/* Upload dropzones β dashed gold, matching the disclaimer band */
|
| 143 |
+
.image-container, [data-testid="image"], .gr-image, .upload-container {
|
| 144 |
+
border: 1px dashed rgba(242,176,48,0.4) !important; border-radius: 16px !important;
|
| 145 |
+
background: rgba(10,6,8,0.4) !important;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Secondary / download buttons β de-orange */
|
| 149 |
+
button.secondary, .secondary > button {
|
| 150 |
+
background: rgba(255,255,255,0.06) !important; color: var(--dm-text) !important;
|
| 151 |
+
border: 1px solid rgba(255,255,255,0.14) !important; border-radius: 999px !important;
|
| 152 |
+
font-family: 'JetBrains Mono', monospace !important; text-transform: uppercase !important;
|
| 153 |
+
letter-spacing: 0.06em !important; font-size: 12px !important;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Kill the orange Gradio accent + focus ring */
|
| 157 |
+
.gradio-container { --color-accent: var(--dm-gold) !important; --color-accent-soft: rgba(242,176,48,0.2) !important; }
|
| 158 |
+
:focus-visible { outline: 1px solid var(--dm-gold) !important; box-shadow: none !important; }
|
| 159 |
"""
|
| 160 |
|
| 161 |
|
|
|
|
| 356 |
)
|
| 357 |
export_btn.click(export_card, [card_state], [export_file])
|
| 358 |
|
| 359 |
+
# Pre-loaded celebrity matchups (parody). Each entry pre-fills both photos +
|
| 360 |
+
# names + arena. A matchup only appears once BOTH of its photos are present in
|
| 361 |
+
# ui/examples/ (drop in legally-licensed images β see examples/README.md), so
|
| 362 |
+
# this is safe to ship even before the photos are added.
|
| 363 |
+
_EX_DIR = os.path.join(os.path.dirname(__file__), "examples")
|
| 364 |
+
|
| 365 |
+
def _ex_photo(slug):
|
| 366 |
+
for ext in (".jpg", ".jpeg", ".png", ".webp"):
|
| 367 |
+
p = os.path.join(_EX_DIR, slug + ext)
|
| 368 |
+
if os.path.exists(p):
|
| 369 |
+
return p
|
| 370 |
+
return None
|
| 371 |
+
|
| 372 |
+
# (slug_a, name_a, slug_b, name_b, arena)
|
| 373 |
+
EXAMPLE_MATCHUPS = [
|
| 374 |
+
("britney", "Britney Spears", "eminem", "Eminem", "Classic Deathmatch ring"),
|
| 375 |
+
("musk", "Elon Musk", "zuckerberg", "Mark Zuckerberg", "Neon Tokyo street"),
|
| 376 |
+
("keanu", "Keanu Reeves", "scarlett", "Scarlett Johansson", "Rooftop at night"),
|
| 377 |
+
]
|
| 378 |
+
_examples = []
|
| 379 |
+
for a_slug, a_name, b_slug, b_name, ex_arena in EXAMPLE_MATCHUPS:
|
| 380 |
+
pa, pb = _ex_photo(a_slug), _ex_photo(b_slug)
|
| 381 |
+
if pa and pb:
|
| 382 |
+
arena_val = ex_arena if ex_arena in ARENAS else ARENAS[0]
|
| 383 |
+
_examples.append([pa, pb, "", arena_val, STYLES[0], a_name, b_name])
|
| 384 |
+
|
| 385 |
+
if _examples:
|
| 386 |
+
gr.Examples(
|
| 387 |
+
examples=_examples,
|
| 388 |
+
inputs=[img_a, img_b, storyline, arena, style, name_a, name_b],
|
| 389 |
+
label="β‘ Try a celebrity matchup (parody)",
|
| 390 |
+
examples_per_page=3,
|
| 391 |
+
)
|
| 392 |
|
| 393 |
|
| 394 |
if __name__ == "__main__":
|
examples/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Example matchup photos
|
| 2 |
+
|
| 3 |
+
Drop fighter photos here and they appear automatically as one-click **"Try a
|
| 4 |
+
celebrity matchup"** examples on the Tale of the Tape tab. A matchup only shows
|
| 5 |
+
up once **both** of its photos exist, so missing files are simply skipped.
|
| 6 |
+
|
| 7 |
+
## Filenames the app looks for
|
| 8 |
+
|
| 9 |
+
Any of `.jpg` / `.jpeg` / `.png` / `.webp`:
|
| 10 |
+
|
| 11 |
+
| Matchup | Fighter A file | Fighter B file |
|
| 12 |
+
|---|---|---|
|
| 13 |
+
| Britney vs. Eminem | `britney.jpg` | `eminem.jpg` |
|
| 14 |
+
| Elon Musk vs. Mark Zuckerberg | `musk.jpg` | `zuckerberg.jpg` |
|
| 15 |
+
| Keanu Reeves vs. Scarlett Johansson | `keanu.jpg` | `scarlett.jpg` |
|
| 16 |
+
|
| 17 |
+
(The display names + arena are pre-filled by the app β see `EXAMPLE_MATCHUPS`
|
| 18 |
+
in `ui/app.py`. To add a new matchup, add a row there and drop in two photos.)
|
| 19 |
+
|
| 20 |
+
## Photo tips
|
| 21 |
+
|
| 22 |
+
- Roughly **square**, **face/head-and-shoulders**, front-facing, ~512px+.
|
| 23 |
+
- A clear, well-lit single face gives the best clay caricature.
|
| 24 |
+
|
| 25 |
+
## β οΈ Licensing β please read
|
| 26 |
+
|
| 27 |
+
These photos get **committed to a public Hugging Face Space**, so use images you
|
| 28 |
+
have the right to redistribute:
|
| 29 |
+
|
| 30 |
+
- **Wikimedia Commons** is the easiest source β most of these public figures have
|
| 31 |
+
**CC BY / CC BY-SA** or public-domain portraits. Download from there and keep
|
| 32 |
+
the attribution.
|
| 33 |
+
- Avoid press/agency/paparazzi shots (Getty, etc.) β those are copyrighted and
|
| 34 |
+
not yours to redistribute.
|
| 35 |
+
- The app's on-screen "parody / AI caricature" disclaimer covers the *generated*
|
| 36 |
+
output, **not** redistribution of a copyrighted source photo. Source matters.
|
| 37 |
+
|
| 38 |
+
If in doubt, use a CC-licensed Wikimedia Commons portrait (and note the credit).
|
examples/britney.jpg
ADDED
|
Git LFS Details
|
examples/eminem.jpg
ADDED
|
examples/keanu.jpg
ADDED
|
examples/musk.jpg
ADDED
|
Git LFS Details
|
examples/scarlett.jpg
ADDED
|
Git LFS Details
|
examples/zuckerberg.jpg
ADDED
|
Git LFS Details
|