◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/AIモデル
◉ AIモデル/2026-07-17上級

3分の動画だけが失敗する — Rork の動画解析をワーカー経由から端末直送に引き直す

Gemini Files API に動画を渡す経路を、Cloudflare Workers 中継から端末直送へ引き直した記録です。128MB のメモリ上限に当たる仕組み、resumable upload の発行だけを残す実装、Expo SDK 54 での書き換え、20GB の保存枠と請求の見立てまでをまとめます。

Rork583Gemini8Files API動画解析Cloudflare Workers25Expo219個人開発216

✦ プレミアム記事

手元の iPhone で撮った 28 秒のテスト動画は、何度送っても通りました。解析結果も期待どおりに返ってきます。

ところが友人に頼んで撮ってもらった 3 分の動画を送った途端、アップロードが返ってこなくなりました。エラーメッセージらしいメッセージもなく、しばらく待つと 500 が返るだけです。

最初はモデル側の問題を疑いました。動画が長すぎるのか、フォーマットが違うのか。ログを追って分かったのは、そもそも Gemini に届く前の段階で落ちていたということです。落ちていたのは推論ではなく、動画の通り道でした。

28 秒は通るのに、3 分で止まる

当時の構成は、素直といえば素直なものでした。

Expo アプリで動画を選び、Cloudflare Workers に multipart で送り、Worker が Gemini の Files API へ中継します。API キーをアプリに埋め込みたくなかったので、サーバーを1枚挟んでいたわけです。個人開発でバックエンドを持たずに済ませたいとき、Workers は魅力的な選択肢でした。

Worker 側のコードは、こんな形をしていました。

// ❌ 当時の実装 — 動画がまるごと Worker のメモリに載る
async function handleVideoUpload(request: Request, env: Env): Promise<Response> {
  const formData = await request.formData();
  const videoFile = formData.get('video') as File;
 
  // ここで動画の全バイトが Worker のヒープに展開される
  const arrayBuffer = await videoFile.arrayBuffer();
 
  const initResponse = await fetch(`${UPLOAD_URL}?key=${env.GEMINI_API_KEY}`, {
    method: 'POST',
    headers: {
      'X-Goog-Upload-Protocol': 'resumable',
      'X-Goog-Upload-Command': 'start',
      'X-Goog-Upload-Header-Content-Length': arrayBuffer.byteLength.toString(),
      'X-Goog-Upload-Header-Content-Type': videoFile.type,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ file: { display_name: videoFile.name } }),
  });
 
  const uploadUri = initResponse.headers.get('X-Goog-Upload-URL');
 
  // 受け取った全バイトを、そのまま Google へ送り直す
  return fetch(uploadUri!, {
    method: 'POST',
    headers: {
      'Content-Length': arrayBuffer.byteLength.toString(),
      'X-Goog-Upload-Offset': '0',
      'X-Goog-Upload-Command': 'upload, finalize',
    },
    body: arrayBuffer,
  });
}

await videoFile.arrayBuffer() の一行が、すべてでした。

動画がワーカーを通り抜けていた

Cloudflare Workers は、1 つの isolate あたり 128MB のメモリを上限としています。これは JavaScript のヒープと WebAssembly の確保分を合わせた値で、有料プランに上げても増えません。しかも上限は「1リクエストあたり」ではなく「isolate あたり」です。

一方で、リクエストボディのサイズ上限は Free プランで 100MB あります。つまり「100MB の動画は受け取れてしまうが、それをメモリに展開する余地はない」という構造になっていました。

iPhone で撮った動画のサイズを測ってみると、こうです。

撮影設定長さ実測サイズarrayBuffer 後の挙動
1080p / 30fps28秒約 52MB通る
1080p / 30fps1分10秒約 130MB413 が返る
720p / 30fps3分約 96MB断続的に 500
4K / 60fps30秒約 190MB413 が返る

3 分の 720p が最も厄介でした。96MB はボディ上限の 100MB を下回るので受理されます。けれど 96MB をヒープに載せた時点で、128MB のうち大半を1本の動画が占めます。同じ isolate に別のリクエストが相乗りしていれば、そこで超えます。

だから「断続的に」失敗していたのです。手元で1人で試している間は通りますし、複数人が同時に触ると落ちます。再現条件が掴めなかった理由が、ここにありました。

さらに、通ったところで無駄が残ります。動画は端末から Cloudflare へ上り、Cloudflare から Google へもう一度上っていきます。同じバイト列が 2 回ネットワークを流れ、その往復のあいだ Worker の CPU 時間を占有し続けます。中継する意味があったのは API キーを隠すことだけでした。

キーを隠したいだけなら、動画そのものを通す必要はありません。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦動画を Worker の arrayBuffer に載せた瞬間に 128MB のメモリ上限へ当たる仕組みと、端末直送に切り替えるまでの経路設計
✦resumable upload の開始だけをサーバーに残し、API キーを配らずに端末から直接送る Worker / Expo 両側の実装コード
✦Expo SDK 54 で createUploadTask が legacy へ移った件の対処と、expo/fetch へ寄せると進捗表示を失うトレードオフの比較
✦プロジェクト全体 20GB の保存枠を直送設計で見失わないための計上と掃除、および fps と media_resolution で請求を決める判断基準
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

◉ AI モデル2026-10-05
Sonnet 5.5・GPT-6.1 Sol・Opus 5.5 のどれに頼むかを、修正の種類で先に決めておきます。クレジットの行き先を1週間記録して引いた割り当て
Rork のモデルメニューが Sonnet 5.5・GPT-6.1 Sol・Opus 5.5 の三つになったあと、修正を三種類に分けてモデルと effort を割り当てました。変更履歴に書かれた違いの読み方、クレジットの行き先を記録する小さな台帳、そこから引いた線引きを書き残します。
◉ AI モデル2026-09-28
続きで直すか、復元ポイントへ戻して言い換えるか。Rork で同じ不具合を両方の道で直して分かった境目
Rork の修正依頼が三度目にはまったとき、続きで頼むか、Restore で戻して言い換えるか。同じ不具合を両方で直した往復回数と残ったコードを並べ、私が引いた線をお伝えします。
◉ AI モデル2026-09-17
推奨された移行先が、すでに停止していました — Gemini の廃止表 74 行を突き合わせた記録
Gemini の廃止表を突き合わせたところ、推奨代替として書かれたモデル自体がすでに停止している行が5件ありました。10月2日に消える gemini-2.5-flash-image もその一つです。移行先の確かめ方と、404 の受け皿の置き方をまとめました。