@keepylabs/cutroom 0.5.2 → 0.5.3

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 +24 -12
  2. package/package.json +1 -1
package/cutroom.mjs CHANGED
@@ -22061,18 +22061,26 @@ async function composition(input) {
22061
22061
  return { pj, work, html, cutout, words };
22062
22062
  }
22063
22063
  var fmtT = (t) => `${t.toFixed(1)}s`;
22064
- var fxLine = (f) => Object.entries(f).filter(([k]) => !["slug", "start", "dur"].includes(k)).map(([, v]) => Array.isArray(v) ? v.join(" / ") : String(v)).join(" \xB7 ");
22064
+ var SHOWN = ["word", "text", "title", "number", "label", "eyebrow", "keyword", "items", "lines", "labels", "steps", "message", "before", "after", "note"];
22065
+ var fxLine = (f) => SHOWN.filter((k) => f[k] !== void 0 && f[k] !== "").map((k) => k === "keyword" ? `comment "${f[k]}"` : Array.isArray(f[k]) ? f[k].join(" / ") : String(f[k])).join(" \xB7 ");
22065
22066
  async function planReel(input) {
22066
22067
  const { pj, work, html, cutout, words } = await composition(input);
22067
22068
  const effects = [...input.effects ?? []].sort((a, b) => Number(a.start) - Number(b.start));
22068
22069
  const peak = (f) => {
22069
22070
  const d = Math.min(Math.max(Number(f.dur) || 2.5, 1.2), 5);
22070
- return Math.min(pj.duration - 0.1, Number(f.start) + Math.max(d * 0.5, d - 0.55));
22071
+ return Math.min(pj.duration - 0.1, Number(f.start) + Math.max(d * 0.5, d - (f.slug === "comment-dm" ? 0.32 : 0.55)));
22071
22072
  };
22072
22073
  const plainAt = effects.length && Number(effects[0].start) > 2 ? 1.2 : null;
22074
+ const styles = await call("styles");
22075
+ const title = (list, slug3) => (list.find((x) => x.slug === slug3)?.title ?? String(slug3)).replace(/\s*\(.*\)$/, "");
22076
+ const said = (from, to) => words.filter((w) => w.t >= from - 0.05 && w.t < to).map((w) => w.w).join(" ");
22077
+ const ends = effects.map((f, k) => Math.min(effects[k + 1] ? Number(effects[k + 1].start) : pj.duration, Number(f.start) + Math.min(Math.max(Number(f.dur) || 2.5, 1.2), 5)));
22073
22078
  const moments = [
22074
- ...plainAt !== null ? [{ t: plainAt, what: `${input.layout ?? "full-frame"} + ${input.caption ?? "two-tier"} captions`, tag: "layout + captions", at: plainAt }] : [],
22075
- ...effects.map((f) => ({ t: Number(f.start), what: `${f.slug}${fxLine(f) ? `: ${fxLine(f)}` : ""}`, tag: String(f.slug), at: peak(f) }))
22079
+ ...plainAt !== null ? [{ t: plainAt, end: Number(effects[0].start), what: "just you and the captions", tag: "you + captions", at: plainAt }] : [],
22080
+ ...effects.map((f, k) => {
22081
+ const name = title(styles.effects, f.slug);
22082
+ return { t: Number(f.start), end: ends[k], what: `${name}${fxLine(f) ? `: ${fxLine(f)}` : ""}`, tag: name, at: peak(f) };
22083
+ })
22076
22084
  ];
22077
22085
  const shots = join3(work, "storyboard");
22078
22086
  rmSync(shots, { recursive: true, force: true });
@@ -22100,11 +22108,15 @@ ${tail(t.stderr)}`);
22100
22108
  const plan = { layout: input.layout ?? "full-frame", caption: input.caption ?? "two-tier", effects, images: input.images, emphasis: input.emphasis, fixes: input.fixes };
22101
22109
  writeFileSync2(planFile(pj.folder), JSON.stringify(plan, null, 2));
22102
22110
  const text = [
22103
- `PLAN ${pj.name} ${pj.duration.toFixed(1)}s ${effects.length} effects layout: ${plan.layout} captions: ${plan.caption}${cutout ? " (cut-out ready)" : ""}`,
22104
- ...moments.map((m) => `${fmtT(m.t).padStart(6)} ${m.what}`),
22111
+ `Plan for ${pj.name} (${pj.duration.toFixed(1)}s). Layout: ${title(styles.layouts, plan.layout)}. Captions: ${title(styles.captions, plan.caption)}.${cutout ? " You are cut out of the background for this one (done)." : ""}`,
22112
+ "",
22113
+ "| Time | What you say | What shows on screen |",
22114
+ "| - | - | - |",
22115
+ ...moments.map((m) => `| ${m.t.toFixed(1)} to ${m.end.toFixed(1)}s | ${said(m.t, m.end).replace(/\|/g, "/") || "(pause)"} | ${m.what.replace(/\|/g, "/")} |`),
22116
+ "",
22105
22117
  `Storyboard: ${board}`
22106
22118
  ].join("\n");
22107
- return { text, storyboard: board, plan, words_used: words.length, next: "Show the storyboard. On changes, plan_reel again with the new choice. On a go, render_reel with plan:true and draft:true." };
22119
+ return { text, storyboard: board, plan, words_used: words.length, next: "Show this table as it is (plain words, no effect codes) and the storyboard. On changes, plan_reel again with the new choice. On a go, render_reel with plan:true and draft:true." };
22108
22120
  }
22109
22121
  async function renderReel(input) {
22110
22122
  if (input.plan) {
@@ -22128,7 +22140,7 @@ ${tail(r.stderr) || tail(r.stdout)}`);
22128
22140
  renameSync(tmp, out);
22129
22141
  const render_s = Math.round((Date.now() - started) / 100) / 10;
22130
22142
  const qa = await qaVideo({ video: out, write_report: true, layout: input.layout });
22131
- return { video: out, render_s, ...cutout?.made_s ? { cutout_s: cutout.made_s } : {}, qa, next: qa.passed ? "Watch it once, then it's ready to post." : "Fix the issues listed (usually: move an effect, fix a word, or re-record a bit), then render_reel again." };
22143
+ return { video: out, render_s, ...cutout?.made_s ? { cutout_s: cutout.made_s } : {}, qa, next: qa.passed ? "Watch it once, then it's ready to post." : qa.technical?.length ? "Fix the technical issues listed, then render again." : "Look at each flagged spot yourself (grab the frame) and tell the user which are real problems, with the fix, and which are the design." };
22132
22144
  }
22133
22145
  async function qaVideo(input) {
22134
22146
  const file = resolve2(input.video.replace(/^~/, homedir2()));
@@ -22192,7 +22204,7 @@ Tear down a creator: call creator_teardown (compare_to = the user's own handle i
22192
22204
  Finish a video (raw talking-head file: edit it, add captions or effects, make it a reel):
22193
22205
  1. prepare_footage with the file path.
22194
22206
  2. From the timed transcript, list words that look misheard (names, brands) and confirm spellings with the user.
22195
- 3. Plan: list_styles (share its gallery link so the user can see each style moving), then choose one layout, one caption style and 2 to 4 effects tied to moments in the transcript (a number gets number-pop, steps get checklist, the CTA line gets comment-dm or follow-tap). Effect text uses only the user's own words and numbers. Call plan_reel with that choice and show the user the plan lines and the storyboard (one frame per moment). They change anything ("move the checklist to 30s", "swap orbit for card wall"): plan_reel again. Nothing renders until they say go. No B-roll unless they ask for it. Effects and layouts marked needs "cutout" (text behind you, orbit, card wall, cut-out layouts) first cut the speaker out of the background on this computer: about 6 seconds of work per second of video, once per video; say so before you start. Ones marked needs "images" take the user's own pictures as file paths; ask for them, or use the fallback the effect describes.
22207
+ 3. Plan: list_styles (share its gallery link so the user can see each style moving), then get_method('edit_plan') and follow it: every scene of the transcript gets a visual, shown as a plain table (Time | What you say | What shows on screen), never effect codes. Call plan_reel with that choice and show the user its table and the storyboard (one frame per scene). They change anything ("move the checklist to 30s", "swap orbit for card wall"): plan_reel again. Nothing renders until they say go. No B-roll unless they ask for it. Effects and layouts marked needs "cutout" (text behind you, orbit, card wall, cut-out layouts) first cut the speaker out of the background on this computer: about 6 seconds of work per second of video, once per video; say so before you start. Ones marked needs "images" take the user's own pictures as file paths; ask for them, or use the fallback the effect describes.
22196
22208
  4. On a go: render_reel with plan:true and draft:true; tell them where the file is and what QA found.
22197
22209
  5. After they approve the draft: render_reel with plan:true (no draft) for the final. After changes: plan_reel, then render again.
22198
22210
  6. QA "look" lines are spots to check, not failures: busy edits often get flagged on purpose (cards running off the edge, a card over a board). For each flagged time, grab that frame with ffmpeg and look at it yourself, then tell the user which ones are real problems (with the fix) and which are the design. Technical issues (frozen picture, loudness, black frames) are real; say if a freeze is a deliberate hold.
@@ -22201,7 +22213,7 @@ Brand: before a user's first render, call show_brand; if none is set, offer to s
22201
22213
 
22202
22214
  Results are saved in ~/Cutroom. Study only for other creators' reels: never reuse their words, footage or branding.`;
22203
22215
  await serveStdio(() => {
22204
- const server = new McpServer({ name: "cutroom", version: "0.5.2" }, { instructions: INSTRUCTIONS });
22216
+ const server = new McpServer({ name: "cutroom", version: "0.5.3" }, { instructions: INSTRUCTIONS });
22205
22217
  server.registerTool("cutroom_doctor", {
22206
22218
  description: "Check Cutroom is ready: ffmpeg on this computer and an active membership key. Returns each check with the exact fix. Run it first, and whenever a tool fails.",
22207
22219
  inputSchema: object({})
@@ -22269,7 +22281,7 @@ ${m.text}`;
22269
22281
  project: string2().min(1).describe("The project folder returned by prepare_footage."),
22270
22282
  layout: string2().default("full-frame"),
22271
22283
  caption: string2().default("two-tier"),
22272
- effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(5).optional() }).passthrough()).max(12).default([]).describe("Same as render_reel: e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]."),
22284
+ effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(5).optional() }).passthrough()).max(24).default([]).describe("Same as render_reel: e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]."),
22273
22285
  images: array(string2()).max(8).optional().describe("Pictures for the layout, full paths on this computer."),
22274
22286
  emphasis: array(string2()).optional(),
22275
22287
  fixes: array(object({ from: string2(), to: string2() })).optional().describe("Transcript corrections; to:'' drops the word.")
@@ -22290,7 +22302,7 @@ ${r.next}` }, { type: "image", data: readFileSync4(r.storyboard).toString("base6
22290
22302
  project: string2().min(1).describe("The project folder returned by prepare_footage."),
22291
22303
  layout: string2().default("full-frame"),
22292
22304
  caption: string2().default("two-tier"),
22293
- effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(5).optional() }).passthrough()).max(12).default([]).describe("e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]. Params per effect are in list_styles. Effect params image / images are picture files on this computer (full paths, jpg/png/webp); they are copied into the render folder, only the file name goes to Cutroom."),
22305
+ effects: array(object({ slug: string2(), start: number2().min(0), dur: number2().min(1.2).max(5).optional() }).passthrough()).max(24).default([]).describe("e.g. [{slug:'number-pop', start:3.2, number:'24', label:'hours later'}]. Params per effect are in list_styles. Effect params image / images are picture files on this computer (full paths, jpg/png/webp); they are copied into the render folder, only the file name goes to Cutroom."),
22294
22306
  images: array(string2()).max(8).optional().describe("Pictures for the layout (image-cluster: 3 to 5, cutout-grid: 1 to 8, cutout-backdrop: optional): full paths of jpg, png or webp files on this computer. 8 pictures at most per reel, layout and effects together."),
22295
22307
  emphasis: array(string2()).optional().describe("Words to highlight in captions (otherwise numbers, then the longest word of each phrase)."),
22296
22308
  fixes: array(object({ from: string2(), to: string2() })).optional().describe("Transcript corrections; to:'' drops the word."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepylabs/cutroom",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Cutroom for Claude Code: hooks and scripts with scoring, reel breakdowns, creator teardowns, and finishing your own videos with QA. Needs a Keepy Labs membership key.",
5
5
  "type": "module",
6
6
  "bin": {