primocode 8.42.1 → 9.0.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 (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +599 -327
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -0,0 +1,40 @@
1
+ ---
2
+ nome: Slides que prendem
3
+ descricao: apresentação sem template, que se lê de longe e não entedia
4
+ quando: slides, slide, apresentação, apresentacao, deck, pitch, palestra
5
+ autor: PrimoCode
6
+ ---
7
+ Slide não é documento projetado. Se ele se lê inteiro em voz alta, é um
8
+ documento — e o público já leu antes de você falar.
9
+
10
+ ## A estrutura, sem modelo pronto
11
+
12
+ Não existe "layout capa" nem "layout duas colunas". Cada slide é composto do
13
+ zero, como uma cena: fundo, elementos posicionados, tipografia escolhida.
14
+
15
+ O que existe é uma ORDEM que funciona: a tensão antes da solução. Comece pelo
16
+ problema que a plateia reconhece, não pela apresentação de quem fala.
17
+
18
+ ## Uma ideia por slide
19
+
20
+ Se precisa rolar o olho para ler, dividiu errado. Título que é uma AFIRMAÇÃO
21
+ ("o custo dobrou em 6 meses"), não um rótulo ("custos").
22
+
23
+ Número grande é slide inteiro: um número, uma linha do que ele significa, e
24
+ mais nada.
25
+
26
+ ## Legibilidade antes de beleza
27
+
28
+ Texto de corpo nunca abaixo de 28pt no 1920×1080. Contraste alto — slide é
29
+ visto em sala com luz. No máximo duas fontes e três cores.
30
+
31
+ ## Ritmo
32
+
33
+ Varie: slide denso, slide de respiro, slide de número, slide de citação.
34
+ Quinze slides iguais em sequência perdem qualquer plateia, por melhores que
35
+ sejam.
36
+
37
+ ## Prova
38
+
39
+ Todo dado tem origem escrita ao lado, pequena. Dado sem fonte é opinião com
40
+ fonte tipográfica melhor — e alguém vai perguntar.
package/studio/edge.py CHANGED
@@ -117,81 +117,32 @@ def disponivel():
117
117
  return _verificado
118
118
 
119
119
 
120
- # Por que a instalação falhava calada, e o vídeo saía com a voz do Google:
121
- # `pip install --user` é recusado pelo Python do sistema em Debian/Ubuntu e
122
- # no do Homebrew ("externally-managed-environment", PEP 668), e em máquina
123
- # sem pip o `-m pip` nem existe. Um `pip install` só, em segundo plano, com a
124
- # saída jogada fora, era exatamente a falha que ninguém vê. Agora são várias
125
- # tentativas, em ordem, e a última linha do erro de cada uma fica guardada
126
- # para o status contar o que houve.
127
- ESTRATEGIAS = [
128
- ["-m", "pip", "install", "--user", "--quiet", "edge-tts"],
129
- ["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", "edge-tts"],
130
- ["-m", "pip", "install", "--quiet", "edge-tts"],
131
- ["-m", "pip", "install", "--quiet", "--break-system-packages", "edge-tts"],
132
- ]
133
- _ultimo_erro = ""
134
- _thread = None
135
-
136
-
137
120
  def _rotina_instalar():
138
- global _instalando, _verificado, _ultimo_erro
121
+ global _instalando, _verificado
139
122
  try:
140
- # Sem pip, primeiro o ensurepip — é o que o python.org e a Apple
141
- # trazem para o caso.
142
- try:
143
- r = subprocess.run([sys.executable, "-m", "pip", "--version"],
144
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30)
145
- if r.returncode != 0:
146
- subprocess.run([sys.executable, "-m", "ensurepip", "--user", "--default-pip"],
147
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120)
148
- except Exception:
149
- pass
150
- for args in ESTRATEGIAS:
151
- try:
152
- r = subprocess.run([sys.executable] + args, stdout=subprocess.DEVNULL,
153
- stderr=subprocess.PIPE, timeout=240)
154
- except Exception as e:
155
- _ultimo_erro = str(e)[-200:]
156
- continue
157
- _verificado = None
158
- if r.returncode == 0 and disponivel():
159
- _ultimo_erro = ""
160
- sys.stderr.write("[edge] edge-tts instalado (%s)\n" % " ".join(args[2:]))
161
- return
162
- _ultimo_erro = (r.stderr or b"").decode("utf-8", "replace").strip().splitlines()[-1:][0][-200:] \
163
- if (r.stderr or b"").strip() else "pip devolveu %s" % r.returncode
164
- sys.stderr.write("[edge] não consegui instalar o edge-tts: %s\n" % _ultimo_erro)
123
+ subprocess.run(
124
+ [sys.executable, "-m", "pip", "install", "--user", "--quiet", "edge-tts"],
125
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180)
126
+ except Exception as e:
127
+ sys.stderr.write("[edge] pip install falhou: %s\n" % e)
165
128
  finally:
166
129
  _verificado = None # força nova checagem
167
130
  with _tranca:
168
131
  _instalando = False
169
132
 
170
133
 
171
- def instalar(esperar=0):
172
- """Traz o pacote — em segundo plano por padrão; com `esperar` (segundos),
173
- quem chamou fica aguardando até esse teto.
174
-
175
- Esperar existe porque a narração de um vídeo NÃO pode sair pela voz de
176
- reserva só porque o pip ainda estava rodando: quem pede voz agora e tem
177
- internet merece a voz neural agora. Meio minuto de pip é menos ruim que
178
- um vídeo inteiro com voz de robô."""
179
- global _instalando, _thread
134
+ def instalar():
135
+ """Tenta trazer o pacote em segundo plano, uma vez. Não bloqueia: quem
136
+ pediu narração agora ouve o próximo motor da corrente, e a partir da
137
+ próxima vez o Edge já responde."""
138
+ global _instalando
180
139
  if disponivel():
181
- return True
140
+ return
182
141
  with _tranca:
183
- if not _instalando:
184
- _instalando = True
185
- _thread = threading.Thread(target=_rotina_instalar, daemon=True)
186
- _thread.start()
187
- t = _thread
188
- if t and esperar:
189
- t.join(esperar)
190
- return disponivel()
191
-
192
-
193
- def ultimo_erro():
194
- return _ultimo_erro
142
+ if _instalando:
143
+ return
144
+ _instalando = True
145
+ threading.Thread(target=_rotina_instalar, daemon=True).start()
195
146
 
196
147
 
197
148
  def _dur_do_vtt(caminho):
@@ -264,7 +215,6 @@ def estado():
264
215
  "motor": "edge",
265
216
  "disponivel": disponivel(),
266
217
  "instalando": inst,
267
- "erro": _ultimo_erro or None,
268
218
  "precisa_internet": True,
269
219
  "vozes": [{"id": v["id"], "nome": v["nome"], "genero": v["genero"],
270
220
  "padrao": bool(v.get("padrao"))} for v in CATALOGO],
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python3
2
+ """narrar.py — a narração, em Python, sem servidor.
3
+
4
+ "Narração: gerar em Python, com biblioteca de TTS instalada no projeto. A
5
+ IA escolhe a voz e o estilo da narração."
6
+
7
+ ── POR QUE UM SCRIPT, E NÃO UM SERVIDOR ─────────────────────────────────────
8
+ O Studio antigo subia um servidor Python em localhost:7777 e falava com ele
9
+ por HTTP — inclusive para gerar narração. Aquilo existia porque havia uma
10
+ interface web para servir. A interface morreu; o servidor não tem mais razão
11
+ de ser, e com ele some uma classe inteira de defeito ("o estúdio não está
12
+ rodando", porta ocupada, processo zumbi entre sessões).
13
+
14
+ Aqui é um comando: entra texto, sai um mp3 e os tempos de cada fala.
15
+
16
+ ── OS TEMPOS SÃO MEDIDOS, NÃO ESTIMADOS ─────────────────────────────────────
17
+ O edge-tts escreve as legendas junto com o áudio (`--write-media` +
18
+ `--write-subtitles`). Delas sai o instante exato de cada frase.
19
+
20
+ Isso é o que faz a cena caber na fala. Estimar duração por número de palavras
21
+ erra em segundos, e o erro aparece como imagem trocando antes de a frase
22
+ acabar — o defeito de vídeo automático mais fácil de reconhecer.
23
+
24
+ ── A CORRENTE ───────────────────────────────────────────────────────────────
25
+ 1. ELEVENLABS se houver chave com crédito (melhor entonação, custa).
26
+ 2. EDGE TTS neural, grátis, sem chave, sem conta. ← o padrão
27
+ 3. diz que não deu, com o comando de instalar.
28
+
29
+ Nunca uma voz de robô como "reserva": entregar um vídeo com voz dos anos 90
30
+ é pior que entregar sem narração e dizer o que falta.
31
+
32
+ Uso:
33
+ python3 narrar.py falar --texto "..." --saida narracao.mp3 [--voz antonio]
34
+ [--ritmo -8%] [--tom +2Hz]
35
+ python3 narrar.py vozes
36
+ """
37
+
38
+ import argparse
39
+ import json
40
+ import os
41
+ import re
42
+ import subprocess
43
+ import sys
44
+ import tempfile
45
+ from pathlib import Path
46
+
47
+ AQUI = Path(__file__).resolve().parent
48
+ sys.path.insert(0, str(AQUI))
49
+
50
+ # ── catálogo ────────────────────────────────────────────────────────────────
51
+ # As vozes neurais de português do Brasil que existem no serviço da Microsoft.
52
+ # Não há por que inventar mais: são estas três, e o `estilo` é o que a IA
53
+ # escolhe para dar registro diferente à mesma voz.
54
+ VOZES = [
55
+ {"id": "antonio", "voz": "pt-BR-AntonioNeural", "nome": "Antonio",
56
+ "genero": "masculina", "serve": "locução, abertura, institucional", "padrao": True},
57
+ {"id": "francisca", "voz": "pt-BR-FranciscaNeural", "nome": "Francisca",
58
+ "genero": "feminina", "serve": "conversa, explicação, tutorial"},
59
+ {"id": "thalita", "voz": "pt-BR-ThalitaMultilingualNeural", "nome": "Thalita",
60
+ "genero": "feminina", "serve": "jovem, anúncio, redes sociais"},
61
+ ]
62
+
63
+ # O "estilo" não existe como parâmetro no edge-tts: ele é ritmo + tom. Estes
64
+ # quatro cobrem o que se pede num vídeo, e são o ponto de partida — a IA pode
65
+ # mandar ritmo e tom na mão quando quiser outra coisa.
66
+ ESTILOS = {
67
+ "normal": {"ritmo": "+0%", "tom": "+0Hz"},
68
+ "anuncio": {"ritmo": "+8%", "tom": "+2Hz"}, # mais rápido e mais alto: energia
69
+ "calmo": {"ritmo": "-8%", "tom": "-2Hz"}, # mais devagar e mais grave: explicação
70
+ "dramatico": {"ritmo": "-14%", "tom": "-4Hz"}, # pausa e peso
71
+ }
72
+
73
+
74
+ def voz_de(apelido):
75
+ a = (apelido or "").strip().lower()
76
+ for v in VOZES:
77
+ if a in (v["id"], v["voz"].lower(), v["nome"].lower()):
78
+ return v
79
+ return VOZES[0]
80
+
81
+
82
+ def tempo_vtt(marca):
83
+ """"00:00:03.480" → 3.48"""
84
+ h, m, s = marca.split(":")
85
+ return int(h) * 3600 + int(m) * 60 + float(s.replace(",", "."))
86
+
87
+
88
+ def ler_legendas(caminho):
89
+ """As falas com início e fim, lidas do VTT que o edge-tts escreveu."""
90
+ falas = []
91
+ try:
92
+ bruto = Path(caminho).read_text(encoding="utf-8")
93
+ except OSError:
94
+ return falas
95
+ bloco = None
96
+ for linha in bruto.splitlines():
97
+ linha = linha.strip()
98
+ if "-->" in linha:
99
+ de, ate = [p.strip() for p in linha.split("-->")[:2]]
100
+ bloco = {"de": tempo_vtt(de), "ate": tempo_vtt(ate.split(" ")[0]), "texto": ""}
101
+ elif bloco is not None and linha:
102
+ bloco["texto"] = (bloco["texto"] + " " + linha).strip()
103
+ elif bloco is not None and not linha:
104
+ if bloco["texto"]:
105
+ falas.append(bloco)
106
+ bloco = None
107
+ if bloco and bloco["texto"]:
108
+ falas.append(bloco)
109
+ return falas
110
+
111
+
112
+ def edge_disponivel():
113
+ try:
114
+ subprocess.run([sys.executable, "-m", "edge_tts", "--help"],
115
+ capture_output=True, timeout=25, check=True)
116
+ return True
117
+ except Exception:
118
+ return False
119
+
120
+
121
+ # Por que uma única tentativa de pip não serve — e falha CALADA.
122
+ #
123
+ # Em Debian e Ubuntu, e no Python do Homebrew, o `pip install` no Python do
124
+ # sistema é recusado de saída: "externally-managed-environment" (PEP 668). Em
125
+ # máquina sem pip, o `-m pip` nem existe. Uma tentativa só, com a saída jogada
126
+ # fora, é exatamente a falha que ninguém vê: a instalação "não deu erro", o
127
+ # edge-tts continua ausente, e a narração cai calada na voz de reserva — o
128
+ # vídeo sai com outra voz e ninguém sabe por quê.
129
+ #
130
+ # Então são várias tentativas, em ordem da menos invasiva para a mais, e o
131
+ # erro da última fica guardado para a resposta poder contar o que houve.
132
+ ESTRATEGIAS = [
133
+ ["-m", "pip", "install", "--user", "--quiet", "edge-tts"],
134
+ ["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", "edge-tts"],
135
+ ["-m", "pip", "install", "--quiet", "edge-tts"],
136
+ ["-m", "pip", "install", "--quiet", "--break-system-packages", "edge-tts"],
137
+ ]
138
+
139
+ ultimo_erro_pip = ""
140
+
141
+
142
+ def instalar_edge():
143
+ """Traz o edge-tts com pip. Uma vez, e o usuário vê acontecer."""
144
+ global ultimo_erro_pip
145
+
146
+ # Sem pip, primeiro o ensurepip — é o que o python.org e a Apple trazem
147
+ # justamente para este caso.
148
+ try:
149
+ r = subprocess.run([sys.executable, "-m", "pip", "--version"],
150
+ capture_output=True, timeout=30)
151
+ if r.returncode != 0:
152
+ subprocess.run([sys.executable, "-m", "ensurepip", "--default-pip"],
153
+ capture_output=True, timeout=120)
154
+ except Exception as e:
155
+ ultimo_erro_pip = str(e)
156
+
157
+ for args in ESTRATEGIAS:
158
+ try:
159
+ r = subprocess.run([sys.executable] + args, capture_output=True, timeout=240)
160
+ if r.returncode == 0 and edge_disponivel():
161
+ return True
162
+ saida = (r.stderr or b"").decode("utf-8", "replace").strip()
163
+ if saida:
164
+ ultimo_erro_pip = saida.splitlines()[-1][:300]
165
+ except Exception as e:
166
+ ultimo_erro_pip = str(e)[:300]
167
+ return False
168
+
169
+
170
+ def falar_edge(texto, saida, voz, ritmo, tom):
171
+ with tempfile.TemporaryDirectory() as tmp:
172
+ vtt = str(Path(tmp) / "legenda.vtt")
173
+ cmd = [
174
+ sys.executable, "-m", "edge_tts",
175
+ "--voice", voz,
176
+ "--text", texto,
177
+ "--write-media", str(saida),
178
+ "--write-subtitles", vtt,
179
+ ]
180
+ if ritmo and ritmo != "+0%":
181
+ cmd += ["--rate", ritmo]
182
+ if tom and tom != "+0Hz":
183
+ cmd += ["--pitch", tom]
184
+
185
+ r = subprocess.run(cmd, capture_output=True, timeout=300)
186
+ if r.returncode != 0 or not Path(saida).exists():
187
+ return {"ok": False, "error": (r.stderr or b"").decode("utf-8", "ignore")[-400:]}
188
+
189
+ falas = ler_legendas(vtt)
190
+ return {
191
+ "ok": True,
192
+ "arquivo": str(saida),
193
+ "motor": "edge",
194
+ "voz": voz,
195
+ "segundos": round(falas[-1]["ate"], 3) if falas else None,
196
+ "falas": [{"de": round(f["de"], 3), "ate": round(f["ate"], 3), "texto": f["texto"]}
197
+ for f in falas],
198
+ }
199
+
200
+
201
+ def falar_elevenlabs(texto, saida, voz):
202
+ """Só quando há chave. Sem ela, nem se tenta — e não é erro."""
203
+ chave = os.environ.get("ELEVENLABS_API_KEY", "").strip()
204
+ if not chave:
205
+ return None
206
+ try:
207
+ sys.path.insert(0, str(AQUI))
208
+ import elevenlabs # o módulo que já existia no Studio
209
+ r = elevenlabs.falar(texto, str(saida), voz=voz, chave=chave)
210
+ if r and r.get("ok"):
211
+ r["motor"] = "elevenlabs"
212
+ return r
213
+ except Exception:
214
+ pass
215
+ return None
216
+
217
+
218
+ def comando_falar(args):
219
+ texto = args.texto
220
+ if args.arquivo:
221
+ texto = Path(args.arquivo).read_text(encoding="utf-8")
222
+ texto = re.sub(r"\s+", " ", (texto or "")).strip()
223
+ if not texto:
224
+ return {"ok": False, "error": "não há texto para narrar"}
225
+
226
+ v = voz_de(args.voz)
227
+ estilo = ESTILOS.get((args.estilo or "normal").lower(), ESTILOS["normal"])
228
+ ritmo = args.ritmo or estilo["ritmo"]
229
+ tom = args.tom or estilo["tom"]
230
+
231
+ premium = falar_elevenlabs(texto, args.saida, v["id"])
232
+ if premium:
233
+ return premium
234
+
235
+ if not edge_disponivel() and not instalar_edge():
236
+ return {
237
+ "ok": False,
238
+ "error": "a narração precisa do pacote edge-tts, e não deu para instalá-lo aqui.",
239
+ "comoResolver": f"{sys.executable} -m pip install edge-tts"
240
+ + (" (no Debian/Ubuntu ou no Homebrew, acrescente --break-system-packages)"
241
+ if "externally-managed" in ultimo_erro_pip else ""),
242
+ "erroDoPip": ultimo_erro_pip or None,
243
+ }
244
+
245
+ return falar_edge(texto, args.saida, v["voz"], ritmo, tom)
246
+
247
+
248
+ def main():
249
+ p = argparse.ArgumentParser(description="Narração do PrimoCode Studio")
250
+ sub = p.add_subparsers(dest="comando", required=True)
251
+
252
+ f = sub.add_parser("falar")
253
+ f.add_argument("--texto", default="")
254
+ f.add_argument("--arquivo", default="")
255
+ f.add_argument("--saida", required=True)
256
+ f.add_argument("--voz", default="antonio")
257
+ f.add_argument("--estilo", default="normal")
258
+ f.add_argument("--ritmo", default="")
259
+ f.add_argument("--tom", default="")
260
+
261
+ sub.add_parser("vozes")
262
+
263
+ args = p.parse_args()
264
+ if args.comando == "vozes":
265
+ saida = {"ok": True, "vozes": VOZES, "estilos": list(ESTILOS.keys())}
266
+ else:
267
+ saida = comando_falar(args)
268
+
269
+ print(json.dumps(saida, ensure_ascii=False))
270
+ sys.exit(0 if saida.get("ok") else 1)
271
+
272
+
273
+ if __name__ == "__main__":
274
+ main()
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """pessoa.py — tirar você do fundo do seu próprio vídeo.
3
+
4
+ "Recorte de pessoa: segmentar meu rosto/corpo e trocar o fundo por uma
5
+ imagem relacionada ao que estou falando (ex.: falando sobre o olho →
6
+ imagem do olho no fundo)."
7
+
8
+ ── POR QUE ISTO NÃO É FEITO COM ffmpeg ──────────────────────────────────────
9
+ O ffmpeg sabe fazer chroma key: tirar um fundo VERDE. Ele não sabe olhar um
10
+ quadro e dizer onde termina a pessoa e começa a parede — isso é trabalho de
11
+ modelo de visão, quadro a quadro.
12
+
13
+ O MediaPipe faz exatamente isso, roda no computador da pessoa, é grátis e não
14
+ precisa de chave. O custo é o download do modelo na primeira vez.
15
+
16
+ ── O QUE SAI DAQUI ──────────────────────────────────────────────────────────
17
+ Um .webm com CANAL ALFA: a pessoa recortada, e transparência no lugar do
18
+ fundo. Ele não é o resultado final — é a peça que o `video.sobreporAlfa`
19
+ costura sobre a imagem escolhida, ou que entra como elemento de vídeo numa
20
+ cena com qualquer coisa atrás.
21
+
22
+ Separar assim é de propósito: o recorte é caro (segundos por segundo de
23
+ vídeo) e o fundo muda de ideia. Recortar uma vez e testar cinco fundos custa
24
+ um recorte; recortar junto com o fundo custa cinco.
25
+
26
+ ── A BORDA É O QUE DENUNCIA ─────────────────────────────────────────────────
27
+ Recorte automático erra na borda do cabelo, e o erro aparece como um contorno
28
+ duro, tipo adesivo. Duas defesas: suavizar a máscara (desfoque leve) e
29
+ encolhê-la um pouco, para o corte cair um fio DENTRO da pessoa em vez de
30
+ pegar um halo da parede.
31
+
32
+ Uso:
33
+ python3 pessoa.py recortar --entrada gravacao.mp4 --saida pessoa.webm
34
+ python3 pessoa.py conferir
35
+ """
36
+
37
+ import argparse
38
+ import json
39
+ import subprocess
40
+ import sys
41
+
42
+
43
+ def faltando():
44
+ """O que precisa estar instalado, e não está."""
45
+ falta = []
46
+ for modulo, pacote in (("cv2", "opencv-python"), ("mediapipe", "mediapipe"), ("numpy", "numpy")):
47
+ try:
48
+ __import__(modulo)
49
+ except ImportError:
50
+ falta.append(pacote)
51
+ return falta
52
+
53
+
54
+ def instalar(pacotes):
55
+ try:
56
+ subprocess.run([sys.executable, "-m", "pip", "install", "--quiet", *pacotes],
57
+ capture_output=True, timeout=900, check=True)
58
+ return not faltando()
59
+ except Exception:
60
+ return False
61
+
62
+
63
+ def recortar(entrada, saida, suavizar=7, encolher=2, limiar=0.6):
64
+ import cv2
65
+ import numpy as np
66
+ import mediapipe as mp
67
+
68
+ captura = cv2.VideoCapture(entrada)
69
+ if not captura.isOpened():
70
+ return {"ok": False, "error": f"não consegui abrir {entrada}"}
71
+
72
+ fps = captura.get(cv2.CAP_PROP_FPS) or 30
73
+ largura = int(captura.get(cv2.CAP_PROP_FRAME_WIDTH))
74
+ altura = int(captura.get(cv2.CAP_PROP_FRAME_HEIGHT))
75
+ total = int(captura.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
76
+
77
+ # VP9 num .webm é o formato de vídeo com transparência que o Chromium do
78
+ # Remotion abre — e é o Chromium dele que vai desenhar isto.
79
+ escritor = subprocess.Popen(
80
+ ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
81
+ "-f", "rawvideo", "-pix_fmt", "bgra",
82
+ "-s", f"{largura}x{altura}", "-r", str(fps), "-i", "-",
83
+ "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-b:v", "3M",
84
+ "-auto-alt-ref", "0", saida],
85
+ stdin=subprocess.PIPE, stderr=subprocess.PIPE)
86
+
87
+ segmentador = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=1)
88
+ nucleo = np.ones((3, 3), np.uint8)
89
+ feitos = 0
90
+
91
+ try:
92
+ while True:
93
+ ok, quadro = captura.read()
94
+ if not ok:
95
+ break
96
+ resultado = segmentador.process(cv2.cvtColor(quadro, cv2.COLOR_BGR2RGB))
97
+ mascara = (resultado.segmentation_mask > limiar).astype(np.uint8) * 255
98
+
99
+ # Encolher: o corte cai um fio DENTRO da pessoa, em vez de levar
100
+ # um halo da parede junto — é o halo que faz parecer adesivo.
101
+ if encolher > 0:
102
+ mascara = cv2.erode(mascara, nucleo, iterations=int(encolher))
103
+ # Suavizar: borda dura no cabelo é o segundo sinal de recorte
104
+ # automático. Ímpar porque o desfoque gaussiano exige.
105
+ if suavizar > 0:
106
+ k = int(suavizar) | 1
107
+ mascara = cv2.GaussianBlur(mascara, (k, k), 0)
108
+
109
+ bgra = cv2.cvtColor(quadro, cv2.COLOR_BGR2BGRA)
110
+ bgra[:, :, 3] = mascara
111
+ escritor.stdin.write(bgra.tobytes())
112
+ feitos += 1
113
+ finally:
114
+ try:
115
+ escritor.stdin.close()
116
+ except Exception:
117
+ pass
118
+ captura.release()
119
+ segmentador.close()
120
+
121
+ escritor.wait()
122
+ if escritor.returncode != 0:
123
+ erro = (escritor.stderr.read() or b"").decode("utf-8", "ignore")[-400:]
124
+ return {"ok": False, "error": f"o ffmpeg recusou o recorte: {erro}"}
125
+
126
+ return {"ok": True, "arquivo": saida, "quadros": feitos,
127
+ "segundos": round(feitos / fps, 2) if fps else None,
128
+ "largura": largura, "altura": altura, "fps": round(fps, 2)}
129
+
130
+
131
+ def main():
132
+ p = argparse.ArgumentParser(description="Recorte de pessoa do PrimoCode Studio")
133
+ sub = p.add_subparsers(dest="comando", required=True)
134
+ sub.add_parser("conferir")
135
+ r = sub.add_parser("recortar")
136
+ r.add_argument("--entrada", required=True)
137
+ r.add_argument("--saida", required=True)
138
+ r.add_argument("--suavizar", type=int, default=7)
139
+ r.add_argument("--encolher", type=int, default=2)
140
+ r.add_argument("--limiar", type=float, default=0.6)
141
+ args = p.parse_args()
142
+
143
+ falta = faltando()
144
+
145
+ if args.comando == "conferir":
146
+ saida = {"ok": not falta, "falta": falta,
147
+ "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta) if falta else None}
148
+ else:
149
+ if falta:
150
+ # A instalação é pesada (o MediaPipe passa de 100 MB) e demora.
151
+ # Tentar em silêncio seria uma tarefa parada sem explicação; por
152
+ # isso o aviso sai ANTES, na saída de erro, que o agente mostra.
153
+ print(f"instalando {', '.join(falta)} (uma vez, alguns minutos)…", file=sys.stderr)
154
+ if not instalar(falta):
155
+ saida = {"ok": False,
156
+ "error": "o recorte de pessoa precisa do MediaPipe, e não deu para instalá-lo.",
157
+ "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta)}
158
+ print(json.dumps(saida, ensure_ascii=False))
159
+ sys.exit(1)
160
+ saida = recortar(args.entrada, args.saida, args.suavizar, args.encolher, args.limiar)
161
+
162
+ print(json.dumps(saida, ensure_ascii=False))
163
+ sys.exit(0 if saida.get("ok") else 1)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
File without changes