or

September 15, 2026 · View on GitHub

CozyClay

Block a scene, pose the cast, cut the camera — in a browser tab.

Created and maintained by Doyun at NomaDamas.

License: AGPL-3.0 npm Node 22.13+ Stars

Try it in the browser · Greybox to AI video · Previs software compared · Quick start · Features · AI control · Controls · Issues


CozyClay is a browser-based previs studio built with Three.js and React Three Fiber. Block a scene, pose the cast, cut the camera on a timeline, then hand the same shot to an AI video model (Seedance, Kling, Veo, or your own) as a first frame, a reference clip, or a prompt — all from one local workspace. The keyframe pack's greybox clip is the input Seedance 2.5 documents as white-model control: "Use the white-model reference video as the sole guide for camera movement, pacing, framing, subject motion, and blocking" (seed.bytedance.com/en/seedance2_5). MiniMax H3, Wan 3.0, LTX Desktop and fal render-to-real take the same plain RGB clip.

npx cozyclay

That is the whole install. Not sure yet? Try it in the browser first — a seven-step camera tutorial on a live scene — then keep going on your machine with the same set:

npx cozyclay --scene city-block

The studio ships seeded with a pre-generated motion clip, so you can scrub the timeline, drive the cameras and draw a dolly rail straight away — generating new motion is optional and uses the Kimodo bridge when configured.

New to the camera? The same seven-step tutorial runs inside the Studio on the City Block set: Settings ▾ → Camera tutorial, or open http://127.0.0.1:5180/app/?tutorial=camera. Each step points at the control it needs and completes only when you actually make the move.

Demo

https://github.com/user-attachments/assets/1d0113e5-6922-443d-affc-1bdabc666247

What you can do

Stage a sceneCreate primitives and set pieces, then move, rotate and scale them with the transform strip's gizmo. Grid snapping is a preference, not a law — hold Ctrl mid-drag to invert it. A bird's-eye Top-View drives 2D root waypoints for character paths. View ▾ on the viewport bar holds the reference grid and Auto Color — Blender's random viewport color, so twenty grey blockout boxes stay tellable apart without touching the colors you authored (captures include the display colors while it is on).
Fly the cameraRight-drag flies (WASD walks, Q/E cranes), middle-drag pans, Alt+drag orbits the selection, click selects, F frames — the muscle memory you already have from a 3D editor. Selecting the camera switches to Camera mode. Look through in the Shot monitor puts you behind the shot camera with the same bindings; click the on-screen Shot camera indicator or press Esc to return to the free camera.
Cut and move the cameraAdd shots on the timeline, draw a dolly rail on the Top-View, set speed, height and crane, and preview the move through the shot camera. Each shot carries a Target model (Seedance 2.5, Kling 2, Veo 3, self-hosted MiniMax-H3) and is flagged when the cut runs past that model's limits.
Export for AI videoOne Export ▾ menu: a keyframe pack (first/last frame, clip, camera JSON, prompt, README) as a zip, an mp4 of the shot, depth + normal conditioning passes, a storyboard contact sheet, and an OTIO cut list. Seedance 2.5 reads the pack's greybox clip as its white-model reference video, and MiniMax H3, Wan 3.0, LTX Desktop and fal render-to-real accept the same clip. The Shot Prompt turns the framing into a structured prompt for the model you picked.
Undo anythingEvery scene mutation goes through one history store: a drag, a scrub, an inspector edit is exactly one undo entry. Esc cancels an in-flight drag and restores the pre-drag transform.
Generate motionPose characters and export poses, sequence multi-phase motion as Prompt Blocks on a resizable timeline, send them to Kimodo, then play the result back with sparse IK correction where the generated motion needs fixing. Draw over a joint's trail to reshape a take, keep most of it and regenerate a window, and step back through its history.
Capture motion from video or a photoDrop a clip or a still: the GPU box runs GVHMR and the result is retargeted onto the character with stabilisation, contact correction and a quality gate.
Direct it with an AIConnect Claude — or any MCP client — and ask for a shot in plain language. It places the cast, frames “a low wide profile”, generates multi-phase motion, and the viewport moves in front of you. See AI control. Inside the Studio, View ▾ → Panels → Agent panel (or Cmd/Ctrl+B) opens a chat column that signs in with your ChatGPT account and works the same scene.

Requirements

  • Node.js 22.13 or newer
  • npm, or bun
  • A Chromium-based browser
  • A machine running Kimodo, for motion generation — run npm run kimodo:setup once; the installer detects the host and picks the best-supported backend (see the route table below), then downloads the matching checkpoint and text-encoder stack. An SSH-accessible NVIDIA box is the classic target; the same box runs GVHMR for video and photo mocap (CCLAY_EXTRACT_BACKEND=gvhmr is the default; there is no browser fallback).

Quick start

npx cozyclay
# or
bunx cozyclay

That downloads the built studio and opens it at http://127.0.0.1:5180/app/. Nothing to compile, no dependency tree to install. Useful flags: --port 5200, --no-open, --no-motion, --scene city-block (start on the bundled starter scene instead of an empty room; the first-run dialog offers the same under Start from a scene, and a .cclayproject downloaded from the browser tutorial opens with Open a project).

A global install gives you cclay, the same command with less typing. Once a day the launcher checks npm for a newer release and prints a one-line notice after the studio is up; it stays quiet when you're current or offline. cclay update installs the latest release, and --no-update-check skips the check entirely.

Motion generation uses Kimodo by default once you point it at a Kimodo host:

CCLAY_KIMODO_HOST=user@your-gpu-box npx cozyclay

Install the worker once, on whichever machine should generate motion:

CCLAY_KIMODO_HOST=user@your-gpu-box npm run kimodo:setup
# or directly on the machine itself:
bash tools/kimodo/setup-on-box.sh

The installer is a router — it detects OS, architecture, RAM and CUDA, and installs the best-supported Kimodo variant for that host:

HostBackend installedWhy
macOS Apple Silicon, RAM > 32 GBkimodo-mlx (MLX/Metal)The 15 GB text encoder stays resident in unified memory: ~0.9 s warm generation vs ~37 s streaming on an M4 Max 64 GB
macOS Apple Silicon, RAM ≤ 32 GBkimodo.cpp + Metal (GGML)Residency doesn't fit; streaming transformer layers from disk is the right trade
Linux + working NVIDIA CUDANVIDIA Kimodo (PyTorch)Full CUDA acceleration with the upstream stack
Other Unix, no CUDAkimodo.cpp CPU (Vulkan when available)Local GGML execution without a GPU

The installer supports all routes above. CUDA motion generation works over SSH. For local generation, set CCLAY_KIMODO_BACKEND to kimodo-mlx, kimodo.cpp-metal, or kimodo.cpp-cpu and leave CCLAY_KIMODO_HOST unset: single unconstrained SOMA30 prompts at 30 fps now convert to the Studio NPZ format. The MLX wrapper retains runtime arrays that the upstream CLI's JSON omits; both local routes expand the 30-joint output using NVIDIA's canonical SOMA77 hierarchy and relaxed-hand rest pose, then run forward kinematics before retargeting to cskel27. Sequencing, waypoints, pinned poses, and preserve still require CUDA/SSH and are explicitly refused locally. Local conversion is tested with synthetic outputs; model inference requires the installed runtime and weights. The local integration originated in #239.

For the CUDA route, the installer places the checkout at $HOME/.cozyclay/kimodo and its virtual environment at $HOME/.cozyclay/kimodo-venv, then links the venv at $CCLAY_KIMODO_REPO/.venv where the Studio runner expects it.

Inspect the route without changing anything — and override it when you know better:

bash tools/kimodo/setup-on-box.sh --dry-run
bash tools/kimodo/setup-on-box.sh --backend kimodo.cpp-metal   # or CCLAY_KIMODO_BACKEND=...

The RAM threshold between the two macOS routes defaults to 32 GB (CCLAY_KIMODO_MLX_MIN_RAM_GB).

AI control (MCP)

The studio ships an MCP server, so an AI assistant can drive it — the same scene, the same viewport, live:

“Put a detective and a courier in an alley, give me a low wide profile shot, then make her stand up from the chair, sprint, and trip.”

{
  "mcpServers": {
    "cozyclay": {
      "command": "npx",
      "args": ["-y", "cozyclay", "mcp"]
    }
  }
}

Drop that into claude_desktop_config.json (or any MCP client config) and restart the client. The first run automatically installs the MCP SDK's 95-package tree; opening the studio never waits on it, so those dependencies are fetched only when you actually want the server.

  • Editor open? Tool calls move the visible viewport — camera, cast, set, generated motion, prompt blocks on the timeline.
  • No editor? Scene and project tools run headless: block scenes, derive film vocabulary (“wide shot · right profile · knee level · 24mm”), render AI video prompts, and write .cclayproject files. capture_frame, set_prompt_blocks, generate_motion, and apply_batch require the live editor.

Tools, transports and the live-control protocol are documented in mcp/README.md.

From a clone

git clone https://github.com/NomaDamas/CozyClay.git
cd CozyClay
npm install
npm run dev

Open http://127.0.0.1:5180/app/ for the Studio (the root redirects there). The Workflow canvas is at http://127.0.0.1:5180/workflow/. npm run dev starts the studio together with its local Kimodo bridge once CCLAY_KIMODO_HOST points at a GPU box; without that variable it starts the studio alone and says so, and Block Generation stays unavailable until you set it. npm run dev:ui starts the browser UI alone in every case. The bridge listens on loopback only; Kimodo host variables are documented in tools/kimodo/setup-on-box.sh.

Workflow canvas

The Workflow canvas at http://127.0.0.1:5180/workflow/ is a node editor around the Studio: a CozyClay Scene node with a live viewport that previews the shot camera, a Shot Prompt node that builds a structured prompt from the capture, Image nodes (versions, A/B, pinned references) and a Video node that runs through your own ComfyUI (COZYCLAY_COMFY_URL) or fal (FAL_KEY) — bring your own key, nothing is proxied. The Agent panel on the right signs in with your ChatGPT account (Codex OAuth, token stored in ~/.config/cozyclay/) and builds and runs canvas nodes for you. Graphs are saved in the browser and execute locally.

Hosted demo

Installing a GPU motion backend is the hard part, so cozyclay.org also runs a queued demo: a visitor writes one prompt, gets a ticket link, and a GPU box owned by the maintainer generates the motion and uploads it. The visitor never installs anything and never leaves the site — the result opens in the studio itself.

The pieces live in this repository, under AGPL-3.0-or-later like everything else:

PathRole
demo/, d/Static composer and ticket/result pages, built into dist/ by the same npm run build
workers/api/Cloudflare Worker queue API (D1 for state, R2 for results), with its own pinned toolchain
tools/demo-worker/The GPU-box poller. Outbound fetch only — it never opens a listening socket

Queue policy. Jobs run in a single FIFO queue. All of these values live in workers/api/src/policy.js; nothing else carries a copy.

RuleValue
Active jobs per account1
Daily cap2 per account
Global waiting cap200, then submissions are refused
Lease / heartbeat / hard timeout15 min lease, renewed every 60 s, 20 min hard stop
Attempts2 (one automatic retry); a failed job refunds the daily cap
Result retention30 days, then the R2 object is deleted
Prompt limitshared with the studio via tools/ardy/prompt-limits.mjs

Secrets. Never committed. Configure each with wrangler secret put against workers/api/wrangler.toml: GOOGLE_CLIENT_SECRET, CC_WORKER_SECRET, SESSION_SIGNING_KEY, TURNSTILE_SECRET_KEY. The non-secret GOOGLE_CLIENT_ID and TURNSTILE_SITE_KEY vars in wrangler.toml must also be replaced before a real deployment.

Running the API locally.

npm run demo:api:install   # npm --prefix workers/api ci
npm --prefix workers/api exec -- wrangler d1 migrations apply cozyclay-demo --local
npm run demo:api           # wrangler dev on 127.0.0.1:8787

Running the GPU-box worker. The hosted queue worker has its own isolated runtime and reaches the API outbound only. It is independent from the local Studio's Kimodo backend.

CC_DEMO_API_BASE=https://api.cozyclay.org \
CC_WORKER_ID=box1 \
CC_WORKER_SECRET=... \
  npm run demo:worker

See workers/api/README.md for the deployment, migration and rollback runbook, and tools/demo-worker/README.md for service units, environment-file permissions and the listening-socket check.

Controls

InputAction
Right-dragLook around (fly)
RMB + WASDWalk while flying
RMB + Q/ECrane down / up
RMB + ShiftBoost fly speed 2.6×
Middle-dragPan
Alt + dragOrbit the selection
ScrollDolly; while flying, sets the fly speed instead
ClickSelect; empty space clears
Transform stripMove / rotate / scale tool, snap
Ctrl/Cmd (during drag)Invert grid snapping
Ctrl/Cmd+Z, Ctrl/Cmd+Shift+ZUndo / redo
EscCancel the in-flight drag
EndDrop the selection to the surface
Ctrl/Cmd+DDuplicate the selection
Delete / BackspaceDelete the selection
FFrame the selection
Look through / EscFly / leave the shot camera
Cmd/Ctrl+BShow / hide the Agent panel

Every control's home is recorded in docs/studio-ui-ia.md, with the rule behind each placement.

Validate

CommandCovers
npm run test:historyUndo/redo store and transaction coordinator
npm run test:scene-objectsScene-object model
npm run test:hierarchyHierarchy panel model
npm run test:objectsGizmo interaction in a real browser — needs npm run dev:ui in another shell
npm run test:theme / test:appearance / test:layoutUI theme, appearance, layout
npm run test:lifecycleDev-server process lifecycle
npm run test:ardyMotion conversion, playback, and IK pipeline
node tools/run-tests.mjsEvery Node verification file (what CI runs)
npm run qa:browser -- node test/qa-camera-tutorial-browser.mjsThe seven-step tutorial driven with real input, beacons pinned to their controls
QA_URL=… CDP_PORT=… OUT=/tmp/count npm run qa:browser -- node tools/qa/studio-control-count.mjsSimultaneously visible controls per mode (budget: Scene ≤35 / Camera ≤38 / Motion ≤52)
cd mcp && npm install && npm run verifyMCP server over real stdio — all 420 framing combinations
cd mcp && npm run verify:liveLive-control protocol against a fake editor (same npm install first)
npm run buildProduction build

Ad-hoc browser QA, while a dev server is available (the browser opens the studio at /app/):

npm run qa:browser -- node <qa-script>

Contributing

Found something broken, or want a feature? Open an issue — bug reports with a repro are the most useful thing you can send. Contributions are accepted under AGPL-3.0-or-later.

Repository hygiene. Generated motion archives, QA output, build output, logs and local runtime artifacts are not source files and must not be committed. Keep tools/ardy/out/, artifacts/, dist/, .gjc/ and .npz files local.

All runtime libraries intentionally live in devDependencies because the published npm package ships the prebuilt dist/, so npx cozyclay must not install the studio's dependency tree.

Analytics & privacy

The hosted site at cozyclay.org collects anonymous usage analytics via PostHog (US Cloud). There are no cookies and no session recording, and Do-Not-Track is respected. A random pseudonymous identifier is kept in your browser's localStorage so that returning visits and retention can be counted; it is never linked to an account or project content and is removed by clearing site data or using the opt-out toggle.

Events collected:

EventPurpose
install:first_launchFirst run of the official npm package
app:session_startedStart of an official npm package session
app:session_endedSession duration, action count, and scenes touched (bucketed)
feature:usedOne signal per feature per session
$pageviewFunnel and drop-off analysis
scene:createdFunnel and drop-off analysis
scene:loadedFunnel and drop-off analysis
project:savedUser-owned project persistence
project:openedReturn to a saved project (age bucket)
craft:first_actionFunnel and drop-off analysis
craft:first_editFirst meaningful Studio edit (edit_kind, definition_version: 1)
playground:first_editFirst meaningful Playground edit, separate from the Studio funnel
motion:backend_stateMotion capability at session start (none, local_kimodo, or hosted)
motion:generate_requestedExplicit Generate request, never prompt-block authoring
motion:preflight_blocked, motion:preflight_passedRequest readiness outcome before execution
motion:job_startedCorrelated generation execution starts
motion:job_succeeded, motion:job_failedGeneration result, with normalized failure/cancellation codes
motion:result_appliedGenerated result applied to its requesting character, separately from job success
export:blocking_frame_succeededFunnel and drop-off analysis
activation:completedFunnel and drop-off analysis
hosted:composer_viewed, hosted:login_started, hosted:ticket_createdHosted demo funnel
hosted:result_opened, hosted:opened_in_studioHosted result funnel

Geo data comes from ingest-time GeoIP country lookup only — no precise location is collected. Prompt text, asset names, file names, project content, local paths, and any user-entered text are never collected.

Motion requests use one ephemeral random request_id across intent, preflight, job outcome and application. The September 15, 2026 issue #273 contract stops emitting the ambiguous motion:generate_blocked; old data is not relabeled or included in the new demand metric. The exact properties, entry-point mapping and deduplicated funnel query are in docs/analytics-queries.md. Browser regression QA: QA_URL=http://127.0.0.1:5254/app/ CDP_PORT=9493 node tools/qa-browser.mjs -- node test/qa-motion-intent-browser.mjs.

The official npm package also measures anonymous first launches, sessions, and the same in-app funnel on its 127.0.0.1 studio. It stores one random installation identifier in ~/.config/cozyclay/state.json so returning use can be counted across normal CLI restarts, ports and browser storage resets ($XDG_CONFIG_HOME/cozyclay/state.json when configured). The browser SDK uses memory-only storage bootstrapped from that identifier; it does not generate a new install identity on each launch. Different state files get independent random IDs, with no fingerprinting, account linking or cross-device matching. Do not copy this state file between users. Source checkouts, forks, development servers, CI, and tests do not send analytics. Official npm artifacts carry a signature checked by the launcher, so copying or repackaging the source does not enable telemetry.

Every regular event and the session-end beacon use the same distribution (npm or hosted), app_version (when available), origin_kind (local or hosted), a coarse operating-system label, and (for npm sessions) install_kind (npx or global). Source checkouts are classified as clone and remain telemetry-off. The first npm launch may optionally answer a one-line channel question (x, hn, reddit, github, friend, other, or skip); skip sends no acquisition value. Session duration and action counts are buckets, and project events never include names, paths, prompts, or timestamps.

Explicit internal / QA traffic

Internal marking is opt-in and independent of telemetry consent:

cclay telemetry internal on
cclay telemetry internal off

The CLI stores the boolean internalQa in the same state file and applies it on the next launch/reload. On the hosted Studio or Playground, open https://cozyclay.org/app/?internal_qa=1 to mark that browser, or use ?internal_qa=0 to clear the marker. Only an explicit single 1 or 0 is accepted; the choice persists in cozyclay.internalQa localStorage for that origin. The URL parameter is read by the app, not by a second landing snippet. The npm app ignores this parameter and uses the CLI choice.

All app captures, including $pageview and the end beacon, carry boolean internal_qa (false by default). Queries exclude only explicitly marked events. Localhost, ports, country and source checkout do not identify internal people; unmarked official local-app traffic stays in the external cohort. Marking never enables collection on a disabled build, unapproved origin, source checkout, CI, Do-Not-Track or opted-out session. Existing open tabs must be reloaded after CLI changes. Historical unmarked QA cannot be inferred or retroactively removed.

See the external cohort queries and baseline procedure for complete KST weeks, distinct-ID denominators, retention maturity and coverage limits.

First-edit definition (version 1)

craft:first_edit and playground:first_edit carry only edit_kind and the numeric definition_version: 1. The closed edit-kind set is pose_edit, object_insert, cutout_insert, object_transform, shot_add, shot_edit, camera_key_record, rail_edit, prompt_block_add, and prompt_block_edit. An event requires an actual, successful authoring change through the shared semantic hook, whether the change came from the UI, window.__cozyclay, or MCP live control. Prompt text and scene values are never event properties.

Deduplication is per editor session (one App mount), not per install. Repeated pointer callbacks, React effects, later edits, undo, and redo cannot emit another first edit in that mount. A reload or a fresh editor mount starts a new boundary. Studio and Playground use separate event names and never contribute to each other's first-edit funnel.

Camera navigation (including look-through fly, orbit, and wheel dolly), playback/scrubbing, passive scene or project loading, initialization, restoration, tutorial auto-seeding, failed/no-op commands, and undo/redo do not qualify. Explicitly recording a camera key or authoring a shot or rail does qualify; moving a viewing camera alone does not.

The legacy craft:first_action / playground:first_action streams retain their existing insertion/camera-key triggers and deduplication unchanged during migration. Their historical first-launch conversion is not the percentage of people who edited or used the app. Do not combine those events with version 1 first-edit events in a conversion numerator.

See the version-filtered ordered funnel query for a new-install cohort with explicit event ordering and a conversion window.

The npm package prints this disclosure once on first launch. Control it at any time:

cclay telemetry status
cclay telemetry off
cclay telemetry on

COZYCLAY_TELEMETRY=0 and DO_NOT_TRACK=1 disable collection for a launch. The in-app toggle under Settings ▾ → Privacy changes the same npm-package setting and removes its anonymous installation identifier. Hosted-site visitors can opt out with that toggle, browser Do-Not-Track, or a content blocker. Disabling npm telemetry deletes the stored ID; enabling it later creates an unlinked new ID. The old first-launch receipt is retained, so re-enabling does not manufacture another new-install event. Environment overrides only suspend collection and do not erase the stored ID. Neither opt-out path sends a session-end beacon after collection has been disabled.

PostHog's free plan retains events for 1 year.

License & credits

GNU Affero General Public License v3.0 or later — see LICENSE and the transition details in LICENSING.md. Modified network services must offer their users the corresponding source. Third-party projects retain their own licenses and copyright; see THIRD_PARTY_NOTICES.md.

The hosted demo worker may use an externally installed NVIDIA ARDY runtime. ARDY is a separate third-party project owned and maintained by NVIDIA; it is not included in this repository, and CozyClay is not affiliated with or endorsed by NVIDIA. The local Studio uses Kimodo instead.