Learn / Create / Automate

BATIQ documentation.

From your first frame to a repeatable render. Desktop workflows, the command line, and Python — all in one place.

BATIQ User Guide

BATIQ is a node-based compositor for assembling image and video processing graphs, viewing the result, and rendering frames or sequences. It works in a scene-linear float image core and uses a native wgpu viewer. It is a prototype: keep backups of important projects and verify final renders.

Install and launch

BATIQ is distributed as a platform package when a release is available. Unpack the archive and launch the BATIQ application using your operating system's normal application launcher. On macOS, a locally packaged build is opened with:

open target/release/BATIQ.app

For a source checkout, the supported development launch is:

cargo run --release --bin batiq

For a packaged build, launch the platform executable from the unpacked release folder: open BATIQ.app on macOS, run batiq on Linux (the package launcher starts batiq-app), or run batiq.exe on Windows. BATIQ does not require a separate installer in these package layouts.

You can open a project or media file at launch. Replace the placeholder with a real path:

open target/release/BATIQ.app --args /path/to/project.batiq
open target/release/BATIQ.app --args /path/to/shot.0001.exr

The application also accepts .batiq, .exr, .png, .jpg, .jpeg, .mov, .mp4, .m4v, and .mkv through Open (⌘O), or by dragging one file onto the window. Video reading uses the FFmpeg tools included with release packages; development builds can also discover them beside the executable, in the app resources, or on PATH.

Your first project

On first launch BATIQ displays a generated HDR test plate and an editable graph. To start with your own material:

  1. Press ⌘O and choose an image, numbered sequence, or video.

  2. Select the source and route it to the active Viewer with the Viewer toolbar or 19. Viewer output is workspace state, not a node you need to create.

  3. Select the source or an effect node to inspect its controls in the Inspector.

  4. Press ⌘S, choose a .batiq filename, and save early.

Project format settings define resolution, pixel aspect, frame range, and FPS. These settings are shared by generators, playback, and rendering. Use Settings when the project needs timing or format different from the source.

Importing media

Open accepts a single supported project or media file. A numbered image such as shot.0001.exr can be opened as a sequence; BATIQ discovers the surrounding numbered frames. The Read node exposes sequence range, frame offset, missing-frame behavior, input color space, and premultiplication controls. EXR display/data windows and pixel aspect are retained.

For video, use a supported container and codec readable by the configured FFmpeg backend. If a sequence is not detected, open a representative numbered frame and check that its neighboring files use the same naming pattern and directory.

Build and use the node graph

The graph is the processing recipe: data flows from Read or generator nodes through effects to a Viewer or Write node. The Node palette lists the available node types by category. Click a palette row to create that node. You can also move the pointer over the graph and press Tab to open QUICK ADD NODE, type in Search node name or category…, then click a result or use the arrow keys and Enter.

  • Drag nodes to position them; select one or more nodes to edit or move them.

  • Connect an output to a target input by clicking the output and then the target, or by dragging between ports.

  • Click a red disconnect trail point to remove a connection; ⌘D disconnects the selected node input (Ctrl+D is the usual equivalent on platforms that map Command shortcuts to Control).

  • ⌘A selects all; Esc clears selection; Delete removes selected nodes.

  • Select a node in the graph and press 19 to route it to that numbered Viewer; the selected node becomes the active Viewer output. With no graph selection, press a number to show an already-routed Viewer. The Viewer toolbar also identifies the active Viewer.

  • ⌘C and ⌘V copy and paste selected nodes as versioned, human-readable JSON. Connections outside the selection are intentionally not copied, so include source nodes when the complete subgraph is needed.

  • ⌘Z and Shift+⌘Z undo and redo graph and parameter edits.

Graph direction

Open Settings → Node graph and choose Horizontal for inputs on the left and output on the right, or Vertical for inputs across the top and output at the bottom. Changing direction transposes existing placement while preserving relative layout; auto-arrange follows the selected direction. Masks remain readable as a side input in vertical mode.

Common editing patterns

Use Transform for translation, signed scale, rotation, and pivot; Reformat for image dimensions and letterboxing; Grade and Exposure for tonal adjustments; Blur for filtering; and Merge to combine branches. Select a Mask node, choose Draw rectangle or Draw B-spline, and draw in the Viewer. Double-click or press Enter to close a B-spline, Esc to cancel, and drag control points to edit. Right-click a knob or individual channel value to reset it, create/delete a keyframe, show its track, or remove its animation.

Tracker and Warp provide direct Viewer editing. Tracker passes are asynchronous and can be stopped; Warp starts with draggable perspective corners and supports additional grid points. AI FX accepts a natural-language effect description and can use the configured local AI service, with an offline fallback. See AI FX usage for details.

Viewer and timeline

The Viewer follows the active routed output. Use the mouse wheel to zoom, middle/right-drag to pan, and F to fit. Press Space to play or pause. Playback Auto uses motion proxies (512/1024-pixel tiers); Full, 1/2, and 1/4 are explicit alternatives. Pausing refines the displayed frame to full resolution.

The transport can follow Project timing or Input timing. Input timing is available when one Read feeds the active Viewer; otherwise composites with multiple Read branches use project timing. Set IN and OUT points beside the transport, and click or drag the playhead to scrub. AUTO KEY records edited controls at the current frame.

Press ⌘4 to show the docked clip timeline. Each Read has a lane: drag a clip body to move it, or drag its end grip to trim. Edits can be undone. Shift suspends snapping and Esc cancels an active drag. ⌘1, ⌘2, ⌘3, ⌘4, and ⌘5 show or hide the palette, Inspector, animation timeline, clip timeline, and node graph respectively; ⌘0 resets the workspace and interface scale.

Save, load, and compatibility

Save projects as .batiq with ⌘S; load them with Load project (Shift+⌘O) or Open. Projects contain graph topology, node parameters, animation, positions, enabled state, and project format settings. BATIQ migrates versioned project data when supported. Keep the original project when testing a newer build, and save a new copy after a migration. Media remains external: moving or renaming source files can make a Read node unresolved.

Playback and export

Add or select a Write node, choose EXR, PNG, or JPEG, then set its output pattern or folder. Render the current frame or a range. A sequence path without #### or %04d receives a safe frame token automatically. Write controls include channel selection, half/float data, compression, color-space conversion, and range guards. The status bar reports progress and ETA; an active render can be canceled.

EXR preserves float data and arbitrary flat channels/layers. PNG and JPEG are suitable for display-oriented output. Confirm the Write color space and format before a final render, and inspect representative frames for alpha, window, and color-management expectations.

Troubleshooting

The Viewer is empty. Select a graph node and route it with 19, or inspect the Viewer status and graph wires.

Media will not import. Confirm the extension is supported, the file exists, and a numbered sequence uses consistent frame naming. For video, verify FFmpeg/ffprobe discovery; release packages include them, while development builds may need them on PATH or configured explicitly.

Playback is slow. Choose a lower playback tier, reduce the graph while checking a shot, or use the playback cache folder in Settings. The cache is bounded and stores processed proxies as half-float PIZ EXRs.

Colors look wrong. Check the Read input color space, project working space, Viewer transform/exposure/gamma, and any ColorSpace node. BATIQ can discover an OCIO configuration from OCIO or a selected .ocio file. See OCIO and ACES verification.

A render fails or stops. Check the Write path and permissions, confirm the source range is available, and read the status-bar error. Render a single frame first, then retry the range. Keep the project and source media unchanged while diagnosing.

A project opens differently after an upgrade. Work from a copy, note the BATIQ version, and compare the graph and representative output. Project migrations are versioned, but output should still be reviewed after a version change.

Further reading

BATIQ headless CLI

batiq-headless is an isolated, display-free process for pipeline validation, project migration, graph edits, and rendering. It uses the same project loader and blocking renderer as BATIQ. Its stdout is machine-readable; keep stderr separate for diagnostics.

Find and invoke it

Release packages place the executable beside the desktop binary (inside the app bundle on macOS). Development builds produce it with Cargo. The Python SDK can discover it from BATIQ_HEADLESS_EXECUTABLE, a sibling of BATIQ_EXECUTABLE, PATH, or the standard macOS app bundle; you can also pass an explicit executable path to HeadlessTransport.

With no arguments, the executable reads bounded JSON-RPC 2.0 messages from stdin until EOF:

batiq-headless < requests.jsonl > responses.jsonl 2> diagnostics.log

The process reads the bounded JSONL batch to EOF before producing responses, so close stdin after sending the request batch. The other command-line mode is the Deadline adapter. It accepts paired options after --deadline-render; paths are UTF-8 hex to avoid shell quoting issues:

batiq-headless --deadline-render \
  --project-hex 2f73686f772f73686f742e6261746971 \
  --output-hex 2f746d702f72656e6465722e657872 \
  --start 1 --end 10 --step 1

Required options are --project-hex, --output-hex, --start, --end, and --step. Unknown or missing options fail. Deadline substitutions such as <STARTFRAME> and <ENDFRAME> should be resolved before launch.

JSONL protocol

Send initialize first. The supported protocol is major version 1 (minor 0):

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocol_version":{"major":1,"minor":0}}}
{"jsonrpc":"2.0","id":2,"method":"validate_project","params":{"project":"/path/to/project.batiq"}}

The response advertises the BATIQ version, current project format, and capabilities: render, validate_project, migrate_project, edit_function, edit_variants, and atomic in-place migration. Cancellation is not supported. Every request must have a JSON-RPC 2.0 id; one JSON object per line is read and written. Blank or whitespace-only input lines are ignored.

A successful one-frame render includes a notification followed by the response to request id 2 (elapsed time and output directory are runtime values):

{"jsonrpc":"2.0","method":"render_event","params":{"sequence":2,"event":"finished","data":{"written":1,"elapsed_seconds":0.42,"output_directory":"/path/to/renders"}}}
{"jsonrpc":"2.0","id":2,"result":{"written":1,"elapsed_seconds":0.42,"output_directory":"/path/to/renders"}}

Protocol failures use the JSON-RPC error shape. For example, an incompatible major version is rejected during initialization:

{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"unsupported protocol major 2; expected 1"}}

validate_project requires project and returns source/current versions, migration status, node count, and frame range. It does not write the file.

migrate_project requires project. By default it writes a separate current- version file; use destination to choose it. Use in_place: true to replace the source atomically, with backup: true (the default) to retain a backup. overwrite: true is required before replacing an existing separate destination.

edit_function and edit_variants require an explicit destination or in_place: true for mutations; list is read-only and needs neither. Function actions include create, list, rename, update, delete, and instantiate. Variant actions include list, group.add, option.add, set, preset.save, and preset.apply. See Node functions for reusable Functions and examples.

render requires project, output, and frames (start, end, step). An optional positive write_node selects the Write node; otherwise BATIQ requires an unambiguous Write node. Optional variants selects one option per group, variant_options requests Cartesian combinations, and variant_presets requests saved presets. Output formats include exr, png, and jpeg/jpg.

Rendering emits render_event notifications with increasing sequence numbers, followed by exactly one terminal finished, failed, or canceled event. The final response contains the render result. Variant expansion may produce multiple jobs and output paths.

Inspect the terminal event to determine success: a runtime failure can still have a JSON-RPC result. Setup failures return a JSON-RPC error and may occur before any render events. A zero process exit code in JSONL mode alone does not prove the render succeeded.

Errors and limits

Malformed JSON returns JSON-RPC parse errors; invalid requests, unknown methods, protocol/version failures, and operation failures return JSON-RPC errors. A render failure uses the render-specific error code. The process exits non-zero for startup failures, invalid Deadline arguments, or failed/canceled Deadline renders. Do not parse diagnostics from stdout. Stdin is limited to 2 MiB and a render may request at most 1,000,000 frames.

For typed discovery, timeouts, bounded output capture, and Python examples, see PYTHON_API.md and python/README.md. For the embedded runtime, see PYTHON_EMBEDDING.md.

BATIQ Python API architecture

BATIQ has two deliberately different Python interfaces:

ImportBoundaryUse
import batiqIn-process, embedded in the running desktopScripts, menus, graph/project edits, events
import batiq_apiExternal Python process/packageHeadless render, validation, migration, farm automation

batiq_api.BatiqApplication.open() is therefore an external process helper, not the way to control the currently open desktop project.

Operator path

For an interactive script, BATIQ starts a dedicated Python runtime and the script talks to the current application:

import batiq

read = batiq.nodes.create("Read")
read["path"] = "/show/shot/plate.####.exr"
batiq.project.set_metadata("pipeline", {"source": "editorial"})

Remote Link nodes are available to scripts as normal "Remote Link" nodes. Create a wire-free receiver with batiq.nodes.create_linked(remote.id, (x, y)). Its remote_link_source property returns the source node or None; assign a Remote Link node ID to rebind it, or None to clear it. "Linked Input" is accepted by nodes.all(kind=...) for inspection, but nodes should be created through create_linked so the source reference is validated. Copy a receiver together with its Remote Link source to preserve and rebind the connection in the pasted selection; copying the receiver alone clears the reference, following BATIQ's existing cross-project clipboard policy.

The graph and project remain owned by the Rust/egui main thread. Python holds node IDs, never Rust node copies, and every read or mutation is a typed, bounded HostCommand round trip. Requests have an explicit timeout; the GIL is released while Python waits for the main-thread reply. Deleted IDs report exists == False and raise NodeNotFoundError on access or mutation.

The split is intentional: project data, orchestration, and production-system connections change at different speeds.

studio tool / publish hook
          |
      BatiqClient
       /       \
.batiq model   AdapterRegistry
               /      |      \
          Deadline  ftrack  studio tools

Embedded API surface

The current built-in module exposes app, project, nodes, events, and ui singletons. Project operations include new, open, save, and save_as, plus frame/format properties and JSON-compatible metadata. Nodes support all(kind=...), create, by_id, selected, select, typed common properties, parameter access through node[name], deletion, and metadata.

events.on("project_opened", callback) and events.off(...) deliver asynchronous notifications on the Python runtime thread. Callback exceptions are logged and isolated. ui.add_menu_action("Scripts/Load...", callback) only registers a native menu path; the callback is queued to Python, so an egui menu action never synchronously waits for Python that might call back into the host. This queueing is the deadlock boundary for future script-editor work.

The exception hierarchy is Error, ProjectError, NodeError, NodeNotFoundError, ValidationError, TimeoutError, and OutcomeUnknownError. The latter means a command began on the application thread but did not finish within the second bounded reply window; callers should refresh live state before retrying a mutation.

External SDK stable core

The core package uses only the Python standard library and consists of:

  • models.py: immutable request/result values and provider-neutral job states;

  • project.py: .batiq JSON validation and explicit non-destructive copies;

  • application.py: the real desktop launch boundary available today;

  • interfaces.py: structural protocols for render and publish providers;

  • registry.py: named provider selection with clear missing-capability errors;

  • transport.py: a stdlib-only, one-process-per-operation JSONL client for the installed batiq-headless contract;

  • client.py: a thin orchestration facade.

Mappings carried by frozen models are normalized into immutable values. This prevents a caller from changing job metadata after submission by mutating the original dictionary.

Install and render

The SDK targets Python 3.10 or newer and has no mandatory third-party dependencies. From a checkout, install it in an isolated environment:

cd python
python3 -m venv .venv
.venv/bin/python -m pip install -e .

Use BatiqProject for safe inspection of projects through format version 30, and pass a normalized project path directly to HeadlessTransport for an external render. The headless executable uses BATIQ's current Rust loader (format version 38 in this checkout); the SDK's inspection model has a lower version ceiling. Paths may be relative; the transport resolves them before launch.

from batiq_api import FrameRange, HeadlessTransport, RenderRequest

project_path = "/path/to/shot.batiq"  # already normalized by BATIQ, if needed

result = HeadlessTransport(timeout=300).render(RenderRequest(
    project=project_path,
    output="/path/to/renders/beauty.####.exr",
    frames=FrameRange(1, 10),
    name="beauty",
))
print(result.terminal, result.result)

BatiqClient is the convenience facade: load_project, open, render_headless, submit_render, job_status, cancel_job, and publish. BatiqApplication.discover() locates the desktop executable and open(path) launches a project/media file; it does not attach to an existing window. Register provider implementations explicitly with AdapterRegistry.

BatiqProject.document, nodes, settings, and frame_range return copies or typed values. save() refuses to overwrite an existing file unless overwrite=True, writes through a temporary file, and atomically replaces the destination. The SDK's BatiqProject validates the BATIQ Node Compositor format and supports project versions through 30; newer versions raise ProjectVersionError. It cannot inspect current v38 files. Keep project JSON as the compatibility boundary and let the BATIQ headless loader perform format migrations via HeadlessTransport.migrate_project() before rendering; this does not make a v38 document readable by the SDK's v30 inspection model.

Integrations

Vendor imports are lazy and factories are explicit. Supplying an already configured connection is the preferred production path:

  • Deadline 10: Deadline.DeadlineConnect.DeadlineCon; submission is isolated behind connection.Jobs.SubmitJob(...). Deadline 10 entered maintenance mode in November 2025, so this remains an optional adapter and does not shape the core API.

  • Optional standalone adapters can be supplied with their own configured provider client. Entity fields belong in caller-managed context/metadata because studio schemas vary.

  • ftrack: ftrack_api.Session or a compatible injected object. The adapter uses the official unit-of-work flow and requires a studio-supplied file attachment callback, so it cannot report a successful publish after only creating an empty tracking entity.

No adapter imports a vendor package or opens a network connection merely by being imported.

Current BATIQ boundary

BATIQ ships two process boundaries:

  • batiq accepts a project or media path and opens the eframe desktop, or serves the same protocol when launched with --headless;

  • batiq-headless accepts bounded JSON-RPC 2.0 lines on stdin and keeps stdout protocol-only. Its --deadline-render mode decodes shell-free path arguments and Deadline's task frame tokens into the same initialize/render request pair; failed or canceled tasks exit nonzero.

Both enter through the Rust-owned project loader and graph migrations. The SDK treats these as external boundaries:

  • .batiq JSON for project interchange;

  • rendered image sequences for output interchange;

  • the desktop executable for interactive opening;

  • provider adapters for orchestration.

The embedded module does not launch another BATIQ process, edit a .batiq file behind the live application, poll files, or use localhost RPC. Keep batiq-headless as the isolated render boundary: save a workfile, then let the external SDK or farm worker render it. The existing optional batiq_api.integrations.ayon adapter remains standalone automation only: it does not install a host integration, add BATIQ menus, or load an AYON runtime into BATIQ. Host integrations are separately installed projects.

Metadata and project format

Project and node metadata are generic BTreeMap<String, serde_json::Value> namespaces. BATIQ core does not own an integration schema. Callers receive copies, and values must be JSON-compatible. The shared GUI/headless loader currently uses project format version 38; older files default missing metadata to an empty mapping. Integration metadata is system-owned: duplicate and copy/paste operations clear it so an integration container identity is not silently cloned.

Write nodes expose generic pipeline parameters through node[name]: path, write_format (also output_format), output_space, write_limit_range, write_first, write_last, and write_channels. Write formats accept exr, png, or jpeg (jpg normalizes to jpeg); channels accept all, rgba, rgb, or alpha.

Embedded runtime configuration

The runtime accepts BATIQ_STARTUP_MODULES as a comma-separated list of controlled module names. Development search configuration may use BATIQ_PYTHON_HOME and BATIQ_PYTHONPATH; startup import failures are logged without crashing BATIQ unless a future policy explicitly requires Python. Release artifacts instead link and initialize the checksum-pinned bundled CPython 3.11.16 runtime described in PYTHON_EMBEDDING.md. Do not assume that the embedded interpreter's sys.executable is a standalone interpreter for launching external tools.

Headless transport contract

HeadlessTransport resolves an explicit executable, BATIQ_HEADLESS_EXECUTABLE, PATH, the sibling of BATIQ_EXECUTABLE, or the standard macOS app bundle. It starts a fresh process for each operation with JSON-RPC 2.0 lines on stdin/stdout. The client sends initialize followed by the operation in one batch and closes stdin. BATIQ then acknowledges protocol major version 1 and capabilities before handling the operation. Render progress is reported as render_event notifications with strictly increasing sequences and exactly one finished, failed, or canceled terminal event. The client bounds captured output, validates IDs and objects, kills timed-out processes, and never invokes a shell. Render setup selects a unique Write node (or an explicit ID), rebuilds Read sequences from the migrated graph, and calls the same blocking CPU render implementation used by the desktop scheduler.

validate_project reports the source and current project versions without rewriting. migrate_project serializes the Rust-normalized graph to a separate current-version file by default. Explicit in-place migration writes a temporary file beside the source, flushes it, optionally creates a backup, and atomically renames it over the source. A separate migration destination is never replaced unless the caller explicitly passes overwrite=True.

Node functions

Select a connected set of nodes, or click a backdrop title, and press Tab over the graph. Choose Make into Function. A backdrop supplies the function name; otherwise BATIQ suggests a numbered name. Enter the desired name in the creation dialog. The original nodes remain the editable master definition, surrounded by a dotted border with a centered Fn label.

Click the Fn title to open Function properties in the inspector. Rename the function, create an instance, update its definition, or delete it there. Press Delete or Backspace while the Function is selected for the same action; BATIQ asks for confirmation before removing the Function and its instances. Deleting a function removes its instances and keeps the original master nodes as an ordinary graph branch. Renaming preserves the stable function ID and every instance link; custom instance names are kept.

Alternatively, press Tab and search Create instance or the function name to place a single instance node. Every instance has exactly two inputs: source and overall alpha mask. Internal masks and auxiliary connections remain in the master definition. Route its output like an ordinary node. Every instance uses the current master parameters, animation, and effects.

Search Edit function to select and frame the original nodes. Parameter edits propagate automatically. After adding or removing nodes from a function's scope, select the intended nodes and choose Update function from selection in Tab search. The definition must have one output. Instances keep their source and mask connections when the definition is updated.

Functions are saved in the project and participate in graph undo/redo. Their execution uses the existing node evaluator, including its CPU/GPU operations; new ordinary node types need no separate function implementation.

Current boundaries: nested function instances and clipboard transfer of instances are rejected. Keep master nodes in the project while instances reference them. This implementation does not introduce a separate nested graph editor or user-exposed parameter overrides on instances.

CLI and Python

The JSONL CLI (batiq-headless) exposes edit_function with actions create, list, rename, update, delete, and instantiate. Mutations require either an explicit destination or in_place: true; listing does not write the project.

from batiq_api.transport import HeadlessTransport
cli = HeadlessTransport()
created = cli.edit_function("shot.batiq", "create", destination="functions.batiq",
                            nodes=[2, 3, 4], name="Soft grade")
function_id = created["result"]["function_id"]
cli.edit_function("functions.batiq", "instantiate", in_place=True,
                  function_id=function_id, position=(400, 100))
cli.edit_function("functions.batiq", "rename", in_place=True,
                  function_id=function_id, name="Warm soft grade")

Embedded Python provides batiq.functions.all(), create(node_ids, name), rename(id, name), update(id, node_ids), and instantiate(id, position).

AI FX: what you can ask for

Build with local AI asks the installed Ollama model to compose a typed graph of operations, with named controls. It can create new combinations, rather than only select a named effect. Offline uses predefined recipes, including the spiral and UV-map shortcuts. The planner's source is shown before applying a plan.

Useful requests include:

  • “Create concentric sine-wave rings with frequency and contrast controls.”

  • “Make a colored organic noise texture with adjustable texture scale.”

  • “Use separate smooth noise fields to displace the image horizontally and vertically.”

  • “Create a radial mask, use it to mix two colors, then blur the result.”

The vocabulary includes arithmetic and trigonometry, coordinates, smooth seeded value noise, independent vector components, coordinate/color/RGBA construction, image sampling, color adjustments, and ordered blur, sharpen, denoise and local contrast passes. Noise layers can be combined at different scales. Controls change runtime parameters without recompiling the shader.

This is a compositing kernel system, not an image-generation model or an unrestricted shader compiler. A plan is limited to 64 nodes, four exposed controls and four GPU dispatches. Arbitrary branched spatial processing, morphology and external assets are not available. Model output is validated; these examples describe expressible effects, not guaranteed success from every local model. Unsupported or invalid plans must report an error or refusal.

For a reproducible local-model test, run:

cargo run --example fx_kernel_demo -- SOURCE.exr OUTPUT.batiq 'YOUR PROMPT'

This writes a reviewable project and fails if the planner used an offline fallback. Omitting the prompt retains the example's offline UV-map behavior.

BATIQ AI FX dependencies

The AI FX node plans through a local language model. That model is the one part of BATIQ that does not ship inside the application bundle, so an operator's first encounter with AI FX has historically been a silent fallback to the offline resolver with no explanation of what was missing.

This document records what the dependency actually is, what BATIQ now automates, what it deliberately does not, and why.

The dependency, precisely

LayerWhat it isSizeWho provides it
Ollama runtimeA local HTTP service on 127.0.0.1:11434~50–200 MB installedThird party, per platform
A planning modelWeights the runtime loads on demand2–40 GB per modelDownloaded by Ollama from a registry
GPU/compute for the modelMetal, CUDA or CPUThe machine

BATIQ's own GPU FX engine is unrelated to this and shares eframe's wgpu device. A missing model degrades planning to the deterministic offline resolver; it never degrades rendering.

What BATIQ automates now

All of the following is implemented in src/ollama.rs and surfaced in the AI FX inspector's Local AI planner section.

  1. Runtime detection. GET /api/version on loopback. If it does not answer, BATIQ looks for a runtime on disk in the platform's standard install locations and on PATH. The three outcomes are reported separately, because they need different actions:

  2. Running — version shown, everything below is enabled.

  3. InstalledNotRunning — the path found, with an instruction to start it.

  4. Absent — the platform's own install command, plus a button that opens https://ollama.com/download. Conflating “not installed” with “installed but stopped” is what makes a dependency prompt useless, so the two states never merge.

  5. Model inventory. GET /api/tags fills the model field's dropdown with what is genuinely installed, each with its size, parameter count and quantization. The previously configured model always remains selectable and is marked (not installed), so opening a project that names a model this machine lacks never silently rewrites the project's setting.

  6. Model installation. POST /api/pull with stream: true. BATIQ reads the newline-delimited progress stream on a worker thread and drives a progress bar with the phase, bytes completed and total. The download is cancellable. Any model name the registry accepts works, including a Hugging Face path such as hf.co/user/repo:Q4_K_M, so “models of their choosing” is literal.

  7. Deletion. DELETE /api/delete is implemented for reclaiming disk.

Detection and listing are blocking loopback calls, so they run when the operator opens the panel or presses Check runtime and models — never per frame. The pull runs on its own named thread and reports through a channel, so a multi-hour download cannot block playback or UI input.

The security boundary is unchanged

The local AI planner connects to Ollama over TCP at 127.0.0.1:11434. ollama::open refuses any non-loopback address before connecting, mirroring the guard the planner already had, and a test pins it.

A pull obviously reaches the internet — but Ollama makes that request, not BATIQ. Registry TLS, mirror selection, manifest handling and per-layer digest verification all stay inside the component that already implements them and is already trusted with the model store. BATIQ contributes a validated model name and reads progress.

The runtime setup panel can also open https://ollama.com/download in the system browser, from a compile-time constant with no interpolation, only when the operator clicks the button.

What BATIQ deliberately does not do

It does not install the Ollama runtime

This is technically within reach. src/update.rs already downloads over HTTPS with ureq, verifies a SHA-256 checksum, unpacks a zip and stages a signed bundle; that machinery would work against an Ollama release just as well.

It is still the wrong trade, for four reasons.

  1. It would make BATIQ an installer of third-party software. Downloading and executing a vendor's .dmg, .exe or install.sh on the operator's behalf means BATIQ is responsible for a payload it does not build, cannot sign and cannot meaningfully audit. A compromised or moved release URL becomes a BATIQ code-execution path.

  2. The install is privileged and platform-specific. macOS wants a dragged .app plus a helper for the ollama CLI symlink; Windows runs a per-user installer that registers a background service; Linux's script writes to /usr/local/bin, creates an ollama user and installs a systemd unit, and distribution packages differ again. Each is a separate privileged flow with its own failure modes, and none of them is BATIQ's competence.

  3. Verification would be weaker than the platform's. Ollama does not publish a stable per-release checksum manifest in the shape update.rs consumes, so BATIQ would be verifying less than the platform installer already does through Gatekeeper, Authenticode or the distribution's package signing.

  4. The benefit is small. The install is a one-time, few-minute step that the vendor documents well. The recurring friction is not installing Ollama; it is knowing which models exist and getting one, which is exactly the part BATIQ now automates.

The chosen behaviour — detect precisely, explain in platform-appropriate terms, open the vendor's page on request, and automate everything from the moment a runtime exists — captures the value without taking on that liability.

It does not start a runtime it finds

installed_runtime_path is detection only; BATIQ never executes what it finds. Launching a background service is a decision about the machine, not about the shot, and a compositor silently starting a daemon is a surprise. BATIQ reports the path and the operator starts it.

It does not manage a model per project

The planner's model name is project state, because it affects how a prompt was planned. Which models a machine has installed is workspace reality and is not serialized anywhere. A project that names an absent model loads unchanged, reports the model as (not installed), and plans through the offline resolver until the model is pulled.

Findings worth keeping

Two things surfaced while building this that are easy to rediscover the hard way.

Ollama's structured outputs enforce structure, not numbers. Constrained decoding honours type, enum, required and object shape. It does not honour minimum, maximum or multipleOf — those pass through as hints and are only caught by BATIQ's own validation afterwards. So a constraint the host wants guaranteed must be expressed structurally. When a prompt's chromatic intent is known, BATIQ marks amount required rather than relying on its narrowed range; numeric limits are additionally restated in prose, where the model can at least read them.

A defaulted control is not an unset control. FxInstance::reset_to_defaults fills every registered value before the model's parameters are applied, so instance.values cannot answer “did the model state this?”. chromatic_lens defaults amount to +3, which reads as an addition plan — so a correction request that omitted the field failed validation three times and fell back offline, while reporting that “the model returned 3” for a value the model never wrote. Semantic validation now consults the draft parameters and says which of the two actually happened.

Manual verification

With a runtime running:

BATIQ_TEST_OLLAMA_MODEL=gemma4:latest \
RUSTUP_TOOLCHAIN=stable-aarch64-apple-darwin \
  cargo test --locked --lib local_fx -- --ignored --nocapture

The unit tests for detection, listing, progress framing and the loopback guard run in the ordinary suite and need no service:

RUSTUP_TOOLCHAIN=stable-aarch64-apple-darwin cargo test --locked --lib ollama::

The progress-stream test feeds a chunk-framed response one byte at a time, which is what a real socket does and what a whole-buffer test cannot reproduce. It found two decoder defects that would otherwise only have appeared mid-download on a slow link: a forgotten chunk terminator, and the zero-length terminating chunk re-parsing as a chunk size.

Not yet done

  • No llama.cpp transport; Ollama remains the only planning backend.

  • No disk-space precheck before a pull. Ollama reports the failure, but BATIQ could warn before a 40 GB download starts.

  • No model recommendation. The panel lists what exists and pulls what is asked for; it does not suggest a model suited to the machine's memory.

  • Detection does not notice a runtime bound to a non-default port, because the loopback-only contract fixes the port at 11434.

Playback and export

Playback favors throughput. Paused video work and exports use the source's precision.

Source quality

sequence::DecodeQuality is independent of viewer resolution. Playback may use the persistent 8-bit video decoder and half-float disk cache. For video, Full bypasses that cache and decodes the original source. Image sequences retain their existing cache policy in both modes. A full-resolution viewer image is not, by itself, evidence of full source precision.

When video playback stops, the app cancels pending preview work, advances the source generation, and requests the paused frame again. Results from the previous generation cannot repopulate the precise frame's caches. Subsequent paused adjustments reuse those precise source pixels. Image-only pause/resume must not invalidate warmed source, render, or GPU caches.

Export sources

render/source.rs owns source resolution and per-render video decoders. A Write loads its upstream Reads; unrelated media is not an export dependency. Ordered video reads use persistent 16-bit RGBA pipes. Random access retains the exact frame-number decoder. Image sequences retain their normal EXR/raster path.

Each render owns its decoder lifetime, so completion, failure, or cancellation drops the pipes. Color preparation still uses the render's captured OCIO state.

Maintenance and verification

The app/ modules separate playback, source scheduling, and export controls from the main UI. Keep source quality decisions at request boundaries and preserve generation checks when changing queues or caches.

Run the sequence, render, video, and app library tests after changes here. BATIQ_TEST_VIDEO enables real-video decoder tests. Set BATIQ_TEST_PRECISION_VIDEO to a high-bit-depth video gradient with more than 256 distinct levels to verify that paused decoding bypasses playback precision. Benchmark decoding separately from graph evaluation and output encoding; decoder improvements are not whole-render speed guarantees.

GPU display colour

The viewer compiles OCIO 2.5's generated analytical GLSL through Naga to the existing wgpu device (Metal on Mac). ACES 1.3/2.0 display transforms and their original float32 tables stay on the GPU. The transform is cached independently of graph edits; gamma is a uniform. CPU-only graph results upload once when dirty. Scene buffers and exports retain their existing float precision.

Unsupported custom shaders retain the CPU colour path. Tests compare native Metal display output with CPU OCIO including alpha, channels and exposure. The opt-in gpu_ocio_display_throughput test measures a real EXR supplied via BATIQ_DISPLAY_BENCH_IMAGE; display throughput is not whole-app playback FPS.

OCIO and ACES execution

BATIQ executes OpenColorIO 2.5.0 using the native CPU processor shipped in its embedded Python runtime (CPython 3.11.16, NumPy 2.3.3). Release packaging pins and verifies wheel SHA-256 hashes for macOS arm64/x86_64, Windows x86_64 and Linux x86_64. No system Python or OCIO installation is required.

Colour contract

  • ACES 2.0 uses cg-config-v4.0.0_aces-v2.0_ocio-v2.5, display sRGB - Display, view ACES 2.0 - SDR 100 nits (Rec.709).

  • ACES 1.3 uses cg-config-v2.2.0_aces-v1.3_ocio-v2.4, display sRGB - Display, view ACES 1.0 - SDR Video. This replaces the old fitted curve.

  • Display and inverse processors use OPTIMIZATION_NONE and full float32 input. The old forward and inverse cube assets are removed: their finite domain and interpolation caused visible errors in saturated/HDR colours.

  • Custom configs execute their actual colourspace names, roles, context, external LUTs, looks and display views. Custom definitions override built-in names. Reload invalidates native LUT caches and BATIQ's processing caches.

  • Read and ColorSpace nodes preserve explicit alpha association. Colour transforms operate on straight RGB; requested premultiplication follows. Unknown or invalid transforms report errors instead of silently bypassing.

  • The Viewer uses the evaluated frame's colourspace, then converts to the project working space before scene-linear exposure and the selected view. Viewer gamma follows the view. The three working-space choices are ACEScg, ACES2065-1 and Linear sRGB; custom configs must provide the selected space.

  • Default PNG/JPEG writes use the selected view without temporary Viewer exposure/gamma adjustments. Explicit output colourspaces and EXR writes execute named transforms. The output display is SDR sRGB; this is not a monitor ICC/HDR calibration implementation.

  • sRGB - ACES 2.0 inverse explicitly inverts BATIQ's fixed ACES 2.0 view. Historical BATIQ sRGB - Display labels remain compatible in built-in mode. An arbitrary photograph need not have been rendered through that view, and inversion does not recover its original scene radiance. The display transform is not globally invertible; round trips are not an accuracy oracle.

The linear AP0/AP1/Rec.709 matrix paths remain on CPU/GPU. Supported Viewer transforms use OCIO-generated analytical shaders and float32 tables on the GPU. Custom shaders that cannot use this path fall back to the native CPU processor. See Playback and export for the current display path and its verification boundaries.

Reference evidence

scripts/verify_color_reference.py --strict compares the embedded bridge with separately constructed direct OCIO processors. Deterministic probes include black, neutral, saturated, negative and HDR/out-of-domain values. On macOS:

ProbeSamplesMaximum channel difference
Native forward ACES 2.040960
Native inverse ACES 2.040960
Clamped display output40960

These finite probes establish implementation agreement, not Academy certification or a global mathematical error bound. Rust also consumes 128 independent ACES 1.3/2.0 view vectors and 64 inverse vectors per CPU architecture over valid SDR display codes [0, 1] through the actual embedded runtime, with float32 platform tolerance 2e-4 (relative scaling for HDR inverse values). Inverse golden vectors are architecture-specific because OCIO itself has amplified float32 differences near the inverse white endpoint (about 0.053 at a scene-linear value of 128 between arm64 and x86_64). Strict same-platform comparisons still require agreement with the official processor, including the out-of-range stress probes. The existing 63 linear conversion vectors use tolerance 2e-6 * max(1, abs(reference)) on both CPU and required GPU paths.

Focused regressions cover config roles/context/LUT reload, actual source-space handling, Read alpha, invalid-transform errors and raster/view consistency. The cross-platform workflow executes native packaged-runtime tests and Viewer GPU upload/readback on Metal, Vulkan and DX12; results are recorded in the PR.

Reproduce

python -m pip install opencolorio==2.5.0 numpy==2.3.3
python scripts/verify_color_reference.py --strict
cargo test --locked --lib ocio
cargo test --locked --lib aces
BATIQ_REQUIRE_GPU=1 BATIQ_GPU_TEST_BACKEND=metal cargo test --locked required_gpu_display

For embedding, point PYO3_CONFIG_FILE to the config produced by scripts/python_runtime.py config and BATIQ_PYTHON_HOME to the complete runtime containing the pinned wheels. Use vulkan on Linux or dx12 on Windows; software adapters additionally set BATIQ_GPU_TEST_FALLBACK=1.

References: official ACES 2.0 config release, OpenColorIO Python API.

Embedded Python in BATIQ

BATIQ embeds CPython with PyO3 0.29 and registers the built-in batiq module before interpreter startup. The interpreter runs on a dedicated batiq-python thread. The egui thread remains the sole owner of the graph, project, viewer, and UI state.

Command boundary

Bindings are an adapter over src/host_api, not a second mutable application model. Each operation creates a typed HostCommand, sends it through a bounded channel, wakes the main thread, and waits for a typed reply with a finite timeout. PyO3 detaches the GIL during that wait. Rust maps project, node, validation, unavailable, and timeout failures to deliberate Python exceptions.

Node wrappers contain only a stable node ID. They resolve that ID for every operation, preventing stale Python copies from overwriting current graph state.

Callbacks and menus

Main-thread events enqueue notifications to Python. Event and menu callbacks run on the Python thread; failures are printed/logged and do not stop sibling callbacks or the compositor. Native egui menu actions enqueue Python work instead of synchronously waiting for it. This avoids the cycle where the main thread waits for Python while Python waits for a main-thread command.

Startup and runtime paths

BATIQ_STARTUP_MODULES imports comma-separated module names after the native module is registered. BATIQ_PYTHON_HOME and BATIQ_PYTHONPATH explicitly configure development search paths, and BATIQ removes the current working directory from the inherited path.

Release artifacts ship checksum-pinned CPython 3.11.16 runtimes from astral-sh/python-build-standalone release 20260814. The build links PyO3 against that exact target runtime, then packages it under BATIQ.app/Contents/Resources/python on macOS or python/ beside the BATIQ executable on Windows and Linux. BATIQ supplies the bundled directory as PyConfig.home before interpreter initialization; it never falls back to a different Python found on PATH. Windows also places the runtime DLLs beside the executable for loader discovery, macOS adds an executable-relative rpath, and Linux adds an $ORIGIN/python/lib rpath. The Linux package's public batiq entry point is a dependency-free recovery launcher; the Python-linked desktop executable is batiq-app. The launcher repairs a legacy binary-only update by verifying and installing the complete matching release package.

scripts/python_runtime.py owns the version, release asset names, and SHA256 checksums. Updating Python means updating all target entries together, running its unit tests, and validating every packaged executable with --python-smoke-test. The pinned stripped inputs add roughly 25–31 MB to each release before final archive compression; the macOS runtime currently occupies about 71 MB when unpacked. Third-party studio packages add their own size and must support Python 3.11.

If initialization itself fails, the compositor can report that Python integration is unavailable only after the operating-system loader has started BATIQ. Release packaging therefore treats the runtime library, standard library, loader path, code signature, and smoke test as one required artifact.

Boundary with headless Python

batiq-headless remains an external, isolated JSON-RPC process for rendering, validation, and migration. Embedded scripting is for the live desktop; it is not a replacement for deterministic render isolation or farm execution.

BATIQ Python packages

For the wire-level command reference, see the headless CLI guide.

BATIQ has two intentionally separate Python entry points:

  • import batiq is the live embedded API. It exists inside the running BATIQ desktop and operates on the current project through typed host commands.

  • import batiq_api is the optional external SDK in this directory. It is for batiq-headless, offline validation/migration, render automation, Deadline, and standalone provider integrations.

The embedded API owns no copied graph. batiq.nodes returns ID-backed Node handles; reads and mutations resolve on the Rust/egui main thread through a bounded request/reply channel. The GIL is released while waiting, requests have finite timeouts, and NodeNotFoundError reports deleted handles. Events and menu callbacks are asynchronous on the dedicated Python runtime; callback failures are logged without freezing the compositor or creating a main-thread deadlock.

External SDK development

cd python
python3 -m venv .venv
.venv/bin/python -m pip install -e .
.venv/bin/python -m unittest discover -s tests -v

The external SDK keeps its version-negotiated batiq-headless JSON-RPC transport. It starts a fresh worker per operation, keeps protocol stdout separate from stderr, enforces timeouts, and supports validation, migration, and explicit Write-node rendering. BatiqApplication.open() launches an external desktop executable; it does not control an already running BATIQ window.

Optional adapter dependencies remain independent:

.venv/bin/python -m pip install -e '.[ayon]'
.venv/bin/python -m pip install -e '.[ftrack]'

The external batiq_api.integrations.ayon adapter is standalone automation only. It does not install a host integration, create BATIQ menus, or load an AYON runtime into BATIQ; host integrations are separately installed projects.

Embedded startup configuration

Development/runtime configuration is explicit through BATIQ_PYTHON_HOME, BATIQ_PYTHONPATH, and comma-separated BATIQ_STARTUP_MODULES. Release artifacts bundle the checksum-pinned CPython 3.11.16 runtime described in ../docs/PYTHON_EMBEDDING.md; packaged BATIQ does not depend on an arbitrary system python3. Do not treat embedded sys.executable as a standalone launcher.

The external BatiqProject inspection model supports project format through version 30; the BATIQ Rust/headless loader is currently version 38 in this checkout. Generic project and node metadata is JSON-compatible and namespace-owned by integrations; metadata is cleared on duplicate/copy-paste so pipeline container identity is not accidentally cloned.