primocode 9.10.0 → 10.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 +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +41 -18
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/remotion.js +87 -43
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +3 -4
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/studio/texto.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""texto.py — desenha os textos do editor de vídeo em PNG transparente.
|
|
2
|
+
|
|
3
|
+
python3 texto.py estados job.json → um png por estado (lib/video/texto.js)
|
|
4
|
+
python3 texto.py grade job.json → a folha do inspect: quadros com grade
|
|
5
|
+
python3 texto.py conferir → {"ok": true} se o Pillow está aqui
|
|
6
|
+
|
|
7
|
+
── POR QUE PNG E NÃO drawtext ─────────────────────────────────────────────
|
|
8
|
+
O ffmpeg do Homebrew saiu sem drawtext e sem libass (ffmpeg 9, set/2026).
|
|
9
|
+
Desenhar aqui funciona igual em qualquer ffmpeg, e a animação vem pronta do
|
|
10
|
+
JavaScript: este arquivo só desenha o que recebe, não decide nada.
|
|
11
|
+
|
|
12
|
+
Responde sempre UMA linha de JSON no stdout.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
22
|
+
except Exception: # sem Pillow: o node avisa com o comando de instalar
|
|
23
|
+
Image = None
|
|
24
|
+
|
|
25
|
+
FONTES = {
|
|
26
|
+
"bold": [
|
|
27
|
+
"/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/Library/Fonts/Arial Bold.ttf",
|
|
28
|
+
"C:/Windows/Fonts/arialbd.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
29
|
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
|
30
|
+
],
|
|
31
|
+
"black": [
|
|
32
|
+
"/System/Library/Fonts/Supplemental/Arial Black.ttf", "C:/Windows/Fonts/ariblk.ttf",
|
|
33
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
34
|
+
],
|
|
35
|
+
"regular": [
|
|
36
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf", "/Library/Fonts/Arial.ttf", "C:/Windows/Fonts/arial.ttf",
|
|
37
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_cache = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fonte(nome, tamanho):
|
|
45
|
+
tamanho = max(4, int(round(tamanho)))
|
|
46
|
+
chave = (nome, tamanho)
|
|
47
|
+
if chave in _cache:
|
|
48
|
+
return _cache[chave]
|
|
49
|
+
caminhos = [nome] if nome and os.path.isfile(str(nome)) else FONTES.get(nome or "bold", FONTES["bold"]) + FONTES["bold"]
|
|
50
|
+
f = None
|
|
51
|
+
for c in caminhos:
|
|
52
|
+
if os.path.isfile(c):
|
|
53
|
+
try:
|
|
54
|
+
f = ImageFont.truetype(c, tamanho)
|
|
55
|
+
break
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
if f is None:
|
|
59
|
+
try:
|
|
60
|
+
f = ImageFont.load_default(size=tamanho)
|
|
61
|
+
except TypeError:
|
|
62
|
+
f = ImageFont.load_default()
|
|
63
|
+
_cache[chave] = f
|
|
64
|
+
return f
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cor(hexa, alpha=1.0):
|
|
68
|
+
h = str(hexa or "#FFFFFF").lstrip("#")
|
|
69
|
+
if len(h) == 3:
|
|
70
|
+
h = "".join(ch * 2 for ch in h)
|
|
71
|
+
a = int(h[6:8], 16) / 255 if len(h) == 8 else 1.0
|
|
72
|
+
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), int(255 * max(0.0, min(1.0, a * alpha))))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def largura(f, t):
|
|
76
|
+
return f.getlength(t) if hasattr(f, "getlength") else f.getsize(t)[0]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def desenhar_item(img, it):
|
|
80
|
+
escala = float(it.get("scale", 1))
|
|
81
|
+
alpha = float(it.get("alpha", 1))
|
|
82
|
+
tam = it["size"] * escala
|
|
83
|
+
f = fonte(it.get("font"), tam)
|
|
84
|
+
espaco = largura(f, " ")
|
|
85
|
+
maxw = max(10, it["w"] * escala)
|
|
86
|
+
# Quebra de linha pela largura, com a palavra no tamanho BASE: a palavra
|
|
87
|
+
# em destaque cresce sem empurrar as outras (senão a legenda treme).
|
|
88
|
+
linhas, atual, w_atual = [], [], 0.0
|
|
89
|
+
for w in it["words"]:
|
|
90
|
+
lw = largura(f, w["t"])
|
|
91
|
+
if atual and w_atual + espaco + lw > maxw * 0.92:
|
|
92
|
+
linhas.append((atual, w_atual))
|
|
93
|
+
atual, w_atual = [], 0.0
|
|
94
|
+
w_atual = lw if not atual else w_atual + espaco + lw
|
|
95
|
+
atual.append((w, lw))
|
|
96
|
+
if atual:
|
|
97
|
+
linhas.append((atual, w_atual))
|
|
98
|
+
altura_linha = tam * 1.18
|
|
99
|
+
total_h = altura_linha * len(linhas)
|
|
100
|
+
cx, cy = it["cx"], it["cy"]
|
|
101
|
+
camada = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
|
102
|
+
d = ImageDraw.Draw(camada)
|
|
103
|
+
if it.get("bg"):
|
|
104
|
+
pad = tam * 0.35
|
|
105
|
+
larg = max(l for _, l in linhas)
|
|
106
|
+
d.rounded_rectangle([cx - larg / 2 - pad, cy - total_h / 2 - pad * 0.6, cx + larg / 2 + pad, cy + total_h / 2 + pad * 0.6],
|
|
107
|
+
radius=tam * 0.3, fill=cor(it["bg"], alpha))
|
|
108
|
+
sw = int(round(it.get("sw", 0) * escala))
|
|
109
|
+
for n, (palavras, _) in enumerate(linhas):
|
|
110
|
+
y = cy - total_h / 2 + altura_linha * (n + 0.5)
|
|
111
|
+
# A linha é quebrada no tamanho base, mas posicionada com a largura
|
|
112
|
+
# REAL de cada palavra: a palavra em destaque abre espaço para si.
|
|
113
|
+
medidas = []
|
|
114
|
+
for w, lw in palavras:
|
|
115
|
+
s = float(w.get("s", 1))
|
|
116
|
+
fw = fonte(it.get("font"), tam * s) if s != 1 else f
|
|
117
|
+
medidas.append((w, largura(fw, w["t"]) if s != 1 else lw, fw, s))
|
|
118
|
+
larg = sum(m[1] for m in medidas) + espaco * (len(medidas) - 1)
|
|
119
|
+
alinhar = it.get("align", "center")
|
|
120
|
+
x = cx - larg / 2 if alinhar == "center" else (cx - maxw / 2 if alinhar == "left" else cx + maxw / 2 - larg)
|
|
121
|
+
for w, lw, fw, s in medidas:
|
|
122
|
+
a = alpha * float(w.get("a", 1))
|
|
123
|
+
texto = w.get("show", w["t"])
|
|
124
|
+
if a > 0.01 and texto:
|
|
125
|
+
meio = x + lw / 2
|
|
126
|
+
# "show" (typewriter) escreve a partir da esquerda da palavra.
|
|
127
|
+
if "show" in w:
|
|
128
|
+
d.text((x, y), texto, font=fw, anchor="lm", fill=cor(w.get("c", it["color"]), a),
|
|
129
|
+
stroke_width=int(sw * s), stroke_fill=cor(it.get("stroke") or "#000000", a))
|
|
130
|
+
else:
|
|
131
|
+
d.text((meio, y), texto, font=fw, anchor="mm", fill=cor(w.get("c", it["color"]), a),
|
|
132
|
+
stroke_width=int(sw * s) if it.get("stroke") else 0, stroke_fill=cor(it.get("stroke") or "#000000", a))
|
|
133
|
+
x += lw + espaco
|
|
134
|
+
if it.get("rot"):
|
|
135
|
+
camada = camada.rotate(-float(it["rot"]), center=(cx, cy), resample=Image.BICUBIC)
|
|
136
|
+
img.alpha_composite(camada)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def estados(job):
|
|
140
|
+
W, H = int(job["width"]), int(job["height"])
|
|
141
|
+
feitos = 0
|
|
142
|
+
for e in job["estados"]:
|
|
143
|
+
img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|
144
|
+
for it in e["items"]:
|
|
145
|
+
desenhar_item(img, it)
|
|
146
|
+
img.save(e["file"], optimize=False, compress_level=3)
|
|
147
|
+
feitos += 1
|
|
148
|
+
vazio = job.get("vazio")
|
|
149
|
+
if vazio and not os.path.exists(vazio):
|
|
150
|
+
Image.new("RGBA", (W, H), (0, 0, 0, 0)).save(vazio)
|
|
151
|
+
return {"ok": True, "feitos": feitos}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def grade(job):
|
|
155
|
+
"""Cada quadro com a grade 0–1 e o número do quadro; todos numa folha."""
|
|
156
|
+
quadros = []
|
|
157
|
+
for q in job["frames"]:
|
|
158
|
+
im = Image.open(q["png"]).convert("RGBA")
|
|
159
|
+
W, H = im.size
|
|
160
|
+
cam = Image.new("RGBA", im.size, (0, 0, 0, 0))
|
|
161
|
+
d = ImageDraw.Draw(cam)
|
|
162
|
+
f = fonte("bold", max(12, H // 60))
|
|
163
|
+
for k in range(1, 10):
|
|
164
|
+
forte = k == 5
|
|
165
|
+
c = (255, 255, 255, 150 if forte else 80)
|
|
166
|
+
d.line([(W * k / 10, 0), (W * k / 10, H)], fill=c, width=3 if forte else 1)
|
|
167
|
+
d.line([(0, H * k / 10), (W, H * k / 10)], fill=c, width=3 if forte else 1)
|
|
168
|
+
rot = "%.1f" % (k / 10)
|
|
169
|
+
d.text((W * k / 10 + 4, 4), rot, font=f, fill=(255, 255, 0, 230), stroke_width=2, stroke_fill=(0, 0, 0, 230))
|
|
170
|
+
d.text((4, H * k / 10 + 2), rot, font=f, fill=(0, 255, 255, 230), stroke_width=2, stroke_fill=(0, 0, 0, 230))
|
|
171
|
+
rotulo = "frame %d %.2fs" % (q["frame"], q["tempo"])
|
|
172
|
+
fg = fonte("bold", max(16, H // 34))
|
|
173
|
+
d.rectangle([0, H - H // 20, largura(fg, rotulo) + 24, H], fill=(0, 0, 0, 190))
|
|
174
|
+
d.text((12, H - H // 40), rotulo, font=fg, anchor="lm", fill=(255, 255, 255, 255))
|
|
175
|
+
im.alpha_composite(cam)
|
|
176
|
+
quadros.append(im.convert("RGB"))
|
|
177
|
+
alvo = int(job.get("altura", 640))
|
|
178
|
+
reduzidos = [q.resize((max(1, int(q.width * alvo / q.height)), alvo)) for q in quadros]
|
|
179
|
+
cols = min(len(reduzidos), int(job.get("cols", 4)))
|
|
180
|
+
linhas = (len(reduzidos) + cols - 1) // cols
|
|
181
|
+
cw = max(r.width for r in reduzidos)
|
|
182
|
+
folha = Image.new("RGB", (cw * cols + 8 * (cols - 1), alvo * linhas + 8 * (linhas - 1)), (40, 40, 40))
|
|
183
|
+
for i, r in enumerate(reduzidos):
|
|
184
|
+
folha.paste(r, ((i % cols) * (cw + 8), (i // cols) * (alvo + 8)))
|
|
185
|
+
folha.save(job["out"])
|
|
186
|
+
return {"ok": True, "out": job["out"], "size": [folha.width, folha.height]}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main():
|
|
190
|
+
acao = sys.argv[1] if len(sys.argv) > 1 else "conferir"
|
|
191
|
+
if Image is None:
|
|
192
|
+
print(json.dumps({"ok": False, "error": "o Pillow não está instalado", "comoResolver": "python3 -m pip install pillow"}))
|
|
193
|
+
return 1
|
|
194
|
+
if acao == "conferir":
|
|
195
|
+
print(json.dumps({"ok": True}))
|
|
196
|
+
return 0
|
|
197
|
+
with open(sys.argv[2], encoding="utf-8") as fh:
|
|
198
|
+
job = json.load(fh)
|
|
199
|
+
try:
|
|
200
|
+
r = estados(job) if acao == "estados" else grade(job) if acao == "grade" else {"ok": False, "error": "ação " + acao}
|
|
201
|
+
except Exception as e:
|
|
202
|
+
r = {"ok": False, "error": "%s: %s" % (type(e).__name__, e)}
|
|
203
|
+
print(json.dumps(r))
|
|
204
|
+
return 0 if r.get("ok") else 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main())
|