README.md
# 8-bit-video-gen
A two-step pipeline that turns a guest's photo into a short 8-bit pixel-art video. Gemini stylizes the photo as an 8-bit still, then Replicate's seedance-2.0 animates that still into an MP4.
## how it's used
Hugo runs this on guest headshots ahead of each *Show Us Your Agent Skills* livestream. The clips play in the intro reel and as cutaways throughout the show.
## how to use it
`SKILL.md` is the artifact. Copy this whole folder into the location your agent harness expects, then prompt the agent to use the skill. Common locations:
- **Claude Code:** `.claude/skills/8-bit-video-gen/` (project) or `~/.claude/skills/8-bit-video-gen/` (user)
- **Cursor, Codex, and other harnesses with skill support:** see your harness's documentation for the expected directory
You will also need `GEMINI_API_KEY` and `REPLICATE_API_TOKEN` set in your shell or in a `.env` in your working directory.
Then ask your agent:
> "Use the 8-bit-video-gen skill to make a video from /path/to/photo.jpg."
## see it in action
[Episode 1 of *Show Us Your Agent Skills*](https://youtube.com/live/Pq3xuChdwxQ?feature=share): the intro reel and several mid-show cutaways are generated with this skill.
<img src="images/hero.png" alt="Eric Ma's headshot rendered as 8-bit pixel art by the skill" />
<sub>Eric Ma's headshot, run through `8-bit-video-gen` and stylised by Gemini.</sub>
scripts/make_8bit.py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "google-genai>=1.53.0",
# "pillow",
# "python-dotenv",
# ]
# ///
import argparse
import base64
import mimetypes
import sys
from pathlib import Path
from dotenv import load_dotenv
from google import genai
load_dotenv()
DEFAULT_PROMPT = "This exact image, but 8 bit."
parser = argparse.ArgumentParser(description="Stylize an image as an 8-bit still via Gemini.")
parser.add_argument("input_image", type=Path, help="input image to stylize")
parser.add_argument("output_image", nargs="?", type=Path, help="output PNG path (default: <input_stem>_8bit.png)")
parser.add_argument("--prompt", default=DEFAULT_PROMPT, help="override still-image prompt for this run")
parser.add_argument("--model", default="gemini-3.1-flash-image", help="Gemini image model to use")
args = parser.parse_args()
input_path = args.input_image
output_path = args.output_image if args.output_image else input_path.with_name(f"{input_path.stem}_8bit.png")
client = genai.Client()
mime_type, _ = mimetypes.guess_type(input_path)
if mime_type is None:
mime_type = "image/png"
image_data = base64.b64encode(input_path.read_bytes()).decode("utf-8")
interaction = client.interactions.create(
model=args.model,
input=[
{"type": "text", "text": args.prompt},
{"type": "image", "data": image_data, "mime_type": mime_type},
],
)
if interaction.output_text:
print(interaction.output_text)
if interaction.output_image:
output_path.write_bytes(base64.b64decode(interaction.output_image.data))
print(f"saved: {output_path}")
else:
print("no image returned", file=sys.stderr)
sys.exit(2)
scripts/make_video.py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "httpx",
# "replicate",
# "python-dotenv",
# ]
# ///
import argparse
from pathlib import Path
import httpx
import replicate
from dotenv import load_dotenv
load_dotenv()
DEFAULT_PROMPT = "This person, agentic coding themself into an 8 bit video game where they battle agents side scrolling. Make sure to keep the person 8-bit throughout. Do not change their features. Do not use guns."
# seedance-2.0's max supported duration; we want the longest clip the model will produce.
DEFAULT_DURATION = 15
# 42, the answer to life, the universe, and everything. Fixed for reproducibility.
DEFAULT_SEED = 42
parser = argparse.ArgumentParser(description="Animate an image into an 8-bit video via Replicate seedance-2.0.")
parser.add_argument("image", type=Path, help="input image (used as first frame)")
parser.add_argument("output", nargs="?", type=Path, help="output mp4 path (default: <image_stem>.mp4)")
parser.add_argument("--prompt", default=DEFAULT_PROMPT, help="override video prompt for this run")
parser.add_argument("--duration", type=int, default=DEFAULT_DURATION, help="seconds")
parser.add_argument("--seed", type=int, default=DEFAULT_SEED)
args = parser.parse_args()
output_path = args.output if args.output else args.image.with_suffix(".mp4")
with open(args.image, "rb") as image_file:
input = {
"seed": args.seed,
"prompt": args.prompt,
"duration": args.duration,
"image": image_file,
}
prediction = replicate.models.predictions.create(
model="bytedance/seedance-2.0",
input=input,
wait=False,
)
print(f"prediction: {prediction.id}", flush=True)
prediction.wait()
if prediction.status != "succeeded":
raise RuntimeError(f"prediction {prediction.id} {prediction.status}: {prediction.error}")
if not isinstance(prediction.output, str):
raise RuntimeError(f"prediction {prediction.id} succeeded without a video URL")
print(prediction.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
partial_path = output_path.with_suffix(output_path.suffix + ".part")
with httpx.stream("GET", prediction.output, follow_redirects=True, timeout=None) as response:
response.raise_for_status()
with open(partial_path, "wb") as f:
for chunk in response.iter_bytes():
f.write(chunk)
if partial_path.stat().st_size == 0:
raise RuntimeError(f"downloaded an empty video for prediction {prediction.id}")
partial_path.replace(output_path)
print(f"saved: {output_path} ({output_path.stat().st_size} bytes)")
SKILL.md
---
name: 8-bit-video-gen
description: Turns a photo of a person into a short 8-bit pixel-art video via a two-step pipeline. Gemini stylizes the photo as an 8-bit still, then Replicate's seedance-2.0 animates that still into an MP4. Use when the user supplies a portrait photo and asks for an "8-bit video", "pixel art animation", or "retro video game version" of themselves. Trigger even if they only mention one of the two steps.
---
# 8-bit video pipeline
Two-step pipeline that turns a portrait photo into a short pixel-art video clip:
1. **Stylize.** Gemini converts the photo into an 8-bit still PNG.
2. **Animate.** Replicate's `bytedance/seedance-2.0` uses that still as the first frame to generate an MP4.
Both scripts live in `scripts/` and use [PEP 723 inline script metadata](https://peps.python.org/pep-0723/), so `uv run` resolves their dependencies automatically.
If the user only wants the still, run step 1 and stop. If they already have an 8-bit image, skip to step 2.
## Setup
Before running either script:
1. **Verify `uv` is installed:** `uv --version`. If missing, ask the user to install it (https://docs.astral.sh/uv/getting-started/installation/). The scripts declare their Python dependencies inline (PEP 723 metadata), so `uv run` handles the rest.
2. **Verify the API keys are available.** Both scripts load from a `.env` in the working directory, falling back to the shell environment. Confirm both are set before running:
- `GEMINI_API_KEY` for step 1 (Gemini stylize)
- `REPLICATE_API_TOKEN` for step 2 (Replicate animate)
If either is missing, ask the user to add it to `.env` or export it. Keys come from:
- https://aistudio.google.com/apikey
- https://replicate.com/account/api-tokens
## Before each run: ask about customization
Ask the user whether they want to customize the still prompt, video prompt, duration, or seed. Defaults live as `DEFAULT_PROMPT` in `scripts/make_8bit.py` and `DEFAULT_PROMPT` / `DEFAULT_DURATION` / `DEFAULT_SEED` in `scripts/make_video.py`; pass `--prompt`, `--duration`, or `--seed` to override for one run, or edit the constants to change the shipped default.
## Step 1: Stylize the photo
```bash
uv run <skill-dir>/scripts/make_8bit.py <input_photo> [output_png] [--prompt "..."]
```
- Default output: `<input_stem>_8bit.png` next to the input.
- Stylize prompt is `DEFAULT_PROMPT` at the top of `scripts/make_8bit.py`. Pass `--prompt` to override it for one run.
- Default model ID: `gemini-3.1-flash-image`.
- Current fallback IDs to try are `gemini-3.1-flash-lite-image` and `gemini-2.5-flash-image`. Do not use the old `*-preview` fallback names unless `ListModels` shows they are supported for the current API key.
- The script uses Gemini's Interactions API for image editing, matching the current Google AI docs for Nano Banana image generation.
- The model can sometimes add facial features that aren't in the source (e.g. a beard on a clean-shaven subject). Flag the output when this happens; do not silently rewrite the prompt to compensate.
## Step 2: Animate the still
```bash
uv run <skill-dir>/scripts/make_video.py <input_image> [output_mp4] \
[--prompt "..."] [--duration 15] [--seed 42]
```
- `<input_image>` is normally the 8-bit PNG from step 1, but any image works (used as the first frame).
- This call costs Replicate credits and can take 5 to 10 minutes.
- Output is an MP4 plus a `replicate.delivery` URL printed to stdout. The URL expires; the local file is the durable artifact.
- Run only one Replicate prediction at a time. Every retry creates another chargeable prediction.
- Never retry automatically after a timeout, disconnection, or ambiguous error. Use the printed prediction ID to check the existing job first; retry only with explicit user approval.
- If generation succeeded but the download failed, recover the output from that prediction instead of generating again.
## End-of-run
Tell the user the local MP4 path and the temporary URL. To iterate on the scene, rerun step 2 with a new `--prompt`; no need to redo step 1.
## Design notes
- **Defaults live in the scripts; CLI flags override per run.** Editing the constants changes the shipped default. Passing a flag overrides for one run without mutating the file.
- **Minimal prompts for the still.** Long, hedged prompts to image models tend to over-direct.
## After each run: iterate on the skill
After every run, ask the user whether anything about the workflow should be folded back into the skill: new defaults, prompt changes, extra steps, things that surprised them, anything they corrected by hand. If yes, update `SKILL.md` and/or the scripts so the next run starts from the improved version.