editorbrain 0.32.0 → 0.34.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/README.md +2 -2
- package/bin/editorbrain.mjs +415 -254
- package/package.json +1 -1
- package/skill/editorbrain/SKILL.md +3 -1
- package/skill/editorbrain/references/production-continuity.md +93 -0
- package/skill/editorbrain-formats/SKILL.md +4 -3
- package/skill/editorbrain-formats/references/aspirational-list-demo.md +140 -0
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Editorial references, brand context and portable video projects for Claude Code
|
|
|
5
5
|
Inspect the CLI without configuring your agent:
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx editorbrain@0.
|
|
8
|
+
npx editorbrain@0.34.0 help
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
For a persistent installation:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npm install -g editorbrain@0.
|
|
14
|
+
npm install -g editorbrain@0.34.0
|
|
15
15
|
editorbrain login --url https://api.editorbrain.com
|
|
16
16
|
editorbrain status --url https://api.editorbrain.com
|
|
17
17
|
editorbrain projects list
|
package/bin/editorbrain.mjs
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// packages/cli/src/sfx.ts
|
|
4
|
-
import { readdir as readdir2, lstat as lstat3 } from "node:fs/promises";
|
|
5
|
-
import { join as join2, extname as extname2, basename } from "node:path";
|
|
6
|
-
|
|
7
3
|
// packages/core/src/creative-direction.ts
|
|
8
4
|
var creativePreferenceSections = [
|
|
9
5
|
{ id: "brand", title: "Marca y tipograf\xEDa", question: "\xBFConservamos tus colores, fuentes e idioma, o quieres ajustarlos?" },
|
|
@@ -57125,13 +57121,6 @@ function canonicalFiles(files) {
|
|
|
57125
57121
|
return result2;
|
|
57126
57122
|
}
|
|
57127
57123
|
|
|
57128
|
-
// packages/cli/src/media-runtime.ts
|
|
57129
|
-
import { spawn } from "node:child_process";
|
|
57130
|
-
import { createHash } from "node:crypto";
|
|
57131
|
-
import { constants } from "node:fs";
|
|
57132
|
-
import { lstat, open, realpath } from "node:fs/promises";
|
|
57133
|
-
import { dirname, extname, parse, resolve } from "node:path";
|
|
57134
|
-
|
|
57135
57124
|
// packages/cli/src/errors.ts
|
|
57136
57125
|
var CliError = class extends Error {
|
|
57137
57126
|
constructor(code, message, hint, retryable = false, details = {}) {
|
|
@@ -57154,7 +57143,346 @@ function errorOutput(error) {
|
|
|
57154
57143
|
return { error: safe, code: "COMMAND_FAILED", retryable: false, hint: "Read the command help and run editorbrain doctor. Do not retry writes blindly." };
|
|
57155
57144
|
}
|
|
57156
57145
|
|
|
57146
|
+
// packages/cli/src/production-output.ts
|
|
57147
|
+
import { constants as constants2 } from "node:fs";
|
|
57148
|
+
import { mkdir as mkdir2, open as open2 } from "node:fs/promises";
|
|
57149
|
+
import { dirname as dirname2 } from "node:path";
|
|
57150
|
+
|
|
57151
|
+
// packages/cli/src/io.ts
|
|
57152
|
+
import { constants } from "node:fs";
|
|
57153
|
+
import { lstat, mkdir, open, readdir, realpath, rename, unlink, link } from "node:fs/promises";
|
|
57154
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
57155
|
+
import { randomUUID } from "node:crypto";
|
|
57156
|
+
async function ensureRoot(directory, create = false) {
|
|
57157
|
+
const root = resolve(directory);
|
|
57158
|
+
if (create) await mkdir(root, { recursive: true });
|
|
57159
|
+
const info = await lstat(root);
|
|
57160
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Workspace root must be a real directory, not a symlink");
|
|
57161
|
+
return realpath(root);
|
|
57162
|
+
}
|
|
57163
|
+
async function checkedPath(root, path, document2 = true) {
|
|
57164
|
+
if (document2) safeDocumentPath(path);
|
|
57165
|
+
const target = resolve(root, path);
|
|
57166
|
+
const rel = relative(root, target);
|
|
57167
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve(path) === path) throw new Error("Path escapes workspace");
|
|
57168
|
+
let cursor = root;
|
|
57169
|
+
for (const segment of rel.split(sep)) {
|
|
57170
|
+
cursor = join(cursor, segment);
|
|
57171
|
+
try {
|
|
57172
|
+
const info = await lstat(cursor);
|
|
57173
|
+
if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
|
|
57174
|
+
} catch (error) {
|
|
57175
|
+
if (error.code !== "ENOENT") throw error;
|
|
57176
|
+
}
|
|
57177
|
+
}
|
|
57178
|
+
return target;
|
|
57179
|
+
}
|
|
57180
|
+
async function readText(root, path, document2 = true) {
|
|
57181
|
+
const target = await checkedPath(root, path, document2);
|
|
57182
|
+
const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
57183
|
+
try {
|
|
57184
|
+
const info = await handle.stat();
|
|
57185
|
+
if (!info.isFile()) throw new Error(`Not a regular file: ${path}`);
|
|
57186
|
+
if (document2 && info.size > MAX_DOCUMENT_BYTES) throw new Error(`Document exceeds 128 KiB: ${path}`);
|
|
57187
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(await handle.readFile());
|
|
57188
|
+
} finally {
|
|
57189
|
+
await handle.close();
|
|
57190
|
+
}
|
|
57191
|
+
}
|
|
57192
|
+
async function atomicWrite(root, path, content, options = {}) {
|
|
57193
|
+
const target = await checkedPath(root, path, options.document ?? true);
|
|
57194
|
+
await mkdir(dirname(target), { recursive: true });
|
|
57195
|
+
await checkedPath(root, path, options.document ?? true);
|
|
57196
|
+
const temporary = join(dirname(target), `.eh-${randomUUID()}.tmp`);
|
|
57197
|
+
const handle = await open(temporary, "wx", options.mode ?? 420);
|
|
57198
|
+
try {
|
|
57199
|
+
await handle.writeFile(content, "utf8");
|
|
57200
|
+
await handle.close();
|
|
57201
|
+
await checkedPath(root, path, options.document ?? true);
|
|
57202
|
+
if (options.overwrite === false) {
|
|
57203
|
+
await link(temporary, target);
|
|
57204
|
+
await unlink(temporary);
|
|
57205
|
+
} else await rename(temporary, target);
|
|
57206
|
+
} catch (error) {
|
|
57207
|
+
await handle.close().catch(() => {
|
|
57208
|
+
});
|
|
57209
|
+
await unlink(temporary).catch(() => {
|
|
57210
|
+
});
|
|
57211
|
+
throw error;
|
|
57212
|
+
}
|
|
57213
|
+
}
|
|
57214
|
+
async function readWorkspace(root) {
|
|
57215
|
+
const files = /* @__PURE__ */ Object.create(null);
|
|
57216
|
+
async function walk(path) {
|
|
57217
|
+
const directory = await checkedPath(root, path, false);
|
|
57218
|
+
let entries;
|
|
57219
|
+
try {
|
|
57220
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
57221
|
+
} catch (error) {
|
|
57222
|
+
if (error.code === "ENOENT") return;
|
|
57223
|
+
throw error;
|
|
57224
|
+
}
|
|
57225
|
+
for (const entry of entries) {
|
|
57226
|
+
const child = `${path}/${entry.name}`;
|
|
57227
|
+
if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${child}`);
|
|
57228
|
+
if (entry.name.startsWith(".")) continue;
|
|
57229
|
+
if (entry.isDirectory()) {
|
|
57230
|
+
if (path.startsWith("videos/") && ["assets", "renders"].includes(entry.name)) continue;
|
|
57231
|
+
await walk(child);
|
|
57232
|
+
} else if (entry.isFile() && /\.(json|md)$/.test(entry.name)) {
|
|
57233
|
+
safeDocumentPath(child);
|
|
57234
|
+
files[child] = await readText(root, child);
|
|
57235
|
+
}
|
|
57236
|
+
}
|
|
57237
|
+
}
|
|
57238
|
+
await walk(".editorharness");
|
|
57239
|
+
await walk(".editorbrain");
|
|
57240
|
+
await walk("videos");
|
|
57241
|
+
return canonicalFiles(files);
|
|
57242
|
+
}
|
|
57243
|
+
async function readProjectWorkspace(root, projectId) {
|
|
57244
|
+
assertIdentifier(projectId);
|
|
57245
|
+
const projectPath = `videos/${projectId}/project.json`;
|
|
57246
|
+
const projectContent = await readText(root, projectPath);
|
|
57247
|
+
const checked = validateDocument(projectPath, projectContent);
|
|
57248
|
+
if (!checked.valid) throw new ValidationError(checked.issues);
|
|
57249
|
+
const project = JSON.parse(projectContent);
|
|
57250
|
+
const files = { [projectPath]: projectContent };
|
|
57251
|
+
for (const path of /* @__PURE__ */ new Set([...projectSources(project), ...optionalProjectSources(project)])) {
|
|
57252
|
+
if (path === projectPath) continue;
|
|
57253
|
+
const aliases2 = path.startsWith(".editorbrain/") ? [path, path.replace(/^\.editorbrain\//, ".editorharness/")] : [path];
|
|
57254
|
+
for (const alias of aliases2) {
|
|
57255
|
+
try {
|
|
57256
|
+
files[alias] = await readText(root, alias);
|
|
57257
|
+
} catch (error) {
|
|
57258
|
+
if (error.code !== "ENOENT") throw error;
|
|
57259
|
+
}
|
|
57260
|
+
}
|
|
57261
|
+
}
|
|
57262
|
+
if (await readText(root, projectPath) !== projectContent) throw new Error("The selected project changed while reading its context. Retry before editing.");
|
|
57263
|
+
return canonicalFiles(files);
|
|
57264
|
+
}
|
|
57265
|
+
|
|
57266
|
+
// packages/cli/src/production-output.ts
|
|
57267
|
+
async function productionOutputPath(root, relativePath) {
|
|
57268
|
+
if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
|
|
57269
|
+
throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
|
|
57270
|
+
}
|
|
57271
|
+
return checkedPath(root, relativePath, false);
|
|
57272
|
+
}
|
|
57273
|
+
async function prepareSampleDirectory(root, path) {
|
|
57274
|
+
const target = await productionOutputPath(root, path);
|
|
57275
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
57276
|
+
await productionOutputPath(root, path);
|
|
57277
|
+
return target;
|
|
57278
|
+
}
|
|
57279
|
+
async function readProductionJson(root, path) {
|
|
57280
|
+
const target = await checkedPath(root, path, false);
|
|
57281
|
+
const handle = await open2(target, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
|
|
57282
|
+
try {
|
|
57283
|
+
const stat = await handle.stat();
|
|
57284
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
|
|
57285
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
57286
|
+
} finally {
|
|
57287
|
+
await handle.close();
|
|
57288
|
+
}
|
|
57289
|
+
}
|
|
57290
|
+
async function saveProductionReport(root, path, report) {
|
|
57291
|
+
const target = await productionOutputPath(root, path);
|
|
57292
|
+
if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
|
|
57293
|
+
await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
|
|
57294
|
+
return target;
|
|
57295
|
+
}
|
|
57296
|
+
function summarizeProductionReport(value) {
|
|
57297
|
+
const r = value;
|
|
57298
|
+
const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
|
|
57299
|
+
if (r.kind === "editorbrain.media-inventory") return {
|
|
57300
|
+
...base,
|
|
57301
|
+
complete: r.complete,
|
|
57302
|
+
contentReviewed: false,
|
|
57303
|
+
counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
|
|
57304
|
+
assets: r.assets.slice(0, 8),
|
|
57305
|
+
failures: r.failures.slice(0, 5),
|
|
57306
|
+
omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
|
|
57307
|
+
next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
|
|
57308
|
+
};
|
|
57309
|
+
if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
|
|
57310
|
+
if (r.kind === "editorbrain.production-plan-check") return {
|
|
57311
|
+
...base,
|
|
57312
|
+
valid: r.valid,
|
|
57313
|
+
inventoryComplete: r.inventoryComplete,
|
|
57314
|
+
observations: r.observations,
|
|
57315
|
+
sourceFilesReverified: false,
|
|
57316
|
+
counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
|
|
57317
|
+
findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
|
|
57318
|
+
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
57319
|
+
next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
|
|
57320
|
+
};
|
|
57321
|
+
if (r.kind === "editorbrain.render-check") return {
|
|
57322
|
+
...base,
|
|
57323
|
+
complete: r.complete,
|
|
57324
|
+
technicalStatus: r.technicalStatus,
|
|
57325
|
+
file: r.file,
|
|
57326
|
+
checks: r.checks,
|
|
57327
|
+
findings: r.findings.slice(0, 8),
|
|
57328
|
+
findingsSummary: r.findingsSummary,
|
|
57329
|
+
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
57330
|
+
next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
|
|
57331
|
+
};
|
|
57332
|
+
if (r.kind === "editorbrain.media-source-search") return {
|
|
57333
|
+
...base,
|
|
57334
|
+
complete: r.complete,
|
|
57335
|
+
results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
|
|
57336
|
+
metadataAbbreviated: true,
|
|
57337
|
+
failures: r.failures.slice(0, 5),
|
|
57338
|
+
counts: r.counts,
|
|
57339
|
+
omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
|
|
57340
|
+
searchExhaustive: false,
|
|
57341
|
+
rightsVerified: false,
|
|
57342
|
+
next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
|
|
57343
|
+
};
|
|
57344
|
+
if (r.kind === "editorbrain.evidence-check") return {
|
|
57345
|
+
...base,
|
|
57346
|
+
valid: r.valid,
|
|
57347
|
+
inventoryComplete: r.inventoryComplete,
|
|
57348
|
+
observations: r.observations,
|
|
57349
|
+
sourceFilesReverified: false,
|
|
57350
|
+
factualTruth: "not_assessed",
|
|
57351
|
+
reuseRights: "not_assessed",
|
|
57352
|
+
findings: r.findings.slice(0, 8),
|
|
57353
|
+
omitted: { findings: Math.max(0, r.findings.length - 8) }
|
|
57354
|
+
};
|
|
57355
|
+
if (r.kind === "editorbrain.audio-check") return {
|
|
57356
|
+
...base,
|
|
57357
|
+
complete: r.complete,
|
|
57358
|
+
technicalStatus: r.technicalStatus,
|
|
57359
|
+
file: r.file,
|
|
57360
|
+
fileStable: r.fileStable,
|
|
57361
|
+
coverage: r.coverage,
|
|
57362
|
+
measurements: r.measurements.slice(0, 8),
|
|
57363
|
+
errorCode: r.errorCode,
|
|
57364
|
+
creativeQuality: r.creativeQuality,
|
|
57365
|
+
omitted: { measurements: Math.max(0, r.measurements.length - 8) },
|
|
57366
|
+
next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
|
|
57367
|
+
};
|
|
57368
|
+
if (r.kind === "editorbrain.motion-review") return {
|
|
57369
|
+
...base,
|
|
57370
|
+
samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
|
|
57371
|
+
geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
|
|
57372
|
+
omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
|
|
57373
|
+
next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
|
|
57374
|
+
};
|
|
57375
|
+
throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
|
|
57376
|
+
}
|
|
57377
|
+
|
|
57378
|
+
// packages/cli/src/editing-batches.ts
|
|
57379
|
+
async function batchCommand(client, root, command, argument, flags) {
|
|
57380
|
+
const fail2 = () => {
|
|
57381
|
+
throw new CliError(
|
|
57382
|
+
"INVALID_ARGUMENT",
|
|
57383
|
+
"Use batches list/get/create/pause/resume/cancel/retry/detach/claim/renew/release/attention.",
|
|
57384
|
+
"Create with --file batch.json; updates need --expected-revision; leases need --project and --lease-token UUID."
|
|
57385
|
+
);
|
|
57386
|
+
};
|
|
57387
|
+
const lease = ["claim", "renew", "release", "attention"].includes(command), update = ["pause", "resume", "cancel", "retry", "detach"].includes(command);
|
|
57388
|
+
if (!["list", "get", "create"].includes(command) && !lease && !update) fail2();
|
|
57389
|
+
const allowed = [
|
|
57390
|
+
"dir",
|
|
57391
|
+
"url",
|
|
57392
|
+
...command === "create" ? ["file"] : command === "list" ? ["after"] : lease ? ["project", "lease-token"] : update ? [
|
|
57393
|
+
"expected-revision",
|
|
57394
|
+
...["retry", "detach"].includes(command) ? ["project"] : []
|
|
57395
|
+
] : []
|
|
57396
|
+
];
|
|
57397
|
+
for (const key of Object.keys(flags)) if (!allowed.includes(key)) fail2();
|
|
57398
|
+
if (command === "list") {
|
|
57399
|
+
if (argument) fail2();
|
|
57400
|
+
return client.request(
|
|
57401
|
+
"/v1/batches" + (typeof flags.after === "string" ? "?after=" + encodeURIComponent(flags.after) : "")
|
|
57402
|
+
);
|
|
57403
|
+
}
|
|
57404
|
+
if (command === "create") {
|
|
57405
|
+
if (argument || typeof flags.file !== "string") fail2();
|
|
57406
|
+
const body = await readProductionJson(root, flags.file);
|
|
57407
|
+
return client.request("/v1/batches", { method: "POST", body });
|
|
57408
|
+
}
|
|
57409
|
+
if (!argument || !/^[a-f0-9-]{36}$/i.test(argument)) fail2();
|
|
57410
|
+
const path = "/v1/batches/" + argument;
|
|
57411
|
+
if (command === "get") return client.request(path);
|
|
57412
|
+
if (update) {
|
|
57413
|
+
const revision = Number(flags["expected-revision"]);
|
|
57414
|
+
if (!Number.isSafeInteger(revision) || revision < 1 || ["retry", "detach"].includes(command) && typeof flags.project !== "string")
|
|
57415
|
+
fail2();
|
|
57416
|
+
return client.request(path, {
|
|
57417
|
+
method: "POST",
|
|
57418
|
+
body: {
|
|
57419
|
+
action: command,
|
|
57420
|
+
expectedRevision: revision,
|
|
57421
|
+
...["retry", "detach"].includes(command) ? { projectId: flags.project } : {}
|
|
57422
|
+
}
|
|
57423
|
+
});
|
|
57424
|
+
}
|
|
57425
|
+
if (typeof flags.project !== "string" || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(flags.project) || typeof flags["lease-token"] !== "string" || !/^[a-f0-9-]{36}$/i.test(flags["lease-token"]))
|
|
57426
|
+
fail2();
|
|
57427
|
+
return client.request(path + "/lease", {
|
|
57428
|
+
method: "POST",
|
|
57429
|
+
body: {
|
|
57430
|
+
action: command,
|
|
57431
|
+
projectId: flags.project,
|
|
57432
|
+
leaseToken: flags["lease-token"]
|
|
57433
|
+
}
|
|
57434
|
+
});
|
|
57435
|
+
}
|
|
57436
|
+
|
|
57437
|
+
// packages/cli/src/project-progress-client.ts
|
|
57438
|
+
async function projectProgressCommand(client, command, flags) {
|
|
57439
|
+
const fail2 = (message) => {
|
|
57440
|
+
throw new CliError(
|
|
57441
|
+
"INVALID_ARGUMENT",
|
|
57442
|
+
message,
|
|
57443
|
+
"Use projects progress/start/queue/complete --project ID. Read progress first; pass --expected-version HASH for changes."
|
|
57444
|
+
);
|
|
57445
|
+
};
|
|
57446
|
+
if (!["progress", "start", "queue", "complete"].includes(command))
|
|
57447
|
+
fail2("Unknown project command.");
|
|
57448
|
+
const allowed = [
|
|
57449
|
+
"project",
|
|
57450
|
+
"url",
|
|
57451
|
+
"dir",
|
|
57452
|
+
...command === "progress" ? [] : ["expected-version"],
|
|
57453
|
+
...command === "complete" ? ["revision", "user-confirmed"] : []
|
|
57454
|
+
];
|
|
57455
|
+
for (const name of Object.keys(flags))
|
|
57456
|
+
if (!allowed.includes(name)) fail2(`Unsupported flag --${name}.`);
|
|
57457
|
+
if (typeof flags.project !== "string") fail2("--project is required.");
|
|
57458
|
+
const project = assertIdentifier(flags.project), path = `/v1/projects/${encodeURIComponent(project)}/progress`;
|
|
57459
|
+
if (command === "progress") return client.request(path);
|
|
57460
|
+
if (typeof flags["expected-version"] !== "string" || !/^[a-f0-9]{64}$/.test(flags["expected-version"]))
|
|
57461
|
+
fail2("--expected-version must be the current progress version.");
|
|
57462
|
+
if (command === "complete" && (typeof flags.revision !== "string" || !/^[a-f0-9-]{36}$/.test(flags.revision) || flags["user-confirmed"] !== true))
|
|
57463
|
+
fail2(
|
|
57464
|
+
"Completion requires --revision UUID and --user-confirmed after explicit final-video approval."
|
|
57465
|
+
);
|
|
57466
|
+
return client.request(path, {
|
|
57467
|
+
method: "POST",
|
|
57468
|
+
body: {
|
|
57469
|
+
action: command,
|
|
57470
|
+
expectedVersion: flags["expected-version"],
|
|
57471
|
+
...command === "complete" ? { revisionId: flags.revision, userConfirmed: true } : {}
|
|
57472
|
+
}
|
|
57473
|
+
});
|
|
57474
|
+
}
|
|
57475
|
+
|
|
57476
|
+
// packages/cli/src/sfx.ts
|
|
57477
|
+
import { readdir as readdir2, lstat as lstat3 } from "node:fs/promises";
|
|
57478
|
+
import { join as join2, extname as extname2, basename } from "node:path";
|
|
57479
|
+
|
|
57157
57480
|
// packages/cli/src/media-runtime.ts
|
|
57481
|
+
import { spawn } from "node:child_process";
|
|
57482
|
+
import { createHash } from "node:crypto";
|
|
57483
|
+
import { constants as constants3 } from "node:fs";
|
|
57484
|
+
import { lstat as lstat2, open as open3, realpath as realpath2 } from "node:fs/promises";
|
|
57485
|
+
import { dirname as dirname3, extname, parse, resolve as resolve2 } from "node:path";
|
|
57158
57486
|
var MediaToolError = class extends CliError {
|
|
57159
57487
|
constructor(code, message) {
|
|
57160
57488
|
super(code, message, code === "MEDIA_TOOL_NOT_FOUND" ? "Install FFmpeg/FFprobe separately and make them available on PATH; no account or model is needed." : "Review the local input and reported limit. An incomplete check does not validate the media.");
|
|
@@ -57206,24 +57534,24 @@ function resolveLocalMediaPath(value) {
|
|
|
57206
57534
|
if (!value || value.includes("\0") || /[\r\n]/.test(value) || /^[a-z][a-z\d+.-]*:/i.test(value) && !(process.platform === "win32" && /^[a-z]:[\\/]/i.test(value)) || value.startsWith("\\\\")) {
|
|
57207
57535
|
throw new MediaToolError("MEDIA_LOCAL_PATH_REQUIRED", "Provide a local filesystem path, not a URL, protocol or network share.");
|
|
57208
57536
|
}
|
|
57209
|
-
return
|
|
57537
|
+
return resolve2(value);
|
|
57210
57538
|
}
|
|
57211
57539
|
async function assertNoMediaSymlinks(value) {
|
|
57212
57540
|
const path = resolveLocalMediaPath(value);
|
|
57213
57541
|
let cursor = path;
|
|
57214
57542
|
while (cursor !== parse(cursor).root) {
|
|
57215
|
-
const info = await
|
|
57543
|
+
const info = await lstat2(cursor);
|
|
57216
57544
|
if (info.isSymbolicLink()) {
|
|
57217
|
-
const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await
|
|
57545
|
+
const systemAlias = process.platform === "darwin" && (cursor === "/tmp" || cursor === "/var") && await realpath2(cursor) === `/private${cursor}`;
|
|
57218
57546
|
if (!systemAlias) throw new MediaToolError("MEDIA_SYMLINK", "Symlink media paths are not followed. Use the original local path.");
|
|
57219
57547
|
}
|
|
57220
|
-
cursor =
|
|
57548
|
+
cursor = dirname3(cursor);
|
|
57221
57549
|
}
|
|
57222
57550
|
return path;
|
|
57223
57551
|
}
|
|
57224
57552
|
async function assertLocalMediaFile(value) {
|
|
57225
57553
|
const path = await assertNoMediaSymlinks(value);
|
|
57226
|
-
const info = await
|
|
57554
|
+
const info = await lstat2(path);
|
|
57227
57555
|
if (!info.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
|
|
57228
57556
|
if (!SUPPORTED_MEDIA_EXTENSIONS.has(extname(path).toLowerCase())) throw new MediaToolError("MEDIA_FORMAT_UNSUPPORTED", "This file extension is not supported for local media inspection.");
|
|
57229
57557
|
return path;
|
|
@@ -57308,7 +57636,7 @@ var rate2 = (value) => {
|
|
|
57308
57636
|
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
57309
57637
|
async function probeLocalMedia(value) {
|
|
57310
57638
|
const path = await assertLocalMediaFile(value);
|
|
57311
|
-
const handle = await
|
|
57639
|
+
const handle = await open3(path, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
|
|
57312
57640
|
try {
|
|
57313
57641
|
const before = await handle.stat();
|
|
57314
57642
|
if (!before.isFile()) throw new MediaToolError("MEDIA_NOT_REGULAR", "The media input must be a regular local file.");
|
|
@@ -57318,7 +57646,7 @@ async function probeLocalMedia(value) {
|
|
|
57318
57646
|
for await (const chunk of handle.createReadStream({ autoClose: false })) hash6.update(chunk);
|
|
57319
57647
|
const after = await handle.stat();
|
|
57320
57648
|
await assertNoMediaSymlinks(path);
|
|
57321
|
-
const current2 = await
|
|
57649
|
+
const current2 = await lstat2(path);
|
|
57322
57650
|
if (!sameFile(before, after) || !sameFile(after, current2)) throw new MediaToolError("MEDIA_FILE_CHANGED", "The media file changed while it was being inspected. Retry with a stable source.");
|
|
57323
57651
|
let raw;
|
|
57324
57652
|
try {
|
|
@@ -57346,121 +57674,6 @@ async function probeLocalMedia(value) {
|
|
|
57346
57674
|
}
|
|
57347
57675
|
}
|
|
57348
57676
|
|
|
57349
|
-
// packages/cli/src/io.ts
|
|
57350
|
-
import { constants as constants2 } from "node:fs";
|
|
57351
|
-
import { lstat as lstat2, mkdir, open as open2, readdir, realpath as realpath2, rename, unlink, link } from "node:fs/promises";
|
|
57352
|
-
import { dirname as dirname2, join, relative, resolve as resolve2, sep } from "node:path";
|
|
57353
|
-
import { randomUUID } from "node:crypto";
|
|
57354
|
-
async function ensureRoot(directory, create = false) {
|
|
57355
|
-
const root = resolve2(directory);
|
|
57356
|
-
if (create) await mkdir(root, { recursive: true });
|
|
57357
|
-
const info = await lstat2(root);
|
|
57358
|
-
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Workspace root must be a real directory, not a symlink");
|
|
57359
|
-
return realpath2(root);
|
|
57360
|
-
}
|
|
57361
|
-
async function checkedPath(root, path, document2 = true) {
|
|
57362
|
-
if (document2) safeDocumentPath(path);
|
|
57363
|
-
const target = resolve2(root, path);
|
|
57364
|
-
const rel = relative(root, target);
|
|
57365
|
-
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve2(path) === path) throw new Error("Path escapes workspace");
|
|
57366
|
-
let cursor = root;
|
|
57367
|
-
for (const segment of rel.split(sep)) {
|
|
57368
|
-
cursor = join(cursor, segment);
|
|
57369
|
-
try {
|
|
57370
|
-
const info = await lstat2(cursor);
|
|
57371
|
-
if (info.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${path}`);
|
|
57372
|
-
} catch (error) {
|
|
57373
|
-
if (error.code !== "ENOENT") throw error;
|
|
57374
|
-
}
|
|
57375
|
-
}
|
|
57376
|
-
return target;
|
|
57377
|
-
}
|
|
57378
|
-
async function readText(root, path, document2 = true) {
|
|
57379
|
-
const target = await checkedPath(root, path, document2);
|
|
57380
|
-
const handle = await open2(target, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
57381
|
-
try {
|
|
57382
|
-
const info = await handle.stat();
|
|
57383
|
-
if (!info.isFile()) throw new Error(`Not a regular file: ${path}`);
|
|
57384
|
-
if (document2 && info.size > MAX_DOCUMENT_BYTES) throw new Error(`Document exceeds 128 KiB: ${path}`);
|
|
57385
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(await handle.readFile());
|
|
57386
|
-
} finally {
|
|
57387
|
-
await handle.close();
|
|
57388
|
-
}
|
|
57389
|
-
}
|
|
57390
|
-
async function atomicWrite(root, path, content, options = {}) {
|
|
57391
|
-
const target = await checkedPath(root, path, options.document ?? true);
|
|
57392
|
-
await mkdir(dirname2(target), { recursive: true });
|
|
57393
|
-
await checkedPath(root, path, options.document ?? true);
|
|
57394
|
-
const temporary = join(dirname2(target), `.eh-${randomUUID()}.tmp`);
|
|
57395
|
-
const handle = await open2(temporary, "wx", options.mode ?? 420);
|
|
57396
|
-
try {
|
|
57397
|
-
await handle.writeFile(content, "utf8");
|
|
57398
|
-
await handle.close();
|
|
57399
|
-
await checkedPath(root, path, options.document ?? true);
|
|
57400
|
-
if (options.overwrite === false) {
|
|
57401
|
-
await link(temporary, target);
|
|
57402
|
-
await unlink(temporary);
|
|
57403
|
-
} else await rename(temporary, target);
|
|
57404
|
-
} catch (error) {
|
|
57405
|
-
await handle.close().catch(() => {
|
|
57406
|
-
});
|
|
57407
|
-
await unlink(temporary).catch(() => {
|
|
57408
|
-
});
|
|
57409
|
-
throw error;
|
|
57410
|
-
}
|
|
57411
|
-
}
|
|
57412
|
-
async function readWorkspace(root) {
|
|
57413
|
-
const files = /* @__PURE__ */ Object.create(null);
|
|
57414
|
-
async function walk(path) {
|
|
57415
|
-
const directory = await checkedPath(root, path, false);
|
|
57416
|
-
let entries;
|
|
57417
|
-
try {
|
|
57418
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
57419
|
-
} catch (error) {
|
|
57420
|
-
if (error.code === "ENOENT") return;
|
|
57421
|
-
throw error;
|
|
57422
|
-
}
|
|
57423
|
-
for (const entry of entries) {
|
|
57424
|
-
const child = `${path}/${entry.name}`;
|
|
57425
|
-
if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed: ${child}`);
|
|
57426
|
-
if (entry.name.startsWith(".")) continue;
|
|
57427
|
-
if (entry.isDirectory()) {
|
|
57428
|
-
if (path.startsWith("videos/") && ["assets", "renders"].includes(entry.name)) continue;
|
|
57429
|
-
await walk(child);
|
|
57430
|
-
} else if (entry.isFile() && /\.(json|md)$/.test(entry.name)) {
|
|
57431
|
-
safeDocumentPath(child);
|
|
57432
|
-
files[child] = await readText(root, child);
|
|
57433
|
-
}
|
|
57434
|
-
}
|
|
57435
|
-
}
|
|
57436
|
-
await walk(".editorharness");
|
|
57437
|
-
await walk(".editorbrain");
|
|
57438
|
-
await walk("videos");
|
|
57439
|
-
return canonicalFiles(files);
|
|
57440
|
-
}
|
|
57441
|
-
async function readProjectWorkspace(root, projectId) {
|
|
57442
|
-
assertIdentifier(projectId);
|
|
57443
|
-
const projectPath = `videos/${projectId}/project.json`;
|
|
57444
|
-
const projectContent = await readText(root, projectPath);
|
|
57445
|
-
const checked = validateDocument(projectPath, projectContent);
|
|
57446
|
-
if (!checked.valid) throw new ValidationError(checked.issues);
|
|
57447
|
-
const project = JSON.parse(projectContent);
|
|
57448
|
-
const files = { [projectPath]: projectContent };
|
|
57449
|
-
for (const path of /* @__PURE__ */ new Set([...projectSources(project), ...optionalProjectSources(project)])) {
|
|
57450
|
-
if (path === projectPath) continue;
|
|
57451
|
-
const aliases2 = path.startsWith(".editorbrain/") ? [path, path.replace(/^\.editorbrain\//, ".editorharness/")] : [path];
|
|
57452
|
-
for (const alias of aliases2) {
|
|
57453
|
-
try {
|
|
57454
|
-
files[alias] = await readText(root, alias);
|
|
57455
|
-
} catch (error) {
|
|
57456
|
-
if (error.code !== "ENOENT") throw error;
|
|
57457
|
-
}
|
|
57458
|
-
}
|
|
57459
|
-
}
|
|
57460
|
-
if (await readText(root, projectPath) !== projectContent) throw new Error("The selected project changed while reading its context. Retry before editing.");
|
|
57461
|
-
return canonicalFiles(files);
|
|
57462
|
-
}
|
|
57463
|
-
|
|
57464
57677
|
// packages/cli/src/production-context.ts
|
|
57465
57678
|
async function prepareProductionContext(root, projectId, client) {
|
|
57466
57679
|
const resolved = await resolveProject(await readProjectWorkspace(root, projectId), projectId);
|
|
@@ -57561,123 +57774,6 @@ async function remoteSfx(client, subcommand, params) {
|
|
|
57561
57774
|
// packages/cli/src/media-preview.ts
|
|
57562
57775
|
import { chmod, link as link2, mkdtemp, rm } from "node:fs/promises";
|
|
57563
57776
|
import { dirname as dirname4, join as join3 } from "node:path";
|
|
57564
|
-
|
|
57565
|
-
// packages/cli/src/production-output.ts
|
|
57566
|
-
import { constants as constants3 } from "node:fs";
|
|
57567
|
-
import { mkdir as mkdir2, open as open3 } from "node:fs/promises";
|
|
57568
|
-
import { dirname as dirname3 } from "node:path";
|
|
57569
|
-
async function productionOutputPath(root, relativePath) {
|
|
57570
|
-
if (!relativePath.startsWith(".editorbrain-local/") || relativePath.split(/[\\/]/).some((p) => !p || p === ".." || p === ".") || /[\\\x00-\x1f\x7f]/.test(relativePath)) {
|
|
57571
|
-
throw new CliError("LOCAL_OUTPUT_REQUIRED", "Production output must be inside .editorbrain-local/.", "Use --out .editorbrain-local/<project>/inventory.json, or omit --out for JSON stdout. These reports are not synced.");
|
|
57572
|
-
}
|
|
57573
|
-
return checkedPath(root, relativePath, false);
|
|
57574
|
-
}
|
|
57575
|
-
async function prepareSampleDirectory(root, path) {
|
|
57576
|
-
const target = await productionOutputPath(root, path);
|
|
57577
|
-
await mkdir2(dirname3(target), { recursive: true });
|
|
57578
|
-
await productionOutputPath(root, path);
|
|
57579
|
-
return target;
|
|
57580
|
-
}
|
|
57581
|
-
async function readProductionJson(root, path) {
|
|
57582
|
-
const target = await checkedPath(root, path, false);
|
|
57583
|
-
const handle = await open3(target, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
57584
|
-
try {
|
|
57585
|
-
const stat = await handle.stat();
|
|
57586
|
-
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new CliError("INVALID_REPORT", "Expected a regular JSON file of at most 16 MiB.", "Select a local inventory, plan or check profile.");
|
|
57587
|
-
return JSON.parse(await handle.readFile("utf8"));
|
|
57588
|
-
} finally {
|
|
57589
|
-
await handle.close();
|
|
57590
|
-
}
|
|
57591
|
-
}
|
|
57592
|
-
async function saveProductionReport(root, path, report) {
|
|
57593
|
-
const target = await productionOutputPath(root, path);
|
|
57594
|
-
if (!path.endsWith(".json")) throw new CliError("INVALID_ARGUMENT", "Report output needs a .json extension.", "Use a new path under .editorbrain-local/.");
|
|
57595
|
-
await atomicWrite(root, path, stableJson(report), { document: false, overwrite: false, mode: 384 });
|
|
57596
|
-
return target;
|
|
57597
|
-
}
|
|
57598
|
-
function summarizeProductionReport(value) {
|
|
57599
|
-
const r = value;
|
|
57600
|
-
const base = { schemaVersion: r.schemaVersion, kind: r.kind, summary: true, mediaUploaded: false, quality: "not_assessed" };
|
|
57601
|
-
if (r.kind === "editorbrain.media-inventory") return {
|
|
57602
|
-
...base,
|
|
57603
|
-
complete: r.complete,
|
|
57604
|
-
contentReviewed: false,
|
|
57605
|
-
counts: { assets: r.assets.length, failures: r.failures.length, skipped: r.skipped.length },
|
|
57606
|
-
assets: r.assets.slice(0, 8),
|
|
57607
|
-
failures: r.failures.slice(0, 5),
|
|
57608
|
-
omitted: { assets: Math.max(0, r.assets.length - 8), failures: Math.max(0, r.failures.length - 5), skipped: r.skipped.length },
|
|
57609
|
-
next: "Inspect selected source content. Use media sample for specific frames. Read the saved report or rerun with --details for omitted metadata; no content was reviewed automatically."
|
|
57610
|
-
};
|
|
57611
|
-
if (r.kind === "editorbrain.media-samples") return { ...base, sourceSha256: r.source.sha256, frames: r.frames, contentReviewed: false, next: "View these frames to inspect composition. They do not prove motion or sound." };
|
|
57612
|
-
if (r.kind === "editorbrain.production-plan-check") return {
|
|
57613
|
-
...base,
|
|
57614
|
-
valid: r.valid,
|
|
57615
|
-
inventoryComplete: r.inventoryComplete,
|
|
57616
|
-
observations: r.observations,
|
|
57617
|
-
sourceFilesReverified: false,
|
|
57618
|
-
counts: { sources: r.coverage.length, findings: r.findings.length, repeatedPairs: r.repeatedPairs ?? 0 },
|
|
57619
|
-
findings: [...r.findings].sort((a, b) => ({ error: 0, warning: 1, info: 2 })[a.severity] - { error: 0, warning: 1, info: 2 }[b.severity]).slice(0, 8),
|
|
57620
|
-
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
57621
|
-
next: "Review source reuse and inspection gaps in the saved report. Observations are agent-declared, not independently certified."
|
|
57622
|
-
};
|
|
57623
|
-
if (r.kind === "editorbrain.render-check") return {
|
|
57624
|
-
...base,
|
|
57625
|
-
complete: r.complete,
|
|
57626
|
-
technicalStatus: r.technicalStatus,
|
|
57627
|
-
file: r.file,
|
|
57628
|
-
checks: r.checks,
|
|
57629
|
-
findings: r.findings.slice(0, 8),
|
|
57630
|
-
findingsSummary: r.findingsSummary,
|
|
57631
|
-
omittedFindings: Math.max(0, r.findings.length - 8),
|
|
57632
|
-
next: "Reconcile technical candidates with the brief; inspect the actual video/audio. Technical checks do not assess narrative, aesthetics or SFX audibility."
|
|
57633
|
-
};
|
|
57634
|
-
if (r.kind === "editorbrain.media-source-search") return {
|
|
57635
|
-
...base,
|
|
57636
|
-
complete: r.complete,
|
|
57637
|
-
results: r.results.slice(0, 5).map((item) => ({ provider: item.provider, id: item.id.slice(0, 180), title: item.title.slice(0, 180), sourceUrl: item.sourceUrl.length <= 1500 ? item.sourceUrl : null, author: item.author?.slice(0, 120) ?? null, license: { id: item.license.id, reason: item.license.reason, url: item.license.url && item.license.url.length <= 500 ? item.license.url : null }, contentReviewed: false, rightsVerified: false })),
|
|
57638
|
-
metadataAbbreviated: true,
|
|
57639
|
-
failures: r.failures.slice(0, 5),
|
|
57640
|
-
counts: r.counts,
|
|
57641
|
-
omitted: { results: Math.max(0, r.results.length - 5), failures: Math.max(0, r.failures.length - 5) },
|
|
57642
|
-
searchExhaustive: false,
|
|
57643
|
-
rightsVerified: false,
|
|
57644
|
-
next: "Inspect relevant source pages and media. These archives supplement web image/video search; empty results do not prove absence on the internet."
|
|
57645
|
-
};
|
|
57646
|
-
if (r.kind === "editorbrain.evidence-check") return {
|
|
57647
|
-
...base,
|
|
57648
|
-
valid: r.valid,
|
|
57649
|
-
inventoryComplete: r.inventoryComplete,
|
|
57650
|
-
observations: r.observations,
|
|
57651
|
-
sourceFilesReverified: false,
|
|
57652
|
-
factualTruth: "not_assessed",
|
|
57653
|
-
reuseRights: "not_assessed",
|
|
57654
|
-
findings: r.findings.slice(0, 8),
|
|
57655
|
-
omitted: { findings: Math.max(0, r.findings.length - 8) }
|
|
57656
|
-
};
|
|
57657
|
-
if (r.kind === "editorbrain.audio-check") return {
|
|
57658
|
-
...base,
|
|
57659
|
-
complete: r.complete,
|
|
57660
|
-
technicalStatus: r.technicalStatus,
|
|
57661
|
-
file: r.file,
|
|
57662
|
-
fileStable: r.fileStable,
|
|
57663
|
-
coverage: r.coverage,
|
|
57664
|
-
measurements: r.measurements.slice(0, 8),
|
|
57665
|
-
errorCode: r.errorCode,
|
|
57666
|
-
creativeQuality: r.creativeQuality,
|
|
57667
|
-
omitted: { measurements: Math.max(0, r.measurements.length - 8) },
|
|
57668
|
-
next: "Measurements describe this exact file. Listen for intelligibility, pronunciation, SFX and sync when the modality is available."
|
|
57669
|
-
};
|
|
57670
|
-
if (r.kind === "editorbrain.motion-review") return {
|
|
57671
|
-
...base,
|
|
57672
|
-
samples: r.samples ? { frameCount: r.samples.frameCount, fps: r.samples.fps, radiusFrames: r.samples.radiusFrames, samples: r.samples.samples.slice(0, 12), omittedSamples: Math.max(0, r.samples.samples.length - 12) } : null,
|
|
57673
|
-
geometryReviews: r.geometryReviews.slice(0, 3).map((g) => ({ scope: g.scope, status: g.status, candidates: g.candidates.slice(0, 8), omittedCandidates: Math.max(0, g.candidates.length - 8), checks: g.checks })),
|
|
57674
|
-
omittedGeometryReviews: Math.max(0, r.geometryReviews.length - 3),
|
|
57675
|
-
next: "This plans samples and checks declared boxes only. Render/view those frames; read saved details for omitted candidates and intentional overlaps."
|
|
57676
|
-
};
|
|
57677
|
-
throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
|
|
57678
|
-
}
|
|
57679
|
-
|
|
57680
|
-
// packages/cli/src/media-preview.ts
|
|
57681
57777
|
async function prepareMediaPreview(root, file, out, quality = 720, audioStream) {
|
|
57682
57778
|
if (![720, 1080].includes(quality) || !out.endsWith(".mp4")) throw new CliError("INVALID_ARGUMENT", "Use --quality 720 or 1080 and a new .mp4 output.", "Previews belong under .editorbrain-local/.");
|
|
57683
57779
|
const source = await probeLocalMedia(await assertLocalMediaFile(file));
|
|
@@ -57736,6 +57832,16 @@ async function prepareStudioAudio(root, file, out, streamIndex) {
|
|
|
57736
57832
|
// packages/cli/src/client.ts
|
|
57737
57833
|
import { setTimeout as delay } from "node:timers/promises";
|
|
57738
57834
|
var searchErrors = {
|
|
57835
|
+
batch_changed: { status: 409, code: "BATCH_CHANGED", hint: "Read batches get before retrying; preserve new changes." },
|
|
57836
|
+
batch_claim_blocked: { status: 409, code: "BATCH_CLAIM_BLOCKED", hint: "Read the batch. Wait for an available slot or reconcile an expired turn; do not start work without a claim." },
|
|
57837
|
+
batch_budget_exceeded: { status: 409, code: "BATCH_BUDGET_EXCEEDED", hint: "Stop generation. The batch or video budget was reached. Do not increase the budget yourself." },
|
|
57838
|
+
batch_not_running: { status: 409, code: "BATCH_NOT_RUNNING", hint: "The batch is paused/cancelled or the turn expired. Read the batch before continuing." },
|
|
57839
|
+
batch_retry_blocked: { status: 409, code: "BATCH_RETRY_BLOCKED", hint: "Inspect existing jobs and outputs before retrying. Do not duplicate a paid request." },
|
|
57840
|
+
batch_project_busy: { status: 409, code: "BATCH_PROJECT_BUSY", hint: "Wait for existing Studio jobs before grouping these projects." },
|
|
57841
|
+
batch_project_taken: { status: 409, code: "BATCH_PROJECT_TAKEN", hint: "Use the project existing batch or create a separate video project." },
|
|
57842
|
+
studio_context_or_lease_changed: { status: 409, code: "STUDIO_CONTEXT_OR_LEASE_CHANGED", hint: "Refresh the workflow and batch lease before generating." },
|
|
57843
|
+
progress_changed: { status: 409, code: "PROGRESS_CHANGED", hint: "Read projects progress --project ID again. Preserve the latest state; never force an old progress version." },
|
|
57844
|
+
final_not_approved: { status: 409, code: "FINAL_NOT_APPROVED", hint: "Review and approve the latest complete video, resolve pending changes, then record explicit final-delivery acceptance." },
|
|
57739
57845
|
invalid_search_brief: { status: 400, code: "INVALID_SEARCH_BRIEF", hint: "Check references/search-brief.md and the service contract. Older services may reject eventQueries objects with channels. Do not silently remove constraints or retry a changed query as if filtering succeeded." },
|
|
57740
57846
|
source_inspection_required: { status: 422, code: "SOURCE_INSPECTION_REQUIRED", hint: "Inspect the supplied source and declare only modalities actually reviewed. A script requires script inspection; video requires frames, motion or audio." },
|
|
57741
57847
|
search_index_not_ready: { status: 503, code: "SEARCH_INDEX_NOT_READY", hint: "No current authorized evidence is indexed. Report the gap; literal library lookup is available but is not semantic retrieval or proof of fit." }
|
|
@@ -57835,7 +57941,7 @@ var Client = class {
|
|
|
57835
57941
|
const retryAfterMs = retryDelay(response.headers.get("Retry-After"), this.runtime.now());
|
|
57836
57942
|
if (!response.ok) {
|
|
57837
57943
|
let errorBody;
|
|
57838
|
-
if (path === "/v1/search/brief" && [400, 422, 503].includes(response.status)) {
|
|
57944
|
+
if ((path.startsWith("/v1/batches") || path === "/v1/studio/jobs") && response.status === 409 || path === "/v1/search/brief" && [400, 422, 503].includes(response.status) || /^\/v1\/projects\/[^/]+\/progress$/.test(path) && response.status === 409) {
|
|
57839
57945
|
try {
|
|
57840
57946
|
const diagnostic = searchDiagnostic(response.status, await boundedJson(response, 16 * 1024));
|
|
57841
57947
|
errorBody = diagnostic ? { error: { code: diagnostic.serviceCode } } : void 0;
|
|
@@ -58017,7 +58123,7 @@ ${conflicts.join("\n")}`, "Run pull --dry-run and reconcile the reported paths."
|
|
|
58017
58123
|
|
|
58018
58124
|
// packages/cli/src/studio.ts
|
|
58019
58125
|
async function studioCommand(client, root, command, argument, flags) {
|
|
58020
|
-
const allowed = /* @__PURE__ */ new Set(["dir", "url", ...command === "quote" ? ["file"] : command === "run" ? ["max-credits", "user-confirmed"] : []]);
|
|
58126
|
+
const allowed = /* @__PURE__ */ new Set(["dir", "url", ...command === "quote" ? ["file"] : command === "run" ? ["max-credits", "user-confirmed", "batch-lease-token"] : []]);
|
|
58021
58127
|
for (const flag of Object.keys(flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for studio ${command}.`, "Read editorbrain help.");
|
|
58022
58128
|
try {
|
|
58023
58129
|
if (command === "catalog" || command === "jobs") {
|
|
@@ -58035,7 +58141,7 @@ async function studioCommand(client, root, command, argument, flags) {
|
|
|
58035
58141
|
if (command === "run") {
|
|
58036
58142
|
const maxCredits = Number(flags["max-credits"]);
|
|
58037
58143
|
if (flags["user-confirmed"] !== true || !Number.isFinite(maxCredits) || maxCredits <= 0 || maxCredits > 1e4) throw new CliError("SPENDING_APPROVAL_REQUIRED", "Supply --user-confirmed and --max-credits only after the user authorized this quote or budget.", "Preferences and direct editing mode do not authorize spending.");
|
|
58038
|
-
return await client.request("/v1/studio/jobs", { method: "POST", body: { quoteId: argument, maxCredits, userConfirmed: true } });
|
|
58144
|
+
return await client.request("/v1/studio/jobs", { method: "POST", body: { quoteId: argument, maxCredits, userConfirmed: true, ...flags["batch-lease-token"] ? { batchLeaseToken: flags["batch-lease-token"] } : {} } });
|
|
58039
58145
|
}
|
|
58040
58146
|
throw new CliError("INVALID_ARGUMENT", "Use studio catalog, quote, run, get or jobs.", "Read editorbrain help.");
|
|
58041
58147
|
} catch (error) {
|
|
@@ -58381,6 +58487,41 @@ var VERIFIED_GUIDANCE_RELEASES = [
|
|
|
58381
58487
|
"editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
|
|
58382
58488
|
"editorbrain/references/workspace-files.md": "18462e7b66ba6204770c654e4a2f7e505c343319671a0447aa55c59f0b9096ec"
|
|
58383
58489
|
}
|
|
58490
|
+
},
|
|
58491
|
+
{
|
|
58492
|
+
"version": "0.32.0",
|
|
58493
|
+
"archiveSha256": "30c6c6d97da7de16bb620c0975c2d3f2a4b7a6ea4a56f2eb3c5b0f5f53914f45",
|
|
58494
|
+
"files": {
|
|
58495
|
+
"editorbrain-formats/SKILL.md": "b8b03266a9b7cde8337846144d3032b63b6d9cfbfc4530ae1b06f3dd43074ff2",
|
|
58496
|
+
"editorbrain-formats/references/numbered-tutorial.md": "95754ae7dbfaa678e11ddf1a2b84710dba83a16e2fa4055051bd4f5f4f868a5f",
|
|
58497
|
+
"editorbrain-motion/SKILL.md": "86617d39341d7325d613b877838258f40f05b4286301dacf8e1ec85ccd502aab",
|
|
58498
|
+
"editorbrain-motion/references/authored-scenes.md": "11109dcd35ab7138fdc18e54605885bb26d9e948f12221e18dea4038987dede2",
|
|
58499
|
+
"editorbrain-motion/references/code-motion.md": "50382a797adc1e1596086e2408e81ee75ceb6730ca409be9c884646db643a7cd",
|
|
58500
|
+
"editorbrain-motion/references/component-library.md": "e4593622ebe371bba1b1c62f698c6709c36a8d01ddce4a07d40534fca44b797e",
|
|
58501
|
+
"editorbrain-motion/references/generated-motion.md": "a8d214b53c15f742658abd84f03b768fa2821f8cde94f50aab82a3759a30b36b",
|
|
58502
|
+
"editorbrain-motion/references/motion-craft.md": "adba1b0cbb3c6327a7665857b0a6d144f97dc843464ccf154d3f76ee3ada905d",
|
|
58503
|
+
"editorbrain/AGENTS.md": "2361aaa3202d3f4ea9e941684fbda1a84d9711389cca89f7f6cd3ae03b7ab6d0",
|
|
58504
|
+
"editorbrain/SKILL.md": "b15eb4e2438c0a3e6ec5c3c3170f11c973676d5c6a850cf5d2e9cb3bc7c44e42",
|
|
58505
|
+
"editorbrain/references/audio-production.md": "038504d738db98939aeed14407b15561e4f82c650aace7eb545e77cdffd8dadb",
|
|
58506
|
+
"editorbrain/references/cli-mcp.md": "9febfcac6c1dc763b79ab26ffb07e53bce703cbe4c2332ddcd250e10f2fa75dd",
|
|
58507
|
+
"editorbrain/references/editorial-evidence.md": "caf72365b3e7492a62d96b39cd17031fa7c961671ce32bc4649420d8a353a026",
|
|
58508
|
+
"editorbrain/references/first-edit.md": "d71271ab32c6432cce01316917b4504f15411ea59511566a1f6169ac6d09723c",
|
|
58509
|
+
"editorbrain/references/integrations.md": "a3500dd79c71ab3ee80a865bb5bf8c94287efb933e07d577738891e276a819a0",
|
|
58510
|
+
"editorbrain/references/local-production.md": "2d170bb217854eda7581f64d8342fffe3b56234bbe56b2715fa93eb3210e6354",
|
|
58511
|
+
"editorbrain/references/media-preferences.md": "d1b92b181fbc226c8d3fda762726dd413944175768651fbfb8200eae44fb2ec8",
|
|
58512
|
+
"editorbrain/references/memory-and-review.md": "2a89d6fc4737b750a380c69630d6fd4f4132aed27656b92e8f0c5748887b70ae",
|
|
58513
|
+
"editorbrain/references/motion-review.md": "0fb09a2d61ecd13114ced5cca5daa4580a922cf7d7b3f8698077ceb0e96da6f0",
|
|
58514
|
+
"editorbrain/references/onboarding.md": "875d1b9049c4663780f993c2843528c531a51d6b51380b08ed77d82f618185fb",
|
|
58515
|
+
"editorbrain/references/production-and-review.md": "50f10a98a4f3709a5a585f58e4154f33d6d883b218cd25f3abd72eccc43b15a7",
|
|
58516
|
+
"editorbrain/references/production-continuity.md": "c4b346c9634f3d1ef63b24a97d178e0253c2e45d53f6934076a5417e66e34d9b",
|
|
58517
|
+
"editorbrain/references/real-media.md": "106bee5a34641d53f021ead5d41630d473c2bfc98e4f7ee5500dae837e2e45bb",
|
|
58518
|
+
"editorbrain/references/search-brief.md": "ea2a9c2d5a5cf685c1e5918366901ef2589c4d79aaa78a0dd31e97d6fed42b7b",
|
|
58519
|
+
"editorbrain/references/sfx-library.md": "fa0b02c8765988257e46e16298ecca9cdb0e338a863915e29f831162e237eac4",
|
|
58520
|
+
"editorbrain/references/storage-and-previews.md": "8a3057f1c1a2a1e1cd32028059dc897a8461da38c36e812fbd24943d3740b4d5",
|
|
58521
|
+
"editorbrain/references/studio.md": "7a84e43bd72f8590d1a91c7d1bc5f6b60678c1c8eba6523b51b99243e22a1322",
|
|
58522
|
+
"editorbrain/references/style-creator.md": "1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2",
|
|
58523
|
+
"editorbrain/references/workspace-files.md": "c16fe08b209c65baf8f57054f7cc01f514292fc073b7072298b86c0054da3e19"
|
|
58524
|
+
}
|
|
58384
58525
|
}
|
|
58385
58526
|
];
|
|
58386
58527
|
function isVerifiedHistoricalGuidance(skillName, relativePath, sha2562) {
|
|
@@ -63531,7 +63672,16 @@ editorbrain register --url https://server --name workspace --invite-stdin
|
|
|
63531
63672
|
editorbrain status
|
|
63532
63673
|
editorbrain pull [--project id | --all] [--dir folder] [--dry-run]
|
|
63533
63674
|
editorbrain push [--dir folder] [--dry-run]
|
|
63675
|
+
editorbrain batches list [--after UUID]
|
|
63676
|
+
editorbrain batches create --file batch.json
|
|
63677
|
+
editorbrain batches get BATCH_ID
|
|
63678
|
+
editorbrain batches pause|resume|cancel BATCH_ID --expected-revision N
|
|
63679
|
+
editorbrain batches retry|detach BATCH_ID --project ID --expected-revision N
|
|
63680
|
+
editorbrain batches claim|renew|release|attention BATCH_ID --project ID --lease-token UUID
|
|
63534
63681
|
editorbrain projects list
|
|
63682
|
+
editorbrain projects progress --project ID
|
|
63683
|
+
editorbrain projects start|queue --project ID --expected-version HASH
|
|
63684
|
+
editorbrain projects complete --project ID --expected-version HASH --revision UUID --user-confirmed
|
|
63535
63685
|
editorbrain documents list [--prefix path]
|
|
63536
63686
|
editorbrain documents get <path> [--revision N]
|
|
63537
63687
|
editorbrain documents history <path> [--before N]
|
|
@@ -63550,7 +63700,7 @@ editorbrain studio prepare --file source.mp4 --out .editorbrain-local/studio/aud
|
|
|
63550
63700
|
editorbrain studio catalog
|
|
63551
63701
|
editorbrain billing status
|
|
63552
63702
|
editorbrain studio quote --file request.json [--dir folder]
|
|
63553
|
-
editorbrain studio run QUOTE_ID --max-credits N --user-confirmed
|
|
63703
|
+
editorbrain studio run QUOTE_ID --max-credits N --user-confirmed [--batch-lease-token UUID]
|
|
63554
63704
|
editorbrain studio get JOB_ID
|
|
63555
63705
|
editorbrain studio jobs
|
|
63556
63706
|
editorbrain integrations catalog
|
|
@@ -63636,6 +63786,9 @@ function parseArgs(argv) {
|
|
|
63636
63786
|
known.add("preview-kind");
|
|
63637
63787
|
known.add("preset");
|
|
63638
63788
|
known.add("context-version");
|
|
63789
|
+
known.add("expected-version");
|
|
63790
|
+
known.add("lease-token");
|
|
63791
|
+
known.add("batch-lease-token");
|
|
63639
63792
|
for (let i = 0; i < argv.length; i++) {
|
|
63640
63793
|
const arg = argv[i];
|
|
63641
63794
|
if (!arg.startsWith("--")) {
|
|
@@ -64221,6 +64374,14 @@ async function main() {
|
|
|
64221
64374
|
}
|
|
64222
64375
|
throw new Error("Use keys list, keys create --label name, or keys revoke <key-id>");
|
|
64223
64376
|
}
|
|
64377
|
+
if (command === "batches") {
|
|
64378
|
+
output(await batchCommand(new Client(await config(args)), await ensureRoot(str(args, "dir") ?? process.cwd()), subcommand, argument, args.flags));
|
|
64379
|
+
return;
|
|
64380
|
+
}
|
|
64381
|
+
if (command === "projects" && ["progress", "start", "queue", "complete"].includes(subcommand)) {
|
|
64382
|
+
output(await projectProgressCommand(new Client(await config(args)), subcommand, args.flags));
|
|
64383
|
+
return;
|
|
64384
|
+
}
|
|
64224
64385
|
if (command === "projects" && subcommand === "list") {
|
|
64225
64386
|
output({ projects: await listProjects(new Client(await config(args))) });
|
|
64226
64387
|
return;
|
package/package.json
CHANGED
|
@@ -24,12 +24,14 @@ For factual subjects or real demonstrations, inspect authentic footage/images fr
|
|
|
24
24
|
5. Ground each new edit or materially changed direction in the **edited-video reference library**, including when a favorite style is selected. Search with `search_from_brief` (CLI `editorbrain search brief --file <brief.json>`) and the project ID after inspecting the material. Read [search brief](references/search-brief.md) for the schema and [editorial evidence](references/editorial-evidence.md) for selection/adaptation. Search communicative needs, not a guessed template. A scoped revision may reuse still-valid inspected evidence and approval. The optional `search_for_edit` server-analysis path is never an automatic fallback.
|
|
25
25
|
6. Survey candidates across the video's needs, then inspect selected documents and actual frames/intervals, plus audio when sound matters. Build a coherent scene-level palette of techniques: opening, imagery, type, explanation and transitions can come from different references. A shortlist is not a single template for the whole video. Use preferences to retain, adapt or reject mechanisms; save evidence, limitations and beat-level adaptations in `direction.md`. A motion-catalog lookup does not complete this step. If evidence is inaccessible or unsuitable, disclose the gap. For factual beats, compare allowed user footage and authentic web media with authored explanation. The engine does not dictate the imagery or entire layout.
|
|
26
26
|
7. Follow the effective review mode: `direct` → implement and review; `directions` → propose suitable alternatives with labelled reference evidence; `sample` → show a frame/short clip made with customer material before the full edit. Respect existing approval. Check the preference version before proposals, costly generation and full rendering; a web update cannot interrupt an active tool.
|
|
27
|
-
8. Carry the same `editorialSnapshot` into implementation and any specialist handoff. For a sample or localized correction, use [production continuity](references/production-continuity.md): register the exact version, retain unaffected work and retrieve pending feedback. Before delivery, use `production verify --project ID --file <render>` when available, inspect its real samples and review the final motion/audio. An editing request needs the edit, not only a plan. Use [production and review](references/production-and-review.md) to compare editorial quality with the chosen references and any applicable user-approved sample; a technical pass alone cannot satisfy that comparison. Report unverified dimensions separately from technical results.
|
|
27
|
+
8. When available, read `check_workflow.projectProgress` on resuming (or `get_project_progress` / `editorbrain projects progress --project ID`). Register actual work with `update_project_progress` (`action:start`, current `expectedVersion`) / `projects start`. This keeps the web board current; it is not a heartbeat. See [production continuity](references/production-continuity.md) for final delivery and conflict handling. Carry the same `editorialSnapshot` into implementation and any specialist handoff. For a sample or localized correction, use [production continuity](references/production-continuity.md): register the exact version, retain unaffected work and retrieve pending feedback. Before delivery, use `production verify --project ID --file <render>` when available, inspect its real samples and review the final motion/audio. An editing request needs the edit, not only a plan. Use [production and review](references/production-and-review.md) to compare editorial quality with the chosen references and any applicable user-approved sample; a technical pass alone cannot satisfy that comparison. Report unverified dimensions separately from technical results.
|
|
28
28
|
|
|
29
29
|
## Specialized work, loaded when needed
|
|
30
30
|
|
|
31
31
|
Use the decision ownership in [the shared contract](AGENTS.md#decision-ownership): montage, media, motion, sound and review share one direction. For animated scenes, read [editorbrain-motion](../editorbrain-motion/SKILL.md) with the active project and resolved context. Ordinary cuts, static graphics or sync/install tasks need no motion skill. When the edit follows a recognizable format (for example a numbered tutorial, or a reference whose structure the user wants replicated), read [editorbrain-formats](../editorbrain-formats/SKILL.md) for its structure and sync rules; identity still comes from the resolved brand and style. User memory belongs in scoped brand/style/project files; shared guides describe methods and examples supply evidence.
|
|
32
32
|
|
|
33
|
+
For multiple final videos, read the batch section of [production continuity](references/production-continuity.md): confirm clip grouping, isolate projects, acquire turns and respect batch budgets. `check_workflow.batch` carries coordination; it does not replace project preferences.
|
|
34
|
+
|
|
33
35
|
For built-in transcription, image generation or narration, read [EditorBrain Studio](references/studio.md) only when needed. Discover availability first; quote and obtain an authorized budget before spending. Local MP4 preparation, durable job recovery and private result retrieval are covered there.
|
|
34
36
|
|
|
35
37
|
For cloud uploads or storage cleanup, read [storage and previews](references/storage-and-previews.md) only then; keep originals and masters distinct from lightweight review copies.
|
|
@@ -100,3 +100,96 @@ allowed lists. `MEDIA_USE_REVIEW` stays pending even after a technical pass: com
|
|
|
100
100
|
the plan and actual visible resources, including useful omissions, using
|
|
101
101
|
[production review](production-and-review.md). The tool does not inspect or count
|
|
102
102
|
imagery automatically, generate assets or authorize any provider.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Keep the project board current (discover availability)
|
|
106
|
+
|
|
107
|
+
Web, CLI and MCP use the same server state; do not store an independent status in
|
|
108
|
+
`project.json`. First discover these tools/commands with the installed CLI help or
|
|
109
|
+
MCP tools list. Older clients/services remain usable; report unavailable progress
|
|
110
|
+
tracking without claiming that the board was updated.
|
|
111
|
+
|
|
112
|
+
- `get_project_progress` / `editorbrain projects progress --project ID` returns
|
|
113
|
+
`stage`, `version`, latest review and pending count. Read on resume and before writes.
|
|
114
|
+
- `update_project_progress` with `action:start`, `expectedVersion` / `projects start
|
|
115
|
+
--project ID --expected-version HASH`: mark actual editing, including a correction.
|
|
116
|
+
It does not run a render, wake an agent or authorize paid tools.
|
|
117
|
+
- New registered samples automatically appear **Para revisar**. Requested changes
|
|
118
|
+
return to **En edición**. Sample approval alone never finishes a project.
|
|
119
|
+
- When the user accepts the exact complete video as the final delivery, use existing
|
|
120
|
+
production approval for that revision, read progress again, then `projects complete
|
|
121
|
+
--project ID --revision UUID --expected-version HASH --user-confirmed` or MCP
|
|
122
|
+
`update_project_progress` with `action:complete`, `revisionId`, `expectedVersion`,
|
|
123
|
+
`userConfirmed:true`. Do not request the same approval twice; record an existing
|
|
124
|
+
explicit final acceptance. A frame, preview, successful render or technical pass
|
|
125
|
+
is insufficient. Only the latest current, available, approved video with zero
|
|
126
|
+
pending project feedback can finish. Direct editing mode still skips proposal
|
|
127
|
+
gates; final acceptance is not inferred from that preference.
|
|
128
|
+
- `action:queue` / `projects queue` returns work to **Por editar** when the user
|
|
129
|
+
defers it and no corrections are pending. `start` can reopen a completed video.
|
|
130
|
+
- On `progress_changed`, refresh and reconcile; never automatically replay stale
|
|
131
|
+
intent. If a write response is lost, read progress before retrying. No push/pull
|
|
132
|
+
is needed for this operational state. No live-agent presence is implied.
|
|
133
|
+
|
|
134
|
+
Archived samples and approvals remain historical evidence. Context changes,
|
|
135
|
+
new samples, removed media and requested corrections can invalidate completion.
|
|
136
|
+
Report unavailable/unknown state honestly; do not assign a stage from filenames.
|
|
137
|
+
|
|
138
|
+
## Multiple videos: one batch, isolated projects
|
|
139
|
+
|
|
140
|
+
Load this section only for multiple deliverables. Discover `batches` / editing-batch
|
|
141
|
+
MCP tools first. One project means one final video, not necessarily one source file.
|
|
142
|
+
Inspect the supplied inventory; if grouping is ambiguous, show a short visual list
|
|
143
|
+
of proposed titles and assigned clips for confirmation. For videos from scratch,
|
|
144
|
+
create one project/brief per idea through the existing project workflow. Never
|
|
145
|
+
silently turn ten clips of one story into ten videos. Keep originals intact.
|
|
146
|
+
|
|
147
|
+
Create the projects normally, then `batches create --file batch.json` (MCP
|
|
148
|
+
`create_editing_batch`). Retain a UUID batchId for retry-safe creation:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{"batchId":"UUID","name":"This week","styleMode":"series","maxConcurrency":1,"maxCredits":0,"items":[{"projectId":"first-video","maxCredits":0}],"userConfirmed":true}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Confirm the grouping and ceilings with the user; start paused. Zero forbids paid
|
|
155
|
+
Studio generation, not local editing. `series` means a shared direction, not copied
|
|
156
|
+
scenes. `individual` chooses references for each story. Neither overwrites brand,
|
|
157
|
+
language, exclusions, review preference, project exceptions or integration access.
|
|
158
|
+
Reuse relevant shared context but search edited-video references for each brief;
|
|
159
|
+
carry the evidence into each project. Never copy a previous video's factual assets
|
|
160
|
+
or assume its approval transfers. Keep directories, renders and feedback separate.
|
|
161
|
+
|
|
162
|
+
Read `batches get ID` / `get_editing_batch`. Resume with `batches resume ID
|
|
163
|
+
--expected-revision N` / `update_editing_batch`. This enables scheduling; it does
|
|
164
|
+
not start a renderer or authorize spending. Work serially by default; use at most
|
|
165
|
+
the configured slots if the host supports independent workers and enough disk,
|
|
166
|
+
RAM and render capacity. Do not load every project's full context into one task.
|
|
167
|
+
|
|
168
|
+
Before work, claim ONE project using `batches claim ID --project PROJECT
|
|
169
|
+
--lease-token UUID` / `editing_batch_lease`. Keep that UUID for retries; another
|
|
170
|
+
session cannot acquire the same item. Read `check_workflow.batch` and the project's
|
|
171
|
+
snapshot; mark actual editing through project progress. Renew within 20 minutes
|
|
172
|
+
and recheck before each expensive step. Stop scheduling on pause, cancellation,
|
|
173
|
+
expiration or conflict. Local processes already running cannot be remotely killed
|
|
174
|
+
by the queue. On host shutdown work waits for another authorized session.
|
|
175
|
+
|
|
176
|
+
Studio still needs exact quotes and authorized spending. Pass `--batch-lease-token`
|
|
177
|
+
to `studio run` (MCP `studio_run.batchLeaseToken`). The server enforces the shared
|
|
178
|
+
lot ceiling, per-video ceiling and credit balance atomically. External services
|
|
179
|
+
have separate budgets: these limits cannot control a user's own API/MCP charges.
|
|
180
|
+
Read existing job IDs after unknown outcomes; never replace them with new quotes.
|
|
181
|
+
|
|
182
|
+
After registering a fresh project sample, `batches release` / lease action release
|
|
183
|
+
frees the turn for the next video. This does NOT approve the sample or finish the
|
|
184
|
+
video. On failure use `attention`, preserve outputs and continue unaffected items.
|
|
185
|
+
Expired turns require inspection of local outputs and paid jobs before explicit
|
|
186
|
+
`batches retry ID --project PROJECT --expected-revision N`. Retry resets only that
|
|
187
|
+
queue item; it does not regenerate, delete files or waive existing approvals.
|
|
188
|
+
A requested correction similarly requeues its project after pending jobs settle.
|
|
189
|
+
Final acceptance remains per exact video; batch completion is derived from those
|
|
190
|
+
individual acceptances, never inferred from all renders finishing.
|
|
191
|
+
|
|
192
|
+
Pause stops new turns/paid jobs. Cancel also prevents future resumption of that
|
|
193
|
+
batch; in-flight jobs may still finish and be charged. To work separately afterward,
|
|
194
|
+
`batches detach ID --project PROJECT --expected-revision N` removes active membership
|
|
195
|
+
only after the cancelled project's jobs and turn have ended. History/costs remain.
|
|
@@ -3,9 +3,9 @@ name: editorbrain-formats
|
|
|
3
3
|
description: >-
|
|
4
4
|
Replicate a proven video format's structure in an EditorBrain edit, such as a
|
|
5
5
|
numbered tutorial or demonstration (hook promise, numbered steps, a visible result
|
|
6
|
-
for each step, presenter returns, CTA)
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
for each step, presenter returns, CTA) or an aspirational list that turns personal
|
|
7
|
+
and ends in a demonstration. Use when the user names a format or editorial family,
|
|
8
|
+
asks to follow a reference's structure, or the material is a list of steps or items. Formats define structure, sync and pacing only; identity comes
|
|
9
9
|
from the user's resolved brand and style.
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -42,6 +42,7 @@ The number of steps, durations, demo mechanisms and the headline's wording come
|
|
|
42
42
|
| Format | Use when | Guide |
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| Numbered tutorial / demonstration | The voice teaches N discrete steps, tips or instructions and each one can be shown working | [numbered tutorial](references/numbered-tutorial.md) |
|
|
45
|
+
| Aspirational list → personal experience → demonstration | The voice lists N people, tools or habits, each illustrated by strong images, then turns personal and shows how to act on it with real material | [aspirational list](references/aspirational-list-demo.md) |
|
|
45
46
|
|
|
46
47
|
Read only the guide you will use. For animated beats, also use the sibling [motion skill](../editorbrain-motion/SKILL.md); for authentic results, follow [real media](../editorbrain/references/real-media.md).
|
|
47
48
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Format: aspirational list → personal experience → demonstration
|
|
2
|
+
|
|
3
|
+
A narrator promises a list ("N things, people or tools that…"). Each item opens its own short block of images carrying one lesson. The list then turns personal, toward the narrator's own practice or the viewer's future self, and lands on a concrete demonstration of how to act on it. The narrator stays present as a small element over full-frame imagery instead of filling the frame.
|
|
4
|
+
|
|
5
|
+
Evidence status: one implemented, user-approved pilot (talking head, a list of five practices, Spanish, vertical, about 130 s). Its reference was a library profile of an English aspirational list of about 85 s ending in a product demo. Transfer to other niches and to voice-over-only material is untested; record the first adaptation's review.
|
|
6
|
+
|
|
7
|
+
## Authority and adaptation
|
|
8
|
+
|
|
9
|
+
Reuse the coordinator's resolved context. This guide offers reference grammar, not a mandatory layout or an override of presenter, media, motion or accessibility preferences. In direct mode, make compatible decisions and record them; ask for a sample approval only when the resolved review mode or an unresolved user requirement calls for one.
|
|
10
|
+
|
|
11
|
+
Match visual changes to meaning; do not cut the audio to force a picture change onto a timestamp. ASR word starts are candidates to verify against playback. Short phrases may need a shared image and longer explanations may need several states.
|
|
12
|
+
|
|
13
|
+
## Fit
|
|
14
|
+
|
|
15
|
+
Use when:
|
|
16
|
+
|
|
17
|
+
- the script enumerates items that can each be illustrated by strong images: people, tools, places, habits, products;
|
|
18
|
+
- the voice carries continuously (voice-over or talking head), and reducing the presenter to a small element does not lose meaning;
|
|
19
|
+
- the list ends in something actionable that can be shown with real material: the narrator's own practice, a configured tool, a product, a first step.
|
|
20
|
+
|
|
21
|
+
Niche examples: people to study in any field, tools for a craft, habits for a goal, places to visit, investments to understand, routines of known performers.
|
|
22
|
+
|
|
23
|
+
Avoid or adapt when:
|
|
24
|
+
|
|
25
|
+
- the items cannot be illustrated without fabricating evidence (biographical claims, results, quotes);
|
|
26
|
+
- the presenter's face is the content (reaction, emotion, testimony). Keep them full frame instead;
|
|
27
|
+
- the demonstration would promote something that does not deliver what is said. Omit the demo and keep the list.
|
|
28
|
+
|
|
29
|
+
## Skeleton
|
|
30
|
+
|
|
31
|
+
| Beat | Function | What must be on screen |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Hook | Promise the list and who it is for | A collage of the list's strongest visuals (one per cell), a short headline lockup over it and the presenter small over the collage |
|
|
34
|
+
| Item k (repeat N times) | Name the item and its lesson | Item marker (k of N plus name) and one full-frame image per idea, cut on the word that introduces it |
|
|
35
|
+
| Personal turn | Move from examples to the narrator or the viewer | The narrator's real material: their folders, screens, products, recordings |
|
|
36
|
+
| Demonstration | Show how to act on the list | Real interface or product: page scroll, configured screens, steps; the presenter may shrink, move or leave |
|
|
37
|
+
| CTA | The real ask | The presenter returns (full frame or small), optional recap of the N items, the CTA object |
|
|
38
|
+
|
|
39
|
+
The personal turn can be a final block or woven into every item, with each item ending on the narrator's own tool or result. The pilot wove it into every item. Choose by the script; do not force a twist the voice does not make.
|
|
40
|
+
|
|
41
|
+
## Voice and image
|
|
42
|
+
|
|
43
|
+
- **One idea, one image.** Cut when the voice introduces a new idea, name or example (on that word), not on a timer.
|
|
44
|
+
- **The image answers the sentence:**
|
|
45
|
+
- a named tool or product: its real mark or interface at the moment it is named;
|
|
46
|
+
- an abstract claim: an illustrative image, labelled when it could be mistaken for evidence;
|
|
47
|
+
- a number: the number on screen;
|
|
48
|
+
- a spoken series in one sentence ("apps, documents, tables, projects"): one demo that advances on each word (scroll steps, highlighted rows).
|
|
49
|
+
- **Authentic material first** for anything factual: the user's own product screens, folders, repositories, dashboards and recordings. Clean captures of public pages for named products are authentic support. Hide consent overlays without accepting them, and use logged-in or private screens only when the user provided or explicitly authorized them. Retain source attribution and exclude private details unnecessary to the edit.
|
|
50
|
+
- **Illustrative stills** (generated or licensed) only within the media preferences and an authorized budget. Keep one coherent look across them (lighting, grade, depth) so the list reads as one piece.
|
|
51
|
+
- **Brand characters:** when generating a brand character or mascot, always attach the real asset as a reference; an off-model character undermines trust.
|
|
52
|
+
- **Do not reuse an image, capture or clip** for a different idea. A deliberate callback is the exception and must read as one.
|
|
53
|
+
- **The hook headline** sits directly on the collage in the reference. Keep it legible with the resolved type treatment (outline, shadow, weight) before adding a backing band; follow accessibility and the user's caption/text preferences.
|
|
54
|
+
|
|
55
|
+
## Presenter treatment
|
|
56
|
+
|
|
57
|
+
- **Reference grammar:** the presenter is a small floating cut-out (person matte, no background, no frame) placed over each image and relocated per shot.
|
|
58
|
+
- Placement keeps faces, headlines, controls and the demo's key interface visible.
|
|
59
|
+
- Adapt to the user's presenter/layout preferences: a split layout, full-frame return, framed picture-in-picture or no presenter may fit better. A clean matte is not a requirement for using the narrative structure.
|
|
60
|
+
- **Matte:** generate it for the whole cut before designing. The pilot used on-device person segmentation on a static camera; other cameras and motions need verification. Apple Vision is an optional macOS implementation, not a client dependency. On other platforms use an available, authorized masking tool or an agreed layout without a matte. Do not install tools or upload private footage merely because this guide mentions segmentation.
|
|
61
|
+
- Check hair, hands and anything held near the body.
|
|
62
|
+
- If edges fail, compare the available alternatives within the user's tools, budget and data permissions; a simpler layout is acceptable.
|
|
63
|
+
- **Size and crop:** head to mid-torso, hands included, at about a third of the frame width in the pilot.
|
|
64
|
+
- **Edge treatment:** a clean crop, fades, outline or shadow is an identity choice; resolve it from preferences or ask. The pilot shipped a clean crop with a soft drop shadow.
|
|
65
|
+
- **Relocation:** when the presenter moves with the cut, a small settle (scale or position) can help the eye find them again; follow the resolved motion intensity.
|
|
66
|
+
|
|
67
|
+
## Rhythm
|
|
68
|
+
|
|
69
|
+
- Measured pilot holds ranged from about 0.7 s to 6 s (median about 3 s) per image; item blocks lasted 15–31 s. These are evidence, not targets: holds follow the idea and the copy.
|
|
70
|
+
- A longer hold can use a slow push, page scroll or a later overlay when it helps meaning. A deliberate static hold is valid for reading or calm motion preferences.
|
|
71
|
+
- Cuts can land with a short push-in to give each new image energy, within the resolved motion preferences.
|
|
72
|
+
- Captions stay in one stable band, never under the presenter. Short phrases suit the fast image rhythm.
|
|
73
|
+
|
|
74
|
+
## Filling the frame
|
|
75
|
+
|
|
76
|
+
Authored scenes (numbers, code or file windows, badges, logo grids) should use the chosen output frame deliberately, including square and landscape adaptations. Options include:
|
|
77
|
+
|
|
78
|
+
- a full-bleed image behind them;
|
|
79
|
+
- windows that span the width;
|
|
80
|
+
- overlays sized to be read on a phone.
|
|
81
|
+
|
|
82
|
+
A small centered element on an empty stage reads as a placeholder next to full-bleed images. Crop real captures to their most legible region (one card, one heading, one logo) instead of shrinking a whole page into a cell.
|
|
83
|
+
|
|
84
|
+
## Identity roles to resolve
|
|
85
|
+
|
|
86
|
+
Map these roles to the user's tokens and `design.md`, never to a reference's or pilot's look:
|
|
87
|
+
|
|
88
|
+
| Role | Notes |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Hook headline lockup | Dominant and accent styles, legibility treatment |
|
|
91
|
+
| Item marker | Numeral and item name; persistent or entering per item |
|
|
92
|
+
| Captions | The brand's caption rules |
|
|
93
|
+
| Tags on real captures | Source or explanation labels |
|
|
94
|
+
| Overlay cards | Numbers, badges, stamps, logo tiles |
|
|
95
|
+
| Grade of illustrative stills | Consistent across the video |
|
|
96
|
+
| Presenter edge treatment and shadow | |
|
|
97
|
+
|
|
98
|
+
## Failure modes seen in practice
|
|
99
|
+
|
|
100
|
+
- **A framed picture-in-picture** where the reference floats a cut-out: it changes the format's look. Confirm the presenter treatment before rendering.
|
|
101
|
+
- **Opacity fades or backing bands** added for legibility without being asked for. Prefer type treatment, and confirm visible changes to the presenter or headline.
|
|
102
|
+
- **Authored scenes as small elements on an empty dark stage.**
|
|
103
|
+
- **The same image used in the hook, an item and a recap.**
|
|
104
|
+
- **Collage cells with unreadable page text.**
|
|
105
|
+
- **Generic stand-ins** (an unbranded laptop app) where the voice names a product: show its real mark or interface.
|
|
106
|
+
- **A brand character generated without its reference image**, coming out off-model.
|
|
107
|
+
- **The presenter covering the key element** of a real capture.
|
|
108
|
+
- **Consent banners left visible** on page captures.
|
|
109
|
+
|
|
110
|
+
## Review checklist
|
|
111
|
+
|
|
112
|
+
1. **Contact sheet:** one still per shot at its key word.
|
|
113
|
+
2. **Named products:** every named product, person or tool shows its real mark or interface when named.
|
|
114
|
+
3. **Reuse:** list every visual; confirm none repeats except a deliberate callback.
|
|
115
|
+
4. **Presenter:** check placement per shot (nothing important covered) and matte edges on hair and hands in motion.
|
|
116
|
+
5. **Frame use:** authored scenes fill the frame; overlays are readable at phone size.
|
|
117
|
+
6. **Captions:** the band is stable and safe areas are respected for every element.
|
|
118
|
+
7. **Labelling:** report what is authentic, illustrative and generated.
|
|
119
|
+
8. **Audio:** follows the user's choices; report what was not heard.
|
|
120
|
+
|
|
121
|
+
## Pilot evidence (example values, not defaults)
|
|
122
|
+
|
|
123
|
+
| Measure | Pilot value |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Total | ≈ 130 s; hook 4.2 s; CTA 10.5 s |
|
|
126
|
+
| Images | 37 shots, none repeated |
|
|
127
|
+
| Public page captures and the narrator's own screen recordings | 14 shots |
|
|
128
|
+
| The narrator's own brand assets (product mascot) | 3 |
|
|
129
|
+
| Authored scenes built from the narrator's real data (folders, files) | 3 |
|
|
130
|
+
| Illustrative generated stills | 15 |
|
|
131
|
+
| Hook collage and full-frame presenter CTA | 2 |
|
|
132
|
+
| Presenter | Floating cut-out, relocated on almost every cut |
|
|
133
|
+
|
|
134
|
+
What the user approved:
|
|
135
|
+
- the floating cut-out;
|
|
136
|
+
- real product pages and the narrator's own tools as the personal experience;
|
|
137
|
+
- one image per idea;
|
|
138
|
+
- the headline set directly on a collage that shows the real brand marks of the list.
|
|
139
|
+
|
|
140
|
+
What the user corrected: a framed presenter, an opacity fade on the cut-out, a backing band behind the headline, repeated images, empty authored scenes and generic stand-ins for named products.
|