primocode 8.42.2 → 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.
- package/README.md +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- 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
|
|
121
|
+
global _instalando, _verificado
|
|
139
122
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
140
|
+
return
|
|
182
141
|
with _tranca:
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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],
|
package/studio/narrar.py
ADDED
|
@@ -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()
|
package/studio/pessoa.py
ADDED
|
@@ -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
|