fablecut 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -26,12 +26,22 @@ Every Claude Code session then has these tools:
26
26
  into `./media/` and register it. The stored `src` is always `/media/…`.
27
27
  - `fablecut_analyze_reference` — turn a reference video into an edit blueprint
28
28
  (shots, beats, BPM, energy, drop) + extract its music. See "Remake a reference video".
29
- - `fablecut_encode_profiles` — list export presets from `encoding-profiles.json` (each is a
30
- raw ffmpeg args list). Set `project.encodeProfile` via patch to pin a project default.
29
+ - `fablecut_frame` — **look at** a moment and get back an actual image.
30
+ `{path, t}` returns one frame of a source clip; `{path, frames}` returns a
31
+ contact sheet across it; with no `path`, `{time}` renders the composed
32
+ timeline. See "Look at the footage" below.
33
+ - `fablecut_encode_profiles` — list the **delivery presets** (YouTube, Shorts, Reels, TikTok,
34
+ X, LinkedIn, Master, Broadcast, Web, Archive, saved ones) and the raw ffmpeg **encoding
35
+ profiles** from `encoding-profiles.json`. `{preset:"youtube"}` returns one preset's spec.
36
+ See "Export" below.
31
37
  - `fablecut_normalize_audio` — measure clips (BS.1770 LUFS or sample peak, via ffmpeg) and
32
38
  set their clip gain to a loudness target. See "Audio mix" below.
33
39
  - `fablecut_auto_duck` — find where the voice tracks speak and write `duck` keyframes that
34
40
  dip the music under it. See "Audio mix" below.
41
+ - `fablecut_frame` — **see** a moment. `{path, t}` → one JPEG frame of a source
42
+ clip; `{path, frames:12}` → a contact sheet across it; `{time}` → the composed
43
+ timeline, graded, as export renders it. Returns a real image, not numbers.
44
+ See "Look at the footage" below.
35
45
  - `fablecut_denoise` — reduce background noise on audio clips (renders a cleaned copy of the
36
46
  file and switches the clip's stems to it). See "Audio mix" below.
37
47
  - `fablecut_export` — render the timeline to a file in `exports/` with the editor's own
@@ -41,6 +51,9 @@ Every Claude Code session then has these tools:
41
51
  select. See "Color" and "Masks" below.
42
52
  - `fablecut_track` — motion tracking: follow a region of a video clip, store it as
43
53
  a track, and make a mask or another clip follow it. See "Tracking" below.
54
+ - `fablecut_proxy` — make, check or delete proxies: light copies of heavy footage
55
+ the editor plays while editing (export always reads the originals). See
56
+ "Media management" below.
44
57
 
45
58
  ### Token-efficient editing (important for agents)
46
59
 
@@ -59,8 +72,12 @@ Editing via full get→modify→set costs thousands of tokens per change. Cheape
59
72
  Re-check the clips, then retry with that revision.
60
73
  3. **Docs**: request `fablecut_docs {section:"schema"}` (or "Recipes", "Remake", …)
61
74
  instead of the whole manual; skip it entirely if the schema is already in context.
62
- 4. **Media questions** (duration, fps, size): read them from the registered media
63
- entries — don't shell out to ffprobe; the browser probes and writes them back.
75
+ 4. **Media questions** (duration, resolution, fps, codec, audio channels, file
76
+ size, how often it is used): read them from the registered media entries or
77
+ the compact view's MEDIA lines — don't shell out to ffprobe; the editor
78
+ probes every file and writes them back (see "Media management").
79
+ For what the footage *looks like*, use `fablecut_frame` — it is far cheaper
80
+ than reading a file, and far more reliable than inferring from a number.
64
81
  5. Batch related changes into ONE patch call (ops apply in order, one revision bump).
65
82
  6. **Cut and trim with the edit ops** (next section) rather than recomputing
66
83
  `start` / `in` / `duration` by hand — they keep linked stems in sync.
@@ -133,6 +150,9 @@ reports the real paths.
133
150
  Tests (and nothing else) may set **`FABLECUT_NO_FS_WATCH=1`** to skip `fs.watch`.
134
151
  On Windows, libuv can abort the process when a file is created under a temp
135
152
  data dir. Production leaves watching on so the UI live-reloads.
153
+ Tests may also set **`FABLECUT_NO_AUTOSTART=1`** so `ensureUIServer()` reports
154
+ a missing editor server instead of spawning one — the auto-start is detached,
155
+ so a test that triggers it would leave a `server.js` running after it finishes.
136
156
 
137
157
  ## Run
138
158
 
@@ -254,6 +274,10 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
254
274
  "untargetedTracks": [ "V2" ],
255
275
  // ^ optional — lanes edits skip (split, insert, ripple, close gap…); default: all targeted
256
276
  "encodeProfile": "hq", // optional — fast-export profile id (see encoding-profiles.json)
277
+ "delivery": { "preset": "youtube", "width": 1920, "height": 1080, "…": "…" },
278
+ // ^ optional — the delivery spec exports use by default (see "Delivery presets"); setProject takes a preset id
279
+ "outputs": [ { "id": "o_1", "on": true, "preset": "master", "…": "…" } ],
280
+ // ^ optional — batch outputs: files made together, one render per frame rate (see "Batch outputs and the export queue")
257
281
  "master": { "gain": -1.5, "fx": [ { "type": "limiter", "ceiling": -1 } ] },
258
282
  // ^ optional — master fader, dB (−60 = silence … +12; omit = 0 dB) and master effects (see "Audio mix")
259
283
  "tracks": [ // optional — timeline lanes (default V3…V1 + A1…A4)
@@ -268,6 +292,8 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
268
292
  { "id": "A4", "kind": "audio" }
269
293
  ],
270
294
  "buses": [ { "id": "B1", "name": "Dialogue", "gain": -2, "fx": [ { "type": "compressor" } ] } ],
295
+ "collections": [ { "id": "col_sel", "name": "Selects", "media": [ "m_abc" ] } ],
296
+ // ^ optional — Project-bin collections: media grouped across folders; see "Media management"
271
297
  "comps": [ { "id": "lt", "name": "Lower third", "width": 1280, "height": 720, "duration": 5, "clips": [ /* … */ ] } ],
272
298
  // ^ optional — compositions (nested sequences), placed with kind:"comp" clips; see "Compositions (nested sequences)"
273
299
  // ^ optional — submix buses B1…B8: name, gain (dB), pan, mute, fx; tracks route in with `out`
@@ -275,6 +301,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
275
301
  { "id": "m_abc", "name": "intro.mp4", "kind": "video", // video|audio|image|svg
276
302
  "src": "/media/intro.mp4", // path under ./media or ./library (never a raw https:// URL)
277
303
  "duration": 12.4, "width": 1920, "height": 1080,
304
+ "fps": 29.97, "codec": "h264", "audioCodec": "aac", "audioChannels": 2, "sampleRate": 48000,
305
+ "size": 45200000, "bitrate": 29300000, // probed from the file (read-only; see "Media management")
306
+ "favorite": true, "tags": [ "interview", "day 1" ], // optional: bin organization
278
307
  "folderId": null }, // optional: id of a folders[] entry
279
308
  { "id": "m_abd", "name": "intro.denoise-medium.flac", "kind": "audio", "src": "/media/intro.denoise-medium.flac",
280
309
  "derivedFrom": "m_abc", "denoise": "medium" } // a noise-reduced copy of m_abc (see "Audio mix")
@@ -1346,6 +1375,140 @@ with `fields:{logo:"<your logo's media id>", brand, accent}` at 0;
1346
1375
  it in every project; **titles that arrive and leave the same way** —
1347
1376
  `applyPreset` an In and an Out on every title in one patch.
1348
1377
 
1378
+ ### Media management
1379
+
1380
+ The Project bin knows each file's details and helps find, group and track
1381
+ footage. Everything here is organization: none of it changes what plays or
1382
+ exports.
1383
+
1384
+ **Metadata.** The editor reads every file in `media/` and `library/` with
1385
+ ffprobe (via `GET /api/media-info`) and writes what it finds onto the media
1386
+ entry. These fields are read-only — they describe the file:
1387
+
1388
+ | key | does |
1389
+ |---|---|
1390
+ | `fps` | video frame rate (29.97, 25, 59.94 …); absent for stills and audio |
1391
+ | `codec` | video codec (`h264`, `hevc`, `prores`, `vp9` …), or the image format (`png`, `mjpeg`) |
1392
+ | `audioCodec` · `audioChannels` · `sampleRate` | first audio stream: codec, channel count (`0` = the file has no audio), Hz |
1393
+ | `size` · `bitrate` | file size in bytes · overall bitrate, bits per second |
1394
+ | `width` · `height` · `duration` | as before; filled in when missing (phone footage shot rotated reports its upright size) |
1395
+
1396
+ Without ffprobe on PATH only `size` is filled in; the browser still probes
1397
+ `duration`, `width` and `height`.
1398
+
1399
+ **Organizing.** `favorite: true` stars a file; `tags` is a list of free-form
1400
+ labels (at most 24, each up to 32 characters, matched without regard to case).
1401
+ `collections` at the top of the document group media across folders — a file
1402
+ can be in any number of them, while it sits in one folder (`folderId`).
1403
+
1404
+ **Usage.** A file's *placements* are the clips that show it, on the main
1405
+ timeline and inside every composition. A video and the audio stems linked to it
1406
+ count as one placement, so a shot used twice reads "used 2×".
1407
+
1408
+ In the editor, the Project tab has:
1409
+
1410
+ - **Search** — every word must match the name, kind, extension, codec,
1411
+ resolution (`3840x2160`, `2160p`) or a tag; `#word` matches tags only. Esc clears it.
1412
+ - **Show** — All media · ★ Favorites · Used in the edit · Unused · a kind · a
1413
+ collection · a tag. While searching or filtering, the bin lists the hits flat
1414
+ (each with its folder), with a count and **Clear**; a collection's view also
1415
+ has **Rename** and **Delete** (its media stay).
1416
+ - **Sort** — name (natural: `shot 2` before `shot 10`), kind, duration,
1417
+ resolution, frame rate, codec, audio channels, file size or times used, with
1418
+ ↑ / ↓. Files missing a value sort last either way. Folders keep their tree;
1419
+ the sort applies inside each.
1420
+ - **☰ List view** — one row per file with columns: name, duration, resolution,
1421
+ FPS, codec, audio, size and uses; click a column heading to sort by it. On a
1422
+ narrow bin the columns drop out right to left; widen the panel to see them all.
1423
+ - On each file: **☆** stars it, the **uses** badge opens its placements.
1424
+ Right-click a file → **Open in Source**, **Find timeline instances** (every
1425
+ placement, main timeline and comps, with track and time — click one to jump
1426
+ there, opening its comp if needed), **Media info…**, favorites, **Tags…**,
1427
+ **Add to / Remove from** a collection, **New collection…**, **Remove from project**.
1428
+ - Right-click a clip on the timeline → **Reveal in project** scrolls the bin to
1429
+ its file (or its comp), opening folders and clearing a search that hides it.
1430
+
1431
+ **Offline media.** A file is *offline* when its `src` names nothing under
1432
+ `media/` or `library/` — it was moved, renamed or deleted. The editor checks
1433
+ on load and whenever those folders change. An offline file's clips stay where
1434
+ they are: the timeline hatches them red (**⚠ Offline**), the monitor and the
1435
+ export draw a **Media offline** slate in the picture's place, and its audio is
1436
+ silent. The bin marks the file **OFFLINE**, says how many are missing, and the
1437
+ Show filter gains **⚠ Offline**. The Export dialog warns before rendering one.
1438
+ Files under `media/` may sit in subfolders (`/media/archive/a.mp4`).
1439
+
1440
+ - **Relink** points a media entry at the same footage where it is now; clips
1441
+ keep their In points, edits and everything else. Right-click the file →
1442
+ **Relink…** (or **Relink…** on the offline banner for all of them). The
1443
+ dialog looks through `media/` and `library/` for each file — same name, the
1444
+ same name with another extension, a similar name, the same size — and
1445
+ picks a same-name match for you. **Browse…** picks one file and **Choose
1446
+ folder…** matches a whole folder by name; either copies the chosen files
1447
+ into `media/`. A relinked file whose length differs from the original gets
1448
+ a warning (is it really the same footage?).
1449
+ - **Replace file…** (right-click a file) puts a *different* file behind the
1450
+ media entry — a new render, a better take. Every clip using it switches;
1451
+ each keeps its In point and timing, trimmed to fit a shorter file. The name
1452
+ and details follow the new file; favorites, tags and collections stay. To
1453
+ swap the file of one clip only, use the Inspector's **Source** menu instead.
1454
+ - Relink and replace are one undo step each.
1455
+
1456
+ **Proxies.** Heavy footage — 4K, HEVC, ProRes, long phone clips — can stutter
1457
+ while you scrub and play. A *proxy* is a light copy the editor plays instead
1458
+ while you edit: H.264 at 540p (or 720p / 1080p, never larger than the
1459
+ source), a keyframe every 12 frames so seeking is quick, the source's own
1460
+ timestamps (frame for frame — no frames added or dropped) and its first audio
1461
+ stream. The edit always refers to the original: the timeline, `project.json`
1462
+ and every clip stay the same whether proxies are on or off.
1463
+
1464
+ **Export always reads the originals**, and so do `fablecut_frame` (timeline
1465
+ mode), `fablecut_scopes`, tracking and every other measurement — the
1466
+ players switch to the original files for the render and back afterwards. A
1467
+ proxy only changes how smoothly the preview runs, never the result.
1468
+
1469
+ - Right-click a video in the bin → **Create proxy (540p)** / **(720p)**;
1470
+ right-click the **Project** tab → **Create proxies for N videos**. ffmpeg
1471
+ makes them on the server one at a time in the background; the bin shows
1472
+ each file's progress (**P 42%**), then a green **P** once it is ready.
1473
+ **Delete proxy** removes one. The bin's list view has a **Proxy** column,
1474
+ Show has **Has a proxy** / **Video without a proxy**, and **Media info**
1475
+ says which file is playing.
1476
+ - The monitor's **Proxy** button turns proxy playback on and off (on by
1477
+ default; remembered in this browser). The Source monitor follows it too.
1478
+ - Proxies live in `analysis/proxies/`, a disposable cache: each is named
1479
+ after its source file *and that file's size and modification time*, so a
1480
+ re-rendered or replaced original simply has no proxy until you make a new
1481
+ one. Deleting the folder loses nothing but time. Proxies need ffmpeg.
1482
+
1483
+ **Agents:**
1484
+
1485
+ - `{op:"updateMedia", id | ids, set:{name?, favorite?, tags?, folderId?}, addTags?, removeTags?}`
1486
+ — organize files (`favorite:false` unstars; `tags:[]` clears). Metadata keys
1487
+ are refused: they come from the file.
1488
+ - `{op:"setCollection", id?, name?, media?, add?, remove?}` creates a collection
1489
+ (a new `id`, or a `name` no collection has) or changes one: `media` replaces
1490
+ its list, `add` / `remove` edit it. `{op:"removeCollection", id}` deletes it;
1491
+ its media stay. `removeMedia` takes a file out of every collection.
1492
+ - The compact view lists each file with its metadata, ★, tags, folder and
1493
+ `uses:N`, and the collections on one line:
1494
+ `m_abc video "intro.mp4" 12.4s 1920×1080 29.97 fps h264 stereo 48 kHz 45 MB ★ #interview uses:2`.
1495
+ - `fablecut_import_media` probes the file as it imports it, so the entry it
1496
+ returns already has these fields.
1497
+ - `fablecut_status` lists offline media, and the compact view marks each
1498
+ `OFFLINE (file missing — relinkMedia)` and each video with a proxy `proxy:540p`.
1499
+ - `fablecut_proxy {}` reports every video's proxy; `{create:true, mediaIds?, height?}`
1500
+ makes them (waits until done unless `wait:false`); `{delete:true, mediaIds?}`
1501
+ removes them. Proxies never change what you measure or export, so an agent
1502
+ only needs them when the user asks for smoother playback.
1503
+ - `{op:"relinkMedia", id, src:"/media/…", name?}` — the same footage found
1504
+ elsewhere: put the file in `media/` (copy it, or `fablecut_import_media` and
1505
+ `removeMedia` the extra entry it registers), then relink. The note warns when
1506
+ the new file's length differs.
1507
+ - `{op:"replaceMedia", id, src, name?}` — a different file behind the entry;
1508
+ clips keep their timing, trimmed to fit (the note says how many). `src` must
1509
+ be an existing file under `/media/` or `/library/` of the same kind (an
1510
+ audio entry may use a video container).
1511
+
1349
1512
  ### Semantics
1350
1513
 
1351
1514
  - Rendering order: V1 is drawn first, then V2, V3, … on top. SVG/image/text clips
@@ -1441,6 +1604,44 @@ it in every project; **titles that arrive and leave the same way** —
1441
1604
  - `bgRemove` and `chromaKey` can combine with all filters; heavy pixel work is
1442
1605
  automatic (only runs when those props are set).
1443
1606
 
1607
+ ## Look at the footage (`fablecut_frame`)
1608
+
1609
+ `fablecut_analyze_reference` gives you the **ears** — where the cuts are, how hard
1610
+ each shot hits. `fablecut_frame` gives you the **eyes**. It answers with a real
1611
+ image (an MCP `image` content block), so you can judge a shot instead of
1612
+ inferring it from an `energy` value.
1613
+
1614
+ | call | returns |
1615
+ |---|---|
1616
+ | `{path:"/media/x.mp4", t:3.2}` | that exact frame |
1617
+ | `{path:"/media/x.mp4", frames:12}` | a 4×3 contact sheet across the whole clip |
1618
+ | `{path:"/media/x.mp4", frames:12, from:30, to:90}` | a contact sheet across part of it |
1619
+ | `{time:8.5}` (no `path`) | the **composed timeline** at 8.5 s, graded, all tracks |
1620
+ | `{path}` with an absolute path | the file is read in place without copying into `media/` |
1621
+ | `{path, text:true}` | text-only statistics (luma, colors, texture, saturation, motion) for non-vision agents |
1622
+
1623
+ Each answer leads with a text line naming the source, the timestamp(s) and (in
1624
+ timeline mode) the clips on screen, so you always know what you are looking at.
1625
+ `width` sets the pixel width (default 768 for one frame, 320 per contact-sheet
1626
+ cell); `text:true` returns descriptive statistics instead of an image block, for
1627
+ agents without vision capability — luma levels (min, p1, median, mean, p99, max,
1628
+ all 0–1 like `fablecut_scopes`), dominant colors, texture, saturation and
1629
+ per-cell motion; `where`/`timeout` apply to timeline mode only.
1630
+
1631
+ The REST endpoint `/api/frame` accepts `src`, `t`, `frames`, `from`, `to`, `w`,
1632
+ `q` (JPEG quality 1–31) and `text=1` (statistics mode) as query parameters.
1633
+
1634
+ **When it pays.** Before choosing a clip's `in` point, sample a few candidate
1635
+ moments — the blueprint tells you a shot spans 2.1 s–4.9 s and reads
1636
+ `energy: 88`, but only a frame tells you whether that is a car jump or a pan
1637
+ across a landscape. Contact sheets are the cheap survey: one call shows you
1638
+ what a whole 3-minute clip holds, then you grab single frames where it matters.
1639
+ Timeline mode answers "what did I actually build?" — the same picture the user
1640
+ sees and export renders, grades and transitions included.
1641
+
1642
+ Needs ffmpeg on PATH for the source modes. Timeline mode runs in the open editor
1643
+ tab, else a headless browser, like `fablecut_export`.
1644
+
1444
1645
  ## Remake a reference video (analyze → blueprint → rebuild)
1445
1646
 
1446
1647
  Given a reference edit (a reel/montage the user likes), FableCut can analyze it
@@ -1480,6 +1681,8 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
1480
1681
  (hard cuts by default — that's what shot detection saw). Pick source footage
1481
1682
  whose motion matches each shot's `energy` (calm ≤40, action ≥70), and choose
1482
1683
  each clip's `in` so something interesting happens inside the window.
1684
+ **Then look**: `fablecut_frame {path, frames:12}` to survey a candidate clip,
1685
+ `{path, t}` at the exact moment you want, before committing to an `in`.
1483
1686
  4. The `drop`: put the hero shot there; classic garnish = a speed ramp landing
1484
1687
  on it, an impact `adjust` layer (shake+rgbSplit), or a whip transition.
1485
1688
  5. Pacing garnish to taste: shots shorter than ~0.6 s read as beat-flashes;
@@ -1495,6 +1698,23 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
1495
1698
  Append `?force=1` to overwrite unconditionally. Writes are atomic (tmp file +
1496
1699
  rename), so a crashed write never corrupts the file.
1497
1700
  - `GET /api/media` — list files in ./media (name, src, size)
1701
+ - `GET /api/media-info?src=/media/x.mp4` → `{src, info}` · `POST /api/media-info {srcs:[…], stat?}` →
1702
+ `{info:{src: info}}` (up to 2000; `stat:true` answers only `{size, mtimeMs}`, no ffprobe —
1703
+ how the editor checks for offline files) — a file's metadata from ffprobe: `size`, `fps`, `codec`,
1704
+ `audioCodec`, `audioChannels`, `sampleRate`, `bitrate`, `duration`, `width`, `height` and
1705
+ `probe` (`"ffprobe"`, or `"stat"` when only the size could be read). `info` is `null` when
1706
+ the src names no file under `/media/` or `/library/`. Cached per file version.
1707
+ - `POST /api/proxy {srcs:[…], height?:540|720|1080}` → `{status:{src: status}}` queues proxies
1708
+ (ffmpeg, one at a time); `POST /api/proxy/status {srcs}` → `{status:{src: status | null}}`;
1709
+ `POST /api/proxy/delete {srcs}` → `{deleted:{src: n}}`. A status is
1710
+ `{state:"ready", src:"/proxies/…", width, height, size}`, `{state:"queued"|"running", progress, height}`,
1711
+ `{state:"failed", error}` or `{state:"none"}` (`null` = no such source file). Progress and
1712
+ results are also pushed as SSE event `proxy` (`{src, status}`). `GET /proxies/<name>` serves
1713
+ a proxy file from `analysis/proxies/`. `/api/proxy` answers 503 without ffmpeg.
1714
+ - `POST /api/relink/candidates {media:[{id, name, src?, size?, kind?}]}` →
1715
+ `{candidates:{id:[{src, name, size, score, why}]}}` — where a missing file might be now:
1716
+ files under `media/` and `library/` (subfolders included) of the same kind, ranked by
1717
+ name (same name, same name with another extension, similar name) and size, best 5 each.
1498
1718
  - `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` — list library assets
1499
1719
  - `GET /api/presets` · `GET /api/templates` — the saved presets / templates, validated (bad files skipped);
1500
1720
  `POST /api/presets {preset}` · `POST /api/templates {template}` — validate one and save it as
@@ -1520,9 +1740,27 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
1520
1740
  - `POST /api/analyze` — body `{src:"/media/ref.mp4", threshold?, music?}`: analyze a
1521
1741
  reference video into an edit blueprint (see "Remake a reference video"); extracts
1522
1742
  its music into ./media. `GET /api/analyze?src=…` returns the cached blueprint.
1743
+ - `GET /api/frame?src=/media/x.mp4&t=3.2` — one JPEG still of a source clip, the
1744
+ "eyes" to go with `/api/analyze`'s ears (see "Look at the footage"). Optional
1745
+ `w` (pixel width, default 768) and `q` (ffmpeg `-q:v` 1–31, default 4).
1746
+ `&frames=N[&cols=][&from=][&to=][&w=320]` instead returns one N-up **contact
1747
+ sheet** across the clip (N 2–60; `cols` omitted → the squarest layout, so 6
1748
+ frames give 3×2 rather than 4×2 with two black cells). `src` must name an
1749
+ existing file under `/media/` or `/library/` (404 otherwise). `?path=` reads
1750
+ any file on the machine but only with the `X-FableCut-Client: mcp` request
1751
+ header, which only the local MCP client sends — without it the request is
1752
+ refused (403) before the path is even resolved. Results are cached in
1753
+ `analysis/frames/` keyed by source path, size and mtime; the cache is capped
1754
+ (500 files / 128 MB, oldest first). The response carries
1755
+ `X-FableCut-Frame-Times` (comma-separated sample times, so a sheet's cells can
1756
+ be mapped back to timestamps) and `X-FableCut-Frame-Source`.
1757
+ - `POST /api/scopes/request` also takes `{"image":true}`, which makes the editor
1758
+ return the measured still as a JPEG data URL in `result.image` — how
1759
+ `fablecut_frame {time}` shows an agent the composed program picture.
1523
1760
  - `GET /api/events` — SSE: named event `change` when project.json, ./media or
1524
1761
  ./library changes; named event `profiles` when `encoding-profiles.json` changes
1525
- (UI refreshes the export-profile list only — no project reload)
1762
+ (UI refreshes the export-profile list only — no project reload); named event
1763
+ `proxy` as a proxy is queued, makes progress, finishes or is deleted
1526
1764
  - Fast / WebCodecs export (browser compositor → server ffmpeg):
1527
1765
  `GET /api/export/ffmpeg` → `{available}` · `GET /api/export/profiles[?detail=1]` →
1528
1766
  `{default, profiles, issues}` · `POST /api/export/begin`
@@ -1765,12 +2003,135 @@ agent last wrote, so an export right after a patch includes that patch.
1765
2003
 
1766
2004
  - `fablecut_export {}` — waits, then returns the file path under `exports/`.
1767
2005
  Range: `inPoint`→`outPoint` when either is set, else the whole timeline;
1768
- `range:"entire"|"in-out"` overrides. `profile:"hq"` picks an encoding profile.
2006
+ `range:"entire"|"in-out"` overrides.
2007
+ - `preset:"instagram-reels"` picks a delivery preset; `delivery:{…}` changes any of its
2008
+ settings (`{preset:"youtube", delivery:{fps:60, video:{rate:"vbr", bitrate:16000}}}`).
2009
+ `profile:"hq"` picks a raw encoding profile instead. With neither, `project.delivery`
2010
+ is used when set, else `project.encodeProfile`, else the server's default profile.
2011
+ - `outputs:true` exports the project's checked batch outputs; `outputs:[{preset:"master"},
2012
+ {preset:"youtube"}, {preset:"instagram-reels"}, …]` exports a list. One render per
2013
+ frame rate; the reply lists every file (or why one failed).
1769
2014
  - `where:"headless"` renders in the background and leaves the user's tab alone;
1770
2015
  `where:"tab"` refuses if no tab is open.
1771
2016
  - Long edits: `wait:false` returns a job id at once; poll with `{job:"x_…"}`
1772
2017
  and stop one with `{cancel:"x_…"}`. One export runs at a time.
1773
2018
 
2019
+ ### Delivery presets
2020
+
2021
+ The Export dialog's **Preset** list. Each preset is a structured spec — no ffmpeg
2022
+ arguments to write — that `delivery.js` turns into one ffmpeg pass:
2023
+
2024
+ | Preset | Output |
2025
+ |---|---|
2026
+ | `youtube` | 1920×1080, H.264 CRF 18, keyframe every 0.5 s, AAC 384k 48 kHz |
2027
+ | `youtube-shorts` | 1080×1920, as YouTube; warns past 3 min |
2028
+ | `instagram-reels` | 1080×1920, H.264 CRF 19 capped at 25 Mb/s, AAC 256k; ≤ 60 fps, warns past 3 min |
2029
+ | `tiktok` | 1080×1920, H.264 CRF 19 capped at 20 Mb/s, AAC 256k; warns past 10 min |
2030
+ | `x` | 1920×1080, H.264 CRF 20 capped at 25 Mb/s, AAC 192k; warns past 2:20 and 512 MB |
2031
+ | `linkedin` | 1920×1080, H.264 CRF 20 capped at 20 Mb/s, AAC 192k; warns past 15 min and 5 GB |
2032
+ | `master` (default) | the picture's own size, H.264 CRF 16, AAC 320k |
2033
+ | `broadcast` | 1920×1080 ProRes 422 HQ MOV, PCM 24-bit 48 kHz, Rec. 709, normalized to −23 LUFS / −1 dBTP (EBU R128); warns off 25 / 29.97 / 50 / 59.94 fps |
2034
+ | `web` | 1280×720 H.264 CRF 23, AAC 128k, fast start |
2035
+ | `archive` | the picture's own size, lossless FFV1 4:2:2 10-bit MKV, PCM 24-bit |
2036
+ | `custom` | your own settings |
2037
+
2038
+ A spec (every key optional — what is left out comes from its `preset`):
2039
+
2040
+ ```jsonc
2041
+ {
2042
+ "preset": "youtube",
2043
+ "width": 1920, "height": 1080, // null = the picture's own size (the export frame, else the canvas)
2044
+ "fps": null, // null = the project's; 23.976 / 29.97 / 59.94 are exact NTSC rates
2045
+ "reframe": "fill", // another shape: fill crops the middle (or the export frame), fit pads, stretch
2046
+ "container": "mp4", // mp4 · mov · mkv · webm · mxf
2047
+ "video": {
2048
+ "codec": "h264", // h264 · hevc · av1 · vp9 · prores · dnxhr · mpeg2 (XDCAM HD422) · ffv1
2049
+ "encoder": "libx264", // or h264_nvenc / h264_qsv / h264_amf … — listed only when they work here
2050
+ "rate": "crf", // crf · vbr · cbr (ProRes, DNxHR, XDCAM, FFV1 are fixed)
2051
+ "crf": 18, "bitrate": 12000, "maxrate": null, // kb/s; maxrate caps a CRF or VBR encode
2052
+ "keyint": 2, // seconds between keyframes, 0 = the encoder's default
2053
+ "speed": "balanced", // fast · balanced · best
2054
+ "pixFmt": "yuv420p", // 4:2:0 / 4:2:2 / 4:4:4, 8 or 10 bit, as the codec allows
2055
+ "profile": null // ProRes: proxy lt standard hq 4444 4444xq · DNxHR: lb sq hq hqx 444
2056
+ },
2057
+ "audio": { "codec": "aac", "bitrate": 320, "sampleRate": 48000, "channels": 2,
2058
+ "loudness": null, "truePeak": -1 },
2059
+ // aac · opus · mp3 · pcm16 · pcm24 · flac · none
2060
+ // loudness: a LUFS target (−14 streaming, −16 podcast, −23 EBU R128, −24 ATSC) or null
2061
+ "color": "bt709", // bt709 · bt709-full · bt601-625 (PAL) · bt601-525 (NTSC)
2062
+ "filename": "{project}_{preset}_{res}",
2063
+ // tokens: {project} {preset} {res} (1080p, 1080x1920, 1080sq) {w} {h} {fps} {codec} {date} {time} {range} {n}
2064
+ "metadata": { "title": "", "artist": "", "description": "", "copyright": "", "date": "", "keywords": "" }
2065
+ }
2066
+ ```
2067
+
2068
+ - **Impossible combinations are corrected, and the reply says how** — ProRes moves to MOV,
2069
+ WebM takes Opus, Quick Sync gets 4:2:0. The dialog shows the same notes.
2070
+ - **Reframing happens in ffmpeg**: the browser sends the whole canvas and ffmpeg crops
2071
+ the export frame (or the middle, for another shape) and scales. The dialog warns
2072
+ when the output is upscaled or cropped, when a platform's length, size or frame-rate
2073
+ limit is passed, and estimates the file size where it can (bitrate and fixed-rate
2074
+ codecs; CRF cannot know).
2075
+ - Outputs that keep more than 4:2:0 8-bit (ProRes, DNxHR, FFV1, 10-bit, 4:2:2, 4:4:4)
2076
+ get raw RGBA frames instead of JPEG, so no chroma is lost on the way. Slower to send.
2077
+ - The audio is cut to exactly the frames' length.
2078
+ - **Loudness** (`audio.loudness`): the mix is measured and turned to the target with
2079
+ ffmpeg's two-pass `loudnorm` — one linear gain, and a true-peak limiter at `truePeak`.
2080
+ When the peaks leave no room for the gain it compresses instead and may land under the
2081
+ target; the check after the export says so. Off (null) leaves the mix as mixed —
2082
+ except Broadcast, which normalizes to −23 LUFS / −1 dBTP. Mixes shorter than 3 s may
2083
+ not reach the target.
2084
+ - **Checked after the export.** Every file is read back with ffprobe and measured
2085
+ (BS.1770 integrated loudness and true peak) and compared with what was asked:
2086
+ size, frame rate, frame count, codec, color tags, audio codec / rate / channels,
2087
+ length, loudness against the target (±1 LU) and the true-peak ceiling, and file size
2088
+ against the platform's limit. Without a target the report says where the platform
2089
+ plays it ("YouTube plays it 4.2 dB quieter"). The report shows in the queue, a toast
2090
+ after a single export, and the `fablecut_export` reply.
2091
+ - **Saved presets**: "Save preset…" in the dialog writes `library/delivery/<id>.json`,
2092
+ listed under "Saved" in every project (`GET / POST /api/export/presets`,
2093
+ `POST /api/export/presets/delete {id}`).
2094
+ - The settings are checked by a 0.1 s ffmpeg dry run before anything renders.
2095
+ - The dialog remembers the last settings in this browser. `setProject {delivery:"youtube"}`
2096
+ (or a spec) pins them in the project, for the dialog and for `fablecut_export`.
2097
+ - Fast export only — WebCodecs and Realtime export encode their own way.
2098
+
2099
+ ### Batch outputs and the export queue
2100
+
2101
+ **One project → many files.** `project.outputs` is the project's list of files to make
2102
+ (up to 16), each a delivery spec plus `{id, on}`. In the Export dialog, **Batch outputs**
2103
+ lists them: **+ Add these settings** adds the preset shown above, **+ Typical set** adds
2104
+ Master, YouTube 1080p, Reel 1080×1920, Square 1080 and a 720p preview, the checkbox picks
2105
+ which go out, **Load** shows one's settings above. **Export checked** queues the checked
2106
+ ones and starts.
2107
+
2108
+ ```jsonc
2109
+ // setProject {outputs:[…]} — a preset id or a spec each; stored complete with an id
2110
+ {"op":"setProject","set":{"outputs":[
2111
+ "master", "youtube",
2112
+ {"preset":"instagram-reels"},
2113
+ {"preset":"custom","width":1080,"height":1080,"filename":"{project}_square_{res}"},
2114
+ {"preset":"web","filename":"{project}_preview_{res}","on":false}
2115
+ ]}}
2116
+ ```
2117
+
2118
+ - **Rendered once.** Outputs with the same frame rate and range go out in ONE render:
2119
+ the compositor draws each frame once and every output's ffmpeg encodes it (up to 8 per
2120
+ render). Four 1080p files take about as long as one. Outputs at another frame rate get
2121
+ their own render. When any output needs full chroma (ProRes, 10-bit, lossless) the
2122
+ frames go as RGBA to all of them.
2123
+ - **The export frame** is the picture for outputs of its own shape (and for the
2124
+ picture's own size). An output of another shape reframes the whole canvas — a project
2125
+ framed for 9:16 still makes a proper 16:9 file.
2126
+ - **The export queue** (Export dialog → **Add to queue**, or **Queue**) runs waiting
2127
+ outputs back to back with a progress bar each, **Stop**, **Retry** for a failed one,
2128
+ **Open** for a finished file and **Clear finished**. It lives in the tab: a reload
2129
+ empties it. One output that fails does not stop the others in its render.
2130
+ - REST: `POST /api/export/begin {deliveries:[{delivery, name}, …], canvasW, canvasH,
2131
+ region?, duration, fps, hasAudio}` → `{id, input, jpegQuality, outputs:[{file, spec,
2132
+ summary, notes, size}]}`; frames and audio as for one output; `/api/export/end` →
2133
+ `{src, outputs:[{file, ok, src, report:{ok, checks:[{ok, text}], line}, mixLoudness?} | {file, ok:false, error}]}`.
2134
+
1774
2135
  ### Encoding profiles (`encoding-profiles.json`)
1775
2136
 
1776
2137
  User-editable at the repo root. A profile is a **raw ffmpeg argument list** plus the
@@ -1834,11 +2195,12 @@ ffmpeg -y -f image2pipe -framerate <fps> -i - [-i audio.wav] <jpeg-color> <args
1834
2195
  not `-i source.mp4`. Filters and codec settings apply to that frame stream. To transcode
1835
2196
  an existing file verbatim, run ffmpeg directly — that is outside the compositor path.
1836
2197
 
1837
- **Which profile is used (priority):**
1838
- 1. Profile picked in the Export dialog (one-off; saved to browser settings unless overridden)
1839
- 2. `project.encodeProfile` — set via UI reload or `{op:"setProject", set:{encodeProfile:"hq"}}`
1840
- 3. Browser setting `encodeProfile` in localStorage (set when you change the Export dropdown)
1841
- 4. `default` in `encoding-profiles.json`
2198
+ Encoding profiles are listed at the bottom of the Export dialog's Preset list.
2199
+
2200
+ **What the Export dialog opens on (priority):**
2201
+ 1. `project.delivery`, then `project.encodeProfile` — set with `setProject`
2202
+ 2. The last preset or profile used in this browser
2203
+ 3. The `master` delivery preset
1842
2204
 
1843
2205
  **MCP:** `fablecut_encode_profiles` lists profiles; `{detail:true}` includes each `args`
1844
2206
  array; `{profile:"hq"}` returns one profile. `fablecut_status` shows the effective profile.
package/README.md CHANGED
@@ -120,6 +120,8 @@ same time.
120
120
  ffmpeg; the clip's audio switches to it and **Off** switches back. Agents
121
121
  use `fablecut_denoise`.
122
122
  - **Project bin folders** — tree view with expand/collapse; drag media or folders to nest; right-click the **Project** tab → New folder; drop files onto a folder to import into it
123
+ - **Media management** — each file's frame rate, codecs, audio channels and size (ffprobe); search by name, codec, resolution or `#tag`; filter by favorites, used / unused, kind, tag or collection; sort by any detail or by times used; a list view with metadata columns; favorites, tags and collections; **Find timeline instances** and **Reveal in project**; offline files flagged with a "Media offline" slate, **Relink…** (finds moved files by name and size) and **Replace file…**
124
+ - **Proxies** — light, frame-accurate H.264 copies of 4K / HEVC / ProRes footage made in the background (ffmpeg); edit on the proxy with the monitor's **Proxy** button, export always renders the originals
123
125
  - **Import from URL** — **+ URL** downloads an HTTPS video/audio/image into
124
126
  `./media/` (same-origin after import). Remote SVG is refused. Agents use
125
127
  `fablecut_import_media` with an `https://` path. The URL is not kept as
@@ -287,6 +289,20 @@ same time.
287
289
  `node analyze.js ref.mp4`, `POST /api/analyze`, or the
288
290
  `fablecut_analyze_reference` MCP tool.
289
291
 
292
+ **Look at the footage**
293
+
294
+ - An agent (or you, over REST) can fetch an actual **image** of any moment:
295
+ `GET /api/frame?src=/media/x.mp4&t=3.2` returns a JPEG of that frame, and
296
+ `&frames=12` returns a 4×3 **contact sheet** across the clip — a cheap survey
297
+ of everything a 3-minute take holds. Grabs are cached under `analysis/frames/`.
298
+ - The `fablecut_frame` MCP tool wraps it and returns the image inline, so a
299
+ multimodal model can *look*. Pass a `time` instead of a `path` and it renders
300
+ the **composed timeline** through the editor's own compositor — graded, all
301
+ tracks, identical to what export produces — so "is this grade right?" and "what
302
+ did I actually build?" have a visual answer.
303
+ - Together with the analyzer above, agents pick footage by looking at it instead
304
+ of guessing from an energy number.
305
+
290
306
  **Asset library**
291
307
 
292
308
  - `library/` folders surface as tabs in the UI: **Elements** (overlay art),
@@ -294,18 +310,36 @@ same time.
294
310
 
295
311
  **Export**
296
312
 
313
+ - **Delivery presets**: YouTube, YouTube Shorts, Instagram Reels, TikTok, X,
314
+ LinkedIn, Master, Broadcast (ProRes 422 HQ), Web, Archive (lossless FFV1) and
315
+ Custom. Every setting is a control: resolution and reframing (crop, fit or
316
+ stretch to another shape), frame rate, codec (H.264, HEVC, AV1, VP9, ProRes,
317
+ DNxHR, XDCAM HD422, FFV1) and hardware encoder, CRF / VBR / CBR, max rate,
318
+ keyframe interval, pixel format, color, container, audio codec, bitrate and
319
+ rate, loudness normalization (−14 / −16 / −23 / −24 LUFS with a true-peak
320
+ ceiling), a file-name template and metadata. Save your own presets; the
321
+ dialog warns about upscaling, crops and platform limits before it renders,
322
+ and every finished file is read back and checked — size, frame rate, frame
323
+ count, codecs, length, loudness, true peak and file size
324
+ - **Batch export**: one project → many files (master, YouTube 1080p, a 9:16
325
+ reel, a square, a 720p preview…) from **one render per frame rate and
326
+ range** (up to 8 outputs each) — every frame is drawn once and each output's ffmpeg
327
+ encodes it. Batch outputs are saved with the
328
+ project; an **export queue** runs them back to back with progress, stop,
329
+ retry and open
297
330
  - Fast export: browser renders every frame + an offline audio mix; ffmpeg
298
- encodes **JPEG frames** via an **encoding profile** from
331
+ encodes them with a delivery preset, or a raw **encoding profile** from
299
332
  `encoding-profiles.json` (keeps rendering if you switch tabs). Encode and
300
333
  upload run ahead of the compositor so a fast timeline is not stalled by
301
- `toBlob`. The Export dialog has a profile selector; pin a project default
302
- with `encodeProfile` in `project.json`
334
+ `toBlob`
303
335
  - WebCodecs export: the browser HW-encodes Annex-B H.264; the server
304
336
  stream-copies and muxes audio. Faster uploads; bitrate/VBR-CBR in the
305
337
  Export dialog. Unavailable while an export frame is set (use Fast)
306
338
  - Realtime MediaRecorder fallback when ffmpeg or WebCodecs isn't available
307
339
  - Agents export too: `fablecut_export` runs the Fast export in the open editor,
308
- or in a headless Chrome / Edge the server starts, and returns the file path
340
+ or in a headless Chrome / Edge the server starts, and returns the file path;
341
+ `preset` and `delivery` pick and adjust a delivery preset; `outputs` makes
342
+ several files in one go
309
343
  - Export **Range** dropdown: Entire timeline or IN–OUT (defaults to IN–OUT when
310
344
  markers are set). Effective IN/OUT export bounds are clamped to `projDur()`.
311
345
 
@@ -412,7 +446,7 @@ Three equivalent control surfaces:
412
446
 
413
447
  Tools: `fablecut_status` (auto-starts the editor), `fablecut_docs`,
414
448
  `fablecut_get_project`, `fablecut_set_project`, `fablecut_patch_project`,
415
- `fablecut_import_media`, `fablecut_analyze_reference`,
449
+ `fablecut_import_media`, `fablecut_analyze_reference`, `fablecut_frame`,
416
450
  `fablecut_encode_profiles`, `fablecut_normalize_audio`, `fablecut_auto_duck`,
417
451
  `fablecut_denoise`, `fablecut_export`, `fablecut_scopes`, `fablecut_track`.
418
452
 
@@ -441,6 +475,15 @@ my clips, same music"*. The agent calls `fablecut_analyze_reference`, gets the
441
475
  blueprint (cuts, beats, BPM, energy, drop, extracted music), and rebuilds the
442
476
  structure shot-for-shot with your footage.
443
477
 
478
+ **The agent can see, not just measure.** `fablecut_analyze_reference` gives it
479
+ the *ears* — where the cuts land, which shots hit hard. `fablecut_frame` gives it
480
+ the *eyes*: ask for any moment of any clip (`{path, t:3.2}`) and the call comes
481
+ back with an actual image the model can look at, or ask for a 12-frame contact
482
+ sheet to survey a whole clip in one call (`{path, frames:12}`). Pass a `time`
483
+ with no `path` and it renders the composed timeline — graded, all tracks, exactly
484
+ as export would. So footage gets chosen by looking at it rather than guessing
485
+ from a number, and a grade gets checked against the real picture.
486
+
444
487
  **Conflict-safe concurrent editing**: the UI, the MCP tools, and direct
445
488
  `project.json` writes all agree on a `revision` counter. If you edit a clip in
446
489
  the UI while an agent is mid-task, the agent's next write is rejected (409 from