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