"""GPT Image 2.6 vs Nano Banana 2.1: how each vendor's guide and each model's creators write prompts, and what an image costs.

Reproduces https://agentcraft.dev/data/gpt-image-2-6-vs-nano-banana-2-1-prompts-2026-11.json.

Sources:
- the public prompt collection https://github.com/ai-boost/awesome-image-video-prompts (one YAML file per prompt):
  OpenAI's GPT Image 2.6 guide examples and creators' posts on X for both models;
- Google's Nano Banana image generation guide, https://aistudio.google.com/docs/image-generation, whose example
  prompts (the "Prompt" tabs, not the fill-in templates) are read from the live page.

  git clone https://github.com/ai-boost/awesome-image-video-prompts
  git -C awesome-image-video-prompts checkout 590b532
  pip install pyyaml
  python3 gpt-image-2-6-vs-nano-banana-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 source.
"""
import datetime
import html
import json
import math
import re
import statistics
import subprocess
import sys
import urllib.request
from pathlib import Path

import yaml

REPO = Path(sys.argv[1] if len(sys.argv) > 1 else "awesome-image-video-prompts")
MODELS = {"GPT Image 2.6": datetime.date(2026, 11, 4), "Nano Banana 2.1": datetime.date(2026, 10, 6)}
GOOGLE_GUIDE = "https://aistudio.google.com/docs/image-generation"

LABEL = re.compile(r"^\s*[\[【]?[\w一-鿿぀-ヿ /&()-]{2,30}[\]】]?\s*[:：]")
QUOTED = re.compile(r'"[^"\n]{1,60}"|“[^”\n]{1,60}”|\'[A-Z][^\'\n]{1,60}\'|「[^」\n]{1,60}」')   # short quoted strings
RATIO = re.compile(r"(?i)\b\d{1,2}\s*[:x×]\s*\d{1,2}\b|aspect ratio|vertical|horizontal|portrait orientation|比例|アスペクト")
CAMERA = re.compile(r"(?i)\b\d{2,3}\s?mm\b|\bf/\d|\blens\b|bokeh|depth of field|\biso\s?\d|shutter|wide-angle|close-up|macro|镜头|レンズ")
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|\bonly\b|\bidentical\b|保持|不变|そのまま|維持")
USE = re.compile(r"(?i)\b(?:poster|logo|advert|ad\b|banner|infographic|diagram|product (?:photo|shot)|thumbnail|sticker|mockup|menu|label|cover|e-commerce)")
JSON_START = re.compile(r'\s*(?:\{|"[A-Za-z_][\w ]{0,30}"\s*:)')


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):
    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 features(p):
    p = p.strip()
    lines = [l for l in p.split("\n") if l.strip()]
    is_json = bool(JSON_START.match(p))
    return {
        "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,
        "quotedText": bool(QUOTED.search(p)) and not is_json,
        "aspectRatio": bool(RATIO.search(p)),
        "cameraTerms": bool(CAMERA.search(p)),
        "negative": bool(NEGATIVE.search(p)),
        "keep": bool(KEEP.search(p)),
        "namesUse": bool(USE.search(p)),
    }


def google_guide():
    """Example prompts from Google's guide: each 'Prompt' tab, with the section it sits in."""
    req = urllib.request.Request(GOOGLE_GUIDE, headers={"User-Agent": "Mozilla/5.0"})
    page = html.unescape(urllib.request.urlopen(req, timeout=60).read().decode("utf-8", "ignore"))
    rows = []
    for m in re.finditer(r'\{% tab label="Prompt" %\}\s*```none\s*(.*?)```', page, re.S):
        section = (re.findall(r'id="([^"]+)"', page[:m.start()]) or ["-"])[-1]
        text = re.sub(r"\s+", " ", m.group(1)).strip().strip('"')
        edit = bool(re.search(r"(?i)provided (?:image|photograph)|this rough|this man|first image", text))
        rows.append({"id": f"google-guide-{section}", "model": "Nano Banana 2.1", "source": "google_guide",
                     "mode": "image-edit" if edit else "text-to-image", "gif": False, "posted": None,
                     **features(text), "file": f"{GOOGLE_GUIDE}#{section}"})
    return rows


def collection():
    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") not in MODELS:
            continue
        src = e.get("source", {})
        url = src.get("url", "")
        source = "openai_guide" if "OpenAI" in src.get("name", "") else ("x" if "x.com/" in url else "other")
        day = posted(url)
        rows.append({"id": e["id"], "model": e["model"], "source": source, "mode": e.get("mode"),
                     "gif": "gif" in (e.get("tags") or []), "posted": str(day) if day else None,
                     "daysAfterRelease": (day - MODELS[e["model"]]).days if day else None,
                     **features(e["prompt"]),
                     "file": f"https://github.com/ai-boost/awesome-image-video-prompts/blob/main/{path}"})
    return rows


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
    out = {"n": n, "medianChars": med("chars"), "medianWords": med("words"), "medianLines": med("lines"),
           "pctGif": pct("gif"), "pctEdit": round(100 * sum(1 for r in rows if r["mode"] == "image-edit") / n),
           "languages": {l: sum(1 for r in rows if r["lang"] == l) for l in sorted({r["lang"] for r in rows})}}
    for k in ["json", "labeledSections", "quotedText", "aspectRatio", "cameraTerms", "negative", "keep", "namesUse"]:
        out["pct" + k[0].upper() + k[1:]] = pct(k)
    return out


# ---- price per image, from each vendor's published method

def openai_tokens(width, height, quality):
    """OpenAI's GPT Image 2.6 output-token formula, as used by the calculator on
    https://developers.openai.com/api/docs/guides/image-generation (checked 2026-11-07)."""
    grid = {"low": 16, "medium": 24, "high": 48, "xhigh": 64, "max": 96}[quality]
    factor = {"low": 4.2, "medium": 3.3, "high": 1, "xhigh": 1, "max": 1}[quality]
    long_side, short_side = max(width, height), min(width, height)
    s = grid / (long_side / short_side)
    lo = math.floor(s)
    other = lo + lo % 2 if s - lo == 0.5 else round(s)
    cells = (grid if width >= height else other) * (other if width >= height else grid)
    return round(math.ceil(cells * (2e6 + width * height) / 4e6) * factor)


def prices():
    out = {"GPT Image 2.6": {}, "Nano Banana 2.1": {"1K": 0.0336, "2K": 0.0504, "4K": 0.113}}
    # 2560x1440 is the largest size OpenAI does not call experimental; 3840x2160 is its experimental 4K.
    for label, (w, h) in {"1024x1024": (1024, 1024), "2560x1440": (2560, 1440), "3840x2160 (experimental)": (3840, 2160)}.items():
        for q in ["low", "medium", "high"]:
            t = openai_tokens(w, h, q)
            out["GPT Image 2.6"][f"{label} {q}"] = {"outputTokens": t, "usd": round(t * 30 / 1e6, 5)}
    return out


def main():
    commit = git("rev-parse", "--short", "HEAD").strip()
    rows = collection() + google_guide()
    x = lambda m: [r for r in rows if r["model"] == m and r["source"] == "x"]
    still_en = lambda rs: [r for r in rs if not r["gif"] and r["lang"] == "en"]
    groups = {
        "OpenAI guide, GPT Image 2.6": summary([r for r in rows if r["source"] == "openai_guide" and r["model"] == "GPT Image 2.6"]),
        "Google guide, Nano Banana": summary([r for r in rows if r["source"] == "google_guide"]),
        "GPT Image 2.6 creators on X": summary(x("GPT Image 2.6")),
        "GPT Image 2.6 creators on X, GIFs": summary([r for r in x("GPT Image 2.6") if r["gif"]]),
        "GPT Image 2.6 creators on X, English still images": summary(still_en(x("GPT Image 2.6"))),
        "Nano Banana 2.1 creators on X": summary(x("Nano Banana 2.1")),
        "Nano Banana 2.1 creators on X, English still images": summary(still_en(x("Nano Banana 2.1"))),
        "Nano Banana 2.1 creators on X, first 3 days": summary([r for r in x("Nano Banana 2.1") if r["daysAfterRelease"] is not None and r["daysAfterRelease"] <= 2]),
    }
    json.dump({
        "title": "GPT Image 2.6 vs Nano Banana 2.1 prompts: per-prompt features and price per image",
        "source": f"https://github.com/ai-boost/awesome-image-video-prompts/tree/{commit}",
        "googleGuide": GOOGLE_GUIDE,
        "page": "https://agentcraft.dev/en/blog/gpt-image-2-6-vs-nano-banana-2-1-prompts-2026-11/",
        "release": {m: str(d) for m, d in MODELS.items()},
        "definitions": {
            "labeledSections": "two or more lines start with a short label and a colon, like Style: or Lighting:",
            "quotedText": "quotes a short string, the usual way to ask for exact words in the image; JSON prompts are never counted",
            "aspectRatio": "names a ratio like 3:4, or says aspect ratio, vertical or horizontal",
            "cameraTerms": "names a lens, focal length, aperture, ISO, shutter, bokeh, depth of field, wide-angle, close-up or macro",
            "negative": "contains no, don't, avoid, without or never (or the Chinese and Japanese equivalents)",
            "keep": "says what must stay or be the only change: keep, preserve, unchanged, maintain, retain, only, identical",
            "namesUse": "names what the image is for: poster, logo, ad, banner, infographic, diagram, product photo, sticker, mockup, label, cover",
            "price": "GPT Image 2.6: output tokens from OpenAI's formula at $30 per million image output tokens, input not included; Nano Banana 2.1: Google's per-image price",
        },
        "groups": groups,
        "pricePerImage": prices(),
        "rows": rows,
    }, sys.stdout, ensure_ascii=False, indent=1)


if __name__ == "__main__":
    main()
