#!/usr/bin/env python3
"""
wan-flf2v.py — 最初と最後のコマを渡して、その間を Wan 3.0 に作らせる（FLF2V）

  同じ画像を両端に渡す  → 始まりと終わりがそろう = ループ素材
  写真 → ロゴ          → ロゴで終わるトランジション
  ロゴ → 写真          → ロゴから始まるオープニング
  --last を省略         → 最初のコマだけ（カメラの動きを素直に出したいとき）

使い方:
  export AICU_API_KEY=...      # video スコープの鍵
  python3 wan-flf2v.py --first trophy.jpg --last trophy.jpg --out loop.mp4 \
      --prompt "The white cloth ripples gently ... Fixed camera."            # 見積もりだけ
  python3 wan-flf2v.py ... --apply                                            # 生成

標準ライブラリだけで動きます。AP は開始時に預かり、失敗したら全額戻ります。
"""
import argparse, base64, json, os, sys, time, urllib.request, urllib.error

BASE = os.environ.get("AICU_BASE_URL", "https://api.aicu.ai/v1")
AP_PER_SEC = {"480p": 700, "720p": 1400, "1080p": 2700}  # wan3.0-video の公開単価（見積もり表示用）


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


def b64(path):
    return base64.b64encode(open(path, "rb").read()).decode()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--first", required=True)
    ap.add_argument("--last", help="省略すると最初のコマだけ渡す（画像から動画）")
    ap.add_argument("--prompt", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--model", default="wan3.0-video")
    ap.add_argument("--resolution", default="720p")
    ap.add_argument("--seconds", type=int, default=5)
    ap.add_argument("--apply", action="store_true")
    a = ap.parse_args()

    est = AP_PER_SEC.get(a.resolution, 0) * a.seconds
    print(f"{a.model} {a.resolution} {a.seconds} 秒・見積もり {est:,} AP（wan3.0-video の単価で計算）")
    if not a.apply:
        print("dry-run です。生成は --apply")
        return
    key = os.environ.get("AICU_API_KEY") or sys.exit("AICU_API_KEY がありません")

    t0 = time.time()
    body = {"model": a.model, "prompt": a.prompt, "first_frame": b64(a.first),
            "resolution": a.resolution, "seconds": a.seconds}
    if a.last:
        body["last_frame"] = b64(a.last)
    for _ in range(60):  # 429 = 1 本の鍵で 3 本走っている。空くまで待つ
        code, job = call("POST", f"{BASE}/video/generate", key, body)
        if code != 429 or job.get("error") == "key_daily_cap":
            break
        time.sleep(20)
    if code not in (200, 202):
        sys.exit(f"開始できませんでした: HTTP {code} {json.dumps(job, ensure_ascii=False)}")
    print(f"受け付け: {job['id']}（{job.get('ap_cost')} AP を預かり）")

    while True:
        time.sleep(10)
        _, st = call("GET", f"{BASE}/video/status/{job['id']}", key)
        if st.get("status") in ("completed", "failed"):
            break
    if st["status"] != "completed":
        sys.exit(f"失敗しました（AP は戻ります）: {json.dumps(st, ensure_ascii=False)}")

    req = urllib.request.Request(f"{BASE}/video/file/{job['id']}", headers={"Authorization": f"Bearer {key}"})
    with urllib.request.urlopen(req, timeout=300) as r, open(a.out, "wb") as f:
        f.write(r.read())
    print(f"完了: {a.out}・{round(time.time() - t0)} 秒・{st.get('ap_cost')} AP")
    print(json.dumps({k: st.get(k) for k in ("model", "upstream_model", "resolution", "seconds", "ap_cost")},
                     ensure_ascii=False))


if __name__ == "__main__":
    main()
