@intelli-ia/easyedit 0.1.0 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -3
- package/package.json +1 -1
- package/payload/.easyedit/TOOLS.md +8 -6
- package/payload/skills/easyedit-build/SKILL.md +2 -2
- package/payload/skills/easyedit-zonas-seguras/SKILL.md +2 -2
- package/payload/skills/easyedit-zonas-seguras/scripts/verificar_zonas.mjs +74 -43
- package/src/doctor.mjs +24 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
- Suporte a Windows (e Linux): `doctor` funciona com `npx.cmd`, mostra o sistema e como instalar cada dependência por sistema.
|
|
5
|
+
- Verificador de zonas seguras: sem detecção automática (Windows/Linux), gera folhas de revisão visual com as zonas em vermelho e exige a conferência; `--manual 1` força o modo; `EASYEDIT_DETECTOR` aceita um detector externo.
|
|
6
|
+
- `TOOLS.md` agora é neutro quanto a sistema e caminhos.
|
|
7
|
+
- CI no GitHub Actions em ubuntu, macOS e Windows (Node 22 e 24).
|
|
8
|
+
|
|
3
9
|
## 0.1.0
|
|
4
10
|
- Primeira versão: `install`, `update`, `doctor`, `novo`, `progress`.
|
|
5
11
|
- 13 skills `easyedit-*`, contrato `.easyedit/Spec.md` e template de spec.
|
package/README.md
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
Especificação, entrevista e acompanhamento de edição de vídeo (HyperFrames) em forma de skills para agentes de código.
|
|
4
4
|
|
|
5
5
|
## Instalar a CLI
|
|
6
|
-
npm install --global @intelli-ia/easyedit
|
|
7
|
-
npm install --global github:intelli-ia/easyedit # direto do GitHub
|
|
6
|
+
npm install --global @intelli-ia/easyedit
|
|
8
7
|
# desenvolvimento: npm link nesta pasta
|
|
9
8
|
|
|
10
9
|
## Usar num projeto
|
|
@@ -16,5 +15,12 @@ Especificação, entrevista e acompanhamento de edição de vídeo (HyperFrames)
|
|
|
16
15
|
Depois abra uma sessão nova do agente e use `/easyedit-novo` → `/easyedit-interviewer` → `/easyedit-build`.
|
|
17
16
|
|
|
18
17
|
## Requisitos
|
|
19
|
-
Node 22+, FFmpeg
|
|
18
|
+
Node 22+, FFmpeg no `PATH` e HyperFrames no projeto (`npm i -D hyperframes`). Whisper.cpp (ou Parakeet) para transcrição. `easyedit doctor` diz o que falta e como instalar no seu sistema.
|
|
19
|
+
|
|
20
|
+
| | macOS | Windows |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| CLI e skills | ✔ | ✔ |
|
|
23
|
+
| FFmpeg | `brew install ffmpeg` | `winget install Gyan.FFmpeg` |
|
|
24
|
+
| Detecção automática de rosto nas zonas seguras | ✔ (Vision) | revisão visual de folhas de contato (obrigatória no gate); `EASYEDIT_DETECTOR` aceita um detector próprio |
|
|
25
|
+
| Render | mais rápido (Apple Silicon) | ✔ |
|
|
20
26
|
O EasyEdit espera a estrutura `clientes/<cliente>/{raw,assets,projects,output}`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intelli-ia/easyedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Especificação, entrevista e acompanhamento de edição de vídeo com HyperFrames, em forma de skills para agentes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/intelli-ia/easyedit.git" },
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Referência das ferramentas usadas neste repositório. É o equivalente do `STACK.md` do Specsfy: as skills do EasyEdit leem este arquivo para saber **o que existe, para que serve e como invocar**.
|
|
4
4
|
|
|
5
|
-
Levantado
|
|
5
|
+
Levantado com `npx hyperframes doctor` e `--help` de cada comando. Este arquivo é criado uma vez pelo `easyedit install` e não é sobrescrito no `update`: ajuste-o à sua máquina. Para ver o que está instalado agora, rode `easyedit doctor`.
|
|
6
|
+
|
|
7
|
+
**Sistemas:** macOS e Windows são suportados. No macOS o verificador de zonas seguras detecta rostos automaticamente (Vision); no Windows (e Linux) ele gera folhas de revisão visual e a camada declarada (`elementos.json`) continua exata.
|
|
6
8
|
|
|
7
9
|
## 1. Motor principal: HyperFrames
|
|
8
10
|
|
|
@@ -85,13 +87,13 @@ Workflows que o roteador instala sob demanda, ainda não presentes na máquina:
|
|
|
85
87
|
|
|
86
88
|
| Ferramenta | Versão / caminho | Papel no pipeline |
|
|
87
89
|
| --- | --- | --- |
|
|
88
|
-
| Node.js |
|
|
89
|
-
| FFmpeg / FFprobe |
|
|
90
|
-
| Chrome headless-shell |
|
|
91
|
-
| whisper.cpp (`whisper-cli`) |
|
|
90
|
+
| Node.js | ≥ 22 | Roda a CLI e os scripts. |
|
|
91
|
+
| FFmpeg / FFprobe | no `PATH` (macOS: `brew install ffmpeg` · Windows: `winget install Gyan.FFmpeg` ou `choco install ffmpeg`) | Codifica o vídeo, faz mux e mix de áudio, corte, conversão e extração de frames. O FFprobe lê metadados (duração, fps, codec, resolução) dos arquivos brutos. |
|
|
92
|
+
| Chrome headless-shell | baixado e gerenciado por `hyperframes browser` (cache do usuário) | Renderiza cada frame da composição. Gerenciado por `hyperframes browser`. |
|
|
93
|
+
| whisper.cpp (`whisper-cli`) | no `PATH` (macOS: `brew install whisper-cpp` · Windows: binário do release do whisper.cpp, ou use o Parakeet) | Transcrição local. Modelos: `tiny.en`, `base.en`, `small.en` (padrão), `medium.en`, `large-v3`. Os `.en` são só inglês; **para português use `-l pt` com `large-v3`** ou o Parakeet. |
|
|
92
94
|
| Parakeet (opcional) | `hyperframes models install parakeet` | Alternativa de ASR, mais rápida e precisa. Com `--engine auto` ela é preferida quando instalada e cobre o idioma. |
|
|
93
95
|
| GSAP | embutido via adapter | Biblioteca de animação em JavaScript: movimento, easing e timelines. Fica pausada e é controlada por seek. |
|
|
94
|
-
| Docker |
|
|
96
|
+
| Docker | opcional; só para `--docker` | Render determinístico opcional (`--docker`). |
|
|
95
97
|
| Kokoro TTS | **não instalado** (opcional) | Voz local. `pip install kokoro-onnx soundfile`. |
|
|
96
98
|
| MusicGen | **não instalado** (opcional) | Música local. `pip install transformers torch soundfile numpy`. |
|
|
97
99
|
| yt-dlp | não instalado | Não faz parte do fluxo atual. |
|
|
@@ -53,7 +53,7 @@ Durante o trabalho:
|
|
|
53
53
|
## Validar e renderizar
|
|
54
54
|
|
|
55
55
|
1. `npx hyperframes check` (lint, runtime e layout). Corrija até passar.
|
|
56
|
-
2. Em vídeo 9:16 de vertical (tráfego/Reels), rode as três verificações de `$easyedit-zonas-seguras` (inclui `verificar_zonas.mjs` num render `-q draft`) e corrija até **0 erros**.
|
|
56
|
+
2. Em vídeo 9:16 de vertical (tráfego/Reels), rode as três verificações de `$easyedit-zonas-seguras` (inclui `verificar_zonas.mjs` num render `-q draft`) e corrija até **0 erros**. Sem detecção automática (Windows/Linux), o gate inclui a **revisão visual das folhas** que o verificador gera, com o resultado registrado em Evidências.
|
|
57
57
|
3. `npx hyperframes snapshot` e confira os frames-chave contra os `AC`. Veja os PNGs antes de declarar pronto.
|
|
58
58
|
4. Renderize para a saída do cliente, sem sobrescrever versões anteriores:
|
|
59
59
|
|
|
@@ -62,7 +62,7 @@ npx hyperframes render -o ../../output/<slug>-vN.mp4 -f <fps> -q delivery
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
5. Confira o arquivo com `ffprobe` (resolução e fps) contra a seção 2 e registre a duração final; a duração não tem limite nem meta.
|
|
65
|
-
6. Rode `verificar_zonas.mjs` também sobre o render final (`delivery`) quando for vertical, e confirme 0 erros.
|
|
65
|
+
6. Rode `verificar_zonas.mjs` também sobre o render final (`delivery`) quando for vertical, e confirme 0 erros (e a revisão visual das folhas, em Windows/Linux).
|
|
66
66
|
7. Registre as evidências na seção 11 (comando, resultado, data) e a versão na seção 14.
|
|
67
67
|
8. Marque `Status: Review` e `Delivery Gate: Pending`. Informe o caminho do render e quais `AC` foram verificados, e quais não puderam ser.
|
|
68
68
|
|
|
@@ -46,7 +46,7 @@ node ../../../../.claude/skills/easyedit-zonas-seguras/scripts/zonas.mjs comando
|
|
|
46
46
|
|
|
47
47
|
Mantenha o arquivo em dia a cada mudança de posição.
|
|
48
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
|
|
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) e compara tudo com a área útil. Também confere o `elementos.json`. No **macOS** a detecção é automática (framework Vision, local, sem rede). Em **Windows/Linux** não há detecção automática de rosto: o verificador gera folhas de contato com as zonas em vermelho (`revisao-visual/folha01.png`…, 18 quadros por folha) e imprime `REVISÃO VISUAL OBRIGATÓRIA`. Nesse caso **abra cada folha com a ferramenta de leitura de imagens**, confira se algum rosto, texto, logo ou produto cai sobre o vermelho, e registre o resultado (e os instantes com problema) em Evidências. Use `--manual 1` para pedir esse modo também no macOS e `--manual-fps` para mudar a amostragem (padrão 1 quadro por segundo). Um detector próprio pode ser plugado com a variável `EASYEDIT_DETECTOR` (executável que recebe JPGs e devolve o mesmo JSON do `detectar.swift`).
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
node .claude/skills/easyedit-zonas-seguras/scripts/verificar_zonas.mjs \
|
|
@@ -69,5 +69,5 @@ node .claude/skills/easyedit-zonas-seguras/scripts/verificar_zonas.mjs \
|
|
|
69
69
|
## Limites
|
|
70
70
|
|
|
71
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)
|
|
72
|
+
- Requer `ffmpeg` em qualquer sistema. A detecção automática exige macOS com `swiftc` (Xcode Command Line Tools); na primeira execução o detector é compilado em `~/.cache/easyedit/detectar`. Sem ela (Windows/Linux), a revisão visual das folhas é parte obrigatória do gate, e o `elementos.json` continua sendo a verificação exata.
|
|
73
73
|
- Não altere os valores de `ZONAS` sem registrar a mudança e a fonte.
|
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
// Verifica se rostos, texto, logo, produto e CTA ficam dentro da área útil (fora das zonas cinzentas).
|
|
3
3
|
// node verificar_zonas.mjs --video <render.mp4> [--elementos <elementos.json>] [--fps 2] [--out <dir>] [--tol 0.05]
|
|
4
4
|
// Camadas:
|
|
5
|
-
// 1. Pixels (Vision,
|
|
5
|
+
// 1. Pixels (Vision no macOS; EASYEDIT_DETECTOR em qualquer sistema; sem detector, folhas de revisão visual): rosto → erro; texto lido e objeto saliente (candidato a produto/logo) → aviso
|
|
6
6
|
// (texto nosso é medido com exatidão pelo `check --caption-zone`; OCR em pixel pega texto dentro de imagem/vídeo).
|
|
7
7
|
// 2. Declarada (elementos.json): logo/produto/cta/rosto/texto com caixa em px → erro. Geometria exata.
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
-
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { UTIL, ZONAS } from './zonas.mjs';
|
|
14
14
|
|
|
15
15
|
const aqui = dirname(fileURLToPath(import.meta.url));
|
|
16
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
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]');
|
|
18
|
+
if (!args.video && !args.elementos) fail('uso: --video <render.mp4> [--elementos <elementos.json>] [--fps 2] [--out <dir>] [--tol 0.05] [--manual 1]');
|
|
19
19
|
|
|
20
20
|
const fps = Number(args.fps ?? 2);
|
|
21
21
|
const tol = Number(args.tol ?? 0.05);
|
|
@@ -43,6 +43,7 @@ const regioes = ({ x, y, w, h }) => {
|
|
|
43
43
|
|
|
44
44
|
const achados = [];
|
|
45
45
|
const info = [];
|
|
46
|
+
let manual = null;
|
|
46
47
|
|
|
47
48
|
// ---- camada 2: elementos declarados
|
|
48
49
|
if (args.elementos) {
|
|
@@ -57,52 +58,81 @@ if (args.elementos) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// ---- camada 1: pixels
|
|
61
|
+
// Detector: Vision (macOS), executável externo (EASYEDIT_DETECTOR) ou revisão visual manual (Windows/Linux).
|
|
62
|
+
// Um detector externo recebe caminhos de JPG como argumentos e imprime em stdout o mesmo JSON do detectar.swift:
|
|
63
|
+
// [{ "arquivo": "<jpg>", "deteccoes": [{ "tipo": "rosto|texto|objeto", "x": 0-1, "y": 0-1, "w": 0-1, "h": 0-1, "conf": 0-1, "texto": "..." }] }]
|
|
64
|
+
let modoPixels = 'sem-video';
|
|
60
65
|
if (args.video) {
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
}
|
|
66
|
+
const externo = process.env.EASYEDIT_DETECTOR;
|
|
67
|
+
modoPixels = args.manual === '1' ? 'manual' : externo ? 'externo' : process.platform === 'darwin' ? 'vision' : 'manual';
|
|
77
68
|
const frames = join(out, 'frames');
|
|
78
69
|
rmSync(frames, { recursive: true, force: true });
|
|
79
70
|
mkdirSync(frames, { recursive: true });
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
|
|
72
|
+
if (modoPixels === 'manual') {
|
|
73
|
+
// Sem detector automático: gera folhas de contato com as zonas em vermelho para o agente/pessoa olhar.
|
|
74
|
+
const mfps = Number(args['manual-fps'] ?? 1);
|
|
75
|
+
const tw = 270, th = 480;
|
|
76
|
+
const zonasMini = [
|
|
77
|
+
`drawbox=x=0:y=0:w=${tw}:h=${Math.round(th * UTIL.y0)}:color=red@0.6:t=fill`,
|
|
78
|
+
`drawbox=x=0:y=${Math.round(th * UTIL.y1)}:w=${tw}:h=${th - Math.round(th * UTIL.y1)}:color=red@0.6:t=fill`,
|
|
79
|
+
`drawbox=x=0:y=${Math.round(th * UTIL.y0)}:w=${Math.round(tw * UTIL.x0)}:h=${Math.round(th * (UTIL.y1 - UTIL.y0))}:color=red@0.6:t=fill`,
|
|
80
|
+
`drawbox=x=${Math.round(tw * UTIL.x1)}:y=${Math.round(th * UTIL.y0)}:w=${tw - Math.round(tw * UTIL.x1)}:h=${Math.round(th * (UTIL.y1 - UTIL.y0))}:color=red@0.6:t=fill`,
|
|
81
|
+
`drawbox=x=${Math.round(tw * UTIL.x0)}:y=${Math.round(th * UTIL.y0)}:w=${Math.round(tw * (UTIL.x1 - UTIL.x0))}:h=${Math.round(th * (UTIL.y1 - UTIL.y0))}:color=white:t=2`, // contorno da área útil
|
|
82
|
+
];
|
|
83
|
+
const dir = join(out, 'revisao-visual');
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', args.video, '-vf', [`fps=${mfps}`, `scale=${tw}:${th}`, ...zonasMini, 'tile=6x3:padding=4:color=black'].join(','), '-fps_mode', 'passthrough', join(dir, 'folha%02d.png')]);
|
|
87
|
+
const folhas = readdirSync(dir).filter((f) => f.endsWith('.png')).sort();
|
|
88
|
+
info.push(`detecção automática de pixels indisponível neste sistema (${process.platform}); ${folhas.length} folha(s) de revisão visual a ${mfps} fps, 18 quadros por folha, da esquerda para a direita e de cima para baixo`);
|
|
89
|
+
manual = { dir, folhas, fps: mfps };
|
|
90
|
+
} else {
|
|
91
|
+
let bin;
|
|
92
|
+
if (modoPixels === 'vision') {
|
|
93
|
+
bin = join(homedir(), '.cache', 'easyedit', 'detectar');
|
|
94
|
+
const fonte = join(aqui, 'detectar.swift');
|
|
95
|
+
if (!existsSync(bin) || statSync(bin).mtimeMs < statSync(fonte).mtimeMs) {
|
|
96
|
+
mkdirSync(dirname(bin), { recursive: true });
|
|
97
|
+
console.error('compilando detector (uma vez)…');
|
|
98
|
+
// O SDK padrão pode ser mais novo que o compilador; tenta os SDKs instalados do mais novo ao mais antigo.
|
|
99
|
+
const sdks = ['/Library/Developer/CommandLineTools/SDKs', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs']
|
|
100
|
+
.filter(existsSync).flatMap((d) => readdirSync(d).filter((s) => /^MacOSX\d+(\.\d+)?\.sdk$/.test(s)).map((s) => join(d, s)))
|
|
101
|
+
.sort((a, b) => b.localeCompare(a, 'en', { numeric: true }));
|
|
102
|
+
let ok = false;
|
|
103
|
+
for (const sdk of [null, ...sdks]) {
|
|
104
|
+
try { execFileSync('swiftc', ['-O', ...(sdk ? ['-sdk', sdk] : []), fonte, '-o', bin], { stdio: 'ignore' }); ok = true; break; } catch { /* próximo SDK */ }
|
|
105
|
+
}
|
|
106
|
+
if (!ok) fail('não foi possível compilar o detector (swiftc + SDK do macOS)');
|
|
107
|
+
}
|
|
108
|
+
} else bin = externo;
|
|
109
|
+
execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', args.video, '-vf', `fps=${fps},scale=${W}:${H}`, '-q:v', '3', join(frames, 'f%04d.jpg')]);
|
|
110
|
+
const arquivos = readdirSync(frames).filter((f) => f.endsWith('.jpg')).sort().map((f) => join(frames, f));
|
|
111
|
+
const res = [];
|
|
112
|
+
for (let i = 0; i < arquivos.length; i += 40) {
|
|
113
|
+
res.push(...JSON.parse(execFileSync(bin, arquivos.slice(i, i + 40), { maxBuffer: 1 << 28 }).toString()));
|
|
100
114
|
}
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
res.forEach((fr, i) => {
|
|
116
|
+
const t = r3(i / fps);
|
|
117
|
+
for (const d of fr.deteccoes) {
|
|
118
|
+
const caixa = { x: d.x, y: d.y, w: d.w, h: d.h };
|
|
119
|
+
if (d.tipo === 'rosto' && d.w < 0.04) continue; // rosto minúsculo ao fundo
|
|
120
|
+
const f = foraDaUtil(caixa);
|
|
121
|
+
if (f <= tol) continue;
|
|
122
|
+
const grande = d.w * d.h > 0.6; // objeto saliente do quadro inteiro: ruído
|
|
123
|
+
if (d.tipo === 'objeto' && grande) continue;
|
|
124
|
+
achados.push({
|
|
125
|
+
camada: 'pixels', gravidade: d.tipo === 'objeto' || d.tipo === 'texto' ? 'aviso' : 'erro', conf: r3(d.conf),
|
|
126
|
+
tipo: d.tipo === 'objeto' ? 'objeto (possível produto/logo)' : d.tipo, tempo: `${t} s`,
|
|
127
|
+
fora: r3(f), regiao: regioes(caixa), caixa, texto: d.texto ?? undefined, frame: fr.arquivo,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
info.push(`${arquivos.length} frames analisados a ${fps} fps (${modoPixels})`);
|
|
132
|
+
}
|
|
103
133
|
}
|
|
104
134
|
|
|
105
|
-
writeFileSync(join(out, 'relatorio.json'), JSON.stringify({ zonas: ZONAS, tolerancia: tol, info, achados }, null, 2));
|
|
135
|
+
writeFileSync(join(out, 'relatorio.json'), JSON.stringify({ zonas: ZONAS, tolerancia: tol, pixels: modoPixels, revisaoVisual: manual, info, achados }, null, 2));
|
|
106
136
|
const erros = achados.filter((a) => a.gravidade === 'erro');
|
|
107
137
|
|
|
108
138
|
// quadros anotados: zonas em vermelho, detecções em amarelo (erro) ou ciano (aviso)
|
|
@@ -112,7 +142,7 @@ for (const [frame, lista] of porFrame) {
|
|
|
112
142
|
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
143
|
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
144
|
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
|
|
145
|
+
try { execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', frame, '-vf', [...zonasF, ...dets].join(','), join(out, 'anotados', `${basename(frame).replace('.jpg', '.png')}`)]); } catch { /* anotação é opcional */ }
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
console.log(`${info.join(' · ')}`);
|
|
@@ -124,5 +154,6 @@ for (const [chave, lista] of grupos) {
|
|
|
124
154
|
const textos = [...new Set(lista.map((a) => a.texto).filter(Boolean))].slice(0, 3);
|
|
125
155
|
console.log(`${grav.toUpperCase().padEnd(5)} ${tipo} → ${regiao} · ${lista.length}x · ${tempos.slice(0, 8).join(', ')}${tempos.length > 8 ? ', …' : ''}${textos.length ? ` · "${textos.join('", "')}"` : ''}`);
|
|
126
156
|
}
|
|
157
|
+
if (manual) console.log(`REVISÃO VISUAL OBRIGATÓRIA: abra ${manual.folhas.map((f) => join(manual.dir, f)).join(', ')} e confira se há rosto, texto, logo ou produto sobre as áreas vermelhas (zonas cinzentas). Registre o resultado no spec.`);
|
|
127
158
|
console.log(`relatório: ${join(out, 'relatorio.json')}${porFrame.size ? ` · quadros anotados: ${join(out, 'anotados')}` : ''}`);
|
|
128
159
|
process.exit(erros.length ? 2 : 0);
|
package/src/doctor.mjs
CHANGED
|
@@ -3,14 +3,25 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { versaoPacote } from "./paths.mjs";
|
|
5
5
|
|
|
6
|
+
const SO = process.platform; // darwin | win32 | linux
|
|
7
|
+
const NOME_SO = { darwin: "macOS", win32: "Windows", linux: "Linux" }[SO] ?? SO;
|
|
8
|
+
|
|
9
|
+
// No Windows, npx e outros são scripts .cmd: precisam de shell. Os argumentos aqui são fixos.
|
|
6
10
|
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(
|
|
11
|
+
const r = spawnSync(c, args, { encoding: "utf8", shell: SO === "win32" });
|
|
12
|
+
return r.error || r.status !== 0 ? null : (r.stdout || r.stderr).trim().split(/\r?\n/)[0];
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
const COMO_INSTALAR = {
|
|
16
|
+
ffmpeg: { darwin: "brew install ffmpeg", win32: "winget install Gyan.FFmpeg (ou choco install ffmpeg)", linux: "sudo apt install ffmpeg" },
|
|
17
|
+
whisper: { darwin: "brew install whisper-cpp", win32: "baixe o whisper.cpp (releases do GitHub) e coloque whisper-cli.exe no PATH; ou use `npx hyperframes models install parakeet`", linux: "compile o whisper.cpp ou use `npx hyperframes models install parakeet`" },
|
|
18
|
+
hyperframes: { darwin: "npm i -D hyperframes", win32: "npm i -D hyperframes", linux: "npm i -D hyperframes" },
|
|
19
|
+
};
|
|
20
|
+
const como = (k) => `instale: ${COMO_INSTALAR[k][SO] ?? COMO_INSTALAR[k].linux}`;
|
|
21
|
+
|
|
11
22
|
export function doctor({ project }) {
|
|
12
23
|
const alvo = resolve(project);
|
|
13
|
-
const linhas = [];
|
|
24
|
+
const linhas = [`Sistema: ${NOME_SO} (${process.arch})`];
|
|
14
25
|
let falhas = 0;
|
|
15
26
|
const item = (ok, nome, detalhe, obrigatorio = true) => {
|
|
16
27
|
if (!ok && obrigatorio) falhas++;
|
|
@@ -20,12 +31,17 @@ export function doctor({ project }) {
|
|
|
20
31
|
const nodeMaior = Number(process.versions.node.split(".")[0]);
|
|
21
32
|
item(nodeMaior >= 22, "Node ≥ 22", process.versions.node);
|
|
22
33
|
const ff = cmd("ffmpeg", ["-version"]);
|
|
23
|
-
item(!!ff, "FFmpeg", ff ?? "
|
|
24
|
-
item(!!cmd("ffprobe", ["-version"]), "FFprobe");
|
|
34
|
+
item(!!ff, "FFmpeg", ff ?? como("ffmpeg"));
|
|
35
|
+
item(!!cmd("ffprobe", ["-version"]), "FFprobe", "vem junto com o FFmpeg");
|
|
25
36
|
const hf = cmd("npx", ["--no-install", "hyperframes", "--version"]);
|
|
26
|
-
item(!!hf, "HyperFrames", hf ?? "
|
|
27
|
-
|
|
28
|
-
item(
|
|
37
|
+
item(!!hf, "HyperFrames", hf ?? `${como("hyperframes")} (dentro do projeto)`);
|
|
38
|
+
const whisper = !!cmd("whisper-cli", ["--help"]) || (SO === "darwin" && existsSync("/opt/homebrew/bin/whisper-cli"));
|
|
39
|
+
item(whisper, "whisper-cli (transcrição)", whisper ? "" : como("whisper"), false);
|
|
40
|
+
|
|
41
|
+
// Verificador de zonas seguras: detecção automática de rosto só no macOS (Vision) ou com detector externo.
|
|
42
|
+
if (process.env.EASYEDIT_DETECTOR) item(existsSync(process.env.EASYEDIT_DETECTOR), "Detector externo de zonas (EASYEDIT_DETECTOR)", process.env.EASYEDIT_DETECTOR, false);
|
|
43
|
+
else if (SO === "darwin") { const sw = !!cmd("swiftc", ["--version"]); item(sw, "swiftc (detector de rosto/texto do verificador de zonas)", sw ? "" : "instale as Xcode Command Line Tools: xcode-select --install", false); }
|
|
44
|
+
else item(true, "Zonas seguras", `sem detecção automática de rosto no ${NOME_SO}: o verificador gera folhas de revisão visual (confira-as no preview) e a camada declarada (elementos.json) segue valendo`);
|
|
29
45
|
|
|
30
46
|
const v = join(alvo, ".easyedit", "version");
|
|
31
47
|
const inst = existsSync(v) ? readFileSync(v, "utf8").trim() : null;
|