Skip to content

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 language field:
  • javascript steps run inside the panel (UXP eval, with the Premiere DOM in scope).
  • python steps 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:

  1. Cookbook (cookbook/premiere/…) — recipes own what the buttons are and what each step does. Already scaffolded:
  2. premiere/menus/alchemy/ — the standard alchemy menu (Build, Art, Render, Review, Publish, Refresh) + alchemy.json.
  3. premiere/render/edt/ — first real recipe: EnsureAlchemyPath → SetRenderPath → RenderTimeline.
  4. premiere/shelves/alchemy/ — empty for now; becomes the icon-strip row.
  5. Alchemy server — serves resolved recipes to the panel, executes python steps, receives telemetry, registers renders/publishes through the normal choke points (register_asset(), commit_version()).
  6. UXP panel — dumb renderer + step dispatcher. No pipeline logic lives here beyond the dispatcher and a small alc helper 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/alchemy steps render as a horizontal row of buttons across the top of the panel (the same standard alchemy menu users see in Blender). Steps ordered by order, labeled by label, tooltips from goal.
  • Shelf recipe → icon strip below the toolbar (from shelves/alchemy), when populated.
  • Body region — contextual content per verb. Art fills it with the todo/kanban module (served HTML from the app, rendered in a UXP webview, or a native re-render — see §8 Art). Render/Publish show 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 .prproj path via PathObject.from_path server-side. If the project isn't in an Alchemy path, the panel shows the "work inside Alchemy" state (same behavior as EnsureAlchemyPath'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 a PathObject.from_path call server-side). Stays .py.
  • SetRenderPath — hybrid: python computes the output path/filename from PathObject naming; 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 (EncoderManager export of the active sequence). Becomes RenderTimeline.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") with domains: "all". Known flaky area (open Adobe issues on localhost permission handling). If denied: try explicit http://127.0.0.1:8080 entry, then WebSocket, then (last resort) a 127.0.0.1.nip.io-style hostname.
  • S2 — eval: allowCodeGenerationFromStrings: true + new Function("ctx", src) with require('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 exportSequence return false silently — 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.
  • getExportFileExtension returned "" 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/premiere connection 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/alchemy steps currently say ext: ".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; .ccx packaging + signing when we hand it to outside crews.