@keepylabs/cutroom 0.5.4 → 0.5.5

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 +35 -6
  2. package/package.json +1 -1
package/cutroom.mjs CHANGED
@@ -21549,7 +21549,9 @@ function probe(file) {
21549
21549
  const info = JSON.parse(execFileSync("ffprobe", ["-v", "error", "-print_format", "json", "-show_format", "-show_streams", file], { encoding: "utf8" }));
21550
21550
  const v = info.streams.find((s) => s.codec_type === "video");
21551
21551
  if (!v) throw new Error("That file has no video.");
21552
- return { duration: Number(info.format.duration), width: v.width, height: v.height, has_audio: info.streams.some((s) => s.codec_type === "audio") };
21552
+ const rot = Math.abs(Number(v.side_data_list?.find((d) => d.rotation !== void 0)?.rotation ?? v.tags?.rotate ?? 0)) % 180;
21553
+ const [width, height] = rot === 90 ? [v.height, v.width] : [v.width, v.height];
21554
+ return { duration: Number(info.format.duration), width, height, has_audio: info.streams.some((s) => s.codec_type === "audio") };
21553
21555
  }
21554
21556
  function parseCuts(log) {
21555
21557
  return [...log.matchAll(/pts_time:([\d.]+)/g)].map((m) => Math.round(Number(m[1]) * 100) / 100).filter((t, i, a) => i === 0 || t - a[i - 1] > 0.3);
@@ -21639,6 +21641,7 @@ import { homedir } from "node:os";
21639
21641
  import { join as join2, resolve } from "node:path";
21640
21642
  import { Readable } from "node:stream";
21641
21643
  import { pipeline } from "node:stream/promises";
21644
+ var VERSION = "0.5.5";
21642
21645
  var GATEWAY = () => (process.env.CUTROOM_GATEWAY || "https://keepylabs.com/api/cutroom").replace(/\/$/, "");
21643
21646
  var HOME = () => process.env.CUTROOM_HOME || join2(homedir(), "Cutroom");
21644
21647
  async function call(op, body = {}, fetchImpl = fetch) {
@@ -21682,7 +21685,7 @@ async function doctor() {
21682
21685
  } catch (e) {
21683
21686
  checks.push({ name: "Cutroom membership key", ok: false, fix: e.message });
21684
21687
  }
21685
- return { ready: checks.every((c) => c.ok), checks, member, results_folder: HOME() };
21688
+ return { ready: checks.every((c) => c.ok), helper_version: VERSION, checks, member, results_folder: HOME() };
21686
21689
  }
21687
21690
  function approxWords(text, duration3) {
21688
21691
  const words = (text ?? "").split(/\s+/).filter(Boolean);
@@ -21929,10 +21932,12 @@ ${meta2.error ?? tail(t.stderr)}`);
21929
21932
  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.`);
21930
21933
  const project = { name, folder, source, footage, duration: Math.round(duration3 * 100) / 100, words, removed_s: Math.round((p.duration - duration3) * 10) / 10, segments };
21931
21934
  writeFileSync2(projectFile(folder), JSON.stringify(project, null, 2));
21935
+ const landscape = (p.width ?? 0) > (p.height ?? 0) * 1.05;
21932
21936
  return {
21933
21937
  project: folder,
21934
21938
  duration: project.duration,
21935
21939
  pauses_removed_s: project.removed_s,
21940
+ ...landscape ? { warning: "This video is landscape (wide). Cutroom crops the middle of it to make it vertical, so anything off-center gets cut, and an already-edited video (split screen, titles, captions burned in) will look wrong. Tell the user plainly and ask for the raw vertical take of them talking; continue only if they say this one is fine." } : {},
21936
21941
  transcript: words.map((w) => `${w.t.toFixed(2)} ${w.w}`).join("\n"),
21937
21942
  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."
21938
21943
  };
@@ -22080,10 +22085,34 @@ async function planReel(input) {
22080
22085
  const plainAt = effects.length && Number(effects[0].start) > 2 ? 1.2 : null;
22081
22086
  const styles = await call("styles");
22082
22087
  const title = (list, slug3) => (list.find((x) => x.slug === slug3)?.title ?? String(slug3)).replace(/\s*\(.*\)$/, "");
22083
- const said = (from, to) => words.filter((w) => w.t >= from - 0.05 && w.t < to).map((w) => w.w).join(" ");
22088
+ const sentences = words.reduce((out, w, k) => {
22089
+ if (!out.length || /[.!?]["”']?$/.test(words[k - 1]?.w ?? "")) out.push({ t: w.t, text: [] });
22090
+ out[out.length - 1].text.push(w.w);
22091
+ return out;
22092
+ }, []);
22093
+ const table = () => {
22094
+ const rows = [];
22095
+ let at = 0;
22096
+ for (const m of [...moments].sort((x, y) => x.t - y.t)) {
22097
+ if (m.t - at > 1.5) rows.push({ t: at, end: m.t, what: "just you and the captions" });
22098
+ rows.push(m);
22099
+ at = Math.max(at, m.end);
22100
+ }
22101
+ if (pj.duration - at > 1.5) rows.push({ t: at, end: pj.duration, what: "just you and the captions" });
22102
+ const merged = rows.filter((r2, k) => !(r2.what === "just you and the captions" && rows[k - 1]?.what === r2.what));
22103
+ rowStarts = merged.map((r2) => r2.t);
22104
+ return merged;
22105
+ };
22106
+ let rowStarts = [];
22107
+ const said = (from) => {
22108
+ const own2 = sentences.filter((x) => (rowStarts.filter((st) => st <= x.t + 0.3).pop() ?? rowStarts[0]) === from);
22109
+ if (own2.length) return own2.map((x) => x.text.join(" ")).join(" ");
22110
+ const cur = sentences.filter((x) => x.t <= from).slice(-1);
22111
+ return cur.length ? `(\u2026${cur[0].text.join(" ")})` : "";
22112
+ };
22084
22113
  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)));
22085
22114
  const moments = [
22086
- ...plainAt !== null ? [{ t: plainAt, end: Number(effects[0].start), what: "just you and the captions", tag: "you + captions", at: plainAt }] : [],
22115
+ ...plainAt !== null ? [{ t: 0, end: Number(effects[0].start), what: "just you and the captions", tag: "you + captions", at: plainAt }] : [],
22087
22116
  ...effects.map((f, k) => {
22088
22117
  const name = title(styles.effects, f.slug);
22089
22118
  return { t: Number(f.start), end: ends[k], what: `${name}${fxLine(f) ? `: ${fxLine(f)}` : ""}`, tag: name, at: peak(f) };
@@ -22119,7 +22148,7 @@ ${tail(t.stderr)}`);
22119
22148
  "",
22120
22149
  "| Time | What you say | What shows on screen |",
22121
22150
  "| - | - | - |",
22122
- ...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, "/")} |`),
22151
+ ...table().map((m) => `| ${m.t.toFixed(1)} to ${m.end.toFixed(1)}s | ${said(m.t).replace(/\|/g, "/") || "(pause)"} | ${m.what.replace(/\|/g, "/")} |`),
22123
22152
  "",
22124
22153
  `Storyboard: ${board}`
22125
22154
  ].join("\n");
@@ -22214,7 +22243,7 @@ QA flags are spots to check: look at each flagged frame yourself and say which a
22214
22243
 
22215
22244
  Results are saved in ~/Cutroom. Other creators' reels are for study only: never reuse their words, footage or branding.`;
22216
22245
  await serveStdio(() => {
22217
- const server = new McpServer({ name: "cutroom", version: "0.5.4" }, { instructions: INSTRUCTIONS });
22246
+ const server = new McpServer({ name: "cutroom", version: VERSION }, { instructions: INSTRUCTIONS });
22218
22247
  server.registerTool("cutroom_doctor", {
22219
22248
  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.",
22220
22249
  inputSchema: object({})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepylabs/cutroom",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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": {