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.
Files changed (60) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/estilo.js +51 -0
  12. package/lib/estudio/glifos.js +215 -0
  13. package/lib/estudio/index.js +41 -18
  14. package/lib/estudio/kit/base.jsx +285 -0
  15. package/lib/estudio/kit/index.jsx +11 -0
  16. package/lib/estudio/kit/marca.jsx +741 -0
  17. package/lib/estudio/kit/vox.jsx +357 -0
  18. package/lib/estudio/kit.js +72 -0
  19. package/lib/estudio/lancamento.js +413 -0
  20. package/lib/estudio/motion.js +2 -5
  21. package/lib/estudio/plano.js +28 -3
  22. package/lib/estudio/remotion.js +87 -43
  23. package/lib/estudio/ritmo.js +77 -0
  24. package/lib/estudio/vox.js +385 -0
  25. package/lib/fala.js +1 -94
  26. package/lib/pipeline.js +55 -56
  27. package/lib/repeticao.js +3 -1
  28. package/lib/skills.js +8 -2
  29. package/lib/snapshot.js +111 -0
  30. package/lib/tools.js +79 -20
  31. package/lib/ui.js +13 -6
  32. package/lib/video/analise.js +89 -0
  33. package/lib/video/cli.js +74 -0
  34. package/lib/video/delta.js +90 -0
  35. package/lib/video/edits.js +379 -0
  36. package/lib/video/filters.js +247 -0
  37. package/lib/video/ops.js +322 -0
  38. package/lib/video/projeto.js +133 -0
  39. package/lib/video/render.js +221 -0
  40. package/lib/video/texto.js +142 -0
  41. package/lib/video/timeline.js +310 -0
  42. package/lib/video/transcript.js +124 -0
  43. package/lib/voz.js +75 -223
  44. package/package.json +2 -3
  45. package/skills/motion-de-primeira/SKILL.md +3 -4
  46. package/studio/texto.py +208 -0
  47. package/voz/primo_voz/__main__.py +96 -236
  48. package/voz/primo_voz/transcrever.py +31 -1
  49. package/app/ansi.js +0 -55
  50. package/app/app.css +0 -500
  51. package/app/app.js +0 -1017
  52. package/app/icone.svg +0 -6
  53. package/app/index.html +0 -220
  54. package/app/primo-marca.js +0 -192
  55. package/lib/app.js +0 -615
  56. package/lib/icone.js +0 -263
  57. package/lib/instalar-app.js +0 -231
  58. package/lib/painel.js +0 -291
  59. package/voz/primo_voz/desperta.py +0 -195
  60. package/voz/primo_voz/orbe.py +0 -406
@@ -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())