@intelli-ia/easyedit 0.0.0-stage → 0.1.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 (38) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -2
  4. package/bin/easyedit.mjs +6 -0
  5. package/package.json +12 -4
  6. package/payload/.easyedit/Spec.md +82 -0
  7. package/payload/.easyedit/TOOLS.md +111 -0
  8. package/payload/.easyedit/templates/Spec.md +125 -0
  9. package/payload/claude-section.md +5 -0
  10. package/payload/skills/easyedit-build/SKILL.md +78 -0
  11. package/payload/skills/easyedit-destaques/SKILL.md +49 -0
  12. package/payload/skills/easyedit-interviewer/SKILL.md +54 -0
  13. package/payload/skills/easyedit-legendas/SKILL.md +28 -0
  14. package/payload/skills/easyedit-log/SKILL.md +46 -0
  15. package/payload/skills/easyedit-log/scripts/campo.mjs +24 -0
  16. package/payload/skills/easyedit-log/scripts/registrar.mjs +34 -0
  17. package/payload/skills/easyedit-novo/SKILL.md +48 -0
  18. package/payload/skills/easyedit-novo/scripts/novo_video.mjs +43 -0
  19. package/payload/skills/easyedit-progress/SKILL.md +27 -0
  20. package/payload/skills/easyedit-progress/scripts/progress.mjs +39 -0
  21. package/payload/skills/easyedit-ritmo/SKILL.md +42 -0
  22. package/payload/skills/easyedit-ritmo/scripts/pausas.mjs +73 -0
  23. package/payload/skills/easyedit-setup/SKILL.md +31 -0
  24. package/payload/skills/easyedit-split-broll/SKILL.md +37 -0
  25. package/payload/skills/easyedit-takes/SKILL.md +52 -0
  26. package/payload/skills/easyedit-takes/scripts/takes.mjs +73 -0
  27. package/payload/skills/easyedit-vertical-trafego/SKILL.md +55 -0
  28. package/payload/skills/easyedit-vertical-trafego/references/playbook.md +37 -0
  29. package/payload/skills/easyedit-zonas-seguras/SKILL.md +73 -0
  30. package/payload/skills/easyedit-zonas-seguras/references/zonas.md +18 -0
  31. package/payload/skills/easyedit-zonas-seguras/scripts/detectar.swift +37 -0
  32. package/payload/skills/easyedit-zonas-seguras/scripts/verificar_zonas.mjs +128 -0
  33. package/payload/skills/easyedit-zonas-seguras/scripts/zonas.mjs +37 -0
  34. package/src/cli.mjs +45 -0
  35. package/src/delegar.mjs +10 -0
  36. package/src/doctor.mjs +40 -0
  37. package/src/install.mjs +62 -0
  38. package/src/paths.mjs +10 -0
@@ -0,0 +1,37 @@
1
+ # Playbook · Criativo para tráfego pago na Meta
2
+
3
+ Valores marcados com (padrão) são sugestões iniciais do EasyEdit a calibrar com resultado real; não são regra da Meta.
4
+
5
+ ## Estrutura do criativo
6
+ - **Gancho (0–3 s):** a dor, a promessa ou a pergunta já na primeira frase e no primeiro quadro. Nada de logo animada ou apresentação.
7
+ - **Desenvolvimento:** problema → solução → prova, em frases curtas.
8
+ - **CTA:** dito e escrito, com uma ação única (matricule, clique, agende). Quadro final de 1–2 s (padrão) com CTA e marca, dentro da área útil.
9
+ - **Duração: livre.** Não há mínimo, máximo nem meta de tempo: o criativo pode ter 30 s, 90 s, 120 s ou vários minutos. O que se exige é **concisão da ideia**, do gancho ao CTA: cada trecho cumpre uma função no argumento (gancho, problema, solução, prova, CTA). Corte digressão, repetição e trecho que não faz a ideia avançar; mantenha o que sustenta a oferta, por mais longo que fique.
10
+
11
+ ## Ritmo
12
+ - Pausa máxima (padrão): 0,3 s (`$easyedit-ritmo`).
13
+ - Um destaque visual a cada 2–4 s (padrão) (`$easyedit-destaques`).
14
+ - Troca de b-roll a cada 2–4 s. Sem trecho de fala "limpa" por mais de ~5 s.
15
+
16
+ ## Som e legenda
17
+ - Legenda sempre ligada (`$easyedit-legendas`): muita gente assiste sem som.
18
+ - Música baixa (padrão: -18 a -22 dB sob a voz) com ducking (`/hyperframes-audio`); voz sempre inteligível.
19
+ - Música e SFX com licença de uso comercial; sem trilha de tendência do Instagram, que não pode ser licenciada no render.
20
+
21
+ ## Zonas e formatos
22
+ - Zonas cinzentas rigorosas (`$easyedit-zonas-seguras`).
23
+ - Entregue 9:16 (Stories/Reels) e, quando a campanha usar feed, uma variante 4:5 recomposta para a área útil do 4:5.
24
+
25
+ ## Variantes para teste
26
+ - Gere variantes de **gancho** (primeiros 3 s) mantendo o resto igual. Use `--variables` do `render` e nomes `<slug>-hookA-v1.mp4`, `<slug>-hookB-v1.mp4`.
27
+ - Registre cada variante na seção 14 do spec e a diferença em `$easyedit-log`.
28
+
29
+ ## Checklist de conformidade (alerta, não parecer jurídico)
30
+ Confira com o cliente e com as políticas de anúncios vigentes da Meta antes de publicar:
31
+ - promessa de resultado garantido, ganho financeiro ou "cura";
32
+ - antes/depois, saúde, corpo e estética;
33
+ - afirmações sobre atributos pessoais do espectador ("você que sofre de…");
34
+ - depoimentos e provas reais, com autorização de imagem;
35
+ - uso de marcas, pessoas e músicas de terceiros;
36
+ - setor regulado (educação, saúde, finanças): exigências específicas.
37
+ Qualquer item suspeito vira pendência na seção 8 do spec, e a decisão é da pessoa responsável.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: easyedit-zonas-seguras
3
+ description: "Use ao posicionar legendas, textos, ícones, logo, produto, rostos ou CTA num vídeo 9:16 para Reels ou anúncio da Meta, e antes de entregar, para garantir que nada importante fique nas zonas cinzentas (topo, rodapé e laterais cobertos pela interface do Instagram/Facebook). Verifica texto (HyperFrames check), elementos declarados e rostos/objetos detectados automaticamente no render, com quadros anotados. Não escolhe estilo de legenda; para isso use easyedit-legendas."
4
+ ---
5
+
6
+ # Respeitar as zonas cinzentas
7
+
8
+ ## Preparação obrigatória
9
+
10
+ Antes de executar esta skill, carregue `$easyedit-setup`.
11
+
12
+ ## Modo de interação
13
+
14
+ Modo de interação: `sem perguntas`.
15
+
16
+ ## Zonas
17
+
18
+ Topo 14%, rodapé 35% e laterais 6% do canvas 1080x1920. Detalhes e ressalvas em [references/zonas.md](references/zonas.md). Área útil: x 65–1015, y 269–1248.
19
+
20
+ ## Ao construir
21
+
22
+ 1. Posicione legenda, texto, ícone, logo, rosto, produto e CTA **dentro da área útil**. Fundos e b-roll podem sangrar para fora.
23
+ 2. Registre cada logo, produto, CTA e texto posicionado em `elementos.json` (ver abaixo).
24
+ 3. Declare a área útil como variáveis CSS na composição (`--util-x0: 65px; --util-y0: 269px; --util-x1: 1015px; --util-y1: 1248px`) e ancore os elementos a elas, em vez de repetir números soltos.
25
+
26
+ ## Ao validar
27
+
28
+ São **três verificações**, todas automáticas, mais a conferência visual dos quadros que elas apontam.
29
+
30
+ 1. **Texto (DOM)**: os quatro `check --caption-zone`, na pasta do projeto. Cada um usa `severity=error` e amostra cinco instantes. Qualquer `caption_zone_collision` é defeito: reposicione.
31
+
32
+ ```bash
33
+ node ../../../../.claude/skills/easyedit-zonas-seguras/scripts/zonas.mjs comandos
34
+ ```
35
+
36
+ 2. **Elementos declarados (geometria exata)**: ao construir a composição, escreva `elementos.json` na pasta do projeto com **cada logo, produto, CTA, rosto/enquadramento e texto** que você posicionou, na tela, com a caixa em pixels do canvas 1080x1920. Fundos e b-roll que podem sangrar usam `"sangra": true`.
37
+
38
+ ```json
39
+ [
40
+ {"id": "logo-final", "role": "logo", "inicio": 27, "fim": 30, "x": 390, "y": 300, "w": 300, "h": 120},
41
+ {"id": "cta-final", "role": "cta", "inicio": 27, "fim": 30, "x": 140, "y": 900, "w": 800, "h": 200},
42
+ {"id": "produto-1", "role": "produto", "inicio": 8, "fim": 11, "x": 300, "y": 500, "w": 480, "h": 480},
43
+ {"id": "bg-broll", "role": "fundo", "inicio": 0, "fim": 30, "x": 0, "y": 0, "w": 1080, "h": 1920, "sangra": true}
44
+ ]
45
+ ```
46
+
47
+ Mantenha o arquivo em dia a cada mudança de posição.
48
+
49
+ 3. **Pixels do render (detecção automática)**: renderize uma versão `-q draft` e rode o verificador. Ele extrai quadros (2 por segundo), detecta **rostos**, **texto** e **objetos salientes** (candidatos a produto ou logo) com o framework Vision do macOS (local, sem rede) e compara tudo com a área útil. Também confere o `elementos.json`.
50
+
51
+ ```bash
52
+ node .claude/skills/easyedit-zonas-seguras/scripts/verificar_zonas.mjs \
53
+ --video <render.mp4> --elementos <projects/slug>/elementos.json --out <projects/slug>/verificacao-zonas
54
+ ```
55
+
56
+ Saída: `relatorio.json`, quadros anotados em `anotados/` (zonas em vermelho, detecções em amarelo/ciano) e código de saída 2 se houver erro.
57
+
58
+ | Detecção | Gravidade | Como corrigir |
59
+ | --- | --- | --- |
60
+ | Rosto (pixels) fora da área útil | **erro** | Reenquadrar com `object-fit`/foco, reposicionar a faixa ou trocar o b-roll. |
61
+ | Elemento declarado (logo, produto, CTA, texto) fora | **erro** | Mover para dentro da área útil e atualizar o `elementos.json`. |
62
+ | Texto lido em pixels fora (legenda queimada, texto numa imagem) | aviso | Conferir no quadro anotado; texto do próprio filme (placa, bordado no uniforme) pode ser aceitável, texto nosso não. |
63
+ | Objeto saliente fora (possível produto/logo) | aviso | Conferir no quadro anotado; se for produto/logo/assunto principal, mover. |
64
+
65
+ Corrija os **erros** e reexecute até zerar. Avalie cada **aviso** nos quadros anotados e registre a decisão (corrigido ou aceito, e por quê) na seção 11 do spec. Rostos muito pequenos ao fundo (< 4% da largura) são ignorados.
66
+
67
+ 4. Registre os resultados na seção 11 (comando, nº de erros e avisos, data) e as correções com `$easyedit-log`.
68
+
69
+ ## Limites
70
+
71
+ - A detecção é aproximada: o Vision acha rostos de frente e de perfil, mas pode errar em rostos muito pequenos, cobertos ou desenhados. Produto e logo em pixels são só **candidatos** (objeto saliente), por isso o `elementos.json` é a verificação exata. Mantenha-o completo.
72
+ - Requer macOS com `swiftc` (Xcode Command Line Tools) e `ffmpeg`. Na primeira execução, o detector é compilado em `~/.cache/easyedit/detectar`.
73
+ - Não altere os valores de `ZONAS` sem registrar a mudança e a fonte.
@@ -0,0 +1,18 @@
1
+ # Zonas cinzentas (9:16, 1080x1920)
2
+
3
+ Regiões onde a interface do Instagram/Facebook cobre o vídeo. Fonte única dos valores: `scripts/zonas.mjs` (`ZONAS`).
4
+
5
+ | Região | Fração | Pixels | O que a plataforma mostra |
6
+ | --- | --- | --- | --- |
7
+ | Topo | 14% | ≈ 269 px | Perfil, nome da conta, rótulo "Patrocinado". |
8
+ | Rodapé | 35% | ≈ 672 px | Botão de CTA, legenda do post, curtir/comentar/compartilhar, áudio. |
9
+ | Laterais | 6% | ≈ 65 px | Margem de respiro e ícones laterais. |
10
+ | **Área útil** | | x 65–1015 · y 269–1248 | Tudo que importa (legenda, texto, rosto, produto, logo, CTA) fica aqui. |
11
+
12
+ **Origem e cautela:** valores consolidados de guias de terceiros (AdNabu, FirstPier, Adspirer e outros, consultados em 2026-10-14), não da documentação oficial da Meta. As fontes divergem no rodapé (20% em Stories, até 35% em Reels) e só uma cita as laterais. Adotamos o mais conservador para Reels e anúncios. Valide no preview do Gerenciador de Anúncios e ajuste `ZONAS` se a Meta divergir.
13
+
14
+ **Regras de uso**
15
+ - Fundo, b-roll e vídeo base **podem** ocupar o quadro inteiro. A regra vale para texto, **rostos**, produto, logo e CTA, inclusive os que aparecem *dentro* do b-roll ou do vídeo base.
16
+ - A legenda fica dentro da área útil, preferencialmente no terço inferior dela (y ≈ 900–1200 px).
17
+ - Em tela dividida, a linha de divisão e os dois rostos/assuntos principais ficam na área útil.
18
+ - Para a variante 4:5 de feed, recalcule: o feed só corta o 9:16 em proporção, então use o centro 4:5 como área útil (1080x1350 no meio do quadro) e reposicione.
@@ -0,0 +1,37 @@
1
+ // Detecta rostos, texto e objetos salientes em imagens com o framework Vision (macOS, local, sem rede).
2
+ // uso: detectar <imagem1> [<imagem2> ...] → JSON em stdout
3
+ // Caixas normalizadas (0–1) com origem no canto superior esquerdo.
4
+ import Foundation
5
+ import Vision
6
+ import ImageIO
7
+
8
+ struct Det: Codable { let tipo: String; let x: Double; let y: Double; let w: Double; let h: Double; let conf: Double; let texto: String? }
9
+ struct Res: Codable { let arquivo: String; let deteccoes: [Det] }
10
+
11
+ func caixa(_ r: CGRect, _ tipo: String, _ conf: Float, _ texto: String? = nil) -> Det {
12
+ Det(tipo: tipo, x: Double(r.minX), y: Double(1 - r.maxY), w: Double(r.width), h: Double(r.height), conf: Double(conf), texto: texto)
13
+ }
14
+
15
+ var saida: [Res] = []
16
+ for caminho in CommandLine.arguments.dropFirst() {
17
+ let url = URL(fileURLWithPath: caminho)
18
+ let handler = VNImageRequestHandler(url: url, options: [:])
19
+ let rostos = VNDetectFaceRectanglesRequest()
20
+ let texto = VNRecognizeTextRequest()
21
+ texto.recognitionLevel = .fast
22
+ texto.usesLanguageCorrection = false
23
+ let saliente = VNGenerateObjectnessBasedSaliencyImageRequest()
24
+ var dets: [Det] = []
25
+ do {
26
+ try handler.perform([rostos, texto, saliente])
27
+ for o in rostos.results ?? [] { dets.append(caixa(o.boundingBox, "rosto", o.confidence)) }
28
+ for o in texto.results ?? [] { dets.append(caixa(o.boundingBox, "texto", o.confidence, o.topCandidates(1).first?.string)) }
29
+ for o in saliente.results?.first?.salientObjects ?? [] { dets.append(caixa(o.boundingBox, "objeto", o.confidence)) }
30
+ } catch {
31
+ FileHandle.standardError.write(Data("falha em \(caminho): \(error)\n".utf8))
32
+ }
33
+ saida.append(Res(arquivo: caminho, deteccoes: dets))
34
+ }
35
+ let enc = JSONEncoder()
36
+ enc.outputFormatting = [.sortedKeys]
37
+ print(String(data: try! enc.encode(saida), encoding: .utf8)!)
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ // Verifica se rostos, texto, logo, produto e CTA ficam dentro da área útil (fora das zonas cinzentas).
3
+ // node verificar_zonas.mjs --video <render.mp4> [--elementos <elementos.json>] [--fps 2] [--out <dir>] [--tol 0.05]
4
+ // Camadas:
5
+ // 1. Pixels (Vision, macOS): rosto → erro; texto lido e objeto saliente (candidato a produto/logo) → aviso
6
+ // (texto nosso é medido com exatidão pelo `check --caption-zone`; OCR em pixel pega texto dentro de imagem/vídeo).
7
+ // 2. Declarada (elementos.json): logo/produto/cta/rosto/texto com caixa em px → erro. Geometria exata.
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { UTIL, ZONAS } from './zonas.mjs';
14
+
15
+ const aqui = dirname(fileURLToPath(import.meta.url));
16
+ const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => (x.startsWith('--') ? [...a, [x.slice(2), all[i + 1]]] : a), []));
17
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
18
+ if (!args.video && !args.elementos) fail('uso: --video <render.mp4> [--elementos <elementos.json>] [--fps 2] [--out <dir>] [--tol 0.05]');
19
+
20
+ const fps = Number(args.fps ?? 2);
21
+ const tol = Number(args.tol ?? 0.05);
22
+ const out = resolve(args.out ?? 'verificacao-zonas');
23
+ const W = 1080, H = 1920;
24
+ const r3 = (n) => Math.round(n * 1000) / 1000;
25
+ mkdirSync(out, { recursive: true });
26
+
27
+ /** Fração da caixa (frações do canvas) que está fora da área útil. */
28
+ const foraDaUtil = ({ x, y, w, h }) => {
29
+ const area = w * h;
30
+ if (area <= 0) return 0;
31
+ const iw = Math.max(0, Math.min(x + w, UTIL.x1) - Math.max(x, UTIL.x0));
32
+ const ih = Math.max(0, Math.min(y + h, UTIL.y1) - Math.max(y, UTIL.y0));
33
+ return 1 - (iw * ih) / area;
34
+ };
35
+ const regioes = ({ x, y, w, h }) => {
36
+ const r = [];
37
+ if (y < UTIL.y0) r.push('topo');
38
+ if (y + h > UTIL.y1) r.push('rodapé');
39
+ if (x < UTIL.x0) r.push('lateral esquerda');
40
+ if (x + w > UTIL.x1) r.push('lateral direita');
41
+ return r.join(' + ');
42
+ };
43
+
44
+ const achados = [];
45
+ const info = [];
46
+
47
+ // ---- camada 2: elementos declarados
48
+ if (args.elementos) {
49
+ const els = JSON.parse(readFileSync(args.elementos, 'utf8'));
50
+ for (const e of els) {
51
+ if (e.sangra) continue; // fundo/b-roll que pode ocupar o quadro todo
52
+ const caixa = { x: e.x / W, y: e.y / H, w: e.w / W, h: e.h / H };
53
+ const f = foraDaUtil(caixa);
54
+ if (f > 0.001) achados.push({ camada: 'declarado', gravidade: 'erro', tipo: e.role, id: e.id ?? null, tempo: `${e.inicio ?? 0}–${e.fim ?? '?'} s`, fora: r3(f), regiao: regioes(caixa), caixa });
55
+ }
56
+ info.push(`${els.length} elementos declarados conferidos`);
57
+ }
58
+
59
+ // ---- camada 1: pixels
60
+ if (args.video) {
61
+ if (process.platform !== 'darwin') fail('a detecção por pixels usa o framework Vision (macOS)');
62
+ const bin = join(homedir(), '.cache/easyedit/detectar');
63
+ const fonte = join(aqui, 'detectar.swift');
64
+ if (!existsSync(bin) || statSync(bin).mtimeMs < statSync(fonte).mtimeMs) {
65
+ mkdirSync(dirname(bin), { recursive: true });
66
+ console.error('compilando detector (uma vez)…');
67
+ // O SDK padrão pode ser mais novo que o compilador; tenta os SDKs instalados do mais novo ao mais antigo.
68
+ const sdks = ['/Library/Developer/CommandLineTools/SDKs', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs']
69
+ .filter(existsSync).flatMap((d) => readdirSync(d).filter((s) => /^MacOSX\d+(\.\d+)?\.sdk$/.test(s)).map((s) => join(d, s)))
70
+ .sort((a, b) => b.localeCompare(a, 'en', { numeric: true }));
71
+ let ok = false;
72
+ for (const sdk of [null, ...sdks]) {
73
+ try { execFileSync('swiftc', ['-O', ...(sdk ? ['-sdk', sdk] : []), fonte, '-o', bin], { stdio: 'ignore' }); ok = true; break; } catch { /* próximo SDK */ }
74
+ }
75
+ if (!ok) fail('não foi possível compilar o detector (swiftc + SDK do macOS)');
76
+ }
77
+ const frames = join(out, 'frames');
78
+ rmSync(frames, { recursive: true, force: true });
79
+ mkdirSync(frames, { recursive: true });
80
+ execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', args.video, '-vf', `fps=${fps},scale=${W}:${H}`, '-q:v', '3', join(frames, 'f%04d.jpg')]);
81
+ const arquivos = readdirSync(frames).filter((f) => f.endsWith('.jpg')).sort().map((f) => join(frames, f));
82
+ const res = [];
83
+ for (let i = 0; i < arquivos.length; i += 40) {
84
+ res.push(...JSON.parse(execFileSync(bin, arquivos.slice(i, i + 40), { maxBuffer: 1 << 28 }).toString()));
85
+ }
86
+ res.forEach((fr, i) => {
87
+ const t = r3(i / fps);
88
+ for (const d of fr.deteccoes) {
89
+ const caixa = { x: d.x, y: d.y, w: d.w, h: d.h };
90
+ if (d.tipo === 'rosto' && d.w < 0.04) continue; // rosto minúsculo ao fundo
91
+ const f = foraDaUtil(caixa);
92
+ if (f <= tol) continue;
93
+ const grande = d.w * d.h > 0.6; // objeto saliente do quadro inteiro: ruído
94
+ if (d.tipo === 'objeto' && grande) continue;
95
+ achados.push({
96
+ camada: 'pixels', gravidade: d.tipo === 'objeto' || d.tipo === 'texto' ? 'aviso' : 'erro', conf: r3(d.conf),
97
+ tipo: d.tipo === 'objeto' ? 'objeto (possível produto/logo)' : d.tipo, tempo: `${t} s`,
98
+ fora: r3(f), regiao: regioes(caixa), caixa, texto: d.texto ?? undefined, frame: fr.arquivo,
99
+ });
100
+ }
101
+ });
102
+ info.push(`${arquivos.length} frames analisados a ${fps} fps`);
103
+ }
104
+
105
+ writeFileSync(join(out, 'relatorio.json'), JSON.stringify({ zonas: ZONAS, tolerancia: tol, info, achados }, null, 2));
106
+ const erros = achados.filter((a) => a.gravidade === 'erro');
107
+
108
+ // quadros anotados: zonas em vermelho, detecções em amarelo (erro) ou ciano (aviso)
109
+ const porFrame = Map.groupBy(achados.filter((a) => a.frame), (a) => a.frame);
110
+ if (porFrame.size) mkdirSync(join(out, 'anotados'), { recursive: true });
111
+ for (const [frame, lista] of porFrame) {
112
+ const caixa = (x, y, w, h, cor, fill) => `drawbox=x=iw*${r3(x)}:y=ih*${r3(y)}:w=iw*${r3(w)}:h=ih*${r3(h)}:color=${cor}:t=${fill ? 'fill' : 6}`;
113
+ const zonasF = [caixa(0, 0, 1, UTIL.y0, 'red@0.3', 1), caixa(0, UTIL.y1, 1, 1 - UTIL.y1, 'red@0.3', 1), caixa(0, UTIL.y0, UTIL.x0, UTIL.y1 - UTIL.y0, 'red@0.3', 1), caixa(UTIL.x1, UTIL.y0, 1 - UTIL.x1, UTIL.y1 - UTIL.y0, 'red@0.3', 1)];
114
+ const dets = lista.map((a) => caixa(a.caixa.x, a.caixa.y, a.caixa.w, a.caixa.h, a.gravidade === 'erro' ? 'yellow' : 'cyan', 0));
115
+ try { execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', frame, '-vf', [...zonasF, ...dets].join(','), join(out, 'anotados', `${frame.split('/').pop().replace('.jpg', '.png')}`)]); } catch { /* anotação é opcional */ }
116
+ }
117
+
118
+ console.log(`${info.join(' · ')}`);
119
+ console.log(`erros: ${erros.length} · avisos: ${achados.length - erros.length}`);
120
+ const grupos = Map.groupBy(achados, (a) => `${a.gravidade}|${a.tipo}|${a.regiao}`);
121
+ for (const [chave, lista] of grupos) {
122
+ const [grav, tipo, regiao] = chave.split('|');
123
+ const tempos = [...new Set(lista.map((a) => a.tempo))];
124
+ const textos = [...new Set(lista.map((a) => a.texto).filter(Boolean))].slice(0, 3);
125
+ console.log(`${grav.toUpperCase().padEnd(5)} ${tipo} → ${regiao} · ${lista.length}x · ${tempos.slice(0, 8).join(', ')}${tempos.length > 8 ? ', …' : ''}${textos.length ? ` · "${textos.join('", "')}"` : ''}`);
126
+ }
127
+ console.log(`relatório: ${join(out, 'relatorio.json')}${porFrame.size ? ` · quadros anotados: ${join(out, 'anotados')}` : ''}`);
128
+ process.exit(erros.length ? 2 : 0);
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // Fonte única das zonas cinzentas (frações do canvas 9:16) e utilitários.
3
+ // node zonas.mjs comandos → imprime os 4 comandos `hyperframes check --caption-zone`
4
+ // node zonas.mjs mascara <in.png> <out.png> → desenha as zonas sobre um snapshot (precisa de ffmpeg)
5
+ import { execFileSync } from 'node:child_process';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ export const ZONAS = { topo: 0.14, rodape: 0.35, laterais: 0.06 };
9
+ const principal = import.meta.url === pathToFileURL(process.argv[1]).href;
10
+ const [cmd, a, b] = principal ? process.argv.slice(2) : [];
11
+ const { topo, rodape, laterais } = ZONAS;
12
+
13
+ /** Retângulo da área útil em frações do canvas. */
14
+ export const UTIL = { x0: laterais, y0: topo, x1: 1 - laterais, y1: 1 - rodape };
15
+
16
+ const bandas = {
17
+ topo: `x0=0;y0=0;x1=1;y1=${topo}`,
18
+ rodape: `x0=0;y0=${1 - rodape};x1=1;y1=1`,
19
+ esquerda: `x0=0;y0=${topo};x1=${laterais};y1=${1 - rodape}`,
20
+ direita: `x0=${1 - laterais};y0=${topo};x1=1;y1=${1 - rodape}`,
21
+ };
22
+
23
+ if (cmd === 'comandos') {
24
+ for (const [nome, g] of Object.entries(bandas)) console.log(`npx hyperframes check --caption-zone "${g};severity=error;seek=.1,.3,.5,.7,.9" # ${nome}`);
25
+ console.log(`# área útil (9:16 1080x1920): x ${Math.round(1080 * laterais)}–${Math.round(1080 * (1 - laterais))}, y ${Math.round(1920 * topo)}–${Math.round(1920 * (1 - rodape))}`);
26
+ } else if (cmd === 'mascara' && a && b) {
27
+ const caixa = (x, y, w, h) => `drawbox=x=iw*${x}:y=ih*${y}:w=iw*${w}:h=ih*${h}:color=red@0.35:t=fill`;
28
+ const f = [
29
+ caixa(0, 0, 1, topo), caixa(0, 1 - rodape, 1, rodape),
30
+ caixa(0, topo, laterais, 1 - topo - rodape), caixa(1 - laterais, topo, laterais, 1 - topo - rodape),
31
+ ].join(',');
32
+ execFileSync('ffmpeg', ['-y', '-loglevel', 'error', '-i', a, '-vf', f, b]);
33
+ console.log(b);
34
+ } else if (principal) {
35
+ console.error('uso: zonas.mjs comandos | zonas.mjs mascara <in.png> <out.png>');
36
+ process.exit(1);
37
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,45 @@
1
+ import { install } from "./install.mjs";
2
+ import { doctor } from "./doctor.mjs";
3
+ import { delegar } from "./delegar.mjs";
4
+ import { versaoPacote } from "./paths.mjs";
5
+
6
+ const AJUDA = `easyedit ${versaoPacote()}
7
+
8
+ Uso: easyedit <comando> [opções]
9
+
10
+ install [--project <dir>] Instala skills, contrato e templates no projeto (padrão: pasta atual).
11
+ update [--project <dir>] Atualiza skills e contrato; preserva specs e o TOOLS.md local.
12
+ doctor [--project <dir>] Confere Node, FFmpeg, HyperFrames, whisper e a instalação.
13
+ novo [args] Atalho para o script de /easyedit-novo (--cliente, --title, --vertical).
14
+ progress [args] Atalho para o script de /easyedit-progress.
15
+ --version Mostra a versão do pacote.
16
+
17
+ Opções de install/update: --dry-run (só mostra o que faria), --agents (copia também para .agents/skills).`;
18
+
19
+ export async function main(argv) {
20
+ const [cmd, ...resto] = argv;
21
+ const opts = lerOpcoes(resto);
22
+ switch (cmd) {
23
+ case "install": return install({ ...opts, modo: "install" });
24
+ case "update": return install({ ...opts, modo: "update" });
25
+ case "doctor": return doctor(opts);
26
+ case "novo": return delegar("easyedit-novo", "novo_video.mjs", resto);
27
+ case "progress": return delegar("easyedit-progress", "progress.mjs", resto);
28
+ case "--version": case "-v": console.log(versaoPacote()); return;
29
+ case undefined: case "help": case "--help": case "-h": console.log(AJUDA); return;
30
+ default:
31
+ console.error(`Comando desconhecido: ${cmd}\n`);
32
+ console.log(AJUDA);
33
+ process.exitCode = 1;
34
+ }
35
+ }
36
+
37
+ function lerOpcoes(args) {
38
+ const o = { project: process.cwd(), dryRun: false, agents: false };
39
+ for (let i = 0; i < args.length; i++) {
40
+ if (args[i] === "--project") o.project = args[++i];
41
+ else if (args[i] === "--dry-run") o.dryRun = true;
42
+ else if (args[i] === "--agents") o.agents = true;
43
+ }
44
+ return o;
45
+ }
@@ -0,0 +1,10 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ export function delegar(skill, script, args) {
6
+ const p = join(process.cwd(), ".claude", "skills", skill, "scripts", script);
7
+ if (!existsSync(p)) throw new Error(`Skill ${skill} não encontrada em ${process.cwd()}. Rode \`easyedit install\` primeiro.`);
8
+ const r = spawnSync(process.execPath, [p, ...args], { stdio: "inherit" });
9
+ process.exitCode = r.status ?? 1;
10
+ }
package/src/doctor.mjs ADDED
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { versaoPacote } from "./paths.mjs";
5
+
6
+ function cmd(c, args) {
7
+ const r = spawnSync(c, args, { encoding: "utf8" });
8
+ return r.error || r.status !== 0 ? null : (r.stdout || r.stderr).trim().split("\n")[0];
9
+ }
10
+
11
+ export function doctor({ project }) {
12
+ const alvo = resolve(project);
13
+ const linhas = [];
14
+ let falhas = 0;
15
+ const item = (ok, nome, detalhe, obrigatorio = true) => {
16
+ if (!ok && obrigatorio) falhas++;
17
+ linhas.push(`${ok ? "✔" : obrigatorio ? "✘" : "–"} ${nome}${detalhe ? `: ${detalhe}` : ""}`);
18
+ };
19
+
20
+ const nodeMaior = Number(process.versions.node.split(".")[0]);
21
+ item(nodeMaior >= 22, "Node ≥ 22", process.versions.node);
22
+ const ff = cmd("ffmpeg", ["-version"]);
23
+ item(!!ff, "FFmpeg", ff ?? "instale (ex.: brew install ffmpeg)");
24
+ item(!!cmd("ffprobe", ["-version"]), "FFprobe");
25
+ const hf = cmd("npx", ["--no-install", "hyperframes", "--version"]);
26
+ item(!!hf, "HyperFrames", hf ?? "instale no projeto (npm i -D hyperframes)");
27
+ item(!!cmd("whisper-cli", ["--help"]) || existsSync("/opt/homebrew/bin/whisper-cli"), "whisper-cli (transcrição)", "", false);
28
+ item(process.platform === "darwin" && !!cmd("swiftc", ["--version"]), "swiftc (verificador de zonas seguras, só macOS)", "", false);
29
+
30
+ const v = join(alvo, ".easyedit", "version");
31
+ const inst = existsSync(v) ? readFileSync(v, "utf8").trim() : null;
32
+ item(!!inst, "EasyEdit instalado", inst ? `v${inst}${inst !== versaoPacote() ? ` (pacote é v${versaoPacote()}; rode update)` : ""}` : "rode `easyedit install`");
33
+ const sk = join(alvo, ".claude", "skills");
34
+ const n = existsSync(sk) ? readdirSync(sk).filter((s) => s.startsWith("easyedit-")).length : 0;
35
+ item(n > 0, "Skills easyedit-*", `${n} encontradas`);
36
+ item(existsSync(join(alvo, "clientes")), "Pasta clientes/", "", false);
37
+
38
+ console.log(linhas.join("\n"));
39
+ if (falhas) { console.log(`\n${falhas} item(ns) obrigatório(s) com problema.`); process.exitCode = 1; }
40
+ }
@@ -0,0 +1,62 @@
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { PAYLOAD, versaoPacote } from "./paths.mjs";
4
+
5
+ const INI = "<!-- easyedit:begin -->";
6
+ const FIM = "<!-- easyedit:end -->";
7
+
8
+ export function install({ project, modo, dryRun, agents }) {
9
+ const alvo = resolve(project);
10
+ if (!existsSync(alvo) || !statSync(alvo).isDirectory()) throw new Error(`Pasta não encontrada: ${alvo}`);
11
+ const fx = join(alvo, ".easyedit");
12
+ const versaoAntiga = existsSync(join(fx, "version")) ? readFileSync(join(fx, "version"), "utf8").trim() : null;
13
+ if (modo === "update" && !versaoAntiga) throw new Error("EasyEdit não está instalado aqui. Rode `easyedit install`.");
14
+
15
+ const acoes = [];
16
+ const fazer = (descricao, fn) => { acoes.push(descricao); if (!dryRun) fn(); };
17
+
18
+ // skills: substitui apenas easyedit-* (não toca em skills de outros)
19
+ const destinos = [join(alvo, ".claude", "skills")];
20
+ if (agents) destinos.push(join(alvo, ".agents", "skills"));
21
+ const skills = readdirSync(join(PAYLOAD, "skills")).filter((n) => n.startsWith("easyedit-"));
22
+ for (const d of destinos) {
23
+ for (const s of skills) {
24
+ fazer(`skill ${s} → ${d}`, () => {
25
+ rmSync(join(d, s), { recursive: true, force: true });
26
+ mkdirSync(d, { recursive: true });
27
+ cpSync(join(PAYLOAD, "skills", s), join(d, s), { recursive: true });
28
+ });
29
+ }
30
+ }
31
+
32
+ // contrato e template: sempre atualizados
33
+ fazer(".easyedit/Spec.md e templates/", () => {
34
+ mkdirSync(join(fx, "templates"), { recursive: true });
35
+ cpSync(join(PAYLOAD, ".easyedit", "Spec.md"), join(fx, "Spec.md"));
36
+ cpSync(join(PAYLOAD, ".easyedit", "templates"), join(fx, "templates"), { recursive: true });
37
+ });
38
+
39
+ // TOOLS.md descreve a máquina: só cria se não existir
40
+ if (!existsSync(join(fx, "TOOLS.md"))) {
41
+ fazer(".easyedit/TOOLS.md (novo)", () => cpSync(join(PAYLOAD, ".easyedit", "TOOLS.md"), join(fx, "TOOLS.md")));
42
+ } else acoes.push(".easyedit/TOOLS.md preservado");
43
+
44
+ fazer(".easyedit/version", () => writeFileSync(join(fx, "version"), versaoPacote() + "\n"));
45
+ fazer("CLAUDE.md (seção EasyEdit)", () => escreverSecao(join(alvo, "CLAUDE.md")));
46
+
47
+ console.log(`${dryRun ? "[dry-run] " : ""}easyedit ${modo}: ${versaoAntiga ?? "nova instalação"} → ${versaoPacote()}`);
48
+ for (const a of acoes) console.log(` · ${a}`);
49
+ if (!dryRun) console.log("\nAbra uma sessão nova do agente nesta pasta para carregar as skills. Depois rode `easyedit doctor`.");
50
+ }
51
+
52
+ function escreverSecao(arquivo) {
53
+ const secao = `${INI}\n${readFileSync(join(PAYLOAD, "claude-section.md"), "utf8").trim()}\n${FIM}\n`;
54
+ if (!existsSync(arquivo)) return writeFileSync(arquivo, secao);
55
+ const atual = readFileSync(arquivo, "utf8");
56
+ const i = atual.indexOf(INI), f = atual.indexOf(FIM);
57
+ if (i !== -1 && f > i) return writeFileSync(arquivo, atual.slice(0, i) + secao + atual.slice(f + FIM.length).replace(/^\n/, ""));
58
+ // seção antiga sem marcadores (instalação manual): substitui o bloco "## EasyEdit"
59
+ const m = atual.match(/^## EasyEdit[\s\S]*?(?=^## |\s*$(?![\s\S]))/m);
60
+ if (m) return writeFileSync(arquivo, atual.replace(m[0], secao + "\n"));
61
+ writeFileSync(arquivo, atual.replace(/\s*$/, "\n\n") + secao);
62
+ }
package/src/paths.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+
5
+ export const RAIZ_PACOTE = resolve(dirname(fileURLToPath(import.meta.url)), "..");
6
+ export const PAYLOAD = join(RAIZ_PACOTE, "payload");
7
+
8
+ export function versaoPacote() {
9
+ return JSON.parse(readFileSync(join(RAIZ_PACOTE, "package.json"), "utf8")).version;
10
+ }