@keepylabs/cutroom 0.5.3 → 0.5.4

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 +22 -20
  2. package/package.json +1 -1
package/cutroom.mjs CHANGED
@@ -21894,6 +21894,10 @@ function cutAndNormalize(src, out, segments) {
21894
21894
  if (r.status !== 0) throw new Error(`Couldn't cut the footage:
21895
21895
  ${tail(r.stderr)}`);
21896
21896
  }
21897
+ function looksLikeSpeech(words, duration3) {
21898
+ const readable = words.filter((w) => new RegExp("\\p{L}{2,}|\\d", "u").test(w.w) && !/\uFFFD/.test(w.w));
21899
+ return readable.length >= Math.max(3, duration3 * 0.6) && readable.length >= words.length * 0.8;
21900
+ }
21897
21901
  async function prepareFootage(input) {
21898
21902
  const source = resolve2(input.video.replace(/^~/, homedir2()));
21899
21903
  if (!existsSync2(source)) throw new Error(`No such file: ${source}`);
@@ -21909,7 +21913,8 @@ async function prepareFootage(input) {
21909
21913
  const footage = join3(work, "footage.mp4");
21910
21914
  cutAndNormalize(source, footage, segments);
21911
21915
  const duration3 = probe(footage).duration;
21912
- const t = sh("npx", ["--yes", HF, "transcribe", "footage.mp4", "--json", "-d", "."], work);
21916
+ const lang = (input.language ?? "en").toLowerCase().slice(0, 5);
21917
+ const t = sh("npx", ["--yes", HF, "transcribe", "footage.mp4", "--json", "-d", ".", ...lang === "en" ? [] : ["--language", lang, "-m", "large-v3"]], work);
21913
21918
  const meta2 = (() => {
21914
21919
  try {
21915
21920
  return JSON.parse(t.stdout.trim().split("\n").pop() ?? "{}");
@@ -21921,6 +21926,7 @@ async function prepareFootage(input) {
21921
21926
  ${meta2.error ?? tail(t.stderr)}`);
21922
21927
  const raw = JSON.parse(readFileSync3(meta2.transcriptPath ?? join3(work, "transcript.json"), "utf8"));
21923
21928
  const words = raw.map((w) => ({ t: Math.round(w.start * 100) / 100, e: Math.round(w.end * 100) / 100, w: w.text.trim() })).filter((w) => w.w);
21929
+ if (!looksLikeSpeech(words, duration3)) throw new Error(lang === "en" ? "I couldn't hear anyone talking in this video (it sounds like music or silence). Cutroom finishing needs you talking to camera. If you speak another language, tell me which one and I'll try again." : `I couldn't make out the speech in this video, even in that language. Check the sound is you talking to camera.`);
21924
21930
  const project = { name, folder, source, footage, duration: Math.round(duration3 * 100) / 100, words, removed_s: Math.round((p.duration - duration3) * 10) / 10, segments };
21925
21931
  writeFileSync2(projectFile(folder), JSON.stringify(project, null, 2));
21926
21932
  return {
@@ -21928,7 +21934,7 @@ ${meta2.error ?? tail(t.stderr)}`);
21928
21934
  duration: project.duration,
21929
21935
  pauses_removed_s: project.removed_s,
21930
21936
  transcript: words.map((w) => `${w.t.toFixed(2)} ${w.w}`).join("\n"),
21931
- next: "Check the transcript for misheard words (pass them as fixes). Get the menu with list_styles, pick a layout, a caption style and a few effects with start times from this transcript, then call render_reel."
21937
+ next: "Check the transcript for misheard words (pass them as fixes). Then plan: list_styles, get_method('edit_plan') and follow it, then plan_reel to show the storyboard. Nothing renders until the user says go."
21932
21938
  };
21933
21939
  }
21934
21940
  async function ensureFonts(work) {
@@ -22068,7 +22074,8 @@ async function planReel(input) {
22068
22074
  const effects = [...input.effects ?? []].sort((a, b) => Number(a.start) - Number(b.start));
22069
22075
  const peak = (f) => {
22070
22076
  const d = Math.min(Math.max(Number(f.dur) || 2.5, 1.2), 5);
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)));
22077
+ const at = f.slug === "frame-rip" ? d * 0.5 : Math.max(d * 0.5, d - (f.slug === "comment-dm" ? 0.32 : 0.55));
22078
+ return Math.min(pj.duration - 0.1, Number(f.start) + at);
22072
22079
  };
22073
22080
  const plainAt = effects.length && Number(effects[0].start) > 2 ? 1.2 : null;
22074
22081
  const styles = await call("styles");
@@ -22191,29 +22198,23 @@ process.env.PATH = [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", joi
22191
22198
  var result = (value) => ({ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] });
22192
22199
  var failure2 = (error2) => ({ isError: true, content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }] });
22193
22200
  var safe = (op) => Promise.resolve().then(op).then(result).catch(failure2);
22194
- var INSTRUCTIONS = `Cutroom: short-form video help for the user. Speak plainly; assume they are not technical.
22195
-
22196
- Setup or any failure: call cutroom_doctor and show a short checklist. Missing ffmpeg: ask, then run "brew install ffmpeg". Missing Node.js: ask, then run "brew install node". Key problem: tell them to reconnect with the one install command from their welcome email; never ask them to paste the key into the chat.
22201
+ var INSTRUCTIONS = `Cutroom: short-form video help. Speak plainly; the user is not technical. Never show tool or effect codes; use plain names.
22197
22202
 
22198
- Hooks, scripts, captions: call get_method ('hooks', 'script' or 'caption') and follow it step by step, scoring with score_hooks or score_script as the method says.
22203
+ Setup or any failure: cutroom_doctor, then a short checklist. Missing ffmpeg or Node.js: ask, then run "brew install ffmpeg" or "brew install node". Key problem: reconnect Cutroom with their key from the Cutroom key space on Whop; never ask them to paste the key into the chat.
22199
22204
 
22200
- Break down a reel (link or file): call breakdown_reel, then get_method('breakdown_writeup') and follow it.
22205
+ Finish a video (a raw video of them talking: edit it, captions, effects, make it a reel): get_method('finish_video') first and follow it step by step. Nothing renders until they approve the plan.
22201
22206
 
22202
- Tear down a creator: call creator_teardown (compare_to = the user's own handle if known), then get_method('teardown_writeup').
22207
+ Hooks, scripts, captions: get_method('hooks', 'script' or 'caption') and follow it.
22208
+ Break down a reel: breakdown_reel, then get_method('breakdown_writeup').
22209
+ Tear down a creator: creator_teardown (compare_to = their handle), then get_method('teardown_writeup').
22203
22210
 
22204
- Finish a video (raw talking-head file: edit it, add captions or effects, make it a reel):
22205
- 1. prepare_footage with the file path.
22206
- 2. From the timed transcript, list words that look misheard (names, brands) and confirm spellings with the user.
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.
22208
- 4. On a go: render_reel with plan:true and draft:true; tell them where the file is and what QA found.
22209
- 5. After they approve the draft: render_reel with plan:true (no draft) for the final. After changes: plan_reel, then render again.
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.
22211
+ Before a first render: show_brand; if none, offer set_brand (from colors, a website or a logo: pick background, accent, highlight, show them, save on a yes).
22211
22212
 
22212
- Brand: before a user's first render, call show_brand; if none is set, offer to set it with set_brand. If they give a website or logo instead of colors, pick 3 colors from it (background, accent, highlight), show them, and save on a yes.
22213
+ QA flags are spots to check: look at each flagged frame yourself and say which are real problems (with the fix) and which are the design.
22213
22214
 
22214
- Results are saved in ~/Cutroom. Study only for other creators' reels: never reuse their words, footage or branding.`;
22215
+ Results are saved in ~/Cutroom. Other creators' reels are for study only: never reuse their words, footage or branding.`;
22215
22216
  await serveStdio(() => {
22216
- const server = new McpServer({ name: "cutroom", version: "0.5.3" }, { instructions: INSTRUCTIONS });
22217
+ const server = new McpServer({ name: "cutroom", version: "0.5.4" }, { instructions: INSTRUCTIONS });
22217
22218
  server.registerTool("cutroom_doctor", {
22218
22219
  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.",
22219
22220
  inputSchema: object({})
@@ -22272,7 +22273,8 @@ ${m.text}`;
22272
22273
  inputSchema: object({
22273
22274
  video: string2().min(1).describe("Path to the raw video file on this computer."),
22274
22275
  name: string2().optional().describe("Short name for the output, e.g. 'trial-loop'."),
22275
- cut_pauses: boolean2().default(true).describe("Cut pauses longer than about half a second.")
22276
+ cut_pauses: boolean2().default(true).describe("Cut pauses longer than about half a second."),
22277
+ language: string2().optional().describe("Spoken language code if not English, e.g. ko, es, ja (uses a larger model, downloaded once).")
22276
22278
  })
22277
22279
  }, async (input) => safe(() => prepareFootage(input)));
22278
22280
  server.registerTool("plan_reel", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepylabs/cutroom",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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": {