Edit Session
SkillDev toolsLoaded after a draft is ready for back-and-forth editing: cutting, overlay adjustments, timing fixes. Not a pipeline step — this is the interactive editing reference.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Edit Session skill
What this skill tells your AI
The instructions your AI receives, as published by thesampadilla/montaj in skills/edit-session/SKILL.md and read by ahel’s review.
Load this skill when the draft is ready and the user wants to make refinements — cuts, overlay changes, re-timing, new overlays. This is not a workflow step; it covers the tools and conventions for iterative editing.
After every file change: push a UI refresh
After writing project.json or any overlay .jsx file, call the reload endpoint so the UI updates without a manual refresh:
curl -s -X POST http://localhost:3000/api/projects/{project_id}/reload
The file watcher is unreliable under load. Always call this explicitly after changes.
Before editing anything: verify the transcript
Always check that the transcript matches the current clip before using timestamps.
The clip filename tells you which transcript to use. For a clip at:
IMG_4900_fillers_concat_cut_cut.mp4
The transcript must be named after that exact file — e.g. IMG_4900_fillers_concat_cut_cut.json. If only an older transcript exists (e.g. IMG_4900_fillers_concat.json), it is stale — its timestamps come from a pre-cut file and will not align with the current video.
Re-transcribe when in doubt:
montaj transcribe "/abs/path/to/clip.mp4" --model base.en
Output: clip.json (word-level timings) and clip.srt. Always read the fresh clip.json before deriving overlay or cut timings.
Cutting
Cuts are pure JSON. Nothing re-encodes until the final render.
This is the single most important thing to know about editing a v4 draft: a cut, a split, a ripple delete and a trim are all rewrites of tracks and captions in project.json. No intermediate file is produced, no clip file changes, and every edit is reversible by editing the JSON back. The helpers live in montaj_assets/editor/src/video/cuts.ts and the operator reaches them from the timeline; you reach the same outcome by writing the same JSON.
The four shapes, and what each does to the timeline:
| Edit | Effect on tracks[0] | Gap left behind? |
|---|---|---|
| Trim an edge | move one item's start or end, and its matching inPoint/outPoint by the same amount | yes, unless ripple is on |
| Split at time t | one item becomes two adjacent items with no gap; inPoint/outPoint divide at the same source time | no |
| Lift a range out of a clip | the clip's remaining fragments keep their positions | yes, renders as black + silence |
| Ripple delete | as lift, then every later item shifts left to close the gap | no |
Rules that are easy to get wrong:
end - startmust equaloutPoint - inPointfor a clip at normal speed, andoutPoint - inPoint === speed × (end - start)for one with aspeedset. Moving a timeline edge without moving the source point stretches the clip past the footage it has: the picture runs out partway along and freezes on its last frame while the waveform underneath draws the real, shorter stretch. Change both, always.- Never trim an edge past the source. An
inPointbelow0or anoutPointbeyond the source's duration is the same fault by a different route. - Overlay tracks are absolute.
tracks[1+]items carry timeline positions that do not move when you cuttracks[0], which is deliberate — an overlay is meant to sit over whatever ends up underneath it. If a cut should carry its overlays with it, move them yourself. - Captions follow the primary track. A cut that removes a span of
tracks[0]must remove or shift the caption segments inside it, or the words desynchronise from the picture. srcnever changes. Cutting does not produce a new file, so the item keeps pointing at the original source — and keeps itsproxySrcandnormalizedSrcwith it. If you rebuild an item from scratch instead of editing it in place, copy those two fields across or you will silently disable the preview engine.
When you do need a real file — remove_bg needs one, for instance — that is materialize_cut, and it is a deliberate step out of the non-destructive model, not part of ordinary cutting:
montaj materialize-cut clip.mp4 --inpoint 2.0 --outpoint 8.0 # keep one range
montaj materialize-cut clip.mp4 --cuts '[[1.0,3.5],[12.4,14.0]]' # remove ranges, one pass
montaj materialize-cut spec.json # apply every keep in a trim spec
After a materialize the clip file changes. Re-transcribe if you need to update overlay timings.
Overlays
What overlays can do
- Transparent overlays — float over footage. Position with
position: absolute, leave root background alone. - Opaque overlays (
"opaque": truein project.json) — replace the video frame entirely. Root CSS controls the full frame. Good for title cards, stat cards, full-screen animations.
Timing overlays from transcript
Read word-level timings from clip.json. Each word has a start and end in seconds relative to the current clip. Use these to set overlay start/end in project.json and to compute per-item stagger delays inside the JSX.
Example: if "landing" appears at t=16.70s in the transcript and the overlay starts at 16.52s, the item's trigger frame inside the component is:
const ITEM_FRAME = Math.round((16.70 - 16.52) * fps) // 0.18s * fps
tracks layout
{
"tracks": [
{
// Track 0 — primary footage track
"id": "trk-0",
"items": [
{ "id": "clip-0", "type": "video", "src": "/abs/path/clip.mp4", "start": 0.0, "end": 0.0 }
]
},
{
// Track 1 — background layer (opaque sections, backgrounds)
"id": "trk-1",
"items": [
{ "id": "...", "type": "overlay", "src": "/abs/path.jsx", "start": 0.0, "end": 5.0, "opaque": true }
]
},
{
// Track 2+ — renders on top of track 1
"id": "trk-2",
"items": [
{ "id": "...", "type": "overlay", "src": "/abs/path.jsx", "start": 2.0, "end": 4.0 }
]
}
]
}
- No time overlaps within a single track.
- Use separate tracks for simultaneous overlays at different z-levels.
- Always use absolute paths for
src.
Icons
Use icons instead of emojis unless the user explicitly asks for emojis.
Phosphor Icons — Ph
All icons available as Ph.<Name>. Browse at phosphoricons.com.
<Ph.CheckCircle size={52} weight="fill" color="#34d399" />
<Ph.Lock size={48} weight="fill" color="#f87171" />
<Ph.X size={200} weight="bold" color="#ff1a1a" />
Weights: regular (default), bold, fill, duotone, light, thin.
Font Awesome — FaIcon + FaSolid / FaBrands
FaIcon is the renderer. Use FaSolid for general icons, FaBrands for brand logos (GitHub, YouTube, X/Twitter, TikTok, etc.).
<FaIcon icon={FaSolid.faBolt} style={{ fontSize: 48, color: '#fbbf24' }} />
<FaIcon icon={FaBrands.faGithub} style={{ fontSize: 48, color: 'white' }} />
Which to use
- Phosphor — default choice. Cleaner API, consistent weight, 9000+ icons.
- Font Awesome Brands — brand logos only (Phosphor doesn't have brand icons).
- Font Awesome Solid — fallback for anything Phosphor doesn't cover.
All four globals (Ph, FaIcon, FaSolid, FaBrands) are injected automatically in both the browser preview and the render pipeline. No imports needed.
JSX authoring quick reference
All overlay JSX has these globals injected — no imports, ever:
| Global | Description |
|---|---|
frame | Current frame (0 → duration-1) |
fps | Output frame rate |
duration | Total frames this overlay is visible |
props | The props object from project.json |
interpolate(frame, inputRange, outputRange, opts?) | Linear mapping with clamp |
spring({ frame, fps, stiffness?, damping?, mass? }) | Physics spring → 0..1 |
Ph | All Phosphor icons |
FaIcon | FontAwesome renderer |
FaSolid | FA Solid icons |
FaBrands | FA Brand logos |
Rules:
- Default export only
- No hooks (
useState,useEffect) - No CSS
animationortransition— all motion fromframe - Transparent root by default; opaque overlays set
backgroundon the root - Never apply
transform: translate/scaleto the root element - Animations must complete before the overlay ends (last frame is held)
- ALL code that references
fps,frame,duration, orpropsMUST be inside the component function body. Top-level module code runs before these globals are injected in the render path (esbuild IIFE) and will throwReferenceError. Style constant objects that don't reference these globals (e.g.const LABEL_STYLE = { fontSize: 42, ... }) are fine at the top level.
Signals
- GitHub stars
- 25
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
edit-session- Source
- github.com/thesampadilla/montaj