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,66 @@
1
+ {
2
+ "version": 1,
3
+ "about": "Humores da biblioteca de trilhas (helpers/trilhas.py) e o pedido pronto de cada um para o gen_music.py --humor. Só texto: nenhuma música vai junto com a skill.",
4
+ "moods": [
5
+ {
6
+ "id": "animada",
7
+ "label": "Animada",
8
+ "when": "energia, oferta, novidade",
9
+ "prompt": "Upbeat energetic instrumental for a social media ad, bright electronic pop with punchy drums, around 118 bpm, happy and exciting, steady energy from the first second, no vocals, leaves space for voiceover"
10
+ },
11
+ {
12
+ "id": "inspiradora",
13
+ "label": "Inspiradora",
14
+ "when": "conquista, virada de vida, motivação",
15
+ "prompt": "Inspiring motivational instrumental, uplifting piano and soft strings over a steady modern beat, around 95 bpm, hopeful and determined, builds gently, no vocals, leaves space for voiceover"
16
+ },
17
+ {
18
+ "id": "triste",
19
+ "label": "Triste",
20
+ "when": "dor, problema, desabafo",
21
+ "prompt": "Sad emotional instrumental, gentle solo piano with soft ambient pads, slow around 70 bpm, melancholic and reflective, sparse, no drums, no vocals, leaves space for voiceover"
22
+ },
23
+ {
24
+ "id": "tensa",
25
+ "label": "Tensa",
26
+ "when": "segredo, alerta, erro que as pessoas cometem",
27
+ "prompt": "Tense suspenseful instrumental, low pulsing synth bass and ticking percussion, dark cinematic atmosphere, around 90 bpm, mysterious and urgent, no vocals, leaves space for voiceover"
28
+ },
29
+ {
30
+ "id": "calma",
31
+ "label": "Calma",
32
+ "when": "explicação, tutorial, confiança",
33
+ "prompt": "Calm confident instrumental, soft lo-fi beat with warm electric piano, around 85 bpm, relaxed and trustworthy, steady, no vocals, leaves space for voiceover"
34
+ },
35
+ {
36
+ "id": "epica",
37
+ "label": "Épica",
38
+ "when": "revelação, resultado, grande virada",
39
+ "prompt": "Epic cinematic instrumental, big drums and rising orchestral strings, powerful and triumphant, around 100 bpm, dramatic build, no vocals, leaves space for voiceover"
40
+ },
41
+ {
42
+ "id": "divertida",
43
+ "label": "Divertida",
44
+ "when": "humor, bastidor, conteúdo leve",
45
+ "prompt": "Playful fun instrumental, quirky ukulele and light percussion with claps, around 110 bpm, cheerful and light, no vocals, leaves space for voiceover"
46
+ },
47
+ {
48
+ "id": "luxo",
49
+ "label": "Luxo",
50
+ "when": "produto premium, sofisticação",
51
+ "prompt": "Elegant sophisticated instrumental, smooth deep house groove with soft piano chords, around 120 bpm, premium and stylish, no vocals, leaves space for voiceover"
52
+ }
53
+ ],
54
+ "beats": {
55
+ "GANCHO": ["tensa", "animada"],
56
+ "PROBLEMA": ["triste", "tensa"],
57
+ "VIRADA": ["inspiradora", "epica"],
58
+ "PROVA": ["epica", "calma"],
59
+ "CHAMADA": ["animada", "inspiradora"],
60
+ "PASSO": ["calma"],
61
+ "RESULTADO": ["epica", "inspiradora"],
62
+ "CONTEXTO": ["calma", "triste"],
63
+ "EXPERIENCIA": ["inspiradora", "divertida"],
64
+ "RECOMENDACAO": ["animada", "calma"]
65
+ }
66
+ }
@@ -0,0 +1,187 @@
1
+ """valores.py: reads numbers and money said in Portuguese (PT-BR speech and on-screen text).
2
+
3
+ Library used by motion_sugerir.py. Normalizes accents and case (`norm`) and parses numbers written or spoken
4
+ ("1.200", "vinte e cinco") and money ("R$ 5 mil", "10k", "cinco mil reais", "meio milhão", "dois mil e
5
+ quinhentos", the slang "milão", "seis dígitos"). Counts are not money: "10 mil seguidores" is a count.
6
+
7
+ norm(text) -> str lower case, no accents
8
+ parse_number("1.200") -> 1200.0 a plain number (digits or words)
9
+ parse_money("R$ 5 mil") -> 5000.0 the value of a money expression
10
+ find_money(norm(text)) -> [Money] money spans in normalized text (start, end, value, bare)
11
+
12
+ The regex pieces (DIGITS_RE, WORDNUM_RE, MULT_RE, CUR_RE) are public so detectors can build on them.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import unicodedata
19
+ from dataclasses import dataclass
20
+
21
+ # ------------------------------------------------------------------ normalization
22
+
23
+ _PUNCT_MAP = {
24
+ "‘": "'", "’": "'", "´": "'", "`": "'",
25
+ "“": '"', "”": '"', "–": "-", "—": "-",
26
+ " ": " ", " ": " ", "…": "...", "*": "",
27
+ }
28
+
29
+
30
+ def normalize(text: str) -> tuple[str, list[int]]:
31
+ """Lowercase + strip accents. Returns (normalized, index map norm_pos -> original_pos)."""
32
+ out: list[str] = []
33
+ index: list[int] = []
34
+ for i, ch in enumerate(text):
35
+ mapped = _PUNCT_MAP.get(ch)
36
+ pieces = mapped if mapped is not None else unicodedata.normalize("NFKD", ch)
37
+ for piece in pieces:
38
+ if unicodedata.combining(piece):
39
+ continue
40
+ for c in piece.lower():
41
+ out.append(c)
42
+ index.append(i)
43
+ return "".join(out), index
44
+
45
+
46
+ def norm(text: str) -> str:
47
+ return normalize(text)[0]
48
+
49
+
50
+ # ------------------------------------------------------------------ numbers & money
51
+
52
+ WORD_NUMBERS = {
53
+ "um": 1, "uma": 1, "dois": 2, "duas": 2, "tres": 3, "quatro": 4, "cinco": 5, "seis": 6,
54
+ "sete": 7, "oito": 8, "nove": 9, "dez": 10, "onze": 11, "doze": 12, "treze": 13,
55
+ "quatorze": 14, "catorze": 14, "quinze": 15, "dezesseis": 16, "dezessete": 17,
56
+ "dezoito": 18, "dezenove": 19, "vinte": 20, "trinta": 30, "quarenta": 40,
57
+ "cinquenta": 50, "sessenta": 60, "setenta": 70, "oitenta": 80, "noventa": 90,
58
+ "cem": 100, "cento": 100, "duzentos": 200, "duzentas": 200, "trezentos": 300,
59
+ "quatrocentos": 400, "quinhentos": 500, "seiscentos": 600, "setecentos": 700,
60
+ "oitocentos": 800, "novecentos": 900, "meio": 0.5, "meia": 0.5,
61
+ }
62
+ # "milão" is spoken slang for one million ("tá fazendo milão")
63
+ MULTIPLIERS = {"mil": 1_000, "k": 1_000, "milhao": 1_000_000, "milhoes": 1_000_000, "milao": 1_000_000,
64
+ "miloes": 1_000_000, "mi": 1_000_000, "bilhao": 1_000_000_000, "bilhoes": 1_000_000_000,
65
+ "bi": 1_000_000_000}
66
+
67
+ _WORDNUM_ALT = "|".join(sorted(WORD_NUMBERS, key=len, reverse=True))
68
+ WORDNUM_RE = rf"(?:{_WORDNUM_ALT})(?:\s+e\s+(?:{_WORDNUM_ALT}))*"
69
+ DIGITS_RE = r"\d{1,3}(?:\.\d{3})+(?:,\d{1,2})?|\d+(?:[.,]\d{1,2})?"
70
+ NUM_RE = rf"(?:{DIGITS_RE}|\b{WORDNUM_RE})"
71
+ MULT_RE = r"(?:mil|k|milhao|milhoes|milao|miloes|mi|bilhao|bilhoes|bi)"
72
+ CUR_RE = r"(?:reais|real|contos?|pilas?|dolares|dolar|euros?|usd|brl)"
73
+ # spoken compound amounts: "dois mil e quinhentos", "um milhão e meio", "um milhão e 200 mil"
74
+ _COMPOUND_TAIL = rf"(?:\s+e\s+(?:meio|meia|(?:{WORDNUM_RE}|\d{{1,3}})(?:\s+mil\b)?))?"
75
+ # "seis dígitos de faturamento": the number of digits of an amount
76
+ DIGIT_COUNT_RE = r"(?:cinco|seis|sete|oito|nove|5|6|7|8|9)\s+digitos\b"
77
+
78
+ MONEY_RE = (
79
+ rf"(?:(?:r\$|us\$|u\$|\$|€)\s*(?:{DIGITS_RE})(?:\s*{MULT_RE}\b)?(?:\s+(?:de\s+)?{CUR_RE}\b)?"
80
+ rf"|(?<![\w$]){NUM_RE}\s*{MULT_RE}\b{_COMPOUND_TAIL}(?:\s+(?:de\s+)?{CUR_RE}\b)?"
81
+ rf"|(?<![\w$]){NUM_RE}\s+(?:de\s+)?{CUR_RE}\b"
82
+ rf"|\bmil\s+e\s+(?:{WORDNUM_RE}|\d{{1,3}})\b(?:\s+(?:de\s+)?{CUR_RE}\b)?"
83
+ rf"|\bmil\s+(?:de\s+)?{CUR_RE}\b"
84
+ rf"|(?<![\w$]){DIGIT_COUNT_RE}"
85
+ rf"|\b(?:mil\s+)?milh?(?:ao|oes)\b{_COMPOUND_TAIL}(?:\s+(?:de\s+)?{CUR_RE}\b)?)"
86
+ )
87
+ _MONEY_COMPILED = re.compile(MONEY_RE)
88
+
89
+ # Nouns that make a bare "10 mil" a count, not money.
90
+ NON_MONEY_NOUNS = re.compile(
91
+ r"\s*(?:de\s+)?(?:seguidor\w*|inscrit\w*|visualiza\w*|views?|curtidas?|likes?|pessoas?|alun\w*|"
92
+ r"clientes?|leads?|vendas?|pedidos?|unidades?|produtos?|pecas?|metros?|km|quilos?|kg|horas?|"
93
+ r"minutos?|dias?|anos?|mensage\w*|comentarios?|compartilhament\w*|downloads?|acessos?|cliques?|"
94
+ r"impress\w*|membros?|usuari\w*|contatos?|numeros?|videos?|posts?|lojas?|fornecedor\w*|itens|"
95
+ r"copias?|livros?|pacotes?|caixas?|vezes|passos|reels|stories|grupos?|mulheres|homens|familias?)\b"
96
+ )
97
+
98
+
99
+ @dataclass
100
+ class Money:
101
+ start: int
102
+ end: int
103
+ value: float | None
104
+ bare: bool # no currency marker (R$, reais...): needs context to count as money
105
+
106
+
107
+ def parse_number(token: str) -> float | None:
108
+ token = token.strip()
109
+ if not token:
110
+ return None
111
+ if re.fullmatch(r"\d{1,3}(?:\.\d{3})+(?:,\d{1,2})?", token):
112
+ return float(token.replace(".", "").replace(",", "."))
113
+ if re.fullmatch(r"\d+(?:[.,]\d{1,2})?", token):
114
+ return float(token.replace(",", "."))
115
+ words = [w for w in re.split(r"\s+e\s+|\s+", token) if w]
116
+ if words and all(w in WORD_NUMBERS for w in words):
117
+ return float(sum(WORD_NUMBERS[w] for w in words))
118
+ return None
119
+
120
+
121
+ def _parse_compound(t: str) -> float | None:
122
+ """'dois mil e quinhentos' -> 2500, 'mil e quinhentos' -> 1500, 'um milhão e meio' -> 1.5e6."""
123
+ m = re.fullmatch(r"(?:(.+?)\s+)?(mil|milhao|milhoes|milao|miloes)\s+e\s+(meio|meia|.+?)(?:\s+(mil))?", t)
124
+ if not m:
125
+ return None
126
+ unit = float(MULTIPLIERS[m.group(2)])
127
+ head = parse_number(m.group(1)) if m.group(1) else 1.0
128
+ if head is None:
129
+ return None
130
+ if m.group(3) in ("meio", "meia"):
131
+ return head * unit + unit / 2
132
+ tail = parse_number(m.group(3))
133
+ if tail is None:
134
+ return None
135
+ tail *= 1000.0 if m.group(4) else 1.0
136
+ return head * unit + tail if tail < unit else None
137
+
138
+
139
+ def parse_money(text: str) -> float | None:
140
+ """Value in currency units of a money expression. '5 mil' -> 5000, 'R$ 1.500,00' -> 1500."""
141
+ t = norm(text)
142
+ t = re.sub(r"(?:r\$|us\$|u\$|\$|€)", " ", t)
143
+ t = re.sub(rf"\b(?:de\s+)?{CUR_RE}\b", " ", t).strip()
144
+ digits = re.fullmatch(DIGIT_COUNT_RE, t)
145
+ if digits:
146
+ n = WORD_NUMBERS.get(t.split()[0]) or float(t.split()[0])
147
+ return float(10 ** (int(n) - 1))
148
+ compound = _parse_compound(" ".join(t.split()))
149
+ if compound is not None:
150
+ return compound
151
+ mult = 1.0
152
+ m = re.search(rf"({MULT_RE})\s*$", t)
153
+ if m and (m.start(1) == 0 or not t[m.start(1) - 1].isalpha()):
154
+ mult = float(MULTIPLIERS[m.group(1)])
155
+ t = t[: m.start(1)].strip()
156
+ if not t:
157
+ return mult if mult > 1 else None
158
+ if t == "mil": # "mil reais" -> 1000 · "mil milhão" (speech slip) -> 1 milhão
159
+ return 1_000.0 if mult == 1.0 else mult
160
+ base = parse_number(t)
161
+ if base is None:
162
+ return None
163
+ return base * mult
164
+
165
+
166
+ def find_money(ntext: str) -> list[Money]:
167
+ """Money spans in normalized text."""
168
+ found: list[Money] = []
169
+ for m in _MONEY_COMPILED.finditer(ntext):
170
+ s = m.group(0).rstrip()
171
+ start, end = m.start(), m.start() + len(s)
172
+ bare = not re.search(rf"(?:r\$|\$|€|\b{CUR_RE}\b)", s)
173
+ if re.search(r"digitos$", s):
174
+ if re.search(r"\b(?:senha|codigo|numero|pin|token|cep|cpf|cnpj|telefone|celular|chave)\s+(?:\w+\s+)?(?:de|com)\s+$",
175
+ ntext[max(0, start - 30):start]):
176
+ continue # "senha de 6 dígitos"
177
+ bare = False
178
+ counted = NON_MONEY_NOUNS.match(ntext, end) if bare else None
179
+ if counted and not re.match(r"\s+(?:passad[oa]|retrasad[oa]|atras)\b", ntext[counted.end():]):
180
+ continue # "10 mil seguidores" is a count ("1 milhão ano passado" is still money)
181
+ if bare and re.fullmatch(r"\d+\s*(?:mi|bi)", s): # "5 mi" is too ambiguous alone
182
+ continue
183
+ if s in ("milao", "miloes") and re.search(r"\b(?:em|de|para|pra|pro|a|ao|da|do|na|no|via|ate)\s+$",
184
+ ntext[max(0, start - 8):start]):
185
+ continue # "fui pra Milão": the city, not the slang
186
+ found.append(Money(start, end, parse_money(s), bare))
187
+ return found