#!/usr/bin/env python3 """neta.py — ネタ出し係。いま騒がれている人・出来事を拾い、エイイチの経験とつなぎ、事実を確かめた候補を20本出す. 制作チーム(seisaku.py)で、題材を AI が考え出すと面白くならなかったため、題材は「考え出す」のをやめて 「いま騒がれているものを拾う」ことにした。3つの AI は持つ材料を分ける。 置き場所: 動画/_ネタ/{名前}/ かける.txt 印。見回り(debate_runner.py)がこれを見て動かす。終わると かけた.txt 止める.txt 置くと止まる 依頼.md (なくてよい)足したい注文 流れ: 0_急上昇.md YouTube の日本の急上昇50本(Data API の mostPopular) 1_騒ぎ_Gemini.md Gemini(Google 検索):日本で直近7日に騒がれている人・出来事 15件(急上昇も材料にする) 1_騒ぎ_codex.md codex(Web 検索):海外で直近7日に騒がれ、日本にも届きそうな AI・テック・ロボットの騒ぎ 10件 2_候補_Claude.md Claude:騒ぎの中から、エイイチにしか言えない角度があるものを選び、遊びの形にした候補 20本 (ここで止まり、Claudeの番.txt を置く) 候補.md codex(Web 検索):候補ごとに事実を確かめ、判定と直しをつけた最終の表 進み具合.md・エラー.txt・状態.json """ import json, os, time from datetime import datetime, timedelta, timezone import debate_runner as dr cr = dr.cr ROOT = "_ネタ" MARK, DONE, STOP, RUNNING = "かける.txt", "かけた.txt", "止める.txt", "実行中.txt" JIJI = """\ # 時事ネタとは(エイイチが決めたお手本) 楽待の動画「【超知能AI】2030年にも『人類絶滅』か、元OpenAI・Anthropic研究者の警告」(2026-11-14公開、当日約1.4万回)。 - 題材は、いま騒ぎの真ん中にいる人・出来事そのもの。少し古い発表を検索で見つけてきたものではない - その人自身の言葉(発言・投稿・会見)がある - 日本の人に身近な問いに引き寄せている(スカイネット、ドラえもん) 海外Yパパラジオの例:「炎を呼んだ男ファイアー鈴木の話」(2時間で2.4万回)、「どの企業トップの辞め方が一番恥ずかしいか?(恥ずかしい辞任コンテスト)」(15時間で19万回)。 いま騒がれている人を、本人の経験と遊びの形(コンテスト・ランキング・一言)で読む。 """ GEMINI_TASK = JIJI + """ # あなたの仕事:日本で、いま騒がれている人・出来事を15件拾う - 期間は今日から数えて直近7日。それより古いものは入れない(発表日・発生日を必ず確かめる) - 分野は問わない(芸能・経済・事件・事故・企業・政治家の発言・SNSの炎上・スポーツ・AI)。ただし AI・テック・ロボットに関わるものを4件以上入れる - 下の「YouTube の急上昇」も材料にしてよい(そこから拾ったら、そう書く) - 考え出さない。実際に起きて、実際に騒がれているものだけ - 1件ずつ、次の形で書く: ### {番号}. {人・出来事の名前} - 何が起きた:1〜2行 - 本人の言葉:本人の発言・投稿の短い引用(なければ「なし」) - なぜ騒がれている:1行 - 日付:YYYY-MM-DD - 出典:URL(報道・公式。まとめサイトは不可) """ CODEX_COLLECT = JIJI + """ # あなたの仕事:海外で、いま騒がれている AI・テック・ロボットの人・出来事を10件拾う - 期間は今日から数えて直近7日。それより古いものは入れない(日付を必ず確かめる) - 日本でもこれから話題になりそうなもの(有名な CEO・研究者の発言、事故、訴訟、大きな発表、炎上、人型ロボットの映像など) - 考え出さない。実際に起きて、実際に騒がれているものだけ。必ず Web で確かめる - 1件ずつ、次の形で書く: ### {番号}. {人・出来事の名前} - 何が起きた:1〜2行(日本語) - 本人の言葉:本人の発言・投稿の短い引用(日本語訳と原文。なければ「なし」) - なぜ騒がれている:1行 - 日付:YYYY-MM-DD - 出典:URL """ CLAUDE_TASK = JIJI + """ # あなたの仕事(Claude):騒ぎの中から、エイイチにしか言えない角度があるものを選び、候補を20本作る - 材料は下の 1_騒ぎ_Gemini.md と 1_騒ぎ_codex.md。新しい題材を考え出さない - エイイチの経験:ファミレスのホール、ロボットの研究と開発(机のロボットアーム・π0.5の学習・配膳ロボットの試算)、29年のデザイナー、アパレルの量産で遠い工場に仕様を渡してきた、RCカーの会社、(地名)での暮らし、生成AIで仕事が減った - 形は遊んでよい(コンテスト・ランキング・判定・ハコブくんの一言)。ただし釣り・誇張・中身と合わない題名はしない - ロボット以外の AI・時事は全体の30%までという決まりは、ここでは数えない(最後に選ぶときに数える) - 1本ずつ、次の形で書く(1本は5行以内): ### {番号}. {題名} - サムネイル:大きな文字2〜3行(/で区切る)+ハコブくんの一言 - 騒ぎ:材料の番号と名前 - エイイチの角度:1行(どの経験から、何が言えるか) - 見せるもの:1行(撮る・作るもの。2〜3時間で撮れるもの) """ CHECK_TASK = """\ # あなたの仕事:候補20本の事実を確かめ、最終の表を作る - 候補ごとに、題名・サムネイル・騒ぎの事実が出典と合っているかを Web で確かめる - 判定は4つのどれか:確認済/言い過ぎ(直せば使える)/間違い/未確認 - 直しは、事実を合わせるための最小限だけ。角度・形・面白さは変えない(面白くするのはあなたの仕事ではない) - 出力は次の表1つと、表の下に「直した所」の箇条書きだけ: | 番号 | 題名(直したならあとの文) | サムネイル | 判定 | 日付 | 出典 | """ def path(d, n): return os.path.join(d, n) def exists(d, n): return os.path.isfile(path(d, n)) def read(d, n): return dr.read(path(d, n)) def now(): return datetime.now(dr.JST).strftime("%Y-%m-%d %H:%M") def doc(d, name, title, meta, body): dr.write(path(d, name), f"# {title}\n\n- 日時: {now()}(JST)\n- {meta}\n\n---\n\n{body}") def load(d): p = path(d, "状態.json") if os.path.isfile(p): return json.load(open(p, encoding="utf-8")) return {"cost_in": 0, "cost_out": 0, "codex": 0, "start": now()} def save(d, st): with open(path(d, "状態.json"), "w", encoding="utf-8") as f: json.dump(st, f, ensure_ascii=False, indent=1) def progress(d, st, what): usd = st["cost_in"] / 1e6 * 2.0 + st["cost_out"] / 1e6 * 12.0 steps = [("0_急上昇.md", "YouTube の急上昇"), ("1_騒ぎ_Gemini.md", "日本の騒ぎ(Gemini)"), ("1_騒ぎ_codex.md", "海外の騒ぎ(codex)"), ("2_候補_Claude.md", "候補20本(Claude)"), ("候補.md", "事実の確かめ(codex)")] L = [f"# 進み具合(ネタ出し:{os.path.basename(d)})", "", f"- いま:**{what}**", f"- 始めた時刻 {st['start']}・最後に書いた時刻 {now()}", f"- Gemini の費用の目安 約{usd * 155:,.0f}円・codex {st['codex']} 回(ChatGPT Plus の枠)", ""] L += [f"- [{'x' if exists(d, f) else ' '}] {label}({f})" for f, label in steps] dr.write(path(d, "進み具合.md"), "\n".join(L)) def trending(): import youtube_market as ym import youtube_schedule as ys tok = ys.access_token() r = ym.api("videos", tok, part="snippet,statistics", chart="mostPopular", regionCode="JP", maxResults=50) L = [f"YouTube 日本の急上昇({now()} 取得)", ""] for i, v in enumerate(r.get("items", []), 1): s, st = v["snippet"], v.get("statistics", {}) L.append(f"{i}. {s.get('title', '')}|{s.get('channelTitle', '')}|{int(st.get('viewCount', 0)):,}回|{s.get('publishedAt', '')[:10]}") return "\n".join(L) def run(name, d, dry): st = load(d) save(d, st) if dry: return dr.log(f"[試し]ネタ出し {name}") dr.write(path(d, RUNNING), f"開始 {now()}\nPID {os.getpid()}\n") flag = path(d, "Claudeの番.txt") try: if os.path.exists(flag): if exists(d, "2_候補_Claude.md"): os.remove(flag) else: return dr.log(" ネタ出し:まだ Claude の番です") extra = ("\n\n# エイイチからの注文\n\n" + read(d, "依頼.md")) if exists(d, "依頼.md") else "" today = f"\n\n今日は {datetime.now(dr.JST).strftime('%Y-%m-%d')}(JST)。" if not exists(d, "0_急上昇.md"): progress(d, st, "YouTube の急上昇を取っています") try: body = trending() except Exception as e: body = f"(急上昇は取れませんでした: {e})" doc(d, "0_急上昇.md", "YouTube の急上昇", "YouTube Data API(chart=mostPopular・regionCode=JP)", body) if not exists(d, "1_騒ぎ_Gemini.md"): progress(d, st, "Gemini が日本の騒ぎを拾っています") key = dr.gemini_key() model, _ = dr.pick_model(key) ans, meta = dr.gemini(key, model, GEMINI_TASK + today + extra, read(d, "0_急上昇.md"), search=True) st["cost_in"] += dr.LAST_USAGE["inp"] st["cost_out"] += dr.LAST_USAGE["out"] doc(d, "1_騒ぎ_Gemini.md", "日本で騒がれている人・出来事(Gemini)", meta, ans) save(d, st) if not exists(d, "1_騒ぎ_codex.md"): progress(d, st, "codex が海外の騒ぎを拾っています") ans, meta = dr.codex(d, CODEX_COLLECT + today + extra, web=True) st["codex"] += 1 doc(d, "1_騒ぎ_codex.md", "海外で騒がれている AI・テック・ロボット(codex)", meta, ans) save(d, st) if not exists(d, "2_候補_Claude.md"): dr.write(path(d, "Claudeへ.md"), CLAUDE_TASK + today + extra + "\n\n# 材料\n\n" + read(d, "1_騒ぎ_Gemini.md") + "\n\n" + read(d, "1_騒ぎ_codex.md")) progress(d, st, "Claude の番:候補20本(Claudeへ.md を読む・2_候補_Claude.md を待っています)") if not os.path.exists(flag): dr.write(flag, f"{now()}\n候補20本(Claudeへ.md を読む)\n2_候補_Claude.md を置けば続きます\n") dr.notify(f"ネタ出し {name}:Claude の番") return if not exists(d, "候補.md"): progress(d, st, "codex が候補の事実を確かめています") body = (CHECK_TASK + today + "\n\n# 候補\n\n" + read(d, "2_候補_Claude.md") + "\n\n# 材料\n\n" + read(d, "1_騒ぎ_Gemini.md") + "\n\n" + read(d, "1_騒ぎ_codex.md")) ans, meta = dr.codex(d, body, web=True) st["codex"] += 1 doc(d, "候補.md", "候補(事実を確かめた最終の表)", meta, "見たいものに ○ をつけてください(番号だけチャットで伝えるのでもよい)。\n\n" + ans) save(d, st) if os.path.exists(path(d, MARK)): os.rename(path(d, MARK), path(d, DONE)) progress(d, st, "終わり。候補.md を見て、見たいものに ○ をつけてください") dr.notify(f"ネタ出しが終わりました:{name}") dr.log(f"✓ ネタ出し {name}") except Exception as e: dr.write(path(d, "エラー.txt"), f"{now()}\n\n{e}\n") progress(d, st, "エラーで止まりました(エラー.txt)。次の見回りで続きから試します") dr.log(f"× ネタ出し {name}: {e}") finally: save(d, st) if os.path.exists(path(d, RUNNING)): os.remove(path(d, RUNNING)) def sweep(dry): real = cr.listdir_nfc(dr.VIDEOS).get(dr.nfc(ROOT)) if not real: return False root = os.path.join(dr.VIDEOS, real) for name, r in sorted(cr.listdir_nfc(root).items()): d = os.path.join(root, r) if not os.path.isdir(d): continue f = cr.listdir_nfc(d) if RUNNING in f: if time.time() - os.path.getmtime(path(d, f[RUNNING])) < 3 * 3600: dr.log(f"ネタ出し {name} が動いています") return True os.remove(path(d, f[RUNNING])) if MARK in f and STOP not in f: if "Claudeの番.txt" in f and "2_候補_Claude.md" not in f: continue # Claude を待っている間は、ほかの仕事を回す dr.log(f"▶ ネタ出し {ROOT}/{name}") run(name, d, dry) return True return False