Alchemy Premiere Plugin — Spec¶
Status: Draft v1 (2026-07-18) Decision: UXP panel (not CEP). Cookbook stays the source of truth; the panel is a thin client of the Alchemy server.
1. Summary¶
Premiere becomes a first-class Alchemy DCC (premiere in the {dcc}.{verb}.{task}.{op}
etymology). A single UXP panel — the Alchemy panel — renders the standard alchemy
menu (Build / Art / Render / Review / Publish / Refresh) from the cookbook, exactly like
the Blender menu system. The difference from Blender is where steps execute:
- Premiere has no Python interpreter, so cookbook steps route by their
languagefield: javascriptsteps run inside the panel (UXPeval, with the Premiere DOM in scope).pythonsteps run server-side in the Alchemy FastAPI app, which already has the venv,PathObject, MSD, publish spine, etc.- The panel talks to the Alchemy server over localhost HTTP (
http://127.0.0.1:8080).
This is possible because Premiere 25.6 shipped UXP as GA with two manifest permissions
that unlock the pattern: allowCodeGenerationFromStrings (legal eval() of
cookbook-served JS) and network.domains (localhost HTTP/WebSocket). CEP is deprecated
(~1 year of side-by-side support from 25.6); we do not build on it.
2. Why not CEP (recorded for posterity)¶
CEP would give us Node child_process (spawn Python directly) and evalScript. But CEP 12
is the last version, Adobe has announced removal after a transition year, and every CEP
line written in 2026 is a forced rewrite in 2027. UXP's missing child_process doesn't
hurt us: the "companion process" workaround everyone uses is a local service — and the
Alchemy server is that service. If we ever need in-process compute, UXP offers C++
hybrid plugins as an escape hatch.
3. Architecture¶
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ Premiere Pro (25.6+) │ │ Alchemy server :8080 │
│ ┌─────────────────────────┐ │ HTTP │ /api/premiere/* │
│ │ Alchemy UXP panel │◄├───────►│ - GET menus (cookbook) │
│ │ - renders menu/shelf │ │ │ - POST run_step (python) │
│ │ - evals js steps │ │ │ - POST context (grounding) │
│ │ - premierepro DOM │ │ WS │ - POST events (telemetry) │
│ │ - ws command channel* │◄├───────►│ /ws/premiere (*phase 2) │
│ └─────────────────────────┘ │ │ │
└─────────────────────────────┘ └───────────┬──────────────────┘
│ imports
cgl-cookbook/cookbook/premiere/*
Three layers, same as every other DCC:
- Cookbook (
cookbook/premiere/…) — recipes own what the buttons are and what each step does. Already scaffolded: premiere/menus/alchemy/— the standard alchemy menu (Build, Art, Render, Review, Publish, Refresh) +alchemy.json.premiere/render/edt/— first real recipe:EnsureAlchemyPath→SetRenderPath→RenderTimeline.premiere/shelves/alchemy/— empty for now; becomes the icon-strip row.- Alchemy server — serves resolved recipes to the panel, executes
pythonsteps, receives telemetry, registers renders/publishes through the normal choke points (register_asset(),commit_version()). - UXP panel — dumb renderer + step dispatcher. No pipeline logic lives here beyond
the dispatcher and a small
alchelper API.
4. The panel (Premiere UI metaphor)¶
Premiere has no main-menu or shelf extension point the way Blender/Maya do. The metaphor: one dockable UXP panel that contains our standard UI elements.
- Menu recipe → panel toolbar. The
menus/alchemysteps render as a horizontal row of buttons across the top of the panel (the same standard alchemy menu users see in Blender). Steps ordered byorder, labeled bylabel, tooltips fromgoal. - Shelf recipe → icon strip below the toolbar (from
shelves/alchemy), when populated. - Body region — contextual content per verb.
Artfills it with the todo/kanban module (served HTML from the app, rendered in a UXPwebview, or a native re-render — see §8 Art).Render/Publishshow step progress (the recipe's steps with pass/fail state, like run_task's phase output). - Header — current context: project / scene / task / user, resolved from the open
.prprojpath viaPathObject.from_pathserver-side. If the project isn't in an Alchemy path, the panel shows the "work inside Alchemy" state (same behavior asEnsureAlchemyPath's fail criteria). - Flyout menu (the panel's built-in ≡ menu): Refresh Menus (re-fetch cookbook), Open in Alchemy (browser to the task page), Settings, Dev/Reload.
Panel identity follows the cookbook convention: the menu's task folder name
(alchemy) is the panel name, mirroring CustomMenu.load_menus() where a menu's
identity is its task/folder name.
5. Cookbook contract¶
5.1 Recipe schema — no new fields needed¶
The existing RecipeStep schema already carries what we need:
| field | use in Premiere |
|---|---|
language / ext |
The routing switch. "javascript"/.js → eval in panel. "python"/.py → POST to server, executed in venv with path_object. |
module / import_string |
Python steps: unchanged (server imports and calls run(path_object=…)). JS steps: module names the .js file beside the recipe JSON; import_string unused (panel fetches the file body). |
step_type |
validate steps short-circuit the recipe on fail (existing semantics). |
requires_display |
JS steps are inherently display-bound; python steps with requires_display: true are invalid for Premiere recipes (nothing to show them in). |
telemetry fields (expected_hours, expected_savings, …) |
unchanged; effort-graph accounting works identically. |
The current scaffold files are all .py — that's correct for anything that can be
decided server-side. Convert a step to .js only when it must touch the Premiere DOM.
For the render/edt recipe as scaffolded:
EnsureAlchemyPath— python (asks the panel for the project path once at context time; validation is aPathObject.from_pathcall server-side). Stays.py.SetRenderPath— hybrid: python computes the output path/filename fromPathObjectnaming; a JS half applies it in Premiere. Model this as python step that returns a payload + the JS convention in §5.3, or split into two steps (ComputeRenderPath.py,ApplyRenderPath.js). Recommendation: split — keeps each file single-language and the recipe self-documenting.RenderTimeline— javascript (EncoderManagerexport of the active sequence). BecomesRenderTimeline.js.
5.2 JS step convention¶
A .js step file beside the recipe JSON exports a single async run:
// cookbook/premiere/render/edt/RenderTimeline.js
async function run(ctx) {
// ctx.ppro — require('premierepro') DOM
// ctx.alc — helper API (fetch wrappers to the Alchemy server)
// ctx.pathObject — plain-JSON snapshot of the task PathObject (server-resolved)
// ctx.payload — output of the previous step (if any)
const project = await ctx.ppro.Project.getActiveProject();
// ... EncoderManager export to ctx.pathObject.render_dir ...
return { ok: true, output: renderedPath };
}
Mirror of the python signature run(path_object=None, **kwargs) -> bool/dict:
name-in / JSON-out, no live handles leak across the boundary (same rule as the verb
modules in alc/plugins).
5.3 Step chaining¶
Steps run strictly in order. Each step's JSON return becomes the next step's
ctx.payload (JS) / kwargs["payload"] (python). This is how a python step hands a
computed path to the JS step that applies it in the DOM, and how a JS step hands a
rendered file path back to a python step that registers/transcodes it.
5.4 Execution & security rule¶
The panel only ever evals JS fetched from the Alchemy server, which reads it from the cookbook checkout. Never eval strings from any other origin. (The cookbook is already the trusted code source for every DCC — this is the same trust boundary as Blender importing cookbook python.)
6. HTTP contract (panel ↔ server)¶
New route module app/routes/premiere.py, mounted under /api/premiere:
| endpoint | purpose |
|---|---|
POST /api/premiere/context |
body: { project_path, user }. Server grounds a PathObject, returns { path_object, task, valid }. Panel calls this on load and on projectActivated events. |
GET /api/premiere/menus?path=… |
Returns resolved menu + shelf recipes for the context (via SoftwareCookbook(software="premiere")), each JS step inlined with its file body: { steps: [{...step, js_source?: "..." }] }. |
POST /api/premiere/run_step |
body: { recipe_type, task, step, payload, path_object_id }. Executes a python step server-side; returns its JSON result. |
GET /api/premiere/recipe/{recipe_type}/{task} |
Full recipe for verb buttons that trigger recipes (Render button → render/edt). |
POST /api/premiere/events |
Telemetry: button presses, step pass/fail, durations → effort graph / events MSD. |
Reuse: run_step should be a thin wrapper over the same execution path run_task
uses (import module, call run), not a new executor.
Phase 2 — /ws/premiere command channel. A WebSocket the panel keeps open so the
server (and therefore agents, and the web app) can push commands into Premiere:
{ op: "premiere.query.edt.get_sequence_info", args: {...} } → panel executes against
the DOM → replies. This is what makes premiere.{verb}.{task}.{op} addressable from
the agent layer, the same shape as the Blender bridge but with the connection direction
reversed (panel dials out, so no firewall/port issues inside Premiere). Also the
transport for jump-to-frame from the todo/review UI (web app → server → panel →
setPlayerPosition). Not required for v1 buttons.
7. UXP plugin skeleton¶
Lives in the alchemy repo at alc/plugins/premiere/panel/ (source), packaged to .ccx
for distribution; sideloaded via UXP Developer Tools during development.
alc/plugins/premiere/
panel/ # the UXP plugin (JS — not importable python)
manifest.json
index.html
src/
main.js # boot: context → fetch menus → render
dispatcher.js # step runner: eval js / POST python, chaining, telemetry
alc.js # fetch/ws helpers, server discovery, error toasts
ui/ # toolbar, shelf, progress list, header (standard elements)
tasks/edt/ # python side: SmartTask subclass for edt in premiere
__init__.py # class Task(SmartTask) — software="premiere", task="edt"
create.py / edit.py / query.py # ops that run SERVER-side (file/MSD/registry work)
Per alc/plugins conventions: premiere is a DCC package; the panel is its "scene
gateway equivalent" (there is no alchemy.py Scene until the WS channel exists —
SmartTask.scene warns and returns None, which is correct for v1).
SmartTask ontology — BUILT & PROVEN (2026-07-18)¶
The etymology follows all the way to the root. get_task("premiere", "edt") returns a
working Editorial(SmartTask); the twist vs Blender/Maya is that Premiere has no
Python interpreter, so there is no in-process world: _DCC_PROBE["premiere"] = None
(the comfy pattern) and the verb modules are pure-Python HTTP clients whose DOM
halves live in sibling js/{op}.js files, dispatched through
bridge/client.py::run_js_op(). Two rules institutionalized in the client:
args travel as a const args = {json}; data literal (never interpolated into code —
the spike's backslash lesson), and failures raise PremiereBridgeError (no silent
fallback). Premiere is the live-session face of the edt contract
(smart_task/edt.py); the otio plugin is the file face. Live ops act on the ACTIVE
sequence instead of taking source_path — the documented deviation.
Proven live: query.get_project_info / get_timeline_info / get_markers /
get_player_position / get_clips, edit.set_player_position (the todo/review
jump-to-frame primitive), edit.add_marker (undoable, via executeTransaction),
create.render_timeline (poll-file completion).
fps (solved): SequenceSettings exposes no frame rate on 25.6; the reliable
derivation is seq.getTimebase() (ticks/frame, string) against Premiere's fixed
254016000000 ticks/s — timebase 10584000000 → exactly 24.0 fps. Frame-accurate
playhead jumps use tick math directly: createWithTicks(String(frame * timebase)).
FCP XML / OTIO export (version-gated): ppro.ProjectConverter carries
exportAsFinalCutProXML(sequence, path, suppressUI) and
exportAsOpenTimelineIO(sequence, path, suppressUI) — but it is undefined on
live 25.6 (26.x API; types package starts at 26.2). Premiere 2026 (26.3.0.93) is
installed: moving editorial to 2026 unlocks the OTIO spine's NLE-export leg
programmatically — no more manual File → Export → Final Cut Pro XML, and native
.otio beside it. Until then the export leg stays manual on 25.6.
manifest.json (the part that matters)¶
{
"manifestVersion": 5,
"id": "com.cglumberjack.alchemy",
"name": "Alchemy",
"host": [{ "app": "premierepro", "minVersion": "25.6" }],
"entrypoints": [
{ "type": "panel", "id": "alchemy", "label": { "default": "Alchemy" },
"minimumSize": { "width": 320, "height": 240 } }
],
"requiredPermissions": {
"network": { "domains": "all" },
"allowCodeGenerationFromStrings": true,
"localFileSystem": "fullAccess",
"webview": { "allow": "yes", "domains": ["http://127.0.0.1:8080"] },
"launchProcess": { "schemes": ["http", "https"] }
}
}
Notes:
- domains: "all" rather than enumerating localhost — there are open Adobe bugs where
explicit localhost entries are denied. Use 127.0.0.1 in all URLs, never
localhost (spike item S1).
- webview is only needed if Art embeds served HTML (§8); drop it otherwise.
- Dev loop: Premiere Preferences → Plugins → Enable developer mode; UXP Developer
Tools ≥ 2.2.1 to load/debug/package.
8. The six standard verbs¶
| verb | v1 behavior |
|---|---|
| Build | Recipe premiere/build/edt: python steps resolve latest published cuts/plates/audio for the task (import_latest semantics); JS step importFiles into a bin structure + (later) assemble sequence from the scene timeline JSON (OTIO edit task alignment). v1 can stop at "import latest published media into bins". |
| Refresh | Re-resolve latest versions server-side; JS step swaps updated project items (re-link / replace footage). |
| Art | Opens the todo/kanban module for the task in the panel body. v1: webview pointed at the existing app route (/c/{cid}/… todo view). If webview proves clunky, re-render natively from the todo JSON API. (This revives the deferred "native Premiere panel jump-to-frame" idea; actual jump-to-frame needs the phase-2 WS channel.) |
| Render | Runs premiere/render/edt: validate path → compute output path (python, PathObject naming) → apply + export via EncoderManager (JS) → POST result to server for transcode/thumb/registration (python, existing OIIO media service + register_asset()). |
| Review | After render (or on demand): python step flags the rendered version for review (flag_for_review()), returns the review-room URL; panel offers Open review room (launchProcess → browser). Timeline markers ↔ review notes sync is phase 2 (needs createAddMarkerAction + WS). |
| Publish | Save project (DOM), then server-side commit_version() on the task — same publish spine as everything else. Panel shows step progress. |
Per-task specialization follows the normal cookbook fallback: menu button Render
resolves premiere/render/{current_task}, falling back to defaults exactly like
Recipe.resolve does cross-DCC.
9. Spike checklist (do these before building the panel proper)¶
Ordered by risk; each is a throwaway UDT-loaded panel test:
- S1 — localhost fetch:
fetch("http://127.0.0.1:8080/api/health")withdomains: "all". Known flaky area (open Adobe issues on localhost permission handling). If denied: try explicithttp://127.0.0.1:8080entry, then WebSocket, then (last resort) a127.0.0.1.nip.io-style hostname. - S2 — eval:
allowCodeGenerationFromStrings: true+new Function("ctx", src)withrequire('premierepro')reachable from inside the evaluated body. - S3 — EncoderManager: ✅ PROVEN (2026-07-18). Exported the active 229s sequence to
the task render path with an AME systempreset copy
(
alc/plugins/premiere/presets/h264_match_source_high.epr). Hard-won gotchas: - Paths must use native backslashes. Forward-slash paths make
exportSequencereturnfalsesilently — no exception, no event. Same code with\\works. exportSequence(seq, ExportType.IMMEDIATELY, out, preset, exportFull)— errors reject with a plain string ("Invalid parameter."), not an Error; don't read.message.getExportFileExtensionreturned""for every preset tried — useless as a preset-validity signal; ignore it.EVENT_RENDER_*listeners registered without error but never fired during an IMMEDIATELY export — completion detection is poll-the-file for now.- IMMEDIATELY renders in-app and fast (229s timeline → 331MB H.264 in well under a minute, hardware encode).
- S4 — importFiles: import to a named bin; confirm re-import/replace behavior for Refresh.
- S5 — WebSocket: hold a
ws://127.0.0.1:8080/ws/premiereconnection open across panel dock/undock and project switch (phase-2 gate, cheap to test now). - S6 — events: subscribe to project/sequence-changed events for header context updates.
Record S1–S3 outcomes in this doc; S3 failure changes the Render design (watch-folder fallback), S1/S2 failures are architecture-threatening and would reopen the CEP question — everything else is incremental.
10. Cleanup / open items¶
cookbook/premiere/menus/None/is a scaffolding artifact (task=None,None.json) — delete it or it will resolve as a real menu named "None".menus/alchemysteps currently sayext: ".py"— correct for now (they're server-side trampolines); revisit per-step as JS needs emerge (§5.1).- Decide Art's webview-vs-native question after S1 (webview shares the localhost permission surface).
- Phase 2: WS command channel →
premiere.{verb}.{task}.{op}agent addressing, review markers, jump-to-frame. - Distribution: sideload via UDT for the studio now;
.ccxpackaging + signing when we hand it to outside crews.