@intelli-ia/easyedit 0.0.0-stage → 0.2.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 +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +25 -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 +113 -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 +159 -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 +56 -0
  37. package/src/install.mjs +62 -0
  38. package/src/paths.mjs +10 -0
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: easyedit-log
3
+ description: "Use para registrar no spec.md do vídeo o que foi adicionado, ajustado, alterado ou removido (Registro de mudanças), ou para alterar Status e gates do cabeçalho. Chamada por easyedit-build e easyedit-interviewer a cada mudança; use também quando o usuário disser 'anota que mudamos X'. Não edita a composição nem o render."
4
+ ---
5
+
6
+ # Registrar mudanças e estado
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`. Registre apenas fatos já ocorridos ou confirmados na conversa.
15
+
16
+ ## Registrar uma mudança
17
+
18
+ ```bash
19
+ node .claude/skills/easyedit-log/scripts/registrar.mjs \
20
+ --spec <spec.md> --tipo <adicionado|ajustado|alterado|removido> \
21
+ --o-que "<o que mudou, em uma frase>" --motivo "<por quê>" \
22
+ [--origem interno|cliente] [--afeta "RV-002, T-004"]
23
+ ```
24
+
25
+ O script gera o próximo `CH-xxx`, acrescenta a linha na seção 12 e atualiza `Atualizado em`. Imprime o ID.
26
+
27
+ - `adicionado`: algo novo entrou (cena, música, legenda).
28
+ - `ajustado`: refinamento sem mudar a intenção (timing, cor, volume).
29
+ - `alterado`: troca de decisão (outro estilo, outra música).
30
+ - `removido`: algo saiu. Diga o que e por quê.
31
+
32
+ Se a mudança altera um `RV` ou `AC`, edite também o Ato I e cite os IDs em `--afeta`.
33
+
34
+ ## Alterar estado
35
+
36
+ ```bash
37
+ node .claude/skills/easyedit-log/scripts/campo.mjs <spec.md> "<Campo>" "<valor>"
38
+ ```
39
+
40
+ Campos validados: `Status` (Draft, Defined, In Progress, Review, Completed), `Definition Gate` e `Delivery Gate` (Pending, Passed).
41
+
42
+ ## Limites
43
+
44
+ - Uma linha por mudança, curta e específica. Não agrupe mudanças sem relação.
45
+ - Não apague nem reescreva registros antigos; corrija com um novo `CH`.
46
+ - Não use esta skill para aprovar um gate sem a confirmação correspondente.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Altera um campo da tabela de cabeçalho do spec.md (Status, Definition Gate, Delivery Gate…).
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+
5
+ const [spec, campo, ...resto] = process.argv.slice(2);
6
+ const valor = resto.join(' ');
7
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
8
+ if (!spec || !campo || !valor) fail('uso: campo.mjs <spec.md> "<Campo>" "<valor>"');
9
+
10
+ const PERMITIDOS = {
11
+ Status: ['Draft', 'Defined', 'In Progress', 'Review', 'Completed'],
12
+ 'Definition Gate': ['Pending', 'Passed'],
13
+ 'Delivery Gate': ['Pending', 'Passed'],
14
+ Vertical: ['trafego-meta', 'reels', 'a definir'],
15
+ };
16
+ if (PERMITIDOS[campo] && !PERMITIDOS[campo].includes(valor)) fail(`${campo} aceita: ${PERMITIDOS[campo].join(' | ')}`);
17
+
18
+ const hoje = new Date().toISOString().slice(0, 10);
19
+ const re = new RegExp(`^(\\| ${campo} \\| )[^|]*( \\|)$`, 'm');
20
+ let texto = readFileSync(spec, 'utf8');
21
+ if (!re.test(texto)) fail(`campo não encontrado no cabeçalho: ${campo}`);
22
+ texto = texto.replace(re, `$1${valor}$2`).replace(/^(\| Atualizado em \| )[^|]*( \|)$/m, `$1${hoje}$2`);
23
+ writeFileSync(spec, texto);
24
+ console.log(`${campo}: ${valor}`);
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // Acrescenta uma linha ao Registro de mudanças (seção 12) de um spec.md e atualiza "Atualizado em".
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+
5
+ const args = Object.fromEntries(
6
+ process.argv.slice(2).reduce((acc, a, i, all) => {
7
+ if (a.startsWith('--')) acc.push([a.slice(2), all[i + 1]]);
8
+ return acc;
9
+ }, []),
10
+ );
11
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
12
+ const TIPOS = ['adicionado', 'ajustado', 'alterado', 'removido'];
13
+ const ORIGENS = ['interno', 'cliente'];
14
+
15
+ const { spec, tipo, quem = 'interno', afeta = '-' } = args;
16
+ const oque = args['o-que'], motivo = args.motivo;
17
+ if (!spec || !tipo || !oque || !motivo) fail('uso: --spec <spec.md> --tipo <adicionado|ajustado|alterado|removido> --o-que "…" --motivo "…" [--origem interno|cliente] [--afeta "RV-001, T-002"]');
18
+ if (!TIPOS.includes(tipo)) fail(`tipo inválido: ${TIPOS.join(' | ')}`);
19
+ const origem = args.origem ?? 'interno';
20
+ if (!ORIGENS.includes(origem)) fail(`origem inválida: ${ORIGENS.join(' | ')}`);
21
+
22
+ let texto = readFileSync(spec, 'utf8');
23
+ const fim = '<!-- /easyedit:changes -->';
24
+ if (!texto.includes(fim)) fail('bloco easyedit:changes não encontrado no spec');
25
+
26
+ const ids = [...texto.matchAll(/\| CH-(\d{3}) \|/g)].map((m) => Number(m[1]));
27
+ const id = `CH-${String((ids.length ? Math.max(...ids) : 0) + 1).padStart(3, '0')}`;
28
+ const hoje = new Date().toISOString().slice(0, 10);
29
+ const c = (s) => String(s).replaceAll('|', '/').replace(/\s*\n\s*/g, ' ');
30
+ const linha = `| ${id} | ${hoje} | ${tipo} | ${c(oque)} | ${c(motivo)} | ${origem} | ${c(afeta)} |\n`;
31
+
32
+ texto = texto.replace(fim, linha + fim).replace(/(\| Atualizado em \| )[^|]*( \|)/, `$1${hoje}$2`);
33
+ writeFileSync(spec, texto);
34
+ console.log(id);
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: easyedit-novo
3
+ description: "Use quando o usuário pede para começar, criar ou iniciar um novo vídeo para um cliente, ou para adotar o EasyEdit num projeto de vídeo que já existe sem spec.md. Cria o projeto HyperFrames e o spec.md em Draft a partir do template e depois chama a entrevista. Para vídeo com spec existente use easyedit-interviewer; para executar use easyedit-build."
4
+ ---
5
+
6
+ # Abrir um vídeo novo
7
+
8
+ ## Preparação obrigatória
9
+
10
+ Antes de executar esta skill, carregue `$easyedit-setup`. Reutilize o cliente confirmado na conversa.
11
+
12
+ ## Modo de interação
13
+
14
+ Modo de interação: `perguntas`, limitado ao necessário para criar o projeto (título do vídeo, proporção). O restante vai para a entrevista. Siga o contrato de perguntas de `.easyedit/Spec.md` (uma por vez, opções de 1 a 5).
15
+
16
+ ## Passos
17
+
18
+ 1. Defina o título e o mês (`AAAA-MM`, por padrão o atual). O nome da pasta será `AAAA-MM-<slug>`.
19
+ 2. Pergunte a **vertical**: `trafego-meta` (criativo para anúncio) ou `reels` (ainda sem playbook próprio; use o padrão geral). Escolha a proporção: `portrait` (1080x1920, Reels/TikTok/Shorts), `landscape` (1920x1080) ou `square`.
20
+ 3. Crie o projeto HyperFrames dentro de `projects/` do cliente. Se o usuário já trouxe um vídeo base, passe `--video <caminho>` e **não** use `raw/` como destino de escrita:
21
+
22
+ ```bash
23
+ cd clientes/<cliente>/projects
24
+ npx hyperframes init <AAAA-MM-slug> --non-interactive --resolution <preset> [--video <caminho>]
25
+ ```
26
+
27
+ Se o `init` for transcrever, faça a pergunta de autorização do modelo no formato do contrato: os modelos `.en` do whisper são só de inglês, então para português use `--language pt` com o modelo `large-v3`, ou `--skip-transcribe` e rode a transcrição depois.
28
+
29
+ 4. Crie o spec a partir do template:
30
+
31
+ ```bash
32
+ node .claude/skills/easyedit-novo/scripts/novo_video.mjs \
33
+ --cliente <cliente> --title "<título>" [--slug <slug>] [--mes AAAA-MM] [--vertical trafego-meta|reels]
34
+ ```
35
+
36
+ O script imprime o caminho do `spec.md` e **cria `clientes/<cliente>/raw/<AAAA-MM-slug>/`**: é nessa pasta que a pessoa coloca todas as gravações do vídeo (quantas forem). Avise isso na resposta. Se a pasta já tinha sido criada pelo `init`, ele só adiciona o arquivo.
37
+
38
+ 5. Se já houver arquivos na pasta de raw, anuncie `$easyedit-takes` para inventariar e ordenar antes da entrevista. Preencha o cabeçalho e a seção 2 do spec com o que já foi respondido. Mantenha `Status: Draft` e `Definition Gate: Pending`.
39
+ 6. Anuncie `Transição automática: $easyedit-novo → $easyedit-interviewer — motivo: spec em Draft — resultado esperado: Ato I completo` e carregue a entrevista.
40
+
41
+ ## Modo retroativo
42
+
43
+ Para um projeto existente sem spec: não rode o `init`. Crie só o `spec.md`, preencha o Ato I a partir do que o projeto já contém (`index.html`, `transcript.json`, `public/`, render em `output/`) e registre em `Suposições` que foi inferido. Marque as tarefas já visíveis como `Done`, com evidência quando houver. Confirme tudo com a pessoa antes de passar para `Defined`.
44
+
45
+ ## Limites
46
+
47
+ - Não sobrescreva um `spec.md` existente.
48
+ - Não crie cliente: se `clientes/<cliente>/` não existir, indique `scripts/novo-cliente.sh <nome>`.
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // Cria clientes/<cliente>/projects/<AAAA-MM-slug>/spec.md a partir de .easyedit/templates/Spec.md.
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const args = Object.fromEntries(
8
+ process.argv.slice(2).reduce((acc, a, i, all) => {
9
+ if (a.startsWith('--')) acc.push([a.slice(2), all[i + 1]]);
10
+ return acc;
11
+ }, []),
12
+ );
13
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
14
+
15
+ const root = resolve(args.root ?? join(dirname(fileURLToPath(import.meta.url)), '../../../..'));
16
+ const { cliente, title } = args;
17
+ if (!cliente || !title) fail('uso: --cliente <kebab> --title "<nome do vídeo>" [--slug <kebab>] [--mes AAAA-MM] [--vertical trafego-meta|reels] [--root <raiz>]');
18
+
19
+ const kebab = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
20
+ const hoje = new Date().toISOString().slice(0, 10);
21
+ const mes = args.mes ?? hoje.slice(0, 7);
22
+ if (!/^\d{4}-\d{2}$/.test(mes)) fail('--mes deve ser AAAA-MM');
23
+ const slug = `${mes}-${kebab(args.slug ?? title)}`;
24
+
25
+ const clienteDir = join(root, 'clientes', cliente);
26
+ if (!existsSync(clienteDir)) fail(`cliente não existe: ${clienteDir} (use scripts/novo-cliente.sh)`);
27
+ const projeto = join(clienteDir, 'projects', slug);
28
+ const specPath = join(projeto, 'spec.md');
29
+ if (existsSync(specPath)) fail(`já existe: ${specPath}`);
30
+
31
+ const modelo = join(root, '.easyedit/templates/Spec.md');
32
+ if (!existsSync(modelo)) fail(`template ausente: ${modelo}`);
33
+
34
+ const vertical = args.vertical ?? 'a definir';
35
+ if (!['trafego-meta', 'reels', 'a definir'].includes(vertical)) fail('--vertical: trafego-meta | reels');
36
+ mkdirSync(projeto, { recursive: true });
37
+ mkdirSync(join(clienteDir, 'raw', slug), { recursive: true });
38
+ const texto = readFileSync(modelo, 'utf8')
39
+ .replaceAll('{{TITULO}}', title).replaceAll('{{SLUG}}', slug)
40
+ .replaceAll('{{CLIENTE}}', cliente).replaceAll('{{VERTICAL}}', vertical).replaceAll('{{DATA}}', hoje);
41
+ writeFileSync(specPath, texto);
42
+ console.log(specPath);
43
+ console.log(`coloque as gravações em: ${join(clienteDir, 'raw', slug)}`);
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: easyedit-progress
3
+ description: "Use quando o usuário pede o andamento, status ou panorama dos vídeos: quais estão em rascunho, em execução, em revisão ou entregues, por cliente. Lê os spec.md e resume estado, gates, requisitos, tarefas e mudanças. Somente leitura."
4
+ ---
5
+
6
+ # Acompanhar o andamento
7
+
8
+ ## Modo de interação
9
+
10
+ Modo de interação: `sem perguntas`. Somente leitura.
11
+
12
+ ## Passos
13
+
14
+ 1. Rode:
15
+
16
+ ```bash
17
+ node .claude/skills/easyedit-progress/scripts/progress.mjs [--cliente <nome>] [--json]
18
+ ```
19
+
20
+ 2. Apresente um quadro por cliente com `Status`, gates, `RV`/`AC`, tarefas concluídas sobre o total e número de mudanças.
21
+ 3. Destaque o que pede ação: `Draft` parado, `Review` aguardando cliente, tarefas pendentes em `In Progress`, projetos `[sem spec]` (ofereça `$easyedit-novo` em modo retroativo).
22
+ 4. Para um vídeo específico, leia o `spec.md` e resuma as últimas mudanças da seção 12 e os feedbacks sem resposta da seção 13.
23
+
24
+ ## Limites
25
+
26
+ - Não altere arquivos.
27
+ - Os números vêm do `spec.md`; não estime nem complete lacunas.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ // Resume o estado dos vídeos: node progress.mjs [--cliente <nome>] [--json]
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const argv = process.argv.slice(2);
8
+ const flag = (n) => { const i = argv.indexOf(`--${n}`); return i < 0 ? undefined : argv[i + 1]; };
9
+ const root = resolve(flag('root') ?? join(dirname(fileURLToPath(import.meta.url)), '../../../..'));
10
+ const filtro = flag('cliente');
11
+ const json = argv.includes('--json');
12
+
13
+ const dirs = (p) => (existsSync(p) ? readdirSync(p, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : []);
14
+ const campo = (t, n) => t.match(new RegExp(`^\\| ${n} \\| ([^|]*) \\|$`, 'm'))?.[1].trim() ?? '-';
15
+ const linhas = (t, prefixo) => [...t.matchAll(new RegExp(`^\\| ${prefixo}-\\d{3} \\|.*$`, 'gm'))].map((m) => m[0]);
16
+
17
+ const out = [];
18
+ for (const cliente of dirs(join(root, 'clientes'))) {
19
+ if (filtro && cliente !== filtro) continue;
20
+ for (const video of dirs(join(root, 'clientes', cliente, 'projects'))) {
21
+ const spec = join(root, 'clientes', cliente, 'projects', video, 'spec.md');
22
+ if (!existsSync(spec)) { out.push({ cliente, video, status: 'sem spec' }); continue; }
23
+ const t = readFileSync(spec, 'utf8');
24
+ const tarefas = linhas(t, 'T');
25
+ out.push({
26
+ cliente, video, vertical: campo(t, 'Vertical'), status: campo(t, 'Status'),
27
+ definition: campo(t, 'Definition Gate'), delivery: campo(t, 'Delivery Gate'),
28
+ rv: linhas(t, 'RV').length, ac: linhas(t, 'AC').length,
29
+ tarefas: `${tarefas.filter((l) => /\| Done \|$/.test(l)).length}/${tarefas.filter((l) => !/\| Dropped \|$/.test(l)).length}`,
30
+ mudancas: linhas(t, 'CH').length,
31
+ });
32
+ }
33
+ }
34
+
35
+ if (json) console.log(JSON.stringify(out, null, 2));
36
+ else if (!out.length) console.log('nenhum vídeo encontrado');
37
+ else for (const v of out) console.log(v.status === 'sem spec'
38
+ ? `${v.cliente}/${v.video} [sem spec]`
39
+ : `${v.cliente}/${v.video} [${v.status}] ${v.vertical} def:${v.definition} entrega:${v.delivery} RV:${v.rv} AC:${v.ac} tarefas:${v.tarefas} mudanças:${v.mudancas}`);
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: easyedit-ritmo
3
+ description: "Use para eliminar respiros e pausas em vídeo falado, a pedido do usuário ou porque o perfil da vertical (por exemplo criativo de tráfego) exige ritmo sem pausa. Mede os intervalos entre palavras na transcrição, propõe a pausa máxima, gera os segmentos de corte e a transcrição com tempos remapeados. Não altera raw/. Para escolher o que inserir sobre a fala use easyedit-destaques."
4
+ ---
5
+
6
+ # Cortar respiros
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`. Use a pausa máxima definida na seção 5 do spec (padrão: tráfego 0,3 s; Reels 0,5 s).
15
+
16
+ ## Passos
17
+
18
+ 0. Se o vídeo tem várias gravações, rode `$easyedit-takes` antes e use `transcript.takes.json` como `--transcript`: o script corta as pausas dentro de cada take e emenda na ordem; `cortes.json` traz o `arquivo` de cada segmento (um `<video>` por segmento, apontando para o arquivo de `raw/<slug>/`).
19
+ 1. Garanta uma transcrição de palavras (`transcript.json`, array `{text, start, end}`). Se não houver, rode `npx hyperframes transcribe <vídeo> --language pt --model large-v3` (pergunte antes de baixar o modelo; os `.en` não servem para português).
20
+ 2. Simule sem gravar e mostre o efeito:
21
+
22
+ ```bash
23
+ node .claude/skills/easyedit-ritmo/scripts/pausas.mjs --transcript <transcript.json> --max-pausa 0.3 --dry
24
+ ```
25
+
26
+ Se os cortes forem poucos (o vídeo já estava apertado), diga isso e pare.
27
+ 3. Grave `cortes.json` e `transcript.cut.json` na pasta do projeto:
28
+
29
+ ```bash
30
+ node .claude/skills/easyedit-ritmo/scripts/pausas.mjs --transcript <transcript.json> --max-pausa 0.3 --out <pasta-do-projeto>
31
+ ```
32
+
33
+ 4. Monte a composição emendando um clipe de vídeo por segmento de `cortes.json`, com `data-media-start = fonte_inicio`, `data-start = timeline_inicio` e `data-duration = duracao` (receita *Split / splice* de `/hyperframes-core`, `references/creator-editing-recipes.md`). O áudio acompanha cada clipe.
34
+ 5. Use `transcript.cut.json` para legendas e destaques: os tempos já estão na linha do tempo final.
35
+ 6. Confira nos snapshots as emendas: corte seco no meio de uma sílaba é defeito. Aumente `--ini`/`--fim` ou `--max-pausa` se necessário.
36
+ 7. Registre com `$easyedit-log` (`ajustado`, "pausas encurtadas para Xs, N cortes, de A s para B s").
37
+
38
+ ## Limites
39
+
40
+ - Nunca altere `raw/`; o corte existe só na composição.
41
+ - Faça este passo **antes** de `easyedit-destaques` e `easyedit-legendas`, para que tudo use a linha do tempo final.
42
+ - Respiro que serve ao sentido (suspense, ênfase deliberada) não deve ser cortado: se a vertical permitir, sinalize e pergunte.
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Encurta pausas a partir de transcript.json (array [{text,start,end}]).
3
+ // Gera cortes.json (segmentos de fonte para emendar com data-media-start) e transcript.cut.json (tempos remapeados).
4
+ // uso: node pausas.mjs --transcript <transcript.json> [--max-pausa 0.3] [--ini 0.1] [--fim 0.25] [--duracao <s>] [--out <dir>] [--dry]
5
+ import { readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join, resolve } from 'node:path';
7
+
8
+ const args = Object.fromEntries(process.argv.slice(2).reduce((a, x, i, all) => {
9
+ if (x.startsWith('--')) a.push([x.slice(2), x.startsWith('--dry') ? 'true' : all[i + 1]]);
10
+ return a;
11
+ }, []));
12
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
13
+ if (!args.transcript) fail('uso: --transcript <transcript.json> [--max-pausa 0.3] [--ini 0.1] [--fim 0.25] [--duracao <s>] [--out <dir>] [--dry]');
14
+
15
+ const maxPausa = Number(args['max-pausa'] ?? 0.3);
16
+ const ini = Number(args.ini ?? 0.1);
17
+ const fim = Number(args.fim ?? 0.25);
18
+ const r = (n) => Math.round(n * 1000) / 1000;
19
+
20
+ const palavras = JSON.parse(readFileSync(args.transcript, 'utf8'))
21
+ .filter((w) => Number.isFinite(w.start) && Number.isFinite(w.end));
22
+ if (!palavras.length) fail('transcrição vazia');
23
+
24
+ // Agrupa palavras consecutivas pelo take de origem (sem `take` = um único arquivo).
25
+ const grupos = [];
26
+ for (const w of palavras) {
27
+ const g = grupos.at(-1);
28
+ if (g && g.take === w.take) g.palavras.push(w); else grupos.push({ take: w.take, palavras: [w] });
29
+ }
30
+ const segmentos = [];
31
+ let tl = 0;
32
+ const cortadas = [];
33
+ grupos.forEach((g, gi) => {
34
+ const ps = g.palavras;
35
+ const ultimo = gi === grupos.length - 1;
36
+ const fimGrupo = ps.at(-1).end + fim;
37
+ const limite = ultimo && args.duracao && !g.take ? Number(args.duracao) : fimGrupo;
38
+ const manter = [];
39
+ let abre = Math.max(0, ps[0].start - ini);
40
+ for (let i = 0; i < ps.length - 1; i++) {
41
+ const gap = ps[i + 1].start - ps[i].end;
42
+ if (gap > maxPausa) {
43
+ manter.push([abre, ps[i].end + maxPausa / 2]);
44
+ abre = ps[i + 1].start - maxPausa / 2;
45
+ }
46
+ }
47
+ manter.push([abre, Math.min(limite, fimGrupo)]);
48
+ const doGrupo = manter.map(([a, b]) => {
49
+ const s = { ...(g.take ? { arquivo: g.take } : {}), fonte_inicio: r(a), fonte_fim: r(b), timeline_inicio: r(tl), duracao: r(b - a) };
50
+ tl += b - a;
51
+ return s;
52
+ });
53
+ segmentos.push(...doGrupo);
54
+ for (const w of ps) {
55
+ const s = doGrupo.find((x) => w.start >= x.fonte_inicio - 1e-6 && w.start <= x.fonte_fim + 1e-6) ?? doGrupo.at(-1);
56
+ cortadas.push({ ...w, start: r(s.timeline_inicio + (w.start - s.fonte_inicio)), end: r(s.timeline_inicio + (w.end - s.fonte_inicio)) });
57
+ }
58
+ });
59
+ const duracao = r(grupos.reduce((acc, g) => acc + (g.palavras.at(-1).end + fim), 0));
60
+ // cortes = lacunas removidas dentro de um mesmo arquivo
61
+ const cortes = [];
62
+ segmentos.slice(1).forEach((s, i) => {
63
+ const ant = segmentos[i];
64
+ if (ant.arquivo === s.arquivo) cortes.push({ ...(s.arquivo ? { arquivo: s.arquivo } : {}), de: ant.fonte_fim, ate: s.fonte_inicio, removido: r(s.fonte_inicio - ant.fonte_fim) });
65
+ });
66
+
67
+ const resumo = { max_pausa: maxPausa, duracao_original: duracao, duracao_final: r(tl), removido: r(duracao - tl), n_cortes: cortes.length, segmentos, cortes };
68
+ console.log(`pausa máxima ${maxPausa}s · ${cortes.length} cortes · ${resumo.duracao_original}s → ${resumo.duracao_final}s · ${new Set(segmentos.map((s) => s.arquivo)).size} arquivo(s) (−${resumo.removido}s)`);
69
+ if (args.dry === 'true') process.exit(0);
70
+ const out = resolve(args.out ?? dirname(args.transcript));
71
+ writeFileSync(join(out, 'cortes.json'), JSON.stringify(resumo, null, 2));
72
+ writeFileSync(join(out, 'transcript.cut.json'), JSON.stringify(cortadas, null, 2));
73
+ console.log(`gravado: ${join(out, 'cortes.json')} e transcript.cut.json`);
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: easyedit-setup
3
+ description: "Carregue antes de qualquer outra skill easyedit-*: resolve a raiz do repositório, o cliente e o vídeo (projeto) alvo, e lê o contexto obrigatório (CLAUDE.md global e do cliente, .easyedit/TOOLS.md, .easyedit/Spec.md, spec.md do vídeo). Use também quando o usuário disser 'vamos trabalhar no vídeo X' sem dizer o que fazer. Não cria nem edita nada."
4
+ ---
5
+
6
+ # Preparar o contexto do vídeo
7
+
8
+ ## Modo de interação
9
+
10
+ Modo de interação: `sem perguntas`, exceto para desambiguar cliente ou vídeo quando houver mais de um candidato.
11
+
12
+ ## Passos
13
+
14
+ 1. Confirme a raiz: o diretório que contém `.easyedit/` e `clientes/`. Se não existir, pare e avise.
15
+ 2. Resolva o **cliente** (`clientes/<cliente>/`) pelo pedido do usuário ou pelo diretório atual. Se houver mais de um candidato, pergunte.
16
+ 3. Resolva o **vídeo** (`clientes/<cliente>/projects/<AAAA-MM-slug>/`). Sem vídeo indicado, rode `node .claude/skills/easyedit-progress/scripts/progress.mjs --cliente <cliente>` e peça a escolha, ou encaminhe para `$easyedit-novo` se for um vídeo novo.
17
+ 4. Leia, nesta ordem, só o que existir:
18
+ - `CLAUDE.md` da raiz e `clientes/<cliente>/CLAUDE.md` (marca, tom, preferências);
19
+ - `.easyedit/Spec.md` (contrato, estados, perguntas) e `.easyedit/TOOLS.md` (ferramentas);
20
+ - `projects/<vídeo>/spec.md`.
21
+ 5. Rode `npx hyperframes doctor` apenas quando a tarefa for renderizar ou transcrever e o ambiente ainda não tiver sido conferido na conversa.
22
+ 6. Informe em uma linha: cliente, vídeo, vertical, `Status` e gates, e quantos arquivos há em `raw/<slug>/`. Se o `CLAUDE.md` do cliente estiver com campos em branco, sinalize que a entrevista deve preenchê-los.
23
+
24
+ ## Continuar sem pedir permissão
25
+
26
+ Depois de informar o contexto, **não pergunte "posso começar?"**. Anuncie a transição e carregue a skill do próximo passo conforme o estado: sem spec → `$easyedit-novo`; `Draft` → `$easyedit-interviewer`; `Defined`/`In Progress`/`Review` → `$easyedit-build`. Se a tarefa pedida for outra (status, registro), siga esse pedido. Pendências do contexto (por exemplo, `CLAUDE.md` em branco, arquivo solto em `raw/`) entram como perguntas da etapa seguinte, no formato do contrato, uma por vez.
27
+
28
+ ## Limites
29
+
30
+ - Não edite `raw/`, não crie arquivos e não avance estado.
31
+ - Se o vídeo não tiver `spec.md` (projeto anterior ao EasyEdit), não o crie aqui: ofereça `$easyedit-novo` em modo retroativo.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: easyedit-split-broll
3
+ description: "Use para montar vídeo de fala com b-roll em tela dividida na horizontal (duas faixas, uma em cima e outra embaixo, por exemplo pessoa falando de um lado e b-roll do outro) em canvas 9:16, escolhendo e sincronizando o b-roll com a fala. Respeita as zonas cinzentas. Para destaques pontuais em tela cheia use easyedit-destaques."
4
+ ---
5
+
6
+ # Tela dividida com b-roll
7
+
8
+ ## Preparação obrigatória
9
+
10
+ Antes de executar esta skill, carregue `$easyedit-setup` e `$easyedit-zonas-seguras`. Carregue `/hyperframes-core` antes de escrever o HTML.
11
+
12
+ ## Modo de interação
13
+
14
+ Modo de interação: `sem perguntas`. Dúvida de estilo volta para `$easyedit-interviewer`.
15
+
16
+ ## Layout
17
+
18
+ Canvas 1080x1920 dividido em duas faixas empilhadas:
19
+
20
+ - **Padrão:** fala em cima, b-roll embaixo. Ao decidir, lembre que o rodapé (35%) é coberto pela interface: o b-roll pode ocupar a faixa inferior inteira, mas **nada informativo dentro dele** (texto, rosto, produto) pode ficar nos 672 px de baixo.
21
+ - A **linha de divisão** e o assunto principal de cada faixa ficam dentro da área útil (y 269–1248). Na prática, divida próximo de y ≈ 960 e enquadre o rosto e o b-roll para que o essencial caia acima de y = 1248.
22
+ - Referência de estrutura: `~/.agents/skills/talking-head-recut/references/layouts/split.html` e `stack.html`, adaptando proporções e zonas.
23
+
24
+ ## Passos
25
+
26
+ 1. Parta do plano de `$easyedit-destaques` (tempos e materiais). Cada trecho de b-roll cobre a frase da fala correspondente.
27
+ 2. Tire o b-roll de `assets/broll/` (imagens viram vídeo com movimento sutil via `/hyperframes-keyframes`, por exemplo Ken Burns). Material faltante: `/media-use` ou pedido ao cliente.
28
+ 3. Troque b-roll a cada 2–4 s; mantenha a faixa da fala contínua para o áudio ficar sincronizado.
29
+ 4. Recorte cada mídia com `object-fit: cover` e o ponto focal ajustado, sem deformar.
30
+ 5. Anime a entrada e a troca de b-roll com movimento curto (`/hyperframes-animation`); transição entre trechos rápida, sem fade longo.
31
+ 6. Registre cada faixa e o assunto principal (rosto, produto) em `elementos.json` e valide com `$easyedit-zonas-seguras`, dê `npx hyperframes check`, tire snapshots e registre com `$easyedit-log`.
32
+
33
+ ## Limites
34
+
35
+ - A faixa de fala não pode ficar vazia nem congelar.
36
+ - **Rostos no b-roll também contam**: um b-roll com crianças ou pessoas na faixa de baixo cai nos 35% do rodapé e é erro do verificador. Escolha ou reenquadre o b-roll para que o rosto fique acima de y = 1248, ou use a faixa de baixo só para planos sem rosto.
37
+ - Não use b-roll cuja origem ou direito de uso seja desconhecido.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: easyedit-takes
3
+ description: "Use quando um vídeo (criativo) chega em várias gravações separadas na pasta do vídeo: inventaria os arquivos, define a ordem cronológica (numeração do nome e data de geração), transcreve cada take, identifica repetições e refações, escolhe o melhor trecho, e prepara a sequência para corte e colagem. Rode antes de easyedit-ritmo. Com um único arquivo bruto, pule esta skill."
4
+ ---
5
+
6
+ # Ordenar e juntar as gravações
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: `perguntas`, só quando a ordem for ambígua, quando houver refações com escolha subjetiva ou quando os formatos divergirem. Siga o `Contrato de perguntas (uma por vez)` de `.easyedit/Spec.md`: **uma pergunta por mensagem**, em múltipla escolha de 1 a 5 (1–3 opções técnicas, 4 gerar outras opções, 5 avançar sem responder). Ordem de perguntas: ordem dos takes, depois autorização do modelo de transcrição, depois descartes de refação.
15
+
16
+ ## Onde ficam os arquivos
17
+
18
+ A pessoa coloca todas as gravações do vídeo em **`clientes/<cliente>/raw/<AAAA-MM-slug>/`**, com o mesmo nome da pasta do projeto. Nunca edite esses arquivos. Se houver arquivos soltos em `raw/`, pergunte a qual vídeo pertencem antes de mover; mover é ação sensível.
19
+
20
+ ## Passos
21
+
22
+ 1. **Inventariar e ordenar:**
23
+
24
+ ```bash
25
+ node .claude/skills/easyedit-takes/scripts/takes.mjs listar \
26
+ --pasta clientes/<cliente>/raw/<slug> --out clientes/<cliente>/projects/<slug>
27
+ ```
28
+
29
+ Gera `takes.json` com duração, resolução, fps, rotação, áudio e data de criação (metadado do arquivo; se ausente, do sistema de arquivos). Regra de ordem:
30
+ - nomes numerados (`take 1`, `01-`, `IMG_0175`) → ordem natural do nome;
31
+ - nomes sem número → data de criação;
32
+ - **se nome e data divergirem, a confiança é `baixa`**: pergunte a ordem (1. por nome, 2. por data, 3. deixar o conteúdo decidir, transcrevendo tudo e propondo a ordem pelo roteiro; 4 e 5 como no contrato; sugestão: 3 quando a divergência for grande). Ajuste `ordem` em `takes.json`.
33
+ 2. **Transcrever cada take** (se o modelo não estiver baixado, pergunte: 1. baixar `large-v3` (~3 GB, melhor para português) (sugestão), 2. usar um modelo menor, 3. pular a transcrição; 4 e 5 como no contrato; use `--language pt` com `large-v3`, os modelos `.en` são só de inglês) e guarde em `projects/<slug>/takes/<nome-sem-extensão>.transcript.json`.
34
+ 3. **Entender a sequência pelo conteúdo.** Leia as transcrições na ordem e confira com o roteiro da seção 4 do spec: abertura, desenvolvimento, CTA. Aponte o que não bate (take fora de lugar, parte do roteiro sem gravação).
35
+ 4. **Refações:** quando a mesma frase aparece em mais de um take, ou há começo falso, gagueira ou erro no meio:
36
+ - proponha a melhor versão (a mais completa e fluente, em regra a última) e mostre a tabela *take · trecho · motivo*;
37
+ - com a aprovação, marque `usar: false` no take descartado ou o intervalo `[de, ate]` em `descartar` do take (tempos da fonte, em segundos).
38
+ 5. **Montar a transcrição unificada** na ordem final:
39
+
40
+ ```bash
41
+ node .claude/skills/easyedit-takes/scripts/takes.mjs montar --out clientes/<cliente>/projects/<slug>
42
+ ```
43
+
44
+ Gera `transcript.takes.json`, com cada palavra marcada com o `take` de origem e os tempos da fonte. Passe-o para `$easyedit-ritmo` (`--transcript transcript.takes.json`), que corta as pausas **dentro** de cada take e emenda os segmentos na ordem.
45
+ 6. **Normalizar a junção:** se os takes tiverem resolução, fps ou rotação diferentes, a composição usa o canvas definido na seção 2 do spec e enquadra cada clipe com `object-fit` e foco ajustados. Níveis de áudio diferentes: `npx hyperframes normalize-audio` entre takes. Procure saltos de enquadramento, luz e som entre takes e proponha *punch-in* (`/hyperframes-keyframes`) ou b-roll para mascarar a emenda.
46
+ 7. Registre na seção 3 do spec a tabela dos takes (ordem, arquivo, duração, uso) e, com `$easyedit-log`, cada descarte e a ordem escolhida (`ajustado`).
47
+
48
+ ## Limites
49
+
50
+ - Nunca altere, renomeie ou apague arquivos em `raw/`.
51
+ - Não invente a ordem: quando houver conflito, a pessoa decide.
52
+ - Não descarte um take sem mostrar o motivo e obter a aprovação.
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Inventaria e ordena as gravações (takes) de um vídeo e monta a transcrição unificada.
3
+ // node takes.mjs listar --pasta <raw/slug> --out <projects/slug>
4
+ // node takes.mjs montar --out <projects/slug>
5
+ import { execFileSync } from 'node:child_process';
6
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
7
+ import { basename, extname, join, resolve } from 'node:path';
8
+
9
+ const [cmd, ...rest] = process.argv.slice(2);
10
+ const args = Object.fromEntries(rest.reduce((a, x, i, all) => (x.startsWith('--') ? [...a, [x.slice(2), all[i + 1]]] : a), []));
11
+ const fail = (m) => { console.error(`erro: ${m}`); process.exit(1); };
12
+ const EXT = new Set(['.mp4', '.mov', '.m4v', '.webm', '.mkv', '.mp3', '.wav', '.m4a']);
13
+ const r = (n) => Math.round(n * 1000) / 1000;
14
+
15
+ if (cmd === 'listar') {
16
+ if (!args.pasta || !args.out) fail('uso: listar --pasta <raw/slug> --out <projects/slug>');
17
+ const pasta = resolve(args.pasta);
18
+ if (!existsSync(pasta)) fail(`pasta não existe: ${pasta}`);
19
+ const arquivos = readdirSync(pasta).filter((f) => !f.startsWith('.') && EXT.has(extname(f).toLowerCase()));
20
+ if (!arquivos.length) fail('nenhum arquivo de mídia na pasta');
21
+
22
+ const itens = arquivos.map((arquivo) => {
23
+ const caminho = join(pasta, arquivo);
24
+ const j = JSON.parse(execFileSync('ffprobe', ['-v', 'error', '-print_format', 'json', '-show_format', '-show_streams', caminho]).toString());
25
+ const v = j.streams.find((s) => s.codec_type === 'video');
26
+ const a = j.streams.find((s) => s.codec_type === 'audio');
27
+ const [num, den] = (v?.avg_frame_rate ?? '0/1').split('/').map(Number);
28
+ const rot = Number(v?.side_data_list?.find((d) => d.rotation !== undefined)?.rotation ?? v?.tags?.rotate ?? 0);
29
+ const st = statSync(caminho);
30
+ const tag = j.format?.tags?.creation_time ?? v?.tags?.creation_time;
31
+ return {
32
+ arquivo, duracao: r(Number(j.format.duration)),
33
+ largura: v?.width ?? null, altura: v?.height ?? null, fps: den ? r(num / den) : null, rotacao: rot,
34
+ audio: Boolean(a), video: Boolean(v),
35
+ criado_em: tag ?? st.birthtime.toISOString(), criado_em_origem: tag ? 'metadado' : 'sistema de arquivos',
36
+ modificado_em: st.mtime.toISOString(),
37
+ };
38
+ });
39
+
40
+ const porNome = [...itens].sort((x, y) => x.arquivo.localeCompare(y.arquivo, 'pt', { numeric: true }));
41
+ const porData = [...itens].sort((x, y) => x.criado_em.localeCompare(y.criado_em));
42
+ const todosComNumero = itens.every((i) => /\d/.test(i.arquivo));
43
+ const mesmaOrdem = porNome.every((x, i) => x.arquivo === porData[i].arquivo);
44
+ const regra = mesmaOrdem ? 'nome e data concordam' : todosComNumero ? 'nome (numeração), data diverge' : 'data de criação (nomes sem numeração)';
45
+ const ordenado = mesmaOrdem || !todosComNumero ? (mesmaOrdem ? porNome : porData) : porNome;
46
+
47
+ const takes = ordenado.map((t, i) => ({ ordem: i + 1, usar: true, descartar: [], ...t }));
48
+ const saida = { pasta, regra_de_ordem: regra, confianca: mesmaOrdem ? 'alta' : 'baixa', diverge: !mesmaOrdem, takes };
49
+ writeFileSync(join(resolve(args.out), 'takes.json'), JSON.stringify(saida, null, 2));
50
+ console.log(`${takes.length} takes · ordem: ${regra} · confiança ${saida.confianca}`);
51
+ for (const t of takes) console.log(`${String(t.ordem).padStart(2)}. ${t.arquivo} ${t.duracao}s ${t.largura}x${t.altura}@${t.fps}${t.rotacao ? ` rot${t.rotacao}` : ''} ${t.audio ? '' : '[sem áudio] '}${t.criado_em.slice(0, 19)} (${t.criado_em_origem})`);
52
+ if (saida.diverge) console.log('\nATENÇÃO: ordem por nome ≠ ordem por data. Confirme com a pessoa e ajuste "ordem" em takes.json.');
53
+ const res = new Set(takes.map((t) => `${t.rotacao % 180 ? t.altura : t.largura}x${t.rotacao % 180 ? t.largura : t.altura}@${t.fps}`));
54
+ if (res.size > 1) console.log(`ATENÇÃO: takes com formatos diferentes (${[...res].join(', ')}). Normalizar na montagem.`);
55
+ } else if (cmd === 'montar') {
56
+ if (!args.out) fail('uso: montar --out <projects/slug>');
57
+ const out = resolve(args.out);
58
+ const { takes } = JSON.parse(readFileSync(join(out, 'takes.json'), 'utf8'));
59
+ const palavras = [];
60
+ const faltando = [];
61
+ for (const t of [...takes].sort((a, b) => a.ordem - b.ordem)) {
62
+ if (!t.usar) continue;
63
+ const f = join(out, 'takes', `${basename(t.arquivo, extname(t.arquivo))}.transcript.json`);
64
+ if (!existsSync(f)) { faltando.push(f); continue; }
65
+ for (const w of JSON.parse(readFileSync(f, 'utf8'))) {
66
+ if ((t.descartar ?? []).some(([de, ate]) => w.start >= de && w.end <= ate)) continue;
67
+ palavras.push({ ...w, take: t.arquivo });
68
+ }
69
+ }
70
+ if (faltando.length) fail(`transcrições ausentes:\n${faltando.join('\n')}`);
71
+ writeFileSync(join(out, 'transcript.takes.json'), JSON.stringify(palavras, null, 2));
72
+ console.log(`${palavras.length} palavras de ${new Set(palavras.map((p) => p.take)).size} takes → transcript.takes.json`);
73
+ } else fail('comandos: listar | montar');
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: easyedit-vertical-trafego
3
+ description: "Use quando o vídeo é um criativo para anúncio de tráfego pago na Meta (Instagram/Facebook Ads): define o perfil da vertical no spec (requisitos e aceites padrão) e orquestra as skills de takes, ritmo, destaques, tela dividida, legendas e zonas seguras com foco em conversão. Aplica-se quando o campo Vertical do spec é trafego-meta. Para Reels orgânico (ainda não implementado), não use."
4
+ ---
5
+
6
+ # Vertical · Criativo para tráfego na Meta
7
+
8
+ ## Preparação obrigatória
9
+
10
+ Antes de executar esta skill, carregue `$easyedit-setup`. Leia [references/playbook.md](references/playbook.md).
11
+
12
+ ## Modo de interação
13
+
14
+ Modo de interação: `perguntas` na fase de especificação (uma por vez, só o que o playbook não responde, sempre em múltipla escolha de 1 a 5); `sem perguntas` na execução.
15
+
16
+ ## Na especificação (chamada pelo `$easyedit-interviewer`)
17
+
18
+ 1. Marque `Vertical: trafego-meta` no cabeçalho do spec.
19
+ 2. **Não pergunte nem fixe meta de duração**: ela é livre (ver o playbook). Pergunte, **uma por vez** e nesta ordem de prioridade (contrato de perguntas do `.easyedit/Spec.md`), só o que muda o criativo: **oferta e público**, **CTA único** e destino, **gancho** (dor/promessa), **provas** disponíveis, **placements** (só Stories/Reels ou também feed 4:5), **quantas variantes de gancho**, restrições do setor.
20
+ 3. Preencha os padrões, para a pessoa confirmar ou alterar:
21
+
22
+ | Item | Padrão |
23
+ | --- | --- |
24
+ | Formato | 1080x1920, 30 fps; variante 4:5 se houver feed |
25
+ | Duração | Livre: sem mínimo, máximo ou meta de tempo; a duração final é só registrada |
26
+ | RV | Gancho na fala e no visual nos primeiros 3 s |
27
+ | RV | A ideia é concisa do gancho ao CTA: cada cena tem uma função no argumento (gancho, problema, solução, prova, CTA), sem digressão nem repetição |
28
+ | RV | Legenda em 100% da fala |
29
+ | RV | Nenhum texto, rosto, produto, logo ou CTA nas zonas cinzentas |
30
+ | RV | Pausa entre palavras ≤ 0,3 s |
31
+ | RV | CTA falado e escrito, com quadro final |
32
+ | RV | Destaque visual a cada 2–4 s |
33
+ | RV | Música sob a voz, sem competir |
34
+ | AC | Concisão: a tabela de cenas da seção 4 mapeia cada trecho a uma função; trecho sem função é cortado ou justificado por escrito |
35
+ | AC | Cada RV acima com ao menos um critério observável (por exemplo, `verificar_zonas.mjs` com 0 erros e `check` das 4 zonas sem erro; `pausas.mjs` com `max-pausa 0.3`) |
36
+
37
+ 4. Passe os itens do checklist de conformidade do playbook para a seção 8 como pendências a validar com o cliente.
38
+
39
+ ## Na execução (chamada pelo `$easyedit-build`)
40
+
41
+ Ordem de trabalho:
42
+
43
+ 1. `$easyedit-takes` quando houver mais de um arquivo bruto.
44
+ 2. `$easyedit-ritmo` com `--max-pausa 0.3`.
45
+ 3. `$easyedit-destaques` com densidade de 2–4 s, e aprovação do plano.
46
+ 4. `$easyedit-split-broll` para a base da tela dividida; `$easyedit-legendas` para a legenda.
47
+ 5. Áudio (`/hyperframes-audio`): voz clara, música baixa, ducking.
48
+ 6. `$easyedit-zonas-seguras` (as três verificações): `check --caption-zone` ×4 sem erro, `elementos.json` completo e `verificar_zonas.mjs` sobre um render `-q draft` com **0 erros** (rostos, logo, produto, CTA e texto dentro da área útil) e avisos avaliados nos quadros anotados.
49
+ 7. Renderize `<slug>-hookA-v1.mp4` (e as demais variantes), `-q delivery`, 30 fps. Confira com `ffprobe`.
50
+ 8. Verifique os AC da vertical um a um e registre as evidências; diga o que não foi possível verificar.
51
+
52
+ ## Limites
53
+
54
+ - Os padrões numéricos são ponto de partida; calibre com resultado de campanha e registre a mudança.
55
+ - O checklist de conformidade é alerta. Quem decide a publicação é a pessoa e o cliente.