@keepylabs/cutroom 0.10.0 → 0.10.1
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/cutroom.mjs +34 -24
- package/package.json +1 -1
package/cutroom.mjs
CHANGED
|
@@ -21554,7 +21554,7 @@ import { readFileSync as readFileSync10 } from "node:fs";
|
|
|
21554
21554
|
|
|
21555
21555
|
// src/community/local/render.ts
|
|
21556
21556
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
21557
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as
|
|
21557
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync5, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
21558
21558
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
21559
21559
|
import { basename as basename2, extname, join as join9, resolve as resolve3 } from "node:path";
|
|
21560
21560
|
|
|
@@ -22081,7 +22081,7 @@ async function transcribeVideo(video, dir2, language) {
|
|
|
22081
22081
|
}
|
|
22082
22082
|
|
|
22083
22083
|
// src/community/local/tools.ts
|
|
22084
|
-
var VERSION = "0.10.
|
|
22084
|
+
var VERSION = "0.10.1";
|
|
22085
22085
|
var GATEWAY = () => (process.env.CUTROOM_GATEWAY || "https://keepylabs.com/api/cutroom").replace(/\/$/, "");
|
|
22086
22086
|
var HOME = () => process.env.CUTROOM_HOME || join6(homedir2(), "Cutroom");
|
|
22087
22087
|
async function call(op, body = {}, fetchImpl = fetch) {
|
|
@@ -22381,7 +22381,7 @@ var startVideoNotes2 = (folder, transcript) => startVideoNotes(folder, transcrip
|
|
|
22381
22381
|
var { setStatus: setStatus2, addCheck: addCheck2 } = studio_core_exports;
|
|
22382
22382
|
|
|
22383
22383
|
// src/community/local/custom.ts
|
|
22384
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
22384
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
22385
22385
|
import { join as join8 } from "node:path";
|
|
22386
22386
|
var isCustom = (f) => f.slug === "custom";
|
|
22387
22387
|
var customDur = (f) => Math.min(Math.max(Number(f.dur) || 4, 1.5), 15);
|
|
@@ -22394,6 +22394,15 @@ var cardOf = (work, id) => {
|
|
|
22394
22394
|
}
|
|
22395
22395
|
};
|
|
22396
22396
|
var same = (a, b) => (a ?? "").trim().toLowerCase() === (b ?? "").trim().toLowerCase();
|
|
22397
|
+
var near = (a, b) => Math.abs(a - b) < 0.05;
|
|
22398
|
+
function savedFor(work, s, dur, layout2) {
|
|
22399
|
+
if (!existsSync6(dir(work))) return null;
|
|
22400
|
+
return readdirSync2(dir(work)).filter((f) => f.endsWith(".json")).map((f) => ({ f, at: statSync(join8(dir(work), f)).mtimeMs })).sort((a, b) => b.at - a.at).map(({ f }) => cardOf(work, f.replace(/\.json$/, ""))).find((c) => !!c && near(c.start, s.start) && near(c.dur, dur) && same(c.request, s.request) && c.layout === layout2 && existsSync6(join8(dir(work), `${c.id}.html`))) ?? null;
|
|
22401
|
+
}
|
|
22402
|
+
function previousAt(work, start) {
|
|
22403
|
+
if (!existsSync6(dir(work))) return null;
|
|
22404
|
+
return readdirSync2(dir(work)).filter((f) => f.endsWith(".json")).map((f) => ({ f, at: statSync(join8(dir(work), f)).mtimeMs })).sort((a, b) => b.at - a.at).map(({ f }) => cardOf(work, f.replace(/\.json$/, ""))).find((c) => !!c && near(c.start, start)) ?? null;
|
|
22405
|
+
}
|
|
22397
22406
|
async function waitScene(id, wait = (ms) => new Promise((r) => setTimeout(r, ms)), deadline = Date.now() + 7 * 6e4) {
|
|
22398
22407
|
while (Date.now() < deadline) {
|
|
22399
22408
|
await wait(8e3);
|
|
@@ -22408,9 +22417,10 @@ async function ensureCustom(work, slots, ctx) {
|
|
|
22408
22417
|
let made = 0;
|
|
22409
22418
|
const out = await Promise.all(slots.map(async (s) => {
|
|
22410
22419
|
const dur = customDur(s);
|
|
22411
|
-
const
|
|
22412
|
-
|
|
22413
|
-
|
|
22420
|
+
const given = s.scene ? cardOf(work, s.scene) : null;
|
|
22421
|
+
const have = given && existsSync6(join8(dir(work), `${given.id}.html`)) && near(given.start, s.start) && near(given.dur, dur) && same(given.request, s.request) && given.layout === ctx.layout ? given : s.redo ? null : savedFor(work, s, dur, ctx.layout);
|
|
22422
|
+
if (have && !s.redo) return { ...s, dur, scene: have.id, title: have.title, shows: have.shows, redo: void 0 };
|
|
22423
|
+
const before = given ?? previousAt(work, s.start);
|
|
22414
22424
|
const { id } = await call("scene_start", {
|
|
22415
22425
|
start: s.start,
|
|
22416
22426
|
dur,
|
|
@@ -22420,9 +22430,9 @@ async function ensureCustom(work, slots, ctx) {
|
|
|
22420
22430
|
about: ctx.about,
|
|
22421
22431
|
request: s.request,
|
|
22422
22432
|
job: s.job,
|
|
22423
|
-
...
|
|
22433
|
+
...before ? { previous: { shows: before.shows } } : {}
|
|
22424
22434
|
});
|
|
22425
|
-
const scene = await waitScene(id);
|
|
22435
|
+
const scene = await waitScene(id, ctx.wait);
|
|
22426
22436
|
writeFileSync6(join8(dir(work), `${id}.html`), scene.html);
|
|
22427
22437
|
const card = { id, start: s.start, dur, request: s.request, job: scene.job, title: scene.title, shows: scene.shows, layout: ctx.layout };
|
|
22428
22438
|
writeFileSync6(join8(dir(work), `${id}.json`), JSON.stringify(card, null, 2));
|
|
@@ -22552,7 +22562,7 @@ function localImages(work, layoutImages, effects) {
|
|
|
22552
22562
|
if (/^assets\/[\w.-]+$/.test(raw) && existsSync7(join9(work, raw))) return raw;
|
|
22553
22563
|
const file = resolve3(raw.replace(/^~(?=$|\/)/, homedir3()));
|
|
22554
22564
|
if (!names.has(file)) {
|
|
22555
|
-
if (!existsSync7(file) || !
|
|
22565
|
+
if (!existsSync7(file) || !statSync2(file).isFile()) throw new Error(`Can't find the picture ${raw}. Use the full path of a jpg, png or webp file on this computer.`);
|
|
22556
22566
|
if (names.size >= 8) throw new Error("8 pictures at most per reel.");
|
|
22557
22567
|
const name = safeAssetName(file, names.size);
|
|
22558
22568
|
mkdirSync7(dir2, { recursive: true });
|
|
@@ -22594,7 +22604,7 @@ function headTop(work, duration3) {
|
|
|
22594
22604
|
}
|
|
22595
22605
|
function ensureCutout(work, duration3) {
|
|
22596
22606
|
const footage = join9(work, "footage.mp4"), cut = join9(work, "cut.webm"), stamp = join9(work, "cut.json"), still = join9(work, "assets", "still.jpg");
|
|
22597
|
-
const st =
|
|
22607
|
+
const st = statSync2(footage);
|
|
22598
22608
|
const key = { size: st.size, mtime: Math.round(st.mtimeMs) };
|
|
22599
22609
|
let cached2 = null;
|
|
22600
22610
|
try {
|
|
@@ -22695,7 +22705,7 @@ async function planReel(input) {
|
|
|
22695
22705
|
} finally {
|
|
22696
22706
|
rmSync5(join9(work, "index.html"), { force: true });
|
|
22697
22707
|
}
|
|
22698
|
-
const pngs = existsSync7(shots) ?
|
|
22708
|
+
const pngs = existsSync7(shots) ? readdirSync3(shots).filter((f) => f.endsWith(".png")).sort() : [];
|
|
22699
22709
|
if (r.status !== 0 || pngs.length < moments.length) throw new Error(`Couldn't make the storyboard. Run cutroom_doctor.
|
|
22700
22710
|
${tail(r.stderr) || tail(r.stdout)}`);
|
|
22701
22711
|
const board = join9(pj.folder, "storyboard.jpg");
|
|
@@ -22724,8 +22734,8 @@ ${tail(t.stderr)}`);
|
|
|
22724
22734
|
].join("\n");
|
|
22725
22735
|
setStatus2(pj.folder, "plan ready (waiting for your go)", text.replace(board, "storyboard.jpg"));
|
|
22726
22736
|
const nCustom = effects.filter(isCustom).length;
|
|
22727
|
-
const scenesNote = nCustom ? ` Rows marked CUSTOM are graphics Cutroom made for those lines (${made} new this time; each new one counts toward the daily limit of custom scenes, unchanged ones are reused). To change one,
|
|
22728
|
-
return { text, storyboard: board, plan, words_used: words.length, next: `Show this table as it is (plain words, no effect codes) and the storyboard.${scenesNote} On changes, plan_reel again with the
|
|
22737
|
+
const scenesNote = nCustom ? ` Rows marked CUSTOM are graphics Cutroom made for those lines (${made} new this time; each new one counts toward the daily limit of custom scenes, unchanged ones are reused). To change one, give that slot a request (e.g. request:'a phone with the feed') or redo:true for a fresh take; to go back to a menu effect, put that effect in the slot instead.` : "";
|
|
22738
|
+
return { text, storyboard: board, plan, words_used: words.length, next: `Show this table as it is (plain words, no effect codes) and the storyboard.${scenesNote} On changes, plan_reel again with the whole choice; a custom slot with the same time, length and request reuses its scene (no new one is written), so only change the slots they asked to change. On a go, render_reel with plan:true and draft:true.` };
|
|
22729
22739
|
}
|
|
22730
22740
|
async function renderReel(input) {
|
|
22731
22741
|
if (input.plan) {
|
|
@@ -23066,7 +23076,7 @@ import { isAbsolute as isAbsolute3, join as join11, resolve as resolve5 } from "
|
|
|
23066
23076
|
|
|
23067
23077
|
// src/carousel.ts
|
|
23068
23078
|
import { execFileSync as execFileSync2, spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
|
|
23069
|
-
import { copyFileSync as copyFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as
|
|
23079
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
23070
23080
|
import { homedir as homedir4 } from "node:os";
|
|
23071
23081
|
import { basename as basename3, dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join10, resolve as resolve4 } from "node:path";
|
|
23072
23082
|
|
|
@@ -23102,13 +23112,13 @@ function paletteFromRgb(px, k = 6) {
|
|
|
23102
23112
|
const merged = [];
|
|
23103
23113
|
for (const b of [...buckets.values()].sort((x, y) => y.n - x.n)) {
|
|
23104
23114
|
const c = { n: b.n, r: b.r / b.n, g: b.g / b.n, b: b.b / b.n };
|
|
23105
|
-
const
|
|
23106
|
-
if (
|
|
23107
|
-
const n =
|
|
23108
|
-
|
|
23109
|
-
|
|
23110
|
-
|
|
23111
|
-
|
|
23115
|
+
const near2 = merged.find((m) => Math.hypot(m.r - c.r, m.g - c.g, m.b - c.b) < 48);
|
|
23116
|
+
if (near2) {
|
|
23117
|
+
const n = near2.n + c.n;
|
|
23118
|
+
near2.r = (near2.r * near2.n + c.r * c.n) / n;
|
|
23119
|
+
near2.g = (near2.g * near2.n + c.g * c.n) / n;
|
|
23120
|
+
near2.b = (near2.b * near2.n + c.b * c.n) / n;
|
|
23121
|
+
near2.n = n;
|
|
23112
23122
|
} else merged.push(c);
|
|
23113
23123
|
}
|
|
23114
23124
|
const hex = (v) => Math.round(v).toString(16).padStart(2, "0");
|
|
@@ -23150,7 +23160,7 @@ async function saveCarousel(meta2, dir2, opts = {}) {
|
|
|
23150
23160
|
files.push(target);
|
|
23151
23161
|
}
|
|
23152
23162
|
} else if (opts.folder) {
|
|
23153
|
-
const names =
|
|
23163
|
+
const names = readdirSync4(opts.folder).filter((n) => IMAGE_EXT.test(n)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
23154
23164
|
if (!names.length) throw new Error(`No images in ${opts.folder} (jpg, png, webp).`);
|
|
23155
23165
|
for (const [i, n] of names.entries()) {
|
|
23156
23166
|
const target = join10(slidesDir, `${String(i + 1).padStart(2, "0")}${extname2(n).toLowerCase()}`);
|
|
@@ -23517,7 +23527,7 @@ function findChrome(env = process.env) {
|
|
|
23517
23527
|
for (const c of cands) if (existsSync8(c)) return c;
|
|
23518
23528
|
const pw = join10(homedir4(), "Library", "Caches", "ms-playwright");
|
|
23519
23529
|
if (existsSync8(pw)) {
|
|
23520
|
-
for (const d of
|
|
23530
|
+
for (const d of readdirSync4(pw).filter((n) => /^chromium-\d+$/.test(n)).sort().reverse()) {
|
|
23521
23531
|
for (const sub of ["chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", "chrome-mac/Chromium.app/Contents/MacOS/Chromium", "chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"]) {
|
|
23522
23532
|
if (existsSync8(join10(pw, d, sub))) return join10(pw, d, sub);
|
|
23523
23533
|
}
|
|
@@ -23792,7 +23802,7 @@ ${m.text}`;
|
|
|
23792
23802
|
project: string2().min(1).describe("The project folder returned by prepare_footage."),
|
|
23793
23803
|
layout: string2().default("full-frame"),
|
|
23794
23804
|
caption: string2().default("two-tier"),
|
|
23795
|
-
effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(15).optional() }).passthrough()).max(24).default([]).describe("Same as render_reel: e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]. Menu effects last 1.2 to 5s. A custom scene, made by Cutroom for that line (Explainer Panel and Split Reel layouts): {slug:'custom', start, dur (1.5 to 15s, the whole line or idea), request (optional, what they want it to show, e.g. 'a phone scrolling a feed'), job (optional: number, comparison, process, app, social-proof, list, chart, map, emphasis)}.
|
|
23805
|
+
effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(15).optional() }).passthrough()).max(24).default([]).describe("Same as render_reel: e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]. Menu effects last 1.2 to 5s. A custom scene, made by Cutroom for that line (Explainer Panel and Split Reel layouts): {slug:'custom', start, dur (1.5 to 15s, the whole line or idea), request (optional, what they want it to show, e.g. 'a phone scrolling a feed'), job (optional: number, comparison, process, app, social-proof, list, chart, map, emphasis)}. A slot with the same start, dur and request as before reuses its scene; change request (or add redo:true) for a new one. A new scene takes about 1 to 2 minutes; they are written at the same time."),
|
|
23796
23806
|
about: string2().optional().describe("What the video is about, in a sentence or two (for custom scenes)."),
|
|
23797
23807
|
images: array(string2()).max(8).optional().describe("Pictures for the layout, full paths on this computer."),
|
|
23798
23808
|
emphasis: array(string2()).optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keepylabs/cutroom",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Cutroom for Claude Code: hooks and scripts with scoring, reel breakdowns, creator teardowns, and finishing your own videos with QA. Sign in with your Keepy Labs membership on Whop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|