0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code × Streamlit:AIチャンネル自動生成ツールを自走させるまでの実装ログ

0
Posted at

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箇所で使う「多層防御」パターンは、キャッシュ済みデータへの後方互換対応にも有効でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?