or
September 15, 2026 · View on GitHub
Block a scene, pose the cast, cut the camera — in a browser tab.
Created and maintained by Doyun at NomaDamas.
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 scene | Create 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 camera | Right-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 camera | Add 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 video | One 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 anything | Every 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 motion | Pose 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 photo | Drop 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 AI | Connect 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:setuponce; 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=gvhmris 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:
| Host | Backend installed | Why |
|---|---|---|
| macOS Apple Silicon, RAM > 32 GB | kimodo-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 GB | kimodo.cpp + Metal (GGML) | Residency doesn't fit; streaming transformer layers from disk is the right trade |
| Linux + working NVIDIA CUDA | NVIDIA Kimodo (PyTorch) | Full CUDA acceleration with the upstream stack |
| Other Unix, no CUDA | kimodo.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
.cclayprojectfiles.capture_frame,set_prompt_blocks,generate_motion, andapply_batchrequire 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:
| Path | Role |
|---|---|
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.
| Rule | Value |
|---|---|
| Active jobs per account | 1 |
| Daily cap | 2 per account |
| Global waiting cap | 200, then submissions are refused |
| Lease / heartbeat / hard timeout | 15 min lease, renewed every 60 s, 20 min hard stop |
| Attempts | 2 (one automatic retry); a failed job refunds the daily cap |
| Result retention | 30 days, then the R2 object is deleted |
| Prompt limit | shared 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
| Input | Action |
|---|---|
| Right-drag | Look around (fly) |
| RMB + WASD | Walk while flying |
| RMB + Q/E | Crane down / up |
| RMB + Shift | Boost fly speed 2.6× |
| Middle-drag | Pan |
| Alt + drag | Orbit the selection |
| Scroll | Dolly; while flying, sets the fly speed instead |
| Click | Select; empty space clears |
| Transform strip | Move / rotate / scale tool, snap |
| Ctrl/Cmd (during drag) | Invert grid snapping |
| Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z | Undo / redo |
| Esc | Cancel the in-flight drag |
| End | Drop the selection to the surface |
| Ctrl/Cmd+D | Duplicate the selection |
| Delete / Backspace | Delete the selection |
| F | Frame the selection |
| Look through / Esc | Fly / leave the shot camera |
| Cmd/Ctrl+B | Show / hide the Agent panel |
Every control's home is recorded in docs/studio-ui-ia.md, with the rule behind each placement.
Validate
| Command | Covers |
|---|---|
npm run test:history | Undo/redo store and transaction coordinator |
npm run test:scene-objects | Scene-object model |
npm run test:hierarchy | Hierarchy panel model |
npm run test:objects | Gizmo interaction in a real browser — needs npm run dev:ui in another shell |
npm run test:theme / test:appearance / test:layout | UI theme, appearance, layout |
npm run test:lifecycle | Dev-server process lifecycle |
npm run test:ardy | Motion conversion, playback, and IK pipeline |
node tools/run-tests.mjs | Every Node verification file (what CI runs) |
npm run qa:browser -- node test/qa-camera-tutorial-browser.mjs | The 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.mjs | Simultaneously visible controls per mode (budget: Scene ≤35 / Camera ≤38 / Motion ≤52) |
cd mcp && npm install && npm run verify | MCP server over real stdio — all 420 framing combinations |
cd mcp && npm run verify:live | Live-control protocol against a fake editor (same npm install first) |
npm run build | Production 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:
| Event | Purpose |
|---|---|
install:first_launch | First run of the official npm package |
app:session_started | Start of an official npm package session |
app:session_ended | Session duration, action count, and scenes touched (bucketed) |
feature:used | One signal per feature per session |
$pageview | Funnel and drop-off analysis |
scene:created | Funnel and drop-off analysis |
scene:loaded | Funnel and drop-off analysis |
project:saved | User-owned project persistence |
project:opened | Return to a saved project (age bucket) |
craft:first_action | Funnel and drop-off analysis |
craft:first_edit | First meaningful Studio edit (edit_kind, definition_version: 1) |
playground:first_edit | First meaningful Playground edit, separate from the Studio funnel |
motion:backend_state | Motion capability at session start (none, local_kimodo, or hosted) |
motion:generate_requested | Explicit Generate request, never prompt-block authoring |
motion:preflight_blocked, motion:preflight_passed | Request readiness outcome before execution |
motion:job_started | Correlated generation execution starts |
motion:job_succeeded, motion:job_failed | Generation result, with normalized failure/cancellation codes |
motion:result_applied | Generated result applied to its requesting character, separately from job success |
export:blocking_frame_succeeded | Funnel and drop-off analysis |
activation:completed | Funnel and drop-off analysis |
hosted:composer_viewed, hosted:login_started, hosted:ticket_created | Hosted demo funnel |
hosted:result_opened, hosted:opened_in_studio | Hosted 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.