#!/usr/bin/env python3
"""debate_runner.py — 動画の台本を、ほかの AI に議論させる「議論係」.

Claude だけで決めると、同じ書き手の盲点が残る。そこで役を分けた3社に台本を読ませ、
1回目は別々に指摘、2回目はお互いの指摘に賛成か反論かを1回だけ言わせる。
採るかどうかは、最後にエイイチが決める（AI の多数決では決めない）。

役:
  Claude  … 台本の作り手。構成と流れの指摘、最後の論点表（まとめ.md）。チャットの中で書く
  Gemini  … 視聴者役。経営者・PM・開発者になりきって、飽きる所・止まる言葉を言う（API）
  codex   … 事実の番。1回目は事実確認。2回目は「直す案が事実をゆがめないか」だけを見る
             （ChatGPT のログインで動く。codex_runner.py と同じ本体を使う）

置き場所（動画/{動画名}/議論/）:
  依頼.md        Claude が書く。台本と、チャンネルの前提
  1_Claude.md    Claude が書く（1回目）
  1_codex.md     無ければ codex にかける。前の codex_検証結果 を写して置けば、それを使う
  1_Gemini.md    このスクリプトが書く
  2_Gemini.md    このスクリプトが書く（2回目）
  2_codex.md     このスクリプトが書く（2回目）
  かける.txt     印。Claude が置く。終わると かけた.txt に名前を変える

キー:
  Gemini の API キーは Mac のキーチェーン（名前 gemini_api_key）から読む。ファイルには書かない。
  入れ方: security add-generic-password -a "$USER" -s gemini_api_key -w

使い方（Mac のターミナルで）:
  python3 debate_runner.py --check      キーとモデルを確かめるだけ（議論はしない）
  python3 debate_runner.py --once       印のついた動画を1本、議論にかける
  python3 debate_runner.py --dry-run    何をするかを出すだけ
  python3 debate_runner.py --install    5分ごとに見回るように登録する（一度だけ。以後は印を置けば自動で回る）
  python3 debate_runner.py --uninstall  自動の見回りを外す

Python の標準の道具だけで動く。台本（narration.py など）には触らない。ファイルは消さない。
"""
import argparse, json, os, re, subprocess, sys, time, unicodedata, urllib.error, urllib.request
from datetime import datetime, timezone, timedelta

HERE = os.path.dirname(os.path.abspath(__file__))
VIDEOS = os.path.dirname(HERE)                       # 動画/
sys.path.insert(0, HERE)
import codex_runner as cr                            # codex の本体の場所と環境を使い回す

JST = timezone(timedelta(hours=9))
SUB = "議論"
MARK = "かける.txt"
DONE = "かけた.txt"
RUNNING = "実行中.txt"
REQ = "依頼.md"
THEMES = "_議論"          # 動画/_議論/{テーマ}/ … 台本ではなく、企画や方針を議論するとき（印と結果はフォルダの中に直接置く）
ROLE_GEMINI = "役_Gemini.md"   # テーマの議論で、Gemini の1回目の役（あれば視聴者役の代わりに使い、Google 検索も使わせる）
ROLE_CODEX = "役_codex.md"     # テーマの議論で、codex の1回目の役

KEYCHAIN_SERVICE = "gemini_api_key"
GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta"
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "")   # 空なら、使える中でいちばん上の Pro を選ぶ
GEMINI_TIMEOUT = 10 * 60

CODEX_EFFORT = "medium"                              # 2回目は検索なしで、直す案の事実だけを見る
CODEX_TIMEOUT = 30 * 60

# ───────── 共通の前提（3社に同じものを渡す） ─────────

GROUND = """\
# 議論の前提（全員に共通）

あなたは、YouTube の解説動画の台本を良くするための議論に参加しています。
参加者は Claude（台本の作り手）・Gemini（視聴者役）・codex（事実の番）の3つの AI です。
採るかどうかは、最後に人間（エイイチ）が決めます。あなたは材料を出す役です。

## 判定の軸（この4つだけで評価する）
1. ターゲットの役に立つか（ロボティクスに興味がある経営者・PM・マネージャー・開発者）
2. 最後まで分かるか（耳で聞いて分かる平易な言葉か・冗長でないか・飽きないか）
3. エイイチ（チャンネルの人）の見立て・実感が入っているか
4. 事実は正しいか（言い過ぎていないか）

## 出してはいけない提案
- 再生数を稼ぐための誇張・釣り・煽り。中身と合わないタイトルやサムネイル
- 事実を曲げる・不安をあおる・他社や人を悪く言う演出
- 「ユーモアや実体験を AI が作って足す」案（実体験は本人に聞いて入れる決まり）
再生数が伸びそうでも、上の4つの軸に反する案は出さないこと。

## 書き方
- 日本語で書く。英語の文を混ぜない
- 台本の行は「文番号（S001 など）」で指す
- 指摘には重さを付ける：【高】直さないと伝わらない・誤り ／【中】直すと良くなる ／【低】好み
- 直す案は、元の文を置き換える短い案文で出す（長い書き直しはしない）
"""

GEMINI_R1 = """\
# あなたの役：視聴者（Gemini）

次の3人になりきって、この台本の動画を最初から最後まで見たつもりで読んでください。
- 経営者：飲食チェーンの役員。ロボット導入の判断をする立場。数字とお金に敏感で、忙しい
- PM：サービスロボットの導入プロジェクトを任された人。現場とメーカーの間に立つ
- 開発者：ロボットのソフトを書く人。試算の前提の甘さに気づく

## 出してほしいもの（この順で）
1. 離脱しそうな所：3人それぞれ、どの文番号のあたりで「もういいか」と思うか。理由を1行で
2. 止まる言葉：耳で聞いて意味が取れない言葉・数字の詰め込み。文番号と言い換え案
3. 物足りない所：その人が知りたいのに出てこないこと
4. 良かった所：残すべき所（2〜3個）
5. いちばん直すべき3つ：重さ付きで

ほかの AI の意見はまだ見せていません。自分の目で見て書いてください。
"""

ROUND2 = """\
# 2回目：ほかの AI の指摘への返事（{who}）

下に、3つの AI の1回目の指摘があります（あなた自身のものも含みます）。
ほかの AI の指摘1つずつに、次のどれかで答えてください。新しい指摘は足さないこと。

- 賛成：理由を1行
- 反論：理由を1行（直すと何が悪くなるか）
- 保留：判断に人間の情報が要るもの（何を聞けばよいか）

{extra}

最後に「人間が先に決めるべき3つ」を、重さ付きで挙げてください。
話を丸くまとめようとせず、意見が割れる所は割れたまま書いてください。
"""

CODEX_EXTRA = """\
あなたは事実の番です。賛成・反論の判断は「その直す案を採ったとき、事実が正しいままか・
言い過ぎにならないか」だけで決めてください。構成・面白さ・言葉の好みには意見しないこと
（その指摘は「事実の面では問題なし」とだけ書く）。話の筋や構成を組み替える提案もしないこと。
作業フォルダの narration.py の冒頭のコメントに、数字の出どころが書いてあります。
"""

GEMINI_EXTRA = """\
あなたは視聴者役のままです。経営者・PM・開発者の目で、その直し方で見やすくなるかを判断してください。
codex（事実の番）の1回目の指摘には返事をしないこと（事実の判断は codex に任せる）。返事をするのは Claude の指摘だけ。
賛成ばかりにしないこと。視聴者として本当に見やすくなるかを考え、反論すべき所は反論してください。
"""


THEME_EXTRA = """\
賛成ばかりにしないこと。ほかの AI の数字や見立てに、根拠が弱い所・甘い所があれば反論してください。
"""

THEME_CODEX_EXTRA = """\
あなたは数字の番です。ほかの AI が出した数字（再生数の見込み・ほかのチャンネルの数字など）について、
根拠が確かか・言い過ぎていないかを中心に返事をしてください。
"""


def log(*a):
    print(datetime.now(JST).strftime("%Y-%m-%d %H:%M:%S"), *a, flush=True)


def nfc(s):
    return unicodedata.normalize("NFC", s)


def notify(msg):
    try:
        t = msg.replace("\\", "\\\\").replace('"', '\\"')
        subprocess.run(["osascript", "-e", f'display notification "{t}" with title "議論係"'],
                       capture_output=True, timeout=20)
    except Exception:
        pass


def read(path):
    b, _ = cr.read_stable(path)
    return b.decode("utf-8", errors="replace")


def write(path, text):
    with open(path, "w", encoding="utf-8") as f:
        f.write(text.rstrip() + "\n")


# ───────── Gemini ─────────

def gemini_key():
    r = subprocess.run(["/usr/bin/security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-w"],
                       capture_output=True, text=True)
    key = r.stdout.strip()
    if r.returncode != 0 or not key:
        raise RuntimeError("キーチェーンに gemini_api_key が見つかりません。"
                           " security add-generic-password -a \"$USER\" -s gemini_api_key -w で入れてください")
    return key


def http(url, key, body=None, timeout=60):
    data = json.dumps(body).encode("utf-8") if body is not None else None
    req = urllib.request.Request(url, data=data, method="POST" if data else "GET",
                                 headers={"x-goog-api-key": key, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.loads(r.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        detail = e.read().decode("utf-8", "replace")[:800]
        raise RuntimeError(f"Gemini の API がエラーを返しました（{e.code}）: {detail}")


MODEL_RE = re.compile(r"^models/(gemini-(\d+(?:\.\d+)?)-(pro|flash)(-preview(?:-\d{2}-\d{4})?)?)$")  # customtools・tts などの変わり種は外す


def pick_model(key):
    """使えるモデルの一覧から選ぶ。Pro を優先し、版の数字が大きいもの、正式版を優先"""
    if GEMINI_MODEL:
        return GEMINI_MODEL, []
    out, url = [], f"{GEMINI_BASE}/models?pageSize=1000"
    d = http(url, key)
    cands = []
    for m in d.get("models", []):
        name = m.get("name", "")
        mm = MODEL_RE.match(name)
        if not mm or "generateContent" not in m.get("supportedGenerationMethods", []):
            continue
        model, ver, kind, pre = mm.groups()
        cands.append(((kind == "pro", float(ver), not pre), model))
    cands.sort(reverse=True)
    if not cands:
        raise RuntimeError("使える Gemini のモデルが見つかりません")
    return cands[0][1], [c[1] for c in cands[:6]]


THEME_GROUND = """\
# 議論の前提（全員に共通）

あなたは、YouTube チャンネル「フィジカルAI解説｜Pixels to Motors」の企画や方針を決めるための議論に参加しています。
参加者は Claude・Gemini・codex の3つの AI です。採るかどうかは、最後に人間（エイイチ）が決めます。あなたは材料を出す役です。

- 再生数を稼ぐための誇張・釣り・煽り、中身と合わないタイトルやサムネイルは、伸びそうでも提案しない
- 日本語で書く。英語の文を混ぜない
- 数字を言うときは、根拠（実際のデータ・調べた出典・推測）のどれかを必ず添える。推測は推測と書く
- 話を丸くまとめず、ほかの AI と意見が割れる所は割れたまま書く
"""


LAST_USAGE = {"inp": 0, "out": 0}


def gemini(key, model, system, user, search=False):
    body = {
        "systemInstruction": {"parts": [{"text": system}]},
        "contents": [{"role": "user", "parts": [{"text": user}]}],
        "generationConfig": {"temperature": 0.7},
    }
    if search:
        body["tools"] = [{"google_search": {}}]
    t0 = time.time()
    d = http(f"{GEMINI_BASE}/models/{model}:generateContent", key, body, timeout=GEMINI_TIMEOUT)
    parts = (d.get("candidates") or [{}])[0].get("content", {}).get("parts", [])
    text = "\n".join(p.get("text", "") for p in parts if not p.get("thought")).strip()
    if not text:
        raise RuntimeError(f"Gemini の答えが空でした: {json.dumps(d, ensure_ascii=False)[:600]}")
    gm = (d.get("candidates") or [{}])[0].get("groundingMetadata", {})
    srcs = [c.get("web", {}) for c in gm.get("groundingChunks", []) if c.get("web")]
    if srcs:
        text += "\n\n## Gemini が検索で見た出典\n\n" + "\n".join(f"- {w.get('title', '')}: {w.get('uri', '')}" for w in srcs)
    u = d.get("usageMetadata", {})
    LAST_USAGE.update(inp=u.get("promptTokenCount", 0) or 0, out=(u.get("candidatesTokenCount", 0) or 0) + (u.get("thoughtsTokenCount", 0) or 0))
    meta = f"{'Google 検索あり・' if search else ''}モデル {model}・{int(time.time() - t0)}秒・入力 {u.get('promptTokenCount', '?')}・出力 {u.get('candidatesTokenCount', '?')} トークン"
    return text, meta


# ───────── codex ─────────

def codex(wdir, prompt, web=False):
    tmp = os.path.join(os.environ.get("TMPDIR", "/tmp"), f"debate_codex_{os.getpid()}.md")
    cmd = [cr.CODEX, "exec"] + (["-c", "tools.web_search=true"] if web else []) + ["-c", f'model_reasoning_effort="{CODEX_EFFORT}"', "-m", cr.MODEL,
           "-s", "read-only", "--skip-git-repo-check", "-C", wdir, "-o", tmp, "-"]
    t0 = time.time()
    r = subprocess.run(cmd, input=prompt, text=True, encoding="utf-8", capture_output=True,
                       timeout=CODEX_TIMEOUT, env=cr.env_for_codex(), cwd=wdir)
    ans = ""
    if os.path.isfile(tmp):
        with open(tmp, encoding="utf-8", errors="replace") as f:
            ans = f.read().strip()
        os.remove(tmp)
    if r.returncode != 0 or not ans:
        raise RuntimeError(f"codex が失敗しました（終了コード {r.returncode}）:\n" + "\n".join((r.stderr or "").splitlines()[-30:]))
    tk = re.findall(r"tokens used\s*\n?\s*([\d,]+)", r.stderr or "")
    return ans, f"モデル {cr.MODEL}・考える深さ {CODEX_EFFORT}・{int(time.time() - t0)}秒・{tk[-1] if tk else '?'} トークン"


# ───────── 1本分 ─────────

def head(title, name, meta):
    return f"# {title}（{name}）\n\n- 日時: {datetime.now(JST).strftime('%Y-%m-%d %H:%M')}（JST）\n- {meta}\n\n---\n\n"


def run_one(name, vdir, dry, ddir=None):
    theme = ddir is not None
    ddir = ddir or os.path.join(vdir, SUB)
    vdir = vdir or ddir
    files = cr.listdir_nfc(ddir)
    p = lambda n: os.path.join(ddir, files.get(n, n))
    if REQ not in files:
        log(f"× {name}: {SUB}/{REQ} がありません")
        return
    req = read(p(REQ))
    todo = [f for f in ("1_Gemini.md", "1_codex.md", "2_Gemini.md", "2_codex.md") if f not in files]
    log(f"▶ {name}: これから作るもの → {'・'.join(todo) or 'なし'}")
    if "1_Claude.md" not in files:
        log("  注意: 1_Claude.md がありません（Claude 抜きで進めます）")
    if dry:
        return

    write(p(RUNNING), f"開始 {datetime.now(JST).strftime('%Y-%m-%d %H:%M:%S')}\nPID {os.getpid()}\n")
    try:
        key = gemini_key()
        model, _ = pick_model(key)

        # 1回目
        ground = THEME_GROUND if theme else GROUND
        if "1_Gemini.md" not in files:
            log(f"  1回目 Gemini（{model}）…")
            if theme and ROLE_GEMINI in files:
                ans, meta = gemini(key, model, ground + "\n" + read(p(ROLE_GEMINI)), req, search=True)
            else:
                ans, meta = gemini(key, model, GROUND + "\n" + GEMINI_R1, req)
            write(p("1_Gemini.md"), head("1回目 Gemini（視聴者役）", name, meta) + ans)
            files["1_Gemini.md"] = "1_Gemini.md"
        if "1_codex.md" not in files:
            log("  1回目 codex（事実確認）…")
            if theme and ROLE_CODEX in files:
                ans, meta = codex(vdir, ground + "\n" + read(p(ROLE_CODEX)) + "\n\n" + req, web=True)
            else:
                ans, meta = codex(vdir, GROUND + "\n# あなたの役：事実の番（codex）\n台本の事実確認をしてください。"
                                  "観点は事実と言い過ぎだけ。筋・構成・見立ては変えない。\n\n" + req, web=True)
            write(p("1_codex.md"), head("1回目 codex（事実の番）", name, meta) + ans)
            files["1_codex.md"] = "1_codex.md"

        # 2回目（3つの1回目をまとめて見せる）
        r1 = ""
        labels = (("Claude", "1_Claude.md"), ("Gemini", "1_Gemini.md"), ("codex", "1_codex.md")) if theme else \
                 (("Claude（台本の作り手）", "1_Claude.md"), ("Gemini（視聴者役）", "1_Gemini.md"), ("codex（事実の番）", "1_codex.md"))
        for who, f in labels:
            if f in files:
                r1 += f"\n\n==================== {who} の1回目 ====================\n\n" + read(p(f))
        body2 = ("# 依頼\n\n" if theme else "# 台本と前提\n\n") + req + "\n\n# 1回目の意見" + r1
        if "2_Gemini.md" not in files:
            log("  2回目 Gemini…")
            if theme:
                ans, meta = gemini(key, model, ground + "\n" + ROUND2.format(who="Gemini", extra=THEME_EXTRA), body2)
            else:
                ans, meta = gemini(key, model, GROUND + "\n" + ROUND2.format(who="Gemini・視聴者役", extra=GEMINI_EXTRA), body2)
            write(p("2_Gemini.md"), head("2回目 Gemini", name, meta) + ans)
        if "2_codex.md" not in files:
            log("  2回目 codex…")
            if theme:
                ans, meta = codex(vdir, ground + "\n" + ROUND2.format(who="codex", extra=THEME_CODEX_EXTRA) + "\n\n" + body2)
            else:
                ans, meta = codex(vdir, GROUND + "\n" + ROUND2.format(who="codex・事実の番", extra=CODEX_EXTRA) + "\n\n" + body2)
            write(p("2_codex.md"), head("2回目 codex", name, meta) + ans)
    except Exception as e:
        log(f"× {name}: {e}")
        write(p("エラー.txt"), f"{datetime.now(JST).strftime('%Y-%m-%d %H:%M:%S')}\n\n{e}\n")
        notify(f"議論が止まりました：{name}")
        return
    finally:
        if os.path.exists(p(RUNNING)):
            os.remove(p(RUNNING))

    if os.path.exists(p(MARK)):
        os.rename(p(MARK), p(DONE))
    log(f"✓ {name}: 議論が終わりました → 1_・2_ のファイル。次は Claude がまとめる番")
    notify(f"議論が終わりました：{name}")


def research(dry):
    """動画/_調べ/{名前}/ に かける.txt があれば、YouTube を検索して数字を取る（youtube_market.py）"""
    real = cr.listdir_nfc(VIDEOS).get(nfc("_調べ"))
    if not real:
        return False
    root = os.path.join(VIDEOS, real)
    for name, r in sorted(cr.listdir_nfc(root).items()):
        d = os.path.join(root, r)
        f = cr.listdir_nfc(d) if os.path.isdir(d) else {}
        if MARK in f and "キーワード.txt" in f:
            log(f"▶ YouTube の調べ _調べ/{name}")
            if dry:
                return True
            try:
                import youtube_market
                n = youtube_market.run(d)
                os.rename(os.path.join(d, f[MARK]), os.path.join(d, DONE))
                log(f"✓ _調べ/{name}: {n} 語を調べました → 結果.md")
                notify(f"YouTube の調べが終わりました：{name}")
            except Exception as e:
                write(os.path.join(d, "エラー.txt"), f"{datetime.now(JST).strftime('%Y-%m-%d %H:%M:%S')}\n\n{e}\n")
                os.rename(os.path.join(d, f[MARK]), os.path.join(d, "失敗.txt"))
                log(f"× _調べ/{name}: {e}")
            return True
    return False


def sweep(dry):
    if research(dry):
        return
    import neta                                      # 動画/_ネタ/ … ネタ出し（neta.py）
    if neta.sweep(dry):
        return
    import seisaku                                   # 動画/_制作/ … 制作チーム（seisaku.py）
    if seisaku.sweep(dry):
        return
    import kikaku                                    # 動画/_企画/ … 100回の企画会議（kikaku.py）
    if kikaku.sweep(dry):
        return
    troot = cr.listdir_nfc(VIDEOS).get(nfc(THEMES))
    if troot:
        tdir = os.path.join(VIDEOS, troot)
        for name, real in sorted(cr.listdir_nfc(tdir).items()):
            d = os.path.join(tdir, real)
            if not os.path.isdir(d):
                continue
            f = cr.listdir_nfc(d)
            if RUNNING in f:
                log(f"いま動いています: {THEMES}/{name}（今回は何もしません）")
                return
            if MARK in f:
                run_one(f"{THEMES}/{name}", None, dry, ddir=d)
                return
    for name, real in sorted(cr.listdir_nfc(VIDEOS).items()):
        if name.startswith("_"):
            continue
        vdir = os.path.join(VIDEOS, real)
        sub = cr.listdir_nfc(vdir).get(nfc(SUB))
        if not sub:
            continue
        f = cr.listdir_nfc(os.path.join(vdir, sub))
        if RUNNING in f:
            log(f"いま動いています: {name}（今回は何もしません）")
            return
        if MARK in f:
            run_one(name, vdir, dry)
            return                                   # 一度に1本だけ
    log("印（議論/かける.txt・_議論/{テーマ}/かける.txt）のついたものはありません")


LABEL = "jp.adaptdesign.pixelstomotors.debate-runner"
PLIST = os.path.expanduser(f"~/Library/LaunchAgents/{LABEL}.plist")
LOGFILE = os.path.join(cr.CONF, "debate_runner.log")


def install():
    """5分ごとに見回るように登録する。印（かける.txt）を置けば、ターミナルを開かなくても議論が回る"""
    os.makedirs(os.path.dirname(PLIST), exist_ok=True)
    os.makedirs(cr.CONF, exist_ok=True)
    cal = "".join(f"<dict><key>Minute</key><integer>{m}</integer></dict>" for m in range(2, 60, 5))
    plist = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>{LABEL}</string>
  <key>ProgramArguments</key><array><string>{sys.executable}</string><string>{os.path.abspath(__file__)}</string><string>--once</string></array>
  <key>StartCalendarInterval</key><array>{cal}</array>
  <key>RunAtLoad</key><true/>
  <key>WorkingDirectory</key><string>{HERE}</string>
  <key>EnvironmentVariables</key><dict>
    <key>PATH</key><string>/usr/bin:/bin:/usr/sbin:/sbin</string>
    <key>CODEX_HOME</key><string>{cr.CODEX_HOME}</string>
    <key>LANG</key><string>ja_JP.UTF-8</string>
  </dict>
  <key>StandardOutPath</key><string>{LOGFILE}</string>
  <key>StandardErrorPath</key><string>{LOGFILE}</string>
</dict></plist>
"""
    with open(PLIST, "w") as f:
        f.write(plist)
    subprocess.run(["launchctl", "unload", PLIST], capture_output=True)
    r = subprocess.run(["launchctl", "load", PLIST], capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(f"登録できませんでした: {r.stderr.strip()}")
    print(f"登録しました。5分ごとに見回ります（いまも1回動きます）。記録: {LOGFILE}")


def uninstall():
    subprocess.run(["launchctl", "unload", PLIST], capture_output=True)
    if os.path.exists(PLIST):
        os.remove(PLIST)
    print("自動の見回りを外しました")


def check():
    key = gemini_key()
    log(f"キー: 見つかりました（頭 {key[:4]}…・{len(key)} 文字）")
    model, top = pick_model(key)
    log(f"選んだモデル: {model}")
    if top:
        log("候補の上位:", "・".join(top))
    ans, meta = gemini(key, model, "日本語で、ひとことで答えてください。", "動作確認です。「準備できました」とだけ返してください。")
    log(f"Gemini の返事: {ans}（{meta}）")
    log(f"codex の本体: {'あります' if os.path.isfile(cr.CODEX) else '見つかりません'}（{cr.CODEX}）")


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="動画の台本を、Gemini と codex に議論させる")
    ap.add_argument("--check", action="store_true", help="キーとモデルを確かめるだけ")
    ap.add_argument("--once", action="store_true", help="印のついた動画を1本、議論にかける")
    ap.add_argument("--dry-run", action="store_true", help="何をするかを出すだけ")
    ap.add_argument("--install", action="store_true", help="5分ごとに見回るように登録する")
    ap.add_argument("--uninstall", action="store_true", help="自動の見回りを外す")
    a = ap.parse_args()
    try:
        if a.install:
            install()
        elif a.uninstall:
            uninstall()
        elif a.check:
            check()
        else:
            sweep(a.dry_run)
    except Exception as e:
        log("×", e)
        sys.exit(1)
