Claude CodeでAI動画量産ツールを育てる──設定UI・品質ガード・自動量産ページまでの実装ログ
はじめに
MoneyPrinterTurboのフォークとして開発しているReelm(日本語特化・エラー解説チャンネル自動生成ツール)を、Claude Codeと二人三脚で育てたセッションの記録です。
「ボタンは存在するが繋がっていない」「設定ページがあるのにたどり着けない」といった実用上の詰まりを1つずつ解消しながら、最終的にはジャンルと本数を選んで1クリックで複数動画をレビュー待ちまで量産するページまで到達しました。
やったこと
1. 設定ページが「存在するのに見えない」問題
最初のフィードバックは「設定場所がない」。APIキー入力フォームは実装済みでした。調査すると、brand.py の CSS が Streamlit 標準ナビを丸ごと隠していました。
/* 修正前:stSidebarNav を display:none していた */
section[data-testid="stSidebar"] [data-testid="stSidebarNav"] {
display: none;
}
旧UI(カスタムボタンナビ)向けの CSS が st.navigation() ベースの新UIでそのまま生きていたのが原因。1行を display: block に変えるだけで全ページのリンクが現れました。
2. YouTubeチャプターの10秒ルール対応
YouTube のチャプター有効化条件は「先頭 0:00・3個以上・各チャプター10秒以上」。実データを確認すると、0:00/0:06/0:26/0:30 という間隔6秒・4秒のチャプターが生成されていました。
render_spec.py に _enforce_min_section_duration() を追加し、各セクションの末尾シーンをホールドして最低尺を保証しています。
def _enforce_min_section_duration(scenes, min_sec, fps):
"""各セクション最後のシーンを延長して最低尺を確保する"""
...
# ナレーションは流し切り、余白のみ延長 → 同期は崩れない
また _min_section_sec() に「0を10に潰す」バグがあり(get(..., 10) or 10 )、テストで min_section_sec=0 を指定してホールドを無効化できなかったので合わせて修正しました。
3. LLMプロバイダに Anthropic を追加
Gemini が JSON出力に不安定だったため、Anthropic Claude をネイティブ追加しました。Claude APIスキルで確認したとおり、モデルIDは 日付サフィックスなし(claude-sonnet-4-6)が正しいです。
elif llm_provider == "anthropic":
import anthropic
client = anthropic.Anthropic(api_key=api_key)
resp = client.messages.create(
model=model_name,
max_tokens=4096,
messages=[{"role": "user", "content": prompt}],
)
text = "".join(
getattr(block, "text", "")
for block in resp.content
if getattr(block, "type", "") == "text"
)
Gemini と同じ LLMKeyManager を流用した複数キーのローテーション、AuthenticationError/RateLimitError の検知・クールダウンも実装しました。
4. 誠実性ガード+品質・安全ガード
実機で判明した2つの問題をプロンプトとコードの両面で対策しました。
問題①(誠実性):No module named requests に対して、LLMが pip install requests を禁止されていたため偽のスタブモジュールを自作してPASSさせていた。
問題②(安全性):CERTIFICATE_VERIFY_FAILED に対して --trusted-host pypi.org という SSL検証を無効化する回避策をfixとして出力していた。
DRAFTER_SYSTEM_PROMPT に2段階のルールを追記し、コード側にも軽量ガードを追加しました。
_UNSAFE_FIX_PATTERNS = re.compile(
r"--trusted-host|--no-check-certificate|curl\s+-k\b|verify\s*=\s*false|"
r"NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|chmod\s+777|ssl\._create_unverified",
re.IGNORECASE,
)
def _looks_like_unsafe_fix(fix: str, reproduce: str = "") -> bool:
"""危険な回避策パターンを簡易検出"""
return bool(_UNSAFE_FIX_PATTERNS.search(fix + reproduce))
検出時は _redraft_unsafe() でシステム指示を一段強化して1回だけ作り直します。
5. HTMLエンティティの根絶
StackOverflow API のタイトルは HTML エスケープされて返ってきます(" / & 等)。これが error_text に入り、動画フレームに Error "ImportError..." と表示されていました。
対策は2層構成:
# 源流(stackoverflow.py)
title = normalize_text(item.get("title")) # html.unescape + 連続空白圧縮
# チョークポイント(error_script.build_script)
vs.error_text = normalize_text(vs.error_text)
vs.topic_keywords = _clean_keywords(vs.topic_keywords)
# ↑ この1箇所でタイトル/タグ/説明/焼き込み全てがクリーンになる
target_keywords が発掘時点で生のエスケープ文字列からトークン化されていたため quot というタグが混入していた問題も、チョークポイントで _clean_keywords() を通すことで対応しました。
6. YouTube 400エラー(禁止文字 <>)
Java の heredoc(cat > Hello.java << 'EOF')が説明欄に入り、< と > がYouTube禁止文字として弾かれ HttpError 400 invalidDescription が発生。
即効薬:アップロード境界でのサニタイズ
def sanitize_for_youtube(text: str) -> str:
"""YouTube禁止文字を全角に変換(<>)+ 長さ制限"""
text = text.replace("<", "<").replace(">", ">")
return text[:5000] # description 上限
根本対策:コピペ手順からシェル足場を除去
def _format_paste_block(script: str) -> str:
"""heredoc ブロックを「📄 PATH を作成:」表記に整形"""
# cat > PATH << 'EOF' ... EOF を検出・分解
# 実行コマンドはそのまま残す
<<'EOF' / <<"EOF" / << EOF の表記揺れ、複数ブロック、前置き mkdir && にも対応しています。映る手順(display_code)は逐語のまま維持します。
7. 自動量産ページ
既存の auto_pipeline.run_auto() をUIから呼ぶページ webui/studio/auto.py を追加しました。run_auto には既に on_progress(topic_id, stage) コールバックがあったため、パイプライン本体の改変ゼロで進捗表示を実現できました。
- PASSしたネタのみレビュー待ちキューに投入(FAIL/INCONCLUSIVE は理由付きスキップ)
- 発掘数のハードキャップ(20件)で暴走防止
-
このページからは公開しない(
stop_at="review"でキュー投入のみ)
ハマったポイント
shallow clone でのgit push失敗
remote: fatal: did not receive expected object 0703b0c4
--no-thin / gc / repack -adf を試しても解決せず。最終的に .git/shallow の境界コミットの親オブジェクト 0703b0c4 がローカルに存在しない shallow clone 問題でした。
git remote add upstream https://github.com/harry0703/MoneyPrinterTurbo.git
git fetch --unshallow upstream
git push -u origin feature/jp-customizations
shallow clone は --no-thin では直りません。unshallow が唯一の解決策でした。
or 演算子でゼロが潰れる
min_section_sec = config.get("min_section_sec", 10) or 10 は、設定値が 0 のときに 0 を偽として 10 を返してしまいます。テストで min_section_sec=0 を指定したらホールドが無効化されなかったことで気づきました。
# 修正後
val = config.get("min_section_sec", 10)
return float(10 if val is None else val)
環境依存テストの汚染
ローカルの config.toml(gitignore対象)に実際の channel.url が設定されているため、test_empty_urls_are_omitted が継続的に失敗していました。
# 修正後:明示的に空channelを渡して環境非依存に
build_description(..., channel={"name": "エラーの処方箋"})
学び
個人の感想として、今回のセッションを通じて感じたのは「AIコーディングアシスタントとの共同作業では、実装品質よりも"到達可能性"が先に詰まる」という点です。設定ページは完璧に実装されていたのに、CSSが1行ナビを隠していて誰も使えなかった──これはコードレビューでは見逃しやすい種類のバグです。
品質ガードについては、「プロンプトで禁止する」と「コードで検出して再要求する」の二層構成が効果的でした。どちらか一方では漏れが出ます。プロンプトは「意図」を伝え、コードは「万が一」を拾う役割分担が機能しています。
また normalize_text() のような共通正規化関数を base.py に置いて源流・チョークポイント・表示の3箇所で使う「多層防御」パターンは、キャッシュ済みデータへの後方互換対応にも有効でした。