Trip Video (Earth Studio 行程长视频) 制作流水线
Este skill é um repositório git (
kinopico-zhang/trip-video-skill). Abaixo,$T= raiz do repositório (neste computador:~/.claude/skills/trip-video→ link simbólico~/workspace/trip-video-skill). Convenção: cada rota em um diretório de trabalho independente (json / esp / frames ficam lá), scripts são chamados a partir de$T. Detalhes aprofundados em$T/references/:case-study-v17.md(análise completa de v7→v17, leia antes de começar),presentation-rules.md(regras de apresentação, confirme com o usuário para não regredir). Exemplo completo:$T/examples/sz-hkg-ceb-siquijor/.
0. Visão geral do pipeline
Pesquisa de rota (OSRM para carro / Tavily para rotas aéreas / travessias de balsa) ← §1–§2
↓ Escrever script de geração por rota (copiar de examples/) → .esp por trecho + flight_data.json ← §3
Renderização em nuvem Earth Studio via CDP (Chrome headless Windows) ← §4
↓ siq_dl.mjs baixa mp4 → ffmpeg decodifica em frames base
src_frames/ (indexado a partir de 1 f%05d.jpg)
↓ compose20.py composição (linha de rota/veículo/linha tracejada de caminhada/rótulo/ícone)
comp_frames/ (indexado a partir de 0) → QC + verificação de flicker MAD ← §5
↓ ffmpeg codifica (crf 18)
scp nas:/share/Public/earth/ + md5 em ambos os lados ← §6
Arquivo de dados flight_data.json (schema veja flight_data30.json nos exemplos):
route vértices (lat,lon,alt,rt) + flag walk, samples amostras de câmera a cada 0.1s, frames tipo/posição/vetor tangente do veículo por frame,
walks janelas de caminhada, emoji ângulo de orientação do sprite de cada veículo, pontos de ancoragem nomeados (aeroporto/porto/porto de entrada/ponto de partida…), fps.
Ambiente: node≥22 (scripts cdp zero dependências); Pillow via PYTHONPATH (neste computador aponta para ~/workspace/google-earth/pylibs, a máquina não tem PIL do sistema); ffmpeg em /home/admin/.local/bin/ffmpeg; sem numpy (tudo com PIL); NAS usa alias ssh nas, diretório de entrega /share/Public/earth/.
1. Consulta de navegação terrestre (OSRM)
Geometria de trechos de carro usa demo público do OSRM (Google Maps Directions RPC não é acessível em modo headless; OSRM segue a mesma malha viária):
# Rota de carro entre dois pontos (overview=full para geometria completa); pontos múltiplos separados por ponto e vírgula forçam passagem. A máquina não tem jq, use python para obter coordenadas
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"))'
# Resultado [[lon,lat],...] (nota: longitude vem primeiro!), salvar como route_<trecho>_raw.json
- Fixação de extremidades: OSRM prende os pontos à superfície da estrada, não no ponto de ancoragem exato →
_load_forced(implementado em trip_v10.py nos exemplos) fixa o início e fim no ponto de ancoragem exato: se distância >25m do ponto, insere; caso contrário, substitui. - Verificação de trechos sobre água: Pontes sobre o mar são viadutos legítimos sobre água; usa polígonos de terra do Natural Earth
ne_50m_landpara verificar se o segmento está sobre água, distinguindo "viaduto" de "erro do OSRM contornando água". - Trechos a pé (passagem na fronteira/saguão → pista/posição de estacionamento → saída): 1–2s, medir distância no mapa, não consultar malha viária; deslocamento usa curva hermite na corda ECEF (
_walk_ckpt, velocidade zero nos dois extremos, τ=0.3s). - Balsa / rota marítima: OSRM não tem rotas de barco → Tavily consulta empresa de balsa/rota/porto, desenha rota manualmente no mar.
2. Consulta de rota aérea (Tavily → WPTS)
Rota aérea deve ser modelada com dados reais, não pode usar arco de círculo máximo. Use tvly (seção 7) para esclarecer cinco coisas:
- Pista de decolagem e rumo inicial (exemplo: HKG 07L/07R, track 073° para leste)
- Sequência de waypoints de saída (073°→BREAM vire à direita→ATTOL→SIKOU para evitar pico de 934m da Ilha Lantau)
- Altitude de cruzeiro e distância total (FL350=10668m, ~1790km)
- Direção da rota (ao longo do lado oeste de Luzon→Estreito de Tablas→Oeste de Negros→Estreito de Tanon)
- Pista de chegada e direção de aproximação (CEB pouso 04, vento predominante de nordeste)
Palavras-chave de busca: "<DEP> <ARR> flight route waypoints", "<ICAO> departure chart SID"; cruzar com página de rota do flightradar24, diagrams de aeroporto. Saída = lista WPTS no script de geração (exemplo flight10.py: (lat, lon) + comentários para cabeceira/ponto de rotação/waypoints), linha de rota = spline Catmull-Rom pelos waypoints. Conclusão da pesquisa vai no docstring do script com data.
3. Design de câmera e geração de .esp (script por rota)
Padrão generator (copie de $T/examples/sz-hkg-ceb-siquijor/trip_v10.py): cada trecho gera um .esp + flight_data.json consolidado, bibliotecas em $T/compose/ (flight.py/flight_v5.py: ll2ecef, tabela CAL, hermite, build_esp). Matemática principal (tudo verificado experimentalmente, não altere):
- Semântica de interpolação do ES: renderização em nuvem = interpolação linear no espaço numérico dos keyframes; altitude interpola no espaço CAL value (logarítmico), escreva keyframes com
alt_to_value(); trechos de grande escala (≥Mm de distância) devem interpolar no espaço v_write. - Vetores base da câmera (look-at calculado manualmente, alinhamento ≤1px com renderização do ES):
f=norm(poi−cam),r=norm(cross(f,norm(cam))),u=cross(r,f)(u=cross(f,r)causaria inversão vertical). - Visão superior com norte para cima: câmera diretamente acima do alvo, look-at = ponto no solo deslocado 0.007·H para norte (≈0.4° de inclinação, heading constante 0, sem oscilação).
- Densidade de keyframes: amostragem começa a cada 0.1s; eixos de tempo com movimento denso devem ter keyframe a cada frame (interpolação linear a cada 0.1s+ achata o zoom H em uma linha quebrada, pulso de velocidade aparente 1.6× a 10Hz).
- Movimento suave = zoom H: deslocamento toma
du/df = c·s(f)·H(f)(s=easing nos extremos com velocidade zero) → velocidade na tela independente da altitude; trecho de cruzeiro usa método envelopev(t)=COEF·H(t)(resolve COEF da equação de distância) para velocidade constante na tela ~700px/s. - Grande escala interpola no espaço log + sinease; múltiplos pontos de ancoragem usam Hermite cúbico + tangentes manuais (smoothstep com velocidade zero em cada ponto causa engasgos).
- Junta bit-exata entre trechos: estado da câmera do primeiro frame do trecho seguinte é copiado byte a byte do ponto final do trecho anterior via
camera_state(); o generator tem asserção_seam_diff< 1e-12 (diferença de keyframe deve ser = 0; diferença medida de frame 1–16 é apenas ruído de renderização em nuvem). - Armadilha de contagem de frames: sempre use
int(round(T*FPS))(int(4.1*30)=122≠123).
4. Renderização em nuvem Earth Studio (automação CDP)
Pré-requisitos: Chrome no Windows --headless=new --remote-debugging-port=9222 --user-data-dir=C:\Users\admin\ChromeESAuto (Chrome 136+ precisa de user-data-dir independente) já logado no ES; WSL2 rede mirrored conecta diretamente a http://localhost:9222.
node $T/cdp/nav_es.mjs # Recuperar/abrir ES + ajustar janela 1920×1080 (viewport reinicia para 985px a cada import/refresh, precisa reexecutar!)
node $T/cdp/siq_import.mjs 'C:\Users\admin\ChromeESAuto\seg0.esp' # Importar .esp (caminho Windows)
node $T/cdp/siq_scrub_batch.mjs 45 45 15 … # Avançar frame por frame (ArrowRight) lendo painel de propriedades (longitude/latitude/altitude) comparar com valores de design
node $T/cdp/render_submit.mjs "Seg0" # DOM click abre diálogo de renderização + Submit (renderização em nuvem 1080p mp4, ~2 minutos/trecho)
node $T/cdp/siq_poll.mjs "Seg0" 15 # Polling fila até delivered (API leva alguns segundos para registrar após submit, "NONE" ≠ falha)
node $T/cdp/siq_dl.mjs get "Seg0" # Baixar da fila modal; depois use `siq_dl.mjs close`
Três grandes armadilhas do CDP (scripts já têm contramedidas,原理 veja case-study §4):
- Import .esp:
DOM.setFileInputFilesprecisa disparar manualmente evento change, senão ES falha silenciosamente; import só vai para memória, não para disco → Submit direto do estado live. - Cliques do mouse na síntese falham aleatoriamente → botão de renderização/Submit usam DOM
el.click()(render_submit.mjs). - Inputs controlados pelo React às vezes não aceitam nome de job injetado → submeter com nome padrão do .esp, fazer polling e download pelo nome padrão (siq_dl.mjs get precisa do nome do job, senão baixa arquivo errado); link direto bigstore 404, não usar.
- Painel de propriedades >350km de altitude mostra valor 8–17% menor (bug de exibição, caminho de renderização é preciso, não se engane).
- Credenciais não são salvas em disco: login com
node $T/cdp/login2.mjs <password>(passado via argv).
5. Montagem de frames, composição e regras de apresentação
# Decodificar (mp4 f00001 = t=0, indexado a partir de 1)
/home/admin/.local/bin/ffmpeg -i seg0.mp4 -qscale:v 1 seg0_frames/f%05d.jpg
# Renomear por timeline para src_frames/ (contínuo, indexado a partir de 1)
# Composição (KEEP_WM=1 = manter logo Google, única forma de entrega)
cd <diretório de trabalho>
FLIGHT_DATA=flight_data.json SRC=src_frames OUT=comp_frames KEEP_WM=1 \
PYTHONPATH=<caminho Pillow> python3 $T/compose/compose20.py
FLIGHT_DATA=flight_data.json SRC=src_frames OUT=comp_frames \
PYTHONPATH=<caminho Pillow> python3 $T/compose/compose20.py qc
# Verificação de flicker MAD (diferença de frames adjacentes em tons de cinza 192×108): mediana≤14 cena estática / 20–27 zoom logarítmico em degraus / >40 flicker
PYTHONPATH=<caminho Pillow> python3 $T/qc/mad_check.py comp_frames
Regras completas de apresentação em $T/references/presentation-rules.md (confirmado pelo usuário, não regredir), pontos principais:
logo deve permanecer em uma única versão; caminhada = linha tracejada dinâmica sem desenhar pessoa; final sem veículo adicional; ícones estritamente ∝1/alt (13500/alt, piso 0.12) + imagem de fundo lado maior 150px no mesmo tamanho para mesma altitude; veículo pré-espera + parada em ponto de ancoragem exato (desenhar na ordem: espera→parada→atual).
Pontos de edição por rota no compose20.py (todo o resto orientado a dados):
AIRPORTS/PIERSlista de rótulos (texto / janela de tempo / fade-out) — coordenadas dos pontos de ancoragem vêm de flight_data.json;FONT_PATH(fonte CJK, ajustar conforme máquina);- Sprites de veículos
compose/emoji_*.png(novo tipo de veículo usasprite_prep.pya partir deraw_sprites/).
QC deve passar tudo verde antes de codificar: projeção do ponto de parada/espera 0.00px, emoji totalmente dentro do quadro em todos os frames (±80px), diferença de keyframe na junta = 0, janela QC escrita com base no rt medido dos dados (não confiar no tempo de design). Orçamento de velocidade aparente: translação central ≤1.2 tela/s, veículo ≤0.8 tela/s.
6. Codificação e entrega
/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 <rota>_trip_v17.mp4
scp <rota>_trip_v17.mp4 nas:/share/Public/earth/
md5sum <rota>_trip_v17.mp4 && ssh nas md5sum /share/Public/earth/<rota>_trip_v17.mp4 # só considera entregue se ambos os lados conferirem
Estratégia de versões: NAS mantém versões aprovadas históricas, exclui versões rejeitadas/substituídas; entrega com versão única de logo (sem duas versões com sufixo _logo).
7. Referência rápida de pesquisa Tavily (autenticado neste computador)
tvly em /home/admin/.local/bin/tvly (já tvly login). Saída JSON adequada para consumo por 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/...' # Extração completa de texto
tvly map 'earth.google.com' --limit 20 # Descoberta de URLs do site
tvly research "香港机场 07L 跑道 SID 离场航路" # Pesquisa aprofundada (múltiplas rodadas, adequado para verificação de rota)
Uso mais completo veja a série de skills ~/.claude/skills/tavily-*.
8. Referência rápida de armadilhas
| Armadilha | Contramedida |
|---|---|
| Extremidades OSRM não no ponto de ancoragem | _load_forced fixa (insere se >25m / substitui caso contrário) |
| Import .esp falha silenciosamente | Disparar manualmente evento change (já incluso em siq_import.mjs) |
| Clique do mouse na síntese falha | DOM el.click() (render_submit.mjs) |
| Nome do job não é injetado | Submeter com nome padrão do .esp + polling/download |
| Job não encontrado logo após submit | API leva segundos, fazer poll antes de decidir |
| Viewport reseta para 985px | node $T/cdp/nav_es.mjs a cada import/refresh |
| Altitude >350km exibe valor baixo | Bug do painel, confiar na renderização |
| numpy não existe | PIL ImageChops/histogram |
| Oscilação/flicker na timeline | Keyframe a cada frame + zoom em log + movimento com zoom H + mad_check.py |
| Ferramenta Read não consegue ver frames renderizados | Recortar→redimensionar→microscópio ASCII ( .:-=+*#%@) inspeção visual |
| Botguard bloqueia login | Digitar caractere por caractere dispatchKeyEvent + interpolação de mouse (login2.mjs) |
| Credenciais | Sempre passar via argv/variável de ambiente, nunca escrever no repositório |