Trip Video(Earth Studio 行程長動画)制作パイプライン
本スキルは 1 つの git リポジトリ(
kinopico-zhang/trip-video-skill)です。以下$T= リポジトリルート (ローカル:~/.claude/skills/trip-video→ シンボリックリンク~/workspace/trip-video-skill)。 作業ルール:ルートごとに独立した作業ディレクトリ(データ json / esp / フレームはそこに配置)、スクリプトは$Tから呼び出します。 詳細は$T/references/:case-study-v17.md(v7→v17 の全案件振り返り、作業開始前に必読)、presentation-rules.md(表示ルール、ユーザー確認後は後退禁止)。完全な事例:$T/examples/sz-hkg-ceb-siquijor/。
0. パイプライン全体像
ルート調査(OSRM 車線 / Tavily 航空路 / フェリー線) ← §1–§2
↓ per-route 生成スクリプトを書く(examples/ をコピー)→ セグメントごとの .esp + flight_data.json ← §3
Windows headless Chrome CDP 自動化 Earth Studio クラウドレンダリング ← §4
↓ siq_dl.mjs で mp4 ダウンロード → ffmpeg でベースフレームにデコード
src_frames/(1-indexed f%05d.jpg)
↓ compose20.py で合成(ルート/乗り物/徒歩点線/ラベル/アイコン)
comp_frames/(0-indexed)→ qc + MAD フリッカーチェック ← §5
↓ ffmpeg エンコード(crf 18)
scp nas:/share/Public/earth/ + 両端 md5 ← §6
データファイル flight_data.json(スキーマは examples 内の flight_data30.json を参照):
route の各頂点 (lat,lon,alt,rt) + walk フラグ、samples 0.1秒ごとのカメラサンプル、frames 各フレームの乗り物種類/位置/接線ベクトル、
walks 徒歩区間、emoji 各乗り物スプライトの向き角度、名前付きアンカーポイント(空港/フェリーターミナル/国境/出発点…)、fps。
環境:node≥22(cdp スクリプトは依存関係ゼロ);Pillow は PYTHONPATH 経由(ローカルは ~/workspace/google-earth/pylibs を指し、システム PIL は無し);ffmpeg は /home/admin/.local/bin/ffmpeg;numpy 無し(すべて PIL で代用);NAS は ssh エイリアス nas、納品先は /share/Public/earth/。
1. 地上ルート検索(OSRM)
車線のジオメトリは公開 OSRM デモを使用(Google Maps の Directions RPC は headless では到達不可;OSRM は同じ道路ネットワークを使用):
# 2点間の車線(overview=full で全ジオメトリ取得);複数点はセミコロン連結で経由地強制。マシンに jq が無いので python で座標取得
curl -s 'https://router.project-osrm.org/route/v1/driving/114.0354,22.6748;113.9350,22.5101?overview=full&geometries=geojson' \
| python3 -c 'import json,sys; d=json.load(sys.stdin); json.dump(d["routes"][0]["geometry"]["coordinates"], open("route_SZ_raw.json","w"))'
# 結果 [[lon,lat],...](経度が先!)、route_<セグメント名>_raw.json として保存
- 端点の固定:OSRM の端点は道路面に吸着し、正確なアンカーポイントには一致しません →
_load_forced(examples の trip_v10.py に実装あり)で始点/終点を正確なアンカーポイントに固定:アンカーポイントとの距離 >25m なら挿入、そうでなければ置き換え。 - 水上区間の検証:橋梁は合法的な海上高架;Natural Earth の
ne_50m_landポイント・イン・ポリゴンで線分が水上かどうかを判定し、「高架」と「OSRM が水路で間違ったルートを取ったケース」を区別。 - 徒歩区間(国境通過/ターミナルビル→誘導路/駐機場→出口):1–2秒、地図上で距離を測るだけで十分、道路ネットワークは検索しない;移動は ECEF 弦長の Hermite なだらか曲線(
_walk_ckpt、両端速度 0、τ=0.3s)。 - フェリー/海路:OSRM に船線は無い → Tavily でフェリー会社/航路/桟橋を調査、ルートは手動で海上に描く。
2. 航空路線検索(Tavily 調査 → WPTS)
航空路は実際のデータをモデル化して調査する必要があり、大圏ルートでごまかしてはいけません。§7 の tvly で以下の5点を明らかにします:
- 出発滑走路と初期針路(例:HKG 07L/07R、track 073° 東向き)
- 出域ウェイポイント列(073°→BREAM 右旋回→ATTOL→SIKOU でランタオ島 934m 峰を回避)
- 巡航高度と総距離(FL350=10668m、~1790km)
- 航路の走向(ルソン島西側沖→タブラス海峡→ネグロス島以西→タニョン海峡)
- 到着滑走路と進入方向(CEB 着陸 04、卓越北東風)
検索キーワード:"<出発地> <到着地> flight route waypoints"、"<ICAO> departure chart SID";flightradar24 の航路ページ、airport diagrams で相互検証。成果物 = 生成スクリプト内の WPTS リスト(examples の flight10.py パターン:(lat, lon) + 滑走路端/離陸点/各ウェイポイントコメント)、航跡線 = ウェイポイントを通る Catmull-Rom スプライン。調査結果はスクリプトの docstring に記録し、日付を入れる。
3. カメラ設計と esp 生成(per-route スクリプト)
ジェネレータパターン($T/examples/sz-hkg-ceb-siquijor/trip_v10.py をコピー):セグメントごとに .esp + 集約 flight_data.json、ライブラリは $T/compose/(flight.py/flight_v5.py:ll2ecef、CAL テーブル、hermite、build_esp)。コア計算(すべて実測検証済み、変更禁止):
- ES 補間セマンティクス:クラウドレンダリング = キーフレームの数値空間での線形補間;高度は CAL value(対数)空間で補間、kf 記述は
alt_to_value()を使用;超大スケール(≥Mm ズームアウト)は v_write 空間で補間。 - カメラ基底ベクトル(look-at を自力計算、ES レンダリングと ≤1px で一致):
f=norm(poi−cam)、r=norm(cross(f,norm(cam)))、u=cross(r,f)(u=cross(f,r)だと上下反転)。 - 真上俯瞰・北が上:カメラはターゲットの真上、look-at ターゲット = カメラ地上点を北に 0.007·H 移動(≈0.4° 傾き、heading は常に 0 でブレ無し)。
- kf 密度:0.1秒サンプリングが最小;動きの密集した時間軸は必ず全フレーム kf(0.1秒+線形補間では H スケーリングが折れ線になり、10Hz で見かけ速度が 1.6倍に跳ねる)。
- スムーズなカメラワーク = H スケーリング:平行移動量は
du/df = c·s(f)·H(f)(s=速度ゼロ端点のイージング)→ 画面上の速度は高度に依存しない;巡航区間の包絡線法v(t)=COEF·H(t)(走行距離方程式から COEF を逆算)で一定の画面上速度 ~700px/s。 - 大スパンスケールは log 空間で補間 + sinease;複数アンカーは三次 Hermite + 手動接線設定(smoothstep はアンカー毎に速度ゼロになるのでカクつく)。
- セグメント間のビット完全一致接合:後セグメントの最初のフレームカメラ状態は、隣接セグメントの
camera_state()エンドポイントを文字通り書き込み;ジェネレーター内蔵の_seam_diffアサーション < 1e-12(kf 差は必ず = 0;実測フレーム差分 1–16 はクラウドレンダリングノイズのみ)。 - フレーム数トラップ:必ず
int(round(T*FPS))(int(4.1*30)=122≠123)。
4. Earth Studio クラウドレンダリング(CDP 自動化)
事前準備:Windows Chrome --headless=new --remote-debugging-port=9222 --user-data-dir=C:\Users\admin\ChromeESAuto(Chrome 136+ は独立した user-data-dir が必要)で ES にログイン済み;WSL2 mirrored ネットワークで http://localhost:9222 に直接接続。
node $T/cdp/nav_es.mjs # ES を開く/復元 + ウィンドウサイズ 1920×1080(ビューポートはインポート/リフレッシュのたびに 985px にリセットされるので、その都度再実行!)
node $T/cdp/siq_import.mjs 'C:\Users\admin\ChromeESAuto\seg0.esp' # esp をインポート(Windows パス)
node $T/cdp/siq_scrub_batch.mjs 45 45 15 … # ArrowRight で1フレームずつ進み、プロパティパネル(経度/緯度/高度)の値を設計値と比較
node $T/cdp/render_submit.mjs "Seg0" # DOM click でレンダリングダイアログを開く + Submit(クラウドレンダリング 1080p mp4、~2分/セグメント)
node $T/cdp/siq_poll.mjs "Seg0" 15 # キューをポーリングして delivered まで待つ(送信後、API が数秒経たないと認識されないので "NONE"≠失敗)
node $T/cdp/siq_dl.mjs get "Seg0" # キューモーダルからダウンロード;使い終わったら `siq_dl.mjs close`
CDP の三大落とし穴(スクリプト内に対策済み、原理は case-study §4 参照):
- esp インポート:
DOM.setFileInputFiles後、手動で change イベントをディスパッチする必要あり、さもないと ES が静かに失敗;インポートはメモリ上のみでディスクには保存されない → live 状態から直接 Submit する。 - 合成マウスクリックがインポート後ランダムに効かなくなる → レンダリングボタン/Submit は一律 DOM
el.click()(render_submit.mjs)。 - React 制御入力が注入したジョブ名を時々受け付けない → esp のデフォルト名で送信、デフォルト名でポーリング・ダウンロード(siq_dl.mjs get にはジョブ名が必要、間違ったファイルをダウンロードするのを防ぐ);bigstore 直リンク 404 は無視。
- プロパティパネルは >350km 高度で 8–17% 低く表示される(表示バグ、レンダリング経路は正確なので惑わされないこと)。
- 認証情報はディスクに保存しない:ログインは
node $T/cdp/login2.mjs <password>(argv で渡す)。
5. フレーム組立、合成と表示ルール
# デコード(mp4 f00001 = t=0、1-indexed)
/home/admin/.local/bin/ffmpeg -i seg0.mp4 -qscale:v 1 seg0_frames/f%05d.jpg
# タイムラインに沿って src_frames/ にリネーム(1-indexed 連番)
# 合成(KEEP_WM=1 = Google ロゴを残す、唯一の納品形態)
cd <作業ディレクトリ>
FLIGHT_DATA=flight_data.json SRC=src_frames OUT=comp_frames KEEP_WM=1 \
PYTHONPATH=<Pillow のパス> python3 $T/compose/compose20.py
FLIGHT_DATA=flight_data.json SRC=src_frames OUT=comp_frames \
PYTHONPATH=<Pillow のパス> python3 $T/compose/compose20.py qc
# MAD フリッカー(192×108 グレースケール隣接フレーム差分):median≤14 静景 / 20–27 log ズーム段差 / >40 フリッカー
PYTHONPATH=<Pillow のパス> python3 $T/qc/mad_check.py comp_frames
表示ルール全文は $T/references/presentation-rules.md にあり(ユーザー確認済み、後退禁止)、要点:
ロゴは必ず1バージョンのみ残す;徒歩 = 動的点線、人は描かない;末尾に車両は追加しない;アイコンは厳密に ∝1/alt(13500/alt、下限 0.12)+ ベースマップ長辺 150px 同高度で同一サイズ;乗り物は事前待機 + 正確なアンカーポイント停車(待機→停車→通過の順に描画)。
compose20.py のルート別編集箇所(その他はすべてデータ駆動):
AIRPORTS/PIERSラベルリスト(テキスト/タイムウィンドウ/フェードアウト)——アンカー座標は flight_data.json から取得;FONT_PATH(CJK フォント、マシンに合わせて変更);- 乗り物スプライト
compose/emoji_*.png(新しい乗り物種類はsprite_prep.pyでraw_sprites/から生成)。
QC はすべて緑になってからエンコード:停車/待機アンカーの投影 0.00px、絵文字が全フレームで画面内(±80px)、継ぎ目 esp kf 差 = 0、QC ウィンドウはデータ実測 rt セグメントに合わせて記述(設計時間で決めない)。見かけ速度の予算:中央移動 ≤1.2 画面/s、乗り物 ≤0.8 画面/s。
6. エンコードと納品
/home/admin/.local/bin/ffmpeg -framerate 30 -i comp_frames/f%05d.jpg \
-c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -fps_mode passthrough \
-movflags +faststart <ルート名>_trip_v17.mp4
scp <ルート名>_trip_v17.mp4 nas:/share/Public/earth/
md5sum <ルート名>_trip_v17.mp4 && ssh nas md5sum /share/Public/earth/<ルート名>_trip_v17.mp4 # 両端一致で初めて納品完了
バージョン戦略:NAS には履歴のある通過済みバージョンを保持、却下/置き換えられたバージョンは削除;ロゴ1バージョンのみ納品(_logo サフィックス付きの2バージョンは無し)。
7. Tavily 検索クイックリファレンス(ローカル認証済み)
tvly は /home/admin/.local/bin/tvly(既に tvly login 済み)。JSON 出力は LLM での利用に適しています:
tvly search "HKG CEB flight route waypoints FL350" --max-results 8 --include-answer basic
tvly search "Cebu to Siquijor OceanJet ferry schedule pier" --max-results 5 -o ferry.json
tvly extract 'https://www.flightradar24.com/...' # 全文抽出
tvly map 'earth.google.com' --limit 20 # サイトURL発見
tvly research "香港空港 07L 滑走路 SID 出域航路" # 深堀り調査(複数回、航路検証に適している)
より詳しい使い方は ~/.claude/skills/tavily-* シリーズのスキルを参照。
8. 大落とし穴クイックリファレンス
| 落とし穴 | 対策 |
|---|---|
| OSRM 端点がアンカーポイントと一致しない | _load_forced で固定(>25m なら挿入/そうでなければ置き換え) |
| esp インポートが静かに失敗する | 手動で change イベントをディスパッチ(siq_import.mjs に内蔵) |
| 合成マウスクリックが効かない | DOM el.click()(render_submit.mjs) |
| ジョブ名の注入が効かない | esp のデフォルト名で送信+ポーリング・ダウンロード |
| 送信直後にキューにジョブが無い | API に数秒の遅延があるので、poll で再判定 |
| ビューポートが 985px にリセットされる | インポート/リフレッシュのたびに node $T/cdp/nav_es.mjs |
| 高度表示 >350km で低く出る | パネル表示のバグ、レンダリング結果が正しい |
| numpy が存在しない | PIL の ImageChops/histogram で代用 |
| タイムラインのブレ/フリッカー | 全フレーム kf + log 空間スケーリング + H スケーリングカメラワーク + mad_check.py |
| Read ツールでレンダリングフレームが見られない | トリミング→リサイズ→ASCII 顕微鏡( .:-=+*#%@)で目視確認 |
| アカウントが botguard でブロックされる | 1文字ずつ dispatchKeyEvent + マウス補間(login2.mjs) |
| 認証情報 | 全て argv/環境変数で渡し、決してリポジトリに書き込まない |