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,408 @@
1
+ """Sound effect library by category, outside the skill (like the music library in trilhas.py).
2
+
3
+ macOS / Linux: ~/.editor-video/efeitos Windows: %USERPROFILE%\\.editor-video\\efeitos
4
+ (or the folder in EDITOR_VIDEO_EFEITOS)
5
+
6
+ The 24 built-in effects (assets/sfx) are one sound per name, so three pops in a row sound the same. The
7
+ library keeps many takes per category, each cut, cleaned and set to the kit level, with where it came from
8
+ and its license. In edit_data.json a sound is asked for by category ("transicao", "humor"...) and every event
9
+ gets a different take, or narrowed by the source name ("impacto:boom", "transicao:swipe", "humor:boing"), or by an
10
+ exact id ("humor-012"). compose_final resolves those names here.
11
+
12
+ Categories: transicao (whoosh, swish, passagem), impacto (batida, soco, explosão curta), revelacao (grave que
13
+ cresce, brilho mágico, clima antes de mostrar), interface (clique, toque, botão), tecnologia (IA, laser,
14
+ bipes, energia), sucesso (confirmação, subida), erro (negação, falha), humor (boing, pop de desenho, apito).
15
+
16
+ Commands:
17
+ efeitos.py importar PASTA --origem kenney|sse|arquivos [--categoria C] [--licenca TEXTO] [--fonte URL]
18
+ cuts every file in PASTA (long reels split at the pauses), levels it and files it by category
19
+ efeitos.py listar [--categoria C] what the library has, per category
20
+ efeitos.py amostra --categoria C one mp3 with every take of C and a list with the time of each one
21
+ efeitos.py remover ID [ID ...] moves takes to _lixeira (restaurar brings them back)
22
+ efeitos.py restaurar ID [ID ...]
23
+ efeitos.py onde the library folder
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import hashlib
30
+ import json
31
+ import math
32
+ import os
33
+ import re
34
+ import shutil
35
+ import subprocess
36
+ import sys
37
+ import unicodedata
38
+ from pathlib import Path
39
+
40
+ import numpy as np
41
+
42
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
43
+ import make_sfx # noqa: E402
44
+
45
+ SR = make_sfx.SR
46
+ LIBRARY_ENV = "EDITOR_VIDEO_EFEITOS"
47
+ INDEX = "efeitos.json"
48
+ UP_DB = 20 * math.log10(make_sfx.SFX_CALIBRATION) # what sfx.volume 1.0 adds in compose_final
49
+
50
+ # category -> (label, when to use, loudest 200 ms at sfx.volume 1.0 in LUFS, longest take in s)
51
+ CATEGORIES = {
52
+ "transicao": ("Transição", "whoosh e swish: corte, troca de assunto, animação ou imagem entrando", -10.0, 2.5),
53
+ "impacto": ("Impacto", "batida, soco, explosão curta: frase forte, número, palavra que bate na tela", -10.0, 3.0),
54
+ "revelacao": ("Revelação", "grave que cresce, brilho mágico: antes ou no momento de mostrar algo", -12.0, 6.0),
55
+ "interface": ("Interface", "clique, toque, botão, rolagem: item de lista, tela, celular", -14.0, 1.2),
56
+ "tecnologia": ("Tecnologia", "IA, laser, bipes, energia, dados: sistema trabalhando, robô, app", -13.0, 3.0),
57
+ "sucesso": ("Sucesso", "confirmação, subida, nível acima: deu certo, check, conquista", -13.0, 2.5),
58
+ "erro": ("Erro", "negação, falha, buzina: o que não fazer, erro comum", -13.0, 2.0),
59
+ "humor": ("Humor", "boing, pop de desenho, apito, soco de desenho: quebra de padrão, piada", -11.0, 3.0),
60
+ }
61
+ LONG_CAP = -20.0 # loudest 3 s window at sfx.volume 1.0 (the kit's ceiling for long sounds)
62
+ PEAK_MAX_DB = -1.0
63
+ MAX_PER_GROUP = 8 # takes kept per source name (ui-audio has 38 switches)
64
+
65
+ # (origin, regex on "folder/file") -> category or None (left out), first match wins
66
+ RULES = [
67
+ ("kenney", r"footstep|spaceengine|thruster|enginecircular|slime|spacetrash|preview", None),
68
+ ("kenney", r"confirmation|powerup|phaserup|highup|threetone(up)?|zapthreetoneup", "sucesso"),
69
+ ("kenney", r"interface.*error|lowdown|zapthreetonedown|phaserdown|highdown", "erro"),
70
+ ("kenney", r"explosion|impactmetal|impactpunch|impactplate|impactwood|impacttin|impactsoft|impactplank|"
71
+ r"impactmining|impactglass|impactbell|impactgeneric", "impacto"),
72
+ ("kenney", r"door(open|close)", "transicao"),
73
+ ("kenney", r"laser|forcefield|computernoise|zap|phase|tone|pepsound|glitch|lowrandom|lowthree", "tecnologia"),
74
+ ("kenney", r"scratch", "humor"),
75
+ ("kenney", r".", "interface"),
76
+ ("sse", r"swooshes|fireball", "transicao"),
77
+ ("sse", r"rumble|magic", "revelacao"),
78
+ ("sse", r"cartoon|whistles", "humor"),
79
+ ("sse", r"explosions", "impacto"),
80
+ ("sse", r".", "tecnologia"),
81
+ ("sintetico", r"whoosh|swipe", "transicao"),
82
+ ("sintetico", r"riser|swell", "revelacao"),
83
+ ("sintetico", r"boom808", "humor"),
84
+ ("sintetico", r"boom", "impacto"),
85
+ ("sintetico", r"glitch", "tecnologia"),
86
+ ]
87
+ LICENSES = {
88
+ "kenney": ("CC0 1.0 (domínio público), Kenney (www.kenney.nl)", "https://kenney.nl/assets/category:Audio"),
89
+ "sse": ("CC0 1.0 (domínio público), SSE Library / USC Cinema no Internet Archive",
90
+ "https://archive.org/details/SSE_Library_SWOOSHES"),
91
+ "sintetico": ("gerado pelo editor-video (helpers/efeitos_gerar.py), código próprio", "helpers/efeitos_gerar.py"),
92
+ }
93
+ AUDIO_EXT = {".wav", ".ogg", ".mp3", ".flac", ".aif", ".aiff", ".m4a"}
94
+
95
+
96
+ # ------------------------------------------------------------------ library
97
+
98
+ def library_dir() -> Path:
99
+ raw = (os.environ.get(LIBRARY_ENV) or "").strip()
100
+ return Path(raw).expanduser() if raw else Path.home() / ".editor-video" / "efeitos"
101
+
102
+
103
+ def load_index(lib: Path | None = None) -> dict:
104
+ lib = lib or library_dir()
105
+ try:
106
+ data = json.loads((lib / INDEX).read_text(encoding="utf-8"))
107
+ return data if isinstance(data, dict) and isinstance(data.get("efeitos"), dict) else {"efeitos": {}}
108
+ except (OSError, ValueError):
109
+ return {"efeitos": {}}
110
+
111
+
112
+ def save_index(lib: Path, index: dict) -> None:
113
+ lib.mkdir(parents=True, exist_ok=True)
114
+ tmp = lib / (INDEX + ".tmp")
115
+ tmp.write_text(json.dumps(index, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
116
+ tmp.replace(lib / INDEX)
117
+
118
+
119
+ def slug(text: str) -> str:
120
+ t = unicodedata.normalize("NFD", str(text).lower())
121
+ t = "".join(c for c in t if unicodedata.category(c) != "Mn")
122
+ return re.sub(r"[^a-z0-9]+", "-", t).strip("-")
123
+
124
+
125
+ def category_of(name: str) -> str | None:
126
+ s = slug(name)
127
+ for cid, (label, _, _, _) in CATEGORIES.items():
128
+ if s in (cid, slug(label)):
129
+ return cid
130
+ return None
131
+
132
+
133
+ def takes(category: str, lib: Path | None = None, index: dict | None = None) -> list[tuple[str, dict]]:
134
+ lib = lib or library_dir()
135
+ index = index or load_index(lib)
136
+ return sorted((k, v) for k, v in index["efeitos"].items()
137
+ if v.get("categoria") == category and (lib / v["arquivo"]).is_file())
138
+
139
+
140
+ def resolve(names, lib: Path | None = None, seed: str = "") -> dict:
141
+ """{name: {"path", "anchor_s", "id", "takes": [...]}} for names that are a library category or take id.
142
+
143
+ A category gets every take in a fixed shuffled order (seeded by `seed`, the edit folder): the caller hands
144
+ them out one per event, so a category used 5 times plays 5 different takes."""
145
+ lib = lib or library_dir()
146
+ index = load_index(lib)
147
+ out = {}
148
+ for name in names:
149
+ base, _, term = str(name).partition(":") # "impacto:boom" = impacto takes whose name has "boom"
150
+ cat = category_of(base)
151
+ if cat:
152
+ ts = takes(cat, lib, index)
153
+ if term:
154
+ ts = [kv for kv in ts if slug(term).replace("-", "") in slug(kv[1].get("nome", "")).replace("-", "")]
155
+ if not ts:
156
+ continue
157
+ order = sorted(ts, key=lambda kv: hashlib.sha1(f"{seed}|{kv[0]}".encode()).hexdigest())
158
+ out[name] = {"id": cat, "takes": [{"path": lib / v["arquivo"], "anchor_s": float(v.get("anchor_s", 0)),
159
+ "id": k} for k, v in order]}
160
+ continue
161
+ v = index["efeitos"].get(slug(name))
162
+ if v and (lib / v["arquivo"]).is_file():
163
+ t = {"path": lib / v["arquivo"], "anchor_s": float(v.get("anchor_s", 0)), "id": slug(name)}
164
+ out[name] = {"id": slug(name), "takes": [t]}
165
+ return out
166
+
167
+
168
+ # ------------------------------------------------------------------ cutting and leveling
169
+
170
+ def decode(path: Path) -> np.ndarray:
171
+ """(2, n) float at 48 kHz."""
172
+ raw = subprocess.run(["ffmpeg", "-v", "error", "-i", str(path), "-ac", "2", "-ar", str(SR), "-f", "f32le", "-"],
173
+ capture_output=True, check=True).stdout
174
+ x = np.frombuffer(raw, dtype="<f4").astype(np.float64)
175
+ return x.reshape(-1, 2).T
176
+
177
+
178
+ def env_db(x: np.ndarray, hop: int = SR // 100) -> np.ndarray:
179
+ m = x.mean(axis=0)
180
+ n = len(m) // hop
181
+ if n == 0:
182
+ return np.full(1, -120.0)
183
+ f = m[: n * hop].reshape(n, hop)
184
+ return 10 * np.log10((f * f).mean(axis=1) + 1e-12)
185
+
186
+
187
+ def pieces(x: np.ndarray, max_len: float, split: bool) -> list[np.ndarray]:
188
+ """The sounds in x: one trimmed piece, or (split) every sound between pauses of 0.25 s or more."""
189
+ hop = SR // 100
190
+ e = env_db(x, hop)
191
+ top = float(np.percentile(e, 99))
192
+ thr = max(float(np.percentile(e, 5)) + 18.0, top - 45.0)
193
+ loud = e >= thr
194
+ spans, k = [], 0
195
+ gap = 25
196
+ while k < len(loud):
197
+ if not loud[k]:
198
+ k += 1
199
+ continue
200
+ a, quiet = k, 0
201
+ while k < len(loud) and (quiet < gap or not split):
202
+ quiet = 0 if loud[k] else quiet + 1
203
+ k += 1
204
+ if not split and k >= len(loud):
205
+ break
206
+ b = k - quiet if split else int(np.nonzero(loud)[0][-1]) + 1
207
+ spans.append((a, b))
208
+ if not split:
209
+ break
210
+ out = []
211
+ for a, b in spans:
212
+ if (b - a) * hop < 0.06 * SR:
213
+ continue
214
+ s0 = max(0, a * hop - int(0.02 * SR))
215
+ s1 = min(x.shape[1], b * hop + int(0.15 * SR))
216
+ s1 = min(s1, s0 + int(max_len * SR))
217
+ piece = x[:, s0:s1].copy()
218
+ fade = min(int(0.03 * SR), piece.shape[1] // 4)
219
+ if fade > 0:
220
+ piece[:, -fade:] *= np.linspace(1, 0, fade)
221
+ piece[:, : int(0.003 * SR)] *= np.linspace(0, 1, int(0.003 * SR))
222
+ out.append(piece)
223
+ return out
224
+
225
+
226
+ def level(piece: np.ndarray, target_200: float) -> tuple[np.ndarray, dict] | None:
227
+ """Gain so the loudest 200 ms sits at target_200 at sfx.volume 1.0, the 3 s window never above LONG_CAP,
228
+ the peak never above PEAK_MAX_DB. None for a piece that is almost silent."""
229
+ m = make_sfx.measure(piece)
230
+ if m["loudness_200ms_lufs"] < -60:
231
+ return None
232
+ gain = (target_200 - UP_DB) - m["loudness_200ms_lufs"]
233
+ gain = min(gain, (LONG_CAP - UP_DB) - m["short_term_max_lufs"], PEAK_MAX_DB - m["peak_dbfs"])
234
+ out = piece * (10 ** (gain / 20))
235
+ return out, make_sfx.measure(out)
236
+
237
+
238
+ def write_wav(path: Path, x: np.ndarray) -> None:
239
+ path.parent.mkdir(parents=True, exist_ok=True)
240
+ make_sfx.write_wav(path, x)
241
+
242
+
243
+ def classify(origin: str, rel: str) -> str | None:
244
+ key = slug(rel).replace("-", "")
245
+ for org, rx, cat in RULES:
246
+ if org == origin and re.search(rx, key, re.I):
247
+ return cat
248
+ return None
249
+
250
+
251
+ def group_name(rel: str) -> str:
252
+ stem = Path(rel).stem
253
+ stem = re.sub(r"_CH?S_USC$", "", stem)
254
+ return re.sub(r"[_\-]?\d+$", "", stem) or stem
255
+
256
+
257
+ def importar(folder: Path, origin: str, categoria: str | None, licenca: str | None, fonte: str | None,
258
+ lib: Path, dry: bool = False) -> dict:
259
+ index = load_index(lib)
260
+ have = {v.get("origem_arquivo"): k for k, v in index["efeitos"].items()}
261
+ counts: dict[str, int] = {}
262
+ groups: dict[str, int] = {}
263
+ files = sorted(p for p in folder.rglob("*") if p.suffix.lower() in AUDIO_EXT and "_lixeira" not in p.parts)
264
+ lic, src = LICENSES.get(origin, (licenca or "", fonte or ""))
265
+ if licenca:
266
+ lic = licenca
267
+ if fonte:
268
+ src = fonte
269
+ skipped = 0
270
+ for p in files:
271
+ rel = str(p.relative_to(folder))
272
+ cat = categoria or classify(origin, rel)
273
+ if not cat:
274
+ skipped += 1
275
+ continue
276
+ label, _, target, max_len = CATEGORIES[cat]
277
+ g = f"{cat}|{group_name(rel)}"
278
+ try:
279
+ x = decode(p)
280
+ except subprocess.CalledProcessError:
281
+ print(f"não abriu: {rel}")
282
+ continue
283
+ long_reel = x.shape[1] > 6 * SR
284
+ for j, piece in enumerate(pieces(x, max_len, split=long_reel)):
285
+ if groups.get(g, 0) >= MAX_PER_GROUP * (3 if long_reel else 1):
286
+ break
287
+ tag = f"{rel}#{j}"
288
+ if tag in have:
289
+ continue
290
+ got = level(piece, target)
291
+ if got is None:
292
+ continue
293
+ y, m = got
294
+ n = 1 + max([int(k.rsplit("-", 1)[1]) for k in index["efeitos"] if k.startswith(cat + "-")] or [0])
295
+ eid = f"{cat}-{n:03d}"
296
+ if not dry:
297
+ write_wav(lib / cat / f"{eid}.wav", y)
298
+ index["efeitos"][eid] = {
299
+ "categoria": cat, "arquivo": f"{cat}/{eid}.wav", "nome": group_name(rel),
300
+ "anchor_s": m["anchor_s"], "duracao_s": m["duration_s"],
301
+ "loudness_200ms_lufs": m["loudness_200ms_lufs"], "short_term_max_lufs": m["short_term_max_lufs"],
302
+ "origem": origin, "origem_arquivo": tag, "licenca": lic, "fonte": src}
303
+ have[tag] = eid
304
+ groups[g] = groups.get(g, 0) + 1
305
+ counts[cat] = counts.get(cat, 0) + 1
306
+ if not dry:
307
+ save_index(lib, index)
308
+ return {"novos": counts, "fora": skipped}
309
+
310
+
311
+ # ------------------------------------------------------------------ commands
312
+
313
+ def cmd_listar(lib: Path, categoria: str | None) -> None:
314
+ index = load_index(lib)
315
+ for cid, (label, when, _, _) in CATEGORIES.items():
316
+ if categoria and cid != category_of(categoria):
317
+ continue
318
+ ts = takes(cid, lib, index)
319
+ print(f"{cid:<11} {len(ts):>4} sons · {label}: {when}")
320
+ if categoria:
321
+ for k, v in ts:
322
+ print(f" {k} {v['duracao_s']:.2f} s {v['nome']} ({v['origem']})")
323
+
324
+
325
+ def cmd_amostra(lib: Path, categoria: str, out: Path | None) -> Path:
326
+ cat = category_of(categoria)
327
+ if not cat:
328
+ raise SystemExit(f"categoria '{categoria}' não existe ({', '.join(CATEGORIES)})")
329
+ ts = takes(cat, lib)
330
+ if not ts:
331
+ raise SystemExit(f"a categoria {cat} está vazia")
332
+ gap = np.zeros((2, int(0.45 * SR)))
333
+ parts, lines, t = [], [], 0.0
334
+ for k, v in ts:
335
+ x = make_sfx.read_audio(lib / v["arquivo"])
336
+ x = x if x.shape[0] == 2 else np.vstack([x[0], x[0]])
337
+ lines.append(f"{int(t // 60):02d}:{t % 60:05.2f} {k} {v['nome']}")
338
+ parts += [x, gap]
339
+ t += x.shape[1] / SR + gap.shape[1] / SR
340
+ out = out or lib / "_amostras" / f"amostra_{cat}.mp3"
341
+ out.parent.mkdir(parents=True, exist_ok=True)
342
+ wav = out.with_suffix(".wav")
343
+ write_wav(wav, np.hstack(parts))
344
+ subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", str(wav), "-b:a", "192k", str(out)], check=True)
345
+ wav.unlink()
346
+ out.with_suffix(".txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
347
+ print(f"{len(ts)} sons em {out} (tempo de cada um em {out.with_suffix('.txt').name})")
348
+ return out
349
+
350
+
351
+ def cmd_mover(lib: Path, ids: list[str], back: bool) -> None:
352
+ index = load_index(lib)
353
+ trash = lib / "_lixeira"
354
+ for eid in ids:
355
+ v = index["efeitos"].get(slug(eid))
356
+ if not v:
357
+ print(f"{eid}: não existe")
358
+ continue
359
+ src, dst = lib / v["arquivo"], trash / v["arquivo"]
360
+ if back:
361
+ src, dst = dst, src
362
+ if src.is_file():
363
+ dst.parent.mkdir(parents=True, exist_ok=True)
364
+ shutil.move(str(src), str(dst))
365
+ print(f"{eid}: {'de volta' if back else 'na lixeira'}")
366
+
367
+
368
+ def main() -> int:
369
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
370
+ sub = ap.add_subparsers(dest="cmd", required=True)
371
+ im = sub.add_parser("importar")
372
+ im.add_argument("pasta", type=Path)
373
+ im.add_argument("--origem", default="arquivos")
374
+ im.add_argument("--categoria", choices=list(CATEGORIES))
375
+ im.add_argument("--licenca")
376
+ im.add_argument("--fonte")
377
+ im.add_argument("--dry-run", action="store_true")
378
+ ls = sub.add_parser("listar")
379
+ ls.add_argument("--categoria")
380
+ am = sub.add_parser("amostra")
381
+ am.add_argument("--categoria", required=True)
382
+ am.add_argument("-o", type=Path)
383
+ for nm in ("remover", "restaurar"):
384
+ sub.add_parser(nm).add_argument("ids", nargs="+")
385
+ sub.add_parser("onde")
386
+ a = ap.parse_args()
387
+ lib = library_dir()
388
+ if a.cmd == "importar":
389
+ if a.origem == "arquivos" and not a.categoria:
390
+ raise SystemExit("arquivos seus: diga a --categoria")
391
+ if a.origem not in LICENSES and not a.licenca:
392
+ raise SystemExit("diga a --licenca (só entra som com direito de uso comercial)")
393
+ r = importar(a.pasta, a.origem, a.categoria, a.licenca, a.fonte, lib, a.dry_run)
394
+ print("novos: " + (", ".join(f"{k} {v}" for k, v in sorted(r["novos"].items())) or "nenhum") +
395
+ f" · fora (não servem): {r['fora']} arquivos")
396
+ elif a.cmd == "listar":
397
+ cmd_listar(lib, a.categoria)
398
+ elif a.cmd == "amostra":
399
+ cmd_amostra(lib, a.categoria, a.o)
400
+ elif a.cmd in ("remover", "restaurar"):
401
+ cmd_mover(lib, a.ids, a.cmd == "restaurar")
402
+ elif a.cmd == "onde":
403
+ print(lib)
404
+ return 0
405
+
406
+
407
+ if __name__ == "__main__":
408
+ sys.exit(main())
@@ -0,0 +1,159 @@
1
+ """Procedural takes for the sound effect library (efeitos.py): variations the CC0 packs have few of.
2
+
3
+ Every sound is made here with numpy (make_sfx building blocks), from a fixed seed: the same command gives the
4
+ same files. Own code, so the files can go anywhere (package included). Types and where they are filed:
5
+
6
+ whoosh (transicao) band-passed noise sweeping up or down, panning across, 0.35 to 1.2 s
7
+ swipe (transicao) short fast sweep, 0.16 to 0.3 s
8
+ riser (revelacao) noise and a tone climbing to a stop, 1.2 to 3 s (the strong point is the end)
9
+ swell (revelacao) reverse-cymbal-like swell that stops dead, 0.9 to 2 s
10
+ boom (impacto) sub drop with a noise hit, 0.8 to 1.6 s
11
+ boom808 (humor) saturated 808 thump with a room tail (the "boom" of memes, generic)
12
+ glitch (tecnologia) stuttered digital slices, 0.3 to 0.7 s
13
+
14
+ Usage:
15
+ uv run python helpers/efeitos_gerar.py PASTA [--por-tipo 8]
16
+ uv run python helpers/efeitos.py importar PASTA --origem sintetico
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+ import make_sfx as M # noqa: E402
29
+
30
+ SR = M.SR
31
+
32
+
33
+ def _t(d: float) -> np.ndarray:
34
+ return np.arange(int(d * SR)) / SR
35
+
36
+
37
+ def _pan(mono: np.ndarray, start: float, end: float) -> np.ndarray:
38
+ p = np.linspace(start, end, len(mono)) # -1 left .. 1 right, equal power
39
+ a = (p + 1) * np.pi / 4
40
+ return np.stack([mono * np.cos(a), mono * np.sin(a)])
41
+
42
+
43
+ def whoosh(rng) -> np.ndarray:
44
+ d = rng.uniform(0.35, 1.2)
45
+ t = _t(d)
46
+ up = rng.random() < 0.6
47
+ f0, f1 = (rng.uniform(250, 600), rng.uniform(2500, 6000)) if up else (rng.uniform(2500, 5000), rng.uniform(250, 600))
48
+ peak = rng.uniform(0.45, 0.7)
49
+ cut = f0 * (f1 / f0) ** (t / d)
50
+ x = M._svf_bandpass(rng.standard_normal(len(t)), cut, rng.uniform(1.2, 3.0))
51
+ env = np.where(t < peak * d, (t / (peak * d)) ** 2.2, np.exp(-(t - peak * d) / (0.18 * d)))
52
+ body = M._lp(rng.standard_normal(len(t)), 300.0) * 0.35
53
+ mono = M._fade_tail(M._unit(x) * env + M._unit(body) * env * 0.4, 30)
54
+ s = rng.choice([-1, 1])
55
+ return _pan(mono, -0.7 * s, 0.7 * s)
56
+
57
+
58
+ def swipe(rng) -> np.ndarray:
59
+ d = rng.uniform(0.16, 0.3)
60
+ t = _t(d)
61
+ cut = rng.uniform(1500, 3000) * (rng.uniform(3, 5)) ** (t / d)
62
+ x = M._svf_bandpass(rng.standard_normal(len(t)), cut, rng.uniform(2, 4))
63
+ env = np.sin(np.pi * t / d) ** 1.5
64
+ s = rng.choice([-1, 1])
65
+ return _pan(M._fade_tail(M._unit(x) * env, 10), -0.8 * s, 0.8 * s)
66
+
67
+
68
+ def riser(rng) -> np.ndarray:
69
+ d = rng.uniform(1.2, 3.0)
70
+ t = _t(d)
71
+ cut = 200 * (rng.uniform(25, 45)) ** (t / d)
72
+ noise = M._unit(M._svf_bandpass(rng.standard_normal(len(t)), cut, 1.5))
73
+ f = rng.uniform(120, 220) * (rng.uniform(4, 7)) ** ((t / d) ** 1.4)
74
+ tone = np.sign(np.sin(M._phase(f))) * 0.25 + np.sin(M._phase(f * 1.005)) * 0.5
75
+ tone = M._lp(tone, 3000.0)
76
+ env = (t / d) ** rng.uniform(1.8, 3.0)
77
+ mono = (noise * 0.8 + M._unit(tone) * 0.45) * env
78
+ mono[-int(0.01 * SR):] *= np.linspace(1, 0, int(0.01 * SR))
79
+ return _pan(mono, 0, 0) * 0.9 + np.stack([np.roll(mono, 240), mono]) * 0.1
80
+
81
+
82
+ def swell(rng) -> np.ndarray:
83
+ d = rng.uniform(0.9, 2.0)
84
+ t = _t(d)
85
+ x = M._bp(rng.standard_normal(len(t)), rng.uniform(2500, 4000), rng.uniform(9000, 14000))
86
+ env = np.exp((t - d) / (rng.uniform(0.18, 0.35) * d))
87
+ mono = M._unit(x) * env
88
+ mono[-int(0.008 * SR):] *= np.linspace(1, 0, int(0.008 * SR))
89
+ return np.stack([mono, np.roll(mono, 180)])
90
+
91
+
92
+ def boom(rng) -> np.ndarray:
93
+ d = rng.uniform(0.8, 1.6)
94
+ t = _t(d)
95
+ lo = rng.uniform(30, 42)
96
+ f = lo + (rng.uniform(90, 140) - lo) * np.exp(-t / rng.uniform(0.05, 0.12))
97
+ body = np.tanh(rng.uniform(1.5, 3.0) * np.sin(M._phase(f))) * np.exp(-t / (0.35 * d))
98
+ hit = M._lp(rng.standard_normal(len(t)), rng.uniform(1500, 3500)) * np.exp(-t / 0.05)
99
+ mono = M._fade_tail(M._fade_in(M._unit(body) + M._unit(hit) * 0.35, 0.5), 60)
100
+ return np.stack([mono, mono])
101
+
102
+
103
+ def boom808(rng) -> np.ndarray:
104
+ d = rng.uniform(1.1, 1.6)
105
+ t = _t(d)
106
+ f = rng.uniform(45, 60) + rng.uniform(110, 160) * np.exp(-t / 0.04)
107
+ dry = np.tanh(rng.uniform(3.0, 5.0) * np.sin(M._phase(f))) * np.exp(-t / rng.uniform(0.35, 0.55))
108
+ wet = np.zeros_like(dry)
109
+ for k, g in ((0.031, 0.5), (0.047, 0.4), (0.071, 0.3), (0.113, 0.22), (0.17, 0.15)):
110
+ n = int(k * SR)
111
+ wet[n:] += dry[:-n] * g
112
+ mono = M._fade_tail(M._fade_in(M._unit(dry + M._lp(wet, 900.0)), 0.5), 80)
113
+ return np.stack([mono, np.roll(mono, 90)])
114
+
115
+
116
+ def glitch(rng) -> np.ndarray:
117
+ d = rng.uniform(0.3, 0.7)
118
+ n = int(d * SR)
119
+ out = np.zeros(n)
120
+ k = 0
121
+ while k < n:
122
+ L = int(rng.uniform(0.012, 0.06) * SR)
123
+ kind = rng.integers(3)
124
+ tt = np.arange(L) / SR
125
+ if kind == 0:
126
+ seg = np.sign(np.sin(2 * np.pi * rng.uniform(200, 2400) * tt))
127
+ elif kind == 1:
128
+ seg = rng.standard_normal(L)
129
+ else:
130
+ seg = np.sin(2 * np.pi * rng.uniform(800, 5000) * tt)
131
+ seg *= rng.uniform(0.3, 1.0) * (rng.random() > 0.15)
132
+ out[k:k + L] = seg[: max(0, min(L, n - k))]
133
+ k += L
134
+ out = M._fade_tail(M._lp(out, 7000.0), 15)
135
+ return _pan(M._unit(out), rng.uniform(-0.5, 0), rng.uniform(0, 0.5))
136
+
137
+
138
+ TYPES = {"whoosh": whoosh, "swipe": swipe, "riser": riser, "swell": swell, "boom": boom, "boom808": boom808,
139
+ "glitch": glitch}
140
+ SEEDS = {name: 7000 + 100 * i for i, name in enumerate(TYPES)}
141
+
142
+
143
+ def main() -> int:
144
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
145
+ ap.add_argument("pasta", type=Path)
146
+ ap.add_argument("--por-tipo", type=int, default=8)
147
+ a = ap.parse_args()
148
+ a.pasta.mkdir(parents=True, exist_ok=True)
149
+ for name, fn in TYPES.items():
150
+ for i in range(a.por_tipo):
151
+ rng = np.random.default_rng(SEEDS[name] + i)
152
+ x = M._normalize(fn(rng), -3.0)
153
+ M.write_wav(a.pasta / f"{name}_{i + 1:02d}.wav", x)
154
+ print(f"{len(TYPES) * a.por_tipo} sons em {a.pasta}")
155
+ return 0
156
+
157
+
158
+ if __name__ == "__main__":
159
+ sys.exit(main())