primocode 10.0.0 → 10.0.9

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 (55) hide show
  1. package/README.md +2 -2
  2. package/bin/primocode.js +69 -1
  3. package/lib/act.js +118 -19
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +1 -1
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/entrada.js +73 -9
  9. package/lib/estudio/cinema.js +27 -9
  10. package/lib/estudio/diretor.js +13 -1
  11. package/lib/estudio/ende.js +16 -5
  12. package/lib/estudio/estilo.js +23 -8
  13. package/lib/estudio/gerativo.js +1453 -0
  14. package/lib/estudio/hibrido.js +165 -0
  15. package/lib/estudio/index.js +244 -36
  16. package/lib/estudio/kit/gerativo.jsx +980 -0
  17. package/lib/estudio/kit/index.jsx +3 -1
  18. package/lib/estudio/kit/tres.jsx +308 -0
  19. package/lib/estudio/kit.js +7 -1
  20. package/lib/estudio/lancamento.js +28 -0
  21. package/lib/estudio/referencias.js +105 -0
  22. package/lib/estudio/remotion.js +53 -9
  23. package/lib/estudio/ritmo.js +2 -2
  24. package/lib/estudio/sons-leves.js +38 -1
  25. package/lib/estudio/variedade.js +105 -0
  26. package/lib/estudio/vox.js +37 -8
  27. package/lib/mcp.js +156 -0
  28. package/lib/personas.js +67 -0
  29. package/lib/skills.js +36 -0
  30. package/lib/tools.js +22 -3
  31. package/lib/ui.js +41 -6
  32. package/lib/video/auto.js +329 -0
  33. package/lib/video/camadas.js +207 -0
  34. package/lib/video/cli.js +1 -1
  35. package/lib/video/filters.js +39 -6
  36. package/lib/video/fx3d/Camadas.jsx +64 -0
  37. package/lib/video/fx3d/Objetos.jsx +128 -0
  38. package/lib/video/fx3d/Root.jsx +18 -0
  39. package/lib/video/fx3d/modelos.jsx +304 -0
  40. package/lib/video/fx3d.js +108 -0
  41. package/lib/video/gerados.js +119 -0
  42. package/lib/video/objetos.js +111 -0
  43. package/lib/video/ops-fx.js +453 -0
  44. package/lib/video/ops.js +30 -5
  45. package/lib/video/paralelo.js +50 -0
  46. package/lib/video/pedido.js +191 -0
  47. package/lib/video/rastrear.js +95 -0
  48. package/lib/video/rastro.js +276 -0
  49. package/lib/video/render.js +67 -14
  50. package/lib/video/texto.js +6 -4
  51. package/lib/video/timeline.js +9 -1
  52. package/package.json +2 -2
  53. package/skills/motion-de-primeira/SKILL.md +5 -4
  54. package/studio/narrar.py +10 -7
  55. package/studio/rastrear.py +399 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env python3
2
+ """rastrear.py — onde está a pessoa, a cabeça e as mãos em CADA quadro.
3
+
4
+ "Tira o fundo e separa em camadas… põe o logo na minha mão… quando eu
5
+ juntar as mãos…" (30/09/2026)
6
+
7
+ Tudo o que o editor faz "seguindo" alguém nasce aqui, numa passada só pelo
8
+ vídeo, e fica guardado na pasta do projeto. Daí em diante as operações são
9
+ conta sobre números (lib/video/rastro.js) — rápidas e testáveis.
10
+
11
+ ── O QUE SAI ────────────────────────────────────────────────────────────────
12
+ · matte-<asset>.mkv a máscara da pessoa, cinza 0–255, um quadro por quadro
13
+ da origem (FFV1: sem perda, e todo quadro é chave, então
14
+ o -ss do ffmpeg cai no quadro exato — a máscara não
15
+ escorrega um quadro em relação ao vídeo);
16
+ · fundo-<asset>.png a "placa limpa": o fundo SEM a pessoa, montado com os
17
+ pixels que a pessoa não cobriu ao longo do vídeo (e o
18
+ buraco que sobra, preenchido por inpaint). É a camada
19
+ de trás do "separa em camadas";
20
+ · rastro-<asset>.json por quadro: o rosto (caixa), o topo da cabeça (da
21
+ máscara, não do rosto — o cabelo conta) e até duas
22
+ mãos (centro da palma, tamanho, abertura, lado).
23
+
24
+ ── POR QUE A MÁSCARA PASSA POR UM FILTRO GUIADO ─────────────────────────────
25
+ O segmentador do MediaPipe pensa em 256×256: ampliada para 1080×1920, a borda
26
+ vira uma rampa larga e borrada — o "adesivo" que denuncia o recorte. O filtro
27
+ guiado (He et al., 2010) usa o PRÓPRIO quadro como guia: a borda da máscara
28
+ gruda na borda de verdade (ombro, cabelo) sem modelo nenhum a mais. A média
29
+ com o quadro anterior tira o tremor de um quadro para o outro.
30
+
31
+ ── EM PARTES, EM PARALELO ───────────────────────────────────────────────────
32
+ Cada processo analisa um trecho fixo de quadros (lib/video/rastrear.js
33
+ reparte e roda vários ao mesmo tempo); o `juntar` costura as partes na
34
+ ordem: as máscaras por concat sem recodificar, os quadros em fila, as somas
35
+ da placa somadas.
36
+
37
+ Uso (pelo uv, como o pessoa.py e o rostos.py — o MediaPipe só existe até o
38
+ Python 3.12):
39
+ python rastrear.py analisar --entrada v.mp4 --pasta <midia> --id a1 [--de 96 --ate 192 --aquecer 12]
40
+ python rastrear.py juntar --pasta <midia> --id a1
41
+ python rastrear.py conferir
42
+ """
43
+
44
+ import argparse
45
+ import json
46
+ import os
47
+ import shutil
48
+ import subprocess
49
+ import sys
50
+ import urllib.request
51
+
52
+ MODELO_URL = ("https://storage.googleapis.com/mediapipe-models/image_segmenter/"
53
+ "selfie_multiclass_256x256/float32/latest/selfie_multiclass_256x256.tflite")
54
+ PONTAS = (4, 8, 12, 16, 20)
55
+ PALMA = (0, 5, 9, 13, 17)
56
+
57
+
58
+ def achar_ffmpeg():
59
+ achado = shutil.which("ffmpeg")
60
+ if achado:
61
+ return achado
62
+ for c in ("/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"):
63
+ if os.path.exists(c):
64
+ return c
65
+ return None
66
+
67
+
68
+ def modelo_multiclasse():
69
+ """O segmentador de 6 classes (fundo, cabelo, pele, roupa…) recorta o
70
+ cabelo bem melhor que o selfie_segmentation. Baixado uma vez; sem rede,
71
+ fica o antigo, que vem dentro do pacote."""
72
+ pasta = os.path.join(os.path.expanduser("~"), ".primocode", "studio", "modelos")
73
+ arq = os.path.join(pasta, "selfie_multiclass_256x256.tflite")
74
+ if os.path.exists(arq) and os.path.getsize(arq) > 100000:
75
+ return arq
76
+ try:
77
+ os.makedirs(pasta, exist_ok=True)
78
+ tmp = arq + ".tmp"
79
+ urllib.request.urlretrieve(MODELO_URL, tmp)
80
+ os.replace(tmp, arq)
81
+ return arq
82
+ except Exception:
83
+ return None
84
+
85
+
86
+ class Segmentador:
87
+ def __init__(self, mp):
88
+ self.mp = mp
89
+ self.tipo = "selfie"
90
+ arq = modelo_multiclasse()
91
+ if arq:
92
+ try:
93
+ v = mp.tasks.vision
94
+ op = v.ImageSegmenterOptions(
95
+ base_options=mp.tasks.BaseOptions(model_asset_path=arq),
96
+ running_mode=v.RunningMode.VIDEO, output_confidence_masks=True,
97
+ output_category_mask=False)
98
+ self.seg = v.ImageSegmenter.create_from_options(op)
99
+ self.tipo = "multiclasse"
100
+ return
101
+ except Exception:
102
+ pass
103
+ self.seg = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=1)
104
+
105
+ def mascara(self, rgb, ms):
106
+ import numpy as np
107
+ if self.tipo == "multiclasse":
108
+ img = self.mp.Image(image_format=self.mp.ImageFormat.SRGB, data=rgb)
109
+ r = self.seg.segment_for_video(img, int(ms))
110
+ return 1.0 - np.asarray(r.confidence_masks[0].numpy_view(), dtype=np.float32)
111
+ return self.seg.process(rgb).segmentation_mask.astype(np.float32)
112
+
113
+ def fechar(self):
114
+ try:
115
+ self.seg.close()
116
+ except Exception:
117
+ pass
118
+
119
+
120
+ def guiado(guia, p, r=8, eps=1e-2, fator=2):
121
+ """Filtro guiado rápido: a/b calculados em meia resolução, aplicados na cheia."""
122
+ import cv2
123
+ h, w = guia.shape
124
+ gi = cv2.resize(guia, (w // fator, h // fator), interpolation=cv2.INTER_AREA)
125
+ pi = cv2.resize(p, (w // fator, h // fator), interpolation=cv2.INTER_AREA)
126
+ rr = max(1, r // fator)
127
+ box = lambda x: cv2.boxFilter(x, -1, (2 * rr + 1, 2 * rr + 1))
128
+ mi, mp_ = box(gi), box(pi)
129
+ cov = box(gi * pi) - mi * mp_
130
+ var = box(gi * gi) - mi * mi
131
+ a = cov / (var + eps)
132
+ b = mp_ - a * mi
133
+ a = cv2.resize(box(a), (w, h), interpolation=cv2.INTER_LINEAR)
134
+ b = cv2.resize(box(b), (w, h), interpolation=cv2.INTER_LINEAR)
135
+ return a * guia + b
136
+
137
+
138
+ def maos_do_quadro(res, w, h, pl=0, pt=0, W=None, H=None):
139
+ """As mãos em coordenadas do quadro ORIGINAL (0–1), descontada a borda."""
140
+ import math
141
+ W = W or w
142
+ H = H or h
143
+ out = []
144
+ if not res.multi_hand_landmarks:
145
+ return out
146
+ pares = list(zip(res.multi_hand_landmarks, res.multi_handedness or [None] * 9))
147
+ pares.sort(key=lambda x: -(x[1].classification[0].score if x[1] else 0))
148
+ for lm, mao in pares[:2]:
149
+ pts = [((p.x * W - pl) / w, (p.y * H - pt) / h) for p in lm.landmark]
150
+ cx = sum(pts[i][0] for i in PALMA) / len(PALMA)
151
+ cy = sum(pts[i][1] for i in PALMA) / len(PALMA)
152
+ # Tamanho da palma em fração da ALTURA (o mesmo para x e y em pixel).
153
+ palma = max(1e-6, math.hypot((pts[9][0] - pts[0][0]) * w, (pts[9][1] - pts[0][1]) * h))
154
+ abre = sum(math.hypot((pts[i][0] - pts[0][0]) * w, (pts[i][1] - pts[0][1]) * h) for i in PONTAS[1:]) / 4 / palma
155
+ rot = mao.classification[0] if mao else None
156
+ out.append([round(cx, 4), round(cy, 4), round(palma / h, 4), round(abre, 3),
157
+ (rot.label[0] if rot else "?"), round(rot.score if rot else 0, 2)])
158
+ return out
159
+
160
+
161
+ def medidas(entrada):
162
+ import cv2
163
+ cap = cv2.VideoCapture(entrada)
164
+ if not cap.isOpened():
165
+ return None
166
+ r = (cap.get(cv2.CAP_PROP_FPS) or 30, int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
167
+ cap.release()
168
+ return r
169
+
170
+
171
+ def quadros_de(ff, entrada, w, h, fps, inicio, quantos):
172
+ """Os quadros [inicio, inicio+quantos) pelo ffmpeg, um a um (BGR).
173
+
174
+ O -ss meio quadro ANTES do primeiro: o ffmpeg decodifica desde o quadro
175
+ chave e descarta o que vem antes do tempo pedido, então o primeiro que
176
+ sai é exatamente o quadro `inicio` — a parte 3 de um rastreio em
177
+ paralelo começa no mesmo quadro em qualquer máquina."""
178
+ import numpy as np
179
+ args = [ff, "-hide_banner", "-loglevel", "error"]
180
+ if inicio > 0:
181
+ args += ["-ss", f"{(inicio - 0.5) / fps:.6f}"]
182
+ args += ["-i", entrada]
183
+ if quantos is not None:
184
+ args += ["-frames:v", str(quantos)]
185
+ args += ["-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "bgr24", "-"]
186
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
187
+ tam = w * h * 3
188
+ try:
189
+ while True:
190
+ b = p.stdout.read(tam)
191
+ if len(b) < tam:
192
+ break
193
+ yield np.frombuffer(b, np.uint8).reshape(h, w, 3)
194
+ finally:
195
+ p.stdout.close()
196
+ p.wait()
197
+
198
+
199
+ def analisar(entrada, pasta, ident, de=0, ate=None, aquecer=0):
200
+ """Analisa os quadros [de, ate) (ate=None: até o fim) e grava a PARTE.
201
+
202
+ `aquecer` quadros antes de `de` passam pelos modelos e não são gravados:
203
+ o rastreador de mão e a média da máscara chegam à emenda já "quentes",
204
+ então a parte começa como se o vídeo tivesse sido lido desde o início.
205
+ As emendas ficam em quadros FIXOS (o Node corta de 96 em 96), não no
206
+ número de processos: o resultado é o mesmo com 1 ou com 12 deles."""
207
+ import cv2
208
+ import numpy as np
209
+ import mediapipe as mp
210
+
211
+ med = medidas(entrada)
212
+ if not med:
213
+ return {"ok": False, "error": f"não consegui abrir {entrada}"}
214
+ fps, w, h = med
215
+ ff = achar_ffmpeg()
216
+ if not ff:
217
+ return {"ok": False, "error": "falta o ffmpeg"}
218
+ dir_partes = os.path.join(pasta, f"partes-{ident}")
219
+ os.makedirs(dir_partes, exist_ok=True)
220
+ base = os.path.join(dir_partes, f"{de:06d}")
221
+ tmp_matte = base + ".tmp.mkv"
222
+ esc = subprocess.Popen([ff, "-hide_banner", "-loglevel", "error", "-y", "-f", "rawvideo", "-pix_fmt", "gray",
223
+ "-s", f"{w}x{h}", "-r", str(fps), "-i", "-", "-c:v", "ffv1", "-level", "3",
224
+ "-g", "1", "-pix_fmt", "gray", tmp_matte], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
225
+
226
+ seg = Segmentador(mp)
227
+ maos = mp.solutions.hands.Hands(static_image_mode=False, max_num_hands=2, model_complexity=1,
228
+ min_detection_confidence=0.35, min_tracking_confidence=0.35)
229
+ rosto = mp.solutions.face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.5)
230
+
231
+ # A placa limpa em meia resolução: soma dos pixels de fundo e quantas vezes cada um foi visto.
232
+ pw, ph = w // 2, h // 2
233
+ soma = np.zeros((ph, pw, 3), np.float64)
234
+ conta = np.zeros((ph, pw), np.float64)
235
+ anterior = None
236
+ quadros = []
237
+ inicio = max(0, de - aquecer)
238
+ quantos = None if ate is None else ate - inicio
239
+ k = inicio
240
+ try:
241
+ for bgr in quadros_de(ff, entrada, w, h, fps, inicio, quantos):
242
+ grava = k >= de
243
+ rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
244
+ m = seg.mascara(rgb, k * 1000.0 / fps)
245
+ if m.shape != (h, w):
246
+ m = cv2.resize(m, (w, h), interpolation=cv2.INTER_LINEAR)
247
+ guia = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
248
+ m = guiado(guia, m, eps=1e-2)
249
+ # A curva: o segmentador deixa ~0,07 no fundo e uma rampa larga no
250
+ # ombro; sem ela o fundo velho aparece como fantasma atrás da pessoa.
251
+ m = np.clip((m - 0.3) / 0.4, 0, 1)
252
+ if anterior is not None:
253
+ m = 0.7 * m + 0.3 * anterior
254
+ anterior = m
255
+ # A mão que sai pela borda de baixo (o normal em quem fala para a
256
+ # câmera) quase nunca é achada: o detector de palma quer a mão
257
+ # inteira. Esticar a borda (replicada) dá a ele onde "terminar" a
258
+ # mão — medido no vídeo de teste: de 119 para 247 quadros com mão.
259
+ pl, pb = int(w * 0.12), int(h * 0.25)
260
+ borda = cv2.copyMakeBorder(rgb, 0, pb, pl, pl, cv2.BORDER_REPLICATE)
261
+ hs = maos_do_quadro(maos.process(borda), w, h, pl, 0, w + 2 * pl, h + pb)
262
+ k += 1
263
+ if not grava:
264
+ continue
265
+ esc.stdin.write((m * 255).astype(np.uint8).tobytes())
266
+
267
+ # Fundo: só o que está LONGE da pessoa (a máscara dilatada), para a borda não sujar a placa.
268
+ peq = cv2.resize(m, (pw, ph), interpolation=cv2.INTER_AREA)
269
+ livre = (cv2.dilate((peq > 0.15).astype(np.uint8), np.ones((15, 15), np.uint8)) == 0)
270
+ soma[livre] += cv2.resize(bgr, (pw, ph), interpolation=cv2.INTER_AREA)[livre]
271
+ conta[livre] += 1
272
+
273
+ q = {}
274
+ rf = rosto.process(rgb)
275
+ if rf.detections:
276
+ d = max(rf.detections, key=lambda x: x.score[0])
277
+ b = d.location_data.relative_bounding_box
278
+ q["f"] = [round(b.xmin, 4), round(b.ymin, 4), round(b.xmin + b.width, 4), round(b.ymin + b.height, 4), round(d.score[0], 2)]
279
+ # O topo da cabeça: a primeira linha com pessoa na faixa do rosto (o cabelo entra).
280
+ x0 = int(max(0, b.xmin + b.width * 0.3) * w)
281
+ x1 = int(min(1, b.xmin + b.width * 0.7) * w)
282
+ y1 = int(min(1, b.ymin + b.height * 0.5) * h)
283
+ if x1 > x0 and y1 > 0:
284
+ faixa = m[:y1, x0:x1].mean(axis=1)
285
+ linhas = np.where(faixa > 0.5)[0]
286
+ if len(linhas):
287
+ q["t"] = round(float(linhas[0]) / h, 4)
288
+ if hs:
289
+ q["h"] = hs
290
+ quadros.append(q)
291
+ finally:
292
+ try:
293
+ esc.stdin.close()
294
+ except Exception:
295
+ pass
296
+ seg.fechar()
297
+ maos.close()
298
+ rosto.close()
299
+ esc.wait()
300
+ if esc.returncode != 0:
301
+ return {"ok": False, "error": "o ffmpeg recusou a máscara: " + (esc.stderr.read() or b"").decode("utf-8", "ignore")[-300:]}
302
+ os.replace(tmp_matte, base + ".mkv")
303
+ np.savez(base + ".npz", soma=soma, conta=conta)
304
+ with open(base + ".json", "w") as f:
305
+ json.dump({"de": de, "fps": fps, "w": w, "h": h, "segmentador": seg.tipo, "quadros": quadros}, f, separators=(",", ":"))
306
+ return {"ok": True, "parte": de, "quadros": len(quadros)}
307
+
308
+
309
+ def juntar(pasta, ident):
310
+ """As partes, em ordem de quadro, viram o rastro, a máscara e a placa."""
311
+ import cv2
312
+ import numpy as np
313
+ dir_partes = os.path.join(pasta, f"partes-{ident}")
314
+ nomes = sorted(n[:-5] for n in os.listdir(dir_partes) if n.endswith(".json"))
315
+ if not nomes:
316
+ return {"ok": False, "error": "nenhuma parte para juntar"}
317
+ quadros, soma, conta, meta = [], None, None, None
318
+ usadas = []
319
+ for nome in nomes:
320
+ with open(os.path.join(dir_partes, nome + ".json")) as f:
321
+ parte = json.load(f)
322
+ # O contêiner (.mov do celular) promete mais quadros do que o vídeo
323
+ # tem: a duração é a do áudio. A parte do fim sai vazia, e a anterior
324
+ # curta — é o fim do vídeo, não um buraco.
325
+ if not parte["quadros"]:
326
+ continue
327
+ if len(quadros) != parte["de"]:
328
+ return {"ok": False, "error": f"parte {nome} começa em {parte['de']}, esperava {len(quadros)}"}
329
+ quadros += parte["quadros"]
330
+ usadas.append(nome)
331
+ meta = meta or parte
332
+ z = np.load(os.path.join(dir_partes, nome + ".npz"))
333
+ soma = z["soma"] if soma is None else soma + z["soma"]
334
+ conta = z["conta"] if conta is None else conta + z["conta"]
335
+ w, h, fps = meta["w"], meta["h"], meta["fps"]
336
+ ff = achar_ffmpeg()
337
+ lista = os.path.join(dir_partes, "lista.txt")
338
+ with open(lista, "w") as f:
339
+ for nome in usadas:
340
+ f.write(f"file '{os.path.join(dir_partes, nome + '.mkv')}'\n")
341
+ matte = os.path.join(pasta, f"matte-{ident}.mkv")
342
+ r = subprocess.run([ff, "-hide_banner", "-loglevel", "error", "-y", "-f", "concat", "-safe", "0", "-i", lista,
343
+ "-c", "copy", matte + ".tmp.mkv"], capture_output=True)
344
+ if r.returncode != 0:
345
+ return {"ok": False, "error": "não juntei as máscaras: " + r.stderr.decode("utf-8", "ignore")[-300:]}
346
+ os.replace(matte + ".tmp.mkv", matte)
347
+
348
+ ph, pw = conta.shape
349
+ placa = np.zeros((ph, pw, 3), np.uint8)
350
+ visto = conta > 0
351
+ placa[visto] = (soma[visto] / conta[visto][:, None]).astype(np.uint8)
352
+ buraco = (~visto).astype(np.uint8) * 255
353
+ if buraco.any():
354
+ placa = cv2.inpaint(placa, buraco, 9, cv2.INPAINT_TELEA)
355
+ fundo = os.path.join(pasta, f"fundo-{ident}.png")
356
+ cv2.imwrite(fundo, cv2.resize(placa, (w, h), interpolation=cv2.INTER_CUBIC))
357
+
358
+ dados = {"versao": 2, "fps": round(fps, 4), "w": w, "h": h, "n": len(quadros), "segmentador": meta["segmentador"],
359
+ "partes": len(nomes), "buraco": round(float((~visto).mean()), 3), "quadros": quadros}
360
+ arq = os.path.join(pasta, f"rastro-{ident}.json")
361
+ with open(arq, "w") as f:
362
+ json.dump(dados, f, separators=(",", ":"))
363
+ shutil.rmtree(dir_partes, ignore_errors=True)
364
+ return {"ok": True, "rastro": arq, "matte": matte, "fundo": fundo, "quadros": len(quadros), "segmentador": meta["segmentador"],
365
+ "comRosto": sum(1 for q in quadros if "f" in q), "comMao": sum(1 for q in quadros if "h" in q)}
366
+
367
+
368
+ def main():
369
+ p = argparse.ArgumentParser(description="Rastreio de pessoa, rosto e mãos (PrimoCode)")
370
+ sub = p.add_subparsers(dest="comando", required=True)
371
+ sub.add_parser("conferir")
372
+ a = sub.add_parser("analisar")
373
+ a.add_argument("--entrada", required=True)
374
+ a.add_argument("--pasta", required=True)
375
+ a.add_argument("--id", required=True)
376
+ a.add_argument("--de", type=int, default=0)
377
+ a.add_argument("--ate", type=int, default=None)
378
+ a.add_argument("--aquecer", type=int, default=0)
379
+ j = sub.add_parser("juntar")
380
+ j.add_argument("--pasta", required=True)
381
+ j.add_argument("--id", required=True)
382
+ args = p.parse_args()
383
+ if args.comando == "conferir":
384
+ try:
385
+ import cv2, mediapipe, numpy # noqa: F401
386
+ print(json.dumps({"ok": True}))
387
+ except Exception as e:
388
+ print(json.dumps({"ok": False, "error": str(e)}))
389
+ return
390
+ if args.comando == "juntar":
391
+ r = juntar(args.pasta, args.id)
392
+ else:
393
+ r = analisar(args.entrada, args.pasta, args.id, args.de, args.ate, args.aquecer)
394
+ print(json.dumps(r, ensure_ascii=False))
395
+ sys.exit(0 if r.get("ok") else 1)
396
+
397
+
398
+ if __name__ == "__main__":
399
+ main()