primocode 8.42.2 → 9.1.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 (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  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 +42 -195
  7. package/lib/catalogo.js +100 -61
  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 +576 -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 +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
@@ -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,199 @@
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 shutil
40
+ import os
41
+ import subprocess
42
+ import sys
43
+
44
+
45
+ def achar_ffmpeg():
46
+ """Onde está o ffmpeg — pelo PATH, e nos lugares onde ele costuma cair.
47
+
48
+ O PATH de um processo aberto pela interface gráfica não é o PATH do
49
+ terminal: no macOS um app aberto pelo ícone não enxerga /opt/homebrew/bin,
50
+ onde o Homebrew instala. Procurar nos lugares conhecidos evita um "não
51
+ achei" para quem tem o programa instalado.
52
+ """
53
+ achado = shutil.which("ffmpeg")
54
+ if achado:
55
+ return achado
56
+ for c in ("/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg",
57
+ r"C:\Program Files\ffmpeg\bin\ffmpeg.exe"):
58
+ if os.path.exists(c):
59
+ return c
60
+ return None
61
+
62
+
63
+ def faltando():
64
+ """O que precisa estar instalado, e não está."""
65
+ falta = []
66
+ for modulo, pacote in (("cv2", "opencv-python"), ("mediapipe", "mediapipe"), ("numpy", "numpy")):
67
+ try:
68
+ __import__(modulo)
69
+ except ImportError:
70
+ falta.append(pacote)
71
+ return falta
72
+
73
+
74
+ def instalar(pacotes):
75
+ try:
76
+ subprocess.run([sys.executable, "-m", "pip", "install", "--quiet", *pacotes],
77
+ capture_output=True, timeout=900, check=True)
78
+ return not faltando()
79
+ except Exception:
80
+ return False
81
+
82
+
83
+ def recortar(entrada, saida, suavizar=7, encolher=2, limiar=0.6):
84
+ import cv2
85
+ import numpy as np
86
+ import mediapipe as mp
87
+
88
+ captura = cv2.VideoCapture(entrada)
89
+ if not captura.isOpened():
90
+ return {"ok": False, "error": f"não consegui abrir {entrada}"}
91
+
92
+ fps = captura.get(cv2.CAP_PROP_FPS) or 30
93
+ largura = int(captura.get(cv2.CAP_PROP_FRAME_WIDTH))
94
+ altura = int(captura.get(cv2.CAP_PROP_FRAME_HEIGHT))
95
+ total = int(captura.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
96
+
97
+ # VP9 num .webm é o formato de vídeo com transparência que o Chromium do
98
+ # Remotion abre — e é o Chromium dele que vai desenhar isto.
99
+ # O ffmpeg pelo nome cru estoura FileNotFoundError, e o que chega ao
100
+ # usuário é o traceback do Python — que não diz o que instalar. Pior: aqui
101
+ # o vídeo já foi ABERTO e a segmentação está prestes a começar, então a
102
+ # falha aparece depois da espera, não antes dela.
103
+ binario = achar_ffmpeg()
104
+ if not binario:
105
+ captura.release()
106
+ return {"ok": False,
107
+ "error": "o recorte de pessoa precisa do ffmpeg, e não achei nenhum nesta máquina.",
108
+ "comoResolver": "macOS: brew install ffmpeg · Linux: sudo apt install ffmpeg · "
109
+ "Windows: winget install ffmpeg"}
110
+
111
+ escritor = subprocess.Popen(
112
+ [binario, "-hide_banner", "-loglevel", "error", "-y",
113
+ "-f", "rawvideo", "-pix_fmt", "bgra",
114
+ "-s", f"{largura}x{altura}", "-r", str(fps), "-i", "-",
115
+ "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-b:v", "3M",
116
+ "-auto-alt-ref", "0", saida],
117
+ stdin=subprocess.PIPE, stderr=subprocess.PIPE)
118
+
119
+ segmentador = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=1)
120
+ nucleo = np.ones((3, 3), np.uint8)
121
+ feitos = 0
122
+
123
+ try:
124
+ while True:
125
+ ok, quadro = captura.read()
126
+ if not ok:
127
+ break
128
+ resultado = segmentador.process(cv2.cvtColor(quadro, cv2.COLOR_BGR2RGB))
129
+ mascara = (resultado.segmentation_mask > limiar).astype(np.uint8) * 255
130
+
131
+ # Encolher: o corte cai um fio DENTRO da pessoa, em vez de levar
132
+ # um halo da parede junto — é o halo que faz parecer adesivo.
133
+ if encolher > 0:
134
+ mascara = cv2.erode(mascara, nucleo, iterations=int(encolher))
135
+ # Suavizar: borda dura no cabelo é o segundo sinal de recorte
136
+ # automático. Ímpar porque o desfoque gaussiano exige.
137
+ if suavizar > 0:
138
+ k = int(suavizar) | 1
139
+ mascara = cv2.GaussianBlur(mascara, (k, k), 0)
140
+
141
+ bgra = cv2.cvtColor(quadro, cv2.COLOR_BGR2BGRA)
142
+ bgra[:, :, 3] = mascara
143
+ escritor.stdin.write(bgra.tobytes())
144
+ feitos += 1
145
+ finally:
146
+ try:
147
+ escritor.stdin.close()
148
+ except Exception:
149
+ pass
150
+ captura.release()
151
+ segmentador.close()
152
+
153
+ escritor.wait()
154
+ if escritor.returncode != 0:
155
+ erro = (escritor.stderr.read() or b"").decode("utf-8", "ignore")[-400:]
156
+ return {"ok": False, "error": f"o ffmpeg recusou o recorte: {erro}"}
157
+
158
+ return {"ok": True, "arquivo": saida, "quadros": feitos,
159
+ "segundos": round(feitos / fps, 2) if fps else None,
160
+ "largura": largura, "altura": altura, "fps": round(fps, 2)}
161
+
162
+
163
+ def main():
164
+ p = argparse.ArgumentParser(description="Recorte de pessoa do PrimoCode Studio")
165
+ sub = p.add_subparsers(dest="comando", required=True)
166
+ sub.add_parser("conferir")
167
+ r = sub.add_parser("recortar")
168
+ r.add_argument("--entrada", required=True)
169
+ r.add_argument("--saida", required=True)
170
+ r.add_argument("--suavizar", type=int, default=7)
171
+ r.add_argument("--encolher", type=int, default=2)
172
+ r.add_argument("--limiar", type=float, default=0.6)
173
+ args = p.parse_args()
174
+
175
+ falta = faltando()
176
+
177
+ if args.comando == "conferir":
178
+ saida = {"ok": not falta, "falta": falta,
179
+ "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta) if falta else None}
180
+ else:
181
+ if falta:
182
+ # A instalação é pesada (o MediaPipe passa de 100 MB) e demora.
183
+ # Tentar em silêncio seria uma tarefa parada sem explicação; por
184
+ # isso o aviso sai ANTES, na saída de erro, que o agente mostra.
185
+ print(f"instalando {', '.join(falta)} (uma vez, alguns minutos)…", file=sys.stderr)
186
+ if not instalar(falta):
187
+ saida = {"ok": False,
188
+ "error": "o recorte de pessoa precisa do MediaPipe, e não deu para instalá-lo.",
189
+ "comoResolver": f"{sys.executable} -m pip install " + " ".join(falta)}
190
+ print(json.dumps(saida, ensure_ascii=False))
191
+ sys.exit(1)
192
+ saida = recortar(args.entrada, args.saida, args.suavizar, args.encolher, args.limiar)
193
+
194
+ print(json.dumps(saida, ensure_ascii=False))
195
+ sys.exit(0 if saida.get("ok") else 1)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
File without changes