Skip to main content
Transform any video in realtime with AI-powered editing at 720p. Lucy 2.5 is a general editing model - capable across edit type, object, and scene.
Use Lucy 2.5 as your default realtime editing model. It supports text prompts, reference images, or both in the same integration.

Quick start

Installation

npm install @decartai/sdk

Connect and start editing

import { createDecartClient, models } from "@decartai/sdk";

const model = models.realtime("lucy-2.5");

const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    frameRate: model.fps,
    width: model.width,
    height: model.height,
  },
});

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;
  },
  initialState: {
    prompt: {
      text: "Change the wall's color to light blue, natural consistent paint finish.",
      enhance: true,
    },
  },
});

await realtimeClient.set({
  prompt: "Substitute the character in the video with the person in the reference image.",
  image: characterImage,
  enhance: true,
});
Pass the prompt and/or reference image in initialState so the first frame is already transformed - otherwise viewers briefly see the raw camera feed.

Model capabilities

Lucy 2.5 is designed as a general editing model - reliable across a wide range of tasks, object types, and input conditions.
Quality dimensionDescription
Reference adherenceFollows the provided reference image precisely
Task adherenceExecutes the prompt exactly as written
Aesthetic and realismNatural results across face, body, clothing, and environment
Motion accuracyTracks facial expressions, lip sync, and body movement
ConsistencyStays stable across long sessions

Edit types

Edit typeDescription
Character swapReplace a person or character with a new identity from a reference image
Virtual try-onSwap clothing or accessories onto a person
AddInsert a new object into the scene
ReplaceSwap an existing object with something different
RemoveClean removal of objects with background reconstruction
BackgroundSwap the scene environment
Style transformationApply a global visual aesthetic to the entire video
Visual effectsAdd dynamic effects - fire, explosions, smoke, particles
Virtual try-on has a dedicated optimized model. See the Lucy - Virtual try-on model for details.

Use cases

Virtual try-on

Let customers see clothing and accessories on themselves in realtime. Supports a wide range of garment types, materials, and body conditions.

Ad variation

Generate personalized ad creatives at scale. Swap products, backgrounds, or characters across live or pre-recorded content without reshoots.

Social engagement

Give audiences interactive effects, filters, and transformations that drive participation and sharing.

Content monetization

Embed branded products and sponsored placements directly into live video.

Live shopping

Sell any product, in any video. Give sellers the ability to place any item into live content instantly.

Furniture replacement

Swap furniture and home decor in live video, letting customers visualize products in their own space before buying.

Updating the reference image

You can change the reference image at any time without reconnecting. Use the set() method to atomically replace the session state - include all fields you want to keep:
// 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.")
// Change to a new character
val newCharacterBase64 = Base64.encodeToString(newCharacterBytes, Base64.NO_WRAP)
client.realtime.setImage(
    imageBase64 = newCharacterBase64,
    prompt = "Substitute the character in the video with the person in the reference image.",
    enhance = true
)

// Set a new image only
client.realtime.setImage(imageBase64 = newCharacterBase64)

// Set a new prompt only (text-only editing)
client.realtime.setPrompt("Add dark sunglasses to the person's face.")

// Clear the reference image (fall back to text-only editing)
client.realtime.setImage(imageBase64 = null)
set() replaces the entire state - fields you omit are cleared. Always include every field you want to keep. This avoids intermediate states and ensures prompt and image stay in sync.

Reference image best practices

Use a clear, well-lit portrait

Consistent, even lighting works best regardless of edit type. For characters, use a front-facing photo. For garments and objects, avoid harsh shadows or reflections that obscure important details.

Match the framing

Match the reference framing to the source video. If the source shows a full-body standing person and you want to swap the character or change clothes, use a full-body reference shot rather than a head-and-shoulders crop.

Avoid occlusion

Make sure the key features of the reference are fully visible. Partially hidden characters, garments, or objects reduce what the model can extract and may affect output quality.

Resolution and format

At least 512×512 pixels recommended. Supported formats: JPEG, PNG, and WebP.

For object replacement

Use a clean product or object shot against a neutral background. The cleaner the separation between subject and background, the more precisely the model can apply it.

For virtual try-on

A front-facing, evenly lit garment shot on a plain background gives the model the clearest signal. Include the full item from collar to hem where possible.

Text-only editing

Lucy 2.5 works without a reference image too. Use text prompts to add, modify, or remove elements in your live video:
// No reference image needed for text-only edits
await realtimeClient.set({ prompt: "Add a small dog running around in the background." });
await realtimeClient.set({ prompt: "Change the background to a sandy beach with clear blue water." });
await realtimeClient.set({ prompt: "Change the person's hair color to bright blonde." });
# No reference image needed for text-only edits
await realtime.set(prompt="Add a small dog running around in the background.")
await realtime.set(prompt="Change the background to a sandy beach with clear blue water.")
await realtime.set(prompt="Change the person's hair color to bright blonde.")
// No reference image needed for text-only edits
client.realtime.setPrompt("Add a small dog running around in the background.")
client.realtime.setPrompt("Change the background to a sandy beach with clear blue water.")
client.realtime.setPrompt("Change the person's hair color to bright blonde.")
See the prompting guide below for the best prompt structures for each edit type.

Prompting guide

Strong prompts answer five questions: what should change, what should it become, where should it apply, how should it interact with the subject or environment, and what should stay the same. Think of the prompt as short creative direction. You do not need technical language. You do need clear visual details.
For deeper guidance - layered edits, physical effects, restyle prompts, troubleshooting, and dos/don’ts - see the full Prompting guide for Lucy 2.5.
Lucy 2.5 responds best when prompts follow the specific pattern for each edit type. The table below shows all eight supported operations with their recommended templates.
Edit typePrompt template
Character swap"Substitute the character in the video with <description>."
Add"Add <description of object> to <where to add it>."
Replace"Change <object to change> with <description of replacement>."
Remove"Remove <object to remove> from the scene."
Change attribute"Change <object> to <description of new attribute>."
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>."
Avoid negative instructions such as “don’t add a hat” or “the person never changes clothes.” Describe what you want to see instead.

Character swap

When using a reference image, describe the character’s appearance in the prompt. The more detail you provide, the closer the output matches the reference. “Substitute the character in the video with <description of the character in the reference image>.” Examples:
  • Substitute the character with an older man, which has pale, wrinkled skin, light blue eyes, a powdered white wig with side curls, and wears a dark formal coat with a white ruffled neckpiece.
  • Substitute the character with a young person wearing a short-sleeved pink top with white ribbon ties on the back, loose pink pants, and short brown hair tied in a side ponytail.
  • Substitute the character with a furry creature, which has soft brown and orange fur, a light face with dark eye markings, a dark nose, and long claws.
await realtimeClient.set({
  prompt: "Substitute the character with a furry creature, which has soft brown and orange fur, a light face with dark eye markings, a dark nose, and long claws.",
  image: referenceImage,
  enhance: true,
});
Describe what you see in the reference image - skin tone, hair, clothing, distinctive features. Generic prompts like “Transform into this character” still work but produce less precise results.

Adding objects

Add new elements to the scene by specifying what to add and where to place it. “Add <description of object> to <where to add it>.” Examples:
  • Add a red conical hat, covered in sequins, with a white fluffy trim and a matching pompom to the person’s head.
  • Add a coffee mug with a green logo to the person’s right hand.
await realtimeClient.set({
  prompt: "Add a red conical hat, covered in sequins, with a white fluffy trim and a matching pompom to the person's head.",
  image: hatReference,
  enhance: true,
});
Always specify placement (“to the person’s head”, “in the background”, “on the table”). Without a location, the model places the object unpredictably.

Replacing objects

Swap an existing element in the scene with something different. “Change <object to change> with <description of the replacement>.” Examples:
  • Change the person’s sweater with a red knit sweater, which has a white-outlined, gold and white striped rectangular emblem on the chest.
  • Change the phone in the person’s hand with a sleek black smartphone with a triple-lens camera on the back.
await realtimeClient.set({
  prompt: "Change the person's sweater with a red knit sweater, which has a white-outlined, gold and white striped rectangular emblem on the chest.",
  image: sweaterReference,
  enhance: true,
});

Removing objects

Remove an element from the scene. The model reconstructs the background behind it. “Remove <object to remove> from the scene.” Examples:
  • Remove the microphone stand from the scene.
  • Remove the logo on the person’s shirt from the scene.
  • Remove the bottle on the table from the scene.
await realtimeClient.set({
  prompt: "Remove the microphone stand from the scene.",
});

Changing attributes

Modify a property of an existing object - color, texture, material - without replacing the object itself. “Change <object> to <description of new attribute>.” Examples:
  • Change the wall’s color to light blue, natural consistent paint finish.
  • Change the shirt’s texture to knitted, woven fabric.
await realtimeClient.set({
  prompt: "Change the wall's color to light blue, natural consistent paint finish.",
});

Background replacement

Replace the background of the scene with a new environment. Works with or without a reference image. “Change the background to <description of the new background>.” Examples:
  • Change the background to a sandy beach with clear blue water and a bright sunny sky.
  • Change the background to a cozy living room with warm lighting, bookshelves, and a fireplace.
  • Change the background to a neon-lit city street at night with reflections on wet pavement.
await realtimeClient.set({
  prompt: "Change the background to a sandy beach with clear blue water and a bright sunny sky.",
  enhance: true,
});

Style transformation

Apply a global visual style change to the entire scene - mood, aesthetic, or look and feel. “Change the style of the video to <style description>.” Examples:
  • Change the style of the video to a vintage 1970s film aesthetic with warm tones and slight grain.
  • Change the style of the video to an anime illustration style with cel shading and bold outlines.
  • Change the style of the video to a dark cyberpunk aesthetic with teal and magenta color grading.
await realtimeClient.set({
  prompt: "Change the style of the video to a vintage 1970s film aesthetic with warm tones and slight grain.",
  enhance: true,
});

Visual effects

Add dynamic effects to characters or environments. VFX use the same “Add” structure but describe effects rather than physical objects. “Add <effect description> to <location in scene>.” Examples:
  • Add shooting fireballs to the person’s hands.
  • Add a large explosion with rising smoke to the background.
  • Add a swirling vortex of electricity around the person.
await realtimeClient.set({
  prompt: "Add shooting fireballs to the person's hands.",
  enhance: true,
});

Dos and don’ts

DoDon’t
"Add a silver crown floating above the person's head.""Make it royal."
"Remove the coffee mug from the desk, leaving the wooden tabletop visible.""Remove the mug."
"Change the background to a beach with waves crashing, sunlight on the water, and pale sand.""Make it a beach."
"Use the black cropped jacket from the reference, including the glossy leather texture and silver zipper. Apply it to the person's jacket.""Use the reference image."
"Replace the shirt with a red silk blouse. Keep the person's identity, face, and hair unchanged.""Change the person into someone wearing a red blouse."
"Glowing sparks emit from the person's fingertips, trail with their hand motion, and reflect on their fingers.""A magical thing happens around the hands."

Connection lifecycle

Lucy 2.5 shares the same connection lifecycle as all realtime models. See the JavaScript SDK, Python SDK, or Android SDK for details on:
  • Connection states (connecting, connected, generating, reconnecting, disconnected)
  • Auto-reconnect with exponential backoff
  • Error handling with DecartSDKError
  • Session tracking with generationTick events
  • Session viewing with subscribe tokens

Complete example

A full application with character switching, connection management, and error handling:
import { createDecartClient, models, type DecartSDKError } from "@decartai/sdk";

async function setupLucy25() {
  const model = models.realtime("lucy-2.5");

  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      frameRate: model.fps,
      width: model.width,
      height: model.height,
    },
  });

  document.getElementById("input-video").srcObject = stream;

  const client = createDecartClient({
    apiKey: process.env.DECART_API_KEY,
  });

  const realtimeClient = await client.realtime.connect(stream, {
    model,
    onRemoteStream: (transformedStream) => {
      document.getElementById("output-video").srcObject = transformedStream;
    },
  });

  realtimeClient.on("connectionChange", (state) => {
    document.getElementById("status").textContent = state;
  });

  realtimeClient.on("generationTick", ({ seconds }) => {
    document.getElementById("usage").textContent = `${seconds}s`;
  });

  realtimeClient.on("error", (error: DecartSDKError) => {
    console.error("Lucy 2.5 error:", error.code, error.message);
  });

  document.getElementById("character-input").addEventListener("change", async (e) => {
    const file = (e.target as HTMLInputElement).files[0];
    if (file) {
      await realtimeClient.set({
        prompt: "Substitute the character in the video with the person in the reference image.",
        image: file,
        enhance: true,
      });
    }
  });

  window.addEventListener("beforeunload", () => {
    realtimeClient.disconnect();
    stream.getTracks().forEach((track) => track.stop());
  });

  return realtimeClient;
}

setupLucy25();
import asyncio
from decart import DecartClient, models

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),
        )

        @realtime.on("connection_change")
        def on_state(state):
            print(f"Connection: {state}")

        @realtime.on("generation_tick")
        def on_tick(seconds):
            print(f"Usage: {seconds}s")

        @realtime.on("error")
        def on_error(error):
            print(f"Lucy 2.5 error: {error.code} {error.message}")

        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())
import ai.decart.sdk.*
import ai.decart.sdk.realtime.*
import org.webrtc.*

val model = RealtimeModels.LUCY_2_5
val client = DecartClient(context, DecartClientConfig(apiKey = "your-api-key"))
client.realtime.initialize(eglBase)

val videoSource = client.realtime.createVideoSource(false)!!
val videoTrack = client.realtime.createVideoTrack("camera", videoSource)!!
val enumerator = Camera2Enumerator(context)
val capturer = enumerator.createCapturer(
    enumerator.deviceNames.first { enumerator.isFrontFacing(it) }, null
)
capturer.initialize(
    SurfaceTextureHelper.create("CaptureThread", client.realtime.getEglBaseContext()),
    context, videoSource.capturerObserver
)
capturer.startCapture(model.width, model.height, model.fps)

client.realtime.connect(
    localVideoTrack = videoTrack,
    options = ConnectOptions(
        model = model,
        onRemoteVideoTrack = { track -> remoteRenderer.addSink(track) }
    )
)

lifecycleScope.launch {
    client.realtime.connectionState.collect { state -> statusText.text = state.name }
}
lifecycleScope.launch {
    client.realtime.generationTicks.collect { tick -> usageText.text = "${tick.seconds}s" }
}
lifecycleScope.launch {
    client.realtime.errors.collect { error ->
        Log.e("Lucy25", "Error: ${error.code} ${error.message}") }
}

fun onCharacterSelected(imageBytes: ByteArray) {
    val base64 = Base64.encodeToString(imageBytes, Base64.NO_WRAP)
    client.realtime.setImage(
        imageBase64 = base64,
        prompt = "Substitute the character in the video with the person in the reference image.",
        enhance = true
    )
}

capturer.stopCapture()
client.realtime.disconnect()
client.release()
eglBase.release()

Client-side authentication

For browser and mobile apps, use client tokens instead of your permanent API key.
// Backend: generate a short-lived token
const token = await client.tokens.create();

// Frontend: connect with the token
const frontendClient = createDecartClient({ apiKey: token.apiKey });
# Backend: generate a short-lived token
token = await client.tokens.create()

# Frontend: connect with the token
frontend_client = DecartClient(api_key=token.api_key)
// Fetch a short-lived token from your backend
val ephemeralKey = fetchTokenFromBackend()

// Connect with the token
val client = DecartClient(context, DecartClientConfig(apiKey = ephemeralKey))
See the full pattern for JavaScript or Android.

Technical specifications

PropertyValue
Model IDlucy-2.5
Resolution1280×720
OrientationLandscape (16:9) and Portrait (9:16)
TransportWebRTC
Character referenceYes (JPEG, PNG, WebP)
Prompt enhancementYes (default: enabled)
Auto-reconnectYes (exponential backoff, up to 5 retries)

Portrait mode (9:16)

On mobile devices (iOS/Android), portrait mode works automatically - the OS maps the front camera to a vertical stream regardless of the constraints you pass. For desktop browsers or external webcams, swap width and height when calling getUserMedia:
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    frameRate: { ideal: model.fps },
    width: { ideal: model.height },  // swap: use height as width
    height: { ideal: model.width },  // swap: use width as height
    facingMode: "user",
  },
});

Generation stability

In long sessions, Lucy 2.5 can use its own output as an additional anchor - feeding recent generated frames back as the reference image. This self-anchoring mechanism keeps the output stable over time, preventing drifting.
Self-anchoring works best when the scene stays consistent. If the stream changes significantly - a different person enters the frame, the camera cuts, or the scene shifts entirely - the model may still anchor to the last edited frame, which no longer matches the new input. In these cases do not use self-anchoring.

Next steps

JavaScript SDK

Full JavaScript SDK reference for realtime features

Python SDK

Full Python SDK reference for realtime features

Android SDK

Full Android SDK reference for realtime features

All Models

Compare all Decart models side by side