premiere-pro

The PRIMARY finishing path — hand the rough cut to Adobe Premiere Pro, where you do the real editing and project work. Rebuilds the rough cut AS separate, trimmable clips on the Premiere timeline — it

SKILL.md

GitHub

Video Studio: this skill lives in engines/legacy-editor. Before running anything from it, cd engines/legacy-editor and use that folder's copy (engines/legacy-editor/.claude/skills/premiere-pro/), which has its scripts and reference files. Every path below is relative to engines/legacy-editor. This copy only lets Claude find the skill from the Video Studio folder.

To Premiere — Rough Cut Handoff

The primary path (2026-07-27 — not an "off-ramp"). The standard process for all future videos is that edits and project work happen inside a video editing app, and Premiere is the default one. After the rough cut (pipeline step 2), this is where the job normally goes: drop the cut into a ready-to-edit Premiere project and the real work continues there. HyperFrames stays the graphics engine feeding this timeline (alpha overlays) and the chat-only lane for clients without an editing app.

It does exactly one thing: get the cut onto a Premiere timeline, ready to edit. Nothing more — no graphics, no captions, no render. Those happen in Premiere now.

The cut is rebuilt AS cuts — not dropped in as one flat clip. This is the whole point of the hand-off. The rough-cut skill already decided every edit (which raw clip, in/out points) and saved it as an EDL (cuts.json). This skill replays that EDL against the raw footage: each kept segment becomes its own clip on V1, end-to-end, landing at the exact same edit points the rough cut produced. So you open Premiere to the same tight cut you approved — but every boundary is a real edit point you can ripple, slip, slide, or re-trim against the original takes. Importing the flattened outputs/<job>.mp4 as a single clip is the fallback, not the path (see Fallback below).

When to use

  • you say any trigger phrase above, OR
  • A rough cut exists and you want to finish manually in Premiere instead of HyperFrames.

If there's no rough cut yet, run rough-cut first with RENDER=0 on the splice — the EDL + canonical transcript generate in ~8 seconds without a flat render, and this skill consumes nothing else. Never render a flat MP4 for a Premiere-finish job (2026-07-22 — a 4K render gated the handoff by 20+ minutes for zero benefit). Time-to-timeline = transcription + cuts + seconds. (You can import raw footage instead if you explicitly ask, but the point of this handoff is the tightened cut.)

What it needs first — the bridge must be live

This skill drives Premiere through the premiere-pro MCP server (wired in .mcp.json). That server only does anything when Premiere is open with the bridge panel running.

Engine (migrated 2026-07-18): vendor/premiere-mcp/ — a pinned local clone of hetpatel-11/Adobe_Premiere_Pro_MCP (pinned at commit cc57eb4, 2026-07-14; ~280 tools). The old premiere-pro-mcp npm package was a knockoff republish of this repo (its "official repo" link 404s) with a broken hand-rolled CEP CSInterface shim — the all-null bridge failure of 2026-07-09 was knockoff damage, not a Premiere bug. The clone IS the pin: update only via deliberate git pull + re-run the verification pass + record the new hash here. Rebuild after pull: npm install && npm run build in vendor/premiere-mcp/, re-copy cep-plugin/ to the CEP extensions dir if it changed, then restart Premiere.

  1. MCP tools available? The premiere-pro tools (mcp__premiere-pro__*) load at Claude Code session start. If you don't see them, the .mcp.json was added after this session began — tell you to reconnect/restart Claude Code once, then retry. No MCP tools at all (session rooted outside this repo)? Drive the bridge headless instead: node workflows/premiere-bridge.mjs <tool> '<json>' calls the same tool handlers directly via the file bridge — same panel requirement, no MCP session needed. It also batches the whole step-7 replay in one command.
  2. Bridge connected? Call ping first. If it errors or times out, the panel isn't running. Tell you, in one line:

    Open Premiere Pro → Window → Extensions → MCP Bridge (CEP) → click Start Bridge (temp dir defaults to /tmp/premiere-mcp-bridge). Then retry ping. Don't proceed until it returns ok.

The temp dir in the panel must match PREMIERE_TEMP_DIR in .mcp.json (/tmp/premiere-mcp-bridge) — that shared folder is the bridge. Protocol is command-<id>.json/response-<id>.json files (the knockoff used cmd_*.jsx/res_*.json — old panel and new server can NOT interoperate).

Procedure

  1. Resolve the job. Newest folder under projects/ unless you named one.
  2. Locate the EDL + raw footage. The EDL is projects/<job>/transcript/cuts.json (persisted durably by splice.sh); if it's missing there, check /tmp/video-editor/<job>/cuts.json. The raw clips are in projects/<job>/raw/. Both must exist — they're what we replay. If cuts.json is gone from both spots, you can't rebuild the cuts; take the Fallback path instead and tell you why (the EDL wasn't persisted for this older job).
    • Read cuts.json. It's { "segments": [ { "clip": "clip-01.mov", "start": 1.24, "end": 4.60, "transcript": "..." }, ... ] }. Segment order = timeline order. start/end are seconds in the raw clip (raw timeline), which is exactly what the Source Monitor in/out points take.
  3. Preflight the bridge (ping) per above.
  4. Create the project at projects/<job>/premiere/<job>.prproj (create_project). If it already exists, open_project it instead of clobbering your in-progress work — ask if unsure.
  5. Import the raw clips (import_media) — every distinct clip referenced in cuts.json, from projects/<job>/raw/. (Not the flattened outputs/<job>.mp4 — that's the fallback only.) Note each imported item's id/name for the Source Monitor. New-engine arg (2026-07-18): import_media takes filePath (a single string) — one call per file. The old file_paths array form was the knockoff.
  6. Build a matching sequence. On Premiere 26.x create_sequence without a preset is broken (createNewSequence now needs a second arg → "Not Enough Parameters"). The working path: create_sequence_from_clips with the imported raw clip (or DOM-direct via execute_extendscript: app.project.createNewSequenceFromClips(name, [item]) + app.project.activeSequence = seq — verified 2026-07-18) — the sequence inherits the footage's exact dimensions/framerate — then empty the timeline before the replay. remove_selected_clips returns available:false on the new engine — clear via ES instead: loop every video/audio track backwards, track.clips[i].remove(false, false). Confirm with get_sequence_settings (e.g. 1920×1080, videoFrameRate 0.0417s = 23.976).
  7. Replay the EDL onto V1 — one segment at a time, in order. This is three-point editing, the same move a human editor makes. For each segment:
    1. open_in_source the segment's raw clip (item_id = the imported clip's id/name).
    2. set_source_in_out with in_seconds = segment.start, out_seconds = segment.end.
    3. overwrite_from_source onto video_track_index: 0 (V1).
    • The playhead does NOT auto-advance on Premiere 26.3 (verified 2026-07-17) — every overwrite lands at the current playhead, so without advancing it each segment clobbers the last (symptom: final duration = longest segment, fragment clips). Before EACH overwrite, set_playhead_position to the cumulative sum of prior segment durations (frame-snap the sum at the sequence fps), and read get_playhead_position back to confirm it moved. The param is time_seconds — a wrong key (e.g. seconds) interpolates undefined into the ExtendScript and silently no-ops; bridge handlers don't validate args, so always read state back after a write.
    • Easiest: run the whole loop in one command — node workflows/premiere-bridge.mjs replay projects/<job>/transcript/cuts.json <clipName> does set-playhead → in/out → overwrite per segment with the playhead check built in, then saves.
    • setInPoint/setOutPoint FLOOR at tick conversion (measured 2026-07-18): frame-grid times are repeating decimals, and a double that serializes a hair below the exact tick value lands one frame EARLY (one out-point came up a full frame short on an on-grid EDL). The replay command nudges every boundary +1e-4s (0.24% of a frame — floor resolves to the intended frame, off-grid EDLs unaffected); do the same in any hand-written ExtendScript that sets in/outs from EDL seconds, and always verify with get_sequence_structure against the EDL afterward.
    • Result: N separate clips on V1, each a real edit point. you can ripple/slip/slide/re-trim any of them against the full raw take — the original heads and tails are still there in the source, just trimmed off by the in/out. Verify with get_sequence_structure: clip N's startSeconds == clip N-1's endSeconds (no gaps), and each clip's inPointSeconds/outPointSeconds within one frame of the EDL.
  8. Save (save_project) and confirm: report the .prproj path, the segment count on V1, and that the rough cut is rebuilt as individual edits ready for you to fine-tune in Premiere.

Fallback — flat clip import

Only when the EDL is unavailable (old job, cuts.json lost from both durable and /tmp spots) or you explicitly ask for "just the flat cut as a reference":

  1. import_media the outputs/<job>.mp4.
  2. create_sequence matching its dimensions/framerate.
  3. add_to_timeline at the start of V1 (one clip).
  4. Save and tell you it's the flattened cut — the individual edit points aren't recoverable, so re-trims mean razoring by eye. If you want real edit points, re-run rough-cut so a fresh cuts.json gets persisted, then hand off again.

Graphics in Premiere — the full run (validated 2026-07-18, premiere-pipeline-test)

Read workflows/premiere-graphics.md first — the playbook distilled from the shipped your-job intro: standard track stack (V1 EDL footage / V2 alpha graphics / V4 adjustment layers / V5 grain / A3-A5 SFX rails), the behind-talent sandwich→nest pattern, graphics clip lifecycle, keyframe laws, and the film-feel finishing stack. This section below is the original per-clip mechanics.

When you ask for graphics ON the Premiere timeline (not just the rough-cut handoff), the whole pipeline-step-3 flow runs here — plan → build → place → bake → QA — with every graphic left hand-editable (separate V2 clips + real keyframes):

  1. Plan with graphics-plan as usual (long-form liquid-glass language). Anchor beat times to the refined EDL / canonical transcript word timestamps.
  2. Author comps in projects/<job>/hf-graphics/compositions/ and render ProRes 4444 alpha via render.sh <id> (--format mov) — durable copies land in ../assets/. Comp gotchas live in workflows/incremental-graphics.md (required reading; note especially: ALL CSS inline, no <link>).
  3. Place overlays on V2: import_media each mov (filePath, one call each), then one ES loop: seq.videoTracks[1].overwriteClip(item, startSeconds + 1e-4) (the tick-floor nudge). Alpha composites natively — no keying, no effects needed.
  4. Bake footage moves on V1 (they mirror the comp's motion): the zoom command for punch-ins; custom ES for reframe pushes — per-frame keys on Transform Scale H+W and Position (normalized [x,y] arrays), all key times in source-clip time (clip.inPoint.seconds + offset). A move that crosses a V1 edit is split: first clip pushes in and HOLDS to its tail, next clip starts held and releases (frame-aligned local key offsets on each).
  5. QA every graphic from the real program render: node workflows/premiere-bridge.mjs frame '{"time":<sec>,"out":"/abs/path-no-ext"}' — full composite (keyframes, alpha overlays, unsaved state) with zero AME involvement. Iterate: edit comp → render.sh <id> (media auto-relinks by path; mtime change invalidates Premiere's cache) → re-grab → done.
  6. Watchable preview only when needed — NEVER via AME (your hard rule, 2026-07-20): export_sequence queues into AME, so it is off-limits for everything (full exports, QA bounces, audio checks — all of it). Render through Premiere itself instead: sequence.exportAsMediaDirect(outPath, presetPath, workAreaType) via execute_extendscript (synchronous, in-Premiere, system .epr presets like .../systempresets/4E49434B_48323634/YouTube 1080p HD.epr or the WAV preset under 3F3F3F3F_57415645/ still apply — verify the export on first use, it's untested on the new engine). Frame QA stays the bridge frame command; often the right move is no render at all — read state back and let you play the timeline.

Engine arg quirks measured on this run: refresh_media needs projectItemId (skippable — replaced media re-reads by path), export_sequence REQUIRES presetPath (.epr), remove_selected_clips unavailable, import_media takes filePath singular.

Notes

  • Getting the cut onto the timeline is where this skill's job ends — what happens after (final color, export) is your own Premiere work, by design.
  • Keep it surgical: import raw + matching sequence + the EDL replayed onto V1. Graphics only via the section above when asked.
  • Why rebuild from the EDL instead of importing the flat MP4? So the cuts are real edit points, not baked pixels. A flattened clip forces you to razor by eye to adjust any boundary; replaying the EDL gives you every cut as a trim handle with the full raw take behind it. This is the standard hand-off for every app-finish job: the editor-grade hand-off.
  • Audio rides along. overwrite_from_source lays the clip's audio onto A1 in lockstep — no separate audio step. The EDL's segments stay A/V-synced because in/out points apply to the source clip as a whole.
  • Section color-coding: set the project item's label BEFORE the replay (item.setColorLabel(idx), 0–15: Violet Iris Caribbean Lavender Cerulean Forest Rose Mango Purple Blue Teal Magenta Tan Green Brown Yellow). Timeline clips STAMP the label at insert time and track items expose no label API at all (verified 2026-07-23: DOM trackItem has zero label/color methods, QE only setBorderColor, the MCP's select_clips_by_color is a stub) — changing the item's label later does NOT recolor laid clips; the only scripted recolor is wipe + re-replay. Label NAMES lie about rendered color (2026-07-31): Caribbean renders olive/lime green (nearly collided with an existing Green section), Rose failed at ΔRGB 14.5 from Magenta, Teal sat Δ≈31 from both Iris and Green. Never pick by name: read every laid item's getColorLabel(), test the candidate with ONE throwaway clip (setColorLabel → single overwriteClip → window-grab.sh → sample the V1 row; the RGB distance to each laid section must beat the tightest existing pair), then remove the test clip. Reach for the missing hue family, never a neighbour of an existing band.
  • No audio work during a rough-cut handoff (2026-07-30): no Amplify, no clip gain, no level-matching between sections, even when one reads several dB quiet. one audio pass runs over the whole video at the end; measuring levels as information is fine, acting on them is not.
  • ES audio effect params are normalized 0..1 over the slider range (2026-07-30): Amplify Left/Right dB = v*144 − 96 (range −96..+48; beware: the wrong guess −48..+24 ALSO puts 0 dB at 0.66667, so a 0 dB sanity check cannot distinguish them, and a +7 wrote as +14 in the UI before this was caught). Hard Limiter Maximum Amplitude: v = (dB+100)/100 (range −100..0). For any NEW param, verify the domain by writing a NON-zero, off-center value and reading the UI or a frame grab back before batch-applying; two candidate ranges can agree at one point.
  • Job projects live at projects/<job>/premiere/<job>.prproj, always. Never let a project be born from Premiere's own save dialog — its default location is the rotating Auto-Save folder, which Premiere prunes (two real job projects were rescued from there 2026-07-17). Superseded drafts go in premiere/archive/. A project that ended up in the WRONG job folder relocates in ONE call: ES app.project.save(); app.project.saveAs(newPath) — verified 2026-07-29 moving your-job.prproj out of your-job/premiere/ with the project open and a live timeline: app.project.path switches, all media stays online (media paths are absolute), scratch/Auto-Save follow to the new folder on the next save, and the CEP bridge survives. This supersedes the cp + openDocument + closeDocument dance below for relocations (that dance is still the only route if saveAs ever regresses — and note the DOM saveAs is NOT the same as the save_project_as MCP tool, which is still untested). Delete the stale copy at the old path afterward.
  • Engine verification pass (2026-07-18, new engine on Premiere 26.3): FIXED vs the knockoff: list_available_effects (real list), apply_effect (works — args are clipId/effectName), undo (works, verified removing an applied effect), export_frame (works via QE — args sequenceId/time/outputPath/format; it APPENDS the format extension to outputPath, so pass a path without one). ⚠ but its TIME argument is broken (root-caused 2026-07-18): use node workflows/premiere-bridge.mjs frame '{"time":<sec>,"out":"/abs/path-no-ext"}' instead. QE's exportFramePNG accepts ONLY a timecode STRING: numeric seconds THROW ("Illegal Parameter type"), so the vendored tool's fallback chain lands on String(seconds), which QE parses as a FRAMES timecode — time: 18 silently exports frame 18 (~0.75s), not 18s (a ticks string is equally wrong). Fed a proper HH:MM:SS:FF timecode, the QE grab renders the FULL current program state — keyframed effects, fresh unsaved keys, and alpha overlay tracks all composite correctly (verified against an AME export of the same frame) — so it IS the right QA tool, no AME needed. The bridge frame command does the seconds→timecode conversion off the active sequence's fps. export_sequence works (system .epr, queues in AME asynchronously) but is BANNED — AME is off-limits (house rule, 2026-07-20); use exportAsMediaDirect via ES when a render is truly needed. Args are zod-validated — wrong keys fail loudly with the expected schema, so the silent-no-op class is dead; note arg names vary per tool (clipId vs node_id — the validator tells you). --desc prints raw zod internals; read the schema in vendor/premiere-mcp/src/tools/index.ts instead. STILL OURS: add_keyframe writes raw SOURCE time exactly like the knockoff (raw API behavior — keep computing inPoint + offset; the zoom command does). UNTESTED on the new engine: save_project_as, create_bars_and_tone, delete_project_item — verify before first use. remove_effect is intentionally absent from their catalog and no-arg QE removeEffects() returns true but removes NOTHING — but real removal IS scriptable (found 2026-07-23, batch-removed 126 DeNoise instances): QE item.getComponentAt(i) returns a live component whose methods include .remove() — match comp.name, iterate indices BACKWARDS (they shift on removal), then verify survivors via DOM clip.components. Sibling effects and their values survive. (DOM components expose no removal — .remove() exists only on the QE side; a component's bypass checkbox is DOM properties[0], the unnamed boolean, when a soft-off is enough.) The CSInterface re-patch instruction is OBSOLETE — the new panel ships Adobe's real CSInterface.
  • Premiere 26.3 bridge quirks (2026-07-17, measured on the OLD knockoff engine): save_project_as is fully broken — it returns saved: true but writes NOTHING and doesn't switch the open project (never trust it; verify with app.project.path + ls). To relocate a project: cp the saved .prproj on disk to the new path under a DIFFERENT filename, app.openDocument(newPath) (opens as a second project and becomes active), then closeDocument the old one — Premiere refuses to open a path whose filename matches an already-open project, and closing the LAST open project drops Premiere to the Home screen and KILLS the CEP bridge (the editor must reopen a project, then Window → Extensions → MCP Bridge → Start Bridge), so always keep one project open through the swap. export_frame/capture_frame (exportFramePNG gone), create_bars_and_tone, and delete_project_item (deleteProjectItem gone) are all broken on 26.3 — verify visually via the program monitor instead, and have the editor delete panel items by hand. undo (app.project.undo is not a function) and multiple_undo (silently returns undone: 0) are broken too — there is NO undo through the bridge, so revert experiments with inverse writes (e.g. remove_keyframe_range + setTimeVarying(false) via execute_extendscript to clear a stopwatch). Motion-effect control verified working 2026-07-17: add_keyframe/get_keyframes/set_keyframe_interpolation/remove_keyframe_range on Motion (Position/Scale/Rotation/Anchor Point/Crop) and Opacity all land and read back correctly. apply_effect and list_available_effects are broken on 26.3 ("Effect not found" / empty list) — apply effects via execute_extendscript QE instead: app.enableQE(); item.addVideoEffect(qe.project.getVideoEffectByName("Transform")) (walk track.getItemAt(i) for type === "Clip"). Once applied, the normal keyframe/property tools drive it fine — verified with a Transform zoom-out + motion blur (keyframe Scale Height AND Scale Width identically instead of trusting Uniform Scale; crank Shutter Angle, e.g. 360 — measured 2026-07-17 on 26.3: blur renders with Use Composition’s Shutter Angle (U+2019 curly apostrophe in the name) checked OR unchecked, so don't fight over that box). Clip keyframe times are in SOURCE-CLIP time, not clip-relative time (bit hard 2026-07-17): add_keyframe's time_seconds says "relative to clip start" but passes straight through to addKey, whose domain starts at the media's frame 0 — on a trimmed clip the keys land before the in-point: invisible in Effect Controls, stopwatch on, no motion (value sits at the last key). Always add keys at clip.inPoint.seconds + offset (frame-snap the offset at 1001/24000 for 23.976), and use the same domain for get_keyframes/set_keyframe_interpolation/remove_keyframe_range. Read keys back with prop.getKeys() via execute_extendscript when the UI and API disagree. set_keyframe_interpolation only maps linear/hold/bezier; the ExtendScript enum for prop.setInterpolationTypeAtKey(timeObj, mode, 1) is 0=Linear, 1=EaseIn(obsolete), 2=EaseOut(obsolete), 3=Hold, 4=Bezier, 5=Time — on 26.3 the obsolete ease modes return success but SILENTLY WRITE LINEAR (verified 2026-07-17: linear keys confirmed in the UI). The Ease In/Out right-click presets are NOT reachable via scripting; to script an ease, BAKE it — one key per frame sampled off the easing function (e.g. 100 + 30*Math.pow(1-t,3) for a decelerating landing), all keys linear; renders identical to the preset. Baking is THE standard method for scripted motion shapes (locked 2026-07-17): any curve works (GSAP-style eases, overshoot/settle, spring — shapes handles can't make), one ExtendScript call per clip, works on any numeric property. Trade-off: baked moves aren't hand-tweakable — adjust by re-baking from feedback; lay down 2-key bezier instead when the editor wants to sculpt the curve by hand. The opening-zoom preset is a command: node workflows/premiere-bridge.mjs zoom '{"clip":0,"from":130,"to":100,"frames":8,"ease":"cubic"}' (all opts optional; ease: cubic/overshoot/linear) — applies Transform + shutter 360 if missing, bakes, saves. Native .prfpset presets can NOT be applied via scripting — they're for hand editing only. Curve HANDLE geometry is also not readable/writable — hand-sculpted eases are invisible to scripting.
  • Adjustment layers: scripting can't CONSTRUCT one, but it can MINT one from the repo template (solved 2026-07-22). No constructor exists anywhere in the scripting surface: QE has none (full qe.project method list reflected — only bars/black/matte/transparent/leader), the MCP's add_adjustment_layer and export_as_project are accepted-but-no-op stubs, and a hand-crafted FCP XML with <mediaSource>AdjustmentLayer</mediaSource> imports as an OFFLINE ghost (red Media-Offline slate) — the real serialization is an <AdjustmentLayer>true</AdjustmentLayer> flag, not a mediaSource. The scripted route: app.project.importSequences("<repo>/workflows/premiere-templates/adjustment-layer.prproj", ["b2440d0d-b8dc-4899-adf9-40cf306269a6"]) — silent (no import dialog), returns true, and lands a working ONLINE "Adjustment Layer" item at project root plus a junk "adj-template" sequence (delete the sequence, keep the item; verified: places, takes effects, renders passthrough). The template was built by placing a UI-born adjustment layer on a scratch sequence and calling sequence.exportAsProject(path) (the ES method works; the MCP tool of the same name is the stub). If the template is ever lost, rebuild it the same way from any project containing an adjustment layer — only the very first one ever needs the UI. Once the item exists: overwriteClip to place, trim via c.end, QE addVideoEffect, property writes in display units (read back to confirm). Its inPoint sits at ~3600s (infinite synthetic media), so clip keyframes go at inPoint + offset like any trimmed clip. Film-feel recipe validated on your-job: Lens Distortion (slight negative Curvature, Fill Alpha on) + VR Chromatic Aberrations (defaults ±10 are glitch-loud; start small) + a position random-walk wiggle on twos. The wiggle MUST live on a Transform EFFECT, never intrinsic Motion (caught 2026-07-22): on an adjustment layer, Motion transforms only the layer's own bounds — visually a silent no-op — while the Transform effect warps the composite passing through (verify any such assumption with an exaggerated offset + frame grab before baking 500 keys). Transform Position is normalized like Motion's; set Scale Height AND Width explicitly (uniform toggle untrusted) to ~101 to hide the shifted edges; QE track items include gaps, so walk getItemAt(i) for type === \"Clip\" — a head-trimmed layer's clip is NOT item 0.
  • The MCP exposes 266 tools (track targeting, effects, markers, export, etc.). Run get_project_info to discover state when you need more than the basic handoff.
  • Engine: vendor/premiere-mcp (pinned clone, see the bridge section above) → CEP/ExtendScript file bridge. Reinstall the panel by re-copying vendor/premiere-mcp/cep-plugin/ to ~/Library/Application Support/Adobe/CEP/extensions/MCPBridgeCEP/ + restarting Premiere. The clone is gitignored (machine-local, like .mcp.json); restore = git clone + checkout the pinned hash + npm install && npm run build.