"""Nano Banana Pro vs Nano Banana 2.1 prompts: how creators write for each model.

Reproduces https://agentcraft.dev/data/nano-banana-pro-vs-2-1-prompts-2026-11.json from the public prompt
collection https://github.com/ai-boost/awesome-image-video-prompts (one YAML file per prompt).

  git clone https://github.com/ai-boost/awesome-image-video-prompts
  git -C awesome-image-video-prompts checkout 590b532
  pip install pyyaml
  python3 nano-banana-pro-vs-2-1-prompts-2026-11.py awesome-image-video-prompts > out.json

Only standard library plus PyYAML. Prompt text is not copied into the output; each row links to its YAML file.
"""
import datetime
import json
import re
import statistics
import subprocess
import sys
from pathlib import Path

import yaml

REPO = Path(sys.argv[1] if len(sys.argv) > 1 else "awesome-image-video-prompts")
RELEASE = {
    "Nano Banana Pro": datetime.date(2025, 11, 20),  # Google release date (Gemini 3 Pro Image)
    "Nano Banana 2.1": datetime.date(2026, 10, 6),
}

# Feature definitions. Each is a plain test on the prompt text, so anyone can argue with it.
LABEL = re.compile(r"^\s*[\[【]?[\w一-鿿぀-ヿ /&()-]{2,30}[\]】]?\s*[:：]")       # "Style: ..." lines
LIST = re.compile(r"^\s*([-*•]|\d+[.)、])\s")                                     # bullet or numbered line
RATIO = re.compile(r"(?i)\b\d{1,2}\s*[:x×]\s*\d{1,2}\b|aspect ratio|比例|アスペクト")
RESOLUTION = re.compile(r"(?i)\b[248]k\b|\b(?:1080|2160|4096)p?\b|ultra[- ]?hd|uhd\b")
CAMERA = re.compile(r"(?i)\b\d{2,3}\s?mm\b|\bf/\d|\blens\b|bokeh|depth of field|\biso\s?\d|shutter|镜头|焦距|レンズ")
PLACEHOLDER = re.compile(r"\[[A-Z][A-Z0-9 _/-]{2,30}\]|\{[a-z_]{3,20}\}")          # fill-in blanks like [PRODUCT NAME]
NEGATIVE = re.compile(r"(?i)\b(no|don't|do not|avoid|without|never)\b|不要|避免|禁止|しない|なし")
KEEP = re.compile(r"(?i)\bkeep\b|\bpreserv|\bunchanged\b|\bmaintain\b|\bretain\b|\bsame (?:face|person|identity|pose|features)\b|"
                  r"\bidentical\b|保持|不变|保留|そのまま|維持")                         # what an edit must not change
FACE_LOCK = re.compile(r"(?i)\b(?:face|facial|identity|likeness)\b[^.\n]{0,60}\b(?:keep|preserv|unchanged|maintain|retain|exact|same|identical)"
                       r"|\b(?:keep|preserv|maintain|retain|exact|same|identical)\w*\b[^.\n]{0,60}\b(?:face|facial|identity|likeness)\b")
JSON_START = re.compile(r'\s*(?:\{|"[A-Za-z_][\w ]{0,30}"\s*:)')                    # a JSON object, with or without its outer brace
HOLIDAY = re.compile(r"(?i)christmas|xmas|santa|holiday|winter|snow|reindeer|圣诞|クリスマス")             # seasonal subject
REFS = re.compile(r"(?i)\b(?:image|photo|picture|reference|ref)\s*#?\s*[1-9]\b|@img\d|图\s?[1-9]|画像\s?[1-9]")  # numbered uploads


def git(*args):
    return subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, text=True, check=True).stdout


def lang(text):
    if re.search(r"[぀-ヿ]", text):
        return "ja"
    if re.search(r"[가-힯]", text):
        return "ko"
    if len(re.findall(r"[一-鿿]", text)) > 0.2 * len(re.findall(r"[A-Za-z一-鿿]", text)):
        return "zh"
    return "en"


def posted(url):
    """X status ids carry their creation time (snowflake): (id >> 22) + 1288834974657 ms."""
    m = re.search(r"/status/(\d+)", url or "")
    if not m:
        return None
    ms = (int(m.group(1)) >> 22) + 1288834974657
    return datetime.datetime.fromtimestamp(ms / 1000, datetime.timezone.utc).date()


def row(path, e):
    p = e["prompt"].strip()
    lines = [l for l in p.split("\n") if l.strip()]
    src = e.get("source", {})
    url = src.get("url", "")
    day = posted(url)
    is_json = bool(JSON_START.match(p))
    return {
        "id": e["id"],
        "model": e["model"],
        "source": "x" if "x.com/" in url else "other",
        "posted": str(day) if day else None,
        "daysAfterRelease": (day - RELEASE[e["model"]]).days if day else None,
        "mode": e.get("mode"),
        "category": e.get("category"),
        "lang": lang(p),
        "chars": len(p),
        "words": len(p.split()) if lang(p) == "en" else None,
        "lines": len(lines),
        "json": is_json,
        "labeledSections": sum(1 for l in lines if LABEL.match(l)) >= 2,
        "list": any(LIST.match(l) for l in lines),
        "aspectRatio": bool(RATIO.search(p)),
        "resolution": bool(RESOLUTION.search(p)),
        "cameraTerms": bool(CAMERA.search(p)),
        "negative": bool(NEGATIVE.search(p)),
        "placeholders": bool(PLACEHOLDER.search(p)),
        "keep": bool(KEEP.search(p)),
        "faceLock": bool(FACE_LOCK.search(p)),
        "numberedRefs": bool(REFS.search(p)),
        "holiday": bool(HOLIDAY.search(p)),
        "likes": src.get("likes"),
        "file": f"https://github.com/ai-boost/awesome-image-video-prompts/blob/main/{path}",
    }


def summary(rows):
    n = len(rows)
    if not n:
        return {"n": 0}
    pct = lambda k: round(100 * sum(1 for r in rows if r[k]) / n)
    vals = lambda k: [r[k] for r in rows if r[k] is not None]
    med = lambda k: statistics.median(vals(k)) if vals(k) else None
    count = lambda k: {v: sum(1 for r in rows if r[k] == v) for v in sorted({r[k] for r in rows})}
    out = {"n": n, "modes": count("mode"), "languages": count("lang"),
           "medianChars": med("chars"), "medianWords": med("words"), "medianLines": med("lines"),
           "pctOver1500Chars": round(100 * sum(1 for r in rows if r["chars"] > 1500) / n)}
    for k in ["holiday", "json", "labeledSections", "list", "aspectRatio", "resolution", "cameraTerms",
              "negative", "placeholders", "keep", "faceLock", "numberedRefs"]:
        out["pct" + k[0].upper() + k[1:]] = pct(k)
    out["medianLikes"] = med("likes")
    return out


def main():
    commit = git("rev-parse", "--short", "HEAD").strip()
    rows = []
    for path in sorted(git("ls-tree", "--name-only", "HEAD", "prompts/image/").split()):
        e = yaml.safe_load(git("show", f"HEAD:{path}"))
        if e.get("model") in RELEASE:
            rows.append(row(path, e))

    x = [r for r in rows if r["source"] == "x"]
    groups = {}
    for m in RELEASE:
        mine = [r for r in x if r["model"] == m]
        groups[f"{m}, creators on X"] = summary(mine)
        for mode in ["text-to-image", "image-edit"]:
            groups[f"{m}, creators on X, {mode}"] = summary([r for r in mine if r["mode"] == mode])
        # The fairest like-for-like task in both sets: put the person from an uploaded photo into a new portrait.
        groups[f"{m}, creators on X, portrait edits"] = summary([r for r in mine if r["mode"] == "image-edit" and r["category"] == "portrait"])
        # Same popularity band in both sets, to rule out that one set is just more polished.
        groups[f"{m}, creators on X, 100 to 1,000 likes"] = summary([r for r in mine if r["likes"] and 100 <= r["likes"] <= 1000])

    json.dump({
        "title": "Nano Banana Pro vs Nano Banana 2.1 prompts: per-prompt features",
        "source": f"https://github.com/ai-boost/awesome-image-video-prompts/tree/{commit}",
        "page": "https://agentcraft.dev/en/blog/nano-banana-pro-vs-2-1-prompts-2026-11/",
        "release": {m: str(d) for m, d in RELEASE.items()},
        "definitions": {
            "daysAfterRelease": "days between the release date and the post, read from the X status id",
            "holiday": "mentions Christmas, Santa, holiday, winter, snow or reindeer",
            "json": "the prompt is a JSON object, or JSON key-value pairs without the outer brace",
            "labeledSections": "two or more lines start with a short label and a colon, like Style: or Lighting:",
            "aspectRatio": "names a ratio like 3:4 or says aspect ratio",
            "resolution": "names an output size like 4K, 2K or 2160p",
            "cameraTerms": "names a lens, focal length, aperture, ISO, shutter, bokeh or depth of field",
            "negative": "contains no, don't, avoid, without or never (or the Chinese and Japanese equivalents)",
            "keep": "says what must stay: keep, preserve, unchanged, maintain, retain, identical, same face",
            "faceLock": "ties face, identity or likeness to keep, preserve, exact or same within one sentence",
            "numberedRefs": "numbers its uploads: image 1, photo 2, reference 3",
        },
        "groups": groups,
        "rows": rows,
    }, sys.stdout, ensure_ascii=False, indent=1)


if __name__ == "__main__":
    main()
