primocode 10.12.0 → 10.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +74 -12
  2. package/bin/primocode.js +148 -8
  3. package/lib/act.js +48 -1
  4. package/lib/catalogo.js +12 -1
  5. package/lib/config.js +1 -0
  6. package/lib/estudio/assinaturas/gerativo.json +1 -1
  7. package/lib/estudio/camadas.js +4 -0
  8. package/lib/estudio/cota.js +150 -0
  9. package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
  10. package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
  11. package/lib/estudio/dados/projetos/clube-agent.json +120 -0
  12. package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
  13. package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
  14. package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
  15. package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
  16. package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
  17. package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
  18. package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
  19. package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
  20. package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
  21. package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
  22. package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
  23. package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
  24. package/lib/estudio/dados/projetos/clube-clube.json +108 -0
  25. package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
  26. package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
  27. package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
  28. package/lib/estudio/dados/projetos/clube-dub.json +119 -0
  29. package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
  30. package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
  31. package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
  32. package/lib/estudio/dados/projetos/clube-frame.json +108 -0
  33. package/lib/estudio/dados/projetos/clube-glass.json +118 -0
  34. package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
  35. package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
  36. package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
  37. package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
  38. package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
  39. package/lib/estudio/dados/projetos/clube-morph.json +119 -0
  40. package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
  41. package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
  42. package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
  43. package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
  44. package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
  45. package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
  46. package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
  47. package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
  48. package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
  49. package/lib/estudio/dados/projetos/clube-promo.json +114 -0
  50. package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
  51. package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
  52. package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
  53. package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
  54. package/lib/estudio/dados/projetos/clube-robo.json +125 -0
  55. package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
  56. package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
  57. package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
  58. package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
  59. package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
  60. package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
  61. package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
  62. package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
  63. package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
  64. package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
  65. package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
  66. package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
  67. package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
  68. package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
  69. package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
  70. package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
  71. package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
  72. package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
  73. package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
  74. package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
  75. package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
  76. package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
  77. package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
  78. package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
  79. package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
  80. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
  81. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
  82. package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
  83. package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
  84. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
  85. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
  86. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
  87. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
  88. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
  89. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
  90. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
  91. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
  92. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
  93. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
  94. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
  95. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
  96. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
  97. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
  98. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
  99. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
  100. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
  101. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
  102. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
  103. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
  104. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
  105. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
  106. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
  107. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
  108. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
  109. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
  110. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
  111. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
  112. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
  113. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
  114. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
  115. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
  116. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
  117. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
  118. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
  119. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
  120. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
  121. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
  122. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
  123. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
  124. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
  125. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
  126. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
  127. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
  128. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
  129. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
  130. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
  131. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
  132. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
  133. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
  134. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
  135. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
  136. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
  137. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
  138. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
  139. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
  140. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
  141. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
  142. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
  143. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
  144. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
  145. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
  146. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
  147. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
  148. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
  149. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
  150. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
  151. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
  152. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
  153. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
  154. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
  155. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
  156. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
  157. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
  158. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
  159. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
  160. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
  161. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
  162. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
  163. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
  164. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
  165. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
  166. package/lib/estudio/diretor.js +2 -1
  167. package/lib/estudio/editor.js +285 -0
  168. package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
  169. package/lib/estudio/effectcraft.js +152 -0
  170. package/lib/estudio/exportar.js +181 -0
  171. package/lib/estudio/gerativo.js +44 -15
  172. package/lib/estudio/index.js +126 -5
  173. package/lib/estudio/kit/gerativo.jsx +15 -0
  174. package/lib/estudio/kit-ae/base.jsx +1146 -0
  175. package/lib/estudio/kit-ae/cenas.jsx +395 -0
  176. package/lib/estudio/kit-ae/primitivas.jsx +911 -0
  177. package/lib/estudio/kokoro.js +3 -1
  178. package/lib/estudio/maquina-diretor.js +393 -0
  179. package/lib/estudio/maquina-elementos.js +134 -0
  180. package/lib/estudio/maquina-layout.js +198 -0
  181. package/lib/estudio/maquina-som.js +478 -0
  182. package/lib/estudio/maquina-trilha.js +160 -0
  183. package/lib/estudio/maquina-voz.js +429 -0
  184. package/lib/estudio/maquina.js +516 -0
  185. package/lib/estudio/projetos.js +60 -0
  186. package/lib/estudio/remotion.js +6 -4
  187. package/lib/estudio/revisor.js +61 -0
  188. package/lib/estudio/voicestudio.js +81 -0
  189. package/lib/headless.js +71 -0
  190. package/lib/hooks.js +72 -0
  191. package/lib/local/cerebro.json +398 -12
  192. package/lib/local/entrada.js +46 -2
  193. package/lib/marca-kit.js +309 -0
  194. package/lib/mcp.js +286 -6
  195. package/lib/perigo.js +31 -3
  196. package/lib/planos.js +47 -19
  197. package/lib/preparar.js +111 -0
  198. package/lib/retomar.js +55 -0
  199. package/lib/sandbox.js +197 -0
  200. package/lib/skills.js +3 -1
  201. package/lib/tools.js +13 -4
  202. package/lib/ui.js +1 -1
  203. package/lib/video/auto.js +18 -3
  204. package/package.json +4 -2
  205. package/skills/after-effects/SKILL.md +52 -0
  206. package/skills/maquina-de-motion/SKILL.md +84 -0
  207. package/skills/motion-de-primeira/SKILL.md +33 -51
@@ -0,0 +1,64 @@
1
+ """Mede o movimento 2D (semelhança: zoom, giro, translação) da MESA nas cenas do jornal.
2
+ Achado: a mesa do autor é uma camada 2D (retângulo alinhado à tela em 16,54 s), filha dos nulls de câmera;
3
+ só o jornal gira em 3D. Fluxo óptico (DIS) entre quadros vizinhos só na madeira (fora do quadrilátero das
4
+ páginas, que vem da câmera 3D), semelhança por RANSAC, encadeada a partir do quadro de referência.
5
+ Saída: camera/mesa2d-<X>.json {ref, quadros: [{f, t, S: 3x3 (tela no ref -> tela no quadro), area}]}
6
+ Uso: python -I mesa_2d.py F|J '<json do quadrilátero das páginas no plano>'"""
7
+ import json, sys, cv2, numpy as np
8
+ CENA = sys.argv[1]; PAG = np.array(json.loads(sys.argv[2]), float)
9
+ VID = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
10
+ d = json.load(open(f"camera3d-{CENA}.json")); Q = {q["f"]: q for q in d["quadros"]}
11
+ REF = {"F": 394, "J": 670}[CENA]
12
+ K = np.array([[1500, 0, 540], [0, 1500, 540], [0, 0, 1.0]])
13
+ fs = sorted(Q)
14
+ cap = cv2.VideoCapture(VID); cap.set(cv2.CAP_PROP_POS_FRAMES, fs[0]); G = {}
15
+ for f in fs:
16
+ ok, im = cap.read(); G[f] = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
17
+ def mask(f):
18
+ R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"]); Hp = K @ np.c_[R[:, 0], R[:, 1], t]
19
+ P = np.c_[PAG, np.ones(4)] @ Hp.T; ok = (P[:, 2] > 0).all()
20
+ m = np.full((1080, 1080), 255, np.uint8)
21
+ if ok:
22
+ P = P[:, :2] / P[:, 2:3]; cv2.fillPoly(m, [np.clip(P, -1e5, 1e5).astype(np.int32)], 0)
23
+ else:
24
+ m[:] = 0
25
+ m = cv2.erode(m, np.ones((31, 31), np.uint8))
26
+ m[:30] = 0; m[-30:] = 0; m[:, :30] = 0; m[:, -30:] = 0
27
+ # fora da mesa (fundo escuro do fim) também sai
28
+ m[G[f] < 25] = 0
29
+ return m
30
+ dis = cv2.DISOpticalFlow_create(cv2.DISOPTICAL_FLOW_PRESET_MEDIUM)
31
+ def sim(a, b):
32
+ """semelhança tela a -> tela b medida na madeira visível nos dois"""
33
+ ma = mask(a); mb = mask(b)
34
+ fl = dis.calc(G[a], G[b], None)
35
+ ys, xs = np.nonzero(ma[::6, ::6]); ys *= 6; xs *= 6
36
+ if len(xs) < 300: return None, len(xs)
37
+ p1 = np.c_[xs, ys].astype(np.float32); p2 = p1 + fl[ys, xs]
38
+ ok = (p2[:, 0] > 0) & (p2[:, 0] < 1079) & (p2[:, 1] > 0) & (p2[:, 1] < 1079)
39
+ p1, p2 = p1[ok], p2[ok]
40
+ keep = mb[p2[:, 1].astype(int), p2[:, 0].astype(int)] > 0
41
+ p1, p2 = p1[keep], p2[keep]
42
+ if len(p1) < 300: return None, len(p1)
43
+ A, inl = cv2.estimateAffinePartial2D(p1, p2, method=cv2.RANSAC, ransacReprojThreshold=2.0)
44
+ if A is None or inl.sum() < 200: return None, int(len(p1))
45
+ S = np.eye(3); S[:2] = A; return S, int(inl.sum())
46
+ S = {REF: np.eye(3)}; area = {REF: 1}
47
+ for direc in (-1, 1):
48
+ f = REF
49
+ while (f + direc) in Q:
50
+ g = f + direc
51
+ T, n = sim(f, g)
52
+ if T is None: S[g] = None; area[g] = n; f = g; continue
53
+ base = f
54
+ while S.get(base) is None: base -= direc
55
+ S[g] = T @ S[f] if S[f] is not None else None; area[g] = n
56
+ f = g
57
+ out = []
58
+ for f in fs:
59
+ s = S.get(f)
60
+ if s is not None:
61
+ sc = float(np.sqrt(abs(np.linalg.det(s[:2, :2])))); rot = float(np.degrees(np.arctan2(s[1, 0], s[0, 0])))
62
+ print(f, round(f / 24, 3), area.get(f), None if s is None else (round(sc, 3), round(rot, 2), np.round(s[:2, 2], 1).tolist()))
63
+ out.append({"f": f, "t": round(f / 24, 5), "S": None if s is None else s.tolist(), "n": area.get(f)})
64
+ json.dump({"ref": REF, "quadros": out}, open(f"mesa2d-{CENA}.json", "w"))
@@ -0,0 +1,11 @@
1
+ import json, sys, numpy as np
2
+ import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
3
+ d = json.load(open(sys.argv[1])); out = sys.argv[2]
4
+ fig, ax = plt.subplots(4, 1, figsize=(22, 12), sharex=True)
5
+ for k, v in d.items():
6
+ q = v["quadros"]; t = [x["t"] for x in q]
7
+ ax[0].plot(t, np.log2([x["s"] for x in q]), label=k); ax[1].plot(t, [x["wx"] for x in q]); ax[2].plot(t, [x["wy"] for x in q]); ax[3].plot(t, [x["rot"] for x in q])
8
+ for a in ax: a.axvline(v["ini"], color="k", alpha=.3)
9
+ ax[0].text(v["ini"] + .05, ax[0].get_ylim()[1] if False else 5, k, fontsize=9)
10
+ for a, l in zip(ax, ["log2 zoom (0 = ref)", "mira x (mundo)", "mira y (mundo)", "giro (graus)"]): a.set_ylabel(l); a.grid(alpha=.3)
11
+ ax[3].set_xticks(np.arange(0, 34.1, 1)); plt.tight_layout(); plt.savefig(out, dpi=65)
@@ -0,0 +1,38 @@
1
+ """Desentorta quadros do original para o plano do jornal (coordenadas do mundo da cena) e monta um mosaico,
2
+ ficando em cada ponto o quadro que tem mais resolução ali. Serve para decalcar a diagramação da página.
3
+ Uso: python -I retificar_quadros.py F|J escala saida.png quadro1 quadro2 ... (escala = px por unidade do plano)"""
4
+ import json, sys, cv2, numpy as np
5
+ CENA, ESC, OUT = sys.argv[1], float(sys.argv[2]), sys.argv[3]
6
+ QS = [int(x) for x in sys.argv[4:]]
7
+ VID = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
8
+ d = json.load(open(f"camera3d-{CENA}.json")); Q = {q["f"]: q for q in d["quadros"]}
9
+ K = np.array([[1500, 0, 540], [0, 1500, 540], [0, 0, 1.0]])
10
+ def Hps(f):
11
+ R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"]); return K @ np.c_[R[:, 0], R[:, 1], t]
12
+ # extensão: o maior quadro (mais recuado) define a área
13
+ fl = max(QS, key=lambda f: Q[f]["tc"][2])
14
+ Hi = np.linalg.inv(Hps(fl)); cs = np.array([[0, 0, 1], [1080, 0, 1], [1080, 1080, 1], [0, 1080, 1.0]]) @ Hi.T
15
+ cs = cs[:, :2] / cs[:, 2:3]; x0, y0 = cs.min(0); x1, y1 = cs.max(0)
16
+ if len(sys.argv) > 4 and "--" in sys.argv: pass
17
+ W, Hh = int((x1 - x0) * ESC), int((y1 - y0) * ESC)
18
+ T = np.array([[ESC, 0, -x0 * ESC], [0, ESC, -y0 * ESC], [0, 0, 1.0]])
19
+ cap = cv2.VideoCapture(VID)
20
+ acc = np.zeros((Hh, W, 3), np.float32); best = np.zeros((Hh, W), np.float32)
21
+ for f in sorted(QS):
22
+ cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, im = cap.read()
23
+ M = Hps(f) @ np.linalg.inv(T) # saída -> tela
24
+ w = cv2.warpPerspective(im, M, (W, Hh), flags=cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
25
+ # resolução local: px de tela por unidade do plano ~ f / z_cam
26
+ yy, xx = np.mgrid[0:Hh, 0:W].astype(np.float32)
27
+ X = xx / ESC + x0; Y = yy / ESC + y0
28
+ R = np.array(Q[f]["R"]); t = np.array(Q[f]["tc"])
29
+ zc = R[2, 0] * X + R[2, 1] * Y + t[2]
30
+ P = np.stack([X, Y, np.ones_like(X)], -1) @ Hps(f).T; u = P[..., 0] / P[..., 2]; v = P[..., 1] / P[..., 2]
31
+ dentro = (u > 2) & (u < 1078) & (v > 2) & (v < 1078) & (zc > 0)
32
+ res = np.where(dentro, 1500 / np.maximum(zc, 1), 0)
33
+ m = res > best
34
+ acc[m] = w[m]; best[m] = res[m]
35
+ print(f, "z", round(t[2]), "px/unidade", round(1500 / t[2], 3))
36
+ cv2.imencode(".png", acc.astype(np.uint8))[1].tofile(OUT)
37
+ json.dump({"x0": float(x0), "y0": float(y0), "escala": ESC, "W": W, "H": Hh}, open(OUT + ".json", "w"))
38
+ print("extensão do plano", round(x0), round(y0), round(x1), round(y1), "->", W, Hh)
@@ -0,0 +1,52 @@
1
+ """Câmera final das cenas A e B (agente w-agenda, 17/10).
2
+ A: cadeia SIFT de medir_ab.py com 6 passos refeitos por geometria, porque o SIFT falhou neles:
3
+ - quadros 0->3: anel do checkbox (centro e diâmetro medidos na tela; anel = 18,7 px de mundo);
4
+ - quadros 28->31: o evento escurece e ganha risco; escala pela altura do chip (69 px de mundo) e
5
+ translação pelo início do "w" e pelo topo do chip.
6
+ B: cadeia SIFT de medir_ab.py como está (inclusive 4,46–4,83: a câmera NÃO segura, continua recuando).
7
+ Saída: cena-A-agenda.json e cena-B-docs.json no formato do ae/camera.jsx."""
8
+ import json, numpy as np, os
9
+ D = os.path.dirname(os.path.abspath(__file__))
10
+ M = json.load(open(os.path.join(D, "medida-ab.json")))
11
+ def S_de(z, mx, my):
12
+ s = 2 ** z; T = np.eye(3); T[0, 0] = T[1, 1] = s; T[:2, 2] = [540 - s * mx, 540 - s * my]; return T
13
+ def est(T):
14
+ s = T[0, 0]; w = np.linalg.inv(T) @ np.array([540, 540, 1.0]); return float(np.log2(s)), float(w[0]), float(w[1])
15
+ def passo(r, tx, ty):
16
+ T = np.eye(3); T[0, 0] = T[1, 1] = r; T[0, 2] = tx; T[1, 2] = ty; return T
17
+ A = M["A-agenda"]; S = {round(l[0] * 24): S_de(l[1], l[2], l[3]) for l in A}
18
+ # passos medidos por geometria: tela f -> tela f+1
19
+ anel = {0: ((531.5, 548.5), 1158.0), 1: ((449.5, 498.5), 874.0), 2: ((316.0, 414.5), 417.5), 3: ((275.5, 389.0), 274.5)}
20
+ P = {}
21
+ for f in (0, 1, 2):
22
+ (c0, d0), (c1, d1) = anel[f], anel[f + 1]; r = d1 / d0
23
+ P[f] = passo(r, c1[0] - r * c0[0], c1[1] - r * c0[1])
24
+ P[28] = passo(382 / 406, 85 - 382 / 406 * 35, 246 - 382 / 406 * 244)
25
+ P[29] = passo(353 / 382, 150 - 353 / 382 * 85, 246 - 353 / 382 * 246)
26
+ P[30] = passo(321 / 353, 225 - 321 / 353 * 150, 246 - 321 / 353 * 246)
27
+ # refaz a cadeia de trás para frente a partir de f31 (bom) e de f3 (refeito)
28
+ for f in (30, 29, 28):
29
+ S[f] = np.linalg.inv(P[f]) @ S[f + 1]
30
+ # quadros 4..27 dependem da cadeia SIFT ligada ao f28 antigo: aplica a mesma correção relativa
31
+ # (a cadeia SIFT entre 4 e 28 está boa; só o elo 28->31 estava errado)
32
+ Sant28 = S_de(*[l for l in A if round(l[0] * 24) == 28][0][1:4])
33
+ C = np.linalg.inv(Sant28) @ S[28] # correção no espaço do mundo (as transformações relativas entre quadros ficam iguais)
34
+ for f in range(3, 28):
35
+ S[f] = S[f] @ C
36
+ for f in (2, 1, 0):
37
+ S[f] = np.linalg.inv(P[f]) @ S[f + 1]
38
+ def salvar(nome, quadros, ini, fim, ref, pegada):
39
+ ch = {"zoom": [], "mx": [], "my": [], "giro": []}
40
+ for f in sorted(quadros):
41
+ z, mx, my = est(quadros[f]); t = round(f / 24, 4)
42
+ ch["zoom"].append([t, round(z, 4)]); ch["mx"].append([t, round(mx, 2)]); ch["my"].append([t, round(my, 2)]); ch["giro"].append([t, 0.0])
43
+ json.dump({"fps": 24, "ini": ini, "fim": fim, "ref": ref, "pegada": pegada, "fonte": "w-agenda/assar_ab.py", "chaves": ch},
44
+ open(os.path.join(D, "cena-%s.json" % nome), "w"))
45
+ return ch
46
+ chA = salvar("A-agenda", S, 0.0, 2.62, 2.58, None)
47
+ SB = {round(l[0] * 24): S_de(l[1], l[2], l[3]) for l in M["B-docs"]}
48
+ chB = salvar("B-docs", SB, 2.62, 4.95, 4.875, None)
49
+ for n, ch in (("A", chA), ("B", chB)):
50
+ print(n)
51
+ for i in range(0, len(ch["zoom"]), 3):
52
+ print(" t=%.3f z=%.3f mx=%.1f my=%.1f" % (ch["zoom"][i][0], ch["zoom"][i][1], ch["mx"][i][1], ch["my"][i][1]))
@@ -0,0 +1,59 @@
1
+ """Remedição da câmera das cenas A e B (agente w-agenda, 17/10).
2
+ Casa cada quadro com o anterior (SIFT + RANSAC, similaridade sem giro), com máscara que tira elementos que se mexem
3
+ sozinhos (cartão voando, globo), e integra a partir do quadro de referência de cada cena.
4
+ Saída: json {cena: [[t, log2 s, mx, my, n_inliers], ...]} no mesmo sentido do camera.jsx (mundo = tela no ref)."""
5
+ import json, sys, cv2, numpy as np
6
+ sys.stdout.reconfigure(encoding="utf-8")
7
+ VID = sys.argv[1]; OUT = sys.argv[2]
8
+ cap = cv2.VideoCapture(VID); Q = []
9
+ while True:
10
+ ok, q = cap.read()
11
+ if not ok or len(Q) > 125: break
12
+ Q.append(cv2.cvtColor(q, cv2.COLOR_BGR2GRAY))
13
+ sift = cv2.SIFT_create(nfeatures=6000, contrastThreshold=0.02)
14
+ bf = cv2.BFMatcher()
15
+ def mascara(f):
16
+ m = np.full((1080, 1080), 255, np.uint8)
17
+ t = f / 24.0
18
+ if 2.19 <= t <= 2.56: # cartão de detalhes voando (ocupa a metade de baixo): usa só a faixa de cima e a esquerda
19
+ m[:, :] = 0; m[0:300, :] = 255
20
+ if 4.40 <= t <= 4.80: # globo entrando: usa só a faixa do menu/barra
21
+ m[:, :] = 0; m[0:260, :] = 255
22
+ return m
23
+ cache = {}
24
+ def kp(f, mf):
25
+ key = (f, mf)
26
+ if key not in cache: cache[key] = sift.detectAndCompute(Q[f], mascara(mf))
27
+ return cache[key]
28
+ def casar(a, b):
29
+ """transformação tela a -> tela b (similaridade sem giro), usando a máscara do quadro mais crítico"""
30
+ mf = a if mascara(a).mean() < mascara(b).mean() else b
31
+ k1, d1 = kp(a, mf); k2, d2 = kp(b, mf)
32
+ if d1 is None or d2 is None or len(k1) < 8 or len(k2) < 8: return None, 0
33
+ mm = bf.knnMatch(d1, d2, k=2)
34
+ bons = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.75 * n.distance]
35
+ if len(bons) < 8: return None, len(bons)
36
+ p1 = np.float32([k1[m.queryIdx].pt for m in bons]); p2 = np.float32([k2[m.trainIdx].pt for m in bons])
37
+ A, inl = cv2.estimateAffinePartial2D(p1, p2, method=cv2.RANSAC, ransacReprojThreshold=3.0, maxIters=5000)
38
+ if A is None: return None, 0
39
+ s = float(np.hypot(A[0, 0], A[1, 0])); T = np.eye(3); T[0, 0] = T[1, 1] = s; T[:2, 2] = A[:, 2]
40
+ # refaz a translação com escala sem giro (média dos inliers)
41
+ i = inl.ravel().astype(bool)
42
+ T[:2, 2] = (p2[i] - s * p1[i]).mean(0)
43
+ return T, int(i.sum())
44
+ res = {}
45
+ for nome, ini, fim, ref in (("A-agenda", 0, 63, 62), ("B-docs", 63, 119, 117)):
46
+ S = {ref: np.eye(3)}; N = {ref: 999}
47
+ for f in range(ref + 1, fim + 1):
48
+ T, n = casar(f - 1, f); S[f] = (T if T is not None else np.eye(3)) @ S[f - 1]; N[f] = n
49
+ for f in range(ref - 1, ini - 1, -1):
50
+ T, n = casar(f, f + 1); S[f] = np.linalg.inv(T if T is not None else np.eye(3)) @ S[f + 1]; N[f] = n
51
+ lin = []
52
+ for f in range(ini, fim + 1):
53
+ s = S[f][0, 0]; w = np.linalg.inv(S[f]) @ np.array([540, 540, 1.0])
54
+ lin.append([round(f / 24, 4), round(float(np.log2(s)), 4), round(float(w[0]), 2), round(float(w[1]), 2), N[f]])
55
+ res[nome] = lin
56
+ json.dump(res, open(OUT, "w"))
57
+ for k, v in res.items():
58
+ print(k)
59
+ for l in v: print(" t=%.3f z=%.3f mx=%.1f my=%.1f n=%s" % tuple(l))
@@ -0,0 +1,111 @@
1
+ # Câmera por homografia: para cada quadro, H (mundo -> tela) casando com quadros âncora (SIFT), direto ou em cadeia.
2
+ # Decompõe H = P ∘ S: S = similaridade local no centro da tela (zoom, mira, giro -> controles do CAM)
3
+ # P = resto projetivo em tela (vira Corner Pin), guardado como deslocamento dos 4 cantos
4
+ # do quadro ampliado [-M, 1080+M] (M = margem do comp interno).
5
+ # uso: python ajustar_h.py <id> <saida.json> --ancora t [t ...] [--de t0] [--ate t1] [--margem 270] [--sem-ref]
6
+ import sys, json, math, cv2, numpy as np
7
+ P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
8
+ ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
9
+ a = sys.argv; cid = a[1]; out = a[2]
10
+ d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8")); ch = d["chaves"]
11
+ def lista(flag):
12
+ if flag not in a: return []
13
+ i = a.index(flag) + 1; r = []
14
+ while i < len(a) and not a[i].startswith("--"): r.append(float(a[i])); i += 1
15
+ return r
16
+ ancoras = lista("--ancora") or [d["ref"]]
17
+ t0 = lista("--de")[0] if "--de" in a else d["ini"]; t1 = lista("--ate")[0] if "--ate" in a else d["fim"]
18
+ MG = lista("--margem")[0] if "--margem" in a else 270.0
19
+ def interp(lst, t):
20
+ ts = [x for x, y in lst]
21
+ if t <= ts[0]: return lst[0][1]
22
+ if t >= ts[-1]: return lst[-1][1]
23
+ for i in range(1, len(ts)):
24
+ if ts[i] >= t:
25
+ u = (t - ts[i-1]) / (ts[i] - ts[i-1]); return lst[i-1][1] * (1-u) + lst[i][1] * u
26
+ def json_cam(t): return [interp(ch[n], t) for n in ("zoom", "mx", "my", "giro")]
27
+ def S_de(z, mx, my, g):
28
+ s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
29
+ T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
30
+ CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
31
+ def decompor(H):
32
+ H = H / H[2, 2]
33
+ c = np.linalg.inv(H) @ np.array([540, 540, 1.0]); w = c[:2] / c[2]
34
+ def Hp(x, y):
35
+ v = H @ np.array([x, y, 1.0]); return v[:2] / v[2]
36
+ e = 0.5; J = np.column_stack([(Hp(w[0] + e, w[1]) - Hp(w[0] - e, w[1])) / (2 * e), (Hp(w[0], w[1] + e) - Hp(w[0], w[1] - e)) / (2 * e)])
37
+ s = math.sqrt(abs(np.linalg.det(J))); g = math.degrees(math.atan2(J[1, 0] - J[0, 1], J[0, 0] + J[1, 1]))
38
+ S = S_de(math.log2(s), w[0], w[1], g)
39
+ Pm = H @ np.linalg.inv(S); Pm /= Pm[2, 2]
40
+ cp = cv2.perspectiveTransform(CANT[None], Pm)[0] - CANT
41
+ return [math.log2(s), float(w[0]), float(w[1]), g], cp.ravel().tolist()
42
+ def compor(cam, cp):
43
+ S = S_de(*cam); dst = (CANT + np.float32(cp).reshape(4, 2)).astype(np.float32)
44
+ Pm = cv2.getPerspectiveTransform(CANT, dst); return Pm @ S
45
+ cap = cv2.VideoCapture(ORIG); cache = {}
46
+ sift = cv2.SIFT_create(6000); bf = cv2.BFMatcher()
47
+ def quadro(f):
48
+ if f not in cache:
49
+ cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, q = cap.read(); g = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY)
50
+ cache[f] = sift.detectAndCompute(g, None)
51
+ return cache[f]
52
+ def homog(fa, fb):
53
+ k1, d1 = quadro(fa); k2, d2 = quadro(fb)
54
+ if d1 is None or d2 is None or len(k1) < 15 or len(k2) < 15: return None, 0
55
+ mm = bf.knnMatch(d1, d2, k=2); b = [m for m, n in (x for x in mm if len(x) == 2) if m.distance < 0.75 * n.distance]
56
+ if len(b) < 20: return None, 0
57
+ p1 = np.float32([k1[m.queryIdx].pt for m in b]); p2 = np.float32([k2[m.trainIdx].pt for m in b])
58
+ H, inl = cv2.findHomography(p1, p2, cv2.RANSAC, 3.0)
59
+ if H is None: return None, 0
60
+ n = int(inl.sum())
61
+ if n < 25 or n / len(b) < 0.3: return None, n
62
+ q = p2[inl.ravel() > 0]
63
+ if q[:, 0].std() < 50 or q[:, 1].std() < 50: return None, n
64
+ return H, n
65
+ f0 = int(math.ceil(t0 * 24 - 1e-6)); f1 = int(math.floor(t1 * 24 + 1e-6))
66
+ fa = [int(round(t * 24)) for t in ancoras]
67
+ sol = {}
68
+ for f in fa: sol[f] = (S_de(*json_cam(f / 24.0)), "ancora", 9999)
69
+ def plausivel(H, prev):
70
+ cam, cp = decompor(H)
71
+ if prev is None: return True
72
+ pc, pp = decompor(prev)
73
+ if abs(cam[0] - pc[0]) > 0.6 or math.hypot(cam[1] - pc[1], cam[2] - pc[2]) * 2 ** cam[0] > 500: return False
74
+ if max(abs(v) for v in cp) > 1500: return False
75
+ return True
76
+ ordem = sorted(fa)
77
+ for i, A in enumerate(ordem):
78
+ esq = (ordem[i-1] + A) // 2 if i > 0 else f0 - 1
79
+ dir_ = (A + ordem[i+1]) // 2 if i + 1 < len(ordem) else f1 + 1
80
+ for passo, lim in ((-1, esq), (1, dir_)):
81
+ viz = A; f = A + passo
82
+ while (f > lim if passo < 0 else f < lim):
83
+ melhor = None
84
+ H, n = homog(A, f)
85
+ if H is not None and plausivel(H @ sol[A][0], sol[viz][0]):
86
+ melhor = (H @ sol[A][0], "direto %.2f" % (A / 24.0), n)
87
+ else:
88
+ H2, n2 = homog(viz, f)
89
+ if H2 is not None and plausivel(H2 @ sol[viz][0], sol[viz][0]):
90
+ melhor = (H2 @ sol[viz][0], "cadeia", n2)
91
+ if melhor:
92
+ sol[f] = melhor; viz = f
93
+ f += passo
94
+ ok = sorted(k for k in sol if f0 <= k <= f1)
95
+ dec = {k: decompor(sol[k][0]) for k in ok}
96
+ corr = {k: [dec[k][0][j] - json_cam(k / 24.0)[j] for j in range(4)] for k in ok}
97
+ linhas = []
98
+ for f in range(f0, f1 + 1):
99
+ jc = json_cam(f / 24.0)
100
+ if f in dec:
101
+ cam, cp = dec[f]; met = sol[f][1]
102
+ elif ok:
103
+ c = [float(np.interp(f, ok, [corr[k][j] for k in ok])) for j in range(4)]
104
+ cam = [jc[j] + c[j] for j in range(4)]
105
+ cp = [float(np.interp(f, ok, [dec[k][1][j] for k in ok])) for j in range(8)]; met = "buraco"
106
+ else:
107
+ cam, cp, met = jc, [0.0] * 8, "json"
108
+ linhas.append({"t": round(f / 24.0, 4), "zoom": cam[0], "mx": cam[1], "my": cam[2], "giro": cam[3], "cp": cp, "met": met})
109
+ print("%.3f %-12s z%6.3f M(%7.1f,%7.1f) g%6.2f | pin max %5.1f | json z%6.3f M(%7.1f,%7.1f) g%6.2f" % (
110
+ f / 24.0, met, cam[0], cam[1], cam[2], cam[3], max(abs(v) for v in cp), jc[0], jc[1], jc[2], jc[3]))
111
+ json.dump({"cena": cid, "ancoras": ancoras, "margem": MG, "quadros": linhas}, open(out, "w"), indent=0)
@@ -0,0 +1,17 @@
1
+ """Correção manual (conferida no quadro do original) somada a um canal da câmera ajustada.
2
+ uso: python camera/wg_corrige_cam.py <id> <canal: zoom|mx|my|giro> t:delta [t:delta ...]
3
+ Os deltas são interpolados linearmente entre os tempos dados e valem 0 fora do intervalo.
4
+ Grava um histórico em "correcoes" dentro do JSON (para saber o que foi mexido à mão)."""
5
+ import sys, json, numpy as np
6
+ P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
7
+ cid, canal = sys.argv[1], sys.argv[2]
8
+ pts = sorted([tuple(float(v) for v in a.split(":")) for a in sys.argv[3:]])
9
+ arq = P + "camera/cena-%s-ajustada.json" % cid
10
+ d = json.load(open(arq))
11
+ ts = [p[0] for p in pts]; vs = [p[1] for p in pts]
12
+ for k in d["chaves"][canal]:
13
+ t = k[0]
14
+ if ts[0] <= t <= ts[-1]: k[1] = round(k[1] + float(np.interp(t, ts, vs)), 4)
15
+ d.setdefault("correcoes", []).append({"canal": canal, "pontos": pts})
16
+ json.dump(d, open(arq, "w"))
17
+ print("ok", cid, canal, pts)
@@ -0,0 +1,80 @@
1
+ # Câmera por cadeia ECC quadro a quadro (para cenas de pouca textura, ex.: K-maps).
2
+ # uso: python ecc_cadeia.py <id> <saida.json> <t_ancora> [--de t0] [--ate t1] [--margem 270] [--modelo affine|homog]
3
+ # A âncora usa a câmera json; cada passo alinha o quadro f com f±1 (ECC multiescala) começando pelo movimento do json.
4
+ import sys, json, math, cv2, numpy as np
5
+ P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
6
+ ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
7
+ a = sys.argv; cid = a[1]; out = a[2]; tanc = float(a[3])
8
+ d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8")); ch = d["chaves"]
9
+ def opt(flag, dflt):
10
+ return a[a.index(flag) + 1] if flag in a else dflt
11
+ t0 = float(opt("--de", d["ini"])); t1 = float(opt("--ate", d["fim"])); MG = float(opt("--margem", 270)); modelo = opt("--modelo", "homog")
12
+ def interp(lst, t):
13
+ ts = [x for x, y in lst]
14
+ if t <= ts[0]: return lst[0][1]
15
+ if t >= ts[-1]: return lst[-1][1]
16
+ for i in range(1, len(ts)):
17
+ if ts[i] >= t:
18
+ u = (t - ts[i-1]) / (ts[i] - ts[i-1]); return lst[i-1][1] * (1-u) + lst[i][1] * u
19
+ def json_cam(t): return [interp(ch[n], t) for n in ("zoom", "mx", "my", "giro")]
20
+ def S_de(z, mx, my, g):
21
+ s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
22
+ T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
23
+ CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
24
+ def decompor(H):
25
+ H = H / H[2, 2]; c = np.linalg.inv(H) @ np.array([540, 540, 1.0]); w = c[:2] / c[2]
26
+ def Hp(x, y):
27
+ v = H @ np.array([x, y, 1.0]); return v[:2] / v[2]
28
+ e = 0.5; J = np.column_stack([(Hp(w[0] + e, w[1]) - Hp(w[0] - e, w[1])) / (2 * e), (Hp(w[0], w[1] + e) - Hp(w[0], w[1] - e)) / (2 * e)])
29
+ s = math.sqrt(abs(np.linalg.det(J))); g = math.degrees(math.atan2(J[1, 0] - J[0, 1], J[0, 0] + J[1, 1]))
30
+ S = S_de(math.log2(s), w[0], w[1], g); Pm = H @ np.linalg.inv(S); Pm /= Pm[2, 2]
31
+ cp = cv2.perspectiveTransform(CANT[None], Pm)[0] - CANT
32
+ return [math.log2(s), float(w[0]), float(w[1]), g], cp.ravel().tolist()
33
+ cap = cv2.VideoCapture(ORIG); cache = {}
34
+ def quadro(f):
35
+ if f not in cache:
36
+ cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, q = cap.read(); cache[f] = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
37
+ return cache[f]
38
+ def ecc(fa, fb, H0):
39
+ """H tela(fa)->tela(fb). ECC acha W com I_b(W x) ~ I_a(x)? Usamos template = fb, input = fa: W leva coords de fb -> fa."""
40
+ A = quadro(fa); B = quadro(fb)
41
+ W = np.linalg.inv(H0) # fb -> fa
42
+ W = W / W[2, 2]
43
+ cc = -1
44
+ for sc, bl in ((0.125, 5), (0.25, 5), (0.5, 3)):
45
+ As = cv2.GaussianBlur(cv2.resize(A, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
46
+ Bs = cv2.GaussianBlur(cv2.resize(B, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
47
+ Ssc = np.diag([sc, sc, 1.0]); Ws = Ssc @ W @ np.linalg.inv(Ssc)
48
+ try:
49
+ if modelo == "homog":
50
+ cc, Ws = cv2.findTransformECC(Bs, As, Ws.astype(np.float32), cv2.MOTION_HOMOGRAPHY, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
51
+ else:
52
+ cc, W2 = cv2.findTransformECC(Bs, As, Ws[:2].astype(np.float32), cv2.MOTION_AFFINE, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
53
+ Ws = np.vstack([W2, [0, 0, 1]])
54
+ except cv2.error:
55
+ return None, -1
56
+ W = np.linalg.inv(Ssc) @ Ws @ Ssc
57
+ return np.linalg.inv(W), cc
58
+ f0 = int(math.ceil(t0 * 24 - 1e-6)); f1 = int(math.floor(t1 * 24 + 1e-6)); fa = int(round(tanc * 24))
59
+ sol = {fa: (S_de(*json_cam(fa / 24.0)), 1.0)}
60
+ for passo, lim in ((1, f1), (-1, f0)):
61
+ f = fa
62
+ while (f < lim if passo > 0 else f > lim):
63
+ g = f + passo
64
+ H0 = S_de(*json_cam(g / 24.0)) @ np.linalg.inv(S_de(*json_cam(f / 24.0))) # movimento do json
65
+ H, cc = ecc(f, g, H0)
66
+ if H is None or cc < 0.5:
67
+ H, cc = H0, -1 # sem alinhamento: segue o json neste passo
68
+ elif "--sim" in a:
69
+ # projeta o passo numa similaridade (sem cisalhamento acumulado), preservando o centro da tela
70
+ H = H / H[2, 2]; A2 = H[:2, :2]; sc = math.sqrt(abs(np.linalg.det(A2))); rr = math.atan2(A2[1, 0] - A2[0, 1], A2[0, 0] + A2[1, 1])
71
+ c = H @ np.array([540, 540, 1.0]); c = c[:2] / c[2]
72
+ R2 = sc * np.array([[math.cos(rr), -math.sin(rr)], [math.sin(rr), math.cos(rr)]])
73
+ Hs = np.eye(3); Hs[:2, :2] = R2; Hs[:2, 2] = c - R2 @ np.array([540, 540]); H = Hs
74
+ sol[g] = (H @ sol[f][0], cc); f = g
75
+ linhas = []
76
+ for f in range(f0, f1 + 1):
77
+ cam, cp = decompor(sol[f][0]); jc = json_cam(f / 24.0)
78
+ linhas.append({"t": round(f / 24.0, 4), "zoom": cam[0], "mx": cam[1], "my": cam[2], "giro": cam[3], "cp": cp, "met": "ecc %.2f" % sol[f][1]})
79
+ print("%.3f ecc%5.2f z%6.3f M(%7.1f,%7.1f) g%6.2f pin %5.1f | json z%6.3f M(%7.1f,%7.1f) g%6.2f" % (f / 24.0, sol[f][1], cam[0], cam[1], cam[2], cam[3], max(abs(v) for v in cp), jc[0], jc[1], jc[2], jc[3]))
80
+ json.dump({"cena": cid, "ancoras": [tanc], "margem": MG, "quadros": linhas}, open(out, "w"), indent=0)
@@ -0,0 +1,65 @@
1
+ """Câmera ajustada das cenas G, H, I, K (agente w-google, 17/10).
2
+
3
+ A câmera medida (cena-<id>.json) confundia a inclinação 3D das interfaces com giro e errava zoom/mira nos
4
+ chicotes. Aqui ela é refeita por homografia contra o quadro de referência (SIFT, direto ou em cadeia) e
5
+ decomposta em: similaridade no centro da tela (controles do CAM: zoom, mira, giro) + resto projetivo
6
+ (Corner Pin no comp da cena, guardado como deslocamento dos 4 cantos do quadro ampliado com margem).
7
+ Entradas: JSON por quadro gerados por ajustar_h.py / ecc_cadeia.py (scratchpad do agente; o essencial está aqui).
8
+ Saída: camera/cena-<id>-ajustada.json no formato do camera.jsx ({chaves: zoom, mx, my, giro}) + "pin" e "margem".
9
+ Regras: Savitzky-Golay de 5 quadros; pin zerado no quadro da troca e misturado (smoothstep) até a primeira
10
+ medida boa, para o encaixe encher a tela exatamente na troca.
11
+ uso: python camera/wg_finalizar_cam.py <id> <entrada.json> [--pin-zero t_ini t_fim_rampa] [--json-ate t] [--mistura t0 t1]
12
+ """
13
+ import sys, json, math, numpy as np
14
+ from scipy.signal import savgol_filter
15
+ a = sys.argv; cid = a[1]; ent = json.load(open(a[2]))
16
+ P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
17
+ q = ent["quadros"]; t = np.array([x["t"] for x in q])
18
+ z = np.array([x["zoom"] for x in q]); mx = np.array([x["mx"] for x in q]); my = np.array([x["my"] for x in q]); g = np.array([x["giro"] for x in q])
19
+ cp = np.array([x.get("cp", [0.0] * 8) for x in q])
20
+ def ss(u): u = min(1, max(0, u)); return u * u * (3 - 2 * u)
21
+ if "--pin-zero" in a:
22
+ i = a.index("--pin-zero"); ta, tb = float(a[i + 1]), float(a[i + 2])
23
+ jb = int(np.argmin(abs(t - tb))); alvo = cp[jb].copy()
24
+ for j in range(len(t)):
25
+ if t[j] <= ta + 1e-6: cp[j] = 0
26
+ elif t[j] < tb: cp[j] = alvo * ss((t[j] - ta) / (tb - ta))
27
+ if "--mistura" in a: # troca suave entre a câmera json (antes de t0) e a ajustada (depois de t1)
28
+ i = a.index("--mistura"); t0, t1 = float(a[i + 1]), float(a[i + 2])
29
+ d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8"))["chaves"]
30
+ def jv(n, tt): return float(np.interp(tt, [x[0] for x in d[n]], [x[1] for x in d[n]]))
31
+ for j in range(len(t)):
32
+ if t[j] < t1:
33
+ w = ss((t[j] - t0) / (t1 - t0)) if t[j] > t0 else 0.0
34
+ z[j] = jv("zoom", t[j]) * (1 - w) + z[j] * w; mx[j] = jv("mx", t[j]) * (1 - w) + mx[j] * w
35
+ my[j] = jv("my", t[j]) * (1 - w) + my[j] * w; g[j] = jv("giro", t[j]) * (1 - w) + g[j] * w
36
+ cp[j] = cp[j] * w
37
+ z, mx, my, g = [savgol_filter(v, 5, 2) for v in (z, mx, my, g)]
38
+ cp = savgol_filter(cp, 5, 2, axis=0)
39
+ if "--pin-zero" in a:
40
+ for j in range(len(t)):
41
+ if t[j] <= ta + 1e-6: cp[j] = 0
42
+ src = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8"))
43
+ out = {"fps": 24, "ini": src["ini"], "fim": src["fim"], "ref": src["ref"], "pegada": src["pegada"], "margem": ent.get("margem", 270),
44
+ "origem": "wg_finalizar_cam.py (homografia + Corner Pin)",
45
+ "chaves": {"zoom": [[round(float(t[j]), 4), round(float(z[j]), 4)] for j in range(len(t))],
46
+ "mx": [[round(float(t[j]), 4), round(float(mx[j]), 3)] for j in range(len(t))],
47
+ "my": [[round(float(t[j]), 4), round(float(my[j]), 3)] for j in range(len(t))],
48
+ "giro": [[round(float(t[j]), 4), round(float(g[j]), 3)] for j in range(len(t))]},
49
+ "pin": [[round(float(t[j]), 4)] + [round(float(v), 2) for v in cp[j]] for j in range(len(t))]}
50
+ json.dump(out, open(P + "camera/cena-%s-ajustada.json" % cid, "w"))
51
+ print("ok", cid, len(t), "quadros; pin max %.1f" % abs(cp).max())
52
+ # --- cantos do Corner Pin para a margem real do comp interno (alvo em coordenadas da camada interna)
53
+ import cv2
54
+ MR = float(a[a.index("--margem-real") + 1]) if "--margem-real" in a else out["margem"]
55
+ MG = out["margem"]
56
+ CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
57
+ REAL = np.float32([[[-MR, -MR], [1080 + MR, -MR], [-MR, 1080 + MR], [1080 + MR, 1080 + MR]]])
58
+ cantos = []
59
+ for row in out["pin"]:
60
+ Pm = cv2.getPerspectiveTransform(CANT, (CANT + np.float32(row[1:]).reshape(4, 2)).astype(np.float32))
61
+ alvo = cv2.perspectiveTransform(REAL, Pm)[0] + MR
62
+ cantos.append([row[0]] + [round(float(v), 2) for v in alvo.ravel()])
63
+ out["margem_real"] = MR; out["cantos"] = cantos
64
+ json.dump(out, open(P + "camera/cena-%s-ajustada.json" % cid, "w"))
65
+ print("cantos para margem real", MR)
@@ -0,0 +1,61 @@
1
+ # Mosaico do mundo com a câmera AJUSTADA (inclui rx, ry): python mosaico2.py <cam.json> <k> <saida.png> <x0 y0 x1 y1> [t0 t1] [--folha t,t,t saida_folha.png]
2
+ import sys, json, math, cv2, numpy as np
3
+ ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
4
+ a = sys.argv; cam = json.load(open(a[1])); k = float(a[2]); out = a[3]; x0, y0, x1, y1 = [float(v) for v in a[4:8]]
5
+ rest = [v for v in a[8:] if not v.startswith("--")]
6
+ t0 = float(rest[0]) if len(rest) > 0 else -1; t1 = float(rest[1]) if len(rest) > 1 else 99
7
+ F = 1500.0
8
+ def projetar(p, W):
9
+ z, mx, my, g, rx, ry = p; s = 2 ** z; r = math.radians(g)
10
+ X = W[:, 0] - mx; Y = W[:, 1] - my
11
+ x = s * (math.cos(r) * X - math.sin(r) * Y); y = s * (math.sin(r) * X + math.cos(r) * Y); zz = np.zeros_like(x)
12
+ cy, sy = math.cos(math.radians(ry)), math.sin(math.radians(ry)); x, zz = x * cy + zz * sy, -x * sy + zz * cy
13
+ cx, sx = math.cos(math.radians(rx)), math.sin(math.radians(rx)); y, zz = y * cx - zz * sx, y * sx + zz * cx
14
+ return np.column_stack([540 + F * x / (F + zz), 540 + F * y / (F + zz)])
15
+ MG = cam.get("margem", 270.0)
16
+ CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
17
+ def S_de(z, mx, my, g):
18
+ s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
19
+ T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
20
+ def Hq(q):
21
+ S = S_de(q["zoom"], q["mx"], q["my"], q["giro"])
22
+ if "cp" in q:
23
+ dst = (CANT + np.float32(q["cp"]).reshape(4, 2)).astype(np.float32); return cv2.getPerspectiveTransform(CANT, dst) @ S
24
+ p = [q[n] for n in ("zoom", "mx", "my", "giro", "rx", "ry")]
25
+ W = np.float32([[p[1] - 300, p[2] - 300], [p[1] + 300, p[2] - 300], [p[1] - 300, p[2] + 300], [p[1] + 300, p[2] + 300]])
26
+ return cv2.getPerspectiveTransform(W, projetar(p, W.astype(np.float64)).astype(np.float32))
27
+ Wd = int((x1 - x0) * k); Hd = int((y1 - y0) * k)
28
+ C = np.array([[k, 0, -x0 * k], [0, k, -y0 * k], [0, 0, 1.0]]) # mundo -> canvas
29
+ canvas = np.zeros((Hd, Wd, 3), np.uint8); qual = np.full((Hd, Wd), -1e9, np.float32)
30
+ cap = cv2.VideoCapture(ORIG); Q = {}
31
+ qs = cam["quadros"]
32
+ for i, q in enumerate(qs):
33
+ t = q["t"]
34
+ if t < t0 - 1e-6 or t > t1 + 1e-6: continue
35
+ f = int(round(t * 24)); cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, fr = cap.read()
36
+ if not ok: continue
37
+ H = Hq(q) # mundo -> tela
38
+ T = H @ np.linalg.inv(C) # canvas -> tela
39
+ w = cv2.warpPerspective(fr, T, (Wd, Hd), flags=cv2.INTER_LINEAR | cv2.WARP_INVERSE_MAP)
40
+ m = cv2.warpPerspective(np.full(fr.shape[:2], 255, np.uint8), T, (Wd, Hd), flags=cv2.INTER_NEAREST | cv2.WARP_INVERSE_MAP) > 0
41
+ m = cv2.erode(m.astype(np.uint8), np.ones((3, 3), np.uint8)) > 0
42
+ nx = qs[min(i + 1, len(qs) - 1)]
43
+ spd = math.hypot(nx["mx"] - q["mx"], nx["my"] - q["my"]) * 2 ** q["zoom"] + abs(nx["zoom"] - q["zoom"]) * 370 + abs(nx.get("rx", 0) - q.get("rx", 0)) * 9
44
+ qq = 2 ** q["zoom"] / (1 + spd / 6) if q["met"] not in ("buraco", "json") else -1e6
45
+ u = m & (qq > qual); canvas[u] = w[u]; qual[u] = qq
46
+ cv2.imwrite(out, canvas)
47
+ g = canvas.copy()
48
+ for gx in range(int(math.ceil(x0 / 100)) * 100, int(x1), 100):
49
+ X = int((gx - x0) * k); cv2.line(g, (X, 0), (X, Hd), (0, 0, 255) if gx % 500 == 0 else (0, 128, 255), 1); cv2.putText(g, str(gx), (X + 2, 14), 0, 0.4, (0, 0, 255), 1)
50
+ for gy in range(int(math.ceil(y0 / 100)) * 100, int(y1), 100):
51
+ Y = int((gy - y0) * k); cv2.line(g, (0, Y), (Wd, Y), (0, 0, 255) if gy % 500 == 0 else (0, 128, 255), 1); cv2.putText(g, str(gy), (2, Y - 2), 0, 0.4, (0, 0, 255), 1)
52
+ cv2.imwrite(out.replace(".png", "-grade.png"), g)
53
+ if "--folha" in a:
54
+ i = a.index("--folha"); tt = [float(v) for v in a[i + 1].split(",")]; fo = a[i + 2]; tiles = []
55
+ for t in tt:
56
+ q = min(qs, key=lambda x: abs(x["t"] - t)); f = int(round(q["t"] * 24)); cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, fr = cap.read()
57
+ pr = cv2.warpPerspective(canvas, Hq(q) @ np.linalg.inv(C), (1080, 1080))
58
+ row = np.hstack([cv2.resize(fr, (360, 360)), cv2.resize(pr, (360, 360)), cv2.resize(cv2.absdiff(fr, pr), (360, 360))])
59
+ cv2.putText(row, "%.3f %s" % (q["t"], q["met"]), (4, 16), 0, 0.5, (0, 255, 255), 1); tiles.append(row)
60
+ cv2.imwrite(fo, np.vstack(tiles))
61
+ print("ok", out, Wd, Hd)
@@ -0,0 +1,55 @@
1
+ # Lyric edit em interfaces · 01 · Gramática e roteiro
2
+
3
+ > Data: 2026-10-17 · Tipo: tutorial · Status: vigente
4
+
5
+ **Resumo:** o que faz o estilo funcionar (medido nos 3 edits do @subzmotion), como escolher a música, como transformar cada frase numa interface e como montar o roteiro de cenas com os campos de letra. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
6
+
7
+ ## 1. A gramática (o que não pode faltar)
8
+
9
+ 1. **A palavra vira lugar.** Cada frase é digitada no campo que faz sentido para ela. Interface reconhecível em todas as cenas.
10
+ 2. **Digitação no tempo da voz.** Letra a letra com cursor "|" (1–3 quadros por letra), começando até 0,35 s antes da palavra cantada. Em títulos e balões, a palavra também pode cair desfocada no lugar em 3–6 quadros. Interface de letra (Spotify) = karaokê, palavra cantada acesa.
11
+ 3. **Cena dentro de cena.** A cena anterior continua tocando dentro de um elemento pequeno da seguinte (avatar, ícone de app, foto de jornal, miniatura de janela, marcador de mapa). A máscara morfa (retângulo → círculo ou quadrado arredondado) enquanto a câmera recua. Nada de corte seco.
12
+ 4. **Câmera que nunca para**, em cadeia de nulls (parte 02). Só o último meio segundo fica parado.
13
+ 5. **3D sem câmera:** jornal, celular, livro como camadas 3D deitadas, vistas pela câmera padrão da comp.
14
+ 6. **Borrão de movimento** ligado, obturador ~90° (rastro ≈ ¼ do deslocamento do quadro).
15
+ 7. **Formato:** 1080×1080 a 24 qps, música da biblioteca do Instagram (ou o mix), sem efeito sonoro além da música no original. No nosso: card do Spotify no fim e abertura do celular (parte 05).
16
+
17
+ Por que viraliza (inferência da pesquisa): música conhecida + interface que todo mundo usa = reconhecimento imediato a cada frase; take único com câmera viva; a timeline postada no dia seguinte vira prova de trabalho manual.
18
+
19
+ ## 2. Música e trecho
20
+
21
+ - Trecho de 30–40 s, de preferência o refrão ou a parte mais conhecida; 5–8 versos curtos funcionam melhor que frases longas.
22
+ - Áudio: o arquivo da música (ou o do Reel de referência, só como referência interna) em `audio/musica.wav`.
23
+ - **Letra:** o autor copia a letra inteira (Google, Spotify) e cola em `ae/LETRA-COMPLETA.txt`. Pode ser a música inteira: o script acha o trecho. O Claude não escreve nem copia letra de música; explique isso em uma linha se ele pedir.
24
+ - **Idioma:** a interface acompanha a música (música em inglês → interface em inglês). Nada de traduzir a letra.
25
+
26
+ ## 3. Frase → interface (mapa que já funcionou)
27
+
28
+ | A frase fala de… | Interface | Exemplo do On My Way |
29
+ |---|---|---|
30
+ | tempo, compromisso, "quando" | Google Agenda: título de evento, cartão de detalhes | 0–2,6 s |
31
+ | procurar, querer, perguntar | busca do Google, Docs, painel de conhecimento | 2,9–4,4 s; 16,7–19 s; 22,4–24,9 s |
32
+ | mundo, lugar grande | globo com texto curvo | 4,4–5,0 s |
33
+ | trocar de assunto | alt-tab do Windows / app switcher | 5,0–6,0 s |
34
+ | ouvir, cantar | Spotify em tela cheia de letra (karaokê) | 6,0–8,3 s |
35
+ | falar de alguém, status | notas do Instagram (balão em cima do avatar) | 8,3–11 s |
36
+ | afirmação forte, manchete | jornal 3D sobre mesa | 11–16,7 s; 24,9–28,2 s |
37
+ | caminho, chegar, "onde" | mapa de jogo (etiqueta-seta) ou Google Maps (pino, busca, avatar andando) | 19–22,3 s; 28,2–33,9 s |
38
+ | mensagem para alguém | WhatsApp, DM, iMessage | Rockabye e Love Story |
39
+
40
+ Outras do autor: Apple Maps, feed do Instagram, polaroides na mesa, livro que fecha, post-it na parede, widgets do iOS ("Hard Times").
41
+
42
+ ## 4. Roteiro de cenas (como montar)
43
+
44
+ 1. `python -P audio/tempos_da_voz.py audio/musica.wav`: tempo de cada palavra cantada, por verso, sem guardar a letra (`audio/musica-tempos.txt`). É a régua do roteiro. (`ae/letra_automatica.py --teste` confere o encaixe dos campos sem texto nenhum.)
45
+ 2. Divida em cenas-mundo de 1,5–6 s. Cada cena tem: tempo de início/fim, interface, quais palavras são digitadas em quais campos, e **o buraco** (elemento onde a cena anterior entra) ou a janela (onde a próxima abre, se for avanço).
46
+ 3. Escreva o `ROTEIRO-CENAS.md` do projeto (molde: o do projeto-modelo) e a lista de campos em `ae/letra.json`: um slot por campo, com `onde`, `palavras` (quantas palavras o campo mostra) e `canta_em` (os tempos). O `letra_automatica.py` usa janelas de tempo por campo (constante `JANELAS`) e regras de caixa alta / inicial maiúscula (`CAIXA_ALTA`, `INICIAL_MAIUSCULA`): ajuste para a música nova.
47
+ 4. Depois que o autor colar a letra: `python -P ae/letra_automatica.py`. Ele imprime "campo · palavras (colado / original)"; diferença de ±1 em algum campo quase sempre é uma palavra que o Whisper ouviu diferente e caiu na janela vizinha: mova a divisa entre as duas janelas (foi o caso de I2/J1 no On My Way).
48
+ 5. Pontuação e grafia vêm do texto colado (apóstrofo reto, grafia americana). Se ele quiser idêntico ao original, ele corrige no próprio json.
49
+
50
+ ## 5. Personagens, imagens e fontes
51
+
52
+ - **Pessoas:** personagens nossos pelo GPT Image 2 (`python -P 08-integracoes/codex-imagem.py "prompt em inglês" saida.png`, ~50 s cada, até 3 em paralelo). Gere os que aparecem em várias cenas uma vez só em `assets/comum/` (avatar, papel de parede) e reutilize. Nada de rosto real conhecido nem marca de jogo: vira "Studio Quadro" e afins.
53
+ - **Fotos e texturas:** mesa de madeira vista de cima, mapa de satélite estilo jogo, céu, foto antiga P&B, renders abstratos para painéis: tudo gerado.
54
+ - **Ícones de apps:** oficiais do simpleicons/Wikimedia.
55
+ - **Fontes que usamos:** Google Sans Flex (Google), Figtree (Spotify), Roboto (Instagram/Maps), Segoe UI (Windows), Noto Serif Display ExtraCondensed (manchetes), Poppins SemiBold (texto curvo do globo). Fonte nova vai em `C:\Program Files\Common Files\Adobe\Fonts` e só aparece depois de reiniciar a cópia do AE.