@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.
- package/cutroom.mjs +35 -6
- 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
|
-
|
|
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
|
|
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:
|
|
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
|
-
...
|
|
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:
|
|
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.
|
|
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": {
|