@ignix-social/editor 0.1.14 → 0.1.16
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/package.json +1 -1
- package/skills/ignix-editor/SKILL.md +3 -3
- package/src/analysis/review.mjs +5 -6
- package/src/binaries.mjs +35 -4
package/package.json
CHANGED
|
@@ -7,14 +7,14 @@ description: Build and render videos with Ignix Editor (HTML + GSAP compositions
|
|
|
7
7
|
|
|
8
8
|
Ignix Editor turns an HTML page into an MP4. You write `index.html` (layout in HTML/CSS, motion in JS), the editor renders it frame by frame in a headless browser and encodes it with ffmpeg. The Ignix playbooks say WHAT to make; this skill says HOW to write it so that what you write is what renders.
|
|
9
9
|
|
|
10
|
-
Commands
|
|
10
|
+
Commands. `ignix-editor` means the copy the Ignix playbook installed: `~/.ignix-editor/cli/node_modules/.bin/ignix-editor` (Windows PowerShell: `& "$env:USERPROFILE\.ignix-editor\cli\node_modules\.bin\ignix-editor.cmd"`; PowerShell blocks `.ps1` scripts by default, so use the `.cmd` and `npm.cmd`, and `$env:USERPROFILE` instead of `~`). Do not use `npx`: it may fetch another version.
|
|
11
11
|
|
|
12
12
|
| Command | What it does |
|
|
13
13
|
|---|---|
|
|
14
14
|
| `ignix-editor doctor` | Node 22+ and the H.264 encoder; downloads its own ffmpeg and whisper the first time. Run it first. |
|
|
15
15
|
| `ignix-editor init <name>` | Creates `~/ignix-videos/<name>` with a working skeleton, GSAP, local fonts and `ignix-frame.js`. |
|
|
16
16
|
| `ignix-editor transcribe <file> --lang es [--script "…"] -o words.json` | Words with start/end seconds, on this machine. |
|
|
17
|
-
| `ignix-editor faces <video> -o work/faces.json` | Where the face is over time (macOS
|
|
17
|
+
| `ignix-editor faces <video> -o work/faces.json` | Where the face is over time (macOS and Windows; the first run downloads the face detector): a smoothed path `{t, cx, cy, size}` (0–1, top-left origin) to frame the person. |
|
|
18
18
|
| `ignix-editor check [dir]` | HTML well-formed, no network assets, media rules, layout/contrast/motion audit. |
|
|
19
19
|
| `ignix-editor snapshot [dir] --at 0,1.5,3` | PNG frames into `<dir>/snapshots/` to LOOK at. `[dir]` defaults to the current folder. |
|
|
20
20
|
| `ignix-editor render [dir] [-o out.mp4]` | Renders to `<dir>/renders/<dir>.mp4`, audio normalised to −14 LUFS. |
|
|
@@ -147,4 +147,4 @@ Only if the app offers no way, give the user ONE line to paste into Terminal, e.
|
|
|
147
147
|
|
|
148
148
|
1. `ignix-editor check` → 0 errors.
|
|
149
149
|
2. `ignix-editor snapshot --at <every ~0.5 s and every cue>` and LOOK at the frames (or tile them: `ignix-editor ffmpeg -pattern_type glob -i 'snapshots/frame-*.png' -vf "scale=270:480,tile=8x2" sheet.jpg`). Check: hook readable at frame 0, nothing still for >1.5 s, every named thing pointed at on its word, captions never over what is pointed at, nothing outside the safe zone.
|
|
150
|
-
3. `ignix-editor render` → it reviews the MP4 by itself and prints `✓ Review: ready to show` or `✗ Review: NOT READY` with what to fix and where (stretches where nothing on the stage moves, a mostly empty frame, silences between voice lines, no change of scene
|
|
150
|
+
3. `ignix-editor render` → it reviews the MP4 by itself and prints `✓ Review: ready to show` or `✗ Review: NOT READY` with what to fix and where (stretches where nothing on the stage moves, a mostly empty frame, silences between voice lines, no change of scene). NOT READY means it is not finished: fix every ✗, render again, and only then show it. The `!` lines are worth fixing too when it is quick. (`ignix-editor review <mp4>` runs the same check on any video.) Then open the MP4's first frame and a few cues again, and deliver as the playbook says.
|
package/src/analysis/review.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { measureVideo } from './style.mjs';
|
|
|
13
13
|
// encenderse. Los subtítulos solos NO cuentan como movimiento.
|
|
14
14
|
// lleno — qué parte de la zona segura ocupa algo distinto del fondo.
|
|
15
15
|
// huecos — silencios entre frases de la voz.
|
|
16
|
-
// cortes
|
|
16
|
+
// cortes — los de `style measure` (cambios de escena).
|
|
17
17
|
|
|
18
18
|
const FPS = 10;
|
|
19
19
|
/** Fracción de píxeles que tiene que cambiar entre dos fotogramas para contar como movimiento. */
|
|
@@ -212,10 +212,10 @@ export async function reviewVideo({ ffmpeg, ffprobe, path, log = () => {} }) {
|
|
|
212
212
|
if (m?.shots?.cutsPerMinute === 0 && info.duration > 10) {
|
|
213
213
|
issues.push({ level: 'warn', code: 'no_cuts', text: 'No real change of scene anywhere: it plays as one slow sequence of fades. Change scene with cuts or quick whips, and move a camera inside each scene.' });
|
|
214
214
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
// Sin aviso de efectos de sonido (2026-10-07): bajo una voz continua no se
|
|
216
|
+
// distinguen. En un reel de Claude con 8 efectos dijo «no se oye ninguno»
|
|
217
|
+
// (estaban, a −18,6 dB en los huecos). Un aviso que falla en lo bueno enseña
|
|
218
|
+
// al agente a ignorar la revisión; los efectos siguen en el manual.
|
|
219
219
|
return {
|
|
220
220
|
path,
|
|
221
221
|
durationSec: r1(info.duration),
|
|
@@ -225,7 +225,6 @@ export async function reviewVideo({ ffmpeg, ffprobe, path, log = () => {} }) {
|
|
|
225
225
|
longestStillSec: r1(Math.max(0, ...stillRuns(steps, 0).map((s) => s.end - s.start))),
|
|
226
226
|
fill: fill === null ? null : Math.round(fill * 100) / 100,
|
|
227
227
|
textSize: m?.captions?.size ?? null,
|
|
228
|
-
sfxPerMinute: sfx ?? null,
|
|
229
228
|
cutsPerMinute: m?.shots?.cutsPerMinute ?? null,
|
|
230
229
|
},
|
|
231
230
|
issues,
|
package/src/binaries.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (descarga oficial de Google la primera vez).
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { Readable } from 'node:stream';
|
|
7
|
+
import { Readable, Transform } from 'node:stream';
|
|
8
8
|
import { pipeline } from 'node:stream/promises';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
@@ -75,9 +75,40 @@ export async function ensureWhisperModel(model = 'medium', log = () => {}) {
|
|
|
75
75
|
if (existsSync(dest)) return dest;
|
|
76
76
|
mkdirSync(dir, { recursive: true });
|
|
77
77
|
log(`Downloading the ${model} transcription model (${(m.size / 1e9).toFixed(1)} GB, first time only)…`);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Con avance y con freno (2026-10-07): en las pruebas de Windows esta descarga tardó 52 min
|
|
79
|
+
// sin decir nada, y un agente que no ve avance cree que se ha colgado y la corta. Se avisa
|
|
80
|
+
// cada 10 % y, si en 2 min no llega ni un byte, se para con un error claro.
|
|
81
|
+
const ctrl = new AbortController();
|
|
82
|
+
const STALL_MS = 120_000;
|
|
83
|
+
let stall = setTimeout(() => ctrl.abort(), STALL_MS);
|
|
84
|
+
let got = 0;
|
|
85
|
+
let next = 10;
|
|
86
|
+
const progress = new Transform({
|
|
87
|
+
transform(chunk, _enc, cb) {
|
|
88
|
+
clearTimeout(stall);
|
|
89
|
+
stall = setTimeout(() => ctrl.abort(), STALL_MS);
|
|
90
|
+
got += chunk.length;
|
|
91
|
+
const pct = Math.floor((got / m.size) * 100);
|
|
92
|
+
if (pct >= next && pct < 100) {
|
|
93
|
+
log(` ${pct} % (${Math.round(got / 1e6)} of ${Math.round(m.size / 1e6)} MB)`);
|
|
94
|
+
next = Math.floor(pct / 10) * 10 + 10;
|
|
95
|
+
}
|
|
96
|
+
cb(null, chunk);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${m.file}`, { signal: ctrl.signal });
|
|
101
|
+
if (!res.ok) throw new Error(`Could not download the ${model} model: ${res.status}`);
|
|
102
|
+
await pipeline(Readable.fromWeb(res.body), progress, createWriteStream(`${dest}.part`));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
rmSync(`${dest}.part`, { force: true });
|
|
105
|
+
if (ctrl.signal.aborted) {
|
|
106
|
+
throw new Error(`The ${model} transcription model download stalled (no data for 2 minutes). Check the internet connection and run the command again; it starts over.`);
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(stall);
|
|
111
|
+
}
|
|
81
112
|
// El sha256 es el que publica Hugging Face para ese fichero (su oid de LFS).
|
|
82
113
|
const h = createHash('sha256');
|
|
83
114
|
await pipeline(createReadStream(`${dest}.part`), h);
|