発端: Geminiが400MB超の動画を飲み込まない
Twitterから英語動画をダウンロードして、Google AI StudioのGeminiに放り込んで字幕を生成する運用をしている。動画を渡すと文字起こし+英訳までまとめて返してくれるので、英語学習の素材づくりに重宝している。
ところが今朝、Dwarkesh Patelのインタビュー動画(約890MB)をアップロードしようとしたら、赤字のバリデーションエラーが返ってきた。
Video file size cannot exceed 400MB.
400MB制限。2時間超えのポッドキャスト動画だと簡単に超える。毎回手動でffmpegを叩くのも面倒なので、ドラッグ&ドロップで3分割してくれるスクリプトを作ることにした。
対象ファイルのパスがこれ。
G:\マイドライブ\twitter_download\20260416_Dwarkesh Patel_2044442721570439168.mp4
Googleドライブのマイドライブ直下、半角スペース入り、日本語フォルダ名。Windowsでスクリプト書くときに罠が全部乗っているパスだ。
第一弾: 素直にbatファイルで書いたら文字化けした
最初はシンプルに考えた。ffmpegはPATHに通してあるし、batファイルにドラッグ&ドロップすれば%1でパスが拾える。日本語のメッセージをechoしつつ3分割すれば終わり、のはずだった。
Git Bashからテスト実行した瞬間、コンソールが化けた。
陦悟勁...
蛻・牡荳ュ...
chcp 65001でUTF-8に切り替えても、for /fと日本語パスの組み合わせで今度はパスが壊れる。batファイルのエンコーディングとcmd.exeのコードページの相性は、いつ触っても心が折れる。
方針転換: batはランチャー、PowerShellで本番ロジック
batファイルでロジックを書くのはやめた。ただしドラッグ&ドロップの受け口としてのbatファイルは捨てがたい。PowerShellスクリプト(.ps1)に直接ファイルをドロップする運用は、セキュリティ設定の絡みで素直に動かないことが多いからだ。
役割分担をこう決めた。
- batファイル: 英語メッセージだけ。
%1で受け取ったパスをPowerShellに渡すランチャー - PowerShellスクリプト: 動画分割の実ロジック。日本語メッセージもここで出す
batから呼ぶときはこう。
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0split-video.ps1" -InputFile "%~1"
pause
batには日本語を一切書かないので、Shift-JISとUTF-8のどちらで保存されても文字化けが起きない。メッセージは全部PowerShell側に寄せる。
PowerShellスクリプトのハマりどころ
ここから先が本番だった。PowerShellに移しても、罠が3つ連続で待っていた。
罠1: BOMなしUTF-8だとパースが壊れる
最初、VS CodeのデフォルトであるBOMなしUTF-8で保存してスクリプトを実行したら、こんなエラーが出た。
トークン 'xxx' は、式またはステートメントで有効な部分ではありません。
PowerShell 5.1(Windows標準)は、BOMなしUTF-8のマルチバイト文字を正しく解釈してくれない。コメントや文字列リテラルに日本語が入っていると、パーサが途中でコケる。
解決策はシンプルで、BOM付きUTF-8で保存し直すだけ。PowerShellからなら1行で変換できる。
$content = Get-Content -Path "split-video.ps1" -Raw -Encoding UTF8
[System.IO.File]::WriteAllText(
"split-video.ps1",
$content,
[System.Text.UTF8Encoding]::new($true) # $true = BOM付き
)
VS Codeでも右下のエンコーディング表示をクリックして「Save with Encoding」→「UTF-8 with BOM」で変えられる。Windows専用のPowerShellスクリプトを書くときは、BOM付きを常用するのが安全だ。
罠2: "{0} MB" のMBが数値サフィックスとして食われる
ファイルサイズを表示するコードでこう書いた。
Write-Host ("サイズ: {0:N1} MB" -f ($size / 1MB))
これを実行すると、エラーが出る。
式またはステートメントにキャストできません。
PowerShellには数値リテラルのサフィックスとしてKB/MB/GB/TB/PBが組み込まれている。100MBと書けば104857600になる便利機能だ。ところがフォーマット文字列の中に裸でMBが出てくると、パーサが「ここは数値リテラルだな」と誤認識して構文エラーを吐く。
回避策は、MBを文字として明示的に扱うこと。
# NG: MBが数値サフィックス扱いされる
Write-Host "サイズ: {0:N1} MB" -f ($size / 1MB)
# OK: フォーマット対象に埋め込むか、文字列連結にする
$mb = [math]::Round($size / 1MB, 1)
Write-Host "サイズ: $mb MB"
PowerShellの数値サフィックスがフォーマット文字列の中まで効いてくるとは思わなかった。ログをしばらく眺めて気づくまで、5分くらい悩んだ。
罠3: ffprobeがPATHにない
分割の起点を決めるには動画のduration(再生時間)が要る。普段ならffprobeを呼べば一発だ。
ffprobe -v error -show_entries format=duration -of csv=p=0 $InputFile
ところが実行すると'ffprobe' は、内部コマンドまたは外部コマンド...として認識されていません。と返ってきた。ffmpegのビルドによってはffprobeが同梱されていないことがある(今のWindows環境がまさにそうだった)。
ffprobeを別途入れるのも一手だが、ffmpegだけで完結させたい。ffmpegは入力ファイルを解析するときに標準エラー出力にdurationを書き出すので、そこから正規表現で拾えばいい。
$ffmpegOutput = & ffmpeg -i $InputFile 2>&1 | Out-String
if ($ffmpegOutput -match "Duration:\s(\d+):(\d+):(\d+)\.(\d+)") {
$hours = [int]$matches[1]
$minutes = [int]$matches[2]
$seconds = [int]$matches[3]
$totalSeconds = $hours * 3600 + $minutes * 60 + $seconds
}
ffmpegに-iだけ渡して実行すると、出力ファイルを指定していないのでエラー終了するが、先に入力情報は吐き出されている。そこを捕まえる。スマートではないが確実に動く。
最終結果: 3パート+残りカス0.1MB
BOM付きで保存し直して再実行したら、分割が走った。
part000: 392.4 MB
part001: 388.1 MB
part002: 151.2 MB
part003: 0.1 MB
狙い通り3パートすべて400MB以下に収まった。ただし4パート目に0.1MBの極小ファイルが生まれている。
これはffmpegのセグメント分割がキーフレーム境界にスナップする挙動のせいだ。-f segment -segment_timeで3分割を指定しても、指定秒のピッタリでは切れず、直近のキーフレームで切られる。結果、3つ目のセグメント末尾からファイル末尾までの数十ミリ秒が、4つ目として独立してしまう。
実用上は3つで字幕生成は完結するので、part003は無視していい。気になるなら分割後に100KB以下のパートを削除する後処理を足せば綺麗になる。
振り返り: Windows × 日本語パス × ffmpegの三重苦
今日ハマった罠を並べると、全部「Windowsで日本語パスを扱う動画スクリプト」という一点に収束していた。
- Git Bashからbat直叩きの文字化け → batはランチャーに徹して日本語を持たせない
- BOMなしUTF-8でPowerShellがパースを投げる → BOM付きUTF-8で保存
- フォーマット文字列の
MBが数値サフィックス扱い → 変数展開で先に文字列化してから埋め込む - ffprobeがPATHにない → ffmpegの標準エラー出力からdurationを正規表現で拾う
いずれも単体なら10分で解決する話だが、日本語パスを含むドラッグ&ドロップ運用をまとめて実現しようとすると、4つが同時に襲いかかってくる。Windowsで動画処理スクリプトを書くときは、最初からBOM付きUTF-8・batランチャー・ffmpeg単体で完結・MBは変数経由の4点セットを前提にしておくと余計な試行錯誤が減る。
一度ランチャー方式を作っておけば、次に400MB制限で引っかかったときはファイルをbatにドラッグするだけで済む。たぶん来週また同じ動画サイズで困るので、そのときの自分のために作った。