diff --git a/README.md b/README.md index 9eaae08..bce910c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The MiniMax Code Agent communicates through the host MCP client. Business reques | --- | --- | --- | | [Dynamic Workflow Console](plugins/stevenjj33/workflow-console/) | Run and supervise MiniMax Code dynamic workflows from one page: live status, a stage-and-dependency flow graph, per-node output, approval and control, templates, and report export | [stevenjj33](https://github.com/stevenjj33) | | [MiniMax Photo](plugins/webguhui/minimax-photo/) | Layered image editing with an in-app AI assistant, local recovery and programmatic editing tools | [WebGuHui](https://github.com/WebGuHui) | -| [Cutline](plugins/klarkxy/cutline/) | Local timeline editing with MCP tools for cutting, captions, motion graphics, grading, audio and export | [klarkxy](https://github.com/klarkxy) | +| [Cutline](plugins/klarkxy/cutline/) | Browser-based video editor: timeline cuts, captions, motion graphics, grading and export, cut from chat by the Agent | [klarkxy](https://github.com/klarkxy) | | [MiniMax Video](plugins/webguhui/minimax-video/) | Local video editing with captions, a host Agent assistant, reviewable timeline edits, and MP4/MP3 export | [WebGuHui](https://github.com/WebGuHui) | | [AI Office](plugins/cristinamadureira353-svg/ai-office/) | Manage five model-powered employees in a pixel office: client tasks, chats, breaks, meetings and quota-aware offline states; uses your own API keys | [最爱双皮奶](https://github.com/cristinamadureira353-svg) | | [Token Usage Board](plugins/amszuidas/mcode-token-usage-board/) | Explore local Token usage by date, model, and session, including input, output, and cache usage | [amszuidas](https://github.com/amszuidas) | @@ -84,6 +84,17 @@ The MiniMax Code Agent communicates through the host MCP client. Business reques | [MiniMax Pet](plugins/minimax-ai/minimax-pet/) | A lightweight companion with appearance, task and settings tabs, visible-session activity, answer-needed reminders and a transparent desktop pet. Requires MiniMax Code 3.1.6 or later | [MiniMax](https://github.com/MiniMax-AI) | | [MCP OAuth Console](plugins/microbiosis/mcp-oauth-console/) | Sign in to MCP servers that require OAuth: device-code or authorization-code + PKCE flow, the `Authorization` header written into the MCP configuration, and an opt-in refresh before expiry | [Microbiosis](https://github.com/Microbiosis) | +
+Preview: Cutline + +![Cutline editor with synthetic media, titles and a multi-track timeline](plugins/klarkxy/cutline/docs/editor-en.png) + +![Cutline export dialog with range, preset, video and audio settings](plugins/klarkxy/cutline/docs/export-en.png) + +The preview uses a synthetic project. See the [Cutline README](plugins/klarkxy/cutline/README.md) for installation, editing, export and data-access details. + +
+
Preview: Token Usage Board diff --git a/README.zh-CN.md b/README.zh-CN.md index 4ae293e..9b25895 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -59,7 +59,7 @@ MiniMax Code Agent 通过宿主 MCP 客户端与 MiniApp 协作。业务请求 | --- | --- | --- | | [动态工作流控制台](plugins/stevenjj33/workflow-console/README.zh-CN.md) | 在一个页面里运行并盯住 MiniMax Code 动态工作流:实时状态、按阶段与依赖画出的流程图、节点输出、审核与控制、模板与报告导出 | [stevenjj33](https://github.com/stevenjj33) | | [MiniMax Photo](plugins/webguhui/minimax-photo/) | 图层图片编辑、应用内 AI 助手、本地恢复与程序化编辑工具 | [WebGuHui](https://github.com/WebGuHui) | -| [Cutline](plugins/klarkxy/cutline/) | 本地时间线剪辑,MCP 工具覆盖剪切、字幕、动效、调色、音频与导出 | [klarkxy](https://github.com/klarkxy) | +| [Cutline](plugins/klarkxy/cutline/) | 浏览器里的视频剪辑台:时间线剪切、字幕、动效、调色与导出,聊天里一句话就能剪片 | [klarkxy](https://github.com/klarkxy) | | [MiniMax Video](plugins/webguhui/minimax-video/) | 本地视频剪辑、字幕、宿主 Agent 助手、可预览的时间线修改与 MP4/MP3 导出 | [WebGuHui](https://github.com/WebGuHui) | | [AI 事务所](plugins/cristinamadureira353-svg/ai-office/README.zh-CN.md) | 五位真实模型员工的像素事务所:接单、聊天、休息和开会,额度耗尽时角色暂停;使用玩家自己的 API Key | [最爱双皮奶](https://github.com/cristinamadureira353-svg) | | [Token 用量看板](plugins/amszuidas/mcode-token-usage-board/README.zh-CN.md) | 按时间、模型和会话查看本机 Token 用量,包含输入、输出和缓存用量 | [amszuidas](https://github.com/amszuidas) | @@ -84,6 +84,17 @@ MiniMax Code Agent 通过宿主 MCP 客户端与 MiniApp 协作。业务请求 | [MiniMax Pet](plugins/minimax-ai/minimax-pet/README.zh-CN.md) | 提供外观/任务/设置、可见会话状态、需要回答提醒和透明桌宠;需要 MiniMax Code 3.1.6 或更高版本 | [MiniMax](https://github.com/MiniMax-AI) | | [MCP OAuth 令牌台](plugins/microbiosis/mcp-oauth-console/README.zh-CN.md) | 给需要 OAuth 的 MCP 服务器完成登录:设备码或授权码 + PKCE 流程,把 `Authorization` 头写入 MCP 配置,并可选择在到期前自动续期 | [Microbiosis](https://github.com/Microbiosis) | +
+预览:Cutline + +![Cutline 编辑器,使用合成素材展示标题与多轨时间线](plugins/klarkxy/cutline/docs/editor-zh-CN.png) + +![Cutline 导出面板,展示范围、预设、视频与音频设置](plugins/klarkxy/cutline/docs/export-zh-CN.png) + +预览使用合成工程。安装、剪辑、导出和数据访问说明见 [Cutline README](plugins/klarkxy/cutline/README.zh-CN.md)。 + +
+
预览:Token 用量看板 diff --git a/plugins/klarkxy/cutline/.minimax-plugin/plugin.json b/plugins/klarkxy/cutline/.minimax-plugin/plugin.json index 54cf212..4ab526b 100644 --- a/plugins/klarkxy/cutline/.minimax-plugin/plugin.json +++ b/plugins/klarkxy/cutline/.minimax-plugin/plugin.json @@ -2,8 +2,8 @@ "schemaVersion": 1, "name": "cutline", "displayName": "Cutline", - "version": "0.1.0", - "description": "浏览器里的视频剪辑台:时间线、字幕、动效、调色与导出,附带 21 个 MCP 工具,聊天里就能剪片。", + "version": "0.1.1", + "description": "浏览器里的视频剪辑台:时间线剪切、字幕、动效、调色与导出,聊天里一句话就能剪片。", "author": "klarkxy", "icon": "icon.png", "category": "Studio", diff --git a/plugins/klarkxy/cutline/PATCHES.md b/plugins/klarkxy/cutline/PATCHES.md index 5513dfc..7ad42f4 100644 --- a/plugins/klarkxy/cutline/PATCHES.md +++ b/plugins/klarkxy/cutline/PATCHES.md @@ -469,3 +469,20 @@ plugin id, the package name, the Skill directory and id, the MCP endpoint `references/media-generation.md` guidance was removed from the package because media generation is out of scope; it is not shipped with the plugin, and no file in the package refers to it. + +## Tool-payload and public documentation follow-up + +This follow-up keeps the tool surface at 21 entries while shortening the +standing MCP descriptions: nineteen upstream `fablecut_*` tools, the local +`fablecut_import_project` tool and the package's `cutline_export` tool. Tool +names, schema shape and dispatch behavior stay unchanged. Safety-critical +conditions remain in the descriptions, including explicit `baseRevision` +conflict handling, +editor-tab rendering, conditional IN/OUT export range, absolute-path frame +reads, remote-SVG rejection and profile combinations. + +The package and root READMEs now show synthetic English and Simplified Chinese +editor/export previews, describe the standalone export path, and disclose the +absolute-path frame-read boundary and shared-project behavior. The new +`tests/mcp-tool-payload.test.mjs` checks the 16,000-byte serialized tool-list budget, +tool provenance/count, retained safety guidance and schema shape. diff --git a/plugins/klarkxy/cutline/README.md b/plugins/klarkxy/cutline/README.md index da5bead..6e344d8 100644 --- a/plugins/klarkxy/cutline/README.md +++ b/plugins/klarkxy/cutline/README.md @@ -2,143 +2,71 @@ [简体中文](README.zh-CN.md) -A local video editor with 19 upstream Agent-facing tools, a project-folder import tool and a standalone export tool, packaged as a Mini App. It vendors FableCut 1.16.0 rather than requiring a separate installation or Git submodule. Use the timeline directly, or ask the Agent to import media, edit clips, add captions and motion graphics, adjust audio, and export. +Edit video inside MiniMax Code, or hand the cut to the Agent in your chat. -Cutline is a community adaptation of [FableCut](https://github.com/ronak-create/FableCut) packaged for MiniMax Code. It is not an official FableCut release and is not endorsed by the upstream project. +Cutline brings the media bin, timeline, captions, motion graphics, colour, audio and export into one Mini App. Work directly in the editor, ask the Agent to change the timeline for you, and take over whenever you like. -## Install and open - -Install this complete directory as a local Plugin in MiniMax Code, then select Cutline from the Mini App launcher or ask the Agent to open it. Keep the directory's hidden `.minimax-plugin` folder. The Host starts the service and opens its page; there is no fixed localhost URL or separate server to launch. - -FFmpeg and FFprobe must be available on the runtime's PATH for media analysis, proxies, audio processing and video encoding. They are not bundled. Install an appropriate build for your operating system and restart the app after changing PATH. - -The included `cutline` Skill documents the editing operations and the tool calls that drive them. - -## Scope - -Cutline edits media. It imports video, audio and images, lays them on a timeline, and renders the result. - -Built-in media generation is out of scope for this package: there is no image, video, speech or music generation step, and the plugin configures no provider keys. New material reaches the timeline the way any other material does - as an imported file, through `fablecut_import_media` or the project-folder import. - -## Interface language - -The editor's interface is available in English and in Simplified Chinese. Pick one in **Settings → Appearance → Language**. The change applies straight away - there is no reload, and it never changes the project - and the choice is remembered in that browser only, under the `fablecut-locale` key of the browser's own `localStorage`. Nothing is sent anywhere: the two dictionaries are local files in the editor's payload (`miniapp/client/i18n-en.js` and `miniapp/client/i18n-zh-CN.js`), and the editor makes no network request for them. - -On the first run the editor follows the browser: a Chinese browser (`zh*`) starts in Chinese, every other browser starts in English. A choice made once wins over that detection from then on. +![Cutline editor in English: media bin, program monitor, inspector and multi-track timeline](docs/editor-en.png) -What is translated: the shell, the menus, the tooltips, the accessible names and the placeholders, the Settings and Help dialogs, the project dashboard, the media bin and its filters, the timeline and its context menus, the Inspector, the mixer, the Colour page, the export dialog with its progress, the confirmations and the toasts. +Cutline is a community-maintained adaptation of [FableCut](https://github.com/ronak-create/FableCut), not an official FableCut release or an upstream-endorsed project. -What is **not** translated, on purpose: - -- **Text you typed.** Project, media and clip names, captions, the notes on a project, the group you gave a saved template and the values you put into a template field stay byte-for-byte what you wrote, in every language - even when they read like one of ours ("Settings", "Export", "Color"). The editor never hands such text to the translator. -- **Technical values.** Track and clip ids, stored enum values, file names, codec, container and colour-space names, pixel formats and ffmpeg arguments keep their spelling. Where an enum value is shown, only the label next to it is translated; the stored value never changes, so a project written in one language opens in the other. -- **Developer and server error details.** The `[FableCut] …` lines and the raw server messages stay in English so that a bug report can be read as it was written. -- **The text that ends up in the picture.** A caption, a title or a template's own content is drawn from the project, never from the interface language, so an exported frame is identical in both languages. - -## Tool names - -The 21 Agent-facing tools include 19 upstream tools and the project-folder import tool under their existing `fablecut_*` names - `fablecut_status`, `fablecut_get_project`, `fablecut_import_media`, `fablecut_export` and the rest - plus `cutline_export`, this package's own standalone export tool. The plugin, the Mini App and the Skill are branded Cutline, but the upstream tools keep their upstream names, so a later upstream update can be applied without renaming the public tool surface. The MCP server itself is registered as `cutline` and answers on `/mcp/cutline`. - -## Using the tools - -- Open the Mini App and call `fablecut_status` to identify the active project. -- Read `fablecut_docs` with a relevant `section`, and `fablecut_get_project` with `compact: true`. -- Import a local file with `fablecut_import_media`, then reference the returned media ID in timeline clips. -- Use `fablecut_patch_project` for targeted edits. Supply `baseRevision` when the edits depend on a previously read revision; resolve conflicts by reading the current document again. -- Whole-document replacement with `fablecut_set_project` requires the `baseRevision` returned by `fablecut_get_project`. Pass the document and that unchanged base revision; the server advances the stored revision after a successful write. Do not increment the document's revision yourself. -- Use `fablecut_versions` for saved versions and `fablecut_projects` for project management. -- `fablecut_import_project {folder}` copies an existing project's media and replaces the active timeline after saving its previous version. Originals remain unchanged. Missing, escaping or unsupported non-media files refuse the import; nested source media is supported. The UI's JSON-only import does not bring external media with it. -- Open the editor before requesting composed frames, scopes, tracking or export. For long exports, use `wait: false` and query the returned job ID. - -### Local files and absolute paths - -The MCP tools run with the permissions of the person who started the app, and they are not confined to the plugin's own folders. +## Install and open -- A tool that takes a `path` - `fablecut_import_media`, `fablecut_analyze_reference`, `fablecut_frame` - resolves an absolute local path and reads that file directly. **Any file the user can read is readable this way**; there is no directory allowlist and no sandbox around it. Only pass paths the user asked for. -- `fablecut_import_media` copies the file into the private data directory and leaves the original untouched; the project refers to the copy. -- `fablecut_import_project {folder}` reads a project folder anywhere on disk that the user can read, copies the media and library files it names into the private data directory, and saves the imported document as the open project. The originals are not modified. +1. Install the complete `cutline` directory as a local Plugin in MiniMax Code, keeping its hidden `.minimax-plugin` folder. +2. Open Cutline from the Mini App launcher, or ask the Agent to open it. The Host manages the service. +3. Install `ffmpeg` and `ffprobe` separately for media analysis, audio processing and video encoding, add them to the system PATH, then restart the app. -The service is shared across conversations. The upstream project dashboard has one active project shared by all pages; opening another project changes that selection for everyone using this plugin. Coordinate project switches and re-check the active project before editing. +The interface is available in English and Simplified Chinese under **Settings → Appearance → Language**. The choice affects only the current browser's interface. -## Two ways to export +## From footage to finished video -`fablecut_export` renders in the editor tab that is open in the Mini App panel. The user watches the -progress bar, the export uses the editor's job slot, and several files can come out of one render. -It needs the Mini App to be open. +1. **Import media.** Add video, audio or images; the originals stay untouched. +2. **Build the timeline.** Arrange, split and trim clips, then layer titles, images, shapes and audio. +3. **Refine the result.** Add captions, transitions and motion graphics, then adjust picture and sound. +4. **Review and export.** Play the cut, choose a preset or custom settings, and render. -`cutline_export` is this package's own tool. It does not render. It **prepares** a job and -hands the Agent an exact path: +You can also ask in chat: -- The Mini App copies the saved project, every media and library file that project names, the - editor's client payload, the FableCut runtime and a standalone runner into a new job folder under - its private data directory. The copies are byte-for-byte; nothing is hard-linked, and a source - file that is missing or that points outside the project folder refuses the job before it becomes - runnable. -- The Agent starts that runner itself, in the foreground of one of its own background shell tasks: - `node "/runner/runner.mjs" --job ""`. -- The runner serves its own copy of the editor on its own ephemeral loopback port, guarded by a - secret it generates for that run, and drives a headless Chrome or Edge through it. The render is - the editor's own Fast export - the same compositor, audio mix and ffmpeg encoding - so the file - matches what the editor would produce. -- `cutline_export {action:"status"}` reports the progress, the attempt id, the runner's pid and - its last heartbeat; `{action:"cancel"}` asks a running job to stop. +- “Cut this video down to 30 seconds, remove the pauses and add Chinese captions.” +- “Give the three guests simple lower thirds.” +- “Lower the music and export both a vertical version and a high-quality master.” -The runner is a plain Node process that the Agent owns. It is not a child of the Mini App, so -stopping the Mini App does not stop a render that has already been accepted; and it is not a -detached daemon. Cancellation, timeouts and client exit remain subject to the Agent task manager; -continued rendering after client exit is not supported or tested. One job runs at a time: a second attempt on the same -job is refused without touching the running one's state or its output. Re-rendering means preparing -a new job. +Manual and Agent-driven edits operate on the same project, so you can take over at any point. -The finished file is written as provisional and is only marked finished after `ffprobe` has read it -and `ffmpeg` has decoded it all the way through. A file that was made but failed one of those checks -is kept in the job folder's `provisional` directory instead of being deleted: it is what the renderer -actually produced, and it can still be looked at. Nothing removes it. +## Choose an export method -The job folder is where the file lands, and it is not cleaned up for you. It holds the project -snapshot, copies of the media and library files, the renderer and the video, so it costs disk space -of its own. The status gives the finished file's absolute path; copy the file out where you want it -and delete the job folder when you are done with it. +![Cutline export dialog in English: range, presets, video and audio settings](docs/export-en.png) -The runner needs `ffmpeg`, `ffprobe` and a Chrome or Edge install on the machine. It never -downloads a browser and never installs software. Because it has no network access, a font that is -not on the machine and a clip that uses background removal are refused with a clear message rather -than rendered differently from what the editor would produce. Unknown delivery preset IDs are also -refused during preparation rather than silently falling back to Custom. This release renders one -file per job: batch outputs are a `fablecut_export` feature. +| Method | Best for | What to know | +| --- | --- | --- | +| **Export in the editor** | Watching progress or creating several versions | Keep the Mini App open during the render | +| **Agent standalone export** | Rendering after closing the panel or delivering from a fixed snapshot | The Agent manages the background task; one file per job | -## Rendering limits +Standalone export needs a local Chrome or Edge installation and uses the project snapshot captured when the job is prepared. Use a new job for later edits. The finished file passes a full decode check; after reviewing it, clean up the job folder to reclaim disk space. -`fablecut_export` uses the original editor-tab compositor. External headless Chrome cannot access the Host-authenticated Mini App page and is not a fallback for it. Keep the editor open during those renders. Browser resource limits can affect long or high-resolution exports; a successful short export does not establish sustained long-form rendering reliability. This package does not include a separate general-purpose FFmpeg timeline renderer. +## Limits -The editor preserves FableCut's timeline and effect model. The Skill describes tool usage rather than prescribing a production workflow. Whatever produced a file, the boundary is an imported media file, and the timeline format does not change. +- Generate images, video, speech and music with other tools, then import the files into Cutline. +- Captions can come from a script, subtitle file or transcript; use a dedicated transcription tool when starting from audio. +- The project service is shared across conversations, so confirm the active project before coordinating edits. The plugin does not read conversation content or other sessions. +- For long or high-resolution renders, test a short section first. ## Tested environment -- Target: MiniMax Code 3.1.2 or later (managed Mini App runtime and detachable window). -- Development environment: Windows 11 x64, Node.js 26.10.0, FFmpeg/FFprobe 8.1.1, Google Chrome (found automatically). -- The standalone export path was exercised on 2026-10-25 outside the Host: a synthetic 3-second 640x360 project with a video clip, a sine tone and an Anton-font title was prepared through `cutline_export`, rendered by the standalone runner in 3.6 seconds, and the result was read back with `ffprobe` (H.264 640x360 at 30 fps plus AAC, 3.000 s) and decoded end to end with `ffmpeg`. A cancelled job was stopped through the cancel request and left no browser or ffmpeg process behind. That is a short synthetic smoke test, not long-form rendering certification, and it was not run inside an installed Mini App. -- Historical, recorded under the former `fablecut` plugin identity: on 2026-10-24 a local Host publish/inspect/open passed. Browser checks covered synthetic video import, linked video/audio placement, title creation, playback controls, reload persistence and a 4-second 1280x720/30 fps export. The editor reported export checks passed; console and failed-request diagnostics were empty. That is a short smoke test, not long-form rendering certification. -- Historical, recorded under the former `fablecut` plugin identity: on 2026-10-25 a fresh conversation received the Host-injected MCP tools and successfully called `fablecut_status`, `fablecut_get_project` and `fablecut_docs`. Live write calls and macOS remain unverified. Local integration tests exercised the managed HTTP protocol, discovery of all 21 tools and representative editing/import calls. -- On 2026-10-25, Cutline was installed and opened in the Host. Its page and Settings interaction worked with no console warnings/errors or failed requests at that check. Live MCP import and patch calls built a synthetic acceptance project. The installed `cutline_export` prepared a 180-second job; the root-chat Agent launched its runner, then stopped the Mini App after observing 7.3% progress. After the Host confirmed service exit, the same runner advanced to 49.3% and completed about 42 seconds later. The output was 1280x720, 30 fps, 5400 frames, H.264/AAC, 180.000 seconds and 56,238,485 bytes; ffprobe and full decoding passed. Reopening the Mini App and querying the installed tool returned the completed job. No task-specific Node, Chrome/Edge or FFmpeg processes remained. The render logged expected refusals of editor-only proxy requests; this does not establish support for every effect or long-form workload. -- The plugin suite has 188 tests; the final serial run on 2026-10-26 passed 187, with one symlink-boundary test skipped because Windows denied symlink creation (EPERM). Six monitor/menu tests additionally cover snap and sequence translations, preserved values and user names, canvas-settings popover behavior and non-overlapping controls at five viewport widths. Eleven icon-control tests cover accessible names, keyboard import, translated tooltips, click behavior and responsive hit targets. Six layout tests cover both languages at 1280x900, 923x899 and 720x800, sidebar restoration, meter separation and monitor zoom. Installed-Host checks covered the empty workspace and synthetic timeline; a standalone export smoke check still produced a 3-second 1280x720 H.264/AAC file after the layout changes. The localization subset has 37 unit tests and 9 browser tests. Package validation reports no errors or warnings. Intermittent `ECONNRESET` failures have also occurred in export transport tests, including a serial run; isolated reruns passed. The underlying transport flake remains unresolved. -- The localization work added 37 dependency-free tests (`tests/i18n.test.mjs`) and 9 browser tests (`tests/i18n-browser.test.mjs`). The browser tests drive a real headless Chrome against a real Mini App instance and skip themselves when the machine has no Chromium-based browser. They were exercised on 2026-10-25 outside the Host: the page loads with the browser's own language detected, switching to English and back to Chinese takes effect in about 12 ms without a reload, the `html lang` attribute and the document title follow the choice, the choice survives a reload, and a project name that reads like one of ours ("Settings") is left untouched by the switch. A third browser test adds a title card, checks that the transition and text-animation selects keep their raw ids ("fade", "none", "word-pop") while only their labels follow the language, and reads the saved project back to confirm it stores "word-pop" rather than the label. That is a short smoke test of the language switch on one machine, not a certification of the installed Mini App. -- The repository check retains its two pre-existing Windows symlink-fixture EPERM failures (the other 67 root tests pass); other packages also retain their existing warnings. +- Target: MiniMax Code 3.1.2 or later. Installation, media import, timeline editing and standalone export were exercised on Windows 11 with Google Chrome, including a 180-second render. +- macOS, every effect combination and sustained long-form or high-resolution workloads remain on the validation roadmap. Current records cover the tested scenarios. -Run plugin integration tests with `npm test --prefix plugins/klarkxy/cutline` from the repository root. Run repository package validation and checks with `npm run check`. No dependency installation or build is required to install the finished directory. +Run `npm test --prefix plugins/klarkxy/cutline` from the repository root for plugin tests, and `npm run check` for repository validation. ## Data & access -- **State:** projects, media copies, versions, analysis caches, proxies, library data and exports are stored in the Host-provided private `context.dataDir`. Its physical location is opaque. Plugin source files are not a workspace for user projects. Export jobs live in `export-jobs//` inside the same directory; each holds the project snapshot, the copies of the media and library files it names, the editor and renderer copies, the job state and the finished file. The folder is not deleted automatically: it costs disk space of its own, and the user removes it once the file has been copied out. A file that failed its checks stays in the job's `provisional` directory. -- **Files read:** packaged editor assets; imported media selected by the user or passed to an editing tool; project data and generated analysis artifacts. A tool that takes a local path reads that absolute path with the user's own permissions - **any file the user can read, not only files inside the data directory** - and an import copies what it read into the private data directory. Importing a local media file copies it into managed storage and does not edit the original. Preparing an export job reads the saved project and the media and library files it names; nothing outside the project's own media and library folders is read, and a source that points elsewhere refuses the job. -- **Files written:** project documents, project/version records, imported media, analysis/proxy artifacts and export files under the private data directory. Project operations and file deletion are explicit tool or UI actions. Managed media accepts only the supported video, audio, image and SVG extensions; fonts are confined to the library, and HTML and other non-media documents are refused at upload, project-import and standalone-export preparation boundaries. All Node and standalone-runner responses use `nosniff`, and browser-executable document types are served with a sandbox CSP as defense in depth for older files. A prepared export job writes only inside its own job folder. -- **Network:** the editor and MCP endpoint use the single Host-assigned loopback listener. HTTPS media imports contact the URL's host and permitted redirect destinations; there is no fixed third-party host required for local editing. Upstream's optional Google font loader contacts `fonts.googleapis.com` and `fonts.gstatic.com`; optional background removal loads MediaPipe assets from `cdn.jsdelivr.net`. These inherited features have not been exercised in this port. The standalone export runner binds one ephemeral loopback port on 127.0.0.1 for the duration of a render; it contacts no other host and the page it serves may not reach the network. -- **Processes:** the Node runtime invokes local `ffmpeg` and `ffprobe` for media operations. It does not launch a second editor server or an external headless browser. Those programs must be installed separately. The standalone export runner is a separate Node process that the Agent starts in its own background task; it launches a headless Chrome or Edge on the machine, and it starts and stops `ffmpeg` and `ffprobe` itself. -- **Agent turns:** the Mini App does not initiate Agent turns and declares no `agentAccess`. Its MCP tools are called by the user's existing conversation. -- **Host access:** no Host configuration, credentials, private logs or session databases are read. The Host owns transport authentication. The standalone runner holds no Host credential: it guards its own port with a secret it generates for that run, which never appears in a URL, an argument or a log line. +- **State:** projects, media copies, versions, caches and exports live in the Host-provided private data directory; export-job copies remain until you clean them up. +- **Files:** local paths use the MiniMax Code process's file permissions and can point outside the private data directory. `fablecut_frame` reads a supplied absolute path, `/media/…` or `/library/…` source in place. `fablecut_import_media` and `fablecut_analyze_reference` copy accepted media from supplied paths into managed storage; originals stay unchanged. `fablecut_import_project` reads `project.json` and the referenced media in the folder the user supplies, then copies those assets into this installation. Projects and exports are written to the private data directory. +- **Network:** the editor and MCP endpoint use the Host-assigned local address. HTTPS imports contact the supplied URL; optional font loading contacts `fonts.googleapis.com` and `fonts.gstatic.com`, and optional background removal contacts `cdn.jsdelivr.net`. Standalone export is isolated from the external network. +- **Processes:** media operations invoke `ffmpeg` and `ffprobe`; the Agent starts and manages Node plus Chrome or Edge for standalone export. +- **Agent turns and conversations:** Agent turns start from your chat. The plugin exposes MCP tools, leaves `agentAccess` undeclared and does not read conversation content or other sessions. All conversations use the same open project. +- **Host data and devices:** Host configuration, credentials, private logs, session databases and the microphone sit outside the plugin's access. ## Upstream and maintenance -Source: [ronak-create/FableCut](https://github.com/ronak-create/FableCut), version 1.16.0, commit `fd2af081d73ec94464b88df8c21f640adcc78985`. Original copyright and MIT terms are retained in [LICENSE](LICENSE). Third-party asset licenses remain beside the included assets. - -The source is vendored directly in this package. See `upstream.json` for provenance and `PATCHES.md` for local adaptations, including the branding changes made for this package. Updates are deliberate: compare the selected upstream revision, reapply necessary Host adaptations, run integration and repository checks, then exercise the installed editor and export path. Installing or updating the plugin does not fetch upstream code or install system software. +The original copyright and MIT terms are retained in [LICENSE](LICENSE). See [upstream.json](upstream.json) for provenance, [PATCHES.md](PATCHES.md) for the maintenance process, and the [Skill](skills/cutline/SKILL.md) with its references for Agent workflows. diff --git a/plugins/klarkxy/cutline/README.zh-CN.md b/plugins/klarkxy/cutline/README.zh-CN.md index f00576f..1f5d7aa 100644 --- a/plugins/klarkxy/cutline/README.zh-CN.md +++ b/plugins/klarkxy/cutline/README.zh-CN.md @@ -2,115 +2,71 @@ [English](README.md) -本地视频剪辑器,保留上游 19 个 Agent 工具,另增加一个工程目录导入工具和一个独立导出工具。插件内直接包含固定版本 FableCut 1.16.0 源码,无需单独安装 FableCut 或拉取 Git 子模块。可以手动操作时间线,也可以在聊天里让 Agent 导入素材、剪辑、添加字幕与动效、处理音频和导出。 +在 MiniMax Code 里剪视频,也可以把剪辑交给聊天中的 Agent。 -Cutline 是面向 MiniMax Code 的 [FableCut](https://github.com/ronak-create/FableCut) 社区改编版,不是 FableCut 官方版本,也未获上游项目背书。 +Cutline 把素材库、时间线、字幕、动效、调色、音频和导出放进同一个 Mini App。你可以自己动手,也可以让 Agent 按你的要求修改时间线,并随时接管继续编辑。 -## 安装与打开 - -在 MiniMax Code 中将本目录作为本地 Plugin 安装,保留隐藏的 `.minimax-plugin` 目录。随后从 Mini App 入口选择 Cutline,或让 Agent 打开它。服务由 Host 管理,无需手动启动服务器或访问固定端口。 - -媒体分析、代理文件、音频处理和视频编码需要运行环境的 PATH 中存在 `ffmpeg` 和 `ffprobe`。插件不捆绑这些程序;请安装适合操作系统的版本,修改 PATH 后重启应用。 - -附带的 `cutline` Skill 说明各项编辑操作及其对应的工具调用。 - -## 功能范围 - -Cutline 处理的是媒体:导入视频、音频与图片,铺到时间线上,再渲染成片。 - -内置媒体生成不在本插件范围内:没有图片、视频、语音或音乐的生成步骤,插件也不配置供应商密钥。新素材与其他素材一样以导入文件的方式进入时间线——通过 `fablecut_import_media` 或工程目录导入。 - -## 界面语言 - -编辑器界面提供英文与简体中文两种语言。在**设置 → 外观 → 语言**中切换。切换立即生效——不需要重载,也不会改动工程——选择结果只保存在当前浏览器的 `localStorage` 的 `fablecut-locale` 键里。不会向任何地方发送数据:两份词典就是编辑器自带的本地文件(`miniapp/client/i18n-en.js` 与 `miniapp/client/i18n-zh-CN.js`),编辑器不会为它们发起网络请求。 - -首次打开时编辑器跟随浏览器语言:浏览器语言为 `zh*` 时用中文,其他情况用英文。一旦手动选择过,就以你的选择为准。 +![Cutline 中文编辑器:素材库、画面预览、检查器和多轨时间线](docs/editor-zh-CN.png) -**已翻译**:外壳、菜单、悬浮提示、无障碍名称与占位文本、设置与帮助对话框、工程面板、素材库及其筛选、时间线及其右键菜单、检查器、调音台、调色页、导出对话框与进度、确认提示与消息提示。 +Cutline 是基于 [FableCut](https://github.com/ronak-create/FableCut) 的社区改编版,由社区独立维护;它不是 FableCut 官方发行版,也未获上游背书。 -**有意不翻译**: - -- **你输入的文字。**工程名、素材名、片段名、工程备注、自存模板的分组名以及模板字段里填入的值,在任何语言下都逐字节保持原样——即使它们看起来和我们的词条一样("Settings"、"Export"、"Color")。编辑器不会把这些文字交给翻译器。 -- **技术取值。**轨道与片段 id、存储的枚举值、文件名、编码/封装/色彩空间名称、像素格式与 ffmpeg 参数都保持原样。枚举值显示时只翻译旁边的标签,存储值本身不变,因此在一种语言里写好的工程换一种语言打开也没问题。 -- **开发者与服务器错误细节。**`[FableCut] …` 开头的行以及服务器原始报错保持英文,方便按原样阅读问题报告。 -- **最终出现在画面里的文字。**字幕、标题与模板自带的内容取自工程,不取自界面语言,因此两种语言下导出的画面完全一致。 - -## 工具名称 - -21 个面向 Agent 的工具中,20 个沿用上游名称:`fablecut_status`、`fablecut_get_project`、`fablecut_import_media`、`fablecut_export` 等;第 21 个是本包自带的独立导出工具 `cutline_export`。插件、Mini App 与 Skill 使用 Cutline 品牌,但上游工具本身沿用上游名称,这样以后同步上游更新时无需重命名公开工具接口。MCP 服务本身注册为 `cutline`,接口路径为 `/mcp/cutline`。 - -## 使用方式 - -- 打开 Mini App,调用 `fablecut_status` 确认当前工程。 -- 按需读取 `fablecut_docs` 的具体章节,通过 `fablecut_get_project {compact:true}` 查看时间线。 -- 用 `fablecut_import_media` 导入本地文件,再用返回的媒体 ID 创建片段。 -- 优先用 `fablecut_patch_project` 局部编辑。依赖已读版本的修改带上 `baseRevision`,遇到冲突重新读取当前工程。 -- 用 `fablecut_set_project` 替换完整文档时,必须传入 `fablecut_get_project` 返回的 `baseRevision`。提交文档和原样的基线版本号即可;写入成功后由服务端递增存储版本,不要自行递增文档里的 `revision`。 -- 用 `fablecut_versions` 管理版本,用 `fablecut_projects` 管理工程。 -- `fablecut_import_project {folder}` 会复制已有工程的媒体,保存当前工程版本后替换当前时间线,原件不变。缺失、越界或不受支持的非媒体文件会使导入被拒绝,支持源媒体子目录。页面中的纯 JSON 导入不会自动带入外部媒体。 -- 合成画面抽帧、示波器、跟踪和导出需要编辑器页面保持打开。长任务使用 `wait:false` 后按 job ID 查询。 - -### 本地文件与绝对路径 - -MCP 工具以启动应用的那个人的权限运行,并不限于插件自己的目录。 +## 安装与打开 -- 接受 `path` 参数的工具——`fablecut_import_media`、`fablecut_analyze_reference`、`fablecut_frame`——会解析绝对路径并直接读取该文件。**用户能读到的任何文件都能这样被读到**;这里没有目录白名单,也没有沙箱。只使用用户明确指出的路径。 -- `fablecut_import_media` 会把文件复制进私有数据目录,原件保持不动;工程引用的是这份副本。 -- `fablecut_import_project {folder}` 会读取磁盘上任意一个用户能读到的工程文件夹,把它引用到的媒体与素材库文件复制进私有数据目录,并把导入的文档保存为当前工程。原文件不会被修改。 +1. 将完整的 `cutline` 目录作为本地 Plugin 安装到 MiniMax Code,保留隐藏的 `.minimax-plugin` 目录。 +2. 从 Mini App 入口打开 Cutline,或让 Agent 打开。Host 会管理服务。 +3. 媒体分析、音频处理和视频编码需要单独安装 `ffmpeg`、`ffprobe`,加入系统 PATH 后重启应用。 -服务在多个会话之间共享,上游工程管理沿用一个全局当前工程。切换工程会影响其他打开的页面;编辑前应核对当前工程并协调切换。 +界面支持英文和简体中文,可在 **设置 → 外观 → 语言** 中切换。语言选择仅影响当前浏览器界面。 -## 两种导出方式 +## 从素材到成片 -`fablecut_export` 在 Mini App 面板里打开的编辑器页面中渲染:用户能看到进度条,导出占用编辑器的任务位,一次渲染可以产出多个文件,但 Mini App 需要保持打开。 +1. **导入素材。** 添加视频、音频或图片,原件保持原样。 +2. **组织时间线。** 拖放、剪切、修整片段,叠加标题、图片、形状和音频。 +3. **完善效果。** 添加字幕、转场和动效,调整颜色与声音。 +4. **检查并导出。** 播放预览,选择预设或自定义设置,渲染成片。 -`cutline_export` 是本包自带的工具,它不渲染,而是**准备**一个任务并把精确路径交给 Agent: +也可以直接对 Agent 说: -- Mini App 把已保存的工程、工程引用到的全部媒体与素材库文件、编辑器前端、渲染运行时和一个独立运行器,复制到私有数据目录下的新任务文件夹里。复制逐字节进行,不使用硬链接;源文件缺失或指向工程目录之外时,任务在可运行之前就被拒绝。 -- Agent 用自己的后台 shell 工具,在该后台任务的前台启动这个运行器:`node "<任务目录>/runner/runner.mjs" --job "<任务目录>"`。 -- 运行器在自己的临时回环端口上提供编辑器副本,端口由它本次运行生成的密钥保护,并用无头 Chrome 或 Edge 驱动它。渲染用的是编辑器自带的 Fast export——同一套合成器、音频混合与 ffmpeg 编码——因此成片与编辑器里看到的一致。 -- `cutline_export {action:"status"}` 报告进度、尝试 ID、运行器进程号与最近一次心跳;`{action:"cancel"}` 请求正在运行的任务停止。 +- “把这段视频剪成 30 秒,去掉停顿,加上中文字幕。” +- “给三位嘉宾加上简洁的姓名条。” +- “把音乐压低一点,导出竖屏版和高清母版。” -运行器是 Agent 自己拥有的普通 Node 进程,不是 Mini App 的子进程,因此停止 Mini App 不会影响已经被接受的渲染;它也不是脱离管理的常驻守护进程;取消、超时和客户端退出仍受 Agent 任务管理器约束,退出客户端后继续渲染不在支持和实测范围内。同一个任务同时只跑一个:第二次尝试会被拒绝,且不会改动正在运行那次的状态与输出;要重新渲染就重新准备一个任务。 +手动编辑和 Agent 编辑使用同一个工程,你可以随时接管。 -成片先以临时文件写出,只有 `ffprobe` 读通、且 `ffmpeg` 完整解码通过之后,才会标记为完成。写出但没通过检查的文件不会被删除,而是留在任务文件夹的 `provisional` 目录里——它就是渲染器实际产出的东西,还可以查看;没有任何东西会自动清理它。 +## 选择导出方式 -成片落在任务文件夹里,而这个文件夹不会替你清理:它装着工程快照、媒体与素材库文件副本、渲染运行时和成片本身,因此会占用磁盘空间。状态里会给出成片的绝对路径;把文件复制到你要的位置之后,就可以删掉这个任务文件夹。 +![Cutline 中文导出面板:范围、预设、视频和音频设置](docs/export-zh-CN.png) -运行器需要本机 PATH 中存在 `ffmpeg`、`ffprobe`,以及一份 Chrome 或 Edge 安装;它不会下载浏览器,也不会安装任何软件。由于它不访问网络,本机没有的字体和使用背景移除的片段会被明确拒绝,而不是渲染出与编辑器不同的结果。未知交付预设 ID 也会在准备阶段被拒绝,不会静默回退为 Custom。本版本每个任务只产出一个文件:批量输出是 `fablecut_export` 的功能。 +| 方式 | 适合场景 | 使用要点 | +| --- | --- | --- | +| **编辑器内导出** | 查看进度、批量输出 | 渲染期间保持 Mini App 打开 | +| **Agent 独立导出** | 关闭面板后渲染、固定快照交付 | Agent 管理后台任务,每个任务产出一个文件 | -## 导出限制 +独立导出需要本机 Chrome 或 Edge,并使用任务创建时的工程快照。后续修改请通过新任务渲染。成片通过完整解码检查后交付;确认结果即可清理任务目录释放空间。 -`fablecut_export` 使用原有编辑器页面中的合成器。外部无头 Chrome 无法访问 Host 认证保护的页面,因此对它不是兜底方案;这类渲染时请保持编辑器打开。长片或高分辨率导出可能受到浏览器资源限制;短片导出成功不代表长片稳定性已经通过。插件未提供独立的通用 FFmpeg 时间线渲染器。 +## 使用边界 -编辑器保留 FableCut 的时间线与效果模型。Skill 说明工具如何使用,不规定创作流程。无论素材由何而来,边界都是导入的媒体文件,时间线格式不变。 +- 图片、视频、语音和音乐可先用其他工具生成,再导入 Cutline。 +- 字幕可来自文稿、字幕文件或转写结果;语音转写请先使用专门工具完成。 +- 工程服务由多个会话共享,协作编辑前先确认当前工程;插件不会读取聊天内容或其他会话。 +- 长片和高分辨率渲染建议先分段试导出。 ## Tested environment -- 目标客户端:MiniMax Code 3.1.2 或更高版本(托管运行时及独立窗口)。 -- 开发环境:Windows 11 x64、Node.js 26.10.0、FFmpeg/FFprobe 8.1.1、Google Chrome(自动发现)。 -- 独立导出链路于 2026-10-25 在 Host 之外实测:合成 3 秒 640×360 工程(视频片段、正弦音、Anton 字体标题),经 `cutline_export` 准备后由独立运行器渲染,用时 3.6 秒;结果用 `ffprobe` 读回(H.264 640×360 30 fps + AAC,3.000 秒)并用 `ffmpeg` 完整解码通过。通过取消请求停止的任务未留下浏览器或 ffmpeg 进程。这是合成素材的短片冒烟检查,不代表长片稳定性已通过,也未在已安装的 Mini App 内实测。 -- 历史记录(记录于原 `fablecut` 插件标识下):2026-10-24 本地 Host publish/inspect/open 成功。页面实测包括合成视频导入、视频与关联音轨落位、标题创建、播放控件、重载持久化,以及 4 秒 1280×720/30 fps 导出;编辑器报告导出检查通过,控制台及失败请求检查为空。这是短片冒烟检查,不代表长片稳定性已通过。 -- 历史记录(记录于原 `fablecut` 插件标识下):2026-10-25 新会话已收到 Host 注入的 MCP 工具,并成功调用 `fablecut_status`、`fablecut_get_project` 和 `fablecut_docs`。真实 Host 写入调用与 macOS 仍未验证。本地集成测试检查了托管 HTTP 协议、21 个工具的发现,以及代表性编辑和导入调用。 -- 2026-10-25 已在 Host 安装并打开 Cutline,页面及设置交互正常,当次控制台无警告/错误、无失败请求。真实 MCP 导入与编辑调用建立了合成验收工程;已安装的 `cutline_export` 准备 180 秒任务,由根聊天 Agent 启动运行器,在观察到 7.3% 进度后停止 Mini App。Host 确认服务退出后,同一运行器仍推进至 49.3%,并在服务退出约 42 秒后完成。输出为 1280×720、30 fps、5400 帧、H.264/AAC、180.000 秒、56,238,485 字节,ffprobe 与完整解码通过。重新打开 Mini App 后,已安装工具读回同一任务的完成状态;未残留该任务的 Node、Chrome/Edge 或 FFmpeg 进程。渲染日志有编辑器专用代理请求被预期拒绝的记录;本次不代表所有效果和长片负载均已验证。 -- 本包共 188 项测试;2026-10-26 最终串行运行中 187 项通过,1 项符号链接边界测试因 Windows 禁止创建符号链接(EPERM)而跳过。新增的 6 项监看器与菜单测试覆盖吸附和序列菜单翻译、原始参数与用户名称保留、画布设置弹层,以及五种窗口宽度下控件不重叠。11 项图标控件测试覆盖可访问名称、键盘导入、中英文提示、点击行为和响应式点击区域。6 项布局测试覆盖中英文及 1280×900、923×899、720×800 窗口、侧栏恢复、电平表与画面分离、监看器缩放。本机 Host 检查了空工程和合成时间线;布局调整后独立导出冒烟检查仍产出 3 秒 1280×720 H.264/AAC 文件。本地化子集含 37 项单元测试和 9 项浏览器测试。包校验无错误、无警告。导出传输测试仍偶发 `ECONNRESET`,串行运行也曾出现,单独复跑通过;底层传输抖动尚未定位解决。 -- 本地化工作另加 37 项无依赖测试(`tests/i18n.test.mjs`)与 9 项浏览器测试(`tests/i18n-browser.test.mjs`)。浏览器测试用真实无头 Chrome 驱动真实 Mini App 实例,机器上没有 Chromium 系浏览器时自动跳过。2026-10-25 在 Host 之外实测:页面按浏览器语言首次判定,切换到英文再切回中文约 12 毫秒生效、无需重载,`html lang` 与文档标题跟随选择,选择在重载后保留,而读起来像我们词条的工程名("Settings")不被改写。第三项浏览器测试会添加标题卡,检查过渡与文字动画下拉框保留原始 id("fade"、"none"、"word-pop")而只有标签随语言变化,并读回已保存的工程确认存的是 "word-pop" 而不是标签。这只是单台机器上语言切换的短片冒烟检查,不代表已安装 Mini App 的认证结果。 -- 仓库总检查仍有两项原有的 Windows 符号链接夹具 EPERM 失败(其余 67 项根目录测试通过);其他插件的原有警告保持。 +- 目标客户端:MiniMax Code 3.1.2 或更高版本。已在 Windows 11 与 Google Chrome 环境中验证安装、素材导入、时间线编辑和独立导出,包括一次 180 秒渲染。 +- macOS、全部效果组合及长片/高分辨率负载仍待完整验证。当前记录代表已实测场景。 -仓库根目录执行 `npm test --prefix plugins/klarkxy/cutline` 运行插件集成测试,执行 `npm run check` 运行仓库检查。完整插件目录安装时不需要额外构建或安装 npm 依赖。 +在仓库根目录运行 `npm test --prefix plugins/klarkxy/cutline` 执行插件测试,运行 `npm run check` 执行仓库校验。 ## Data & access -- **状态存储:** 工程、媒体副本、版本、分析缓存、代理、素材库和导出文件放在 Host 提供的私有 `context.dataDir` 中,不依赖其物理位置,不写入插件源码目录。导出任务位于同一目录下的 `export-jobs/<任务 ID>/`,其中保存工程快照、其引用到的媒体与素材库文件副本、编辑器和运行时副本、任务状态与成片。该文件夹不会自动删除,会占用自己的磁盘空间,用户在把成片复制出去之后自行清理;未通过检查的文件留在任务的 `provisional` 目录里。 -- **读取文件:** 插件自带资源、用户选择或明确传给工具的媒体文件、工程数据和分析产物。接受本地路径的工具会用用户自身的权限读取该绝对路径——**用户能读到的任何文件,而不只是数据目录内的文件**——导入会把读到的内容复制进私有数据目录。导入本地素材会复制文件,不修改原件。准备导出任务只读取已保存的工程以及工程引用到的媒体与素材库文件,不读工程目录之外的任何内容,指向别处的源会被拒绝。 -- **写入文件:** 在私有数据目录中写入工程、版本记录、媒体、分析结果、代理和导出文件。工程管理与删除由具体工具或页面操作触发。受管媒体只接受支持的视频、音频、图片和 SVG 扩展名;字体仅限素材库,上传、工程导入和独立导出准备都会拒绝 HTML 和其他非媒体文档。Node 与独立运行器的所有响应统一带 `nosniff`,可由浏览器执行的文档类型统一带 sandbox CSP,为既有文件提供纵深防护。准备好的导出任务只在自己的任务文件夹内写入。 -- **网络:** 页面和 MCP 使用 Host 分配的本机监听端口。HTTPS 素材导入访问传入 URL 的主机及允许的重定向目标,本地剪辑不依赖固定外部主机。上游可选的 Google 字体加载访问 `fonts.googleapis.com`、`fonts.gstatic.com`;可选背景移除从 `cdn.jsdelivr.net` 加载 MediaPipe 资源。这些继承功能本轮未实测。独立导出运行器在渲染期间绑定一个 127.0.0.1 上的临时回环端口,不访问其他主机,其提供的页面也不得访问网络。 -- **进程:** Node 运行时调用本机 `ffmpeg`、`ffprobe`;不启动额外编辑器服务器或外部无头浏览器。上述程序需另行安装。独立导出运行器是 Agent 在自己的后台任务里启动的另一个 Node 进程,它会启动本机的无头 Chrome 或 Edge,并自行启动与结束 `ffmpeg`、`ffprobe`。 -- **Agent 回合:** Mini App 不主动发起 Agent 回合,不声明 `agentAccess`。用户现有聊天中的 Agent 调用 MCP。 -- **Host 数据:** 不读取 Host 配置、凭据、私有日志或会话数据库;传输认证由 Host 管理。独立运行器不持有 Host 凭据:它用本次运行生成的密钥保护自己的端口,该密钥不会出现在 URL、命令行参数或日志中。 +- **状态:** 工程、媒体副本、版本、缓存和导出文件保存在 Host 提供的私有数据目录;导出任务副本保留至手动清理。 +- **文件:** 本地路径按 MiniMax Code 进程的文件权限访问,可以指向私有数据目录之外。`fablecut_frame` 会就地读取提供的绝对路径、`/media/…` 或 `/library/…` 来源;`fablecut_import_media` 和 `fablecut_analyze_reference` 会将提供路径中的支持素材复制到受管目录,原件保持原样。`fablecut_import_project` 会读取用户提供文件夹中的 `project.json` 与引用素材,并将资源复制到本安装的数据区。工程与导出结果写入私有数据目录。 +- **网络:** 编辑器和 MCP 使用 Host 分配的本机地址。HTTPS 导入访问你提供的 URL;可选字体功能访问 `fonts.googleapis.com`、`fonts.gstatic.com`,可选背景移除访问 `cdn.jsdelivr.net`。独立导出与外部网络隔离。 +- **进程:** 媒体处理调用 `ffmpeg`、`ffprobe`;独立导出由 Agent 启动并管理 Node、Chrome 或 Edge 后台任务。 +- **Agent 与会话:** Agent 回合由你在聊天中发起。插件提供 MCP 工具,未声明 `agentAccess`,不会读取聊天内容或其他会话;所有会话共用当前打开的工程。 +- **Host 数据与设备:** Host 配置、凭据、私有日志、会话数据库和麦克风均位于访问范围之外。 ## 上游与维护 -来源:[ronak-create/FableCut](https://github.com/ronak-create/FableCut),版本 1.16.0,commit `fd2af081d73ec94464b88df8c21f640adcc78985`。原始版权和 MIT 许可保留在 [LICENSE](LICENSE),第三方素材许可随资源保留。 - -源码直接纳入插件。`upstream.json` 记录来源,`PATCHES.md` 记录必要的本地适配,包括本包所做的品牌改动。升级时对比上游、合并适配、执行测试,再检查安装后的编辑器和导出。安装或更新插件不会自动拉取上游或安装系统软件。 +原始版权和 MIT 许可保留在 [LICENSE](LICENSE)。[upstream.json](upstream.json) 记录来源与版本,[PATCHES.md](PATCHES.md) 说明维护流程,[Skill](skills/cutline/SKILL.md) 及参考文档说明 Agent 工作方式。 diff --git a/plugins/klarkxy/cutline/docs/editor-en.png b/plugins/klarkxy/cutline/docs/editor-en.png new file mode 100644 index 0000000..62fa0c7 Binary files /dev/null and b/plugins/klarkxy/cutline/docs/editor-en.png differ diff --git a/plugins/klarkxy/cutline/docs/editor-zh-CN.png b/plugins/klarkxy/cutline/docs/editor-zh-CN.png new file mode 100644 index 0000000..cee6e10 Binary files /dev/null and b/plugins/klarkxy/cutline/docs/editor-zh-CN.png differ diff --git a/plugins/klarkxy/cutline/docs/export-en.png b/plugins/klarkxy/cutline/docs/export-en.png new file mode 100644 index 0000000..1336925 Binary files /dev/null and b/plugins/klarkxy/cutline/docs/export-en.png differ diff --git a/plugins/klarkxy/cutline/docs/export-zh-CN.png b/plugins/klarkxy/cutline/docs/export-zh-CN.png new file mode 100644 index 0000000..61230d6 Binary files /dev/null and b/plugins/klarkxy/cutline/docs/export-zh-CN.png differ diff --git a/plugins/klarkxy/cutline/icon.png b/plugins/klarkxy/cutline/icon.png index 299a26c..6716514 100644 Binary files a/plugins/klarkxy/cutline/icon.png and b/plugins/klarkxy/cutline/icon.png differ diff --git a/plugins/klarkxy/cutline/miniapp/node/export/export-mcp.mjs b/plugins/klarkxy/cutline/miniapp/node/export/export-mcp.mjs index d5b9976..c781e91 100644 --- a/plugins/klarkxy/cutline/miniapp/node/export/export-mcp.mjs +++ b/plugins/klarkxy/cutline/miniapp/node/export/export-mcp.mjs @@ -18,20 +18,17 @@ import { describeJob } from './job-state.mjs'; export const EXPORT_TOOL = { name: 'cutline_export', description: - 'Render the timeline to a video file with a standalone renderer that runs outside the Mini App. ' + - 'Three actions: prepare (copy the saved project, its media, the library assets and the renderer into a job folder under the plugin\'s private data dir, then return the exact path of the runner to start), ' + - 'status (read a job\'s progress and result), and cancel (ask a running job to stop). ' + - 'prepare does not start anything: start the returned runner with your own background shell tool, then poll status. ' + - 'The render uses the editor\'s own compositor, audio mix and encoding, so the file matches what the editor shows. ' + - 'Needs ffmpeg and ffprobe on PATH, and a Chrome or Edge install for the renderer. ' + - 'One file per render in this release: batch outputs are not supported here.', + 'Render the timeline to a video file with a standalone renderer outside the Mini App. ' + + 'prepare copies the project, its media and the renderer into a job folder and returns the runner path, ' + + 'which you start with your own background shell tool; status reads a job; cancel stops it. ' + + 'Needs ffmpeg and ffprobe on PATH, and a Chrome or Edge install. One file per render.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['prepare', 'status', 'cancel'], - description: 'prepare: build a job to run. status: read a job. cancel: ask a running job to stop.', + description: 'prepare: build a job. status: read one. cancel: stop one.', }, job: { type: 'string', @@ -40,23 +37,23 @@ export const EXPORT_TOOL = { range: { type: 'string', enum: ['entire', 'in-out'], - description: 'The part of the timeline to render: the whole timeline, or the project\'s inPoint to outPoint. Default: in-out when either marker is set, else the whole timeline.', + description: 'The part to render: whole timeline, or the project\'s inPoint to outPoint (default: in-out when set).', }, preset: { type: 'string', description: - 'A delivery preset id: youtube, youtube-shorts, instagram-reels, tiktok, x, linkedin, master, broadcast, web, archive, custom, or a saved preset\'s id. ' + - 'An id nobody has is refused before the job is made, so a typo never renders as Custom.', + 'A delivery preset id (fablecut_encode_profiles lists them) or a saved preset\'s id. ' + + 'An unknown id is refused before the job is made.', }, delivery: { type: 'object', description: - 'Delivery settings over the preset (or over custom): the same shape fablecut_export takes — {width, height, fps, reframe, container, video:{…}, audio:{…}, color, filename, metadata}. ' + - 'It may carry a preset of its own ({preset: "…"}); that id is checked the same way.', + 'Over the preset: {width, height, fps, reframe, container, video, audio, color, filename, metadata}; ' + + 'it may carry preset: "…".', }, profile: { type: 'string', - description: 'A raw encoding profile id (ffmpeg argument lists) instead of a preset. Cannot be combined with preset, delivery or outputs.', + description: 'A raw encoding profile id, not with preset or delivery.', }, }, required: ['action'], diff --git a/plugins/klarkxy/cutline/miniapp/node/vendor/fablecut/mcp-server.js b/plugins/klarkxy/cutline/miniapp/node/vendor/fablecut/mcp-server.js index 0d7ec0c..f4359cc 100644 --- a/plugins/klarkxy/cutline/miniapp/node/vendor/fablecut/mcp-server.js +++ b/plugins/klarkxy/cutline/miniapp/node/vendor/fablecut/mcp-server.js @@ -160,39 +160,39 @@ const uid = () => Math.random().toString(36).slice(2, 9); const TOOLS = [ { name: "fablecut_status", - description: "Cutline video editor: ensure the editor web server is running (auto-starts it), and get the editor URL, project summary and media library. Call this first in a session.", + description: "Report editor URL, open project and media. Call first.", inputSchema: { type: "object", properties: {} }, }, { name: "fablecut_docs", - description: "Return the Cutline project schema documentation: clips, tracks, props, keyframe animation, transitions, and editing recipes. Read this before editing. TOKEN TIP: pass `section` to fetch only the '## …' section(s) you need (substring match, e.g. \"props\", \"Recipes\", \"Remake\") instead of the whole manual.", + description: "Read the Cutline manual: schema, clips, tracks, props, keyframes, transitions, recipes. Fetch one part with `section`.", inputSchema: { type: "object", - properties: { section: { type: "string", description: "Return only '## ' sections whose heading contains this text (case-insensitive). Omit for the full document." } }, + properties: { section: { type: "string", description: "The '## ' sections whose heading matches this" } }, }, }, { name: "fablecut_get_project", - description: "Get the Cutline project (the timeline document). TOKEN TIP: pass compact:true for a one-line-per-clip summary (ids, tracks, timings, non-default props) — usually all you need to plan an edit; fetch the full JSON only when you must inspect exact keyframes.", + description: "Read the project; `compact:true` gives one line per clip.", inputSchema: { type: "object", - properties: { compact: { type: "boolean", description: "Return a compact human-readable summary instead of the full JSON" } }, + properties: { compact: { type: "boolean", description: "A compact summary instead of the JSON" } }, }, }, { name: "fablecut_patch_project", - description: "Apply targeted edits to the Cutline project WITHOUT round-tripping the whole document — PREFER THIS over get+set for every edit (it is ~10-100x cheaper in tokens: it re-reads the latest document from disk, applies your ops in order, bumps revision once, saves atomically). Targeted is not blanket merge-safe: an op that sets a field the user changed in the UI a moment ago overwrites that field as well — pass `baseRevision` (the revision you planned from) to have that concurrent change DETECTED instead of applied; the patch is then refused with CONFLICT and nothing is saved. Ops: {op:'addClip', clip:{…}} (id auto-generated if omitted) · {op:'updateClip', id, set:{…}} · {op:'removeClip', id} · {op:'addMedia', media:{…}} · {op:'removeMedia', id} · MEDIA BIN: {op:'updateMedia', id | ids, set:{name?, favorite?:true|false, tags?:[…], folderId?}, addTags?:[…], removeTags?:[…]} (organize media; fps, codec, audio channels and size are probed from the file and read-only) · {op:'setCollection', id?, name?, media?:[…ids] (replaces), add?:[…], remove?:[…]} (a collection groups media across folders; a new id or name creates it) · {op:'removeCollection', id} · {op:'relinkMedia', id, src:'/media/…'} (the same footage found elsewhere — an OFFLINE file in the compact view; clips keep their In points) · {op:'replaceMedia', id, src, name?} (a different file behind the media — every clip using it switches, keeping its timing, trimmed to fit a shorter file; import the new file first with fablecut_import_media) · {op:'setProject', set:{name|width|height|fps|background|markers|disabledTracks|lockedTracks|untargetedTracks|encodeProfile|delivery|outputs|master}} (markers = the full list [{t, label?, color?}], color: gold|red|orange|green|cyan|blue|purple|pink; master = {gain}, the master fader in dB; delivery = a delivery preset id or spec — see fablecut_export — that exports use by default; outputs = the batch list, [preset id | spec, …] up to 16, exported together by fablecut_export {outputs:true}) · {op:'setTrack', id:'A1', set:{gain?, pan?, out?}} (audio-track fader in dB −60…+12, pan −1…1, out = a submix bus id or 'master'; null or 0 resets) · {op:'setBus', id:'B1', set:{name?, gain?, pan?, mute?}} (submix bus, created if missing; route tracks into it with setTrack out) · {op:'removeBus', id} · {op:'setFx', target:'clip'|'track'|'bus'|'master', id?, preset?:'podcast'|… OR fx:[{type,…params}], append?:true} (audio effects — validated; presets: clean-voice, podcast, radio, deep-voice, telephone, cinematic, wide, muffled; fx:null clears; on a clip it applies to its linked stems too) · {op:'setFxKeys', target, id?, index?|type?, param, keys:[{t, v, ease?}]|null} (automate one effect parameter: t is clip-local for a clip's effects, timeline seconds otherwise; see the 'Audio mix' docs section) · {op:'setGrade', id | ids:[…], grade:{exposure?, temp?, tint?, lift?, gamma?, gain?, offset?, contrast?, pivot?, blacks?, shadows?, midtones?, highlights?, whites?, lowSoft?, highSoft?, saturation?, curves?:{y?, r?, g?, b?}, hueHue?, hueSat?, hueLuma?, satLuma?, layers?:[…], on?}, replace?:true} (color grade on video / image / svg / adjust clips — wheels are [r, g, b, master]; curves are [[x, y], …] point lists on 0…1 and merge per channel; hue curves are [[hue°, value], …] — one point = a ±40° band; layers replaces the whole layer list; merges key by key, null resets a key, replace:true starts from neutral, grade:null clears; see the 'Color' docs section; measure the result with fablecut_scopes) · {op:'setGradeLayer', id | ids, layer: index | 'name', set:{name?, on?, qualifier?:{hue?:[centre°, width°, soft°], sat?:[lo, hi, soft], luma?:[lo, hi, soft], invert?}, mask?:{shape:'ellipse'|'rect'|'poly', x, y, w, h, rotation?, feather?, invert?, points?, keys?:[{t, x?, y?, …}]}, …any grade key}, replace?:true, remove?:true} (a secondary: a grade applied only where the qualifier × mask select; a new name or index = length adds a layer; qualifier and mask merge key by key; check what it selects with fablecut_scopes matte:{clip, layer}) · {op:'setMask', id | ids, mask?: index | 'name', set:{shape?:'rect'|'ellipse'|'bezier'|'freehand', mode?:'add'|'subtract'|'intersect'|'difference', invert?, opacity?, feather? (px), expand? (px, − shrinks), x?, y? (centre, 0…1 of the clip's picture), w?, h?, scale?, rotation?, points?:[[dx, dy, inX?, inY?, outX?, outY?], …] (bezier, around x, y), stroke?:[[u, v], …] (freehand: fitted into a bezier), keys?:[{t, ease?, x?, y?, w?, h?, scale?, rotation?, feather?, expand?, opacity?, points?}], name?, on?}, replace?:true, remove?:true} (clip masks on video / image / svg / text / adjust clips: the clip shows only where its masks combine to, bottom-up; mask left out, an unknown name or index = length adds one — a new mask needs shape; set merges key by key; masks follow the clip's transform; check with fablecut_scopes mask:{clip}) · {op:'removeMask', id, mask} · {op:'setMaskKeys', id, mask, keys:[…]|null} (animate a mask; t = seconds from the clip's start) · {op:'setMatte', id | ids, matte:'alpha'|'alpha-inverted'|'luma'|'luma-inverted'|null, track?:'V3'|null} (track matte: the picture on the video track directly above — or track — cuts the clip by its alpha or its brightness, inverted or not; that track stops drawing while it is a matte; null clears). · {op:'removeTrack', id, track: name | index} (delete a motion track stored by fablecut_track; what follows it keeps its keys). MOTION GRAPHICS: a shape layer is addClip {kind:'shape', track, start, duration, props:{shapes:[…]}} (no mediaId); a null is addClip {kind:'null', …} — invisible, a parent for other clips. {op:'setShape', id | ids, shape?: index | 'name' | '2.0' (path into a group), set:{type?:'rect'|'ellipse'|'polygon'|'star'|'line'|'path'|'group', name?, on?, x?, y? (px from the layer's origin), rotation?, scale?, opacity?, w?, h?, radius? (rect corners), sides?, inner? (star 0.05…1), points?:[[x, y, inX?, inY?, outX?, outY?], …] (line: 2, path: 2…128, px around x, y), closed? (path), fill?, stroke? ('#rrggbb' | '#rrggbbaa' | 'none'), strokeWidth?, lineCap?, lineJoin?, dash?:[on, off…], trimStart?, trimEnd? (0…100 — a draw-on), trimOffset?, items?:[…] (group), keys?:[{t, ease?, …any of x y rotation scale opacity w h radius inner strokeWidth trimStart trimEnd trimOffset fill stroke points}]}, replace?:true, remove?:true} (shape left out, an unknown name or index = length adds one — a new shape needs type; set merges key by key) · {op:'removeShape', id, shape} · {op:'setParent', id | ids, parent: clip id | null, keep?: true, at?} (the clip moves, turns and scales with its parent; keep:true — the default — keeps it where it is at time at, default its start; opacity and effects are not inherited). COMPOSITIONS (nested sequences): project.comps = [{id, name, width, height, duration, background?, tracks?, clips:[…]}]; a clip {kind:'comp', compId, start, in, duration} places one (draws like a picture of the comp's size — transform, masks, grade, blend, blur all apply — and its media plays and exports). {op:'addComp', comp:{id?, name, duration, width?, height?, background?, clips?:[…]}} · {op:'updateComp', id, set:{name?, width?, height?, duration?, background?, tracks?}} · {op:'removeComp', id} (refused while used) · {op:'precompose', ids:[…], name?, id?} (the clips move into a new comp; a comp clip takes their place) · {op:'inComp', comp, ops:[…]} (run clip and timeline ops on the comp's own clips — addClip, updateClip, setShape, split, …; nests). SEQUENCES (several timelines in one project — e.g. a 16:9 cut and a 9:16 cut sharing the media and comps; the open one is the top level of the document, which every op, export and the compact view use): {op:'addSequence', name?, from?:'empty'|'open'| (empty takes the open one's canvas and frame rate; a copy takes its whole timeline), width?, height?, fps?, open?:true} · {op:'openSequence', id} (changes what the user's editor shows — only when asked, or to work on it) · {op:'updateSequence', id, set:{name?, index?}} · {op:'removeSequence', id} (only when the user asked) · {op:'inSequence', sequence, ops:[…]} (run any clip, timeline, track or setProject op on a sequence without opening it). PRESETS AND TEMPLATES: {op:'applyPreset', id | ids, preset, length?, at?} keys a named animation onto visual clips, relative to where they sit (in: fade-in, pop-in, slide-up-in, slide-down-in, slide-left-in, slide-right-in, drop-in, zoom-in, spin-in, whip-in, rise-blur, draw-on · out: fade-out, pop-out, slide-up-out, slide-down-out, slide-left-out, slide-right-out, zoom-out, spin-out, whip-out, sink-blur, draw-off · at (an emphasis at timeline time at): pulse, shake, wobble, bounce, flash · loop (the whole clip): float, breathe, sway, spin, wiggle — or a saved one, or an inline {anchor:'in'|'out'|'at'|'whole', length, keys:{channel:[{t, v, ease?}]}} where scale / opacity multiply and the rest add). {op:'addTemplate', template, start?, track?, duration?, fields?:{key: value}, id?, compId?, name?} places a template as its own comp + a comp clip (lower-third-bar, lower-third-underline, lower-third-boxes [name, title, accent…] · logo-ring, logo-slam, logo-lines [logo = a media id, brand / tagline, accent] · social-story 1080x1920, social-quote 1080x1080, social-end-card [text fields, accent, background]); {op:'setTemplate', id: its clip or comp, fields?, duration?} changes the fields or the length (the exit keeps its timing). {op:'savePreset', id, name, anchor?} / {op:'saveTemplate', comp, name?, fields?} save to library/presets and library/templates for every project.MOTION PATHS: x and y keyed at the same time are a position key; curve handles pin / pout (px) on those x and y keys bend the path between keys ({op:'setMotionPath', id | ids, path:'smooth'|'straight', autoOrient?:bool} sets them for you; props.autoOrient turns the clip along its path). props.motionBlur = shutter angle 0…720 (180 = film look), props.motionBlurSamples 2…32. TEXT ON A PATH: {op:'setTextPath', id | ids, path:'line'|'arc'|'circle'|'wave'|{type:'path'|'line'|'ellipse'|'rect'|'polygon'|'star', …shape item geometry, keys?}|null, offset? (% along, keyable as pathOffset), align?:'start'|'center'|'end'} — the title runs along the path (more lines on parallel curves below it), every textAnim plays along it. Every visual clip also has props.anchorX / anchorY (px from its picture's centre: the point x / y place and rotation / scale turn around) and shape clips props.trimStart / trimEnd / trimOffset (trim every path in the layer) — all keyframable. props.blend: normal · multiply · screen · overlay · darken · lighten · color-dodge · color-burn · hard-light · soft-light · difference · exclusion · hue · saturation · color · luminosity · lighter. TIMELINE EDITS — the editor's own split / ripple / trim code, so linked stems, track targeting (untargetedTracks) and locks behave exactly as in the UI; times in seconds: {op:'split', at, ids?} (no ids: every targeted track) · {op:'rippleDelete', ids} (later clips close the hole) · {op:'closeGap', at} · {op:'lift'|'extract', from?, to?} (remove a range; extract closes it; default = project inPoint/outPoint, which then clear) · {op:'insert'|'overwrite', mediaId, at, in?, duration?} (three-point edit: insert pushes later clips right, overwrite replaces what is there; a video brings one audio stem per channel) · {op:'rippleTrim'|'roll', id, side:'in'|'out', delta} · {op:'slip'|'slide', id, delta} (clamped to the media; the note says what was applied) · {op:'crossfade', ids? | at, duration?} (constant-power audio crossfade, borrowing handles from both sides). Any of these takes tracks:[…] to target lanes for that op only. setProject also takes inPoint / outPoint. updateClip merge rules: top-level keys are replaced (keyframes/transitionIn/transitionOut wholesale), `props` merges key-by-key, and setting any key to null deletes it. LOCKS: the user can lock clips (`locked:true`) and tracks (`lockedTracks`); updateClip / removeClip on a locked clip — or on a clip linked to one — and addClip onto a locked track are refused. Leave locked material alone; only if the user asked you to change it, pass force:true on that op (or unlock first: updateClip set:{locked:null}, which is always allowed). All-or-nothing: an invalid op aborts the whole patch unsaved. Optional baseRevision: the revision your ops were planned against (from fablecut_get_project, fablecut_status or the previous patch result) — if the project has moved past it (e.g. the user edited in the UI meanwhile), the patch is refused with CONFLICT and the current revision, nothing saved; re-check the clips you touch, then retry with the new revision. Omit it to apply on top of whatever is latest.", + description: "Apply edit ops in one revision bump. Op shapes: `fablecut_docs` ({section:\"MCP connection\"}, {section:\"schema\"}). `baseRevision` turns a UI change into a CONFLICT.", inputSchema: { type: "object", properties: { ops: { type: "array", items: { type: "object" }, - description: "Edit operations, applied in order (see tool description for shapes). Any op may carry force:true to override a lock the user set.", + description: "Edit ops in order; force:true overrides a lock", }, baseRevision: { type: "integer", - description: "Revision these ops were planned against. If project.json has moved past it, the patch is refused with CONFLICT (nothing saved). Omit to apply on top of the latest revision.", + description: "The planned revision; a newer project gives CONFLICT", }, }, required: ["ops"], @@ -200,249 +200,249 @@ const TOOLS = [ }, { name: "fablecut_set_project", - description: "Replace the Cutline project JSON. Pass the COMPLETE document (read with fablecut_get_project, modify, send back whole) together with `baseRevision` — the revision that document was based on. The revision is the server's to give: it stores the document at the current revision + 1, so do NOT bump the document's `revision` yourself. CONFLICT-SAFE: if project.json is at a different revision than `baseRevision` (e.g. the user tweaked something in the UI), the call errors instead of overwriting — re-read, re-apply your edit on top of the latest document, and retry.", + description: "Replace the whole project document, with `baseRevision` the revision it was based on; another revision refuses the save with CONFLICT.", inputSchema: { type: "object", properties: { - project: { type: "object", description: "The complete project document (see fablecut_docs for schema). Its `revision` field is ignored — the server sets it." }, - baseRevision: { type: "integer", description: "REQUIRED. The revision the document was based on (from fablecut_get_project, fablecut_status or the last patch result). A different current revision refuses the save with CONFLICT (nothing is saved)." }, - force: { type: "boolean", description: "Save even when the project has moved on since baseRevision (discards those external/user changes). Only when the user explicitly asks." }, + project: { type: "object", description: "The whole project document (`revision` ignored)" }, + baseRevision: { type: "integer", description: "REQUIRED. The revision it was based on" }, + force: { type: "boolean", description: "Save anyway when the project moved on; only on request" }, }, required: ["project", "baseRevision"], }, }, { name: "fablecut_frame", - description: "LOOK at the footage — returns an actual image, not just numbers. REQUIRES A VISION-CAPABLE MODEL: if you cannot read images, pass `text:true` instead (or skip this tool and use fablecut_analyze_reference). Three modes. (1) One frame of a source clip: pass `path` (an absolute file path, or a '/media/…' src — read where it lies, never copied) and `t` in seconds. (2) A contact sheet: pass `frames` (2–60) for one grid image with that many moments spread across the clip (`from`/`to` to survey part of it) — the cheap way to see what a whole clip holds. (3) With no `path`, `time` renders the COMPOSED TIMELINE at that moment through the editor's own compositor, graded and multi-track, exactly as export would. Use it before choosing a clip's `in` point, or to pick footage matching a blueprint's shots[] energy — seeing beats guessing. Needs ffmpeg on PATH (timeline mode runs in the open editor tab; a background browser is not used here).", + description: "Return an image of the footage or the composed timeline; needs a vision model, or `text:true` for measurements. With `path` one frame or a contact sheet.", inputSchema: { type: "object", properties: { - path: { type: "string", description: "Source clip to look at: an absolute file path or a '/media/…' src. Read in place — nothing is copied into media/. Omit to render the timeline instead." }, - t: { type: "number", description: "Seconds into the source clip (default 0). Ignored when `frames` is passed." }, - time: { type: "number", description: "TIMELINE seconds to render, when `path` is omitted (default: the editor's playhead)" }, - frames: { type: "number", description: "Contact-sheet mode: number of moments to sample across the clip, 2–60 (default 12)" }, - cols: { type: "number", description: "Contact-sheet columns (omit for the squarest layout — 6 frames give 3×2, 12 give 4×3)" }, - from: { type: "number", description: "Contact sheet: start of the range to survey (default 0)" }, - to: { type: "number", description: "Contact sheet: end of the range (default: the clip's duration)" }, - width: { type: "number", description: "Pixel width of the returned image (default 768 for one frame, 320 per cell for a sheet)" }, - text: { type: "boolean", description: "Return TEXT measurements instead of the image, for a client or model that cannot see one: brightness and luma levels, dominant named colours with their share, texture (detail within the frame, 0–100), and for a contact sheet a per-cell line with change-since-the-previous-cell. Measurements, NOT a caption — they say what the picture is made of, never what is in it. Use when you have no vision." }, - where: { type: "string", enum: ["auto", "tab", "headless"], description: "Timeline mode only: auto (default) or tab — an open editor tab does the work. headless is refused here: a background browser cannot load the editor's authenticated origin, so open the editor tab first." }, - timeout: { type: "number", description: "Timeline mode: seconds to wait for the render (default 60)" }, + path: { type: "string", description: "Source clip: an absolute file path or '/media/…'/'/library/…' src; read in place, never copied" }, + t: { type: "number", description: "Seconds into the clip (default 0)" }, + time: { type: "number", description: "Timeline seconds (default: the playhead)" }, + frames: { type: "number", description: "Contact sheet: moments, 2–60 (default 12)" }, + cols: { type: "number", description: "Contact-sheet columns" }, + from: { type: "number", description: "Contact sheet start (default 0)" }, + to: { type: "number", description: "Contact sheet end (default: clip duration)" }, + width: { type: "number", description: "Pixel width (default 768, or 320 per cell)" }, + text: { type: "boolean", description: "Text measurements instead of the image" }, + where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab; headless is refused here" }, + timeout: { type: "number", description: "Seconds to wait (default 60)" }, }, }, }, { name: "fablecut_analyze_reference", - description: "Analyze a reference video into an EDIT BLUEPRINT so a similar edit can be rebuilt with different footage over the same music. Returns: shot boundaries (cuts) with per-shot audio energy, music beats + BPM, a loudness curve, the detected drop, and extracts the reference's music track into media/ (registered in the project, ready to place on A1). Remake recipe: copy the reference's width/height/fps to the project, write `beats` into project `markers`, lay the extracted music on A1, then place one clip per blueprint `shot` at the same start/duration — pick calm footage for low-energy shots and action for high-energy ones, and make the biggest moment land on `drop`. See the 'Remake a reference video' section of fablecut_docs.", + description: "Analyze a reference video into an edit blueprint: shot cuts with energy, beats/BPM, loudness, the drop and its music; external local paths must be audio/video.", inputSchema: { type: "object", properties: { - path: { type: "string", description: "The reference video: an absolute file path (copied into media/ automatically) or an existing '/media/…' src" }, - threshold: { type: "number", description: "Scene-cut sensitivity 0–1 (default: adaptive 0.30→0.20→0.12). Lower it if obvious cuts are missed, raise it if too many false cuts." }, - registerMusic: { type: "boolean", description: "Extract the reference's music and register it as project media (default true)" }, + path: { type: "string", description: "An absolute file path (copied into media/) or '/media/…' src" }, + threshold: { type: "number", description: "Scene-cut sensitivity 0–1 (default adaptive)" }, + registerMusic: { type: "boolean", description: "Extract the music (default true)" }, }, required: ["path"], }, }, { name: "fablecut_import_media", - description: "Import a media file into Cutline's media library and register it in the project. Pass a local absolute path (copied, including .svg) or an https:// URL (downloaded into ./media/; video/audio/image only — remote SVG is refused). Only media files are copied in (video, audio, image, SVG): anything else is refused, because media/ is served from the editor's own origin. The URL is never kept as the playback src — CORS would break export. Returns the created media entry (use its id in clips).", + description: "Import video, audio, image or SVG from a user-authorized local path, or video/audio/image from an https URL; remote SVG is refused. Local files are copied and originals stay unchanged.", inputSchema: { type: "object", - properties: { path: { type: "string", description: "Absolute path to a local file, or an https:// URL to download" } }, + properties: { path: { type: "string", description: "Local file path, or an https:// URL" } }, required: ["path"], }, }, { name: "fablecut_encode_profiles", - description: "List what fablecut_export can make. DELIVERY PRESETS (structured settings, the Export dialog's Preset list): youtube, youtube-shorts, instagram-reels, tiktok, x, linkedin, master (project size), broadcast (ProRes 422 HQ), web (720p), archive (lossless FFV1), custom — plus presets the user saved — each a spec of resolution, fps, codec (h264, hevc, av1, vp9, prores, dnxhr, mpeg2 XDCAM, ffv1), rate control (crf / vbr / cbr), keyframe interval, pixel format, color, container, audio, a file-name template and metadata. {preset:'youtube'} returns one preset's full spec; the encoders this ffmpeg can use are listed (hardware ones only when they work on this machine). ENCODING PROFILES (encoding-profiles.json): raw ffmpeg argument lists plus jpegQuality, extension and color, for anything the presets do not cover; edit the file on disk (hot-reloaded); {profile:'hq'} returns one, detail:true includes every args array. Both are dry-run against ffmpeg when an export starts, so bad settings are refused before rendering.", + description: "List the delivery presets and raw ffmpeg encoding profiles the export uses; `{preset}` or `{profile}` returns one in full.", inputSchema: { type: "object", properties: { - detail: { type: "boolean", description: "Include the full ffmpeg args array per encoding profile (default: a truncated summary)" }, - profile: { type: "string", description: "Return one encoding profile by id instead of the full list" }, - preset: { type: "string", description: "Return one delivery preset's full spec (youtube, broadcast, a saved preset id …)" }, + detail: { type: "boolean", description: "Full ffmpeg args array" }, + profile: { type: "string", description: "One encoding profile by id" }, + preset: { type: "string", description: "One delivery preset's full spec" }, }, }, }, { name: "fablecut_normalize_audio", - description: "Loudness-normalize clips: measures each clip's source audio (ITU-R BS.1770 integrated LUFS, or sample peak) after its channel routing and sets `props.gain` (clip gain, dB) so it lands on the target — the same measurement as the editor's Normalize button. Linked stems (a video's per-channel audio clips) are measured together and get one shared gain, so a stereo pair stays balanced; passing a video clip normalizes its stems. Volume keyframes / fades are untouched (they ride on top). Typical targets: -14 LUFS streaming/social, -16 LUFS podcast/dialogue, -23 LUFS broadcast, or mode 'peak' at -1 dBFS. Needs ffmpeg + ffprobe on PATH. Refuses locked clips unless force:true.", + description: "Measure each clip's source loudness and set `props.gain` to the target; linked stems share one gain. Needs ffmpeg on PATH.", inputSchema: { type: "object", properties: { - clipIds: { type: "array", items: { type: "string" }, description: "Clips to normalize (audio, or video with audio)" }, - mode: { type: "string", enum: ["lufs", "peak"], description: "Measure integrated loudness (default) or sample peak" }, - target: { type: "number", description: "Target level: LUFS for mode lufs (default -14), dBFS for mode peak (default -1)" }, - force: { type: "boolean", description: "Also change clips the user locked (only when they asked)" }, + clipIds: { type: "array", items: { type: "string" }, description: "Clips to normalize" }, + mode: { type: "string", enum: ["lufs", "peak"], description: "Loudness (default) or sample peak" }, + target: { type: "number", description: "LUFS for lufs (default -14), dBFS for peak (default -1)" }, + force: { type: "boolean", description: "Also change locked clips" }, }, required: ["clipIds"], }, }, { name: "fablecut_auto_duck", - description: "Duck music under dialogue: finds where the voice tracks have sound (RMS above `threshold`, short gaps bridged so the music doesn't pump between words) and writes `duck` keyframes (dB) on the given music clips — a dip of `amount` dB ramping down `attack` s before speech and back up `release` s after. The duck multiplies the clip's volume, so its own level, volume keyframes and fades are untouched, and re-running replaces the previous dips (amount 0 clears them). Voice = every audio clip on `under` tracks (default: all audio tracks the music isn't on). Linked stems of a music clip get the same keys. The same pipeline as the editor's Auto-duck. Needs ffmpeg + ffprobe on PATH. Refuses locked clips unless force:true.", + description: "Duck music under dialogue by writing `duck` keyframes where the voice tracks have sound; a dip of `amount` dB ramps down and up.", inputSchema: { type: "object", properties: { - clipIds: { type: "array", items: { type: "string" }, description: "The music / bed clips to duck" }, - under: { type: "array", items: { type: "string" }, description: "Audio tracks holding the voice, e.g. [\"A1\"] (default: every other audio track)" }, - amount: { type: "number", description: "Dip depth in dB, -40…0 (default -12; 0 removes the ducking)" }, - threshold: { type: "number", description: "Voice detection level in dBFS (default -40; raise it if room noise triggers ducks)" }, - attack: { type: "number", description: "Seconds to ramp down before speech (default 0.3)" }, - release: { type: "number", description: "Seconds to ramp back up after speech (default 0.6)" }, - force:{ type: "boolean", description: "Also change clips the user locked (only when they asked)" }, + clipIds: { type: "array", items: { type: "string" }, description: "The music clips to duck" }, + under: { type: "array", items: { type: "string" }, description: "Voice tracks" }, + amount: { type: "number", description: "Dip depth dB, -40…0 (default -12; 0 clears)" }, + threshold: { type: "number", description: "Voice level dBFS (default -40)" }, + attack: { type: "number", description: "Ramp-down seconds (default 0.3)" }, + release: { type: "number", description: "Ramp-up seconds (default 0.6)" }, + force: { type: "boolean", description: "Also change locked clips" }, }, required: ["clipIds"], }, }, { name: "fablecut_silence", - description: "Find the silences in speech and, with cut:true, cut them out. Listens to the given clips (a video's linked audio stems, after channel routing, gain and volume) and reports every stretch where none of them is above `threshold` for at least `minSilence` seconds, keeping `padding` seconds of quiet beside each phrase so words aren't clipped. Gaps between clips are left alone. With cut:true each silence is removed with an `extract` on the targeted tracks (or `tracks`), last first, in one revision: later material moves up, linked stems stay in sync, locked tracks and clips stay put — lock the music bed first if it should keep running. A version is saved before the cut. Without cut it only reports, so check the spans (and fablecut_frame them) first. The same as the inspector's Silence section. Needs ffmpeg + ffprobe on PATH.", + description: "Find the silences in speech and, with `cut:true`, cut them out, keeping `padding` of quiet beside each phrase.", inputSchema: { type: "object", properties: { - clipIds: { type: "array", items: { type: "string" }, description: "The speech clips to listen to (a video clip brings its linked stems)" }, - threshold: { type: "number", description: "Below this level (dBFS) counts as silence, -80…-10 (default -40; raise it toward -30 for a noisy room)" }, - minSilence: { type: "number", description: "Shortest silence to report or cut, seconds 0.1…10 (default 0.5)" }, - padding: { type: "number", description: "Quiet kept beside each phrase, seconds 0…2 (default 0.15)" }, - cut: { type: "boolean", description: "Cut the silences out (default false: only report them)" }, - tracks: { type: "array", items: { type: "string" }, description: "cut: the lanes to cut on (default: the targeted tracks)" }, - force: { type: "boolean", description: "cut: also cut through locked clips (only when the user asked)" }, + clipIds: { type: "array", items: { type: "string" }, description: "The speech clips to listen to" }, + threshold: { type: "number", description: "Below this dBFS is silence, -80…-10 (default -40)" }, + minSilence: { type: "number", description: "Shortest silence, 0.1…10 s (default 0.5)" }, + padding: { type: "number", description: "Quiet beside a phrase, 0…2 s (default 0.15)" }, + cut: { type: "boolean", description: "Cut the silences out (default false)" }, + tracks: { type: "array", items: { type: "string" }, description: "cut: the lanes to cut on" }, + force: { type: "boolean", description: "cut: also cut locked clips" }, }, required: ["clipIds"], }, }, { name: "fablecut_denoise", - description: "Reduce background noise (hiss, hum, room tone) on audio clips: ffmpeg's FFT denoiser renders a cleaned FLAC of each clip's whole source file into media/, and the clip — with its linked stems — switches to it (the picture keeps its own file, so links and timing stay intact). The same as the inspector's Noise control. amount 'off' switches back to the original. Re-running reuses a file already rendered. Needs ffmpeg + ffprobe on PATH. Refuses locked clips unless force:true.", + description: "Reduce background noise on audio clips: ffmpeg's denoiser renders a cleaned copy the clip switches to; 'off' restores the original.", inputSchema: { type: "object", properties: { - clipIds: { type: "array", items: { type: "string" }, description: "Audio clips, or video clips with linked audio stems" }, - amount: { type: "string", enum: ["light", "medium", "strong", "off"], description: "How hard to pull the noise down (default medium); off restores the original audio" }, - force: { type: "boolean", description: "Also change clips the user locked (only when they asked)" }, + clipIds: { type: "array", items: { type: "string" }, description: "Audio clips, or video clips with stems" }, + amount: { type: "string", enum: ["light", "medium", "strong", "off"], description: "Noise strength (default medium); off = original" }, + force: { type: "boolean", description: "Also change locked clips" }, }, required: ["clipIds"], }, }, { name: "fablecut_projects", - description: "Several projects in one Cutline: the projects dashboard. ./project.json (what every other tool reads and edits) is always the OPEN project; the others are stored beside it and share media/, exports/ and the library, so a duplicate or a template copies no footage. Actions: list (default; archived:'only'|'all' to see archived ones) · open {id} — switches the editor to that project (the open one is saved first; editor tabs reload; your next fablecut_get_project reads the new one) · new {name, template?, fps?, width?, height?, notes?, tags?, open?:true} — template is a built-in canvas (landscape-1080 default, vertical-1080, square-1080, portrait-1350, uhd-4k, landscape-720) or a saved template id · duplicate {id, name?, open?:false} · update {id, name?, notes?, tags?} · archive / unarchive {id} · delete {id} (archived projects only) · templates · save_template {id, name} (the project's canvas, tracks, clips and media references) · delete_template {template}. Opening another project changes what the user sees — only open, archive or delete when the user asked for it.", + description: "Manage the projects in this Cutline (list, open, new, duplicate, update, archive, delete, templates); opening or deleting changes the user's view.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "open", "new", "duplicate", "update", "archive", "unarchive", "delete", "templates", "save_template", "delete_template"], description: "What to do (default list)" }, - id: { type: "string", description: "The project (p_…), from list" }, - name: { type: "string", description: "new / duplicate / update: the project's name · save_template: the template's name" }, - template: { type: "string", description: "new: the template to start from · delete_template: the saved template's id" }, - fps: { type: "number", description: "new: frame rate (default: the template's)" }, - width: { type: "integer", description: "new: canvas width (default: the template's)" }, - height: { type: "integer", description: "new: canvas height (default: the template's)" }, - notes: { type: "string", description: "new / update: free-text notes (brief, client, deadline …)" }, - tags: { type: "array", items: { type: "string" }, description: "new / update: labels (replaces the list)" }, - open: { type: "boolean", description: "new (default true) / duplicate (default false): open it afterwards" }, + id: { type: "string", description: "The project (p_…)" }, + name: { type: "string", description: "The project's or template's name" }, + template: { type: "string", description: "Template to start from, or its id" }, + fps: { type: "number", description: "Frame rate (template default)" }, + width: { type: "integer", description: "Canvas width (template default)" }, + height: { type: "integer", description: "Canvas height (template default)" }, + notes: { type: "string", description: "Free-text notes" }, + tags: { type: "array", items: { type: "string" }, description: "Labels (replaces the list)" }, + open: { type: "boolean", description: "Open it after (new: true, duplicate: false)" }, archived: { type: "string", enum: ["exclude", "only", "all"], description: "list: archived projects too, or only them" }, }, }, }, { name: "fablecut_versions", - description: "A project's version history: saved copies you can go back to, never destructive. Actions: list (default) · save {name?, note?, final?} — keep the project as it is now (do this before big or risky edits, and when the user is happy with a cut) · diff {version, to?} — what changed from that version to now (or to another version): clips added / removed / changed, length, media, settings · restore {version} — put that version back; the current state is saved as a version first, so a restore can itself be undone · update {version, name?, note?, final?} — rename, annotate, or mark the one final cut · delete {version}. project defaults to the open one (fablecut_projects lists ids). Versions are also made on their own: every 10 minutes while the project changes, and before an agent's first edit of a session or a fablecut_set_project rewrite. Restoring changes what the user sees: only when they asked.", + description: "A project's version history (list, save, diff, restore, update, delete); restoring puts a version back after saving the current state.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "save", "diff", "restore", "update", "delete"], description: "What to do (default list)" }, project: { type: "string", description: "The project (p_…); default the open one" }, - version: { description: "The version: its number (3) or \"v3\"", anyOf: [{ type: "integer" }, { type: "string" }] }, - to: { description: "diff: compare to this version instead of the current project", anyOf: [{ type: "integer" }, { type: "string" }] }, - name: { type: "string", description: "save / update: the version's name (default \"Version NN\")" }, - note: { type: "string", description: "save / update: what this version is (\"client cut, shorter intro\")" }, - final: { type: "boolean", description: "save / update: mark it the final cut (one per project)" }, + version: { description: "Its number (3) or \"v3\"", anyOf: [{ type: "integer" }, { type: "string" }] }, + to: { description: "diff: compare to this version", anyOf: [{ type: "integer" }, { type: "string" }] }, + name: { type: "string", description: "The version's name (default \"Version NN\")" }, + note: { type: "string", description: "What this version is" }, + final: { type: "boolean", description: "Mark it the final cut (one per project)" }, }, }, }, { name: "fablecut_proxy", - description: "Proxies: light H.264 copies (540p by default, 720p or 1080p; never larger than the source) of heavy footage — 4K, HEVC, ProRes, long phone clips — that the editor plays while the user edits, so playback and scrubbing stay smooth. Export, fablecut_frame, fablecut_scopes and every measurement always read the original, so a proxy never changes the result. Without create or delete it reports each video's proxy (ready, being made with its progress, failed, none). create:true makes them (one at a time, in the background on the server) and waits until they are done unless wait:false; delete:true removes them. mediaIds defaults to every video in the project. The user turns proxy playback on or off with the monitor's Proxy button. Needs ffmpeg on PATH.", + description: "Report or make light H.264 copies of heavy footage (540p by default) for smooth playback; exports read the original.", inputSchema: { type: "object", properties: { - mediaIds: { type: "array", items: { type: "string" }, description: "Video media ids (default: all video media)" }, + mediaIds: { type: "array", items: { type: "string" }, description: "Video media ids (default: all)" }, create: { type: "boolean", description: "Make proxies for these files" }, delete: { type: "boolean", description: "Delete their proxies" }, height: { type: "number", enum: [540, 720, 1080], description: "Proxy height in pixels (default 540)" }, - wait: { type: "boolean", description: "With create: wait until every proxy is done (default true)" }, + wait: { type: "boolean", description: "With create: wait for every proxy (default true)" }, timeout: { type: "number", description: "Seconds to wait with create (default 900)" }, }, }, }, { name: "fablecut_export", - description: "Render the timeline to a video file in exports/ — the editor's own Fast export (same compositor, audio mix and encoding profile as the Export button), so the file matches what the user sees. It runs in the open editor tab, which takes the job (the user sees the progress bar). A background browser is not used here: it cannot load the editor's authenticated origin, so the editor tab has to be open. Waits for the file by default and returns its path. Range: the project's inPoint→outPoint when set, else the whole timeline (or pass range). Needs ffmpeg on PATH. One export at a time. Long edits: pass wait:false, then poll with {job}; {cancel:job} stops one.", + description: "Render the timeline to a video file in exports/, with the editor's own compositor and encoding. It runs in the open editor tab, so the Mini App has to be open.", inputSchema: { type: "object", properties: { - range: { type: "string", enum: ["entire", "in-out"], description: "Whole timeline, or the project's inPoint→outPoint (default: in-out when either is set)" }, - preset: { type: "string", description: "Delivery preset: youtube, youtube-shorts, instagram-reels, tiktok, x, linkedin, master, broadcast, web, archive, custom, or a saved preset's id (fablecut_encode_profiles lists them). The output is reframed to the preset's size — cropped from the middle (or from the project's exportFrame), or padded with reframe:'fit'" }, - delivery: { type: "object", description: "Delivery settings over the preset (or over custom): {width, height (null = the picture's size), fps (null = the project's), reframe:'fill'|'fit'|'stretch', container:'mp4'|'mov'|'mkv'|'webm'|'mxf', video:{codec:'h264'|'hevc'|'av1'|'vp9'|'prores'|'dnxhr'|'mpeg2'|'ffv1', encoder?, rate:'crf'|'vbr'|'cbr', crf, bitrate (kb/s), maxrate (kb/s), keyint (s), pixFmt, speed:'fast'|'balanced'|'best', profile (prores: proxy|lt|standard|hq|4444|4444xq · dnxhr: lb|sq|hq|hqx|444)}, audio:{codec:'aac'|'opus'|'mp3'|'pcm16'|'pcm24'|'flac'|'none', bitrate (kb/s), sampleRate, channels, loudness (LUFS target: -14 streaming/social, -16 podcast, -23 EBU R128, -24 ATSC; null = as mixed), truePeak (dBTP ceiling, default -1)}, color:'bt709'|'bt709-full'|'bt601-625'|'bt601-525', filename:'{project}_{preset}_{res}' (tokens {project} {preset} {res} {w} {h} {fps} {codec} {date} {time} {range} {n}), metadata:{title, artist, description, copyright, date, keywords}}. Anything left out comes from the preset; impossible combinations are corrected and the reply says how. Every finished file is checked against what was asked (size, fps, frames, codecs, length, loudness, true peak, file size) and the reply lists the checks" }, - outputs: { description: "Several files, rendered once per frame rate (up to 8 outputs per render): true = the project's checked batch outputs (project.outputs, set with setProject {outputs:[…]}), or a list like [{preset:'master'}, {preset:'youtube'}, {preset:'instagram-reels'}, {preset:'custom', width:1080, height:1080, filename:'{project}_square_{res}'}, {preset:'web', filename:'{project}_preview_{res}'}]. Outputs with the same fps share one render; the reply lists every file", anyOf: [{ type: "boolean" }, { type: "array", items: { anyOf: [{ type: "string" }, { type: "object" }] } }] }, - profile: { type: "string", description: "Encoding profile id (raw ffmpeg args, fablecut_encode_profiles) instead of a preset. With neither: project.delivery, else project.encodeProfile, else the server's default profile" }, - where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab: an open editor tab does the work. headless is refused here: a background browser cannot load the editor's authenticated origin, so open the editor tab first." }, + range: { type: "string", enum: ["entire", "in-out"], description: "Whole timeline, or inPoint→outPoint; default in-out when either marker is set, otherwise entire" }, + preset: { type: "string", description: "Delivery preset id (fablecut_encode_profiles lists them)" }, + delivery: { type: "object", description: "Over the preset: {width, height, fps, reframe, container, video, audio, color, filename, metadata}" }, + outputs: { description: "true = the project's batch outputs, or a list of preset ids", anyOf: [{ type: "boolean" }, { type: "array", items: { anyOf: [{ type: "string" }, { type: "object" }] } }] }, + profile: { type: "string", description: "Encoding profile id (raw ffmpeg args), not with preset, delivery or outputs" }, + where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab; headless is refused here" }, wait: { type: "boolean", description: "Block until the file is written (default true)" }, - timeout: { type: "number", description: "Seconds to wait before returning the job's status instead (default 900); the export keeps running" }, - job: { type: "string", description: "Report on an export started earlier instead of starting one" }, + timeout: { type: "number", description: "Seconds to wait before the status (default 900)" }, + job: { type: "string", description: "Report on an export started earlier" }, cancel: { type: "string", description: "Cancel this export job" }, }, }, }, { name: "fablecut_scopes", - description: "Measure the graded picture at one moment — what the editor's scopes show, as numbers: luma levels (min, 1st percentile, median, mean, 99th percentile, max on 0–1), the % of pixels crushed to black / clipped to white, mean R/G/B, average saturation, and the colour cast of the midtones (hue name + strength; neutral below 0.01), plus the clips on screen with their grades, masks, track mattes and blend modes. Rendered by the same compositor as export (an open editor tab, else headless Chrome / Edge), over the export frame when one is set. Use it to check a grade: e.g. a cast strength above ~0.03 on footage that should be neutral, whitePct above ~1 (clipped highlights), or a median far from ~0.4 for a normally exposed shot. The user's playhead does not move.", + description: "Measure the graded picture at one moment with the editor's scopes: luma, crushed/clipped shares, mean RGB, saturation and colour cast; same compositor as export, playhead stays put.", inputSchema: { type: "object", properties: { - time: { type: "number", description: "Timeline seconds to measure (default: the editor's playhead)" }, + time: { type: "number", description: "Timeline seconds (default: the playhead)" }, matte: { type: "object", - description: "Also measure what one grade layer selects: {clip, layer} (layer = index into the clip's grade.layers). Adds the matte's coverage (% of that clip's picture) and the levels / colour of the selected pixels after grading", + description: "Measure what one grade layer selects: {clip, layer}", properties: { clip: { type: "string" }, layer: { type: "integer", minimum: 0 } }, required: ["clip", "layer"], }, mask: { type: "object", - description: "Also measure a clip's masks: {clip}. Adds how much of that clip's picture the masks keep (coverage %, the feathered matte measured alone) and how much of the frame the masked clip covers", + description: "Measure a clip's masks: {clip}", properties: { clip: { type: "string" } }, required: ["clip"], }, - where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab: an open editor tab does the work. headless is refused here: a background browser cannot load the editor's authenticated origin, so open the editor tab first." }, + where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab; headless is refused here" }, timeout: { type: "number", description: "Seconds to wait (default 60)" }, }, }, }, { name: "fablecut_track", - description: "Follow something in a video clip frame by frame (motion tracking, in the editor: an open tab, else headless Chrome / Edge) and store it on the clip as a named track (props.tracks). Then, in the same call, make a mask follow it (apply:{mask}) or pin another clip to it (apply:{clip} — a title, sticker, blur box… its x / y, and optionally scale / rotation, get keyframes that ride on the track, keeping where it is at `at`). What to follow: region {x, y, w, h} or point {x, y} (+ size), as fractions of the clip's picture (0…1, y down) at time `at`; or leave both out with apply:{mask} to follow what that mask covers. To re-apply an existing track, pass its name with apply and no region. Pick something with detail (a corner, a logo, text, an eye), not a flat area; the result reports match quality (≥ 0.8 is solid) and where the region was lost, if it was. Check the outcome with fablecut_scopes or by looking at the keyed clip.", + description: "Track a detailed region in a video clip frame by frame, store a named track, and let a mask or another clip follow it; reports match quality.", inputSchema: { type: "object", properties: { clip: { type: "string", description: "The video clip to track in" }, - region: { type: "object", description: "The area to follow at `at`: centre x, y and size w, h, fractions of the clip's picture", properties: { x: { type: "number" }, y: { type: "number" }, w: { type: "number" }, h: { type: "number" } }, required: ["x", "y", "w", "h"] }, - point: { type: "object", description: "A spot to follow at `at` (fractions of the picture); a square region of `size` around it is matched", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, - size: { type: "number", description: "With point: the region's width, fraction of the picture's width (default 0.08)" }, - at: { type: "number", description: "Timeline seconds where the region is as given (default: the clip's start)" }, - direction: { type: "string", enum: ["forward", "backward", "both"], description: "Track from `at` to the clip's end (default), to its start, or both" }, - to: { type: "number", description: "Timeline seconds to stop at (default: the clip's end / start)" }, - scale: { type: "boolean", description: "Also follow the region's size (zooms, objects moving closer)" }, + region: { type: "object", description: "The area to follow at `at`: x, y centre and w, h, fractions", properties: { x: { type: "number" }, y: { type: "number" }, w: { type: "number" }, h: { type: "number" } }, required: ["x", "y", "w", "h"] }, + point: { type: "object", description: "A spot to follow at `at`, in fractions", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] }, + size: { type: "number", description: "With point: region width as a fraction (default 0.08)" }, + at: { type: "number", description: "Timeline seconds where the region is as given" }, + direction: { type: "string", enum: ["forward", "backward", "both"], description: "Forward (default), backward or both" }, + to: { type: "number", description: "Timeline seconds to stop at" }, + scale: { type: "boolean", description: "Also follow the region's size" }, rotation: { type: "boolean", description: "Also follow the region's rotation" }, - name: { type: "string", description: "The track's name (default: the mask's name, or Track n). An existing name is extended / replaced over the tracked range" }, + name: { type: "string", description: "The track's name (default: the mask's name, or Track n); an existing name is extended or replaced over the tracked range" }, apply: { type: "object", - description: "Bake the track into keys: {mask: index | name} (the mask's x / y — and scale / rotation when tracked — follow) or {clip: id, scale?, rotation?, at?} (that clip rides on the track)", + description: "Bake the track into keys: {mask} or {clip, scale?, rotation?, at?}", properties: { mask: { type: ["integer", "string"] }, clip: { type: "string" }, scale: { type: "boolean" }, rotation: { type: "boolean" }, at: { type: "number" } }, }, - force: { type: "boolean", description: "Allow changing locked clips — only when the user asked" }, - where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab: an open editor tab does the work. headless is refused here: a background browser cannot load the editor's authenticated origin, so open the editor tab first." }, + force: { type: "boolean", description: "Allow changing locked clips, when asked" }, + where: { type: "string", enum: ["auto", "tab", "headless"], description: "auto (default) or tab; headless is refused here" }, timeout: { type: "number", description: "Seconds to wait (default 300)" }, }, required: ["clip"], @@ -450,13 +450,12 @@ const TOOLS = [ }, { name: "fablecut_import_project", - description: - "Import an existing Cutline project from another Cutline data folder — the one that holds project.json and media/. The document is read, every media file it uses is COPIED into this installation's own library (the originals are left untouched), the media entries point at the copies, and the project becomes the open one at the actual current revision. A bare project.json cannot be imported this way: its /media/ paths belong to the folder it came from, so the files have to travel with it. A media file that is not in the source folder STOPS the import: nothing is imported and nothing is changed, rather than leaving an entry that points at an unrelated file. Only media files are copied (video, audio, image, SVG) — a file that is not one stops the import as well. Use this when the user points at an earlier Cutline folder; use fablecut_import_media plus fablecut_set_project to place single files by hand.", + description: "Import a Cutline project folder (project.json plus media/); referenced media are copied into this installation's library and originals stay untouched. Missing, escaping or unsupported files refuse the import.", inputSchema: { type: "object", properties: { - folder: { type: "string", description: "The Cutline data folder to import from: the one that holds project.json and media/ (or projects//project.json)." }, - project: { type: "string", description: "Which project to import when the folder holds several (projects//project.json). Default: the open one, or the only one." }, + folder: { type: "string", description: "The data folder holding project.json and media/" }, + project: { type: "string", description: "Which project to import from that folder" }, }, required: ["folder"], }, diff --git a/plugins/klarkxy/cutline/package.json b/plugins/klarkxy/cutline/package.json index 378d1ef..b548cca 100644 --- a/plugins/klarkxy/cutline/package.json +++ b/plugins/klarkxy/cutline/package.json @@ -1,6 +1,6 @@ { "name": "cutline", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "commonjs", "description": "Cutline as a MiniMax Code Mini App: the FableCut web editor plus 21 Agent-facing MCP tools (19 upstream fablecut_* tools, a project-folder import tool and the standalone cutline_export tool) on one Host-assigned port.", diff --git a/plugins/klarkxy/cutline/skills/cutline/SKILL.md b/plugins/klarkxy/cutline/skills/cutline/SKILL.md index cb180a5..de06eec 100644 --- a/plugins/klarkxy/cutline/skills/cutline/SKILL.md +++ b/plugins/klarkxy/cutline/skills/cutline/SKILL.md @@ -6,201 +6,97 @@ description: Edit video with the Cutline Mini App - assemble a cut, place titles # Cutline Cutline is a browser video editor whose whole timeline is one JSON document. You edit a project by -patching that document with the `fablecut_*` tools this plugin exposes. The open editor reloads the -document as you work, so the user watches the cut change while you build it. +patching that document with the `fablecut_*` tools this plugin exposes; the open editor reloads as +you work, so the user watches the cut change while you build it. > Tool names, prop names and the recipes in this skill and its references are adapted from the > FableCut project by ronak-create, under the MIT License (Copyright (c) 2026 ronak-create). Keep > that notice with any copy of this text. -> The tool names keep the upstream `fablecut_` prefix. The plugin, the Mini App and this skill are -> branded Cutline, but the tools are the upstream ones under their upstream names, so a later -> upstream update can be applied without renaming the public tool surface. Call them exactly as -> `fablecut_status`, `fablecut_get_project`, and so on. - -## Open the editor first - -The editor is a Host-managed service, not a process you start. - -- Open it with the Host's Mini App open action and `pluginId: "cutline"`. -- Never launch the editor from a shell command and never build an editor URL yourself. The Host - serves the page on its own guarded origin; there is no fixed address to visit and no server for - you to bind. -- `fablecut_status` reports whether the editor is up, which project is open, and what is in the - media bin. It is a status call here, not a launcher. - -## The tools - -| Tool | Use it for | Arguments worth knowing | -| --- | --- | --- | -| `fablecut_status` | Editor state, the open project, the media bin | none | -| `fablecut_docs` | The project schema manual | `section` (substring match on a heading) | -| `fablecut_get_project` | Read the timeline document | `compact: true` | -| `fablecut_patch_project` | Targeted edits - the default way to change anything | `ops[]`, `baseRevision` | -| `fablecut_set_project` | Replace the whole document | `project`, `baseRevision` (required), `force` | -| `fablecut_frame` | Look at footage or at the composed timeline | `path`, `t`, `frames`, `from`, `to`, `time`, `text`, `where` | -| `fablecut_scopes` | Numeric measurement of the graded picture | `time`, `matte`, `mask`, `where` | -| `fablecut_track` | Motion-track a region, then bake it into a mask or another clip | `clip`, `region` or `point`, `at`, `direction`, `apply` | -| `fablecut_import_media` | Bring a local file or an https URL into the project | `path` | -| `fablecut_import_project` | Import a whole project from another Cutline data folder | `folder`, `project` | -| `fablecut_analyze_reference` | Turn a reference video into an edit blueprint | `path`, `threshold`, `registerMusic` | -| `fablecut_proxy` | Light playback copies of heavy footage | `mediaIds`, `create`, `delete`, `height`, `wait` | -| `fablecut_normalize_audio` | Bring clips to a loudness target | `clipIds`, `mode`, `target`, `force` | -| `fablecut_auto_duck` | Dip music under dialogue | `clipIds`, `under`, `amount`, `threshold`, `attack`, `release` | -| `fablecut_silence` | Find, or cut out, silences in speech | `clipIds`, `threshold`, `minSilence`, `padding`, `cut`, `tracks` | -| `fablecut_denoise` | Reduce hiss, hum and room tone | `clipIds`, `amount` | -| `fablecut_projects` | List, open, create, duplicate, archive projects and templates | `action`, `id`, `name`, `template`, `fps`, `width`, `height`, `open` | -| `fablecut_versions` | Save, diff, restore and annotate versions | `action`, `version`, `to`, `name`, `note`, `final` | -| `fablecut_encode_profiles` | Delivery presets and raw encoding profiles | `preset`, `profile`, `detail` | -| `fablecut_export` | Render the timeline to a file, in the open editor tab | `preset`, `delivery`, `outputs`, `profile`, `range`, `where`, `wait`, `job`, `cancel` | -| `cutline_export` | Render the timeline to a file with a standalone runner you start yourself | `action` (`prepare` / `status` / `cancel`), `range`, `preset`, `delivery`, `profile`, `job` | - -That is the upstream set of 19, plus `fablecut_import_project` and `cutline_export`, which this -Mini App adds - 21 in all. The Host may serve a different set, so check the tool list you actually -have instead of assuming. - -## Two ways to render - -`fablecut_export` renders in the open editor tab, so the Mini App has to be open and the user sees -the progress bar. It also supports batch outputs - several files from one render. - -`cutline_export` is this package's own tool. It prepares a job - a snapshot of the saved -project, copies of its media and library files, a copy of the editor and a copy of a standalone -runner - and gives you the exact path of that runner. You start it yourself, in the foreground of -one of your background shell tasks, and watch it with `cutline_export {action:"status"}`. - -- The runner is a plain Node process outside the Mini App. Stopping the Mini App does not stop a - render that has already started. -- It is not a detached daemon: it belongs to the background task that started it, and it goes away - with that task. Do not tell the user it survives the app closing. -- It renders the snapshot, so an edit made after `prepare` is not in the file. -- One file per render - it has no batch outputs. -- It needs ffmpeg, ffprobe and a Chrome or Edge install, and it fails with a clear message when one - of them is missing. -- The finished file lands in the job folder's `exports` directory, and the status gives its absolute - path. Copy it out where the user wants it, then the job folder can go: nothing deletes it for you, - and it holds copies of the media and the video, so it costs disk space. A file that failed its - checks stays in the job folder's `provisional` directory. - -Use the exact paths the tool returns. They are absolute and they are the authority. - -See [export.md](references/export.md) for both. +## The runtime manual is the authority + +`fablecut_docs` returns the editor's master manual: the document schema, every op and property, +and the upstream recipes. Read the section behind your change before you build a call, and read it +again whenever a call is unfamiliar - the tool schemas and the manual are the source of truth, and +these guides only describe how to work. The same manual is on disk as +[CLAUDE.md](../../miniapp/node/vendor/fablecut/CLAUDE.md); its "Scope note - the Cutline Mini App" +section covers how this installation differs from upstream. + +Pass `section` to fetch only the part you need - a substring of a heading, such as "props" or +"Recipes". A section name that misses comes back with the list of headings it has. + +Tool names keep the upstream `fablecut_` prefix, so call them exactly as the schema names them. +The Host may serve a different set of tools than any list written here, so read the tool list you +actually have. + +## Open the editor, then read its state + +The editor is a Host-managed service: the Host opens it and serves it on its own guarded origin. +Open the Mini App with the Host's open action and `pluginId: "cutline"`, then call +`fablecut_status` to see whether the editor is up, which project is open and what is in the media +bin. The status call reports; the Host's open action is the way in. ## A reliable order of calls 1. `fablecut_status` - confirm the editor is up and see which project is open. -2. `fablecut_docs {section:"project.json schema"}`, or another section you need - read the prop - names before sending ops. Skip it when the schema is already in context. If a section name does - not match, the tool replies with the list of headings it has; use that list rather than guessing. -3. `fablecut_get_project {compact:true}` - one line per clip with ids, tracks, timings and - non-default props. It is roughly ten times smaller than the full document, and it is enough to - plan most edits. Fetch the full JSON only when you need exact keyframes. -4. `fablecut_patch_project {ops:[...]}` - send the change. It re-reads the latest document, applies - the ops in order, bumps the revision once and saves atomically, so only the fields your ops - name get written. That is targeted, not blanket merge-safe: an op that sets a field the user - changed in the UI a moment ago overwrites that field as well. Pass `baseRevision` when you - need that concurrent change detected rather than applied. Batch related changes into one call. +2. `fablecut_docs` - read the schema section behind the change you plan to make. +3. `fablecut_get_project` with `compact:true` - one line per clip with ids, tracks, timings and + non-default props. It is enough to plan most edits; fetch the full document when you need exact + keyframe values. +4. `fablecut_patch_project` - send the change as ops. It re-reads the latest document, applies the + ops in order and saves atomically, so only the fields your ops name get written. Batch related + changes into one call. 5. Look at the result: `fablecut_frame` for a picture, `fablecut_scopes` for numbers. -`fablecut_set_project` replaces the whole document and only earns its cost for structural -changes. It takes the document **and** `baseRevision` - the revision the document was -read at, from `fablecut_get_project`, `fablecut_status` or the last patch result. Do not -bump the revision inside the document: the server owns it and stores the document at the -current revision + 1. If the project moved on since your read, the call refuses with -CONFLICT instead of overwriting. On conflict, re-read, re-apply your change on top of the -latest document, and call it again with the new revision. `force: true` overwrites a -conflict - only when the user asked for it. - -## Motion tracking - -`fablecut_track` follows a region of a video clip's picture frame by frame and stores it on the -clip as a named track. - -- `region {x, y, w, h}` or `point {x, y}` - all **fractions of the clip's picture**, 0 to 1, with y - pointing down. A `point` matches a square region around it, `size` wide (0.08 by default). -- Leave both out to follow whatever the mask named in `apply` covers. -- `name` names the track. Reuse a name and the existing track is extended or replaced over the - tracked range, so a second run can carry on from a first one. -- `apply` bakes the result in: `{mask: index | name}` moves that mask, or `{clip: id}` pins another - clip - a title, a sticker - to the track, with optional `scale` and `rotation`. -- `at` is the timeline second the region is described at (the clip's start by default); - `direction` is `forward`, `backward` or `both`. -- Read the tool's own schema before a call; it is the authority on the fields. - -## The service is shared - -The editor service is Host-managed and shared across pages, so the project that is open when you -start is not necessarily the one you will be editing a minute later, and another page may be -editing the same project. +`fablecut_patch_project` is the default way to change anything. `fablecut_set_project` replaces the +whole document and is reserved for structural changes - see [editing.md](references/editing.md). + +## The project is shared + +The editor service is shared across pages, so the open project can change under you and another +page may be editing the same one. - Before each batch of edits, re-read the compact project and confirm which project and revision you are about to change. -- Pass `baseRevision` when you planned against a specific revision. The patch is then refused with - CONFLICT instead of quietly applying on top of someone else's change. -- `fablecut_set_project` needs the same base revision, as its own `baseRevision` argument. The - document you send must be the one you read at that revision. -- On CONFLICT, re-read, re-plan against the new revision and retry. Do not replay the same ops - unchanged. -- Do not promise that two concurrent edits merge perfectly. Say what you changed and which - revision you changed it on. -- `fablecut_projects {action:"open"}` changes what every user of the service sees. Switch, - archive or delete a project only when the user asked for it, and re-read the project afterwards. - -## The media you bring - -Cutline edits media files; it does not invent footage, music or voice. This plugin offers no -media generation step, so new material reaches the timeline the same way any other material does: -as an imported file. - -- `fablecut_import_media {path: ""}` copies a local file into the project's media bin - and registers it. The original file is left untouched. -- The same tool takes an `https://` URL and downloads it into the project. -- A whole project folder can be brought in with `fablecut_import_project {folder}`. -- Only media files are taken: video, audio, images and SVG. An HTML page, a script, a font or - any other non-media file is refused, because the media folder is served from the editor's own - origin - a file a browser would run there would run there. Fonts live in the asset library, - not in the media folder. Every response the editor sends declares its type (`nosniff`), and a - document type among the user's own files is served sandboxed. -- Captions are text clips: the words come from the user (or from a transcript the user supplies), - and the timing comes from the user, from the reference, or from your own reading of the clip. - See [audio-and-subtitles.md](references/audio-and-subtitles.md). - -The import boundary is the contract: whatever produced the file, it ends up as a local file that -goes into `fablecut_import_media`, and the timeline format does not change. - -## References - -- [editing.md](references/editing.md) - the document model, the patch ops, the media bin, - sequences, versions, text and grade. -- [audio-and-subtitles.md](references/audio-and-subtitles.md) - loudness, ducking, silence, denoise, - and building captions. -- [motion-graphics.md](references/motion-graphics.md) - shape layers, nulls, parenting, draw-ons, - motion paths, compositions, presets and templates. -- [export.md](references/export.md) - delivery presets, delivery overrides, batch outputs, ranges, - and the editor-tab requirement. - -## Limits worth stating out loud - -- `fablecut_frame`, `fablecut_scopes`, `fablecut_track`, `fablecut_export`, - `fablecut_analyze_reference`, `fablecut_normalize_audio`, `fablecut_auto_duck`, - `fablecut_silence` and `fablecut_denoise` need ffmpeg (and ffprobe) on PATH. When one of them - fails on a missing binary, report that instead of retrying it. -- `fablecut_export`, `fablecut_scopes`, `fablecut_track` and the timeline mode of - `fablecut_frame` render through the editor's own tab, and there is no headless fallback here: - the editor is served on the Host's authenticated port, so a browser spawned outside it cannot - load the page. When such a call fails for want of a tab, open the Mini App and call it again. - `cutline_export` is the way out: its runner serves its own copy of the editor on its own - port, so it needs no open tab. -- `fablecut_frame` returns an image. If you cannot read images, pass `text:true` and it returns - measurements instead. +- Pass `baseRevision` when you planned against a specific revision. A write against a revision that + has moved on is refused with CONFLICT, which keeps it from landing on top of someone else's + change. +- On CONFLICT, re-read, re-plan against the new revision and retry with the new ops. +- `force` overwrites a conflict; use it only when the user asked for that change. +- Report what you changed and which revision you changed it on. Two concurrent edits can land on + top of each other, so describe yours rather than claiming a perfect merge. +- Opening, archiving or deleting a project changes what every user of the service sees. Do it when + the user asked, and re-read the project afterwards. + +## Media comes from the user + +Cutline edits media files the user supplies; footage, music and voice all reach the timeline as +imported files. Use a path the user named, and leave the original file where it is. Captions are +text clips: the words come from the user, and the timing from the user, the reference or your own +reading of the clip. + +## Review before you report + +A change that parses can still look wrong. Look at a frame after the edit you made, and look at +the composed timeline when you placed a caption or a title, so you can see that it lands on the +right moment and leaves on the right one. Reach for the scopes reading when the picture looks off. +A refusal or a failed check is information about the project: report it plainly, with the reason. + +## Choose the workflow + +- [editing.md](references/editing.md) - reading a project, patching it, media, text, grade, versions. +- [audio-and-subtitles.md](references/audio-and-subtitles.md) - loudness, ducking, silence, denoise, mixing, captions. +- [motion-graphics.md](references/motion-graphics.md) - shape layers, nulls, parenting, motion paths, comps, presets, templates. +- [export.md](references/export.md) - the two render paths, ranges, batch outputs, the checks a file goes through. + +## Cautions worth stating out loud + +- The tools that read or write media need ffmpeg (and ffprobe) on PATH. A failure on a missing + binary is worth reporting once, with the binary named. +- The tools that render through the editor need the Mini App open, and this installation has no + headless path for them. The standalone runner in [export.md](references/export.md) is the way out. - The editor has one job slot. An export, a scope reading, a tracking job and a composed-frame - render all take it, so while one runs the others are refused. Wait for it, or stop it with - `{cancel:"x_..."}`. -- A patch is all-or-nothing: one invalid op aborts the whole patch and nothing is saved. -- The user can also export from the editor UI. `fablecut_export` is a convenience, not a gate. - -## Reporting back - -Describe the change in terms visible on the timeline - which clip, which property, which revision - -and name any check you ran. If a tool refused or could not run, say so plainly and say why; a -refusal is information about the project, not a detail to smooth over. + render share it, so one job runs at a time. +- A patch is all-or-nothing: one invalid op aborts the whole patch and the document stays as it + was. +- The user can export from the editor UI as well, so the tool is a convenience rather than a gate. diff --git a/plugins/klarkxy/cutline/skills/cutline/references/audio-and-subtitles.md b/plugins/klarkxy/cutline/skills/cutline/references/audio-and-subtitles.md index 1ff6530..c8b8e7e 100644 --- a/plugins/klarkxy/cutline/skills/cutline/references/audio-and-subtitles.md +++ b/plugins/klarkxy/cutline/skills/cutline/references/audio-and-subtitles.md @@ -3,119 +3,58 @@ Adapted from the FableCut project by ronak-create (MIT License, Copyright (c) 2026 ronak-create). Keep that notice with any copy of this text. -## The four audio tools - -All four need ffmpeg and ffprobe on PATH, and all four refuse to touch a clip the user has locked -unless you pass `force:true` - which you should only do when the user asked for that specific -change. - -### `fablecut_normalize_audio` - -`{clipIds, mode?, target?, force?}` - `mode` is `lufs` (default) or `peak`; `target` is LUFS for -`lufs` (default -14) and dBFS for `peak` (default -1). - -Measures each clip's source audio after its channel routing and writes `props.gain` so it lands on -the target. It is the same measurement the editor's Normalize button uses. Linked stems - the -per-channel audio clips of one video - are measured together and given one shared gain, so a -stereo pair stays balanced; passing a video clip normalizes its stems. Volume keyframes and fades -are untouched and ride on top of the new gain. - -Useful targets: -14 LUFS for streaming and social, -16 LUFS for podcast and dialogue, -23 LUFS for -broadcast, or `mode:"peak"` at -1 dBFS. - -### `fablecut_auto_duck` - -`{clipIds, under?, amount?, threshold?, attack?, release?, force?}` - `clipIds` are the music or bed -clips to duck; `under` are the audio tracks holding the voice (default: every audio track the -music is not on). - -Finds where the voice tracks have sound and writes `duck` keyframes on the music: a dip of -`amount` dB ramping down `attack` seconds before speech and back up `release` seconds after. -`amount` runs from -40 to 0 dB (default -12; 0 removes the ducking), `threshold` is the voice -detection level in dBFS (default -40, raise it when room noise triggers ducks), `attack` defaults -to 0.3 s and `release` to 0.6 s. Short gaps are bridged so the music does not pump between words. - -The duck multiplies the clip's volume, so the clip's own level, its volume keyframes and its fades -are unaffected, and re-running replaces the previous dips. Linked stems of a music clip get the -same keys. - -### `fablecut_silence` - -`{clipIds, threshold?, minSilence?, padding?, cut?, tracks?, force?}` - `threshold` is the level -below which sound counts as silence, -80 to -10 dBFS (default -40; raise it toward -30 for a -noisy room); `minSilence` is the shortest stretch to report or cut, 0.1 to 10 s (default 0.5); -`padding` is the quiet kept beside each phrase, 0 to 2 s (default 0.15), so words are not clipped. - -Without `cut` it only reports the spans, so check them - and look at them with `fablecut_frame` - -before removing anything. With `cut:true` each silence is removed with an `extract` on the -targeted tracks, last first, in one revision: later material moves up, linked stems stay in sync, -and locked tracks and clips stay put. Lock the music bed first if it should keep running. A -version is saved before the cut. - -### `fablecut_denoise` - -`{clipIds, amount?, force?}` - `amount` is `light`, `medium` (default), `strong` or `off`. - -ffmpeg's FFT denoiser renders a cleaned FLAC of each clip's whole source file into the media -folder, and the clip - with its linked stems - switches to it. The picture keeps its own file, so -links and timing stay intact. `off` switches back to the original audio. Re-running reuses a file -that was already rendered. +The tool schemas and the runtime manual, [CLAUDE.md](../../../miniapp/node/vendor/fablecut/CLAUDE.md), +are the authority for arguments and levels. This guide covers which tool to reach for. + +## Loudness, ducking, silence, noise + +- `fablecut_normalize_audio` brings clips to a loudness target. It measures the source after + channel routing and writes a gain, so the clip's own volume rides on top of it. Use it when the + mix feels uneven, and pick a target the delivery expects. +- `fablecut_auto_duck` dips music under dialogue by writing duck keyframes on the music, so the + music follows the voice instead of fighting it. Give it the music clips and the tracks that hold + the voice. +- `fablecut_silence` reports the silences in speech, and cuts them when you ask it to. Report + first and look at the spans before removing anything, and lock the music bed first when it + should keep running. +- `fablecut_denoise` cleans hiss, hum and room tone. It renders a cleaned copy of the clip's + source audio and switches the clip to it, so the picture and the timing stay as they were. + +Each of them reads and writes the media file, so ffmpeg has to be on PATH, and each of them skips +a clip the user has locked unless you pass `force` for a change the user asked for. ## Mixing -- `setTrack {id:'A1', set:{gain,pan,out}}` - the track fader in dB (-60 to +12), pan -1 to 1, and - `out` a submix bus id or `'master'`. Null or 0 resets. -- `setBus {id:'B1', set:{name,gain,pan,mute}}` - a submix bus, created if it does not exist. Route - tracks into it with `setTrack {out:'B1'}`. -- `setFx {target:'clip'|'track'|'bus'|'master', id?, preset? | fx?, append?}` - presets - `clean-voice`, `podcast`, `radio`, `deep-voice`, `telephone`, `cinematic`, `wide`, `muffled`. - `fx:null` clears. On a clip it applies to the clip's linked stems too. -- `setFxKeys {target, id?, index?|type?, param, keys:[{t,v,ease?}]}` - automate one effect - parameter. `t` is clip-local for a clip's effects and timeline seconds otherwise. - -## Voice, music and sound you bring +- Keep speech and music on their own audio tracks, separate from the video clip. That is what lets + the faders, the buses and the ducking work on them independently. +- Faders, pans, buses and effects are patch ops on tracks, buses and clips. Reach for them when the + mix needs shaping rather than one clip fixed in isolation. +- A voiceover is a file the user has: import it, give it its own track, and align it to the picture + by trimming its in point and its length. A music bed is a file too, and ducking is how the two + share the timeline. +- Sound effects are files as well. The editor's Sound FX tab shows what the user has put in the + library. -Cutline does not synthesise audio, and this plugin has no generation step: the sound in a cut -is the sound the user supplies. +## Captions -- A voiceover is an audio file the user has. Import it with `fablecut_import_media`, place it on - its own audio track, and align it to the picture by trimming its In point and its duration. -- A music bed is an audio file the user has. Import it, place it on an audio track, and use - `fablecut_auto_duck` when dialogue has to sit over it. -- Keep speech and music on their own audio tracks, not mixed into a video clip. That is what lets - the ducking and the track faders work on them independently. -- Sound effects are files too. The package ships none; the editor's Sound FX tab shows what the - user has put in the library's `sfx/` folder. - -## Subtitles - -Cutline has no caption or transcription tool. A caption is a text clip. - -The workable sequence: +A caption is a text clip, and Cutline has no transcription tool, so the words come from the user: a +script, a subtitle file, or a transcript they hand over. 1. Import the media and know which clip carries the speech. -2. Get the words. They come from the user: a script, a subtitle file, or a transcript the user - pasted or handed over. This plugin does not transcribe - it does not turn speech into text. -3. Get the timing. Use the timings the user supplied, or the ones the subtitle file carries. If - there are none, derive cue boundaries from the words and the clip, and tell the user the cue - timings are approximate. -4. Place one text clip per cue, with the cue's line as `props.text`, `start` at the cue boundary - and `duration` matching how long the line should stay up. -5. Look at the result. `fablecut_frame` with no `path` renders the composed timeline, so you can - confirm a caption is on screen at the moment the speech starts and is gone when it ends. - -Styling: the editor's `subtitle` title style is a small Roboto line at the bottom, over a rounded -pill, with the `karaoke` animation. Reproduce it by writing the same props - `font:"Roboto"`, -`align` centred or left, `y` near the bottom of the canvas, `bgColor` with a non-zero `bgOpacity`, -`textAnim:"karaoke"`, and a `wordRate` that sets how fast the karaoke highlight runs. `clip-reveal` -or `word-pop` work well when the caption should feel like part of the design rather than a -transcript. - -A caption is a visual clip, so it takes a track. Put them on a video track above the footage, and -lower the track's opacity or the clips' own opacity when they cover the picture. +2. Take the timings the user supplied or the subtitle file carries. With neither, derive cue + boundaries from the words and the clip, and tell the user the timings are approximate. +3. Place one text clip per cue, with the cue's line as its text, starting at the cue boundary and + lasting as long as the line should stay up. +4. Reproduce the editor's `subtitle` style by writing its props, or pick a different animation + when the caption should feel like part of the design. +5. Look at the composed timeline and check that each caption is on screen while the speech runs + and gone when it ends. + +A caption is a visual clip, so it takes a track: put them on a video track above the footage and +lower the opacity when they cover the picture. ## Reporting -Say what was measured and what was changed: the loudness target you hit, the dB of ducking you -applied, the silences you cut, the number of cues you placed and how their timings were derived. -When a timing is approximate, say so. +Say what you measured and what you changed: the loudness target, the amount of ducking, the +silences you cut, the number of cues you placed, and how their timings were derived. Say so when a +timing is approximate. diff --git a/plugins/klarkxy/cutline/skills/cutline/references/editing.md b/plugins/klarkxy/cutline/skills/cutline/references/editing.md index 42f006a..d0ed870 100644 --- a/plugins/klarkxy/cutline/skills/cutline/references/editing.md +++ b/plugins/klarkxy/cutline/skills/cutline/references/editing.md @@ -3,257 +3,64 @@ Adapted from the FableCut project by ronak-create (MIT License, Copyright (c) 2026 ronak-create). Keep that notice with any copy of this text. -## The document - -One JSON document holds everything: - -| Part | What it holds | -| --- | --- | -| `project` | canvas (`width`, `height`, `fps`), `background`, `markers`, `inPoint` / `outPoint`, `disabledTracks`, `lockedTracks`, `untargetedTracks`, `encodeProfile`, `delivery`, `outputs`, `master` | -| `tracks` | lanes: `V1`-`V3` video, `A1`-`A4` audio, plus buses | -| `clips` | every clip on every track, each with `id`, `kind`, `track`, `start`, `duration`, `props`, and optionally `keyframes`, `transitionIn`, `transitionOut` | -| `media` | the media bin: every imported file with its probed duration, resolution, frame rate, codecs, audio channels and size | -| `comps` | nested timelines, placed by `kind:"comp"` clips | -| `sequences` | several timelines in one project; the open one is the top level of the document | -| `revision` | an integer that bumps on every save; the editor compares it to detect external changes | - -## Reading - -- `fablecut_get_project {compact:true}` gives one line per clip: ids, tracks, timings and the props - that differ from their defaults, plus the MEDIA and SEQUENCES lists. Plan from that. -- `fablecut_get_project` without `compact` gives the full JSON. Use it when you need exact - keyframe values. -- A media entry marked `OFFLINE` is missing from the media folder. A media entry can also be - missing its probed duration or resolution until the editor fills them in, so check the field is - there before relying on it. - -## Patching - -`fablecut_patch_project {ops:[...]}` is the way to change anything. It re-reads the latest -document, applies the ops in order, bumps the revision once and saves atomically. An invalid op -aborts the whole patch and saves nothing. - -Ops, by group: - -**Clips** - `addClip` (`clip:{...}`, id auto-generated when omitted), `updateClip` (`id`, `set`), -`removeClip` (`id`). - -**Media bin** - `addMedia` (`media:{...}`), `removeMedia` (`id`), `updateMedia` (`id` or `ids`, -`set:{name,favorite,tags,folderId}`, `addTags`, `removeTags`; fps, codec, audio channels and size -are probed from the file and read-only), `setCollection` (`id`, `name`, `media` replaces, `add`, -`remove`; a new id or name creates the collection), `removeCollection` (`id`), `relinkMedia` (`id`, -`src`) to point an offline entry at the same file found elsewhere while its clips keep their in -points, `replaceMedia` (`id`, `src`, `name`) to put a different file behind the entry so every -clip using it switches and keeps its timing. - -**Project** - `setProject` (`set:{name,width,height,fps,background,markers,disabledTracks,lockedTracks,untargetedTracks,encodeProfile,delivery,outputs,master}`). `markers` is the full list, `[{t,label?,color?}]`, with colour one of gold, red, orange, green, cyan, blue, purple, pink. `master` is `{gain}` in dB. `delivery` is a delivery preset id or spec that exports use by default. `outputs` is the batch list, up to 16 entries, exported together by `fablecut_export {outputs:true}`. `setProject` also takes `inPoint` and `outPoint`. - -**Tracks and buses** - `setTrack` (`id:'A1'`, `set:{gain,pan,out}`; gain is -60..+12 dB, pan -1..1, -`out` is a bus id or `'master'`, null or 0 resets), `setBus` (`id:'B1'`, -`set:{name,gain,pan,mute}`; created if missing), `removeBus` (`id`). - -**Audio effects** - `setFx` (`target:'clip'|'track'|'bus'|'master'`, `id`, `preset` or `fx`, and -`append:true`). Presets: `clean-voice`, `podcast`, `radio`, `deep-voice`, `telephone`, `cinematic`, -`wide`, `muffled`. `fx:null` clears. On a clip it applies to that clip's linked stems too. -`setFxKeys` (`target`, `id`, `index` or `type`, `param`, `keys:[{t,v,ease?}]`) automates one -parameter; `t` is clip-local for a clip's effects and timeline seconds otherwise. - -**Grade** - `setGrade` (`id` or `ids`, `grade:{...}`, `replace:true`). Keys: `exposure`, `temp`, -`tint`, `lift`, `gamma`, `gain`, `offset`, `contrast`, `pivot`, `blacks`, `shadows`, `midtones`, -`highlights`, `whites`, `lowSoft`, `highSoft`, `saturation`, `curves` (`{y,r,g,b}` point lists on -0..1 that merge per channel), `hueHue`, `hueSat`, `hueLuma`, `satLuma`, `layers`, `on`. The wheels -are `[r,g,b,master]`. A null value resets that key; `grade:null` clears the grade; `replace:true` -starts from neutral. `setGradeLayer` (`id` or `ids`, `layer` as an index or a name, `set:{...}`, -`remove:true`) adds a secondary grade applied only where a qualifier and a mask select - -`qualifier:{hue:[centre,width,soft], sat:[lo,hi,soft], luma:[lo,hi,soft], invert}` and -`mask:{shape:'ellipse'|'rect'|'poly', x,y,w,h,rotation,feather,invert,points,keys}`. - -**Masks and mattes** - `setMask` (`id` or `ids`, `mask` as an index or a name, `set:{...}`, -`remove:true`) with `shape:'rect'|'ellipse'|'bezier'|'freehand'`, -`mode:'add'|'subtract'|'intersect'|'difference'`, `invert`, `opacity`, `feather` (px), `expand` -(px, negative shrinks), `x`, `y` (centre, 0..1 of the clip's picture), `w`, `h`, `scale`, -`rotation`, `points`, `stroke`, `keys`, `name`, `on`. Omitting the mask, or giving an unknown name -or an index equal to the current length, adds one; a new mask needs a `shape`. `removeMask` (`id`, -`mask`), `setMaskKeys` (`id`, `mask`, `keys`), `setMatte` (`id` or `ids`, -`matte:'alpha'|'alpha-inverted'|'luma'|'luma-inverted'`, `track`) makes the picture on the track -directly above cut the clip, and that track stops drawing while it is a matte. - -**Motion graphics** - see [motion-graphics.md](motion-graphics.md). - -**Sequences** - `addSequence` (`name`, `from:'empty'|'open'|`, `width`, `height`, `fps`, -`open`), `openSequence` (`id`), `updateSequence` (`id`, `set:{name,index}`), `removeSequence` -(`id`), `inSequence` (`sequence`, `ops:[...]`) to run any clip, timeline, track or `setProject` op -on a sequence without opening it. - -**Compositions** - `addComp` (`comp:{id,name,duration,width,height,background,clips}`), -`updateComp` (`id`, `set:{...}`), `removeComp` (`id`, refused while a clip still uses it), -`precompose` (`ids`, `name`, `id`) to turn existing clips into a comp, `inComp` (`comp`, -`ops:[...]`) to edit inside one. - -**Presets and templates** - see [motion-graphics.md](motion-graphics.md). - -**Motion tracking** - `removeTrack` (`id`, `track`) removes a track stored by `fablecut_track`. - -**Timeline edits** - these use the editor's own split, ripple and trim code, so linked stems, -track targeting and locks behave exactly as they do in the UI. Times are in seconds: - -| Op | Effect | -| --- | --- | -| `split` (`at`, `ids?`) | split at a time; without `ids` every targeted track | -| `rippleDelete` (`ids`) | remove clips and close the hole | -| `closeGap` (`at`) | close the gap at a time | -| `lift` / `extract` (`from?`, `to?`) | remove a range; `extract` closes it. Defaults to the project's inPoint / outPoint, which then clear | -| `insert` / `overwrite` (`mediaId`, `at`, `in?`, `duration?`) | three-point edit: `insert` pushes later clips right, `overwrite` replaces what is there; a video brings one audio stem per channel | -| `rippleTrim` / `roll` (`id`, `side:'in'|'out'`, `delta`) | trim one side | -| `slip` / `slide` (`id`, `delta`) | slide the source inside the clip, or move the clip in time; clamped to the media, and the reply says what was applied | -| `crossfade` (`ids?` or `at`, `duration?`) | constant-power audio crossfade, borrowing handles from both sides | - -Any of them takes `tracks:[...]` to target lanes for that op only. - -### Merge rules that catch people - -- `updateClip` replaces top-level keys wholesale - `keyframes`, `transitionIn` and `transitionOut` - are replaced, not merged. `props` merges key by key, and setting any key to `null` deletes it. -- Any op may carry `force:true` to override a lock the user set. Use it only when the user asked - for that specific change. `updateClip {set:{locked:null}}` is always allowed. -- The user can lock clips and tracks. `updateClip` or `removeClip` on a locked clip, and `addClip` - onto a locked track, are refused. - -### Replacing the whole document - -`fablecut_set_project {project, baseRevision}` writes the whole document at once. Use it only for -structural changes; a patch is cheaper and touches fewer fields. - -- `baseRevision` is **required**: the revision the document was read at. If the project moved on - since then, the save is refused with CONFLICT and nothing is written. -- Do not bump the revision inside the document. The server owns it and stores the document at the - current revision + 1. -- `force: true` overwrites a conflict - only when the user asked for it. +The document model - the schema, the patch ops, the properties, the media bin - lives in the +runtime manual, [CLAUDE.md](../../../miniapp/node/vendor/fablecut/CLAUDE.md), which `fablecut_docs` +serves section by section. Read the section you need before you build a call. This guide covers +how to work. + +## Read, plan, patch + +- Read with `fablecut_get_project` and `compact:true`: one line per clip with ids, tracks, timings + and the props that differ from their defaults. It is roughly ten times smaller than the full + document and enough to plan most edits. Fetch the full document when you need exact keyframe + values. +- A media entry marked `OFFLINE` means the file has left the media folder. An entry can also arrive + without its probed duration or resolution until the editor fills them in, so check the field is + there before you rely on it. +- Plan the change, then send it as ops in one `fablecut_patch_project` call. The patch re-reads the + latest document, applies the ops in order and saves atomically, so only the fields your ops name + get written. One invalid op aborts the whole patch, so a good patch is a small one. +- Reach for `fablecut_set_project` when the change is structural - a new canvas, a new sequence, a + rebuilt timeline. Everywhere else a patch is cheaper and touches fewer fields. ## Media -- `fablecut_import_media {path}` copies a local file into the project (including `.svg`) or - downloads an `https://` URL into the media folder. The URL is never kept as the playback source, - because a remote source would break the export. The reply returns the new media entry; use its - id in clips. Only media files are taken - video, audio, images and SVG; anything else is refused. -- `fablecut_proxy` makes light H.264 copies (540p by default, 720p or 1080p, never larger than - the source) of heavy footage so playback and scrubbing stay smooth. Export, frames and scopes - always read the original, so a proxy never changes the result. Make proxies when the user asks; - they cost time and disk. -- `fablecut_analyze_reference {path}` analyses a reference video into a blueprint: `cuts`, - `shots[]` with a per-shot `energy` 0-100, `beats[]`, `bpm`, a loudness `energy` curve, `drop` - (the biggest musical rise), and `avgShotLen`. By default it also extracts the reference's music - into the media bin, registered and ready to place on `A1`. Lower `threshold` when obvious cuts - are missed, raise it when camera motion is being read as cuts. -- A blueprint captures structure - timing, pacing, where the energy sits. It does not capture - look, subject or performance. Say so when you report a rebuild: it matches the rhythm of the - reference, and the grade, footage and titles remain separate choices. - -## Importing a project - -`fablecut_import_project {folder, project?}` brings a whole project in from another Cutline data -folder - the folder that holds `project.json` and `media/`, or `projects//project.json`. -`project` picks which one when the folder holds several; by default it takes the open one, or the -only one, and asks you to choose when it cannot tell. - -What it does: - -- Every media file the project uses is **copied** into this installation's media folder. The - **originals are left untouched**, and a copy never overwrites a file that is already here - it - gets a free name instead. A file in a subfolder of the source `media/` is fine; the copy lands - directly in `media/`. -- The media entries are repointed at the copies and re-probed, so their duration and size are the - copies' own. -- A `/library/...` entry names a library asset, and every installation has its own copy of those. - The reference is kept when this installation has the same asset; when it has none, the source - folder's copy is brought over so the reference resolves. A *different* file under that name is - never used - the source folder's copy goes into `media/` and the entry points at that. -- Everything is checked before a single file is written. A media file that is not in the source - folder **stops the import**: nothing is imported and nothing is changed, rather than leaving an - entry that quietly points at whatever happens to have that name here. -- Only media files travel: video, audio, images and SVG. A file that is not one - an HTML page, - a script, a font - stops the import as well, because the media folder is served from the editor's - own origin and a file a browser would run there would run there. A font is not media: fonts - belong to the asset library, and a differing library font is brought over into the library - rather than into the media folder. - -Only what is inside the folder you name is read: the project id has to be a plain name, and a -media path that climbs out of the source `media/` - or a link that points out of it - is refused. - -**It replaces the project that is open.** The imported document is written as the open project at -the current revision, so the editor and the tools agree on what is open. It does not create a -new project on the side - the project you were editing is replaced in place. The project as it was -is saved as a version *before* the import replaces it, so the previous state is recoverable with -`fablecut_versions`. - -If the open project changed while the import was copying and probing its files, the import is -refused as well, so a newer edit is never overwritten: read the project again and retry. - -A bare `project.json` cannot be imported this way: its `/media/` paths belong to the folder it -came from, so the files have to travel with it. For single files, use `fablecut_import_media` -plus `fablecut_set_project` instead. +- Import a file the user named with `fablecut_import_media`; it copies the file into the project's + media bin and leaves the original where it is. A whole project folder comes in with + `fablecut_import_project`. +- Heavy footage can get light playback copies with `fablecut_proxy`. Export, frames and scopes read + the original, so a proxy smooths the working experience and leaves the result alone. Proxies cost + time and disk, so make them when the user asks. +- A reference video can be analysed into a blueprint with `fablecut_analyze_reference`. A blueprint + captures structure - timing, pacing, where the energy sits - and leaves look, subject and + performance to you. Say so when you report a rebuild. ## Text and titles -Text is a clip with `kind:"text"`. Useful props: `text` (`\n` for a line break), `font`, -`fontSize`, `align`, `x`, `y`, `box`, `boxFit`, `vAlign`, `direction` (`auto` detects per line, so -right-to-left scripts shape correctly), `letterSpacing`, `lineHeight`, `textShadow`, `glow` and -`glowColor`, `strokeWidth` and `strokeColor`, `bgColor` and `bgOpacity`, `textAnim`, `wordRate`, -`textPath`, `pathOffset`, `pathAlign`, `fontCutSet`. - -`textAnim` values: `none`, `typewriter`, `word-pop`, `word-slide`, `karaoke`, `letter-pop`, -`wave`, `bounce`, `shake`, `clip-reveal`, `zoom-in`, `font-cut`, `rise-mask`. +A title is a text clip. The editor ships title styles that bundle a font, a placement and an +animation; reproduce one by writing the same props yourself, and vary the typeface from title to +title so the cut does not read as machine-made. -The editor's title styles each bundle a font, a placement and an animation: `impact` (Anton, -uppercase lower third, `word-pop`), `elegant` (Playfair Display, white-to-gold gradient, centred, -`clip-reveal`), `kinetic` (Bebas Neue, gold, `font-cut` cycling Anton, Bebas, Archivo Black, -Oswald), `neon` (Bebas Neue, cyan glow, `wave`), `handwritten` (Caveat, rotated, lower left, -`word-slide`), `serifDrop` (Abril Fatface, centred, `zoom-in`), `subtitle` (Roboto, small, bottom, -pill background, `karaoke`), `boldRise` (Archivo Black, uppercase lower third, `rise-mask`), -`luxury` (Cinzel, cream-to-gold gradient, wide letter spacing, `clip-reveal`). +## Grade -Reproduce a style by writing the same props from the Agent side. Vary the font from title to -title; one typeface across a whole edit is the clearest tell of an automated cut. +`fablecut_scopes` measures the graded picture at one moment with the compositor the export uses. +Use it to check a grade you are unsure about, and again after a look change. The user's playhead +stays where it is while you measure. -## Grade and measurement - -`fablecut_scopes` measures the graded picture at one moment with the same compositor the export -uses: luma levels (min, 1st percentile, median, mean, 99th percentile, max on 0-1), the share of -pixels crushed to black or clipped to white, mean R/G/B, average saturation, and the colour cast -of the midtones (hue name and strength; neutral below 0.01). `matte:{clip,layer}` measures what a -grade layer selects; `mask:{clip}` measures how much of a clip the masks keep. +## Versions -Useful reference points when a grade looks wrong: a cast strength above roughly 0.03 on footage -that should be neutral, a white-clip share above roughly 1 percent, or a median luma far from -roughly 0.4 on a normally exposed shot. The user's playhead does not move when you measure. +`fablecut_versions` keeps copies of a project. Save one before a large or risky change, and one when +the user is happy with a cut, so they have a named point to go back to. Restoring changes what the +user sees, so restore when they ask. -## Traps +## Traps that cost real work -- Video and audio clips must satisfy `in + duration x speed <= media.duration`. -- A crossfade is same-track overlap plus `transitionIn:{type:"fade"}` on the later clip - not a - separate object. -- Cut, ripple, trim and insert with the timeline ops rather than recomputing `start` and `in` by - hand; the ops keep linked audio stems in sync and respect the user's locks. -- `props` values merge key by key, but `keyframes` are replaced wholesale. Send the whole keyframe +- A crossfade is same-track overlap plus a fade on the later clip, rather than a separate object. +- Cut, ripple, trim and insert with the timeline ops instead of recomputing timings by hand: the + ops keep linked audio stems in sync and respect the user's locks. +- `props` merge key by key, while keyframe lists are replaced wholesale. Send the whole keyframe list when you mean to replace it. -- Shape and null clips have no media entry. Do not register media for them. +- A clip or track the user has locked refuses ordinary ops; `force` is for the change the user + asked for. - A project can hold several sequences that share its media and comps. Every op works on the open - one; use `inSequence` to work on another without changing what the user sees. - -## Versions - -`fablecut_versions` keeps saved copies of a project. - -| Action | Arguments | Effect | -| --- | --- | --- | -| `list` | `project?` | list the saved versions (default action) | -| `save` | `name?`, `note?`, `final?` | keep the project as it is now | -| `diff` | `version`, `to?` | what changed from that version to now, or to another version | -| `restore` | `version` | put that version back; the current state is saved first, so a restore can be undone | -| `update` | `version`, `name?`, `note?`, `final?` | rename, annotate, or mark the final cut | -| `delete` | `version` | remove a version | - -Versions are also made on their own: every ten minutes while the project changes, and before an -agent's first edit of a session or a whole-document rewrite. Save one before a large or risky -change so the user has a named point to go back to, and one when they are happy with a cut. -Restoring changes what the user sees, so restore only when they asked. + one, and `inSequence` reaches another while the user keeps seeing the open one. \ No newline at end of file diff --git a/plugins/klarkxy/cutline/skills/cutline/references/export.md b/plugins/klarkxy/cutline/skills/cutline/references/export.md index 41c4d14..3a4f76e 100644 --- a/plugins/klarkxy/cutline/skills/cutline/references/export.md +++ b/plugins/klarkxy/cutline/skills/cutline/references/export.md @@ -3,321 +3,62 @@ Adapted from the FableCut project by ronak-create (MIT License, Copyright (c) 2026 ronak-create). Keep that notice with any copy of this text. -There are **two** ways to render, and they are not the same thing. +The delivery presets, the encoding fields and the argument list live in the runtime manual, +[CLAUDE.md](../../../miniapp/node/vendor/fablecut/CLAUDE.md), and in the tool schema. Read them +with `fablecut_docs` before you build a call. This guide covers the two render paths and what each +one needs. -| | `fablecut_export` | `cutline_export` | -| --- | --- | --- | -| What renders | the open editor tab in the Mini App panel | a standalone runner you start yourself | -| Who starts it | the tool, on the editor's own job slot | you, in one of your background shell tasks | -| Where it runs | inside the Mini App process | a plain Node process outside the Mini App | -| Needs the Mini App open | yes | no | -| Batch outputs | yes (`outputs`) | no - one file per render | -| Job id | `x_…` | `job_…` | +## Two ways to render -`fablecut_export` renders the timeline to a video file. It is the editor's own Fast export - the -same compositor, audio mix and encoding profile as the Export button - so the file matches what the -user sees. It needs ffmpeg on PATH. +`fablecut_export` renders in the editor tab the user has open, so the Mini App has to be open and +the user watches the progress bar. It can also produce several files from one render. -The user can also export from the editor's Export dialog. The tool is a convenience, not a gate: -whatever produces the file, the file belongs to the user. +`cutline_export` prepares a job - a snapshot of the project, copies of its media, a copy of the +editor and a copy of a standalone runner - and hands you the path of that runner. You start the +runner yourself in the foreground of one of your background shell tasks. -## Where it runs +Pick the editor path when the user is watching the panel or wants several files at once. Pick the +standalone path when the export should run without the panel, run for a long time, or be +reproducible from a snapshot. -The export job renders in a browser tab on the editor. `where` decides which, and in this Mini App -there is only one that works: +## Rendering in the editor -| `where` | Behaviour in this Mini App | -| --- | --- | -| `auto` (default) | uses an open editor tab. With no tab open the call fails - there is no headless fallback here | -| `tab` | only an open tab; fails when none is open | -| `headless` | not available here | +Open the Mini App first, so the user has the editor tab. This installation serves the editor on the +Host's guarded origin, so a render has to happen in that tab and the `where` argument has no +headless option here. A call that fails for want of a tab wants the Mini App open. -The reason is the Host's port guard. The editor answers on the Host's authenticated loopback port, -and a browser spawned outside that origin gets a 401 and never loads the page, so an external -browser cannot be the compositor. `where:"headless"` is refused with that explanation, and `auto` -without an open tab fails the same way. +An export holds the editor's single job slot, so nothing else renders at the same time. For a long +export, ask for the job id and poll it instead of holding the call open, and cancel it when the +user changes their mind. -Never follow upstream advice that assumes a headless browser. Upstream's documentation talks -about starting one, about headless timings and about headless timeouts; none of that applies -here, and passing a timeout argument will not make a headless render happen. When a call fails -for want of a tab, reopen or check the editor tab, then retry with `where:"tab"` or -`where:"auto"`. +Ranges follow the project's in and out points when they are set, and the whole timeline otherwise. +Ask for the entire timeline when the user wants the markers kept for later splits and trims. -Open the Mini App first so the user has the editor tab, then export. `where:"auto"` then uses that -tab, and the user sees the progress bar. +## The standalone runner -The editor has one job slot. An export, a scope reading, a tracking job and a composed-frame -render all take it, so while one runs the others are refused. Wait for it, or stop it with -`{cancel:"x_..."}`. The same tab requirement applies to `fablecut_scopes`, `fablecut_track` and -the timeline mode of `fablecut_frame`. +1. Prepare the job. The reply names the job folder, the runner and the exact command. +2. Run that command in the foreground of one of your background tasks, where the task owns it and + its output shows what happened. Keep it attached: a detached start hands the render to a process + you can no longer watch. +3. Poll the job's status while it runs. It carries the progress, the attempt id, the runner's pid + and its last heartbeat. +4. When the status says it succeeded, the file is in the job folder's exports directory and the + status gives its absolute path. Copy it out where the user wants it. +5. Clean up the job folder yourself once the file is copied out - it holds the snapshot, the media + copies and the video, so it costs disk space. -## The call +The runner lives and dies with the background task that started it, so report it that way. It +renders the snapshot it was given, so an edit made after the job was prepared stays out of the +file. One job runs at a time; prepare a new job for a second render. A file that fails its checks +stays in the job folder's provisional directory, where it can still be looked at. -```text -fablecut_export {preset:"youtube", delivery:{fps:60}, wait:true} -``` +The runner needs ffmpeg, ffprobe and a Chrome or Edge install, and it has no network access, so a +font that exists only online, a clip that needs a downloaded model and an unknown preset id are all +refused up front. Built-in and saved presets are both accepted. -| Argument | Meaning | -| --- | --- | -| `preset` | a delivery preset id, or a saved preset's id | -| `delivery` | settings layered over the preset (see below) | -| `profile` | a raw encoding profile id instead of a preset | -| `outputs` | `true` for the project's saved batch list, or a list of preset ids and specs | -| `range` | `"entire"` or `"in-out"`; defaults to in-out when either inPoint or outPoint is set | -| `where` | `"auto"`, `"tab"` or `"headless"` - `"headless"` is refused in this Mini App, so use `"auto"` or `"tab"` | -| `wait` | block until the file is written (default true) | -| `timeout` | seconds to wait before returning the job's status instead (default 900); the export keeps running | -| `job` | report on an export started earlier | -| `cancel` | cancel an export job | - -With neither `preset` nor `profile`, the export uses `project.delivery` when set, else -`project.encodeProfile`, else the server's default profile. - -Long edits: pass `wait:false` to get a job id at once, then poll with `{job:"x_..."}`. Stop one with -`{cancel:"x_..."}`. - -## Range - -Without an explicit `range`, the export covers the project's inPoint to outPoint when either is -set, and the whole timeline otherwise. The rules are: - -1. Entire timeline, or no markers - the whole timeline. -2. IN-OUT with only IN - from the inPoint to the end. -3. IN-OUT with only OUT - from the start to the outPoint. -4. IN-OUT with both - the range between them. - -The effective bounds are clamped to the project duration, so an inPoint past the last clip cannot -produce a one-frame black file. Pick `"entire"` to keep the markers for later splits and trims. - -## Delivery presets - -| Preset | Output | -| --- | --- | -| `youtube` | 1920x1080, H.264 CRF 18, keyframe every 0.5 s, AAC 384k 48 kHz | -| `youtube-shorts` | 1080x1920, as YouTube | -| `instagram-reels` | 1080x1920, H.264 CRF 19 capped at 25 Mb/s, AAC 256k, at most 60 fps | -| `tiktok` | 1080x1920, H.264 CRF 19 capped at 20 Mb/s, AAC 256k | -| `x` | 1920x1080, H.264 CRF 20 capped at 25 Mb/s, AAC 192k | -| `linkedin` | 1920x1080, H.264 CRF 20 capped at 20 Mb/s, AAC 192k | -| `master` (default) | the picture's own size, H.264 CRF 16, AAC 320k | -| `broadcast` | 1920x1080 ProRes 422 HQ MOV, PCM 24-bit 48 kHz, Rec. 709, normalised to -23 LUFS / -1 dBTP | -| `web` | 1280x720 H.264 CRF 23, AAC 128k, fast start | -| `archive` | the picture's own size, lossless FFV1 4:2:2 10-bit MKV, PCM 24-bit | -| `custom` | your own settings | - -`fablecut_encode_profiles` lists the presets and the saved ones, and returns one preset's full spec -with `{preset:"youtube"}`. It also lists the raw encoding profiles from `encoding-profiles.json` - -complete ffmpeg argument lists - with `{profile:"hq"}`, and `detail:true` includes every args -array. The encoders your ffmpeg can actually use are listed; hardware ones only when they work on -this machine. Both are dry-run against ffmpeg when an export starts, so a bad setting is refused -before anything renders. - -## The delivery spec - -Every key is optional; whatever you leave out comes from the preset. - -```jsonc -{ - "preset": "youtube", - "width": 1920, "height": 1080, // null = the picture's own size (export frame, else canvas) - "fps": null, // null = the project's; 23.976 / 29.97 / 59.94 are exact NTSC rates - "reframe": "fill", // fill crops the middle (or the export frame), fit pads, stretch - "container": "mp4", // mp4 | mov | mkv | webm | mxf - "video": { - "codec": "h264", // h264 | hevc | av1 | vp9 | prores | dnxhr | mpeg2 | ffv1 - "encoder": "libx264", // or h264_nvenc / h264_qsv / h264_amf, listed only when they work here - "rate": "crf", // crf | vbr | cbr - "crf": 18, "bitrate": 12000, "maxrate": null, // kb/s - "keyint": 2, // seconds between keyframes, 0 = the encoder's default - "speed": "balanced", // fast | balanced | best - "pixFmt": "yuv420p", // 4:2:0 / 4:2:2 / 4:4:4, 8 or 10 bit, as the codec allows - "profile": null // ProRes: proxy lt standard hq 4444 4444xq; DNxHR: lb sq hq hqx 444 - }, - "audio": { - "codec": "aac", "bitrate": 320, "sampleRate": 48000, "channels": 2, - "loudness": null, "truePeak": -1 - }, // codec: aac | opus | mp3 | pcm16 | pcm24 | flac | none - // loudness: a LUFS target (-14 streaming, -16 podcast, - // -23 EBU R128, -24 ATSC) or null for the mix as it is - "color": "bt709", // bt709 | bt709-full | bt601-625 | bt601-525 - "filename": "{project}_{preset}_{res}", - // filename tokens: {project} {preset} {res} {w} {h} {fps} {codec} {date} {time} {range} {n} - "metadata": { "title": "", "artist": "", "description": "", "copyright": "", "date": "", "keywords": "" } -} -``` - -- Impossible combinations are corrected and the reply says how: ProRes moves to MOV, WebM takes - Opus, and Quick Sync gets 4:2:0. -- Reframing happens in ffmpeg, which receives the whole canvas and crops the export frame - or the - middle - for another shape. A 16:9 timeline exported to a vertical preset is cropped from the - middle unless the project has an export frame. Say so, or set one. -- `audio.loudness` normalises the mix in the export. It does not change the clips; use - `fablecut_normalize_audio` when you want the project itself at a target level. - -## Several files at once - -`outputs` takes `true` for the project's saved batch list, or a list of preset ids and specs: - -```text -fablecut_export {outputs:[ - {preset:"master"}, - {preset:"youtube"}, - {preset:"custom", width:1080, height:1080, filename:"{project}_square_{res}"}, - {preset:"web", filename:"{project}_preview_{res}"} -]} -``` - -The list may hold up to 16 entries, and 16 is also how many a project may save with -`setProject {outputs:[...]}`. But a single render carries at most **8** outputs. The list is split -into renders by frame rate, so the cap is per frame rate: nine outputs that all share one fps do -not fit in one render and are refused with `at most 8 outputs per render`. Spread them over -different frame rates, or export in more than one call. - -Within one render the browser renders each frame once and every output is encoded from those -frames by its own ffmpeg pass. Sharing a render saves the rendering, not the encoding - each -output still costs a full encode. - -## Check the reply +## Check the finished file Every finished file is checked against what was asked - size, fps, frames, codecs, length, -loudness, true peak, file size - and the reply lists the checks. Read it and report any warning -line to the user. A file that was produced but failed a check is not the same as a file that -matches the request. - ---- - -# The standalone export: `cutline_export` - -`cutline_export` is this package's own tool. It does not render. It **prepares** a job - a -snapshot of the saved project, copies of the media and library files that snapshot names, a copy of -the editor and a copy of a standalone runner - and hands you the exact path of that runner. You then -start the runner yourself, with your own background shell tool, and watch the job with `status`. - -The render it produces is the editor's own Fast export: the same compositor, the same audio mix and -the same ffmpeg encoding, driven by a headless Chrome or Edge on a loopback port of its own. - -## The three actions - -| Action | What it does | -| --- | --- | -| `prepare` | builds a job and returns the exact command that starts it | -| `status` | reads the job's progress, its result and its error | -| `cancel` | asks a running job to stop | - -There is no `start` action. Starting the render is your job, in a process you own. - -## The call sequence - -```text -cutline_export {action:"prepare", range:"entire", preset:"web"} -``` - -The reply names the job folder, the runner and the exact command: - -```text -Export job job_1a2b3c4d5e6f7081 is prepared and has not started. - -Start it with your own background shell tool, in the foreground of that task: - - node "C:/…/export-jobs/job_1a2b3c4d5e6f7081/runner/runner.mjs" --job "C:/…/export-jobs/job_1a2b3c4d5e6f7081" -``` - -Then: - -1. Run that command in the foreground of one of your background tasks. Do not start it detached, do - not background it with `&`, and do not use `nohup` or a service manager: the task you started it - from is what owns it, and the task's output is how you see what happened. -2. Poll `cutline_export {action:"status", job:"job_…"}` while it runs. The status carries the - progress, the attempt id, the runner's pid and the time of its last heartbeat. -3. When the status says `succeeded`, the file is in the job folder's `exports` directory; the status - gives its absolute path. Copy it out where the user wants it. -4. To stop it: `cutline_export {action:"cancel", job:"job_…"}`, or stop your own task. - -**The job folder is not cleaned up for you.** It holds the project snapshot, the copies of the media -and library files, the renderer and the video, so it costs disk space of its own. Once the file has -been copied out, the job folder can be deleted. A file that was made but failed its checks stays in -the job folder's `provisional` directory, so it can still be looked at; nothing removes it. - -**Use the paths the tool gives you.** They are absolute and they are the authority. Never guess an -installed path, never build a path from the project's name, and never look for the runner somewhere -else on disk. - -## What the runner needs - -- `node` on PATH - it is the same Node the Mini App runs on. -- `ffmpeg` and `ffprobe` on PATH. -- A Chrome or Edge install. The runner finds the usual install paths; `FABLECUT_CHROME` points it - at another one. If none is found the job fails with that message - it does not download a browser. - -## What it refuses, and why - -The renderer has no network access, so anything that would be fetched from the internet is refused -rather than quietly left out: - -- **A font that is not on the machine** fails the job with the name of the font. Use a font from the - library, a system font, or `fablecut_export` in the editor. -- **A clip with background removal** is refused when the job is prepared, because the model it - needs would have to be downloaded. -- **A media or library file that is missing, points outside the project's own folders, or has an - unsupported non-media type**, is refused when the job is prepared. `/media/` accepts only video, - audio, image and SVG files; fonts belong under `/library/`, not in the media folder. -- **An unknown delivery preset id** is refused before a job folder is created, whether it is passed - as `preset` or `delivery.preset`; there is no silent fallback to Custom. Built-in and saved - delivery presets are both accepted. - -## The exit code - -The runner ends with `0` when the file was made, `4` when the job failed, `130` when it was -cancelled or interrupted, and `3` when it refused to start (the job was already held by another -attempt, or it had already finished). Read the state with `status` for the details. - -## What the runner does not do - -- It does not read the live project. It renders the snapshot it was given, so an edit made after - `prepare` is not in the file. -- It does not talk to the Host. It opens its own loopback port with its own secret and answers - nothing else. -- It does not write outside its own job folder. The finished file lands in that folder's `exports` - directory. -- It does not run Agent turns and it does not need the Mini App to stay open. It is a plain Node - process, started by you. - -## The boundaries, stated plainly - -- The runner is **not** a child of the Mini App. Stopping the Mini App does not stop a render that - has already been accepted. -- The runner is **not** a detached daemon. It lives and dies with the background task that started - it. If that task is stopped, or the client exits, the render is gone with it. Do not promise the - user that a render survives the app closing. -- One job runs at a time. A second attempt on the same job is refused; the state and the output of - the first attempt are left alone. Render again by preparing a new job. -- A job that was interrupted - the machine was rebooted, the task was killed - stays interrupted. - It is not restarted for you, and nothing is killed by a recorded pid: a recycled pid is not - evidence of anything. Prepare a new job. -- The file is written as provisional and is only marked finished after `ffprobe` has read it and - `ffmpeg` has decoded it all the way through. A file that does not survive that is reported as a - failure, not as a finished export - and it is kept in the job folder's `provisional` directory - rather than deleted, so it can still be looked at. -- Heartbeat failures, cancellation and render failure share one idempotent cleanup path. Browser - close has a short bound before the process is killed, and a separate 15-second teardown deadline - bounds cleanup even when the overall render timeout started that cleanup. - -## Limits - -- **One file per render.** `cutline_export` has no `outputs` argument: batch outputs are a - `fablecut_export` feature. For several files, run several jobs, or use `fablecut_export` with the - editor open. -- `range`, `preset`, `delivery` and `profile` are honoured the same way `fablecut_export` honours - them, because it is the same renderer. Settings that the renderer has to correct are corrected the - same way and said so in the reply. Unknown preset ids are the exception: standalone preparation - refuses them instead of allowing the renderer's Custom fallback. -- The renderer is the editor's compositor in a headless browser. Long or high-resolution timelines - are slower than a short one, and a browser's memory limits still apply. A successful short render - does not certify long-form rendering. - -## Which one to use - -- The user is watching the editor, or wants several files at once, or wants the progress bar in the - panel: use `fablecut_export` with the editor open. -- The user wants the export to run without the panel, or to run for a long time, or to be - reproducible from a snapshot: use `cutline_export`. +loudness, true peak, file size - and the reply lists the checks. Read it and report any warning to +the user. A file that was produced but failed a check is a near miss rather than a match, so report +it as such. diff --git a/plugins/klarkxy/cutline/skills/cutline/references/motion-graphics.md b/plugins/klarkxy/cutline/skills/cutline/references/motion-graphics.md index 5a59ab0..6068155 100644 --- a/plugins/klarkxy/cutline/skills/cutline/references/motion-graphics.md +++ b/plugins/klarkxy/cutline/skills/cutline/references/motion-graphics.md @@ -3,229 +3,74 @@ Adapted from the FableCut project by ronak-create (MIT License, Copyright (c) 2026 ronak-create). Keep that notice with any copy of this text. -Motion graphics in Cutline are ordinary clips on the timeline, so everything you know about clips -still applies - tracks, keyframes, transitions, masks, blend. Three pieces do the work: - -- **Shape layer** - a clip with `kind:"shape"` that draws vector items from `props.shapes`. It - stays sharp at any scale and needs no media. -- **Null** - a clip with `kind:"null"` that never renders. It exists to be a parent. -- **Parent** - `parent:""` makes a clip move, turn and scale with another one. Every - visual clip also has an **anchor point** (`anchorX`, `anchorY`) that it turns and scales around. - -## Coordinates - -Get these right first: - -- A clip's `x` / `y` are px from the **canvas centre**. With a parent they are px from the - **parent's centre**, in the parent's turned and scaled space. -- A shape item's `x` / `y` are px from the **layer's origin** (the layer clip's `x` / `y`). Path - and line `points` are px around the item's own `x` / `y`. -- `rotation` is in degrees, clockwise, and y points down. -- An item's `keys[].t` and a clip's `keyframes[].t` are seconds from the **clip's start**, not from - the timeline. - -## Building one - -1. `fablecut_get_project {compact:true}` to see the tracks and what is free. Shapes usually go on - `V2` or `V3`, above the footage. -2. Add the layer with its items in one `addClip`, or add an empty layer and set items into it with - `setShape`. Name every item you will touch again (`name:"Bar"`), so later edits say - `shape:"Bar"` and not an index. -3. Animate the items with `keys`, and the whole layer with the clip's `keyframes` (`x`, `y`, - `scale`, `rotation`, `opacity`, `trimEnd`, and so on). -4. Group what moves as one: put shapes in a `group` item inside a layer, or parent whole clips - - titles, images, other layers - to a null with `setParent`. -5. Check it: the compact view shows each layer as `[3 shapes: rect #ffd166 2 keys, ...]` and each - child as `parent:c_...`. - -```text -fablecut_patch_project {ops:[ - {op:"addClip", clip:{id:"c_lt", kind:"shape", track:"V2", start:1, duration:5, - props:{x:-330, y:230, shapes:[ - {type:"rect", name:"Bar", x:0, w:560, h:96, radius:10, fill:"#111827e6", - keys:[{t:0, w:0}, {t:0.5, w:560, ease:"ease-out"}]}, - {type:"rect", name:"Accent", x:-276, w:8, h:96, fill:"#ffd166"}, - {type:"line", name:"Rule", y:64, points:[[-280,0],[280,0]], stroke:"#ffd166", strokeWidth:4, - keys:[{t:0.3, trimEnd:0}, {t:1, trimEnd:100, ease:"ease-out"}]}]}, - transitionOut:{type:"fade", duration:0.4}}}, - {op:"addClip", clip:{id:"c_name", kind:"text", track:"V3", start:1.2, duration:4.8, - props:{text:"Your Name", font:"Archivo Black", fontSize:44, align:"left", x:-560, y:-8, textAnim:"rise-mask"}}}, - {op:"setParent", id:"c_name", parent:"c_lt"} -]} -``` - -`setParent` keeps the title where it is on screen and rewrites its `x` / `y` into the layer's -space. After that, keyframing the layer's `x` slides both of them out together. - -## The shape op - -`setShape {id | ids, shape?, set:{...}, replace?, remove?}` - `shape` is an index, a name, or a -path into a group such as `"2.0"` for item 0 of group 2. Leaving it out, or giving an unknown name -or an index equal to the current length, adds an item; a new item needs a `type`. - -Set keys: `type` (`rect`, `ellipse`, `polygon`, `star`, `line`, `path`, `group`), `name`, `on`, -`x`, `y`, `rotation`, `scale`, `opacity`, `w`, `h`, `radius` (rect corners), `sides`, `inner` -(star, 0.05 to 1), `points` (`[[x,y,inX?,inY?,outX?,outY?], ...]`; a line has exactly 2 points, a -path 2 to 128, around the item's `x` / `y`), `closed` (path), `fill`, `stroke` (`#rrggbb`, -`#rrggbbaa` or `none`), `strokeWidth`, `lineCap`, `lineJoin`, `dash`, `trimStart`, `trimEnd` -(0 to 100, a draw-on), `trimOffset`, `items` (group), and `keys` (`[{t, ease?, ...}]` over x, y, -rotation, scale, opacity, w, h, radius, inner, strokeWidth, trimStart, trimEnd, trimOffset, fill, -stroke, points). - -`removeShape {id, shape}` removes one. +The item properties, the op names, the preset lists and the worked recipes live in the runtime +manual, [CLAUDE.md](../../../miniapp/node/vendor/fablecut/CLAUDE.md), which `fablecut_docs` serves +section by section. Read the section you need before you build a call. This guide covers how to +design and place a graphic. + +## The three pieces + +- **Shape layer** - a clip that draws vector items, so it stays sharp at any scale and runs without + media. +- **Null** - a clip that stays invisible on screen. It exists to be a parent. +- **Parent** - a clip can move, turn and scale with another one. Every visual clip also has an + anchor point that it turns and scales around. + +Everything you know about clips still applies to these: tracks, keyframes, transitions, masks and +blend. + +## Get the coordinates right first + +- A clip's position is measured from the canvas centre; with a parent it is measured from the + parent's centre, in the parent's turned and scaled space. A shape item's position is measured + from its layer's origin, and its points from its own position. +- Rotation is in degrees and clockwise, and y points down. +- Keyframe times run from the clip's start, so a layer's animation travels with the layer wherever + it sits on the timeline. + +## Build one + +1. Read the compact project and see which tracks are free. Shapes usually belong on `V2` or `V3`, + above the footage. +2. Add the layer with its items, and name every item you will touch again, so a later edit can + address it by name. +3. Animate the items with their own keys, and the whole layer with the clip's keyframes. +4. Group what moves as one: shapes in a group item, or whole clips - titles, images, other layers - + parented to a null. +5. Check the compact project: it shows each layer's items and each child's parent. + +The manual's shapes section carries the worked recipes - draw-ons, a loading ring, a progress bar, +an orbit, a swing - with the properties each one needs. ## Timing that looks designed -- Entrances take 0.3 to 0.6 s with `ease:"ease-out"` on the arriving key. Exits are faster, 0.2 to - 0.4 s, with `ease-in`. Use `linear` only for things that keep turning or running - a spinner, a - ticker, a `trimOffset` loop. -- Stagger related items by 0.08 to 0.15 s so they settle one after another instead of all at once. -- Overshoot comes from three keys, not from an ease: `scale` 0, then 1.12, then 1, over about - 0.35 s. -- Hold a finished graphic for at least two seconds before it leaves. -- Use two or three colours: one strong accent, one neutral (`#111827`, `#ffffff`), and a - translucent backing (`#rrggbbaa`, such as `#000000a0`). - -## Recipes - -- **Draw-on logo or icon** - `path` items with `stroke` and `fill:"none"`, each with - `keys:[{t:a, trimEnd:0}, {t:a+0.8, trimEnd:100}]`, staggered. Then fade the fill in with - `keys:[{t:1.2, fill:"#ffffff00"}, {t:1.6, fill:"#ffffff"}]`. The `fill` must be keyed in the - same item as the draw-on. -- **Whole-layer draw-on** - key the clip's own `trimEnd` from 0 to 100 in its `keyframes`. It trims - every path in the layer at once. -- **Loading ring** - an `ellipse` with `w` and `h`, `fill:"none"`, a `stroke`, `strokeWidth`, and - `trimEnd:25`, with `keys:[{t:0, trimOffset:0}, {t:1, trimOffset:100, ease:"linear"}]`. For more - turns, add more keys - `trimOffset` 200 at t 2, and so on. -- **Progress bar** - a backing `rect` and a fill `rect` with - `keys:[{t:0, w:0, x:-200}, {t:3, w:400, x:0, ease:"linear"}]`. Key `x` together with `w` so the - bar grows from its left edge. -- **Orbit** - a null at the centre with `rotation` keyed from 0 to 360 (`linear`), and a shape or - image parented to it at `x:300`. A second null parented to the first gives a moon. -- **Swing or hinge** - put the anchor on the edge it should hang from (`anchorY: -h/2` for the top - edge), then key `rotation` between -8 and 8. -- **Pop-in sticker** - a `star` or `ellipse` with - `keys:[{t:0, scale:0}, {t:0.25, scale:1.15}, {t:0.4, scale:1}]` and `rotation` from -15 to 0. -- **Callout** - an open `path` (`closed:false`) from the subject to a label, drawn on with - `trimEnd`, plus a small `ellipse` at its start. -- **Burst** - a `group` of six `line` items with `rotation` 0, 60, 120 and so on, and - `points:[[40,0],[90,0]]`. Key the group's `scale` from 0.4 to 1.2 and its `opacity` from 1 to 0 - over half a second. - -## Curves, blur and text on a path - -- **Curved moves** - key `x` and `y` together at two or more times, then call - `{op:"setMotionPath", id, path:"smooth"}`. For a precise arc, write the handles yourself: `pout` - on the leaving key and `pin` on the arriving key, on both the x and y keys, in px from the key's - position. A fly-in that arches up: x `{t:0, v:-500, pout:200}` then `{t:1, v:0, pin:-200}`, y - `{t:0, v:200, pout:-250}` then `{t:1, v:0, pin:0}`. -- **Auto-orient** - `autoOrient:true`, or `setMotionPath` with `autoOrient:true`, makes arrows, - planes and cars point where they go. Draw them pointing right, 0 degrees, the direction of - travel. -- **Motion blur** - `motionBlur` is a shutter angle from 0 to 720 (180 is a film look) and - `motionBlurSamples` runs from 2 to 32. Use it on anything that moves fast - whips, fly-ins, - spinning logos. Leave it off on slow drifts, where it only costs render time. -- **Text on a path** - `{op:"setTextPath", id, path:"circle"}`, or `"arc"`, `"wave"`, `"line"`, or a - shape item's geometry. Key `pathOffset` from 0 to 100 with `ease:"linear"` to send text around a - ring. Add a trailing separator so the ends do not touch, and size the path to the text: a - circle's circumference should be a little longer than the text. `pathAlign:"center"` with - `pathOffset:50` centres a title on an arc. Later lines follow on parallel curves one line-height - below. Every `textAnim` works on a path. - -## Compositions - -A **comp** is a timeline of its own; a `kind:"comp"` clip places it, drawn like one picture of the -comp's size. Use one when the same graphic appears more than once, or when several clips should -move, fade, mask or grade as a unit. - -```text -fablecut_patch_project {ops:[ - {op:"addComp", comp:{id:"sting", name:"Logo sting", duration:2.5, clips:[ - {id:"ring", kind:"shape", track:"V1", start:0, duration:2.5, props:{shapes:[ - {type:"ellipse", w:220, h:220, fill:"none", stroke:"#ffd166", strokeWidth:10, - keys:[{t:0, trimEnd:0}, {t:0.8, trimEnd:100, ease:"ease-out"}]}]}}, - {id:"word", kind:"text", track:"V2", start:0.5, duration:2, props:{text:"FC", font:"Anton", fontSize:96, textAnim:"zoom-in"}}]}}, - {op:"addClip", clip:{kind:"comp", compId:"sting", track:"V3", start:0, duration:2.5}}, - {op:"addClip", clip:{kind:"comp", compId:"sting", track:"V3", start:58, duration:2.5, props:{scale:0.6, x:480, y:260}}} -]} -``` - -- Edit what is inside with `{op:"inComp", comp:"sting", ops:[...]}` - the same clip ops, with the - comp's own times. Every placement updates. -- `{op:"precompose", ids:[...]}` turns clips you already built into a comp and leaves one comp clip - in their place; then animate, mask or fade that clip as a whole. This is the fix for "parenting - does not pass opacity". -- A comp clip's `in` is where in the comp it starts. A comp clip shows nothing past the comp's - `duration`, so lengthen the comp with `updateComp` before stretching the clip. -- The comp's frame is its `width` times `height`, transparent unless it has a `background`. Give a - graphic's comp the canvas size so `x` / `y` inside it read the same as on the timeline. -- Sound inside a comp plays through the main timeline's track of the same name, so a whoosh in a - sting follows the fader it is on. - -## Presets and templates - -Reach for these before keying by hand. - -```text -fablecut_patch_project {ops:[ - {op:"addTemplate", template:"lower-third-bar", id:"c_lt_1", track:"V3", start:4, - fields:{name:"A Name", title:"A Role", accent:"#22d3ee"}}, - {op:"addTemplate", template:"logo-ring", track:"V3", start:0, fields:{logo:"m_logo", brand:"BRAND"}}, - {op:"applyPreset", ids:["c_title1", "c_title2"], preset:"rise-blur"}, - {op:"applyPreset", ids:["c_title1", "c_title2"], preset:"fade-out"}, - {op:"applyPreset", id:"c_badge", preset:"pulse", at:12.5} -]} -``` - -**Templates** - `addTemplate {template, start?, track?, duration?, fields?, id?, compId?, name?}`: - -- lower thirds: `lower-third-bar`, `lower-third-underline`, `lower-third-boxes` - fields `name`, - `title`, `accent`. -- logos: `logo-ring`, `logo-slam`, `logo-lines` - field `logo` takes a media id, so import the - logo first; fields `brand`, `tagline`, `accent`. -- social: `social-story` (1080x1920), `social-quote` (1080x1080), `social-end-card` - text fields, - `accent`, `background`. - -Each placement is its own comp, so one per speaker is right. Change it later with -`{op:"setTemplate", id, fields:{...}, duration:7}`; a longer template holds longer and keeps its -exit. For a vertical template, set the canvas to its size first with -`setProject {width:1080, height:1920}`, or it sits letterboxed in a 16:9 frame. - -**Presets** - `applyPreset {id | ids, preset, length?, at?}` keys a named animation onto visual -clips, relative to where they sit: - -- in: `fade-in`, `pop-in`, `slide-up-in`, `slide-down-in`, `slide-left-in`, `slide-right-in`, - `drop-in`, `zoom-in`, `spin-in`, `whip-in`, `rise-blur`, `draw-on` -- out: `fade-out`, `pop-out`, `slide-up-out`, `slide-down-out`, `slide-left-out`, - `slide-right-out`, `zoom-out`, `spin-out`, `whip-out`, `sink-blur`, `draw-off` -- at, an emphasis at a moment: `pulse`, `shake`, `wobble`, `bounce`, `flash` -- loop, the whole clip: `float`, `breathe`, `sway`, `spin`, `wiggle` - -`length` stretches one. An inline preset is `{anchor:'in'|'out'|'at'|'whole', length, keys:{channel:[{t,v,ease?}]}}`, -where `scale` and `opacity` multiply and the rest add. - -Put the clip where it should rest first, then apply the preset. Use one family of entrances -across an edit rather than a different preset on every clip. Save what the user likes with -`savePreset {id, name, anchor?}` from a clip they animated, and `saveTemplate {comp, name?, -fields?}` from a comp they built; both go to the library and work in every project. - -## Traps - -- Shape and null clips have no media entry. Do not register media for them. -- `keys` in `setShape` replaces that item's whole key list, so send all of them. `points` keys must - have the same number of points as the path. -- A stroke with `strokeWidth:0` is invisible. Setting `stroke` without a width gives 6 px, and 8 px - on lines and open paths. -- A trimmed closed shape fills only the part that is drawn. For a clean outline draw-on, use - `fill:"none"` and fade the fill in afterwards. -- Parenting carries only the transform. Opacity, transitions, shake and effects do not pass to the - children, so fade each child with its own `transitionOut`, or put everything in one shape layer's - groups. -- Rotate or scale around a corner by moving the anchor, not by adjusting `x` / `y` keys in every - frame. -- Parent loops are refused, and so are parents that are audio or adjustment clips. -- A comp cannot contain itself, directly or through another comp, and `removeComp` is refused while - a clip still uses it. Comps nest four deep. -- `props.blend` accepts `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, - `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, - `saturation`, `color`, `luminosity` and `lighter`. +- Entrances run 0.3 to 0.6 s and ease out; exits are faster and ease in. Reserve linear motion for + things that keep turning or running. +- Stagger related items so they settle one after another. +- Overshoot comes from three keys - a scale slightly past its target and back - rather than from a + stronger ease. +- Hold a finished graphic for a couple of seconds before it leaves. +- Two or three colours are enough: one accent, one neutral, one translucent backing. + +## Comps, presets and templates + +- A **comp** is a timeline of its own, placed as one picture. Use one when the same graphic appears + more than once, or when several clips should move, fade, mask or grade as a unit. Turning clips + you already built into a comp lets you animate them as a whole. +- **Templates** place a finished design - a lower third, a logo, a social card - with fields you + fill in. Each placement is its own comp, so one per speaker is right. +- **Presets** key a named animation onto a clip. Put the clip where it should rest first, then + apply the preset, and use one family of entrances across an edit rather than a different preset + on every clip. +- The editor's library holds the templates and presets, and you can save what the user liked so it + is available in every project. + +## Cautions + +- Shape and null clips carry no media entry, so leave them out of the media bin. +- Parenting carries the transform only. Opacity, transitions and effects stay with each child, so + fade every child or put the whole graphic in one layer. Rotate or scale around a corner by moving + the anchor. +- A stroke needs a width to be visible, and a trimmed closed shape fills only the part that is + drawn. +- Parent loops are refused, a comp cannot contain itself, and comps nest a few levels deep. diff --git a/plugins/klarkxy/cutline/tests/mcp-tool-payload.test.mjs b/plugins/klarkxy/cutline/tests/mcp-tool-payload.test.mjs new file mode 100644 index 0000000..7874c29 --- /dev/null +++ b/plugins/klarkxy/cutline/tests/mcp-tool-payload.test.mjs @@ -0,0 +1,140 @@ +// @ts-check +// The size of the tool list the Host injects into every session. +// +// The Host puts the whole `tools/list` result into the model's context, so +// the tool descriptions are a standing cost on every conversation. This test +// measures the real list — nineteen upstream `fablecut_*` tools, the package's +// project-folder import tool, and its own `cutline_export` — and keeps it inside +// a budget, so a description that grows back into a manual is caught here. +// +// The budget is a byte count, not a token count: no tokenizer is involved, so +// the number does not drift with the model. + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { mcpRaw, mcpSession, startApp } from './helpers.mjs'; +import { EXPORT_TOOL } from '../miniapp/node/export/export-mcp.mjs'; + +/** The largest the injected tool list may be. */ +const BUDGET_BYTES = 16_000; + +/** Nineteen upstream tools, the local project import, and cutline_export. */ +const UPSTREAM_TOOL_COUNT = 19; +const EXPECTED_TOOL_COUNT = 21; + +const app = await startApp(); + +test.after(async () => { + await app.dispose(); +}); + +/** + * The tool list the Host actually receives. + * + * @returns {Promise} + */ +async function listTools() { + const { sessionId } = await mcpSession(app.base); + const reply = await mcpRaw( + app.base, + { jsonrpc: '2.0', id: 2, method: 'tools/list' }, + { headers: { 'mcp-session-id': sessionId } }, + ); + assert.equal(reply.status, 200); + const tools = reply.json?.result?.tools; + assert.ok(Array.isArray(tools), 'the server answers tools/list with an array'); + return tools; +} + +test('the injected tool list stays inside its byte budget', async () => { + const tools = await listTools(); + assert.equal(tools.length, EXPECTED_TOOL_COUNT); + + const bytes = Buffer.byteLength(JSON.stringify(tools), 'utf8'); + assert.ok( + bytes <= BUDGET_BYTES, + `the tool list is ${bytes} bytes; the budget is ${BUDGET_BYTES}. Trim the tool descriptions, not the schema.`, + ); +}); + +test('the measured list is the upstream tools, project import and cutline_export', async () => { + const tools = await listTools(); + const projectImport = tools.find((/** @type {any} */ tool) => tool.name === 'fablecut_import_project'); + assert.ok(projectImport, 'the package project-import tool is offered'); + const upstream = tools.filter((/** @type {any} */ tool) => + tool.name.startsWith('fablecut_') && tool.name !== 'fablecut_import_project', + ); + assert.equal(upstream.length, UPSTREAM_TOOL_COUNT, 'all upstream tools are offered'); + + const offered = tools.find((/** @type {any} */ tool) => tool.name === EXPORT_TOOL.name); + assert.ok(offered, 'cutline_export is offered'); + assert.deepEqual(offered, EXPORT_TOOL, 'the offered export tool is the one the package defines'); +}); + +test('the descriptions that stop a destructive mistake are still there', async () => { + const tools = await listTools(); + const byName = new Map( + tools.map((/** @type {any} */ tool) => [tool.name, tool]), + ); + + // A write needs the revision it was planned against, or a concurrent change + // in the editor is overwritten instead of reported. + const setProject = byName.get('fablecut_set_project'); + assert.ok(setProject, 'fablecut_set_project is offered'); + assert.match(setProject.description, /baseRevision/); + assert.match(setProject.description, /CONFLICT/); + assert.deepEqual(setProject.inputSchema.required.sort(), ['baseRevision', 'project']); + + const patchProject = byName.get('fablecut_patch_project'); + assert.ok(patchProject, 'fablecut_patch_project is offered'); + assert.match(patchProject.description, /baseRevision/); + assert.match(patchProject.description, /CONFLICT/); + + // A render runs in the editor the user has open; a background browser + // cannot load the editor's own origin. + const exportTool = byName.get('fablecut_export'); + assert.ok(exportTool, 'fablecut_export is offered'); + assert.match(exportTool.description, /editor tab/i); + assert.deepEqual( + exportTool.inputSchema.properties.where.enum, + ['auto', 'tab', 'headless'], + 'its where enum is unchanged', + ); + + // A lock the user set is only broken on the user's own request. + const force = byName.get('fablecut_set_project').inputSchema.properties.force; + assert.match(force.description, /only on request/i); + + // The Agent starts the standalone render itself. + const cutlineExport = byName.get('cutline_export'); + assert.ok(cutlineExport, 'cutline_export is offered'); + assert.match(cutlineExport.description, /prepare/); + assert.match(cutlineExport.description, /status/); + assert.match(cutlineExport.description, /cancel/); + assert.deepEqual(cutlineExport.inputSchema.properties.action.enum, ['prepare', 'status', 'cancel']); + + const frame = byName.get('fablecut_frame'); + assert.ok(frame); + assert.match(frame.inputSchema.properties.path.description, /absolute file path/); + assert.match(frame.inputSchema.properties.path.description, /read in place/); + + const reference = byName.get('fablecut_analyze_reference'); + assert.ok(reference); + assert.match(reference.description, /external local paths must be audio\/video/); + + const importMedia = byName.get('fablecut_import_media'); + assert.ok(importMedia); + assert.match(importMedia.description, /remote SVG is refused/); + + const range = exportTool.inputSchema.properties.range.description; + assert.match(range, /when either marker is set, otherwise entire/); +}); + +test('every tool still carries a description', async () => { + const tools = await listTools(); + for (const tool of tools) { + assert.ok(tool.description, `${tool.name} has a description`); + assert.equal(tool.inputSchema?.type, 'object', `${tool.name} has an object schema`); + } +}); diff --git a/plugins/klarkxy/cutline/upstream.json b/plugins/klarkxy/cutline/upstream.json index ca5b9c3..0f9ec05 100644 --- a/plugins/klarkxy/cutline/upstream.json +++ b/plugins/klarkxy/cutline/upstream.json @@ -11,7 +11,7 @@ }, "package": { "id": "cutline", - "version": "0.1.0", + "version": "0.1.1", "note": "The package version is this Mini App integration's own; it does not track the upstream version. upstream.version records the vendored revision." }, "vendored": {