← Files PicsartARCHIVED FILE

docs/widget-specs/final-cut.md

12.8 KB · Oct 10, 2026 · 18:02 UTC

↓ Download file

See the change to this file →

# Widget spec — `picsart_final_cut`

The **Final Cut console**: the assembled cut in a real editor, in the chat. Trim,
split, crop, adjust, effects, resize, speed, volume and freeze — and the preview
is the same engine that renders the file, so what the user sees IS the export.
Saving hands back the rendered mp4 *and* the editable document behind it.

New 2026-08-27. Contract at v1.

## Why this is not the review board

The review board this console was once contrasted with is gone. Take and cut
review happen in the scene editor (`picsart_scene_editor`): it plays the scene
document in the browser, lets the user trim, split and adjust it, and hands back
the edited document with their verdict and comments. It renders no file. This
console is the one that saves a rendered mp4.

## Tool input

```ts
{
  clips?: [{             // THE CUT AS ITS SHOTS, in playback order — the preferred path
    id: string,          // '12A'
    url: string,         // Picsart-hosted https
    label?: string,      // '12A Cal turns'
    duration?: number,   // source length, seconds — pass it, see below
    width?: number, height?: number,
    trimIn?: number, trimOut?: number,   // keep-window inside the source
  }],                    // max 96
  videoUrl?: string,   // Picsart-hosted https — one already-assembled video
  replayUrl?: string,  // Picsart-hosted https to a replay JSON from a previous session
  title?: string,      // 'Cully Hill Boys — final cut'
  notes?: string,      // one line: what to look at
}
```

One of `clips` / `videoUrl` / `replayUrl` is required, and they resolve in that
order of preference reversed: **`replayUrl` wins, then `clips`, then `videoUrl`.**
A replay document is a work in progress and the mp4 beside it is a flattened
snapshot of the same thing, so reopening the editable one is always the better
answer. Pass a fallback anyway: if the replay URL has expired the widget drops to
the next source rather than failing, which costs the user their edit *history*
instead of their ability to work.

**Prefer `clips`.** Each shot becomes its own video layer laid end to end in
composition time, so the timeline shows the cut as the shots it is made of — one
cell per shot, and trim/split/move/delete act on a shot rather than on a scrub
position inside a flattened file. It also means no assembled mp4 has to be
rendered first, which would be a full `picsart_media_export` render before every
edit session. This is **not** the editor's own multi-URL source kind, which stacks
media as layers on one canvas — a collage, not a sequence; the sequence is an
authored replay document the widget builds in `toReplay.ts`.

`videoUrl` is the fallback now rather than the main path: one video is one layer,
so the timeline is one continuous strip with no shot boundaries in it. Right for a
cut that genuinely IS one clip, or for reopening a flattened master; wrong for a
film assembled from shots, where the user wants to see the shots.

**Pass `duration` (and `width`/`height`) per clip, and pass them TRUTHFULLY.**
When all three are present the widget skips probing that clip, which removes a
metadata round-trip per shot and — the part that actually matters — works in a
background tab, where a browser defers media loading and a probe never settles at
all. That alone is reason enough to pass them.

Truthfulness matters because the numbers are becoming load-bearing. `toReplay.ts`
writes `duration` into the layer's `meta.sourceDuration`, which a newer editor
reads to clamp every trim against the real source and to bound the timeline's drag
handles by the headroom it implies — so an overstated `duration` describes footage
that does not exist, and an understated one stops the user extending a shot they
trimmed too far. On the pinned `2.1.0` the field goes unread, so today this is
latent rather than visible; it becomes real the moment the §3.31 dock is adopted.
Get all three from `picsart_media_probe_media`, which is free.

## Layout

- **Topbar**: `title` (default *"Final cut"*), `notes` beneath it (default *"Trim,
  split and polish the cut. Save when it plays right."*), and a fullscreen toggle.
- **The editor**, filling a stated 680px stage — it fills its parent and a
  zero-height parent renders nothing, so the height is never inherited.
  Fullscreen takes it to `min(88vh, 1100px)`: a cut is judged on the picture, so
  the extra space goes to height rather than width.

  680px is set by the editor's fixed rows: 170px of chrome idle and 416px with
  Effects open, leaving the picture 510px and 264px. It is tied to the **66px**
  timeline slot, so adopting the §3.31 dock (280px, persistent) moves it to ~900px
  and requires a `max()` floor on the fullscreen height.
- **Save / Discard** live in the editor's own header, not ours. So does its
  **Download** button, which is worth having on: it renders the same result a save
  would and hands it straight to the browser, needing no upload — the one way to
  get the file out if the CDN upload is what fails.
- **A render/save status line** under the stage while an export runs.
- Empty state when neither URL was passed; error state when the replay could not
  be fetched and there was no mp4 to fall back to.

### The timeline — a known gap, parked 2026-08-28

**There is no multi-clip timeline yet, and this is the console's main weakness.**
What the slot holds is the §3.20 `playback` strip: it spans the whole composition
correctly, but it draws **no seams** — boundary dots feed Split and Trim only, so a
three-shot cut reads as one continuous filmstrip. That is why the widget forces
Split open at mount when the payload arrived as `clips` (below): it is the only way
to make the shots visible.

Two other surfaces were tried and rejected:

- **`timeline` (§3.12)** is "Timeline row (SINGLE VIDEO)" — bound to one layer, so
  on a cut assembled from shots it described shot *one* while the playhead ran
  through all of them (4.00s on the strip against 11.00s in the composition). It
  also carries a data-loss hazard: its single-layer trim commits drop sibling media
  layers outside the primary's kept window, so dragging those handles on a
  three-shot cut could silently delete shots two and three as one undoable edit.
  **Do not re-add this id.**
- **`advanced_timeline` (§3.31)** is the right answer and is not usable yet. It
  fills the slot with `@picsart/replay-timeline` as a persistent 280px multi-track
  dock — one draggable cell per shot with a filmstrip, trim/split/move/delete as
  clip gestures, and a base-row trim that *ripples* to close the gap it made. It
  was wired up here, verified working end to end, and reverted the same day for two
  reasons: it ships only in a prerelease off an open MR (40), and resolving it
  makes **split unreachable** — `resolveToolIds` drops the §3.19 Split panel on the
  assumption the dock provides split, while the dock's own razor gates on *screen
  pixels* (`30 / 44 / zoom` seconds of clearance per side) and so stays disabled at
  any zoom a real cut opens at. Its header also overlaps the play button with the
  per-cell volume chip.

  Three further bugs seen while testing it are **not** dock-specific and are live
  on the shipped editor too: Speed and Effects/Adjust apply to every clip rather
  than the selected one (§3.14 single-layer fan-out, plus §3.21's
  uniform-over-the-pipeline v1 write), and the Effects tiles all preview the
  unfiltered frame.

  Re-adopting is a small change once those are fixed upstream: the version range,
  one tool id, the stage height, and deleting the `initialTool` workaround.
  `meta.sourceDuration` is already authored by `toReplay.ts` and simply goes
  unread on 2.1.0.

### The tool set, and what is deliberately absent

Offered: `trim` `split` `crop` `adjust` `effects` `resize` `speed` `volume`
`freeze`.

**`initialTool: 'split'` when the payload arrived as `clips`** — a workaround, and
labelled as one. §3.20 draws seam dots in Split and Trim modes only, so forcing
Split open at mount is the only way a user sees that their cut has three shots in
it. The cost is real: the console opens in a mode nobody chose, with a "press
Escape to leave Split" hint as the first thing they read. Delete it when the §3.31
dock is adopted — the dock shows the shots at rest. Not applied for `videoUrl` or
`replayUrl`: one video has no seams, and a resumed session should reopen however
the user left it.

Absent, for two different reasons:

- **`ai_edit`, `captions`, `extend`, `ai_expand`, `remove_bg`, `upscale`** are
  `ai`-gated: they need an authenticated transport, and this tool is free and
  unauthenticated like every other board here, so the widget holds no credential.
  They would render as buttons that can never become enabled, which is worse than
  no button.
- **`transform`** is filtered by the editor itself: `single-layer` mode is the
  default and treats the whole composition as one edit target. Listing it would be
  a no-op that reads like an offer.

## Feedback → `final_cut_feedback` **v1**

```json
{
  "type": "final_cut_feedback", "version": 1,
  "verdict": "saved",
  "title": "Cully Hill Boys — final cut",
  "videoUrl": "https://…/export.mp4",
  "replayUrl": "https://…/export.json",
  "duration": 78.4,
  "stateHash": "b3f1…"
}
```

`verdict` is `saved` | `discarded`. A save that failed to export carries
`verdict: "saved"` **and** an `error` — the verdict is still the user's; only the
file is missing.

- **Both URLs matter, and the replay one matters more.** The mp4 is the new
  picture. The replay document is what makes the next session resumable: hand it
  back as `replayUrl` and the user reopens inside their own edit rather than
  restarting from a flattened file. Save **both** to `picsart_drive` promptly —
  they are temporary CDN URLs and they expire.
- **This spent nothing.** The render ran in the browser, not through
  `picsart_media_export`. Do not re-export the result server-side to "make it
  real" — that spends a render and a second file to reproduce what you already
  have.
- **Nothing to verify.** The preview and the export came from the same engine, so
  there is no seam to re-check and no draft to compare against.
- `stateHash` names the edit both files came from. Record it: it is how you tell
  later whether a file you hold is still current, and it makes a repeat save
  recognisable as the same edit rather than a new one.
- `discarded` means they closed the editor, **not** that they rejected the cut.
  Ask before changing anything.

## Build notes

Source: `pa-ai-models-mcp-server/server/widgets/src/final-cut/` — `FinalCut.tsx`
(the shell, source resolution, save/discard), `cascade-theme.ts`, `cascade.css`,
`feedback.ts`, `types.ts`. Tool at `server/src/mcp/tools/final-cut.tool.ts`,
output schema `FinalCutOutputSchema`, widget URI
`ui://widget/picsart-final-cut-v1.html`.

It mounts `@picsart/replay-embedded-editor` (`^2.1.0` — the published release) from
`gitlab.com/picsart/web/editor/editor-playground`, whose `SPEC.md` is the contract
for everything inside the frame. The prerelease carrying the multi-clip timeline is
deliberately not used; see the timeline section above and the long note in
`widgets/package.json` under `//replay-embedded-editor`.

**Three obligations that repo puts on its host, all met here and all silent when
broken:**

1. Cascade scopes its colour tokens under `html.light` / `html.dark` **classes**,
   while our foundation writes `<html data-theme>`. `cascade-theme.ts` mirrors the
   attribute onto the class and keeps mirroring it. Without it the editor's chrome
   renders invisible.
2. The editor imports Cascade component JS only, never a stylesheet, so
   `cascade.css` supplies `foundation.css` plus exactly the 18 components the
   editor stack imports — 104KB against 442KB for all 202. They resolve by
   **relative file path**: the package's `exports` map publishes only
   `./foundation.css`, so the specifier form fails to resolve.
3. The engine's `mp_playback_v3.wasm` (4.2MB) cannot be inlined into a widget that
   is served as inline resource text, so the build externalizes it to
   `server/widget-assets/replay-<version>/` and `WidgetAssetsController` serves it.
   The origin is substituted at serve time from `MCP_SERVER_URL` — baking it at
   build time would make a stage deployment fetch its engine from production.

**Cost to know:** `final-cut.html` is 5.03MB raw / 1.34MB gzipped. The reduction
path is serving the editor JS from `/widget-assets` as a module script rather than
inlining it, which needs the widget CSP's `script-src` to admit a cross-origin
module — still unverified, and what `picsart_replay_probe` exists to answer.

The resource `readFileSync`s `widgets/dist/final-cut.html` **at module load**, so a
widget change needs `npm run build:widgets` *and* a server restart. A new tool or a
changed description needs `npm run mcp:contract:update`,
`npm run mcp:descriptions:update`, `UPDATE_SERVED_CONTRACT=1 npx jest served-contract`,
and an entry in the root `tools.json`.

SHA-256: a5b435edfd8d8c00208c5b63d59ed6280d45072be0852bf2ff48bfeb86386350