Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ See `examples/nextjs-realtime` or `examples/react-vite` for runnable demos.
- `realtime/live-avatar.ts` - Live avatar (audio-driven avatar with playAudio or mic input)
- `realtime/connection-events.ts` - Handling connection state and errors
- `realtime/prompt-update.ts` - Updating prompt dynamically
- `realtime/custom-model.ts` - Using a custom model definition (e.g., preview/experimental models)

## API Reference

Expand Down
62 changes: 62 additions & 0 deletions examples/sdk-core/realtime/custom-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Custom Model Definition Example
*
* Demonstrates how to define and use a custom model that isn't
* built into the SDK. This is useful for preview/experimental models
* or private deployments.
*
* Browser-only example - requires WebRTC APIs
* See examples/nextjs-realtime or examples/react-vite for runnable demos
*/

import { createDecartClient } from "@decartai/sdk";
import type { CustomModelDefinition } from "@decartai/sdk";

async function main() {
// Define a custom model that isn't in the SDK's built-in registry.
// This works for any model that conforms to the CustomModelDefinition shape.
const lucy2RtPreview: CustomModelDefinition = {
name: "lucy_2_rt_preview",
urlPath: "/v1/stream",
fps: 20,
width: 1280,
height: 720,
};

// Get webcam stream using the custom model's settings
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
frameRate: lucy2RtPreview.fps,
width: lucy2RtPreview.width,
height: lucy2RtPreview.height,
},
});

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

// Pass the custom model directly to realtime.connect()
const realtimeClient = await client.realtime.connect(stream, {
model: lucy2RtPreview,
onRemoteStream: (transformedStream) => {
const video = document.getElementById("output") as HTMLVideoElement;
video.srcObject = transformedStream;
},
initialState: {
prompt: {
text: "cinematic lighting, film grain",
enhance: true,
},
},
});

console.log("Session ID:", realtimeClient.sessionId);
console.log("Connected:", realtimeClient.isConnected());

// Update prompt dynamically, same as built-in models
realtimeClient.setPrompt("watercolor painting style");
}

main();
1 change: 1 addition & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type {
export type { ConnectionState } from "./realtime/types";
export type { WebRTCStats } from "./realtime/webrtc-stats";
export {
type CustomModelDefinition,
type ImageModelDefinition,
type ImageModels,
isImageModel,
Expand Down
8 changes: 5 additions & 3 deletions packages/sdk/src/realtime/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { modelDefinitionSchema, type RealTimeModels } from "../shared/model";
import { type CustomModelDefinition, type ModelDefinition, modelDefinitionSchema } from "../shared/model";
import { modelStateSchema } from "../shared/types";
import { classifyWebrtcError, type DecartSDKError } from "../utils/errors";
import type { Logger } from "../utils/logger";
Expand Down Expand Up @@ -94,7 +94,9 @@ const realTimeClientConnectOptionsSchema = z.object({
initialState: realTimeClientInitialStateSchema.optional(),
customizeOffer: createAsyncFunctionSchema(z.function()).optional(),
});
export type RealTimeClientConnectOptions = z.infer<typeof realTimeClientConnectOptionsSchema>;
export type RealTimeClientConnectOptions = Omit<z.infer<typeof realTimeClientConnectOptionsSchema>, "model"> & {
model: ModelDefinition | CustomModelDefinition;
};

export type Events = {
connectionChange: ConnectionState;
Expand Down Expand Up @@ -189,7 +191,7 @@ export const createRealTimeClient = (opts: RealTimeClientOptions) => {
customizeOffer: options.customizeOffer as ((offer: RTCSessionDescriptionInit) => Promise<void>) | undefined,
vp8MinBitrate: 300,
vp8StartBitrate: 600,
modelName: options.model.name as RealTimeModels,
modelName: options.model.name,
initialImage,
initialPrompt,
});
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/realtime/webrtc-connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mitt from "mitt";
import type { RealTimeModels } from "../shared/model";

import type { Logger } from "../utils/logger";
import { buildUserAgent } from "../utils/user-agent";
import type { DiagnosticEmitter, IceCandidateEvent } from "./diagnostics";
Expand All @@ -24,7 +24,7 @@ interface ConnectionCallbacks {
customizeOffer?: (offer: RTCSessionDescriptionInit) => Promise<void>;
vp8MinBitrate?: number;
vp8StartBitrate?: number;
modelName?: RealTimeModels;
modelName?: string;
initialImage?: string;
initialPrompt?: { text: string; enhance?: boolean };
logger?: Logger;
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/realtime/webrtc-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pRetry, { AbortError } from "p-retry";
import type { RealTimeModels } from "../shared/model";

import type { Logger } from "../utils/logger";
import type { DiagnosticEmitter } from "./diagnostics";
import type { ConnectionState, OutgoingMessage } from "./types";
Expand All @@ -16,7 +16,7 @@ export interface WebRTCConfig {
customizeOffer?: (offer: RTCSessionDescriptionInit) => Promise<void>;
vp8MinBitrate?: number;
vp8StartBitrate?: number;
modelName?: RealTimeModels;
modelName?: string;
initialImage?: string;
initialPrompt?: { text: string; enhance?: boolean };
}
Expand Down
11 changes: 10 additions & 1 deletion packages/sdk/src/shared/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ export type ModelDefinition<T extends Model = Model> = {
inputSchema: T extends keyof ModelInputSchemas ? ModelInputSchemas[T] : z.ZodTypeAny;
};

/**
* A model definition with an arbitrary (non-registry) model name.
* Use this when providing your own model configuration.
*/
export type CustomModelDefinition = Omit<ModelDefinition, "name" | "inputSchema"> & {
name: string;
inputSchema?: z.ZodTypeAny;
};

/**
* Type alias for model definitions that support synchronous processing.
* Only image models support the sync/process API.
Expand All @@ -221,7 +230,7 @@ export type ImageModelDefinition = ModelDefinition<ImageModels>;
export type VideoModelDefinition = ModelDefinition<VideoModels>;

export const modelDefinitionSchema = z.object({
name: modelSchema,
name: z.string(),
urlPath: z.string(),
queueUrlPath: z.string().optional(),
fps: z.number().min(1),
Expand Down
30 changes: 30 additions & 0 deletions packages/sdk/tests/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3117,3 +3117,33 @@ describe("VideoStall Diagnostic", () => {
expect(event.data.durationMs).toBe(1500);
});
});

describe("CustomModelDefinition", () => {
it("allows arbitrary model names in modelDefinitionSchema", async () => {
const { modelDefinitionSchema } = await import("../src/shared/model.js");

const customModel = {
name: "lucy_2_rt_preview",
urlPath: "/v1/stream",
fps: 20,
width: 1280,
height: 720,
};

const result = modelDefinitionSchema.safeParse(customModel);
expect(result.success).toBe(true);
});

it("rejects invalid custom model definitions", async () => {
const { modelDefinitionSchema } = await import("../src/shared/model.js");

const invalidModel = {
name: "my_custom_model",
urlPath: "/v1/stream",
// missing fps, width, height
};

const result = modelDefinitionSchema.safeParse(invalidModel);
expect(result.success).toBe(false);
});
});
Loading