pqtran ACES-correct colour pipeline

RAW → NEGATIVE → GRADED FRAME → POST

The director sets the vibe.
Every frame develops to it.

pqtr is an ACES-correct colour pipeline: a camera RAW becomes an ACES2065-1 negative, a look is captured from a reference, the negative is developed under that look to a graded display frame, and post finishes it for publishing — deterministically, across every body, at event tempo.

Four engines and a delivery layer

Each engine is pure compute and stands alone, with its own README.

unit scope runtime
dneg RAW → ACES2065-1 negative — clean-room Sony decode, sensor parts (denoise, demosaic, lens, highlights) and the IDT; per-body bundles CPU C++ → arm64 / WASM
aces the colour unit — grade + look on a negative → display frame; ACES stages as typed parts, fused by the aggregator into one WGSL kernel WGSL → WebGPU
vibe look learning — capture a style from a reference (camera JPEG or pro edit) → exposure key + colour .cube + tone curve JS + native vreg
post display-referred finishing after aces — clarity (spatial), effects, geometry; explicitly not colour-managed WGSL on the shared aces host
user deliveryview/ the one browser front-end (Shoot | Edit | Post), desk/ the desktop host, edge/ the Android host browser / Node / Android
  RAW ─────[ dneg ]─────► ACES2065-1 negative ─┐
                                               ├─►[ aces colour ]─► display RGB ─►[ post ]─► finished frame
  reference ─[ vibe ]──► key + cube + curve ───┘

Two seams, both declared interfaces

Each consumer cares how none of its inputs were made.

the negative dneg → aces

A standard ACES2065-1 OpenEXR with provenance metadata. Below it the sensor is done; above it colour begins. It is dneg's only interface — and it is immutable.

display RGB aces → post

The output of the Output Transform. Below it colour is done; above it finishing begins. post has no ACES states, no sealed pipe and no Output Transform, by design.

The third contract is the look — an exposure key, a colour cube and a tone curve — which is what binds authoring to develop. aces only ever applies a look; it never authors one.

The one constraint: the runtime is browser-shaped

WASM + WebGPU in a tab. That binds every design choice below it.

  1. decode plain CPU C++, no GPU and no exiftool dependency — so dneg compiles to arm64 today and WASM next.
  2. develop WGSL compute → WebGPU, confirmed working in the Android WebView. The browser host is a model-agnostic composer + runner that both aces and post use.
  3. learning runs in the same browser as pure JS, with one native helper (vreg registration) shipped on the phone too.
  4. the result both hosts serve the same front-end over the same HTTP contract — the phone runs the pipeline byte-identical to the desktop.

The repo is self-contained — JS / C++ / WGSL, no Python — and no GPL is linked into the runtime.

Principles

Four commitments the code is held to.

  1. Canonical ACES backbone. IDT → working → LMT → Output Transform → ODT: a sealed pipe of colour-space states baked from OCIO. Not tunable — that is the reproducibility guarantee.
  2. Effects are typed parts. A part declares the colour state it operates in; the type checker admits it only where that state holds. Sealed and open parts differ in authority, not structure. Non-colour work is not an aces part at all — it lives in post.
  3. Lineage-first. Every canonical element names its oracle (OCIO, darktable-cli) and a reference; creative parts say so honestly — provisional, or no oracle at all.
  4. Edits are direction, not baked pixels. The negative is immutable; an edit is a JSON recipe re-rendered on demand; a vibe is a portable directed recipe, never a per-frame copy.

The model is the authority

JSON the aggregator actually reads, so it cannot drift from the code.

states.json

The colour-state type system — what a part may declare, and what the checker enforces.

pipe.json

The sealed ACES transforms. Not parts, not editable, inserted by the aggregator.

parts.json

Every part — state, WGSL call, params, lineage. Load-bearing. post carries its own, the same shape, run by the same composer.

camera.json

dneg's per-body parts, with rgb_cam baked per camera and gated for colour parity.

Where prose and JSON disagree, the JSON is right and the prose is stale. That rule is why the documents on this site are served from the repo's own files.

Running it

Two hosts, one front-end, one HTTP contract.

Desktop

user/desk/stage-aces.sh
node user/desk/producer.mjs          # watches user/desk/raws/ for RAWs

Then open http://localhost:8787.

Phone (Android, no Gradle)

user/edge/scripts/stage-view.sh
user/edge/scripts/build.sh
user/edge/scripts/sideload.sh

End to end, on the pro sample set

cp test/local.env.example test/local.env   # set RAWS (your ARW dir) + DAWN
test/e2e.sh DSC00645                      # any qualified-body sample

A look is display-referred and body-agnostic: capture it once from a pro sample, apply it to a negative from any body.