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,272 @@
1
+ """Put every EDL edge in a real pause, measured on the waveform (RMS 10 ms).
2
+
3
+ Whisper word times drift 0.1-0.3 s and the speech regions are too coarse for a tight cut, so an
4
+ edge written from them lands inside a word whenever the speaker runs words together. This helper
5
+ measures the source audio itself and moves each range edge:
6
+
7
+ 1. Edge on top of speech (the source is still sounding there): it goes to the nearest pause of
8
+ at least PAUSE_MIN_S, finishing the word (up to REACH_OUT_S outward) or dropping the unfinished
9
+ one (up to REACH_IN_S inward), whichever is closer. With no pause in reach (speech really runs
10
+ on) it goes to the quietest 10 ms point nearby and is reported as SEM PAUSA: listen to it,
11
+ or move the edge to the end of the sentence.
12
+ 2. Edge in silence: it is pulled in to 0.08 s before the first sound and 0.12 s after the last
13
+ one (floor + 14 dB starts speech, floor + 10 dB ends it, 80 ms hysteresis), then walked back
14
+ out up to 0.12 s while there is still sound above floor + 6 dB, so a weak consonant (the S of
15
+ "Só") is not bitten.
16
+
17
+ Two ranges of one source that nearly touch (the next starts less than CONTIGUOUS_S after the
18
+ previous ends) are one continuous take split in two: the gap is closed and that junction is left
19
+ alone, because any pause search there would delete words from the middle of the sentence.
20
+
21
+ Never crosses the previous or next range of the same source, never goes more than REACH_OUT_S
22
+ past the original edge (stretching further brings back the flub the cut removed).
23
+ Recipe validated on the Vitória VSL (18/09/2026): 73 s of dead air out, zero words cut.
24
+
25
+ Usage:
26
+ uv run python helpers/bordas_precisas.py <edit>/edl.json [--apply]
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import math
33
+ import shutil
34
+ import sys
35
+ from pathlib import Path
36
+
37
+ import numpy as np
38
+
39
+ from common import load_json, save_json
40
+ from speech_regions import load_mono
41
+
42
+ SR = 16000
43
+ HOP = 0.010
44
+ ON_DB, OFF_DB, WEAK_DB = 14.0, 10.0, 6.0 # above the source floor
45
+ HYST = 8 # 80 ms
46
+ PAD_IN, PAD_OUT = 0.08, 0.12 # silence kept before / after the speech
47
+ WALK = 12 # up to 0.12 s outward for weak consonants
48
+ PAUSE_MIN_S = 0.07 # shorter dips are stop consonants or run-on speech
49
+ REACH_OUT_S = 0.25 # an edge on speech may grow this much to finish a word
50
+ REACH_IN_S = 0.40 # ... or shrink this much to drop the unfinished word
51
+ EDGE_PROBE_S = 0.03 # how close to the edge sound counts as "on speech"
52
+ MIN_DYNAMIC_DB = 20.0 # p90 - floor below this: no silence to measure against
53
+ CLICK_S = 0.03 # a blip this short inside a pause does not split it
54
+ CONTIGUOUS_S = 0.15 # next range of the same source starting this close: one take
55
+
56
+
57
+ RUMBLE_AF = "highpass=f=300,highpass=f=300" # car, road, AC: rumble lives below the voice
58
+
59
+
60
+ def envelope(path: str, af: str | None = None) -> np.ndarray:
61
+ mono, _ = load_mono(path, sr=SR, af=af)
62
+ n = int(HOP * SR)
63
+ frames = mono[: (len(mono) // n) * n].reshape(-1, n).astype(np.float64)
64
+ return 10.0 * np.log10(np.mean(frames * frames, axis=1) + 1e-12)
65
+
66
+
67
+ def idx(t: float) -> int:
68
+ return int(round(t / HOP))
69
+
70
+
71
+ def pauses(db: np.ndarray, floor: float, i0: int, i1: int) -> list[tuple[int, int]]:
72
+ """Runs below floor+OFF_DB of at least PAUSE_MIN_S inside [i0, i1), as (first, last+1)."""
73
+ i0, i1 = max(0, i0), min(len(db), i1)
74
+ quiet = db[i0:i1] < floor + OFF_DB
75
+ # a blip of CLICK_S or less inside a pause (lip click, breath) does not split it
76
+ k, short = 0, int(round(CLICK_S / HOP))
77
+ while k < quiet.size:
78
+ if not quiet[k]:
79
+ j = k
80
+ while j < quiet.size and not quiet[j]:
81
+ j += 1
82
+ if j - k <= short and k > 0 and j < quiet.size:
83
+ quiet[k:j] = True
84
+ k = j
85
+ else:
86
+ k += 1
87
+ out, k, need = [], 0, int(round(PAUSE_MIN_S / HOP))
88
+ while k < quiet.size:
89
+ if quiet[k]:
90
+ j = k
91
+ while j < quiet.size and quiet[j]:
92
+ j += 1
93
+ if j - k >= need:
94
+ out.append((i0 + k, i0 + j))
95
+ k = j
96
+ else:
97
+ k += 1
98
+ return out
99
+
100
+
101
+ def on_speech(db: np.ndarray, floor: float, t: float) -> bool:
102
+ a, b = max(0, idx(t - EDGE_PROBE_S)), min(len(db), idx(t + EDGE_PROBE_S) + 1)
103
+ return b > a and float(db[a:b].max()) >= floor + OFF_DB
104
+
105
+
106
+ def fix_edge_on_speech(db, floor, t, side, lo, hi):
107
+ """(new_time, how) for an edge that sits on speech."""
108
+ out_dir = 1 if side == "end" else -1
109
+ w0 = max(idx(lo), idx(t - (REACH_OUT_S if side == "start" else REACH_IN_S)))
110
+ w1 = min(idx(hi), idx(t + (REACH_OUT_S if side == "end" else REACH_IN_S)))
111
+ best = None
112
+ for a, b in pauses(db, floor, w0, w1 + 1):
113
+ # the edge keeps PAD_OUT / PAD_IN of the pause, or all of it when the pause is shorter:
114
+ # stopping mid-pause left 0.04 s after the word and the 30 ms fade ate its end
115
+ if side == "end": # end goes where the pause begins
116
+ new = min(a * HOP + PAD_OUT, b * HOP)
117
+ dist = abs(a * HOP - t)
118
+ else: # start goes where the pause ends
119
+ new = max(b * HOP - PAD_IN, a * HOP)
120
+ dist = abs(b * HOP - t)
121
+ grows = (new - t) * out_dir > 0
122
+ if (dist <= REACH_OUT_S if grows else dist <= REACH_IN_S):
123
+ # finishing the word wins a near tie over dropping it
124
+ score = dist - (0.05 if grows else 0.0)
125
+ if best is None or score < best[0]:
126
+ best = (score, new, "termina a palavra" if grows else "tira a palavra pela metade")
127
+ if best:
128
+ return min(max(best[1], lo), hi), best[2]
129
+ a, b = max(idx(lo), idx(t - 0.08)), min(idx(hi), idx(t + 0.08)) + 1
130
+ if b <= a:
131
+ return t, "SEM PAUSA"
132
+ k = a + int(np.argmin(db[a:b]))
133
+ return k * HOP, "SEM PAUSA"
134
+
135
+
136
+ def tighten(db, floor, s, e, lo, hi, fix_start, fix_end):
137
+ """Pull edges in silence toward the speech, walking out for weak consonants."""
138
+ i0, i1 = idx(s), min(int(math.ceil(e / HOP)), len(db) - 1)
139
+ if i1 <= i0:
140
+ return s, e
141
+ win = db[i0:i1 + 1]
142
+ on = np.where(win >= floor + ON_DB)[0]
143
+ off = np.where(win >= floor + OFF_DB)[0]
144
+ if len(on) == 0 or len(off) == 0:
145
+ return s, e
146
+ ns, ne = s, e
147
+ if fix_start:
148
+ start_i = on[0]
149
+ for i in on:
150
+ if (win[i:i + HYST] >= floor + OFF_DB).mean() > 0.6:
151
+ start_i = i
152
+ break
153
+ k, steps = i0 + start_i, 0
154
+ while steps < WALK and k - 1 >= 0 and db[k - 1] >= floor + WEAK_DB:
155
+ k, steps = k - 1, steps + 1
156
+ ns = min((i0 + start_i) * HOP - PAD_IN, k * HOP - 0.05)
157
+ if fix_end:
158
+ k, steps = i0 + off[-1], 0
159
+ while steps < WALK and k + 1 < len(db) and db[k + 1] >= floor + WEAK_DB:
160
+ k, steps = k + 1, steps + 1
161
+ ne = max((i0 + off[-1] + 1) * HOP + PAD_OUT, (k + 1) * HOP + 0.06)
162
+ return max(ns, lo), min(ne, hi)
163
+
164
+
165
+ def source_levels(path: str, log=None) -> tuple[np.ndarray, float] | None:
166
+ """(10 ms level, noise floor) of a source, or None when it has no silence to measure against.
167
+ A recording with rumble (car, road, air conditioning) is measured again without the lows: the voice
168
+ stands 25 to 30 dB over what is left, while the high consonants (the S) stay in."""
169
+ for af in (None, RUMBLE_AF):
170
+ db = envelope(path, af)
171
+ floor, top = float(np.percentile(db, 10)), float(np.percentile(db, 90))
172
+ if top - floor >= MIN_DYNAMIC_DB:
173
+ if af and log:
174
+ log(f"{Path(path).stem}: ronco de fundo, medi a voz sem os graves (faixa {top - floor:.0f} dB)")
175
+ return db, floor
176
+ if log:
177
+ log(f"{Path(path).stem}: sem silêncio pra medir (chão {floor:.0f} dB, fala {top:.0f} dB; trilha ou "
178
+ f"vídeo já cortado?). Bordas dessa fonte ficam como estão.")
179
+ return None
180
+
181
+
182
+ def refine_edl(edl: dict, envs: dict | None = None, log=print) -> list[str]:
183
+ """Move every edge of edl["ranges"] in place. Returns the SEM PAUSA alerts."""
184
+ envs = {} if envs is None else envs
185
+ for name, path in edl["sources"].items():
186
+ if name in envs:
187
+ continue
188
+ envs[name] = source_levels(path, log)
189
+ if envs[name]:
190
+ db, floor = envs[name]
191
+ log(f"{name}: chão {floor:.0f} dB, {len(db) * HOP:.1f} s")
192
+
193
+ rs = edl["ranges"]
194
+ tirado, alerts, moved = 0.0, [], 0
195
+ frozen_start, frozen_end = set(), set()
196
+ for a, b in zip(rs, rs[1:]):
197
+ if a["source"] == b["source"] and 0 <= float(b["start"]) - float(a["end"]) < CONTIGUOUS_S:
198
+ frozen_end.add(a["id"])
199
+ frozen_start.add(b["id"])
200
+ b["start"] = a["end"]
201
+ for n, r in enumerate(rs):
202
+ env = envs.get(r["source"])
203
+ if env is None:
204
+ continue
205
+ db, floor = env
206
+ s, e = float(r["start"]), float(r["end"])
207
+ lo, hi = max(0.0, s - REACH_OUT_S), min(len(db) * HOP, e + REACH_OUT_S)
208
+ prev = next((p for p in rs[:n][::-1] if p["source"] == r["source"]), None)
209
+ nxt = next((p for p in rs[n + 1:] if p["source"] == r["source"]), None)
210
+ if prev and prev["end"] <= s + 1e-6: # only a neighbour that plays before this one
211
+ lo = max(lo, float(prev["end"]) + 0.02)
212
+ if nxt and nxt["start"] >= e - 1e-6:
213
+ hi = min(hi, float(nxt["start"]) - 0.02)
214
+
215
+ notes = []
216
+ ns, ne = s, e
217
+ keep_s, keep_e = r["id"] in frozen_start, r["id"] in frozen_end
218
+ if keep_s:
219
+ notes.append("início emendado no trecho anterior")
220
+ if keep_e:
221
+ notes.append("fim emendado no próximo trecho")
222
+ start_hot = not keep_s and on_speech(db, floor, s)
223
+ end_hot = not keep_e and on_speech(db, floor, e)
224
+ if start_hot:
225
+ ns, how = fix_edge_on_speech(db, floor, s, "start", lo, hi)
226
+ notes.append(f"início na fala: {how}")
227
+ if end_hot:
228
+ ne, how = fix_edge_on_speech(db, floor, e, "end", lo, hi)
229
+ notes.append(f"fim na fala: {how}")
230
+ ts, te = tighten(db, floor, ns, ne, lo, hi, not (start_hot or keep_s), not (end_hot or keep_e))
231
+ ns, ne = (s if keep_s else round(ts, 3)), (e if keep_e else round(te, 3))
232
+ if ne - ns < 0.1:
233
+ notes.append("ficou curto demais, mantido")
234
+ ns, ne = s, e
235
+ if any("SEM PAUSA" in x for x in notes):
236
+ alerts.append(f"{r['id']} ({r['source']} {s:.2f}-{e:.2f}) {r.get('quote', '')[:50]}")
237
+ d0, d1 = ns - s, ne - e
238
+ mark = "" if abs(d0) < 0.005 and abs(d1) < 0.005 else " <-"
239
+ moved += bool(mark)
240
+ tirado += (e - s) - (ne - ns)
241
+ log(f"{r['id']} {r['source']} {s:7.2f}->{ns:7.2f} ({d0:+.2f}) {e:7.2f}->{ne:7.2f} ({d1:+.2f})"
242
+ f"{mark}{' · ' + '; '.join(notes) if notes else ''}")
243
+ r["start"], r["end"] = ns, ne
244
+
245
+ log(f"\n{moved} de {len(rs)} trechos mexidos · silêncio tirado {tirado:+.2f} s")
246
+ if alerts:
247
+ log("SEM PAUSA (a fala corre direto nessa borda; ouça ou mude o corte pro fim da frase):")
248
+ for a in alerts:
249
+ log(" " + a)
250
+ return alerts
251
+
252
+
253
+ def main() -> int:
254
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
255
+ ap.add_argument("edl", type=Path)
256
+ ap.add_argument("--apply", action="store_true", help="grava no edl.json (backup em edl.json.bak)")
257
+ args = ap.parse_args()
258
+
259
+ edl = load_json(args.edl)
260
+ if not isinstance(edl, dict) or not edl.get("ranges"):
261
+ print(f"EDL sem trechos: {args.edl}", file=sys.stderr)
262
+ return 2
263
+ refine_edl(edl)
264
+ if args.apply:
265
+ shutil.copy(args.edl, args.edl.with_name(args.edl.name + ".bak"))
266
+ save_json(args.edl, edl)
267
+ print(f"gravado em {args.edl} (backup {args.edl.name}.bak)")
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ sys.exit(main())