@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.
Files changed (2) hide show
  1. package/cutroom.mjs +34 -24
  2. 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 readdirSync2, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync5, statSync, writeFileSync as writeFileSync7 } from "node:fs";
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.0";
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 have = s.scene ? cardOf(work, s.scene) : null;
22412
- if (have && existsSync6(join8(dir(work), `${have.id}.html`)) && !s.redo && Math.abs(have.start - s.start) < 0.05 && Math.abs(have.dur - dur) < 0.05 && same(have.request, s.request) && have.layout === ctx.layout)
22413
- return { ...s, dur, scene: have.id, title: have.title, shows: have.shows, redo: void 0 };
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
- ...have ? { previous: { shows: have.shows } } : {}
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) || !statSync(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.`);
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 = statSync(footage);
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) ? readdirSync2(shots).filter((f) => f.endsWith(".png")).sort() : [];
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, plan_reel again with that slot's request (e.g. request:'a phone with the feed'); to go back to a menu effect, put that effect in the slot instead.` : "";
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 new choice (keep the custom slots as plan_reel returned them, with their scene ids, so unchanged ones are reused). On a go, render_reel with plan:true and draft:true.` };
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 readdirSync3, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
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 near = merged.find((m) => Math.hypot(m.r - c.r, m.g - c.g, m.b - c.b) < 48);
23106
- if (near) {
23107
- const n = near.n + c.n;
23108
- near.r = (near.r * near.n + c.r * c.n) / n;
23109
- near.g = (near.g * near.n + c.g * c.n) / n;
23110
- near.b = (near.b * near.n + c.b * c.n) / n;
23111
- near.n = n;
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 = readdirSync3(opts.folder).filter((n) => IMAGE_EXT.test(n)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
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 readdirSync3(pw).filter((n) => /^chromium-\d+$/.test(n)).sort().reverse()) {
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)}. plan_reel returns each custom slot with its scene id: pass it back unchanged to reuse the 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."),
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.0",
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": {