HANAKIN配信システムの技術構成

2026年7月31日、
5thシングル「HANAKIN/シャニカマー」の発売週に、
10時間のYouTube生配信をやりました。
企画は「みんなと協力して家にあるものでMV再現配信」。
視聴者がCDを買ってハッシュタグで投稿するたびに、メイク・衣装・編集アプリが解禁されていく参加型の配信です。

配信アーカイブはこちら




この記事は、
その配信を支えたシステムの技術的な中身を
書き残したものです。
企画そのものや、
コードが書けない私がどうやって作ったのかについては、本編に書いています。

▶ 本編:コードが書けないアイドルが、AIと一緒に10時間配信のシステムを全部作った話https://ameblo.jp/miyamotokarin-official/entry-12974432505.html



先に正直に書いておくと、
この記事に出てくる用語を、
私は完全には理解していません。

実装はすべてClaude Codeに投げていて、
コードは1行も読んでいないので、
「なぜこう書くのか」を説明できるわけではないんです。
それでも、何がどこで動いていて、
なぜその置き場所を選んだのか、
当日どこでつまずいたのか─
そこは自分で判断してきたので、
分かる範囲で書き残しておきます。

用語は、AIに教えてもらいながら
「たぶんこういうこと」と噛み砕いた理解のまま使っています。
技術に詳しい方が読んだら、
粗いところや言葉の使い方が違うところがあると思います。
それでも、
コードが書けない人間がここまで作れたという記録として、
そのまま残しておきたいと思いました。

全体構成

構成はざっくりこうなっています。

配信画面と管理画面

スマホ / PC

管理画面(/admin)

Cloudflare Workers

Cloudflare KV

配信用アカウントのOBSブラウザソース

/obs(HUD)を表示

Xの投稿数を取得する流れ

RapidAPI

X検索API

Cloudflare Workers

Cloudflare KVにカウントを保存

OBSのHUDに反映

AI判定アプリの流れ

開発用アカウント

AI判定アプリ

Node.js + Express

localhost:3000で動作

同じMac内の配信用アカウントからアクセス

OBSブラウザソースに表示

表示用の画面は

localhost:3000/obs.html

カメラや画面キャプチャも、
配信用アカウントのOBSにまとめました。

技術選定の方針は最初から一貫していて、
「7/31に確実に動くこと」を最優先にしました。
美しい設計より、当日壊れないこと。
凝ったフレームワークを入れず、
素のHTML + Vanilla JSで組んだのもそのためです。ビルド工程がない分、壊れる箇所が減ります。

バックエンド:Cloudflare Workers + KV

サーバーサイドはCloudflare Workers(TypeScript)で書き、状態はWorkers KVに持たせました。

KVに置いているキー

supply_total
SUPPLYの表示件数

hype_total
HYPEの表示件数

now_text
「今やってること」テロップ

ai_score
AI判定の再現率

telop_list
ローテーションテロップ(配列)

unlock_supply
次の解放アイテム名・必要件数

unlock_hype
HYPE側の次の解放アイテム名・必要件数

stream_start_at
配信開始時刻(タイマー用)

last_poll
X取得の最終実行時刻

用意したAPI

GET /api/state
全状態を返すAPIです。HUDが5秒ごとに取得します。

POST /api/manual/supply
SUPPLYを手動で増減します。

POST /api/manual/hype
HYPEを手動で増減します。

POST /api/text
テロップ・AI判定スコアを更新します。

POST /api/config
解放アイテム名・閾値を更新します。

POST /api/poll
X検索を実行してKVを更新します。

書き込み系は全部
 Authorization: Bearer トークン で保護しています。

KVに状態を集約したのが、
この構成の肝でした。
理由は2つ。
ひとつは本番でスマホから数字を動かしたかったこと。
もうひとつは、
開発用と配信用でmacOSのユーザーアカウントを分けているので、
ローカルに状態を持つとアカウントまたぎで詰むこと。
KVを単一の情報源にしたことで、
どの端末・どのアカウントから叩いても整合が取れるようになりました。

HUDと管理画面のHTMLも、
最終的にはWorkerから配信する形(/obs、/admin)に寄せました。
当初はローカルのHTMLファイルをOBSのブラウザソースで直接読ませていたのですが、開発用アカウントで修正するたびに共有フォルダ経由でコピーし直す運用になっていて、事故の温床でした。
Worker配信に切り替えてからは wrangler deploy 一発で全端末に反映されます。

CORSでハマった話

最初に踏んだのがこれ。
管理画面からPOSTしたら、こう言われました。

Access to fetch at '...' from origin 'null' has been blocked by CORS policy:
Request header field authorization is not allowed by
Access-Control-Allow-Headers in preflight response

Authorizationヘッダーを付けたことでプリフライト(OPTIONS)が飛び、
Worker側がAccess-Control-Allow-Headersを返していなかったので弾かれていました。
OPTIONSに対して適切なヘッダーを返すようにして解決。
原因ごとエラーメッセージを貼って投げたら、そのまま直してもらえました。

X(Twitter)の投稿数カウント

ここが一番難しかったところです。

X公式APIは個人で持つには高額なので、
RapidAPI経由のサードパーティAPI(twitter241)を使いました。
月1ドルのBasicプランで、
月1,000リクエストのハードリミット付き。
超過課金が発生しない点も、
当日の安心材料として大きかったです。

叩いているのは検索エンドポイントです。

検索エンドポイント

GET https://twitter241.p.rapidapi.com/search-v2

指定している内容

type
Latest

count
20

query
エンコード済みハッシュタグ

送っているヘッダー

X-RapidAPI-Key
キー

X-RapidAPI-Host
twitter241.p.rapidapi.com

レスポンスからツイートのエントリだけを抽出して件数を数えています。
当初は result_count を見る実装だったのですが、
このAPIはレスポンス構造が違ったので、
entryIdが tweet- で始まるものを数える形に変えました。

当初は10分ごとのCron Triggerで自動ポーリングする設計でした。が、ここで壁にぶつかります。

Cron triggers are not supported on the Workers Free plan

Cronは有料プラン(月5ドル)が必要でした。
ここで方針転換して、
Cronを捨てて手動トリガー方式にしました。
管理画面に「Xから取得」ボタンを置いて、
押したときだけ最新件数を取りに行く形です。

結果的にこれは悪くない判断でした。
10時間配信で10分おきに自動ポーリングすると
1日180リクエスト前後になりますが、
手動なら押したときだけ。
リクエスト消費を配信の盛り上がりに合わせてコントロールできます。
盛り上がっている時間帯は5分おき、
そうでない時間帯は間隔を空ける、
といった運用ができました。

取得処理はエラーを握りつぶして、
失敗してもKVの前回値を維持するようにしています。
APIが落ちてもゲージが突然0に落ちない。
生配信では、正しさより「変な挙動をしないこと」のほうが大事でした。

なお count=20 は上限に張り付きやすく、
実際の投稿数を正確に反映しきれない場面がありました。ページング(カーソル)で複数ページ取得する実装も入れましたが、精度の面では今後の課題です。

フェイルセーフ設計

生配信は一発勝負なので、
あらゆる自動処理に手動フォールバックを用意しました。

X APIでの件数取得
管理画面から手動で増減・直接上書き

AI採点
スコアとコメントを手入力してHUDに反映

解放アイテムの段階管理
「次の解放」だけを管理画面から都度書き換え

特に3つ目は運用上かなり効きました。
当初は4段階の閾値をシステムに持たせていたのですが、
当日の伸び方は読めません。
段階表は手元の台本で管理して、
システム側は「次に解放するもの1つ」だけを持つ設計に変更。
これで配信中に
「思ったより伸びてるから閾値を上げる」
といった判断ができるようになりました。

フロントエンド(HUD / 管理画面)

HUDは1920×240程度の横長レイアウトで、
OBSのブラウザソースとして読み込んでいます。
5秒間隔でサーバーに問い合わせて描画を更新する、シンプルなつくりです。




実装した表示要素はこのあたり。

・SUPPLY / HYPE のセグメント式ゲージ
・次の解放アイテム名と残り件数
・AI判定の再現率
・配信タイマーと現在時刻
・ローテーションテロップ(8秒ごとにフェード切り替え)
・閾値超過時の解放演出(全画面フラッシュ+バナー)




テロップは文字数によって溢れるので、
描画時に実測して自動でフォントサイズを縮小、
それでも収まらなければ末尾を省略する処理を入れました。
管理画面側にも文字数カウンターを置いて、
入力時点で警告が出るようにしています。
二重で保険をかけた形です。

AI判定アプリ(ローカル)

MV再現度の採点だけは、
Cloudflareではなくローカルで動かしました。
Node.js + Expressで小さなサーバーを立てて、
ブラウザで開く構成です。

処理の流れはこう。

1.
本家MVと再現MV(MP4)をアップロード

2.
ffmpegで両方から一定間隔のフレームを静止画として抽出

3.
同じタイムコードのフレームをペアにしてVision APIに送信

4.
「構図 / ポーズ / 衣装 / 背景 / 照明 / 雰囲気」の6項目+総合点+コメントをJSONで返させる

5.
SSEで採点結果を1シーンずつストリーミングして画面に描画

SSEにしたのは配信映えのためです。
全部終わってからドンと結果が出るより、
「シーン1を審査中...」「92点」と1件ずつ積み上がっていくほうが、画面として面白い。
AIが考えている過程が見えることに意味がありました。




ローカルに置いた理由は、
動画という重いデータを直接扱う必要があったからです。
書き出したファイルをそのまま読ませられるので、
アップロード待ちがない。

このアプリには画面を2つ用意しました。

操作用画面
http://localhost:3000
動画を読み込んで判定を実行する画面

表示用画面
http://localhost:3000/obs.html
最新の総合点とコメントだけを出す画面

操作画面


判定画面





操作画面をそのままOBSに映すと、
ファイル選択欄やアップロードのUIまで配信に乗ってしまいます。
かといって判定結果は視聴者に見せたい。
なので同じサーバーから、
OBS表示専用のページを別に配信する構成にしました。横長のバナー状レイアウトで、
最新1件の総合点と辛口コメントだけを大きく表示します。

ここでも例のアカウント分離問題が出ましたが、
localhostは同一マシン内であれば
ユーザーアカウントをまたいで到達できるので、
開発用アカウントでサーバーを起動しておけば、
配信用アカウントのOBSブラウザソースから
同じアドレスを指定するだけで画面を取り込めました。
ファイルを配って回る必要がない。

AIプロバイダを乗り換えた話

当初はClaude APIで実装していました。が、
APIクレジットの購入が何度やっても決済失敗します。

・請求先住所を漢字からローマ字に修正しても失敗
・同じカードでRapidAPIの決済は通る

つまりカード側の海外決済ブロックではなく、
決済のリスク判定で弾かれている。
自力での突破は不可能でした。

残り日数を考えて見切りをつけ、
OpenAI APIに乗り換えました。
こちらは5ドルのクレジット購入があっさり通過。
実装の変更は、エンドポイントとリクエスト形式の差し替えだけで済みました。
画像2枚を渡してJSONで採点結果を返させる、という設計自体は変えていません。
プロバイダに依存しない作りにしておいたのが、結果的に効きました。

この乗り換え判断は、
技術的というよりプロジェクトマネジメント的な話です。
「解決に固執せず、締切から逆算して代替案に切り替える」。
動かないものに時間を溶かさない判断が、
10日前の時点では一番重要でした。

開発の進め方

実装はほぼ全てClaude Codeに投げています。
私はコードを1行も読んでいません。

やったのは、
要件を日本語で明確に伝えること。
エラーが出たら開発者ツールのコンソールをそのままスクショで貼ること。
「思った動きと違う」を具体的に言語化すること。
この3つだけです。

事前に仕様書を用意して、
それを丸ごと渡すところから始めました。
仕様書自体もAIと壁打ちしながら作っています。
ディレクトリ構成、データ構造、APIエンドポイント、フェイルセーフ方針、実装順、当日の運用フローまで書いた上で「この通りに実装して」と投げる。この最初の設計フェーズにちゃんと時間をかけたことが、後半の速度に効きました。

まとめ

設計のいちばんの肝は、「複数の端末・アカウントから共有したい状態はクラウドのKVに、重いデータを扱う処理はローカルに」という役割分担でした。

そのうえで、
今回いちばん効いたのは技術的な巧さではなく、
次の3つの判断だったと思っています。

1. 全ての自動処理に手動フォールバックを用意したこと。

生配信は一発勝負なので、
「動かないかもしれない」を前提に組みました。
結果的に、X APIの取得が不安定な場面でも配信は一度も止まりませんでした。

2. 制約にぶつかったら設計ごと変えたこと。

CronがFreeプランで使えないと分かった時点で自動ポーリングを諦め、
手動トリガーに切り替えました。
結果的にリクエスト消費を配信の盛り上がりに合わせて制御できるようになり、
運用としてはむしろ良くなりました。

3. 解決に固執せず乗り換えたこと。

決済が通らないことに時間を溶かさず、
AIプロバイダごと差し替えました。
締切から逆算すれば、これが唯一の正解でした。

今後の課題

投稿数取得の精度

上限に張り付きやすく、
実際の投稿数を正確に反映しきれない場面がありました。ページング実装は入れましたが、まだ荒い。

Cronによる自動ポーリング

有料プランに乗れば実装済みのコードがそのまま動きます。
手動運用は「押し忘れ」というヒューマンエラーが残る。

AI採点のプロンプト調整

点数が全体的に高めに出る傾向があり、
辛口さのチューニング余地があります。

作りながら学んだことのほうが多い10日間でした。同じようにコードが書けないけど何か作りたい人の参考になれば嬉しいです。