"""Kling 4.0 vs Seedance 2.5 prompts: how each model's prompts are written.

Reproduces https://agentcraft.dev/data/kling-4-vs-seedance-2-5-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 kling-4-vs-seedance-2-5-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")
MODELS = ["Kling 4.0", "Seedance 2.5", "MiniMax H3", "Wan 3.0"]

# Feature definitions. Each is a plain test on the prompt text, so anyone can argue with it.
NUM = r"\d{1,3}(?:\.\d+)?"
SEG = re.compile(rf"(?<![\d.:]){NUM}\s*(?:s|sec|secs|seconds|秒)?\s*[-–—~]\s*{NUM}\s*(?:s|sec|secs|seconds|秒)(?![a-z])"
                 r"|\b\d{1,2}:\d{2}\s*[-–—~]\s*\d{1,2}:\d{2}\b", re.I)                # 0-3s, 3–6 sec, 00:00-00:03
SHOT = re.compile(r"(?i)\b(?:shot|scene|cut|beat|keyframe)\s*#?\d{1,2}\b|镜头\s*\d|分镜\s*\d|カット\s*\d")  # Shot 1, Scene 2
SEG_END = re.compile(rf"(?<![\d.:]){NUM}\s*(?:s|sec|secs|seconds|秒)?\s*[-–—~]\s*({NUM})\s*(?:s|sec|secs|seconds|秒)(?![a-z])", re.I)  # end of a range
DURATION = re.compile(rf"(?i)\b{NUM}\s*[- ]?(?:s|sec|secs|seconds?)\b(?!\s*[-–—~])|{NUM}\s*秒")   # 15 seconds, 20-second
REF = re.compile(r"(?i)@\s?(?:image|video|element|audio)\s?\d*|\b(?:reference|input|uploaded)\s+(?:image|video|photo)|\bimage\s?\d\b|图\s?\d|参考图")
COUNT_LOCK = re.compile(r"(?i)\bexact(?:ly)?\s+(?:\d+|one|two|three|four|five)\b|no duplicates")       # EXACTLY 1 CHARACTER, NO DUPLICATES
LABEL = re.compile(r"^\s*[\[【]?[\w一-鿿぀-ヿ /&()-]{2,30}[\]】]?\s*[:：|]|^\s*[【\[][^\]】]{2,20}[\]】]")  # "Camera: ..." or 【Overall Style】
QUOTED_SPEECH = re.compile(r'(?i)(?:says?|said|asks?|whispers?|shouts?|yells?|replies|mutters|dialogue|line)\b[^"“\n]{0,40}["“]|台词|说[:：]|对白')
AUDIO = re.compile(r"(?i)\baudio\b|\bsounds?\b|\bsfx\b|\bambient\b|\bmusic\b|\bsoundtrack\b|\bvoice(?:over)?\b|音效|配乐|音乐|背景音")
NEGATIVE = re.compile(r"(?i)\b(no|don't|do not|avoid|without|never)\b|不要|避免|禁止|しない|なし")
RATIO = re.compile(r"(?i)\b(?:16|9|21|4|3|1)\s*:\s*(?:9|16|4|3|1)\b|aspect ratio|vertical|horizontal|竖屏|横屏")
CAMERA = re.compile(r"(?i)close[- ]?ups?\b|\btracking\b|\bwide shots?\b|\bhand[- ]?held\b|\bslow[- ]?mo(?:tion)?\b|\bdolly\b|"
                    r"\bpush(?:es|ing)?[- ]in\b|\bpull(?:s|ing)?[- ]?(?:back|out)\b|\borbit|\bpan(?:s|ning)?\b|\btilt|\bcrane\b|"
                    r"\baerial\b|\bdrone\b|\bpov\b|\bfpv\b|\bzoom|\bstatic (?:shot|camera)|\blow[- ]angle\b|\bmedium shots?\b|"
                    r"\bwhip[- ]?pan|over[- ]the[- ]shoulder|\bmacro\b|\blong take\b|\bone take\b|推镜|拉镜|摇镜|跟拍|特写|航拍|手持|长镜头")


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", "")
    source_type = "kling_guide" if "kling.ai/" in url else ("x" if "x.com/" in url else "other")
    segs = len(SEG.findall(p))
    ends = [float(m.group(1)) for m in SEG_END.finditer(p)]
    shots = len(set(m.group(0).lower().replace(" ", "") for m in SHOT.finditer(p)))
    day = posted(url)
    return {
        "id": e["id"],
        "model": e["model"],
        "source": source_type,
        "posted": str(day) 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),
        "timedSegments": segs,
        "timed": segs >= 2,
        "timedEndSeconds": max(ends) if segs >= 2 and ends else None,
        "shotLabels": shots,
        "multiShot": segs >= 2 or shots >= 2,
        "duration": bool(DURATION.search(p)),
        "referenceTags": bool(REF.search(p)),
        "atTags": bool(re.search(r"(?i)@\s?(?:image|video|element|audio)", p)),
        "countLock": bool(COUNT_LOCK.search(p)),
        "labeledSections": sum(1 for l in lines if LABEL.match(l)) >= 2,
        "speech": bool(QUOTED_SPEECH.search(p)),
        "audio": bool(AUDIO.search(p)),
        "negative": bool(NEGATIVE.search(p)),
        "aspectRatio": bool(RATIO.search(p)),
        "cameraTerms": bool(CAMERA.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})}
    timed = [r["timedSegments"] for r in rows if r["timed"]]
    return {
        "n": n,
        "modes": count("mode"),
        "languages": count("lang"),
        "medianChars": med("chars"),
        "medianWords": med("words"),
        "medianLines": med("lines"),
        "pctOver2000Chars": round(100 * sum(1 for r in rows if r["chars"] > 2000) / n),
        "pctTimed": pct("timed"),
        "medianSegmentsWhenTimed": statistics.median(timed) if timed else None,
        "medianTimedEndSeconds": med("timedEndSeconds"),
        "pctMultiShot": pct("multiShot"),
        "pctDuration": pct("duration"),
        "pctReferenceTags": pct("referenceTags"),
        "pctAtTags": pct("atTags"),
        "pctCountLock": pct("countLock"),
        "pctLabeledSections": pct("labeledSections"),
        "pctSpeech": pct("speech"),
        "pctAudio": pct("audio"),
        "pctNegative": pct("negative"),
        "pctAspectRatio": pct("aspectRatio"),
        "pctCameraTerms": pct("cameraTerms"),
        "medianLikes": med("likes"),
    }


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

    groups = {}
    for m in MODELS:
        mine = [r for r in rows if r["model"] == m]
        x = [r for r in mine if r["source"] == "x"]
        groups[f"{m}, all"] = summary(mine)
        groups[f"{m}, creators on X"] = summary(x)
        groups[f"{m}, creators on X, English"] = summary([r for r in x if r["lang"] == "en"])
        for mode in ["text-to-video", "reference-to-video"]:
            groups[f"{m}, creators on X, English, {mode}"] = summary([r for r in x if r["lang"] == "en" and r["mode"] == mode])
        if m == "Kling 4.0":
            guide = [r for r in mine if r["source"] == "kling_guide"]
            groups["Kling 4.0, Kling's user guide"] = summary(guide)
            groups["Kling 4.0, Kling's user guide, video-edit"] = summary([r for r in guide if r["mode"] == "video-edit"])
            groups["Kling 4.0, Kling's user guide, other modes"] = summary([r for r in guide if r["mode"] != "video-edit"])
        # Posts from the same weeks as the Kling 4.0 creator set, to rule out timing.
        groups[f"{m}, creators on X, posted 2026-10-16 or later"] = summary([r for r in x if r["posted"] and r["posted"] >= "2026-10-16"])

    json.dump({
        "title": "Kling 4.0 vs Seedance 2.5 prompts: per-prompt features",
        "source": f"https://github.com/ai-boost/awesome-image-video-prompts/tree/{commit}",
        "page": "https://agentcraft.dev/en/blog/kling-4-vs-seedance-2-5-prompts-2026-11/",
        "definitions": {
            "timed": "two or more time ranges like 0-3s, 3–6 sec or 00:00-00:03",
            "timedEndSeconds": "for a timed prompt, the latest end second of any range written in seconds, like 27-30s (mm:ss ranges are not read), i.e. the length the shot list plans for",
            "multiShot": "timed, or two or more distinct labels like Shot 1, Scene 2, Keyframe 3",
            "duration": "states a length like 15 seconds or a 20-second video",
            "referenceTags": "points at an input: @Image1, @Video 1, @Element1, reference image, Image 1",
            "atTags": "uses the @Image / @Video / @Element / @Audio tag form",
            "countLock": "says EXACT or EXACTLY N (characters) or NO DUPLICATES",
            "labeledSections": "two or more lines start with a short label and a colon, or a bracketed header like 【Overall Style】",
            "speech": "a speech verb or the word dialogue before a quoted line",
            "audio": "mentions audio, sound, sfx, ambient, music, soundtrack or voice",
            "negative": "contains no, don't, avoid, without or never (or the Chinese and Japanese equivalents)",
            "aspectRatio": "names a ratio like 16:9 or says vertical / horizontal",
            "cameraTerms": "names a camera move or framing from a fixed list (close-up, tracking, dolly, orbit, long take...)",
        },
        "groups": groups,
        "rows": rows,
    }, sys.stdout, ensure_ascii=False, indent=1)


if __name__ == "__main__":
    main()
