ossclip 0.1.30 → 0.1.31
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/editor-dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ossclip editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-C537KpNT.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Bx2VQLP8.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ossclip",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"tsx": "^4.19.0",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@ossclip/core": "0.1.
|
|
40
|
-
"@ossclip/renderer": "0.1.
|
|
41
|
-
"@ossclip/scenes": "0.1.
|
|
39
|
+
"@ossclip/core": "0.1.31",
|
|
40
|
+
"@ossclip/renderer": "0.1.31",
|
|
41
|
+
"@ossclip/scenes": "0.1.31"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/AhsanAyaz/ossclip#readme",
|
|
44
44
|
"bugs": {
|
package/src/edit.ts
CHANGED
|
@@ -369,6 +369,11 @@ export async function startEditServer(
|
|
|
369
369
|
* file behind this URL). */
|
|
370
370
|
const coverImageUrl = (image: string | null): string | null =>
|
|
371
371
|
image === null ? null : `/api/cover/image?ts=${Math.round(statSync(image).mtimeMs)}`;
|
|
372
|
+
/** Where the Preview button's one-off render lands (handoff-cover-panel
|
|
373
|
+
* §3): a fixed name IN the workdir — never a client-named path, and never
|
|
374
|
+
* the canonical `.cover.jpg` a real Apply owns. Each preview overwrites the
|
|
375
|
+
* last; the file is scratch, like the frame-sampler's cache. */
|
|
376
|
+
const coverPreviewPath = (): string => join(workdir!, "cover-preview.jpg");
|
|
372
377
|
|
|
373
378
|
// ---- Portrait override (editor face swap, 2026-08-17) -------------------
|
|
374
379
|
// A per-project `portrait-override.<ext>` in the workdir that outranks the
|
|
@@ -1193,6 +1198,27 @@ export async function startEditServer(
|
|
|
1193
1198
|
// throw for want of a destination, and the panel says so up front
|
|
1194
1199
|
// rather than after a click.
|
|
1195
1200
|
const outPath = provenance?.out ?? (await recordedArtifactPath(".cover.jpg"));
|
|
1201
|
+
// The finished mp4's OWN span set — the RESOLVED cutlist, never the
|
|
1202
|
+
// proposal /api/cleanup serves: the panel converts the playhead
|
|
1203
|
+
// between the output clock and the source clock
|
|
1204
|
+
// (handoff-cover-panel §1), and a proposal the user's vetoes already
|
|
1205
|
+
// changed is the wrong ruler. Lenient per-span parse, [] on a
|
|
1206
|
+
// missing or corrupt production.json — a cover panel that cannot
|
|
1207
|
+
// convert must still open.
|
|
1208
|
+
let coverCutlist: Segment[] = [];
|
|
1209
|
+
try {
|
|
1210
|
+
const production = JSON.parse(
|
|
1211
|
+
await readFile(join(workdir, "production.json"), "utf8"),
|
|
1212
|
+
) as { cutlist?: unknown };
|
|
1213
|
+
if (Array.isArray(production.cutlist)) {
|
|
1214
|
+
coverCutlist = production.cutlist.flatMap((s) => {
|
|
1215
|
+
const p = SegmentSchema.safeParse(s);
|
|
1216
|
+
return p.success ? [p.data] : [];
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
} catch {
|
|
1220
|
+
// degrade — same as /api/cleanup
|
|
1221
|
+
}
|
|
1196
1222
|
return send(200, {
|
|
1197
1223
|
status: outPath === null ? "unavailable" : "ready",
|
|
1198
1224
|
...(outPath === null
|
|
@@ -1203,6 +1229,7 @@ export async function startEditServer(
|
|
|
1203
1229
|
provenance,
|
|
1204
1230
|
outPath,
|
|
1205
1231
|
imageUrl: coverImageUrl(image),
|
|
1232
|
+
cutlist: coverCutlist,
|
|
1206
1233
|
});
|
|
1207
1234
|
}
|
|
1208
1235
|
|
|
@@ -1283,6 +1310,78 @@ export async function startEditServer(
|
|
|
1283
1310
|
}
|
|
1284
1311
|
}
|
|
1285
1312
|
|
|
1313
|
+
if (url.pathname === "/api/cover/preview-image" && req.method === "GET") {
|
|
1314
|
+
if (!workdir) return send(409, { error: "no workdir open" });
|
|
1315
|
+
const image = coverPreviewPath();
|
|
1316
|
+
if (!existsSync(image)) return send(404, { error: "no preview image" });
|
|
1317
|
+
// /api/cover/image's exact posture, for its exact reason: every
|
|
1318
|
+
// preview REPLACES the file behind a URL the panel busts with ?ts,
|
|
1319
|
+
// and a cached 200 would show the old preview against the new ts.
|
|
1320
|
+
const bytes = await readFile(image);
|
|
1321
|
+
res.writeHead(200, {
|
|
1322
|
+
"content-type": "image/jpeg",
|
|
1323
|
+
"cache-control": "no-store",
|
|
1324
|
+
"content-length": String(bytes.length),
|
|
1325
|
+
});
|
|
1326
|
+
res.end(bytes);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (url.pathname === "/api/cover/preview" && req.method === "POST") {
|
|
1331
|
+
// The regenerate handler above, verbatim, up to the outPath: a
|
|
1332
|
+
// preview is a full cover render — it boots the same headless
|
|
1333
|
+
// browser (hence the same coverBusy gate), takes the same three
|
|
1334
|
+
// steerable values and nothing else, and fails with the same
|
|
1335
|
+
// 200-with-ok:false verbatim-message posture.
|
|
1336
|
+
if (!workdir) return send(409, { error: "no workdir open" });
|
|
1337
|
+
if (coverBusy) return send(409, { error: "a cover regeneration is already running" });
|
|
1338
|
+
const chunks: Buffer[] = [];
|
|
1339
|
+
for await (const c of req) chunks.push(c as Buffer);
|
|
1340
|
+
const parsed = z
|
|
1341
|
+
.object({
|
|
1342
|
+
text: z.string().optional(),
|
|
1343
|
+
atSec: CoverAtSecondsSchema.optional(),
|
|
1344
|
+
from: CoverFromSchema.optional(),
|
|
1345
|
+
})
|
|
1346
|
+
.safeParse(JSON.parse(Buffer.concat(chunks).toString() || "{}"));
|
|
1347
|
+
if (!parsed.success) return send(400, { error: parsed.error.message });
|
|
1348
|
+
coverBusy = true;
|
|
1349
|
+
try {
|
|
1350
|
+
const notes: string[] = [];
|
|
1351
|
+
await regenerateCover(
|
|
1352
|
+
workdir,
|
|
1353
|
+
{
|
|
1354
|
+
text: parsed.data.text,
|
|
1355
|
+
atSec: parsed.data.atSec,
|
|
1356
|
+
from: parsed.data.from,
|
|
1357
|
+
// The ONE-OFF path, derived HERE and never from the body (the
|
|
1358
|
+
// regenerate handler's comment owns why). Same machinery as
|
|
1359
|
+
// `ossclip cover --out` (handoff-cover-panel §3): the
|
|
1360
|
+
// canonical cover is untouched, provenance updates to
|
|
1361
|
+
// describe the previewed frame — which is what makes a
|
|
1362
|
+
// follow-up blank-field Apply adopt it via the cheap path,
|
|
1363
|
+
// and the one-off note riding back is the panel's disclosure
|
|
1364
|
+
// of exactly that.
|
|
1365
|
+
outPath: coverPreviewPath(),
|
|
1366
|
+
},
|
|
1367
|
+
{ renderCover: opts.renderCover, log: (line) => notes.push(line) },
|
|
1368
|
+
);
|
|
1369
|
+
return send(200, {
|
|
1370
|
+
ok: true,
|
|
1371
|
+
notes,
|
|
1372
|
+
// coverImageUrl's mtime rule, aimed at the preview file: the
|
|
1373
|
+
// URL changes exactly when the file does.
|
|
1374
|
+
previewImageUrl: `/api/cover/preview-image?ts=${Math.round(
|
|
1375
|
+
statSync(coverPreviewPath()).mtimeMs,
|
|
1376
|
+
)}`,
|
|
1377
|
+
});
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
return send(200, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
1380
|
+
} finally {
|
|
1381
|
+
coverBusy = false;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1286
1385
|
if (url.pathname.startsWith("/media/")) {
|
|
1287
1386
|
if (!workdir) return send(409, { error: "no workdir open" });
|
|
1288
1387
|
const file = join(workdir, decodeURIComponent(url.pathname.slice("/media/".length)));
|
package/src/produce.ts
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
analyze,
|
|
24
24
|
applyOverrides,
|
|
25
25
|
applyRepairs,
|
|
26
|
+
isParkedOverrideKey,
|
|
27
|
+
parkedOverrideBaseKey,
|
|
28
|
+
remapSceneOverrides,
|
|
26
29
|
assembleScenes,
|
|
27
30
|
buildCaptionLines,
|
|
28
31
|
buildCutlist,
|
|
@@ -1894,6 +1897,20 @@ export function layoutSlotAspects(frame: {
|
|
|
1894
1897
|
});
|
|
1895
1898
|
}
|
|
1896
1899
|
|
|
1900
|
+
/**
|
|
1901
|
+
* One orphaned scene edit, one honest sentence. A parked key (`…#orphaned`,
|
|
1902
|
+
* handoff-edit-anchoring) matches no cue BY DESIGN, so it surfaces in
|
|
1903
|
+
* `applyOverrides`' orphan list on every run — and "dropped" would tell the
|
|
1904
|
+
* user an edit is gone while the doc still holds it, anchor intact, waiting
|
|
1905
|
+
* for a plan that has its words again. Pure so both sentences are testable
|
|
1906
|
+
* without a produce run.
|
|
1907
|
+
*/
|
|
1908
|
+
export function orphanEditLine(id: string): string {
|
|
1909
|
+
return isParkedOverrideKey(id)
|
|
1910
|
+
? ` ⚠ edit for ${parkedOverrideBaseKey(id)} is parked — its words are not in this plan`
|
|
1911
|
+
: ` ⚠ edit for ${id} dropped — the plan no longer has that scene`;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1897
1914
|
async function preflight(bin: string, hint: string): Promise<void> {
|
|
1898
1915
|
try {
|
|
1899
1916
|
await run(bin, ["-version"], { allowNonZero: true });
|
|
@@ -3180,6 +3197,24 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
3180
3197
|
const { cues: assembled, dropped } = assembleScenes(scenes, transcript, map);
|
|
3181
3198
|
for (const d of dropped) console.log(` ⚠ scene ${d.id} dropped: ${d.reason}`);
|
|
3182
3199
|
|
|
3200
|
+
// Re-key scene edits whose positional id no longer means the moment they
|
|
3201
|
+
// were made on (handoff-edit-anchoring; §137 is the caption precedent — in
|
|
3202
|
+
// the field, scene-4 was a TerminalMock in one plan and a FlowDiagram in
|
|
3203
|
+
// the next, and the user's edit landed on the impostor). Placed HERE,
|
|
3204
|
+
// before ANY consumer of `overrideDoc.scenes`: the first `applyOverrides`
|
|
3205
|
+
// pass, `splitThenDropHidden` and the pinned-timing reclamp all join by id,
|
|
3206
|
+
// and a stale key at any of them bakes the misapply this plan exists to
|
|
3207
|
+
// kill — a `hidden` on a renumbered scene would hide the impostor, not the
|
|
3208
|
+
// moved moment. `assembled` is enough for the match: the remap reads only
|
|
3209
|
+
// anchored graphic cues, and take ids (which don't exist until the fill)
|
|
3210
|
+
// carry no anchors and are left untouched by design. Notes are non-empty
|
|
3211
|
+
// exactly when the doc changed, which is what earns the write-back its
|
|
3212
|
+
// turn at the sanctioned write below.
|
|
3213
|
+
const sceneRemap = remapSceneOverrides(overrideDoc, assembled);
|
|
3214
|
+
overrideDoc = sceneRemap.doc;
|
|
3215
|
+
for (const n of sceneRemap.notes) console.log(` ▸ ${n}`);
|
|
3216
|
+
const sceneKeysRemapped = sceneRemap.notes.length > 0;
|
|
3217
|
+
|
|
3183
3218
|
// ---- Framing plan → props (2026-08-16 incident) --------------------------
|
|
3184
3219
|
// The plan used to be BAKED here: every window cropped, scaled and
|
|
3185
3220
|
// re-encoded into a content-<hash>.mp4 that replaced the source for the
|
|
@@ -3438,7 +3473,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
3438
3473
|
console.log(`▸ applied your edits to ${editedCount - orphans.length - hiddenIds.length} scene(s)`);
|
|
3439
3474
|
}
|
|
3440
3475
|
for (const id of orphans) {
|
|
3441
|
-
console.log(
|
|
3476
|
+
console.log(orphanEditLine(id));
|
|
3442
3477
|
}
|
|
3443
3478
|
|
|
3444
3479
|
const graphicCues = sceneCues.filter((c) => c.kind !== "plain");
|
|
@@ -4295,7 +4330,12 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
4295
4330
|
// re-report "the cut removed it" on every later run. It does NOT spend the
|
|
4296
4331
|
// `.bak` — retiring a redundant key is not the cut re-anchoring the backup
|
|
4297
4332
|
// exists to survive.
|
|
4298
|
-
|
|
4333
|
+
// `sceneKeysRemapped` joins for the caption-migration reason: a scene edit
|
|
4334
|
+
// that followed its anchor to a new id (handoff-edit-anchoring) is keyed
|
|
4335
|
+
// right in memory but wrong on disk, and every re-keyed entry was just
|
|
4336
|
+
// printed by name. It does NOT spend the `.bak` — the remap moves keys, it
|
|
4337
|
+
// rewrites no values a user would need to recover.
|
|
4338
|
+
if (cutResult.changed || captionKeysReanchored || hidesPruned || sceneKeysRemapped) {
|
|
4299
4339
|
await writeOverrideDoc(overridesPath, overrideDoc, { refreshBackup: cutResult.changed });
|
|
4300
4340
|
console.log(overridesWriteLine(cutResult.changed));
|
|
4301
4341
|
}
|