primocode 10.16.0 → 10.17.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 (234) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +11 -2
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/nuvem.js +11 -1
  222. package/lib/passagem.js +50 -0
  223. package/lib/perigo.js +19 -11
  224. package/lib/protecao.js +134 -0
  225. package/lib/titulo-conversa.js +46 -0
  226. package/lib/tools.js +3 -0
  227. package/lib/ui.js +1 -1
  228. package/package.json +3 -2
  229. package/skills/editar-meu-video/SKILL.md +6 -2
  230. package/skills/editar-video/SKILL.md +113 -0
  231. package/studio/pessoa.py +52 -0
  232. package/voz/primo_voz/app.py +43 -4
  233. package/voz/primo_voz/chat.html +17 -6
  234. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,426 @@
1
+ """Live camera by default: writes in projeto.json the base zoom that alternates sentence by sentence and the
2
+ punch-ins on the words that carry the emphasis.
3
+
4
+ Reads the words of the cut (`<EDIT>/transcripts/cut.json`, with the start measured on the wave when
5
+ `palavras_onda.json` exists), the cuts (`cut_timeline.json`) and the loudness of the audio. Then, in the stretches
6
+ of the video that have no block yet (or only a plain `tela_cheia`):
7
+
8
+ - one `tela_cheia` block per sentence, base zoom alternating between 1.0 and 1.12 (a long sentence is split at a
9
+ comma or at a cut, so something changes on screen every 2 to 4 s);
10
+ - one `zooms` punch-in on the face (1.25 over the 1.0 base, 1.3 over the 1.12 base) on the emphasis word of the
11
+ sentence: a number, a negation, or the loudest word. At least MIN_GAP_S between two punch-ins.
12
+
13
+ A scene of `cenas_proprias` with `"sobre_a_pessoa": true` (a small card above the head of the person, who stays on
14
+ screen) marks a stretch where the camera does NOT come closer: there the base zoom stays 1.0 and no punch-in is
15
+ written, so the head cannot rise under the card. Its "de" and "ate" are seconds or a word ("ate" missing = to the end).
16
+ (PrimoCode) The pieces that sit over the head (`lettering`, `rotulo`, `carimbo`) get the same calm stretch: with the
17
+ 1.12 base zoom the head rose under the title and the text ended up glued to the forehead. Under a big title on top
18
+ (a `lettering` that is not "base"/"meio", with the captions out of the stretch) the block is 1.12 with
19
+ `"foco_y": 0.62`: the face goes DOWN in the frame and the top opens for the text.
20
+
21
+ It only uses what the build already does (`zooms` in `tela_cheia`, references/enquadramento.md). Blocks written by
22
+ hand that are not a plain `tela_cheia` (`prova`, `contida`, `moldura`, `tela_verde`, `comparar`, or a `tela_cheia`
23
+ with its own `zooms`, `acompanhar` or `"camera_viva": false`) are left exactly as they are. Everything written here
24
+ carries `"auto": "camera_viva"`: running again replaces only that, and `--desfazer` removes it.
25
+
26
+ Usage:
27
+ uv run python helpers/camera_viva.py <PROJ> [--desfazer] [--min-gap 2.5] [--json]
28
+ Exit code 0 when projeto.json was written (or nothing had to change), 2 on error.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import json
35
+ import re
36
+ import sys
37
+ import unicodedata
38
+ from pathlib import Path
39
+
40
+ MARCA = "camera_viva"
41
+ ZOOM_A, ZOOM_B = 1.0, 1.12
42
+ PUNCH_A, PUNCH_B = 1.25, 1.3
43
+ MIN_GAP_S = 2.5
44
+ LONGA_S = 4.5 # a sentence longer than this is split in two
45
+ CURTA_S = 1.2 # a sentence shorter than this joins the next one
46
+ PUNCH_FICA_S = 0.6 # how long the punch-in stays after the word ends
47
+ SNAP_S = 0.25 # a sentence start this close to a cut moves to the cut
48
+ FOCO_TITULO = 0.62 # (PrimoCode) under a big title on top: the face center goes this low in the frame (build: foco_y)
49
+
50
+ NEGACOES = {"nao", "nunca", "nem", "nada", "ninguem", "jamais", "nenhum", "nenhuma", "sem", "zero"}
51
+ NUMEROS = {"um", "uma", "dois", "duas", "tres", "quatro", "cinco", "seis", "sete", "oito", "nove", "dez", "onze",
52
+ "doze", "treze", "catorze", "quatorze", "quinze", "vinte", "trinta", "quarenta", "cinquenta", "sessenta",
53
+ "setenta", "oitenta", "noventa", "cem", "cento", "duzentos", "trezentos", "quinhentos", "mil", "milhao",
54
+ "milhoes", "bilhao", "bilhoes", "dobro", "triplo", "metade", "primeiro", "primeira", "segundo", "segunda",
55
+ "terceiro", "terceira"}
56
+ ARTIGOS_NUM = {"um", "uma"} # "um" and "uma" are almost always the article: they only count with energy
57
+ FRACAS = {"a", "o", "as", "os", "de", "da", "do", "das", "dos", "e", "que", "em", "no", "na", "nos", "nas", "um",
58
+ "uma", "por", "pra", "para", "com", "se", "eu", "ele", "ela", "voce", "me", "te", "foi", "era", "isso",
59
+ "esse", "essa", "este", "esta", "meu", "minha", "seu", "sua", "la", "ja", "mas", "ou", "como", "so", "ai",
60
+ "ne", "tem", "ter", "vai", "vou", "ao", "aos", "quando", "porque", "quem", "tudo", "cada", "mesma", "mesmo"}
61
+
62
+
63
+ class Erro(Exception):
64
+ pass
65
+
66
+
67
+ def norm(s: str) -> str:
68
+ s = unicodedata.normalize("NFD", str(s).lower())
69
+ return re.sub(r"[^a-z0-9%$]", "", "".join(c for c in s if unicodedata.category(c) != "Mn"))
70
+
71
+
72
+ def ler_json(p: Path):
73
+ with open(p, encoding="utf-8") as f:
74
+ return json.load(f)
75
+
76
+
77
+ def palavras_do_corte(edit: Path) -> list[dict]:
78
+ """Words of the cut with the start measured on the wave when palavras_onda.json matches the transcript."""
79
+ p = edit / "transcripts" / "cut.json"
80
+ if not p.is_file():
81
+ raise Erro(f"falta {p}: termine o corte primeiro (render_cut.py e a transcrição do cut.mp4)")
82
+ ws = [dict(text=w["text"].strip(), start=float(w["start"]), end=float(w["end"]))
83
+ for w in ler_json(p)["words"] if w["text"].strip()]
84
+ po = edit / "palavras_onda.json"
85
+ if po.is_file():
86
+ try:
87
+ lista = ler_json(po).get("palavras") or []
88
+ except (ValueError, AttributeError):
89
+ lista = []
90
+ por_hora: dict[float, list[dict]] = {}
91
+ for r in lista:
92
+ try:
93
+ por_hora.setdefault(round(float(r["whisper"]), 2), []).append(r)
94
+ except (KeyError, TypeError, ValueError):
95
+ continue
96
+ for w in ws:
97
+ for r in por_hora.get(round(w["start"], 2), []):
98
+ if norm(r.get("text", "")) == norm(w["text"]) and abs(float(r["start"]) - w["start"]) <= 0.45:
99
+ w["start"] = round(float(r["start"]), 3)
100
+ w["end"] = round(max(w["end"], w["start"] + 0.05), 3)
101
+ break
102
+ return ws
103
+
104
+
105
+ def frases(ws: list[dict], cortes: list[float]) -> list[list[int]]:
106
+ """Indexes of the words of each sentence: split on . ! ? and a long sentence split again at a comma or a cut."""
107
+ out, atual = [], []
108
+ for i, w in enumerate(ws):
109
+ atual.append(i)
110
+ if re.search(r"[.!?…]$", w["text"]):
111
+ out.append(atual)
112
+ atual = []
113
+ if atual:
114
+ out.append(atual)
115
+
116
+ def dur(f):
117
+ return ws[f[-1]]["end"] - ws[f[0]]["start"]
118
+
119
+ partidas = []
120
+ for f in out:
121
+ fila = [f]
122
+ while fila:
123
+ g = fila.pop(0)
124
+ if dur(g) <= LONGA_S or len(g) < 4:
125
+ partidas.append(g)
126
+ continue
127
+ meio = (ws[g[0]]["start"] + ws[g[-1]]["end"]) / 2
128
+ cand = [k for k in range(1, len(g) - 1) if ws[g[k - 1]]["text"].endswith((",", ";", ":"))]
129
+ cand += [k for k in range(1, len(g) - 1)
130
+ if any(abs(c - ws[g[k]]["start"]) <= SNAP_S for c in cortes)]
131
+ cand = [k for k in cand if ws[g[k]]["start"] - ws[g[0]]["start"] >= CURTA_S
132
+ and ws[g[-1]]["end"] - ws[g[k]]["start"] >= CURTA_S]
133
+ if not cand:
134
+ partidas.append(g)
135
+ continue
136
+ k = min(cand, key=lambda k: abs(ws[g[k]]["start"] - meio))
137
+ fila = [g[:k], g[k:]] + fila
138
+ juntas: list[list[int]] = []
139
+ for f in partidas: # a very short sentence rides with the one before it
140
+ if juntas and (dur(f) < CURTA_S or dur(juntas[-1]) < CURTA_S) and dur(juntas[-1]) + dur(f) <= LONGA_S:
141
+ juntas[-1] = juntas[-1] + f
142
+ else:
143
+ juntas.append(f)
144
+ return juntas
145
+
146
+
147
+ def energia(db, w: dict) -> float:
148
+ """Loudness of a word: the 90th percentile of the wave (10 ms windows) between its start and its end."""
149
+ if db is None:
150
+ return 0.0
151
+ import numpy as np # noqa: PLC0415
152
+ a, b = int(w["start"] * 100), max(int(w["end"] * 100), int(w["start"] * 100) + 1)
153
+ seg = db[a:b]
154
+ return float(np.percentile(seg, 90)) if len(seg) else -99.0
155
+
156
+
157
+ def enfase(ws: list[dict], f: list[int], db) -> tuple[int, str] | None:
158
+ """(index, reason) of the emphasis word of a sentence: number, negation, or the loudest content word."""
159
+ en = {i: energia(db, ws[i]) for i in f}
160
+ nums = [i for i in f if re.search(r"\d", ws[i]["text"]) or "%" in ws[i]["text"] or "$" in ws[i]["text"]
161
+ or (norm(ws[i]["text"]) in NUMEROS and norm(ws[i]["text"]) not in ARTIGOS_NUM)]
162
+ if nums:
163
+ return max(nums, key=lambda i: en[i]), "número"
164
+ negs = [i for i in f if norm(ws[i]["text"]) in NEGACOES]
165
+ if negs:
166
+ return max(negs, key=lambda i: en[i]), "negação"
167
+ fortes = [i for i in f if norm(ws[i]["text"]) not in FRACAS and len(norm(ws[i]["text"])) >= 3]
168
+ if not fortes:
169
+ return None
170
+ return max(fortes, key=lambda i: en[i]), "palavra mais forte"
171
+
172
+
173
+ def tempo_do_campo(v, ws: list[dict], depois: float = 0.0) -> float | None:
174
+ """Seconds of a "de"/"ate" of a hand-written block: a number, or the first time the word(s) are said."""
175
+ if isinstance(v, (int, float)):
176
+ return float(v)
177
+ alvo = [norm(x) for x in str(v).split() if norm(x)]
178
+ if not alvo:
179
+ return None
180
+ for i in range(len(ws) - len(alvo) + 1):
181
+ if ws[i]["start"] >= depois - 1e-3 and [norm(w["text"]) for w in ws[i:i + len(alvo)]] == alvo:
182
+ return ws[i]["start"]
183
+ return None
184
+
185
+
186
+ def trechos_sobre_a_pessoa(cfg: dict, ws: list[dict], vdur: float) -> list[tuple[float, float]]:
187
+ """Stretches (de, ate) of the `cenas_proprias` marked "sobre_a_pessoa": true, merged. "de" and "ate" are seconds or a
188
+ word (`de_depois_de` / `ate_depois_de` choose the repetition); no "ate" = to the end of the video."""
189
+ out = []
190
+ for i, c in enumerate(cfg.get("cenas_proprias") or []):
191
+ if not isinstance(c, dict) or c.get("sobre_a_pessoa") is not True:
192
+ continue
193
+ nome = c.get("arquivo") or f"cena própria {i + 1}"
194
+ de = tempo_do_campo(c.get("de", 0.0), ws, float(c.get("de_depois_de", c.get("depois_de", 0.0)) or 0.0))
195
+ if de is None:
196
+ raise Erro(f'{nome}: não achei a palavra "{c.get("de")}" (do "de") na fala; escreva em segundos')
197
+ ate = vdur
198
+ if c.get("ate") is not None:
199
+ ate = tempo_do_campo(c["ate"], ws, max(de, float(c.get("ate_depois_de", 0.0) or 0.0)))
200
+ if ate is None:
201
+ raise Erro(f'{nome}: não achei a palavra "{c["ate"]}" (do "ate") na fala; escreva em segundos')
202
+ de, ate = max(0.0, de), min(vdur, ate)
203
+ if ate > de + 0.05:
204
+ out.append((de, ate))
205
+ out.sort()
206
+ juntos: list[tuple[float, float]] = []
207
+ for de, ate in out:
208
+ if juntos and de <= juntos[-1][1] + 0.05:
209
+ juntos[-1] = (juntos[-1][0], max(juntos[-1][1], ate))
210
+ else:
211
+ juntos.append((de, ate))
212
+ return juntos
213
+
214
+
215
+ def mesclar(trechos: list[tuple[float, float]]) -> list[tuple[float, float]]:
216
+ """Ranges sorted and merged where they touch."""
217
+ out: list[tuple[float, float]] = []
218
+ for a, b in sorted(trechos):
219
+ if out and a <= out[-1][1] + 0.05:
220
+ out[-1] = (out[-1][0], max(out[-1][1], b))
221
+ else:
222
+ out.append((a, b))
223
+ return out
224
+
225
+
226
+ def plano(cfg: dict, ws: list[dict], cortes: list[float], vdur: float, db=None, min_gap: float = MIN_GAP_S) -> dict:
227
+ """The new "blocos" of the project and a short report. Pure: reads nothing from disk."""
228
+ manuais, simples = [], []
229
+ for b in cfg.get("blocos") or []:
230
+ if not isinstance(b, dict) or b.get("auto") == MARCA:
231
+ continue
232
+ b = dict(b)
233
+ if isinstance(b.get("zooms"), list): # punch-ins from a previous run inside a hand-written block
234
+ b["zooms"] = [z for z in b["zooms"] if not (isinstance(z, dict) and z.get("auto") == MARCA)]
235
+ if not b["zooms"]:
236
+ del b["zooms"]
237
+ if b.pop("_zoom_auto", None):
238
+ b.pop("zoom", None)
239
+ de = tempo_do_campo(b.get("de", 0.0), ws)
240
+ ate = tempo_do_campo(b["ate"], ws, de or 0.0) if "ate" in b else vdur
241
+ if de is None or ate is None:
242
+ raise Erro(f'bloco {b.get("cena")}: não achei a palavra de "de"/"ate" na fala; escreva em segundos')
243
+ plain = (b.get("cena") == "tela_cheia" and not b.get("zooms") and not b.get("acompanhar")
244
+ and b.get("camera_viva", True) is not False and not b.get("borrar"))
245
+ (simples if plain else manuais).append((de, min(ate, vdur), b))
246
+ manuais.sort(key=lambda x: x[0])
247
+
248
+ # the stretches that are free for the live camera: everything outside the hand-written blocks that stay
249
+ livres, t = [], 0.0
250
+ for de, ate, _ in manuais:
251
+ if de > t + 0.05:
252
+ livres.append((t, de))
253
+ t = max(t, ate)
254
+ if t < vdur - 0.05:
255
+ livres.append((t, vdur))
256
+
257
+ # lettering, rótulo e carimbo ficam em cima da cabeça: ali a aproximação no rosto empurraria a cabeça para baixo do texto
258
+ ocupado: list[tuple[float, float]] = []
259
+ titulos: list[tuple[float, float]] = []
260
+ for an in cfg.get("animacoes") or []:
261
+ if not isinstance(an, dict) or an.get("tipo") not in ("lettering", "rotulo", "carimbo"):
262
+ continue
263
+ ini = tempo_do_campo(an["em"], ws, float(an.get("depois_de") or 0.0)) if an.get("em") is not None else None
264
+ if ini is None:
265
+ continue
266
+ fim = tempo_do_campo(an["sai"], ws, ini) if an.get("sai") is not None else None
267
+ ocupado.append((ini - 0.1, fim if fim is not None else ini + 2.5))
268
+ # o título grande no alto (lettering que não pediu "base"/"meio", com a legenda fora do trecho): a câmera abre
269
+ # espaço em cima, com o rosto mais baixo no quadro (zoom 1,12 com o foco embaixo), em vez de subir a cabeça
270
+ leg = cfg.get("legenda") if isinstance(cfg.get("legenda"), dict) else {}
271
+ junto = an.get("legenda_junto")
272
+ if junto is None:
273
+ junto = leg.get("estilo") == "discreta"
274
+ if an.get("tipo") == "lettering" and an.get("posicao") not in ("base", "meio") and not junto:
275
+ titulos.append(ocupado[-1])
276
+
277
+ fs = frases(ws, cortes)
278
+ limites = [] # where each sentence starts on screen
279
+ for f in fs:
280
+ s = ws[f[0]]["start"]
281
+ c = min(cortes, key=lambda c: abs(c - s), default=None)
282
+ limites.append(round(c if c is not None and abs(c - s) <= SNAP_S else max(0.0, s - 0.04), 3))
283
+ if limites:
284
+ limites[0] = 0.0
285
+
286
+ sobre = trechos_sobre_a_pessoa(cfg, ws, vdur) # the camera stays at 1.0 and does not come closer here
287
+ # PrimoCode: o mesmo vale para o lettering, o rótulo e o carimbo: o zoom de base 1,12 também sobe a cabeça (o título
288
+ # ficava colado na testa). Nesses trechos a câmera fica em 1,0 e parada; a peça é o que muda na tela.
289
+ calmos = mesclar(sobre + [(max(0.0, a), min(vdur, b)) for a, b in ocupado if b - a > 0.05])
290
+ avisos = []
291
+ for de, ate, b in manuais:
292
+ for sd, sa in sobre:
293
+ if b.get("cena") == "tela_cheia" and de < sa - 0.05 and ate > sd + 0.05:
294
+ avisos.append(f'tela_cheia seu de {de:.1f} a {ate:.1f} s cruza a cena sobre_a_pessoa de {sd:.1f} a {sa:.1f} s: '
295
+ f'tire o zoom dali, senão a cabeça sobe sob o cartão')
296
+
297
+ def pedacos(la: float, lb: float) -> list[tuple[float, float, bool]]:
298
+ """The free stretch cut at the edges of the calm stretches (sobre_a_pessoa, lettering): [(de, ate, calmo)]"""
299
+ out, t = [], la
300
+ for sd, sa in calmos:
301
+ lo, hi = max(sd, la), min(sa, lb)
302
+ if hi - lo <= 0.05:
303
+ continue
304
+ if lo - t > 0.05:
305
+ out.append((t, lo, False))
306
+ t = lo
307
+ out.append((t, hi, True))
308
+ t = hi
309
+ if lb - t > 0.05:
310
+ out.append((t, lb, False))
311
+ elif out:
312
+ out[-1] = (out[-1][0], lb, out[-1][2])
313
+ return out
314
+
315
+ autos, punches, ultima_base, ultimo_punch = [], [], ZOOM_B, -99.0
316
+ for la, lb in livres:
317
+ for pa, pb, sob in pedacos(la, lb):
318
+ pontos = [pa] + ([] if sob else [x for x in limites if pa + CURTA_S * 0.8 <= x <= pb - CURTA_S * 0.8]) + [pb]
319
+ if sob: # o trecho calmo troca de enquadramento só onde começa ou acaba um título no alto
320
+ pontos = sorted(set(pontos + [round(x, 3) for t in titulos for x in t if pa + 0.3 < x < pb - 0.3]))
321
+ for a, b in zip(pontos, pontos[1:]):
322
+ titulo = sob and any(x - 0.01 <= (a + b) / 2 <= y + 0.01 for x, y in titulos) and \
323
+ not any(x < b - 0.05 and a + 0.05 < y for x, y in sobre)
324
+ base = ZOOM_B if titulo else (ZOOM_A if sob or ultima_base == ZOOM_B else ZOOM_B)
325
+ ultima_base = base
326
+ bloco = dict(cena="tela_cheia", de=round(a, 3), ate=round(b, 3), zoom=base, auto=MARCA)
327
+ if titulo:
328
+ bloco["foco_y"] = FOCO_TITULO
329
+ dentro = [f for f in fs if a - 0.05 <= ws[f[0]]["start"] < b - 0.05]
330
+ cands = [] if sob else [e for e in (enfase(ws, f, db) for f in dentro) if e]
331
+ ordem = {"número": 0, "negação": 1, "palavra mais forte": 2}
332
+ cands.sort(key=lambda e: (ordem[e[1]], -energia(db, ws[e[0]])))
333
+ for i, motivo in cands:
334
+ t0 = ws[i]["start"]
335
+ if t0 - ultimo_punch < min_gap or t0 < a + 0.45 or b - t0 < PUNCH_FICA_S + 0.25:
336
+ continue
337
+ t1 = round(min(ws[i]["end"] + PUNCH_FICA_S, b - 0.05), 3)
338
+ if any(x < t1 and t0 - 0.1 < y for x, y in ocupado):
339
+ continue
340
+ bloco["zooms"] = [dict(em=round(t0, 3), ate=t1, escala=PUNCH_A if base == ZOOM_A else PUNCH_B,
341
+ som=False, auto=MARCA)]
342
+ punches.append(dict(em=round(t0, 2), palavra=ws[i]["text"], motivo=motivo))
343
+ ultimo_punch = t0
344
+ break
345
+ autos.append((a, bloco))
346
+ if autos and autos[-1][1]["ate"] >= vdur - 0.05:
347
+ del autos[-1][1]["ate"] # the last block goes to the end (and holds on the still end)
348
+
349
+ blocos = [b for _, _, b in manuais]
350
+ todos = sorted([(de, b) for de, _, b in manuais] + autos, key=lambda x: x[0])
351
+ # the card of a sobre_a_pessoa scene coming and going is also something that changes on screen
352
+ cartoes = {round(x, 2) for d, a in calmos for x in (d, a) if 0.05 < x < vdur - 0.05}
353
+ trocas = sorted({round(de, 2) for de, _ in todos} | {p["em"] for p in punches} | cartoes)
354
+ # inside a sobre_a_pessoa stretch the camera stays still on purpose: the card is what changes there (main() says how long)
355
+ dentro = lambda x, y: any(x >= sd - 0.01 and y <= sa + 0.01 for sd, sa in calmos) # noqa: E731
356
+ maior = max((y - x for x, y in zip([0.0] + trocas, trocas + [vdur]) if not dentro(x, y)), default=0.0)
357
+ return dict(blocos=[b for _, b in todos], frases=len(fs), blocos_auto=len(autos), punches=punches,
358
+ mantidos=len(blocos), simples_trocados=len(simples), maior_intervalo=round(maior, 2),
359
+ sobre_a_pessoa=[[round(d, 2), round(a, 2)] for d, a in sobre], avisos=avisos)
360
+
361
+
362
+ def main(argv: list[str] | None = None) -> int:
363
+ ap = argparse.ArgumentParser(description="Câmera viva: zoom de base alternando por frase e aproximação no rosto "
364
+ "nas palavras de ênfase, escritos no projeto.json.")
365
+ ap.add_argument("projeto", type=Path, help="a pasta do projeto (<PROJ>)")
366
+ ap.add_argument("--desfazer", action="store_true", help="tira tudo o que a câmera viva escreveu (vídeo calmo)")
367
+ ap.add_argument("--min-gap", type=float, default=MIN_GAP_S, help="segundos entre duas aproximações (2,5)")
368
+ ap.add_argument("--json", action="store_true")
369
+ a = ap.parse_args(argv)
370
+ try:
371
+ pj = a.projeto / "projeto.json"
372
+ if not pj.is_file():
373
+ raise Erro(f"não achei {pj}: crie o projeto com o build.py novo")
374
+ cfg = ler_json(pj)
375
+ if a.desfazer:
376
+ antes = cfg.get("blocos") or []
377
+ cfg["blocos"] = [b for b in antes if not (isinstance(b, dict) and b.get("auto") == MARCA)]
378
+ for b in cfg["blocos"]:
379
+ if isinstance(b, dict) and isinstance(b.get("zooms"), list):
380
+ b["zooms"] = [z for z in b["zooms"] if not (isinstance(z, dict) and z.get("auto") == MARCA)]
381
+ pj.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
382
+ print(f"câmera viva: tirei {len(antes) - len(cfg['blocos'])} bloco(s). Rode o build de novo.")
383
+ return 0
384
+ edit = (a.projeto / cfg.get("edit", "../edit")).resolve()
385
+ tl = edit / "cut_timeline.json"
386
+ if not tl.is_file():
387
+ raise Erro(f"falta {tl}: termine o corte primeiro")
388
+ tl = ler_json(tl)
389
+ vdur = float(tl["duration"])
390
+ cortes = sorted(float(s["out_start"]) for s in tl["segments"] if s["out_start"] > 0.01)
391
+ ws = palavras_do_corte(edit)
392
+ db = None
393
+ video = edit / cfg.get("video", "cut.mp4")
394
+ if video.is_file():
395
+ try:
396
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
397
+ import corte_por_onda as onda # noqa: E402, PLC0415
398
+ db = onda.envelope(video, None)
399
+ except Exception as e: # noqa: BLE001 (without the wave the emphasis is number, negation, first strong word)
400
+ print(f"aviso: não medi a onda ({e}); a ênfase fica por número e negação", file=sys.stderr)
401
+ r = plano(cfg, ws, cortes, vdur, db, a.min_gap)
402
+ cfg["blocos"] = r["blocos"]
403
+ pj.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
404
+ except Erro as e:
405
+ print(f"ERRO: {e}", file=sys.stderr)
406
+ return 2
407
+ if a.json:
408
+ print(json.dumps({k: v for k, v in r.items() if k != "blocos"}, ensure_ascii=False))
409
+ return 0
410
+ print(f"câmera viva: {r['blocos_auto']} trecho(s) de tela cheia alternando {ZOOM_A:g} e {ZOOM_B:g}, "
411
+ f"{len(r['punches'])} aproximação(ões) no rosto; {r['mantidos']} bloco(s) seu(s) mantido(s).")
412
+ for p in r["punches"]:
413
+ print(f" {p['em']:6.2f} s \"{p['palavra']}\" ({p['motivo']})")
414
+ for d, a in r["sobre_a_pessoa"]:
415
+ print(f" {d:6.2f} a {a:.2f} s sobre a pessoa: zoom 1,0 e sem aproximação (a cabeça não sobe sob o cartão)"
416
+ + (f"; são {a - d:.1f} s com a câmera parada: o cartão precisa mudar de estado a cada 2 a 4 s" if a - d > 4.0 else ""))
417
+ for aviso in r["avisos"]:
418
+ print(f" aviso: {aviso}")
419
+ print(f" maior intervalo sem mudança de câmera: {r['maior_intervalo']:.1f} s"
420
+ + (" (acima de 4 s: ponha uma peça ou cena nesse trecho)" if r["maior_intervalo"] > 4.0 else ""))
421
+ print(" rode o build de novo (amostra ou completo) para ver no estúdio.")
422
+ return 0
423
+
424
+
425
+ if __name__ == "__main__":
426
+ raise SystemExit(main())