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,});
import asynciofrom decart import DecartClient, modelsfrom decart.types import ModelState, Promptasync 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())
val model = RealtimeModels.LUCY_2_5val client = DecartClient(context, DecartClientConfig(apiKey = "your-api-key"))client.realtime.initialize(eglBase)val videoSource = client.realtime.createVideoSource(isScreencast = false)!!val videoTrack = client.realtime.createVideoTrack("camera", videoSource)!!val enumerator = Camera2Enumerator(context)val cameraName = enumerator.deviceNames.first { enumerator.isFrontFacing(it) }val capturer = enumerator.createCapturer(cameraName, 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) }, initialPrompt = InitialPrompt( text = "Change the wall's color to light blue, natural consistent paint finish.", enhance = true, ), ))val characterBase64 = Base64.encodeToString(characterBytes, Base64.NO_WRAP)client.realtime.setImage( imageBase64 = characterBase64, prompt = "Substitute the character in the video with the person in the reference image.", 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.
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 characterawait 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 characterwith 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 characterval 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 onlyclient.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.
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.
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 editsawait 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 editsawait 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 editsclient.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.
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 type
Prompt 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.
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.
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.
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,});
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.",});
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,});
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,});
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 electricityaround the person.
await realtimeClient.set({ prompt: "Add shooting fireballs to the person's hands.", enhance: true,});
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 asynciofrom decart import DecartClient, modelsasync 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_5val 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()
For browser and mobile apps, use client tokens instead of your permanent API key.
// Backend: generate a short-lived tokenconst token = await client.tokens.create();// Frontend: connect with the tokenconst frontendClient = createDecartClient({ apiKey: token.apiKey });
# Backend: generate a short-lived tokentoken = await client.tokens.create()# Frontend: connect with the tokenfrontend_client = DecartClient(api_key=token.api_key)
// Fetch a short-lived token from your backendval ephemeralKey = fetchTokenFromBackend()// Connect with the tokenval client = DecartClient(context, DecartClientConfig(apiKey = ephemeralKey))
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", },});
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.