#!/usr/bin/env python3
"""
wan-teaser-10.py — 1 枚の画像から Wan 3.0 でティザー動画を 10 パターン作る

ブログ「トロフィー画像を Wan 3.0 でティザー動画にする」の再現用台本。
プロンプトは Alibaba Cloud Model Studio の公式ガイドの式に沿って書いてある:
  https://www.alibabacloud.com/help/en/model-studio/text-to-video-prompt
  - 画像から動画: Prompt = Motion + Camera movement（被写体・場面・画風は画像が決める）
  - 音: Entity + Scene + Motion + Sound description（声・効果音・BGM）。台詞を止めるなら "No dialogue."
  - マルチショット: 全体の説明 + Shot N [時刻] + 内容

使い方:
  export AICU_API_KEY=...            # video スコープの鍵
  python3 wan-teaser-10.py --image trophy.jpg --out out/            # dry-run（費用の見積もりだけ）
  python3 wan-teaser-10.py --image trophy.jpg --out out/ --apply    # 生成
  python3 wan-teaser-10.py ... --apply --only 03,07                  # 一部だけ

冪等: out/<id>.mp4 が既にあるパターンは投げない。結果は out/results.json に追記する。
同時に走らせるのは 3 本まで（API の 1 鍵あたりの上限）。
"""
import argparse, base64, json, os, sys, time, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor

BASE = os.environ.get("AICU_BASE_URL", "https://api.aicu.ai/v1")
MODEL = "wan3.0-video"
AP_PER_SEC_720P = 1400  # 公開単価（docs/pricing.md）。見積もり表示用

PATTERNS = [
    {"id": "01", "title": "依頼文をそのまま", "theme": "（比較の基準）", "seconds": 5,
     "prompt": "A teaser video that evokes shimmering radiance, wind, influence, intelligence, history, "
               "authority, creativity, and contribution to the community."},
    {"id": "02", "title": "基本の式（動き＋カメラ）", "theme": "輝き", "seconds": 5,
     "prompt": "Sunlight shimmers across the facets of the crystal trophy, and the white cloth ripples gently "
               "in the breeze. The camera slowly pushes in."},
    {"id": "03", "title": "ゆらめく輝き", "theme": "ゆらめく輝き", "seconds": 5,
     "prompt": "Golden caustic light dances and ripples across the white cloth around the trophy, and a soft "
               "lens flare pulses at its tip. Backlight, rim light, warm tones. Fixed camera."},
    {"id": "04", "title": "風", "theme": "風", "seconds": 5,
     "prompt": "A gust of wind sweeps across the scene: the white cloth billows in slow waves, the leaves behind "
               "sway, and dappled light flickers over the crystal. The camera moves slowly to the left."},
    {"id": "05", "title": "権威（ローアングル）", "theme": "権威", "seconds": 5,
     "prompt": "The trophy stands still and solemn as the light grows stronger along its edges. Low-angle shot, "
               "the camera slowly pushes in and tilts up toward the glowing tip. Side light, high contrast."},
    {"id": "06", "title": "歴史（タイムラプス）", "theme": "歴史", "seconds": 5,
     "prompt": "Time-lapse photography: the shadow of the trophy sweeps slowly across the white cloth as the "
               "light shifts from bright morning white to deep sunset gold. Fixed camera."},
    {"id": "07", "title": "知性（光の回路）", "theme": "知性", "seconds": 5,
     "prompt": "Inside the crystal, fine lines of light trace and branch like a network, rising slowly from the "
               "base to the tip, while refracted light glints across its facets. Orbiting camera movement, a slow "
               "arc of less than 30 degrees."},
    {"id": "08", "title": "コミュニティ（広がる光）", "theme": "コミュニティへの貢献・影響力", "seconds": 5,
     "prompt": "Small sparks of golden light spread outward from the base of the trophy across the cloth like "
               "ripples, multiplying as they travel toward the edges of the frame. The camera slowly pulls out "
               "to reveal the whole scene."},
    {"id": "09", "title": "音の式（効果音＋BGM）", "theme": "クリエイティビティ", "seconds": 5,
     "prompt": "The white cloth ripples in the wind and light shimmers across the crystal trophy. Sound: wind "
               "rustling through the leaves, a clear crystal chime as a flare passes the tip, and a slow, rising "
               "orchestral score with strings and soft brass. No dialogue."},
    {"id": "10", "title": "マルチショット（10 秒）", "theme": "全部", "seconds": 10,
     "prompt": "A teaser for an international award, in three shots. Shot 1 [0-3 s]: close-up, light glides "
               "along the crystal edge of the trophy, fixed camera. Shot 2 [3-6 s]: medium shot, the white cloth "
               "ripples in the wind and golden caustics dance around the base, the camera moves left. Shot 3 "
               "[6-10 s]: the camera pulls out and rises to reveal the trophy standing in sunlight against the "
               "green trees, and a lens flare blooms at its tip. Sound: soft wind, a crystal chime, an orchestral "
               "swell. No dialogue."},
]


def call(method, url, key, body=None):
    req = urllib.request.Request(url, method=method, headers={"Authorization": f"Bearer {key}",
                                                               "Content-Type": "application/json"})
    data = json.dumps(body).encode() if body is not None else None
    try:
        with urllib.request.urlopen(req, data=data, timeout=120) as r:
            return r.status, json.loads(r.read() or b"{}")
    except urllib.error.HTTPError as e:
        try:
            return e.code, json.loads(e.read() or b"{}")
        except Exception:
            return e.code, {}


def run(p, key, image_b64, out):
    t0 = time.time()
    body = {"model": MODEL, "prompt": p["prompt"], "first_frame": image_b64,
            "resolution": "720p", "seconds": p["seconds"]}
    for attempt in range(30):  # 429 = 3 本走っている。空くまで待つ
        code, job = call("POST", f"{BASE}/video/generate", key, body)
        if code != 429:
            break
        time.sleep(20)
    if code not in (200, 202):
        return {**p, "ok": False, "http": code, "error": job}
    jid = job["id"]
    while True:
        time.sleep(10)
        code, st = call("GET", f"{BASE}/video/status/{jid}", key)
        if st.get("status") in ("completed", "failed"):
            break
        if time.time() - t0 > 1800:
            return {**p, "ok": False, "id": jid, "error": "timeout(30min)"}
    elapsed = round(time.time() - t0)
    if st.get("status") != "completed":
        return {**p, "ok": False, "id": jid, "status": st, "elapsed_s": elapsed}
    req = urllib.request.Request(f"{BASE}/video/file/{jid}", headers={"Authorization": f"Bearer {key}"})
    with urllib.request.urlopen(req, timeout=300) as r, open(os.path.join(out, f"{p['id']}.mp4"), "wb") as f:
        f.write(r.read())
    keep = {k: st.get(k) for k in ("model", "upstream_model", "resolution", "seconds", "ap_cost")}
    return {**p, "ok": True, "id_job": jid, "elapsed_s": elapsed, **keep}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--image", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--only", default="")
    ap.add_argument("--apply", action="store_true")
    a = ap.parse_args()
    os.makedirs(a.out, exist_ok=True)
    todo = [p for p in PATTERNS if (not a.only or p["id"] in a.only.split(","))
            and not os.path.exists(os.path.join(a.out, f"{p['id']}.mp4"))]
    est = sum(p["seconds"] * AP_PER_SEC_720P for p in todo)
    print(f"{len(todo)} 本・{MODEL} 720p・見積もり {est:,} AP（約 ${est / 10000:.2f}）")
    for p in todo:
        print(f"  {p['id']} {p['title']}（{p['seconds']} 秒）")
    if not a.apply:
        print("dry-run です。生成は --apply")
        return
    key = os.environ.get("AICU_API_KEY") or sys.exit("AICU_API_KEY がありません")
    image_b64 = base64.b64encode(open(a.image, "rb").read()).decode()
    path = os.path.join(a.out, "results.json")
    results = json.load(open(path)) if os.path.exists(path) else []
    with ThreadPoolExecutor(max_workers=3) as ex:
        for r in ex.map(lambda p: run(p, key, image_b64, a.out), todo):
            print(("✓" if r["ok"] else "✗"), r["id"], r["title"], r.get("elapsed_s"), "s", r.get("ap_cost"),
                  "" if r["ok"] else json.dumps(r.get("error") or r.get("status"), ensure_ascii=False)[:200],
                  flush=True)
            results = [x for x in results if x["id"] != r["id"]] + [r]
            json.dump(results, open(path, "w"), ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()
