# anime-series-maker

Adopt this skill: https://proskills.md/skills/other/mayzmei-anime-series-maker-skills-anime-series-maker

Page: https://proskills.md/skills/other/mayzmei-anime-series-maker-skills-anime-series-maker
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skills/anime-series-maker/SKILL.md
size: 10019
sha: 535c90978a69d2242053a08e43049d475c54c6a9
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/SKILL.md

````text
---
name: anime-series-maker
description: Turn 2–3 character sheets + one line of story into a finished, postable 2D anime episode (15–90 s) — and keep making the next one with the same cast. Plans the hook, writes a per-second shot script with in-character dialogue, generates each ≤15 s clip on Renoise (Seedance-class video models via Renoise MCP), checks every clip, edits the cut, makes an Instagram-safe cover and writes the caption. Use when the user says "make an anime episode", "next episode", "anime series", "fan anime", "AI anime short", "番剧", "动漫短片", "下一集", "连载", "同人动画", "アニメ", or hands over character sheets and a story idea. NOT for remaking a specific reference video shot-for-shot (use a remake skill), or for realistic talking-head / UGC videos.
---

# Anime Series Maker

One series = one fixed cast, one fixed art style, one fixed voice setup, stored in `series.json`.
Every episode reuses it, so episode 30 looks like episode 1.

The pipeline was built while making a fan-anime series on Instagram (top episodes: 651K / 546K / 346K views).
Every rule in `references/pitfalls.md` is a mistake that cost real credits once. Read it before generating anything.

```
0. setup check ─► 1. series bible (once) ─► 2. hook ─► 3. script ─► 4. build + validate prompts
   ─► 5. quote credits → user says yes → clip 1 → user checks → rest
   ─► 6. QA every clip (retake whole clips) ─► 7. edit ─► 8. cover ─► 9. caption ─► 10. log episode
```

Work in a project folder chosen by the user (default `./my-series/`):

```
my-series/
  series.json            # the bible — cast, style, voices, settings, running gags
  sheets/                # character sheets (design references)
  episodes.json          # log of every episode: premise, hook, views (to avoid repeats, learn what works)
  ep07-cat-girl/
    episode.json         # the script
    prompts/clip01.txt … # built by scripts/build_prompt.py
    clips/clip01.mp4 …   # downloaded results
    ep07_final.mp4  ep07_cover.png  caption.md
```

---

## 0 · Setup check (skip what already works)

- **Renoise MCP** connected: call `get_account`. Balance 0 → stop and tell the user generation needs credits.
  Not connected → point them to the README "Connect Renoise" table.
- `ffmpeg` / `ffprobe` on PATH, `python3` with `Pillow` (`python3 -m pip install pillow`).
- Before choosing any model: `load_skill model-routing`, then `list_models` for the chosen model's real limits
  (max duration per clip, ratios, whether it generates audio). Don't trust model names from memory.

## 1 · Series bible (first time only)

Ask everything in **one** message:

1. **Cast** — 2–3 characters. Character sheets ready? (turnaround / full-body design sheets work best)
   - No sheets → generate them first (image model from model-routing, 1K, plain light background, front + side + back + 3 expressions, no text). Show them; iterate until approved. Sheets are the single biggest factor in consistency.
2. **Personality + voice** per character — one line each (e.g. "proud rich-girl tsundere, blushes instantly, voice cracks when flustered").
3. **Art style** — offer the presets in `references/style-presets.md` with a recommendation (default: Soft Watercolor).
4. **Dialogue language** — Japanese is the default for anime (sounds most "anime" to every audience); English / Chinese / Korean also work.
5. **Ratio** — 16:9 (YouTube / X, default) or 9:16 (Reels / TikTok / Shorts).

Then:
- Upload the sheets with `upload_files` → write each `material:<id>` into `series.json`.
- Fill `series.json` from `templates/series.json`. The `look` field must restate every visible design detail
  (hair length/colour/parting, eyes, uniform pieces, shoes, props). The model re-reads it every clip — vague looks drift.

## 2 · Hook first, then plot

Short anime lives or dies on the premise. Before writing any script, offer **3 one-line premises with one top pick**,
using `references/hooks.md` (formulas that actually got views: transformation, fake-out that fails in 3 seconds,
catchphrase ×3, "she wants X but can't admit it", a tiny everyday moment with the viewer as "you").
Check `episodes.json` so you never repeat a premise, and reuse running gags from `series.json`.

## 3 · Script → `episode.json`

Copy `templates/episode.json`. Rules:

- **Clips ≤ the model's max duration** (usually 15 s). An episode is 1–6 clips. Each clip must be a complete mini-scene with its own beat; the cut between clips is a hard scene cut, never mid-action.
- **About one shot per 2–3 s**, each with `action`, `dialogue`, `sfx`, `camera` (format in `references/prompt-framework.md`).
- **Dialogue in the series language, in character.** Short lines (≤ ~2.5 s spoken each). Write new lines — never translate or paraphrase lines from an existing show or video.
- **Start on the story.** No title card, no cold-open teaser. First shot = the situation.
- End each clip on a readable reaction (blush, smirk, door slam) — it gives the editor a clean cut point.
- Describe copyrighted characters only by appearance in prompts (the bible does this already); never put franchise or character names in prompts.

Show the user the script as a compact table (time · shot · line) and wait for approval or edits.

## 4 · Build + validate prompts

```bash
python3 scripts/build_prompt.py my-series/series.json my-series/ep07-cat-girl/episode.json
```

It writes `prompts/clipNN.txt` (opening directive → reference mapping → character consistency → STYLE → VOICES → setting → FOCUS → CHARACTER DESIGN → per-shot list → FINAL VISUAL → no-text block) and **exits non-zero** if anything is wrong: missing sheet IDs, placeholders left, a shot without Dialogue/SFX/Camera, shots past the clip duration, prompt suspiciously short.
Never submit if it fails. Never pass a prompt to `create_tasks` from a file you didn't just build and check.

## 5 · Quote → approve → clip 1 → rest

1. `estimate_cost` with every planned clip (model, duration, ratio, `audioGeneration: true`, the sheet references with role `reference_image`).
2. Show: clips · seconds · credits · balance. **Stop and wait for the user's explicit "yes" in chat.**
3. `create_tasks` for **clip 1 only**, `wait_tasks`, show it. Ask: characters right? voices right? style right?
4. Only then submit the remaining clips (one `create_tasks` call, ≤10 tasks, each with an `idempotencyKey` like `ep07-clip02`).

References: every clip gets **all** sheets of the characters who appear, in the order the prompt maps them
(`@material:<id>` in the prompt = the same material in `references`).

## 6 · QA every clip

`download_results` → `clips/clipNN.mp4`. For each clip check (use `analyze_media` on the task, plus your own frame grabs: `ffmpeg -ss <t> -i clip.mp4 -frames:v 1 f.jpg` at 4 points):

- [ ] Characters match the sheets (hair length & colour, eye colour, outfit, props). No sheet layout / white panels in frame.
- [ ] No subtitles, speech bubbles or garbled text burned in.
- [ ] Voices: right language, right gender per character, the scripted lines (not random ones).
- [ ] Story logic: who talks faces who; cause → effect reads; the last beat lands.
- [ ] Style stayed 2D (no sudden photoreal / 3D shots).

Fail → **regenerate the whole clip** (same prompt, or tighten the failing part). Never splice a partial retake into a clip — it never looks seamless. Tell the user what failed and the retake cost before resubmitting.

## 7 · Edit

```bash
python3 scripts/assemble.py my-series/ep07-cat-girl/clips/clip0{1,2,3}.mp4 -o my-series/ep07-cat-girl/ep07_final.mp4 \
    [--trim 1:0.2:0.4] [--bgm music.mp3 --bgm-db -20] [--watermark logo.png] [--vertical]
```

What it does (see the script header): trims dead air, normalises every clip (fps, pixel format, colour range, SAR),
joins with **one re-encode** (never `-c copy` — Instagram's transcoder drops segments when formats change mid-file),
exports 1080p (lanczos + light sharpen, no AI upscaling), optional BGM bed under the generated dialogue, loudness to −14 LUFS,
then decodes the result end-to-end and writes a contact sheet of every join (`*_joins.jpg`) — look at it.

Show the final cut to the user **before** making the cover.

## 8 · Cover

- Refs: **the character sheets first**, then one frame from the episode as the scene reference. Frames alone make faces drift toward thick-paint / dark rendering.
- Prompt the sheets' exact art style (copy the STYLE line) and the episode's key moment; describe characters by look, no names.
- 9:16 canvas, but Instagram's grid crops to 3:4: no title or faces in the top/bottom 15%; the scene must still run full-bleed to the edges (never ask for "plain background" there — models paint a white band).
- `python3 scripts/cover_check.py cover.png` → shows the 3:4 crop; check it before delivering.
- Follow `references/hooks.md` → "Cover" for title rules (or no text at all for 小红书-style covers).

## 9 · Caption

Use `references/hooks.md` → "Captions": first line = the premise as one plain sentence aimed at "you",
one punchline, a follow line, hashtags. Give each platform/language as its own code block.

## 10 · Log

Append to `episodes.json`: id, title, premise, hook line, clips, credits spent, date. When the user later tells you views,
store them — next time, rank premise formulas by real views, not by taste.

---

## Files

- `references/prompt-framework.md` — the clip prompt structure, with a full worked example
- `references/style-presets.md` — tested art-style blocks
- `references/hooks.md` — premise formulas, cover and caption patterns
- `references/pitfalls.md` — every expensive mistake and its fix (read before step 5)
- `templates/series.json`, `templates/episode.json`
- `scripts/build_prompt.py`, `scripts/assemble.py`, `scripts/cover_check.py` (Python 3 stdlib + ffmpeg; Pillow for the cover check)
````

## File: skills/anime-series-maker/scripts/assemble.py
size: 6876
sha: fb1c0bb6720fe5012e35aa66e96b80b6e8e4e2d1
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/scripts/assemble.py

```text
#!/usr/bin/env python3
"""Join generated clips into one postable episode.

    python3 assemble.py clip01.mp4 clip02.mp4 clip03.mp4 -o ep07_final.mp4 \
        [--trim 1:0.2:0.4] [--clip-db 0] [--bgm music.mp3 --bgm-db -20] \
        [--watermark logo.png --wm-width 130 --wm-opacity 0.6] [--vertical] [--fps 30]

- --trim N:HEAD:TAIL  cut HEAD seconds from the start and TAIL from the end of clip N (1-based); repeatable.
- Every clip is normalised (scale + pad to 1080p, fps, yuv420p tv range, SAR 1, bt709) and the whole episode is
  encoded ONCE. Never join AI clips with `-c copy`: mixed pixel formats / colour ranges make Instagram drop segments.
- 1080p = lanczos scale + light unsharp. Audio: generated dialogue kept, optional looped BGM bed underneath,
  fade-out, loudness normalised to -14 LUFS.
- Afterwards it decodes the file end-to-end and writes <out>_joins.jpg (frames just before / after every cut).
Needs ffmpeg + ffprobe on PATH. Python 3 stdlib only.
"""
import argparse
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        sys.exit(f"ERROR running {' '.join(cmd[:3])} …\n{r.stderr[-2000:]}")
    return r.stdout

def probe(path):
    out = run(["ffprobe", "-v", "error", "-show_entries", "stream=codec_type,width,height:format=duration",
               "-of", "json", str(path)])
    j = json.loads(out)
    v = next((s for s in j["streams"] if s["codec_type"] == "video"), None)
    if not v:
        sys.exit(f"ERROR: no video stream in {path}")
    return {"w": v["width"], "h": v["height"], "dur": float(j["format"]["duration"]),
            "audio": any(s["codec_type"] == "audio" for s in j["streams"])}

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("clips", nargs="+")
    ap.add_argument("-o", "--out", required=True)
    ap.add_argument("--trim", action="append", default=[])
    ap.add_argument("--clip-db", type=float, default=0.0, help="gain for the generated clip audio")
    ap.add_argument("--bgm")
    ap.add_argument("--bgm-db", type=float, default=-20.0)
    ap.add_argument("--watermark")
    ap.add_argument("--wm-width", type=int, default=130)
    ap.add_argument("--wm-opacity", type=float, default=0.6)
    ap.add_argument("--wm-margin", default="20:8", help="right:bottom margin in px")
    ap.add_argument("--vertical", action="store_true", help="force 1080x1920 (default: follow clip 1)")
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--lufs", type=float, default=-14.0)
    args = ap.parse_args()

    for tool in ("ffmpeg", "ffprobe"):
        if not shutil.which(tool):
            sys.exit(f"ERROR: {tool} not found on PATH")
    clips = [Path(c) for c in args.clips]
    for c in clips:
        if not c.is_file():
            sys.exit(f"ERROR: missing clip {c}")
    info = [probe(c) for c in clips]

    trims = {}
    for t in args.trim:
        n, head, tail = t.split(":")
        trims[int(n)] = (float(head), float(tail))

    vertical = args.vertical or info[0]["h"] > info[0]["w"]
    W, H = (1080, 1920) if vertical else (1920, 1080)

    cmd = ["ffmpeg", "-y", "-v", "error"]
    for c in clips:
        cmd += ["-i", str(c)]
    idx = len(clips)
    if args.bgm:
        cmd += ["-stream_loop", "-1", "-i", args.bgm]
        bgm_idx, idx = idx, idx + 1
    if args.watermark:
        cmd += ["-i", args.watermark]
        wm_idx = idx

    fc, durs = [], []
    for i, inf in enumerate(info):
        head, tail = trims.get(i + 1, (0.0, 0.0))
        d = inf["dur"] - head - tail
        if d <= 0.3:
            sys.exit(f"ERROR: clip {i + 1} trimmed to {d:.2f}s")
        durs.append(d)
        fc.append(
            f"[{i}:v]trim=start={head}:duration={d},setpts=PTS-STARTPTS,"
            f"scale={W}:{H}:force_original_aspect_ratio=decrease:flags=lanczos,"
            f"pad={W}:{H}:(ow-iw)/2:(oh-ih)/2:color=black,unsharp=5:5:0.6:5:5:0.0,"
            f"fps={args.fps},scale=in_range=auto:out_range=tv,format=yuv420p,setsar=1[v{i}]"
        )
        if inf["audio"]:
            fc.append(f"[{i}:a]atrim=start={head}:duration={d},asetpts=PTS-STARTPTS,"
                      f"aresample=48000,aformat=channel_layouts=stereo,volume={args.clip_db}dB[a{i}]")
        else:
            fc.append(f"anullsrc=r=48000:cl=stereo,atrim=duration={d}[a{i}]")
    n = len(clips)
    fc.append("".join(f"[v{i}][a{i}]" for i in range(n)) + f"concat=n={n}:v=1:a=1[vc][ac]")
    total = sum(durs)

    vlast = "vc"
    if args.watermark:
        mr, mb = args.wm_margin.split(":")
        fc.append(f"[{wm_idx}:v]scale={args.wm_width}:-1,format=rgba,colorchannelmixer=aa={args.wm_opacity}[wm]")
        fc.append(f"[vc][wm]overlay=W-w-{mr}:H-h-{mb}[vw]")
        vlast = "vw"

    alast = "ac"
    if args.bgm:
        fc.append(f"[{bgm_idx}:a]atrim=duration={total},aresample=48000,aformat=channel_layouts=stereo,"
                  f"volume={args.bgm_db}dB[bg]")
        fc.append("[ac][bg]amix=inputs=2:duration=first:normalize=0[am]")
        alast = "am"
    fade_st = max(total - 1.5, 0)
    fc.append(f"[{alast}]afade=t=out:st={fade_st}:d=1.5,loudnorm=I={args.lufs}:TP=-1.5:LRA=11,aresample=48000[aout]")

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    cmd += ["-filter_complex", ";".join(fc), "-map", f"[{vlast}]", "-map", "[aout]",
            "-c:v", "libx264", "-preset", "slow", "-crf", "15", "-pix_fmt", "yuv420p",
            "-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709",
            "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-t", f"{total:.3f}", str(out)]
    run(cmd)

    # verify: full decode must be clean
    r = subprocess.run(["ffmpeg", "-v", "error", "-i", str(out), "-f", "null", "-"], capture_output=True, text=True)
    if r.stderr.strip():
        print("WARNING: decode errors:\n" + r.stderr[-1500:], file=sys.stderr)

    # contact sheet of every join
    joins, t = [], 0.0
    for d in durs[:-1]:
        t += d
        joins.append(t)
    sheet = out.with_name(out.stem + "_joins.jpg")
    if joins:
        with tempfile.TemporaryDirectory() as tmp:
            k = 0
            for j in joins:
                for ts in (max(j - 0.25, 0), j + 0.25):
                    k += 1
                    run(["ffmpeg", "-y", "-v", "error", "-ss", f"{ts:.3f}", "-i", str(out), "-frames:v", "1",
                         "-vf", "scale=480:-2", f"{tmp}/f{k:03d}.jpg"])
            run(["ffmpeg", "-y", "-v", "error", "-i", f"{tmp}/f%03d.jpg",
                 "-vf", f"tile=2x{len(joins)}:padding=6:color=white", "-frames:v", "1", str(sheet)])
    print(f"OK {out}  {W}x{H}  {total:.2f}s  clips={n}" + (f"  joins sheet: {sheet}" if joins else ""))

if __name__ == "__main__":
    main()
```

## File: skills/anime-series-maker/scripts/build_prompt.py
size: 8616
sha: c50205c8cba2e1850d46e2cbedd818eccf877a8a
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/scripts/build_prompt.py

```text
#!/usr/bin/env python3
"""Build and validate one prompt per clip from series.json + episode.json.

    python3 build_prompt.py series.json ep07/episode.json [--out ep07/prompts]

Writes <out>/clipNN.txt and <out>/tasks.json (model, duration, ratio, references per clip,
ready to hand to estimate_cost / create_tasks). Exits 1 on any validation error and writes nothing.
Python 3 stdlib only.
"""
import argparse
import json
import re
import sys
from pathlib import Path

PLACEHOLDER = re.compile(r"material:0+\b|【|】|\{\{|\}\}|TODO|TBD|<[A-Z_ ]+>")

def load(path):
    try:
        return json.loads(Path(path).read_text(encoding="utf-8"))
    except Exception as e:
        sys.exit(f"ERROR: cannot read {path}: {e}")

def cap(text):
    """Capitalise the first letter and end with a full stop (empty stays empty)."""
    text = text.strip().rstrip(".")
    return f"{text[0].upper()}{text[1:]}." if text else ""

def fmt_t(x):
    return f"{x:g}s"

def build_clip(series, clip, cast):
    lang = series.get("language", "Japanese")
    dur = clip["duration"]
    lines = []
    lines.append(
        f"Create a continuous {dur}-second {series.get('genre', '2D anime')} scene. "
        "Follow the exact shot pacing, comedic timing and emotional rhythm below, with smooth scene "
        f"continuity from shot to shot. {cap(series.get('tone', ''))}".strip()
    )
    mapping = " ".join(
        f"@{c['material']} = character sheet of the {c['label']} (use only this design: face, hair, outfit, shoes)."
        for c in cast
    )
    lines.append(
        f"Reference mapping: {mapping} The sheets are design references only — never show the sheet "
        "layout, white background, panels or any text from them."
    )
    lines.append("")
    lines.append("Maintain exact character consistency throughout all shots —")
    for c in cast:
        lines.append(
            f"The {c['label']} (@{c['material']}): {c['look'].rstrip('.')}. "
            f"{cap(c['personality'])} Same design in every frame."
        )
    lines.append("")
    lines.append(f"STYLE: {series['style']}")
    voices = " ".join(f"{c['label']}: {c['voice'].rstrip('.')}." for c in cast)
    lines.append(
        f"VOICES & AUDIO: All dialogue is spoken in natural {lang.upper()} by anime voice actors with clear "
        f"lip-sync. {voices} {series.get('audio', '')}".strip()
    )
    setting = clip.get("setting", "")
    if isinstance(setting, int):
        setting = series["settings"][setting]
    lines.append(
        f"Preserve continuity of the setting: {setting} — consistent palette and lighting across the entire scene."
    )
    lines.append("")
    lines.append(f"FOCUS: {clip['focus']}")
    lines.append("")
    lines.append("CHARACTER DESIGN:")
    for c in cast:
        arc = clip.get("arcs", {}).get(c["id"])
        if arc:
            lines.append(f"{c['label']} — {arc}")
    for i, s in enumerate(clip["shots"], 1):
        a, b = s["t"]
        lines.append(f"[{fmt_t(a)} – {fmt_t(b)}] SHOT {i} — {s['title'].upper()}. {s['action']}")
        lines.append(f"Dialogue: {s.get('dialogue') or 'No spoken dialogue.'}")
        lines.append(f"SFX: {s['sfx'].rstrip('.')}. Camera: {s['camera'].rstrip('.')}.")
    lines.append("")
    lines.append(f"FINAL VISUAL: {clip['final_visual']}")
    lines.append(f"On-screen ending mood: {clip['ending_mood']}")
    lines.append(series.get("negative", "NO SUBTITLES. NO ON-SCREEN TEXT OR CAPTIONS. NO SPEECH BUBBLES."))
    lines.append(series.get("closing", "").replace("{language}", lang))
    return "\n".join(lines).strip() + "\n"

def validate(series, ep):
    errs = []
    chars = {c["id"]: c for c in series.get("characters", [])}
    for c in chars.values():
        for k in ("label", "material", "look", "personality", "voice"):
            if not str(c.get(k, "")).strip():
                errs.append(f"character '{c['id']}': missing {k}")
        if not re.fullmatch(r"material:[1-9]\d*", str(c.get("material", ""))):
            errs.append(f"character '{c['id']}': material must be 'material:<id>' of the uploaded sheet (got {c.get('material')!r})")
        if len(c.get("look", "")) < 120:
            errs.append(f"character '{c['id']}': look is too short — restate hair, eyes, every outfit piece, shoes, props")
    if not series.get("style"):
        errs.append("series: style is empty")
    clips = ep.get("clips", [])
    if not clips:
        errs.append("episode: no clips")
    max_dur = series.get("max_clip_seconds", 15)
    for n, clip in enumerate(clips, 1):
        tag = f"clip {n:02d}"
        dur = clip.get("duration", 0)
        if not 1 <= dur <= max_dur:
            errs.append(f"{tag}: duration {dur} outside 1–{max_dur}s (set series.max_clip_seconds from list_models)")
        for cid in clip.get("cast", []):
            if cid not in chars:
                errs.append(f"{tag}: unknown cast id '{cid}'")
        if not clip.get("cast"):
            errs.append(f"{tag}: empty cast")
        for k in ("focus", "final_visual", "ending_mood"):
            if not str(clip.get(k, "")).strip():
                errs.append(f"{tag}: missing {k}")
        st = clip.get("setting", "")
        if isinstance(st, int) and not 0 <= st < len(series.get("settings", [])):
            errs.append(f"{tag}: setting index {st} not in series.settings")
        prev_end = 0
        shots = clip.get("shots", [])
        if not shots:
            errs.append(f"{tag}: no shots")
        for i, s in enumerate(shots, 1):
            for k in ("t", "title", "action", "sfx", "camera"):
                if not s.get(k):
                    errs.append(f"{tag} shot {i}: missing {k}")
            if "dialogue" not in s:
                errs.append(f"{tag} shot {i}: missing dialogue (use \"\" for none)")
            t = s.get("t") or [0, 0]
            if len(t) != 2 or t[0] >= t[1]:
                errs.append(f"{tag} shot {i}: bad time range {t}")
                continue
            if abs(t[0] - prev_end) > 0.01:
                errs.append(f"{tag} shot {i}: starts at {t[0]}s but previous shot ends at {prev_end}s")
            prev_end = t[1]
        if shots and abs(prev_end - dur) > 0.01:
            errs.append(f"{tag}: shots end at {prev_end}s but duration is {dur}s")
    return errs

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("series")
    ap.add_argument("episode")
    ap.add_argument("--out", help="output folder (default: <episode dir>/prompts)")
    args = ap.parse_args()

    series, ep = load(args.series), load(args.episode)
    errs = validate(series, ep)
    chars = {c["id"]: c for c in series.get("characters", [])}
    prompts = []
    if not errs:
        for n, clip in enumerate(ep["clips"], 1):
            cast = [chars[cid] for cid in clip["cast"]]
            p = build_clip(series, clip, cast)
            if len(p) < 1500:
                errs.append(f"clip {n:02d}: prompt only {len(p)} chars — something is missing")
            m = PLACEHOLDER.search(p)
            if m:
                errs.append(f"clip {n:02d}: placeholder left in prompt: {m.group(0)!r}")
            prompts.append((n, clip, cast, p))
    if errs:
        print("NOT BUILT — fix these first:", file=sys.stderr)
        for e in errs:
            print("  -", e, file=sys.stderr)
        sys.exit(1)

    out = Path(args.out) if args.out else Path(args.episode).parent / "prompts"
    out.mkdir(parents=True, exist_ok=True)
    tasks = []
    for n, clip, cast, p in prompts:
        (out / f"clip{n:02d}.txt").write_text(p, encoding="utf-8")
        task = {
            "label": f"{ep.get('id', 'ep')} clip {n:02d}",
            "idempotencyKey": f"{ep.get('id', 'ep')}-clip{n:02d}",
            "duration": clip["duration"],
            "ratio": series.get("ratio", "16:9"),
            "audioGeneration": True,
            "references": [{"ref": c["material"], "role": "reference_image"} for c in cast],
            "promptFile": str(out / f"clip{n:02d}.txt"),
        }
        if series.get("model"):
            task["model"] = series["model"]
        if series.get("resolution"):
            task["resolution"] = series["resolution"]
        tasks.append(task)
        print(f"OK clip{n:02d}.txt  {clip['duration']}s  {len(clip['shots'])} shots  {len(p)} chars  cast={[c['id'] for c in cast]}")
    (out / "tasks.json").write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
    total = sum(c["duration"] for c in ep["clips"])
    print(f"{len(prompts)} clips, {total}s total -> {out}/tasks.json")

if __name__ == "__main__":
    main()
```

## File: skills/anime-series-maker/scripts/cover_check.py
size: 1985
sha: dc2bac5df0f622cd9db57364d97f726df1edaa09
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/scripts/cover_check.py

```text
#!/usr/bin/env python3
"""Preview how a 9:16 cover looks on the Instagram profile grid (3:4 center crop).

    python3 cover_check.py cover.png [-o cover_check.png]

Left: full cover with the crop lines (red) and the 15% no-text bands (shaded).
Right: what the grid actually shows. Look for titles/faces under the shaded bands and for
white / faded strips near the top edge. Needs Pillow (python3 -m pip install pillow).
"""
import argparse
import sys
from pathlib import Path

try:
    from PIL import Image, ImageDraw
except ImportError:
    sys.exit("ERROR: needs Pillow — python3 -m pip install pillow")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("cover")
    ap.add_argument("-o", "--out")
    args = ap.parse_args()

    im = Image.open(args.cover).convert("RGB")
    w, h = im.size
    crop_h = round(w * 4 / 3)
    if crop_h >= h:
        print(f"note: {w}x{h} is already 3:4 or wider — nothing gets cropped")
        crop_h = h
    top = (h - crop_h) // 2
    band = round(h * 0.15)

    marked = im.copy()
    overlay = Image.new("RGBA", im.size, (0, 0, 0, 0))
    d = ImageDraw.Draw(overlay)
    d.rectangle([0, 0, w, band], fill=(255, 0, 0, 60))
    d.rectangle([0, h - band, w, h], fill=(255, 0, 0, 60))
    marked = Image.alpha_composite(marked.convert("RGBA"), overlay).convert("RGB")
    d = ImageDraw.Draw(marked)
    lw = max(3, w // 200)
    d.line([0, top, w, top], fill=(255, 0, 0), width=lw)
    d.line([0, top + crop_h, w, top + crop_h], fill=(255, 0, 0), width=lw)

    cropped = im.crop((0, top, w, top + crop_h))
    gap = w // 20
    sheet = Image.new("RGB", (w * 2 + gap, h), "white")
    sheet.paste(marked, (0, 0))
    sheet.paste(cropped, (w + gap, (h - crop_h) // 2))
    out = Path(args.out) if args.out else Path(args.cover).with_name(Path(args.cover).stem + "_check.png")
    sheet.save(out)
    print(f"OK {out}  (grid crop removes {top}px top and bottom = {top / h:.1%} each)")

if __name__ == "__main__":
    main()
```

## File: skills/anime-series-maker/references/hooks.md
size: 2904
sha: 0a4b1905815b6f27ae96abb4ad7a4862a2212d2e
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/references/hooks.md

````text
# Hooks, covers and captions

## Premise formulas (ranked by real Instagram views of a 40-episode fan-anime series, late Oct 2026)

| Rank | Formula | Example premise | Views |
|---|---|---|---|
| 1 | **Fake-out that fails in 3 seconds** | She dresses up as a scary ghost to frighten him — her scary act lasts three seconds | 651K |
| 2 | **Transformation** (cat girl, kitten, tiny, swapped) | A spell goes wrong and she grows cat ears and a fluffy tail | 546K / 187K |
| 3 | **Catchphrase ×3** — the same line, escalating, three times | Every time something goes wrong she threatens "I'll tell my father" — the third time it backfires | 346K |
| 4 | **Wants X, can't admit it** | She wants a princess carry and keeps "accidentally" tripping near him | 237K |
| 5 | **Rescue / protect** | Someone bullies her; he steps in with one calm line | 196K |
| 6 | **Tiny rule / ritual** | She refuses to say the password to the dorm door | 129K |

Lower performers: long plot-heavy episodes (romance arcs, balls, multi-location stories) — beautiful, fewer views.
Short (14–30 s) single-joke episodes beat 60–100 s stories almost every time.

**Rules**
- One premise = one sentence a stranger understands with no context. If it needs "previously…", it's not a hook.
- The visual premise must be visible in the first 2 seconds (the cat ears, the ghost sheet, the trip).
- Recurring cute states (cat-girl form, a signature accessory) can become sub-series — repeat what got views.
- Offer 3 premises, one marked as the pick. After posting, log views in `episodes.json` and re-rank this table.

## Cover

- 9:16, but Instagram's profile grid shows a 3:4 center crop: title block from ~24% to ~38% of the height; no text or faces in the top/bottom 15%.
- The scene continues full-bleed to every edge — prompt "the scene and its colors extend all the way to the top edge; no fade, no white or blank band". Never "plain background".
- References: character sheets first, one episode frame as scene reference. Prompt the series STYLE line verbatim, plus "not heavy painting, not dark cinematic rendering" for soft styles.
- Title = the premise in ≤ 5 words (or no text at all for 小红书 / Xiaohongshu-style atmospheric covers, 3:4).
- No watermark on covers. Check with `scripts/cover_check.py`.

## Captions

Instagram:
```
<Character> <premise as one plain sentence, aimed at "you"> <emoji>

<one short punchline> <emoji>

follow for more episodes 🪄

<optional credit line: made on …>

#<series tag> #<character tags> #anime #AIVideo #AIanime …
```

X:
```
<Character> <premise sentence> <emoji>

<optional credit line>
```

What gets reach: a cute or absurd visual premise + "your" intimacy — "wakes up as a kitten on your pillow 🐱" —
not a plot recap, not "tries to … but fails". One punchline line only; no POV quotes.
Give every platform and language as its own code block.
````

## File: skills/anime-series-maker/references/pitfalls.md
size: 3260
sha: 1d5f94c27c198b26b8133f1f1f7269f40b8089e5
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/references/pitfalls.md

```text
# Pitfalls (each one cost real credits once)

## Generation

| Symptom | Cause | Fix |
|---|---|---|
| Characters drift (hair shorter, different uniform) by clip 3 | `look` too vague, or a sheet missing from that clip's references | Restate every visible detail in `look`; attach every on-screen character's sheet to every clip |
| The white character-sheet layout appears inside the video | Model treats the sheet as a scene | Keep the "design references only — never show the sheet layout…" line (build_prompt.py adds it) |
| Garbled subtitles / speech bubbles burned in | Model imitates text on refs, or dialogue looks like captions | Keep the no-text block; sheets without text labels are safer. Diegetic text (a sign, a HUD) is fine if the story needs it |
| Wrong voice gender or language, or random lines | Weak voice block, or a **reference video's audio leaking** | One voice line per character with gender words; never attach a reference video with sound — strip it first: `ffmpeg -i in.mp4 -an -c:v copy ref_silent.mp4` |
| Photoreal background in one shot | Missing 2D clamp | Keep "EVERYTHING including all backgrounds must be hand-drawn 2D…" |
| Realistic lips / noses on anime faces | Style too painterly | Add "small simple anime mouths (no realistic lips), minimal nose, flat cel shading" |
| Moderation / copyright rejection | Franchise or character names in the prompt, or real-actor footage as reference | Describe characters by appearance only; for real-footage motion refs use a grayscale depth-map version of the clip |
| Batch submitted with an empty / wrong prompt (credits gone, running tasks can't be cancelled) | Shell loop read a prompt file that had moved | Only submit prompts `build_prompt.py` just built and validated; stop the batch on any failure |
| Image refs silently ignored | Some CLI paths rewrite the reference role | Use Renoise MCP `create_tasks` with `role: "reference_image"`; check the result actually follows the sheet |

## Editing

| Symptom | Cause | Fix |
|---|---|---|
| Instagram shows the video starting mid-way / drops a segment | Joined with `-c copy`; segments differ in pixel format or colour range (e.g. `yuvj420p` full range vs `yuv420p` tv range) | `assemble.py` re-encodes everything in one pass with identical fps / format / range / SAR. Also scrub through in the IG upload editor before posting |
| Partial retake looks like a glitch | A re-generated 3-second piece spliced into a clip | Re-generate the whole clip |
| Music jumps at every cut | Each clip has its own generated score | Turn the clip music down / out and lay one continuous BGM bed (`--bgm`) |
| Soft / blurry upload | Uploaded 720p | Export 1080p with lanczos + light unsharp (assemble.py default); no AI upscaling needed |
| Slow start, viewers scroll away | Title card or cold-open teaser | Start on the story; trim dead air (`--trim`) |

## Cover

| Symptom | Fix |
|---|---|
| Title cut off on the IG grid | Keep text in the 3:4 safe zone (see hooks.md); verify with `cover_check.py` |
| White / faded band at top | Never say "plain background"; ask for full-bleed scene to the edge |
| Faces don't look like the sheets (darker, heavier paint) | Pass the sheets first as references, frames only as scene reference |
```

## File: skills/anime-series-maker/references/prompt-framework.md
size: 5529
sha: 7457cd50a89ac3a531a95d73ef43fdef07ee4592
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/references/prompt-framework.md

````text
# Clip prompt framework

`scripts/build_prompt.py` assembles this from `series.json` + `episode.json`. This file explains each block so you can
write good episode fields and fix a prompt by hand when needed.

Order matters — the model weighs the start of the prompt most:

| # | Block | Comes from | Why |
|---|---|---|---|
| 1 | **Opening directive** — "Create a continuous N-second {genre} scene. Follow the exact shot pacing…" | series `genre`, `tone`, clip `duration` | Sets length, genre and that this is ONE continuous scene |
| 2 | **Reference mapping** — "@material:123 = character sheet of the BLONDE GIRL (use only her design…). The sheets are design references only — never show the sheet layout…" | `characters[].material`, `label` | Without "never show the sheet" models sometimes paste the white sheet panel into frame |
| 3 | **Character consistency** — one paragraph per character: label, sheet, full `look`, `personality`, "Same design in every frame." | `characters[]` | Re-stating every visible detail is what keeps hair length and outfits from drifting |
| 4 | **STYLE:** | series `style` | Pure style keywords + "everything must be hand-drawn 2D" clamp |
| 5 | **VOICES & AUDIO:** language + one voice line per character + music/SFX bed | `language`, `characters[].voice`, `audio` | Gives each character a distinct, gendered voice in the right language |
| 6 | **Setting continuity** | clip `setting` (index or text) | One location per clip keeps palette and lighting stable |
| 7 | **FOCUS:** | clip `focus` | Theme / emotion keywords |
| 8 | **CHARACTER DESIGN:** how each character's expression changes across the clip | clip `arcs` | Gives the model an emotional arc, not just poses |
| 9 | **Shot list** — `[0s – 3s] SHOT 1 — TITLE. action` / `Dialogue:` / `SFX:` / `Camera:` | clip `shots[]` | Per-shot timing, line, sound and camera — the core control surface |
| 10 | **FINAL VISUAL:** + "On-screen ending mood:" | clip `final_visual`, `ending_mood` | A clean last frame = a clean cut point |
| 11 | **No-text block** + closing statement | series `negative`, `closing` | Keep it short: no subtitles / on-screen text / speech bubbles |

## Writing shots

- ~1 shot per 2–3 s. 15 s ≈ 5–7 shots. More shots = faster comedy, but each needs a clear single action.
- `action`: start with shot size (Wide / Medium / Close-up / Over-shoulder), then ONE action, then the background detail.
- `dialogue`: `Speaker (delivery): 「line」`. Several speakers in one shot are fine. No dialogue → `No spoken dialogue`.
  Japanese lines in 「」 work well; keep each ≤ ~15 characters / ~2.5 s.
- `sfx`: concrete sounds ("glasses glint ting", "comedic boing", "crow caw").
- `camera`: one movement per shot: push-in, pull-out, pan, whip pan, tracking, static, low angle, over-shoulder. **No rotating/orbit moves** in short clips.
- Small, readable actions beat big ones: peeking, blushing, pushing up glasses, storming off. Avoid long running/fighting choreography.
- Emotions as visible actions ("covers her face with both hands"), not adjectives ("very embarrassed").

## Worked example (built from `templates/episode.json`)

```
Create a continuous 15-second 2D anime romantic comedy scene. Follow the exact shot pacing, comedic timing and emotional rhythm below, with smooth scene continuity from shot to shot. fast, punchy anime comedy timing with expressive faces and snappy reactions.
Reference mapping: @material:111 = character sheet of the BLONDE GIRL (use only her design: face, hair, outfit, shoes). @material:222 = character sheet of the BOY WITH GLASSES (use only his design: face, hair, outfit, shoes). The sheets are design references only — never show the sheet layout, white background, panels or any text from them.

Maintain exact character consistency throughout all shots —
The BLONDE GIRL (@material:111): very long straight platinum-blonde hair to her thighs … Same design in every frame.
The BOY WITH GLASSES (@material:222): messy jet-black hair, thin round wire-rimmed glasses … Same design in every frame.

STYLE: 2D Japanese anime, soft cel shading, clean thin lineart, …
VOICES & AUDIO: All dialogue is spoken in natural JAPANESE by anime voice actors with clear lip-sync. BLONDE GIRL: bright high-pitched ojou-sama voice … BOY WITH GLASSES: clean calm youthful tenor … Light whimsical orchestral pizzicato comedy music, anime comedy SFX.
Preserve continuity of the setting: sunlit gothic stone corridor with tall arched windows … — consistent palette and lighting across the entire scene.

FOCUS: tsundere rich girl, fake fall to be caught, awkward stiff catch, …

CHARACTER DESIGN:
BLONDE GIRL — plotting and nervous at first, then cringing at herself, …
BOY WITH GLASSES — calm and oblivious-looking, catches her like a stiff wooden plank …
[0s – 3s] SHOT 1 — THE STAKEOUT. Medium close-up: the blonde girl peeks out from behind a stone pillar …
Dialogue: Girl (whispering to herself): 「…いい？彼が来たら、か弱く倒れて…受け止めてもらうだけ。」
SFX: soft footsteps echoing far away, fabric rustle. Camera: slow push-in on her face.
…
FINAL VISUAL: he goes back to his book with a small smile while her blonde hair whips out of frame as she storms off.
On-screen ending mood: playful embarrassment — her plan failed, but she is not giving up.
NO SUBTITLES. NO ON-SCREEN TEXT OR CAPTIONS. NO SPEECH BUBBLES.
All dialogue must exist only as natural in-scene Japanese audio. …
```
````

## File: skills/anime-series-maker/references/style-presets.md
size: 2513
sha: 71cfc077791bd50b6f538dc3370b859f026ee805
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/references/style-presets.md

````text
# Style presets

Paste one into `series.json → style`. Keep the final "EVERYTHING … must be …" clamp — without it, backgrounds
slide toward photoreal in some shots. Pick once per series and never change it mid-series.

Always offer these with a recommendation. Default: **Soft Watercolor** — it held character consistency best across
26 episodes and is the style of the top-performing ones.

## Soft Watercolor (recommended)
Gentle, cute, "slice-of-life romcom" look. Best for comedy, romance, cats, cozy moments.

```
2D Japanese anime, soft cel shading, clean thin lineart, soft watercolor-like painted anime backgrounds, warm candlelight and sunlight shafts, bloom glow, romantic comedy anime, blush stickers on cheeks, sparkle accents, high-quality TV anime key animation, consistent character models, full vibrant color. EVERYTHING including all backgrounds must be hand-drawn 2D anime — no photorealism, no live action, no 3D render.
```

## Sharp Thick-Paint
High-contrast, glossy "theatrical feature film" look. Best for drama, rivalry, action beats, otome-game vibes.

```
2D Japanese anime feature-film style, crisp sharp lineart, rich thick-paint cel shading with strong highlights and deep shadows, detailed painted backgrounds, dramatic rim light, cinematic depth of field, saturated jewel-tone palette, high-quality theatrical anime key animation, consistent character models, full vibrant color. EVERYTHING including all backgrounds must be hand-drawn 2D anime — no photorealism, no live action, no 3D render.
```

## Flat Cel (TV anime / chibi-friendly)
Cleanest, most "TV anime" look; lowest risk of realistic faces. Use if faces drift toward realistic lips/noses.

```
2D Japanese TV anime, flat cel shading with two-tone shadows, clean uniform lineart, small simple anime mouths (no realistic lips), minimal nose, large expressive eyes, bright flat-colored backgrounds, comedic chibi reaction cut-ins allowed, consistent character models, full vibrant color. EVERYTHING must be hand-drawn 2D anime — no photorealism, no 3D render.
```

## Live-Action Drama (optional, not anime)
Same cast as real actors — a fun "live-action adaptation" spin-off. Needs realistic sheets (or the anime sheets + "as real actors").

```
Live-action cinematic drama, real actors with natural skin texture, shallow depth of field, soft film grain, warm practical lighting, 35mm film look, natural color grade, consistent actor appearance in every shot. No anime, no cartoon, no 3D render look.
```
````

## File: skills/anime-series-maker/templates/episode.json
size: 3277
sha: 0ce7527d6bd5d6133c10ffe1211fec386686c0db
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/templates/episode.json

```text
{
  "id": "ep01",
  "title": "The Fake Swoon",
  "premise": "She fakes a faint so he'll catch her — he catches her like a wooden plank.",
  "hook": "she planned the perfect fake faint for you 😳",
  "clips": [
    {
      "duration": 15,
      "cast": ["girl", "boy"],
      "setting": 0,
      "focus": "tsundere rich girl, fake fall to be caught, awkward stiff catch, embarrassment, teasing, comedy of failure",
      "arcs": {
        "girl": "plotting and nervous at first, then cringing at herself, then theatrical fake swoon, then disbelief, then furious tomato-red blush",
        "boy": "calm and oblivious-looking, catches her like a stiff wooden plank with arms locked straight, then a knowing little smirk while pushing up his glasses"
      },
      "shots": [
        {
          "t": [0, 3],
          "title": "THE STAKEOUT",
          "action": "Medium close-up: the blonde girl peeks out from behind a stone pillar, smoothing her long hair, taking a deep breath, eyes darting.",
          "dialogue": "Girl (whispering to herself): 「…いい？彼が来たら、か弱く倒れて…受け止めてもらうだけ。」",
          "sfx": "soft footsteps echoing far away, fabric rustle",
          "camera": "slow push-in on her face"
        },
        {
          "t": [3, 5],
          "title": "SELF-CRINGE",
          "action": "Close-up: she covers her burning red face with both hands and shakes her head.",
          "dialogue": "Girl: 「…って、何やってるのよ私！」",
          "sfx": "comedic boing",
          "camera": "quick snap zoom"
        },
        {
          "t": [5, 8],
          "title": "TARGET APPROACHES",
          "action": "Wide shot: the boy with glasses walks down the sunlit corridor reading a book, unaware. The girl steps out and dramatically swoons toward him, back of her hand to her forehead.",
          "dialogue": "Girl: 「きゃあ〜っ！」",
          "sfx": "whoosh",
          "camera": "tracking side angle"
        },
        {
          "t": [8, 11],
          "title": "THE PLANK CATCH",
          "action": "Medium two-shot: he catches her by both shoulders with arms locked perfectly straight, holding her at a full arm's length like a stiff board. Frozen awkward silence.",
          "dialogue": "Girl (deadpan): 「……え、それだけ？」 Boy (blank): 「何が？」",
          "sfx": "awkward silence sting, a single crow caw",
          "camera": "static, centered, comedic symmetry"
        },
        {
          "t": [11, 15],
          "title": "HIS SMIRK",
          "action": "Close-up: her face turns tomato red and she shoves his hands off; cut to him pushing up his round glasses with one finger, a tiny knowing smirk.",
          "dialogue": "Girl (voice cracking): 「な、なんでもないわよ！」 Boy (calm, teasing): 「はいはい。足元、気をつけてね。」",
          "sfx": "blush pop, glasses glint ting, music button",
          "camera": "quick push-in, then slow push-in on him"
        }
      ],
      "final_visual": "he goes back to his book with a small smile while her blonde hair whips out of frame as she storms off.",
      "ending_mood": "playful embarrassment — her plan failed, but she is not giving up."
    }
  ]
}
```

## File: skills/anime-series-maker/templates/series.json
size: 3026
sha: b9d61c6030ed9678a0301c7af5ba1afcfa7e5c7e
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/skills/anime-series-maker/templates/series.json

```text
{
  "series": "Ojou-sama Can't Admit It",
  "language": "Japanese",
  "ratio": "16:9",
  "resolution": "720p",
  "model": "",
  "max_clip_seconds": 15,
  "genre": "2D anime romantic comedy",
  "tone": "fast, punchy anime comedy timing with expressive faces and snappy reactions",
  "style": "2D Japanese anime, soft cel shading, clean thin lineart, soft watercolor-like painted anime backgrounds, gothic magic-academy castle, warm candlelight and sunlight shafts, bloom glow, romantic comedy anime, blush stickers on cheeks, sparkle accents, high-quality TV anime key animation, consistent character models, full vibrant color. EVERYTHING including all backgrounds must be hand-drawn 2D anime — no photorealism, no live action, no 3D render.",
  "audio": "Light whimsical orchestral pizzicato comedy music, anime comedy SFX.",
  "characters": [
    {
      "id": "girl",
      "label": "BLONDE GIRL",
      "material": "material:000000",
      "sheet": "sheets/girl.png",
      "look": "very long straight platinum-blonde hair to her thighs with a center part, pale silver-grey eyes with long lashes, delicate elegant face, fair skin, small earrings; white shirt, violet-and-silver striped tie, charcoal-grey V-neck sweater vest, charcoal pleated skirt, black hooded academy robe with violet lining, charcoal knee-high socks, glossy black heeled loafers with a gold bit",
      "personality": "proud rich-girl tsundere: haughty smirk, blushes bright red instantly, voice cracks when flustered",
      "voice": "bright high-pitched ojou-sama voice, haughty, cracks when embarrassed",
      "catchphrase": ""
    },
    {
      "id": "boy",
      "label": "BOY WITH GLASSES",
      "material": "material:000000",
      "sheet": "sheets/boy.png",
      "look": "messy jet-black hair, thin round wire-rimmed glasses, green eyes; white shirt, navy-and-gold striped tie, grey V-neck sweater, black academy robe with navy lining, black trousers, black shoes, slim wooden wand",
      "personality": "calm, dry, quietly teasing: half-smile, adjusts his glasses, unbothered",
      "voice": "clean calm youthful tenor, teasing and soft",
      "catchphrase": ""
    }
  ],
  "settings": [
    "sunlit gothic stone corridor with tall arched windows, stone pillars, warm golden light shafts, floating dust sparkles",
    "candle-lit common room with a crackling fireplace, velvet sofas and tall bookshelves",
    "academy library with towering shelves, green reading lamps and floating books"
  ],
  "running_gags": [
    "her elaborate plans to get his attention fail within three seconds",
    "he always sees through her and only says one calm teasing line"
  ],
  "negative": "NO SUBTITLES. NO ON-SCREEN TEXT OR CAPTIONS. NO SPEECH BUBBLES.",
  "closing": "All dialogue must exist only as natural in-scene {language} audio. The final video must look like a clean 2D anime episode with purely visual storytelling and no visible written text anywhere in the frame. Wholesome, cute romantic comedy; camera framing always tasteful."
}
```

## File: README.md
upstream: https://github.com/mayzmei/anime-series-maker/blob/main/README.md

````text
<div align="center">

# Anime Series Maker · AI 番剧连载

**2 character sheets + 1 line of story → a finished anime episode. Then the next one, with the same cast.**
Hook, shot-by-shot script, voiced anime clips, QA, edit, Instagram-safe cover and caption — run by your AI agent.

**English** · [简体中文](README.zh-CN.md)

[![License: MIT](https://img.shields.io/badge/license-MIT-black.svg)](LICENSE)
![Agent Skill](https://img.shields.io/badge/Agent%20Skill-SKILL.md-orange)
![Renoise MCP](https://img.shields.io/badge/video-Renoise%20MCP-ff7a1a)

</div>

> In 2 weeks I made 40 anime episodes for my Instagram with one fixed cast. Top episodes: **651K / 546K / 346K views**.
> This repo is that whole workflow packed into one skill, including every mistake that cost me credits along the way.

---

## What it does

- **Series bible, not one-off prompts.** Cast, art style, voices, locations and running gags live in `series.json`. Episode 30 looks like episode 1.
- **Hook first.** Before writing anything, the agent pitches 3 premises with one pick, based on formulas ranked by real views (fake-out that fails, transformation, catchphrase ×3, "wants it, can't admit it"…).
- **Per-second shot scripts.** Each ≤15 s clip gets a shot list with action, in-character dialogue, SFX and camera for every shot, assembled into a long structured prompt. The prompt restates every design detail, so hair and outfits don't drift.
- **Validated before a single credit is spent.** `build_prompt.py` refuses to build if a sheet ID is missing, a placeholder is left, a shot has no camera, or timings don't add up.
- **Cost gate.** The agent quotes clips · seconds · credits · balance, waits for your "yes", generates clip 1 for you to check, *then* batches the rest.
- **QA every clip.** Character match against the sheets, burned-in subtitles, voice gender and language, story logic. A failed clip is regenerated whole, never patched.
- **Edit that survives Instagram.** One-pass re-encode with every clip normalised (AI clips joined with `-c copy` get segments dropped by IG), 1080p, optional BGM bed and watermark, −14 LUFS, and a contact sheet of every cut for you to check.
- **Cover + caption.** The cover is generated with the character sheets as references, so faces match. 9:16 with the title in Instagram's 3:4 grid safe zone, plus a crop preview. Captions use the hook format that actually got reach.
- **Learns from your views.** Every episode is logged; tell the agent your view counts and the next premises are ranked by them.

## How it works

```
0. setup check (Renoise MCP / ffmpeg / Python)
1. series bible — sheets, personalities, voices, style preset, language, ratio   (once per series)
2. hook — 3 premises, 1 pick
3. script — ≤15 s clips, 1 shot per 2–3 s, new in-character dialogue
4. build_prompt.py — assemble + validate every clip prompt
5. quote → your "yes" → clip 1 → you check → the rest
6. QA every clip → regenerate failures whole
7. assemble.py — trim, normalise, join, 1080p, BGM, loudness, join check
8. cover (sheets as refs) → cover_check.py
9. caption (IG / X)
10. log the episode (+ views later)
```

## Install

### 1 · Get the skill

```bash
git clone https://github.com/Mayzmei/anime-series-maker.git
```

**Claude Code**
```bash
mkdir -p ~/.claude/skills && cp -R anime-series-maker/skills/anime-series-maker ~/.claude/skills/
```
**Codex**
```bash
mkdir -p ~/.codex/skills && cp -R anime-series-maker/skills/anime-series-maker ~/.codex/skills/
```
**Other agents** (OpenClaw, WorkBuddy, …): import the `skills/anime-series-maker` folder (or a zip of it) on the product's Skills page.

### 2 · Connect Renoise (video / image generation)

Make an account at [renoise.ai](https://renoise.ai). Login happens in the browser, so there's no API key to paste.

| Agent | How |
|---|---|
| Claude Code | `/plugin marketplace add ArcoCodes/renoise-plugins-official` then `/plugin install renoise@renoise-plugins-official` |
| Codex | `codex mcp add renoise --url https://renoise.ai/api/public/mcp` then `codex mcp login renoise` |
| Claude desktop / claude.ai | Settings → Connectors → Add custom connector → URL `https://renoise.ai/api/public/mcp` |
| Others (Cursor, WorkBuddy, …) | add a remote MCP server: `https://renoise.ai/api/public/mcp` |

Latest instructions: [renoise.ai/plugin](https://renoise.ai/plugin). Test it by asking your agent to *"check my Renoise balance"*.

### 3 · Local tools

```bash
brew install ffmpeg            # Windows: winget install Gyan.FFmpeg · Linux: sudo apt install ffmpeg
python3 -m pip install pillow  # only for the cover check
```

## Use it

> *Make an anime series with these two character sheets: she's a proud rich-girl tsundere, he's calm and teases her. Japanese voices, soft watercolor style.*

> *Next episode. Something with her turning into a cat.*

The agent asks everything it needs in one message, shows the script and the credit quote, and doesn't spend anything until you say yes.

## Repo layout

```
skills/anime-series-maker/
  SKILL.md                     # the workflow the agent follows
  references/
    prompt-framework.md        # clip prompt structure + full example
    style-presets.md           # Soft Watercolor · Sharp Thick-Paint · Flat Cel · Live-Action
    hooks.md                   # premise formulas ranked by views, cover + caption patterns
    pitfalls.md                # every expensive mistake and its fix
  templates/series.json        # example series bible
  templates/episode.json       # example episode script
  scripts/build_prompt.py      # assemble + validate prompts → prompts/clipNN.txt + tasks.json
  scripts/assemble.py          # trim, normalise, join, 1080p, BGM, watermark, loudness, join sheet
  scripts/cover_check.py       # Instagram 3:4 grid crop preview
```

## Notes

- **Use characters you have the right to use.** Original characters are the safest. Prompts describe characters by appearance only, never by franchise names.
- Generation runs on your own Renoise credits. The skill always quotes before spending.
- Label AI content where your platform asks for it.

## License

MIT
````
