Communitygithub.com

kinopico-zhang/trip-video-skill

制作「地理行程长视频」(多段交通 + 飞机航线 + Google Earth Studio 云渲染 + 合成出片)的完整流水线。

trip-video-skill とは?

このスキルは、複数の交通手段(車・徒歩・飛行機・フェリー)を組み合わせた地理旅行の長尺動画を、Google Earth Studio のクラウドレンダリング機能を駆使して自動生成するための完全なパイプラインです。OSRM を用いた地上ルート検索、Tavily による実際の航空路調査、ヘッドレス Chrome の CDP 自動操作による Earth Studio への ESP インポート・レンダリング・ダウンロード、PIL を用いたフレーム合成、品質チェック(MAD フリッカー検出)、そして NAS への納品までをカバーしています。実証済みの事例として、清湖北駅→深圳湾口岸→香港国際空港→セブ→シキホール島のルート(v17)が含まれており、各工程の詳細な手順と注意点が SKILL.md に記載されています。

対応✓Claude Code~Codex CLI~Cursor
npx skills add kinopico-zhang/trip-video-skill

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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点を明らかにします:

  1. 出発滑走路と初期針路(例:HKG 07L/07R、track 073° 東向き)
  2. 出域ウェイポイント列(073°→BREAM 右旋回→ATTOL→SIKOU でランタオ島 934m 峰を回避)
  3. 巡航高度と総距離(FL350=10668m、~1790km)
  4. 航路の走向(ルソン島西側沖→タブラス海峡→ネグロス島以西→タニョン海峡)
  5. 到着滑走路と進入方向(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/環境変数で渡し、決してリポジトリに書き込まない

関連スキル