From zero to live AI video
Before you build, take a 60-second sneak peek — Lucy edits your camera feed live. Type a prompt, drop a reference image, tap a preset. Then scroll: five short steps to this exact demo, running in your own app.
Install the SDK
One package. This guide builds in the browser with JavaScript — everything maps 1:1 to Python and Android.
npm install @decartai/sdkpip install decart// settings.gradle.kts — add the JitPack repository
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
// app build.gradle.kts — add the dependency
dependencies {
implementation("com.github.DecartAI:decart-android:0.2.0")
}SDK references: JavaScript · Python · Android
Get your API key
Create a key in the platform dashboard — it takes under a minute. Already have one? Skip straight to step 03.
# .env.local — keep the key out of source control
DECART_API_KEY=your-api-key-hereHow this page does it — the sneak peek above runs on this same flow. Our key lives on the server; your browser only ever received a short-lived token scoped to lucy-2.5 and a single 60-second session. Steal the pattern from step 06's production note.
Connect and start editing
Grab a camera stream, open the connection, and Lucy starts editing. The transformed stream arrives in onRemoteStream — wire it to a video element and you're live.
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-2.5");
// 1. Capture the camera at the model's native format
const stream = await navigator.mediaDevices.getUserMedia({
video: {
frameRate: model.fps,
width: model.width,
height: model.height,
},
});
// 2. Create a client and connect — the edited stream comes back via callback
const client = createDecartClient({
apiKey: "your-api-key-here",
});
const realtimeClient = await client.realtime.connect(stream, {
model,
mirror: "auto",
onRemoteStream: (transformedStream) => {
document.getElementById("output").srcObject = transformedStream;
},
// Pass the first edit here so the very first frame is already transformed
initialState: {
prompt: {
text: "Change the wall's color to light blue, natural consistent paint finish.",
enhance: true,
},
},
});
// 3. Edit the live stream at any time
await realtimeClient.set({
prompt: "Substitute the character in the video with the person in the reference image.",
image: characterImage,
enhance: true,
});import asyncio
from decart import DecartClient, models
from decart.types import ModelState, Prompt
async def main():
async with DecartClient(api_key="your-api-key-here") as client:
model = models.realtime("lucy-2.5")
realtime = await client.realtime.connect(
stream=media_stream,
model=model,
on_remote_stream=lambda s: display(s),
initial_state=ModelState(
prompt=Prompt(
text="Change the wall's color to light blue, natural consistent paint finish.",
enhance=True,
),
),
)
with open("character.jpg", "rb") as f:
await realtime.set(
prompt="Substitute the character in the video with the person in the reference image.",
image=f,
enhance=True,
)
asyncio.run(main())First frame, already transformed — pass the prompt and/or reference image in initialState so the very first generated frame uses it — otherwise viewers briefly see the raw feed.
Updating the reference image
Prompts describe the edit; a reference image shows it. Swap either at any time — mid-session, no reconnect.
// Change to a new character
await realtimeClient.set({
prompt: "Substitute the character in the video with the person in the reference image.",
image: newCharacterImage,
enhance: true,
});
// Set a new image only (clears any previous prompt)
await realtimeClient.set({ image: newCharacterImage });
// Set a new prompt only (clears any previous image)
await realtimeClient.set({ prompt: "Add dark sunglasses to the person's face." });
// Clear the reference image (fall back to text-only editing)
await realtimeClient.set({ image: null });# Change to a new character
with open("new_character.jpg", "rb") as f:
await realtime.set(
prompt="Substitute the character in the video with the person in the reference image.",
image=f,
enhance=True,
)
# Set a new image only (clears any previous prompt)
with open("another.jpg", "rb") as f:
await realtime.set(image=f)
# Set a new prompt only (clears any previous image)
await realtime.set(prompt="Add dark sunglasses to the person's face.")set() replaces the entire state — fields you omit are cleared. Always include every field you want to keep — that keeps prompt and image in sync with no intermediate states.
512×512 or larger
JPEG, PNG, or WebP. Sharper reference, sharper result.
Clear, even lighting
Avoid harsh shadows or reflections that hide the details you care about.
Match the framing
Full-body video? Use a full-body reference, not a head-and-shoulders crop.
No occlusion
Keep the key features fully visible — hidden parts can't be extracted.
Prompt like a director
Updating the prompt is one call. Lucy responds best when the prompt follows the pattern for its edit type — steal from the cookbook below; every example copies with a click and works in the demo up top.
// Text-only edit — update the prompt mid-session, no reconnect needed
await realtimeClient.set({
prompt: "Substitute the character in the video with a futuristic robot.",
enhance: true, // Lucy expands terse prompts into rich visual direction
});
// Prompt + reference image — tell Lucy what to take from the image
await realtimeClient.set({
prompt: "Substitute the character in the video with the warrior from the attached image.",
image: referenceImage, // File | Blob
enhance: true,
});# Text-only edit — update the prompt mid-session, no reconnect needed
await realtime.set(
prompt="Substitute the character in the video with a futuristic robot.",
enhance=True,
)
# Prompt + reference image — tell Lucy what to take from the image
with open("warrior.jpg", "rb") as f:
await realtime.set(
prompt="Substitute the character in the video with the warrior from the attached image.",
image=f,
enhance=True,
)Character swap
Substitute the character in the video with <description>.
Replace
Change <object> with <description of replacement>.
Remove
Remove <object> from the scene.
Add
Add <description of object> to <where to add it>.
Background
Change the background to <description of new background>.
Style
Change the style of the video to <style description>.
VFX
Add <effect description> to <location in scene>.
Describe what you want to see — skip negative instructions (“don't add a hat”) and vague asks (“make it royal”) — name the object, the look, and the location. For layered edits and troubleshooting, see the full prompting guide.
The complete example
The demo at the top of this page, reduced to its skeleton: camera in on the left, Lucy out on the right, one prompt line underneath. Two files and a terminal — running in about a minute.
# Scaffold a tiny Vite app and add the SDK
npm create vite@latest my-lucy-demo -- --template vanilla
cd my-lucy-demo
npm install @decartai/sdk
# Add your key (from step 02)
echo "VITE_DECART_API_KEY=your-api-key-here" > .env.local
# Replace index.html and main.js with the two files below, then:
npm run dev<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Lucy Demo</title>
<style>
body { margin: 0; min-height: 100vh; box-sizing: border-box; display: flex; flex-direction: column; gap: 16px; padding: 24px; font-family: system-ui, sans-serif; background: #fff; }
.videos { display: flex; gap: 12px; flex: 1; }
.videos video { width: 50%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 20px; background: #f9f9f9; }
#input-video { transform: scaleX(-1); } /* mirror the camera preview */
#prompt-form { display: flex; gap: 8px; width: 100%; max-width: 702px; margin: 0 auto; }
#prompt { flex: 1; height: 48px; border: 1px solid #e2e2e2; border-radius: 999px; padding: 0 20px; font-size: 16px; outline-color: #2548f6; }
#prompt-form button { height: 48px; padding: 0 24px; border: 0; border-radius: 999px; background: #2548f6; color: #fff; font-size: 16px; cursor: pointer; }
</style>
</head>
<body>
<div class="videos">
<video id="input-video" autoplay playsinline muted></video>
<video id="output-video" autoplay playsinline muted></video>
</div>
<form id="prompt-form">
<input id="prompt" placeholder="Describe your edit — try: Add a black top hat on top of my head." autocomplete="off" />
<button type="submit">Send</button>
</form>
<script type="module" src="/main.js"></script>
</body>
</html>import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-2.5");
// 1. Camera in — ask for the model's native format
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
document.getElementById("input-video").srcObject = stream;
// 2. Connect — Lucy's edited stream comes back through onRemoteStream
const client = createDecartClient({
apiKey: import.meta.env.VITE_DECART_API_KEY, // fine for local dev; see note below
});
const realtimeClient = await client.realtime.connect(stream, {
model,
mirror: "auto",
onRemoteStream: (transformedStream) => {
document.getElementById("output-video").srcObject = transformedStream;
},
initialState: {
prompt: { text: "Restyle to animated movie", enhance: true },
},
});
// 3. The prompt line — every submit becomes a live edit
document.getElementById("prompt-form").addEventListener("submit", async (event) => {
event.preventDefault();
const text = document.getElementById("prompt").value.trim();
if (text) {
await realtimeClient.set({ prompt: text, enhance: true });
}
});
// Tidy up when the tab closes
window.addEventListener("beforeunload", () => {
realtimeClient.disconnect();
stream.getTracks().forEach((track) => track.stop());
});Before you ship — a key in VITE_* env ends up in the browser bundle — fine on localhost, not in production. Mint short-lived client tokens on your backend instead:
// Backend: mint a short-lived token with your permanent key
const token = await client.tokens.create();
// Frontend: connect with the token instead of the real key
const frontendClient = createDecartClient({ apiKey: token.apiKey });Beyond the camera
Lucy edits any MediaStream — the camera is just the default. Point it at an uploaded video, a shared screen, or anything else you can turn into a stream.
// Use an uploaded video file as the input instead of the camera
const video = document.createElement("video");
video.src = URL.createObjectURL(fileInput.files[0]); // <input type="file" accept="video/*">
video.muted = true;
video.loop = true;
video.playsInline = true;
await video.play();
// captureStream() turns the playing <video> into a MediaStream Lucy can edit
// (Safari calls it webkitCaptureStream)
const fileStream = (video.captureStream ?? video.webkitCaptureStream).call(video);
const realtimeClient = await client.realtime.connect(fileStream, {
model,
onRemoteStream: (transformedStream) => {
document.getElementById("output-video").srcObject = transformedStream;
},
initialState: {
prompt: { text: "Change the style of the video to Claymation.", enhance: true },
},
});// Edit a shared screen, window, or tab in realtime
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
const realtimeClient = await client.realtime.connect(screenStream, {
model,
onRemoteStream: (transformedStream) => {
document.getElementById("output-video").srcObject = transformedStream;
},
});Same session, same controls — everything from steps 04–05 — prompts, reference images, set() — works identically whatever the input source is.
This page is the fast path. The docs are the full map.
Connection lifecycle, client tokens, portrait mode, self-anchoring, virtual try-on, and the complete prompting guide — all in the platform docs.
Send your agent this page's URL — the entire guide is served as markdown at /llms.txt — or copy it straight to your clipboard.