From 91f23ed02a435827debfb5669defe8e4fd21f018 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Thu, 26 Mar 2026 15:37:18 +0200 Subject: [PATCH 1/5] feat: remove deprecated models (lucy-pro-t2i, lucy-pro-t2v, lucy-dev-i2v, lucy-pro-i2v, lucy-fast-v2v) Mirrors DecartAI/api-platform#488. Removes model definitions, schemas, tests, docs, and examples for the five sunset models. Co-Authored-By: Claude Opus 4.6 --- examples/bun-cli/README.md | 10 +- examples/bun-cli/cli.ts | 12 +- examples/express-api/README.md | 26 ++- examples/express-api/src/server.ts | 64 +++++-- examples/express-proxy/README.md | 13 +- examples/express-proxy/public/index.html | 67 ++++--- examples/hono-edge/README.md | 19 +- examples/hono-edge/src/index.ts | 77 ++++++-- examples/nextjs-proxy/README.md | 10 +- examples/nextjs-proxy/app/page.tsx | 33 +++- examples/sdk-core/README.md | 19 +- examples/sdk-core/image/text-to-image.ts | 7 +- examples/sdk-core/video/image-to-video.ts | 10 +- examples/sdk-core/video/manual-polling.ts | 7 +- examples/sdk-core/video/text-to-video.ts | 7 +- packages/proxy/README.md | 2 +- packages/proxy/src/express/README.md | 2 +- packages/proxy/src/express/middleware.test.ts | 30 ++-- packages/proxy/src/nextjs/README.md | 2 +- packages/proxy/src/nextjs/route.test.ts | 60 +++---- packages/sdk/AGENTS.md | 5 - packages/sdk/README.md | 8 +- packages/sdk/src/index.ts | 13 +- packages/sdk/src/queue/client.ts | 8 +- packages/sdk/src/shared/model.ts | 103 +---------- packages/sdk/src/tokens/client.ts | 2 +- packages/sdk/tests/e2e.test.ts | 58 ------ packages/sdk/tests/unit.test.ts | 166 +++++++----------- 28 files changed, 378 insertions(+), 462 deletions(-) diff --git a/examples/bun-cli/README.md b/examples/bun-cli/README.md index bf23702..e71d768 100644 --- a/examples/bun-cli/README.md +++ b/examples/bun-cli/README.md @@ -1,6 +1,6 @@ # Bun CLI Example -A simple CLI for text-to-image generation using the Decart SDK. +A simple CLI for image editing using the Decart SDK. ## Setup @@ -27,13 +27,13 @@ A simple CLI for text-to-image generation using the Decart SDK. ## Usage ### Dev-time -``` -./cli.ts text-to-image "A cyberpunk cityscape at night" +```bash +./cli.ts image-edit "A cyberpunk cityscape at night" ./input.png ``` ### Compiled ```bash -decart text-to-image "A cyberpunk cityscape at night" +decart image-edit "A cyberpunk cityscape at night" ./input.png ``` -The generated image will be saved to `output.png`. +The edited image will be saved to `output.png`. diff --git a/examples/bun-cli/cli.ts b/examples/bun-cli/cli.ts index 9aafbfa..a683ce2 100755 --- a/examples/bun-cli/cli.ts +++ b/examples/bun-cli/cli.ts @@ -1,19 +1,21 @@ #!/usr/bin/env bun import { createDecartClient, models } from "@decartai/sdk"; -const [command, prompt] = process.argv.slice(2); +const [command, prompt, inputPath] = process.argv.slice(2); -if (command !== "text-to-image" || !prompt) { - console.error("Usage: decart text-to-image "); +if ((command !== "image-edit" && command !== "text-to-image") || !prompt || !inputPath) { + console.error("Usage: decart image-edit "); process.exit(1); } const client = createDecartClient(); +const inputImage = Bun.file(inputPath); -console.log("Generating image..."); +console.log("Editing image..."); const image = await client.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt, + data: inputImage, }); await Bun.write("output.png", image); diff --git a/examples/express-api/README.md b/examples/express-api/README.md index 567520a..f8fbf5a 100644 --- a/examples/express-api/README.md +++ b/examples/express-api/README.md @@ -27,42 +27,38 @@ pnpm dev ## Endpoints -### Image Generation +These editing endpoints expect `imageDataUrl` / `videoDataUrl` fields containing base64 `data:` URLs. This keeps the example self-contained and avoids server-side fetching of arbitrary remote URLs. + +### Image Editing ```bash -# Text-to-image curl -X POST http://localhost:3000/api/image/generate \ -H "Content-Type: application/json" \ - -d '{"prompt": "A beautiful sunset over mountains"}' \ + -d '{"prompt": "A beautiful sunset over mountains", "imageDataUrl": "data:image/png;base64,"}' \ --output image.png -# Image-to-image curl -X POST http://localhost:3000/api/image/transform \ -H "Content-Type: application/json" \ - -d '{"prompt": "Oil painting style", "imageUrl": "https://example.com/image.jpg"}' \ + -d '{"prompt": "Oil painting style", "imageDataUrl": "data:image/png;base64,"}' \ --output transformed.png ``` -### Video Generation +### Video Editing ```bash -# Submit video job curl -X POST http://localhost:3000/api/video/generate \ -H "Content-Type: application/json" \ - -d '{"prompt": "A cat playing piano"}' + -d '{"prompt": "A cat playing piano", "videoDataUrl": "data:video/mp4;base64,"}' # Returns: {"jobId": "abc123", "status": "pending"} -# Check status curl http://localhost:3000/api/video/status/abc123 # Returns: {"job_id": "abc123", "status": "processing"} -# Get result (when completed) curl http://localhost:3000/api/video/result/abc123 --output video.mp4 -# Or use the sync endpoint (waits for completion) curl -X POST http://localhost:3000/api/video/generate-sync \ -H "Content-Type: application/json" \ - -d '{"prompt": "A timelapse of clouds moving"}' \ + -d '{"prompt": "A timelapse of clouds moving", "videoDataUrl": "data:video/mp4;base64,"}' \ --output video.mp4 ``` @@ -70,9 +66,9 @@ curl -X POST http://localhost:3000/api/video/generate-sync \ | Endpoint | Method | Description | |----------|--------|-------------| -| `/api/image/generate` | POST | Generate image from text | +| `/api/image/generate` | POST | Edit image from base64 data URL + prompt | | `/api/image/transform` | POST | Transform image with prompt | -| `/api/video/generate` | POST | Submit video generation job | +| `/api/video/generate` | POST | Submit video editing job | | `/api/video/status/:id` | GET | Check video job status | | `/api/video/result/:id` | GET | Get completed video | -| `/api/video/generate-sync` | POST | Generate video (waits for completion) | +| `/api/video/generate-sync` | POST | Edit video (waits for completion) | diff --git a/examples/express-api/src/server.ts b/examples/express-api/src/server.ts index 9297080..0c389b3 100644 --- a/examples/express-api/src/server.ts +++ b/examples/express-api/src/server.ts @@ -9,14 +9,37 @@ const client = createDecartClient({ apiKey: process.env.DECART_API_KEY!, }); -// Generate image from text (sync - returns immediately) +function parseBase64DataUrl(dataUrl: unknown, mediaType: "image" | "video"): Blob { + if (typeof dataUrl !== "string") { + throw new Error(`${mediaType}DataUrl must be a base64 data URL string`); + } + + const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl); + if (!match) { + throw new Error(`${mediaType}DataUrl must be a valid base64 data URL`); + } + + const [, mimeType, base64] = match; + if (!mimeType.startsWith(`${mediaType}/`)) { + throw new Error(`${mediaType}DataUrl must contain a ${mediaType} MIME type`); + } + + return new Blob([Buffer.from(base64, "base64")], { type: mimeType }); +} + +// Edit image (sync - returns immediately) app.post("/api/image/generate", async (req, res) => { try { - const { prompt } = req.body; + const { prompt, imageDataUrl } = req.body; + + if (!prompt || !imageDataUrl) { + return res.status(400).json({ error: "prompt and imageDataUrl are required" }); + } const blob = await client.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt, + data: parseBase64DataUrl(imageDataUrl, "image"), }); const buffer = Buffer.from(await blob.arrayBuffer()); @@ -30,12 +53,16 @@ app.post("/api/image/generate", async (req, res) => { // Transform image (sync - returns immediately) app.post("/api/image/transform", async (req, res) => { try { - const { prompt, imageUrl } = req.body; + const { prompt, imageDataUrl } = req.body; + + if (!prompt || !imageDataUrl) { + return res.status(400).json({ error: "prompt and imageDataUrl are required" }); + } const blob = await client.process({ model: models.image("lucy-pro-i2i"), prompt, - data: imageUrl, + data: parseBase64DataUrl(imageDataUrl, "image"), }); const buffer = Buffer.from(await blob.arrayBuffer()); @@ -46,14 +73,19 @@ app.post("/api/image/transform", async (req, res) => { } }); -// Submit video generation job (async - returns job ID) +// Submit video editing job (async - returns job ID) app.post("/api/video/generate", async (req, res) => { try { - const { prompt } = req.body; + const { prompt, videoDataUrl } = req.body; + + if (!prompt || !videoDataUrl) { + return res.status(400).json({ error: "prompt and videoDataUrl are required" }); + } const job = await client.queue.submit({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt, + data: parseBase64DataUrl(videoDataUrl, "video"), }); res.json({ jobId: job.job_id, status: job.status }); @@ -87,12 +119,16 @@ app.get("/api/video/result/:jobId", async (req, res) => { // Generate video with automatic polling (convenience endpoint) app.post("/api/video/generate-sync", async (req, res) => { try { - const { prompt, videoUrl } = req.body; + const { prompt, videoDataUrl } = req.body; + + if (!prompt || !videoDataUrl) { + return res.status(400).json({ error: "prompt and videoDataUrl are required" }); + } const result = await client.queue.submitAndPoll({ - model: videoUrl ? models.video("lucy-pro-v2v") : models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt, - ...(videoUrl && { data: videoUrl }), + data: parseBase64DataUrl(videoDataUrl, "video"), }); if (result.status === "completed") { @@ -112,10 +148,10 @@ app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); console.log(""); console.log("Available endpoints:"); - console.log(" POST /api/image/generate - Generate image from text"); + console.log(" POST /api/image/generate - Edit image from base64 data URL + prompt"); console.log(" POST /api/image/transform - Transform image"); - console.log(" POST /api/video/generate - Submit video job"); + console.log(" POST /api/video/generate - Submit video edit job from base64 data URL + prompt"); console.log(" GET /api/video/status/:id - Check job status"); console.log(" GET /api/video/result/:id - Get video result"); - console.log(" POST /api/video/generate-sync - Generate video (wait for result)"); + console.log(" POST /api/video/generate-sync - Edit video (wait for result)"); }); diff --git a/examples/express-proxy/README.md b/examples/express-proxy/README.md index 830b144..94fe4db 100644 --- a/examples/express-proxy/README.md +++ b/examples/express-proxy/README.md @@ -60,18 +60,19 @@ app.use(route, handler({ The frontend: 1. Loads the Decart SDK from CDN 2. Creates a client pointing to the proxy endpoint -3. Uses the SDK normally - no API key needed! +3. Uploads a source image from the browser and edits it with a prompt ```javascript import { createDecartClient, models } from '@decartai/sdk'; const client = createDecartClient({ - proxy: '/api/decart', // Points to your Express proxy + proxy: '/api/decart', }); const blob = await client.process({ - model: models.image('lucy-pro-t2i'), - prompt: 'A beautiful sunset', + model: models.image('lucy-pro-i2i'), + prompt: 'Turn this into a watercolor painting', + data: sourceFile, }); ``` @@ -86,8 +87,8 @@ const blob = await client.process({ 1. Start the server: `pnpm dev` 2. Open `http://localhost:3000` in your browser -3. Enter a prompt and click "Generate Image" -4. The image will be generated through the proxy +3. Enter a prompt, choose a source image, and click "Edit Image" +4. The edited image will be generated through the proxy ## Security Notes diff --git a/examples/express-proxy/public/index.html b/examples/express-proxy/public/index.html index 1534e37..7a54fd7 100644 --- a/examples/express-proxy/public/index.html +++ b/examples/express-proxy/public/index.html @@ -63,7 +63,8 @@ font-weight: 500; } - input[type="text"] { + input[type="text"], + input[type="file"] { width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; @@ -72,7 +73,8 @@ transition: border-color 0.2s; } - input[type="text"]:focus { + input[type="text"]:focus, + input[type="file"]:focus { outline: none; border-color: #667eea; } @@ -157,45 +159,51 @@

🎨 Decart SDK Proxy Example

-

Generate images using the Decart SDK through a secure proxy

+

Edit an image using the Decart SDK through a secure proxy

- + +
+ +
+ +
- Generated image + Edited image
- diff --git a/examples/hono-edge/README.md b/examples/hono-edge/README.md index 4919074..debdeed 100644 --- a/examples/hono-edge/README.md +++ b/examples/hono-edge/README.md @@ -1,6 +1,6 @@ # Hono Edge Example -A Cloudflare Workers API using Hono demonstrating text-to-image and text-to-video generation. +A Cloudflare Workers API using Hono demonstrating image and video editing. ## Setup @@ -39,29 +39,28 @@ pnpm deploy ## Endpoints -### Text-to-Image +These editing endpoints expect `imageDataUrl` / `videoDataUrl` fields containing base64 `data:` URLs. + +### Image Editing ```bash curl -X POST http://localhost:8787/api/image/generate \ -H "Content-Type: application/json" \ - -d '{"prompt": "A beautiful sunset over mountains"}' \ + -d '{"prompt": "A beautiful sunset over mountains", "imageDataUrl": "data:image/png;base64,"}' \ --output image.png ``` -### Text-to-Video +### Video Editing ```bash -# Submit video job curl -X POST http://localhost:8787/api/video/generate \ -H "Content-Type: application/json" \ - -d '{"prompt": "A cat playing piano"}' + -d '{"prompt": "A cat playing piano", "videoDataUrl": "data:video/mp4;base64,"}' # Returns: {"jobId": "abc123", "status": "pending"} -# Check status curl http://localhost:8787/api/video/status/abc123 # Returns: {"job_id": "abc123", "status": "processing"} -# Get result (when completed) curl http://localhost:8787/api/video/result/abc123 --output video.mp4 ``` @@ -69,7 +68,7 @@ curl http://localhost:8787/api/video/result/abc123 --output video.mp4 | Endpoint | Method | Description | |----------|--------|-------------| -| `/api/image/generate` | POST | Generate image from text | -| `/api/video/generate` | POST | Submit video generation job | +| `/api/image/generate` | POST | Edit image from base64 data URL + prompt | +| `/api/video/generate` | POST | Submit video editing job | | `/api/video/status/:id` | GET | Check video job status | | `/api/video/result/:id` | GET | Get completed video | diff --git a/examples/hono-edge/src/index.ts b/examples/hono-edge/src/index.ts index 61e432b..af56fb8 100644 --- a/examples/hono-edge/src/index.ts +++ b/examples/hono-edge/src/index.ts @@ -1,6 +1,8 @@ -import { createDecartClient, type DecartClient, models } from "@decartai/sdk"; +import { createDecartClient, models } from "@decartai/sdk"; import { Hono } from "hono"; +type DecartClient = ReturnType; + type Bindings = { DECART_API_KEY: string; }; @@ -11,6 +13,25 @@ type Variables = { const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); +function parseBase64DataUrl(dataUrl: unknown, mediaType: "image" | "video"): Blob { + if (typeof dataUrl !== "string") { + throw new Error(`${mediaType}DataUrl must be a base64 data URL string`); + } + + const match = /^data:([^;]+);base64,(.+)$/.exec(dataUrl); + if (!match) { + throw new Error(`${mediaType}DataUrl must be a valid base64 data URL`); + } + + const [, mimeType, base64] = match; + if (!mimeType.startsWith(`${mediaType}/`)) { + throw new Error(`${mediaType}DataUrl must contain a ${mediaType} MIME type`); + } + + const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)); + return new Blob([bytes], { type: mimeType }); +} + // Middleware to create and share the Decart client app.use("*", async (c, next) => { const client = createDecartClient({ @@ -20,32 +41,50 @@ app.use("*", async (c, next) => { await next(); }); -// Text-to-image generation +// Image editing app.post("/api/image/generate", async (c) => { - const client = c.get("decart"); - const { prompt } = await c.req.json<{ prompt: string }>(); + try { + const client = c.get("decart"); + const { prompt, imageDataUrl } = await c.req.json<{ prompt?: string; imageDataUrl?: string }>(); - const blob = await client.process({ - model: models.image("lucy-pro-t2i"), - prompt, - }); + if (!prompt || !imageDataUrl) { + return c.json({ error: "prompt and imageDataUrl are required" }, 400); + } - return new Response(blob, { - headers: { "Content-Type": "image/png" }, - }); + const blob = await client.process({ + model: models.image("lucy-pro-i2i"), + prompt, + data: parseBase64DataUrl(imageDataUrl, "image"), + }); + + return new Response(blob, { + headers: { "Content-Type": "image/png" }, + }); + } catch (error) { + return c.json({ error: String(error) }, 500); + } }); -// Submit video generation job (async) +// Submit video editing job (async) app.post("/api/video/generate", async (c) => { - const client = c.get("decart"); - const { prompt } = await c.req.json<{ prompt: string }>(); + try { + const client = c.get("decart"); + const { prompt, videoDataUrl } = await c.req.json<{ prompt?: string; videoDataUrl?: string }>(); - const job = await client.queue.submit({ - model: models.video("lucy-pro-t2v"), - prompt, - }); + if (!prompt || !videoDataUrl) { + return c.json({ error: "prompt and videoDataUrl are required" }, 400); + } + + const job = await client.queue.submit({ + model: models.video("lucy-pro-v2v"), + prompt, + data: parseBase64DataUrl(videoDataUrl, "video"), + }); - return c.json({ jobId: job.job_id, status: job.status }); + return c.json({ jobId: job.job_id, status: job.status }); + } catch (error) { + return c.json({ error: String(error) }, 500); + } }); // Check video job status diff --git a/examples/nextjs-proxy/README.md b/examples/nextjs-proxy/README.md index 56d3d5b..01c5143 100644 --- a/examples/nextjs-proxy/README.md +++ b/examples/nextjs-proxy/README.md @@ -45,19 +45,19 @@ All requests to `/api/decart/*` are automatically proxied to `api.decart.ai` wit ### Client Side (`app/page.tsx`) -The frontend uses the SDK pointing to the proxy endpoint: +The frontend uses the SDK pointing to the proxy endpoint and uploads a source image directly from the browser: ```typescript import { PROXY_ROUTE } from "@decartai/proxy/nextjs"; import { createDecartClient, models } from "@decartai/sdk"; const client = createDecartClient({ - proxy: PROXY_ROUTE, // "/api/decart" + proxy: PROXY_ROUTE, }); const blob = await client.process({ - model: models.image("lucy-pro-t2i"), - prompt: "A beautiful sunset", + model: models.image("lucy-pro-i2i"), + prompt: "Turn this into a watercolor painting", + data: sourceFile, }); ``` - diff --git a/examples/nextjs-proxy/app/page.tsx b/examples/nextjs-proxy/app/page.tsx index 3a6307b..eef72b4 100644 --- a/examples/nextjs-proxy/app/page.tsx +++ b/examples/nextjs-proxy/app/page.tsx @@ -8,12 +8,13 @@ import styles from "./page.module.css"; export default function Home() { const [prompt, setPrompt] = useState(""); + const [sourceImage, setSourceImage] = useState(null); const [loading, setLoading] = useState(false); const [imageUrl, setImageUrl] = useState(null); const [error, setError] = useState(null); const handleGenerate = async () => { - if (!prompt.trim()) return; + if (!prompt.trim() || !sourceImage) return; setLoading(true); setError(null); @@ -21,11 +22,15 @@ export default function Home() { try { const client = createDecartClient({ proxy: PROXY_ROUTE }); - const blob = await client.process({ model: models.image("lucy-pro-t2i"), prompt }); + const blob = await client.process({ + model: models.image("lucy-pro-i2i"), + prompt, + data: sourceImage, + }); const url = URL.createObjectURL(blob); setImageUrl(url); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to generate image"); + setError(err instanceof Error ? err.message : "Failed to edit image"); } finally { setLoading(false); } @@ -34,7 +39,7 @@ export default function Home() { return (

Decart SDK - Next.js Proxy Example

-

Generate images using the Decart SDK through the Next.js proxy

+

Edit an image using the Decart SDK through the Next.js proxy

setPrompt(e.target.value)} - placeholder="Enter a prompt (e.g., 'A beautiful sunset over mountains')" + placeholder="Enter an edit prompt (e.g., 'Turn this into a watercolor painting')" disabled={loading} onKeyDown={(e) => e.key === "Enter" && handleGenerate()} /> -
@@ -57,7 +74,7 @@ export default function Home() {
Generated console.log(job.status), }); diff --git a/examples/sdk-core/image/text-to-image.ts b/examples/sdk-core/image/text-to-image.ts index e565fc2..f498e45 100644 --- a/examples/sdk-core/image/text-to-image.ts +++ b/examples/sdk-core/image/text-to-image.ts @@ -7,11 +7,14 @@ run(async () => { apiKey: process.env.DECART_API_KEY!, }); - console.log("Generating image from text..."); + console.log("Editing image..."); + + const inputImage = fs.readFileSync("input.png"); const blob = await client.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "A cyberpunk cityscape at night with neon lights", + data: new Blob([inputImage]), }); // Save to file diff --git a/examples/sdk-core/video/image-to-video.ts b/examples/sdk-core/video/image-to-video.ts index d4890c0..206e9c6 100644 --- a/examples/sdk-core/video/image-to-video.ts +++ b/examples/sdk-core/video/image-to-video.ts @@ -7,14 +7,14 @@ run(async () => { apiKey: process.env.DECART_API_KEY!, }); - console.log("Generating video from image..."); + console.log("Editing video..."); - const inputImage = fs.readFileSync("input.png"); + const inputVideo = fs.readFileSync("input.mp4"); const result = await client.queue.submitAndPoll({ - model: models.video("lucy-pro-i2v"), - prompt: "The scene comes to life with gentle motion", - data: new Blob([inputImage]), + model: models.video("lucy-pro-v2v"), + prompt: "The scene transforms with a vibrant new style", + data: new Blob([inputVideo]), onStatusChange: (job) => { console.log(`Job ${job.job_id}: ${job.status}`); }, diff --git a/examples/sdk-core/video/manual-polling.ts b/examples/sdk-core/video/manual-polling.ts index 0755168..04241ec 100644 --- a/examples/sdk-core/video/manual-polling.ts +++ b/examples/sdk-core/video/manual-polling.ts @@ -7,12 +7,15 @@ run(async () => { apiKey: process.env.DECART_API_KEY!, }); - console.log("Submitting video generation job..."); + console.log("Submitting video editing job..."); + + const inputVideo = fs.readFileSync("input.mp4"); // Submit job const job = await client.queue.submit({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "A timelapse of a flower blooming", + data: new Blob([inputVideo]), }); console.log("Job ID:", job.job_id); diff --git a/examples/sdk-core/video/text-to-video.ts b/examples/sdk-core/video/text-to-video.ts index 09a7980..09967c4 100644 --- a/examples/sdk-core/video/text-to-video.ts +++ b/examples/sdk-core/video/text-to-video.ts @@ -7,11 +7,14 @@ run(async () => { apiKey: process.env.DECART_API_KEY!, }); - console.log("Generating video from text..."); + console.log("Editing video..."); + + const inputVideo = fs.readFileSync("input.mp4"); const result = await client.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "An astronaut riding a horse on Mars, cinematic lighting", + data: new Blob([inputVideo]), onStatusChange: (job) => { console.log(`Job ${job.job_id}: ${job.status}`); }, diff --git a/packages/proxy/README.md b/packages/proxy/README.md index ab4e39c..2c99967 100644 --- a/packages/proxy/README.md +++ b/packages/proxy/README.md @@ -27,7 +27,7 @@ The proxy supports all model endpoints, apart from the realtime models. ## How It Works -1. Client SDK makes a request to your proxy endpoint (e.g., `/api/decart/v1/generate/lucy-pro-t2i`) +1. Client SDK makes a request to your proxy endpoint (e.g., `/api/decart/v1/generate/lucy-pro-i2i`) 2. Proxy middleware intercepts the request 3. Proxy attaches your server's API key to the request 4. Proxy forwards the request to `https://api.decart.ai` diff --git a/packages/proxy/src/express/README.md b/packages/proxy/src/express/README.md index e538975..1fb32db 100644 --- a/packages/proxy/src/express/README.md +++ b/packages/proxy/src/express/README.md @@ -25,7 +25,7 @@ const client = createDecartClient({ proxy: "/api/decart" }); // Use the client as normal const result = await client.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "A beautiful sunset", }); ``` diff --git a/packages/proxy/src/express/middleware.test.ts b/packages/proxy/src/express/middleware.test.ts index c406b1c..af94ed6 100644 --- a/packages/proxy/src/express/middleware.test.ts +++ b/packages/proxy/src/express/middleware.test.ts @@ -75,7 +75,7 @@ describe("Decart Proxy Middleware", () => { const testApp = createTestApp({}); const { port, close } = await startTestServer(testApp); - const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-t2i"), { + const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-i2i"), { method: "POST", }); @@ -91,18 +91,18 @@ describe("Decart Proxy Middleware", () => { const { port, close } = await startTestServer(testApp); mswServer.use( - http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { lastRequest = request; return HttpResponse.json({}); }), ); - await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-t2i"), { + await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-i2i"), { method: "POST", }); expect(lastRequest).not.toBeNull(); - expect(lastRequest?.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`); + expect(lastRequest?.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`); await close(); }); @@ -114,14 +114,14 @@ describe("Decart Proxy Middleware", () => { const { port, close } = await startTestServer(app); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { lastRequest = request; return HttpResponse.json({}); }), ); const testBody = JSON.stringify({ prompt: "A beautiful sunset over the ocean", resolution: "720p" }); - const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-t2i"), { + const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-i2i"), { method: "POST", headers: { "Content-Type": "application/json" }, body: testBody, @@ -130,7 +130,7 @@ describe("Decart Proxy Middleware", () => { expect(response.status).toBe(200); expect(lastRequest).not.toBeNull(); expect(lastRequest?.method).toBe("POST"); - expect(lastRequest?.url).toContain("/v1/generate/lucy-pro-t2i"); + expect(lastRequest?.url).toContain("/v1/generate/lucy-pro-i2i"); // The middleware converts body to ArrayBuffer if (lastRequest) { const body = await lastRequest.arrayBuffer(); @@ -168,13 +168,13 @@ describe("Decart Proxy Middleware", () => { const { port, close } = await startTestServer(app); mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, async ({ request }) => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, async ({ request }) => { lastRequest = request; return HttpResponse.json({}); }), ); - await fetch(getProxyUrl(port, "/v1/jobs/lucy-pro-t2v"), { + await fetch(getProxyUrl(port, "/v1/jobs/lucy-pro-v2v"), { method: "POST", headers: { "User-Agent": "Custom-Agent/1.0", @@ -198,7 +198,7 @@ describe("Decart Proxy Middleware", () => { const mockResponse = { job_id: "job_abc123", status: "pending" }; mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, () => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, () => { return HttpResponse.json(mockResponse, { status: 201, headers: { "Content-Type": "application/json" }, @@ -206,7 +206,7 @@ describe("Decart Proxy Middleware", () => { }), ); - const response = await fetch(getProxyUrl(port, "/v1/jobs/lucy-pro-t2v"), { + const response = await fetch(getProxyUrl(port, "/v1/jobs/lucy-pro-v2v"), { method: "POST", }); @@ -254,7 +254,7 @@ describe("Decart Proxy Middleware", () => { const { port, close } = await startTestServer(app); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.json( { error: "Internal Server Error" }, { @@ -265,7 +265,7 @@ describe("Decart Proxy Middleware", () => { }), ); - const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-t2i"), { + const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-i2i"), { method: "POST", }); @@ -292,12 +292,12 @@ describe("Decart Proxy Middleware", () => { // Trigger a fetch failure mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.error(); }), ); - const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-t2i"), { + const response = await fetch(getProxyUrl(port, "/v1/generate/lucy-pro-i2i"), { method: "POST", }); diff --git a/packages/proxy/src/nextjs/README.md b/packages/proxy/src/nextjs/README.md index eb2d07f..080c5b3 100644 --- a/packages/proxy/src/nextjs/README.md +++ b/packages/proxy/src/nextjs/README.md @@ -35,7 +35,7 @@ const client = createDecartClient({ proxy: PROXY_ROUTE }); // Use the client as normal const result = await client.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "A beautiful sunset", }); ``` diff --git a/packages/proxy/src/nextjs/route.test.ts b/packages/proxy/src/nextjs/route.test.ts index 077fd48..f15105c 100644 --- a/packages/proxy/src/nextjs/route.test.ts +++ b/packages/proxy/src/nextjs/route.test.ts @@ -98,7 +98,7 @@ describe("Next.js Proxy Adapter", () => { describe("Initialization & Configuration", () => { it("should return 401 if no API key is provided", async () => { const handlers = route({}); - const request = createNextRequest("/v1/generate/lucy-pro-t2i", { method: "POST" }); + const request = createNextRequest("/v1/generate/lucy-pro-i2i", { method: "POST" }); const response = await handlers.POST(request); @@ -176,18 +176,18 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); - const request = createNextRequest("/v1/generate/lucy-pro-t2i", { method: "POST" }); + const request = createNextRequest("/v1/generate/lucy-pro-i2i", { method: "POST" }); await handlers.POST(request); expect(captureRequest).toHaveBeenCalledTimes(1); const capturedRequest = captureRequest.mock.lastCall?.[0] as Request; - expect(capturedRequest.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`); + expect(capturedRequest.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`); }); }); @@ -197,14 +197,14 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); const testBody = JSON.stringify({ prompt: "A beautiful sunset over the ocean", resolution: "720p" }); - const request = createNextRequest("/v1/generate/lucy-pro-t2i", { + const request = createNextRequest("/v1/generate/lucy-pro-i2i", { method: "POST", headers: { "Content-Type": "application/json" }, body: testBody, @@ -216,7 +216,7 @@ describe("Next.js Proxy Adapter", () => { expect(captureRequest).toHaveBeenCalledTimes(1); const capturedRequest = captureRequest.mock.lastCall?.[0] as Request; expect(capturedRequest.method).toBe("POST"); - expect(capturedRequest.url).toContain("/v1/generate/lucy-pro-t2i"); + expect(capturedRequest.url).toContain("/v1/generate/lucy-pro-i2i"); const body = await capturedRequest.text(); expect(body).toBe(testBody); @@ -248,13 +248,13 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, async ({ request }) => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); - const request = createNextRequest("/v1/jobs/lucy-pro-t2v", { + const request = createNextRequest("/v1/jobs/lucy-pro-v2v", { method: "POST", headers: { "User-Agent": "Custom-Agent/1.0", @@ -278,7 +278,7 @@ describe("Next.js Proxy Adapter", () => { const mockResponse = { job_id: "job_abc123", status: "pending" }; mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, () => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, () => { return HttpResponse.json(mockResponse, { status: 201, headers: { "Content-Type": "application/json" }, @@ -286,7 +286,7 @@ describe("Next.js Proxy Adapter", () => { }), ); - const request = createNextRequest("/v1/jobs/lucy-pro-t2v", { method: "POST" }); + const request = createNextRequest("/v1/jobs/lucy-pro-v2v", { method: "POST" }); const response = await handlers.POST(request); expect(response.status).toBe(201); @@ -325,7 +325,7 @@ describe("Next.js Proxy Adapter", () => { const handlers = route({ apiKey: "test-key" }); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.json( { error: "Internal Server Error" }, { @@ -336,7 +336,7 @@ describe("Next.js Proxy Adapter", () => { }), ); - const request = createNextRequest("/v1/generate/lucy-pro-t2i", { method: "POST" }); + const request = createNextRequest("/v1/generate/lucy-pro-i2i", { method: "POST" }); const response = await handlers.POST(request); expect(response.status).toBe(500); @@ -350,12 +350,12 @@ describe("Next.js Proxy Adapter", () => { const handlers = route({ apiKey: "test-key" }); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.error(); }), ); - const request = createNextRequest("/v1/generate/lucy-pro-t2i", { method: "POST" }); + const request = createNextRequest("/v1/generate/lucy-pro-i2i", { method: "POST" }); const response = await handlers.POST(request); expect(response.status).toBe(500); @@ -409,7 +409,7 @@ describe("Next.js Proxy Adapter", () => { describe("Initialization & Configuration", () => { it("should return 401 if no API key is provided", async () => { const proxyHandler = handler({}); - const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-t2i"], { + const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-i2i"], { method: "POST", }); @@ -426,13 +426,13 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); - const { req, res } = createMockPagesRouter(["v1", "generate", "lucy-pro-t2i"], { + const { req, res } = createMockPagesRouter(["v1", "generate", "lucy-pro-i2i"], { method: "POST", }); @@ -441,7 +441,7 @@ describe("Next.js Proxy Adapter", () => { expect(captureRequest).toHaveBeenCalledTimes(1); const capturedRequest = captureRequest.mock.lastCall?.[0] as Request; - expect(capturedRequest.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-t2i`); + expect(capturedRequest.url).toContain(`${CUSTOM_BASE_URL}/v1/generate/lucy-pro-i2i`); }); }); @@ -451,14 +451,14 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, async ({ request }) => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); const testBody = { prompt: "A beautiful sunset over the ocean", resolution: "720p" }; - const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-t2i"], { + const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-i2i"], { method: "POST", body: testBody, headers: { "content-type": "application/json" }, @@ -472,7 +472,7 @@ describe("Next.js Proxy Adapter", () => { expect(captureRequest).toHaveBeenCalledTimes(1); const capturedRequest = captureRequest.mock.lastCall?.[0] as Request; expect(capturedRequest.method).toBe("POST"); - expect(capturedRequest.url).toContain("/v1/generate/lucy-pro-t2i"); + expect(capturedRequest.url).toContain("/v1/generate/lucy-pro-i2i"); const body = await capturedRequest.text(); expect(body).toBe(JSON.stringify(testBody)); @@ -508,13 +508,13 @@ describe("Next.js Proxy Adapter", () => { const captureRequest = vi.fn(); mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, async ({ request }) => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, async ({ request }) => { captureRequest(request); return HttpResponse.json({}); }), ); - const { req, res } = createMockPagesRouter(["v1", "jobs", "lucy-pro-t2v"], { + const { req, res } = createMockPagesRouter(["v1", "jobs", "lucy-pro-v2v"], { method: "POST", body: { prompt: "A cat playing piano" }, headers: { @@ -539,7 +539,7 @@ describe("Next.js Proxy Adapter", () => { const mockResponse = { job_id: "job_abc123", status: "pending" }; mswServer.use( - http.post(`${BASE_URL}/v1/jobs/lucy-pro-t2v`, () => { + http.post(`${BASE_URL}/v1/jobs/lucy-pro-v2v`, () => { return HttpResponse.json(mockResponse, { status: 201, headers: { "Content-Type": "application/json" }, @@ -547,7 +547,7 @@ describe("Next.js Proxy Adapter", () => { }), ); - const { req, res, getResponse } = createMockPagesRouter(["v1", "jobs", "lucy-pro-t2v"], { + const { req, res, getResponse } = createMockPagesRouter(["v1", "jobs", "lucy-pro-v2v"], { method: "POST", }); @@ -595,7 +595,7 @@ describe("Next.js Proxy Adapter", () => { const proxyHandler = handler({ apiKey: "test-key" }); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.json( { error: "Internal Server Error" }, { @@ -606,7 +606,7 @@ describe("Next.js Proxy Adapter", () => { }), ); - const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-t2i"], { + const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-i2i"], { method: "POST", }); @@ -624,12 +624,12 @@ describe("Next.js Proxy Adapter", () => { const proxyHandler = handler({ apiKey: "test-key" }); mswServer.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.error(); }), ); - const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-t2i"], { + const { req, res, getResponse } = createMockPagesRouter(["v1", "generate", "lucy-pro-i2i"], { method: "POST", }); diff --git a/packages/sdk/AGENTS.md b/packages/sdk/AGENTS.md index f8e1d63..26b40a1 100644 --- a/packages/sdk/AGENTS.md +++ b/packages/sdk/AGENTS.md @@ -69,15 +69,10 @@ - `lucy_2_rt` - Real-time video editing model (supports reference image) ### Video Models (Queue API) -- `lucy-dev-i2v` - image-to-video (Dev quality) -- `lucy-fast-v2v` - video-to-video (Fast quality) -- `lucy-pro-t2v` - text-to-video (Pro quality) -- `lucy-pro-i2v` - image-to-video (Pro quality) - `lucy-pro-v2v` - video-to-video (Pro quality) - `lucy-motion` - motion-based image-to-video (trajectory-guided animation) - `lucy-restyle-v2v` - video restyling (video-to-video) - `lucy-2-v2v` - video-to-video editing (long-form, 720p) ### Image Models (Process API) -- `lucy-pro-t2i` - text-to-image (Pro quality) - `lucy-pro-i2i` - image-to-image (Pro quality) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index feffdf4..edc29f7 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -75,8 +75,9 @@ const client = createDecartClient({ // Submit and poll automatically const result = await client.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "A cat playing piano", + data: videoFile, onStatusChange: (job) => { console.log(`Status: ${job.status}`); } @@ -94,8 +95,9 @@ Or manage the polling manually: ```typescript // Submit the job const job = await client.queue.submit({ - model: models.video("lucy-pro-t2v"), - prompt: "A cat playing piano" + model: models.video("lucy-pro-v2v"), + prompt: "A cat playing piano", + data: videoFile }); console.log(`Job ID: ${job.job_id}`); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index caddfa2..5fd7686 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -221,8 +221,9 @@ export const createDecartClient = (options: DecartClientOptions = {}) => { * ```ts * const client = createDecartClient({ apiKey: "your-api-key" }); * const result = await client.process({ - * model: models.image("lucy-pro-t2i"), - * prompt: "A beautiful sunset over the ocean" + * model: models.image("lucy-pro-i2i"), + * prompt: "A beautiful sunset over the ocean", + * data: imageBlob * }); * ``` */ @@ -238,15 +239,17 @@ export const createDecartClient = (options: DecartClientOptions = {}) => { * * // Option 1: Submit and poll automatically * const result = await client.queue.submitAndPoll({ - * model: models.video("lucy-pro-t2v"), + * model: models.video("lucy-pro-v2v"), * prompt: "A beautiful sunset over the ocean", + * data: videoBlob, * onStatusChange: (job) => console.log(`Job ${job.job_id}: ${job.status}`) * }); * * // Option 2: Submit and poll manually * const job = await client.queue.submit({ - * model: models.video("lucy-pro-t2v"), - * prompt: "A beautiful sunset over the ocean" + * model: models.video("lucy-pro-v2v"), + * prompt: "A beautiful sunset over the ocean", + * data: videoBlob * }); * * // Poll until completion diff --git a/packages/sdk/src/queue/client.ts b/packages/sdk/src/queue/client.ts index aeb92ed..7d822d0 100644 --- a/packages/sdk/src/queue/client.ts +++ b/packages/sdk/src/queue/client.ts @@ -24,8 +24,9 @@ export type QueueClient = { * @example * ```ts * const job = await client.queue.submit({ - * model: models.video("lucy-pro-t2v"), - * prompt: "A cat playing piano" + * model: models.video("lucy-pro-v2v"), + * prompt: "A cat playing piano", + * data: videoBlob * }); * console.log(job.job_id); // "job_abc123" * ``` @@ -62,8 +63,9 @@ export type QueueClient = { * @example * ```ts * const result = await client.queue.submitAndPoll({ - * model: models.video("lucy-pro-t2v"), + * model: models.video("lucy-pro-v2v"), * prompt: "A beautiful sunset", + * data: videoBlob, * onStatusChange: (job) => { * console.log(`Job ${job.job_id}: ${job.status}`); * } diff --git a/packages/sdk/src/shared/model.ts b/packages/sdk/src/shared/model.ts index c095886..341bcea 100644 --- a/packages/sdk/src/shared/model.ts +++ b/packages/sdk/src/shared/model.ts @@ -9,16 +9,12 @@ export const realtimeModels = z.union([ z.literal("live_avatar"), ]); export const videoModels = z.union([ - z.literal("lucy-dev-i2v"), - z.literal("lucy-fast-v2v"), - z.literal("lucy-pro-t2v"), - z.literal("lucy-pro-i2v"), z.literal("lucy-pro-v2v"), z.literal("lucy-motion"), z.literal("lucy-restyle-v2v"), z.literal("lucy-2-v2v"), ]); -export const imageModels = z.union([z.literal("lucy-pro-t2i"), z.literal("lucy-pro-i2i")]); +export const imageModels = z.literal("lucy-pro-i2i"); export const modelSchema = z.union([realtimeModels, videoModels, imageModels]); export type Model = z.infer; @@ -53,15 +49,6 @@ const fileInputSchema = z.union([ }), ]); -/** - * Resolution schema for dev models. Supports only 720p. - */ -const devResolutionSchema = z - .literal("720p") - .default("720p") - .optional() - .describe("The resolution to use for the generation. For dev models, only `720p` is supported."); - /** * Resolution schema for pro models. * @param defaultValue - Optional default value (e.g., "720p") @@ -89,34 +76,6 @@ const proV2vResolutionSchema = z .default("720p"); export const modelInputSchemas = { - "lucy-pro-t2v": z.object({ - prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), - seed: z.number().optional().describe("The seed to use for the generation"), - resolution: proResolutionSchema(), - orientation: z.string().optional().describe("The orientation to use for the generation"), - }), - "lucy-pro-t2i": z.object({ - prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), - seed: z.number().optional().describe("The seed to use for the generation"), - resolution: proResolutionSchema(), - orientation: z.string().optional().describe("The orientation to use for the generation"), - }), - "lucy-pro-i2v": z.object({ - prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), - data: fileInputSchema.describe( - "The image data to use for generation (File, Blob, ReadableStream, URL, or string URL). Output video is limited to 5 seconds.", - ), - seed: z.number().optional().describe("The seed to use for the generation"), - resolution: proResolutionSchema(), - }), - "lucy-dev-i2v": z.object({ - prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), - data: fileInputSchema.describe( - "The image data to use for generation (File, Blob, ReadableStream, URL, or string URL). Output video is limited to 5 seconds.", - ), - seed: z.number().optional().describe("The seed to use for the generation"), - resolution: devResolutionSchema, - }), "lucy-pro-v2v": z.object({ prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), data: fileInputSchema.describe( @@ -131,15 +90,6 @@ export const modelInputSchemas = { resolution: proV2vResolutionSchema, enhance_prompt: z.boolean().optional().describe("Whether to enhance the prompt"), }), - "lucy-fast-v2v": z.object({ - prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), - data: fileInputSchema.describe( - "The video data to use for generation (File, Blob, ReadableStream, URL, or string URL). Output video is limited to 5 seconds.", - ), - seed: z.number().optional().describe("The seed to use for the generation"), - resolution: proV2vResolutionSchema, - enhance_prompt: z.boolean().optional().describe("Whether to enhance the prompt"), - }), "lucy-pro-i2i": z.object({ prompt: z.string().min(1).max(1000).describe("The prompt to use for the generation"), data: fileInputSchema.describe( @@ -289,15 +239,6 @@ const _models = { }, }, image: { - "lucy-pro-t2i": { - urlPath: "/v1/generate/lucy-pro-t2i", - queueUrlPath: "/v1/jobs/lucy-pro-t2i", - name: "lucy-pro-t2i" as const, - fps: 25, - width: 1280, - height: 704, - inputSchema: modelInputSchemas["lucy-pro-t2i"], - }, "lucy-pro-i2i": { urlPath: "/v1/generate/lucy-pro-i2i", queueUrlPath: "/v1/jobs/lucy-pro-i2i", @@ -309,42 +250,6 @@ const _models = { }, }, video: { - "lucy-dev-i2v": { - urlPath: "/v1/generate/lucy-dev-i2v", - queueUrlPath: "/v1/jobs/lucy-dev-i2v", - name: "lucy-dev-i2v" as const, - fps: 25, - width: 1280, - height: 704, - inputSchema: modelInputSchemas["lucy-dev-i2v"], - }, - "lucy-fast-v2v": { - urlPath: "/v1/generate/lucy-fast-v2v", - queueUrlPath: "/v1/jobs/lucy-fast-v2v", - name: "lucy-fast-v2v" as const, - fps: 25, - width: 1280, - height: 720, - inputSchema: modelInputSchemas["lucy-fast-v2v"], - }, - "lucy-pro-t2v": { - urlPath: "/v1/generate/lucy-pro-t2v", - queueUrlPath: "/v1/jobs/lucy-pro-t2v", - name: "lucy-pro-t2v" as const, - fps: 25, - width: 1280, - height: 704, - inputSchema: modelInputSchemas["lucy-pro-t2v"], - }, - "lucy-pro-i2v": { - urlPath: "/v1/generate/lucy-pro-i2v", - queueUrlPath: "/v1/jobs/lucy-pro-i2v", - name: "lucy-pro-i2v" as const, - fps: 25, - width: 1280, - height: 704, - inputSchema: modelInputSchemas["lucy-pro-i2v"], - }, "lucy-pro-v2v": { urlPath: "/v1/generate/lucy-pro-v2v", queueUrlPath: "/v1/jobs/lucy-pro-v2v", @@ -396,13 +301,10 @@ export const models = { * Get a video model identifier. * * Available options: - * - `"lucy-pro-t2v"` - Text-to-video - * - `"lucy-pro-i2v"` - Image-to-video * - `"lucy-pro-v2v"` - Video-to-video - * - `"lucy-dev-i2v"` - Image-to-video (Dev quality) - * - `"lucy-fast-v2v"` - Video-to-video (Fast quality) * - `"lucy-restyle-v2v"` - Video-to-video (Restyling) * - `"lucy-2-v2v"` - Video-to-video (Long-form editing, 720p) + * - `"lucy-motion"` - Motion generation */ video: (model: T): ModelDefinition => { const modelDefinition = _models.video[model]; @@ -415,7 +317,6 @@ export const models = { * Get an image model identifier. * * Available options: - * - `"lucy-pro-t2i"` - Text-to-image * - `"lucy-pro-i2i"` - Image-to-image */ image: (model: T): ModelDefinition => { diff --git a/packages/sdk/src/tokens/client.ts b/packages/sdk/src/tokens/client.ts index 6353f32..18c9afc 100644 --- a/packages/sdk/src/tokens/client.ts +++ b/packages/sdk/src/tokens/client.ts @@ -46,7 +46,7 @@ export type TokensClient = { * // With expiry, model restrictions, and constraints: * const token = await client.tokens.create({ * expiresIn: 300, - * allowedModels: ["lucy-pro-t2v", "lucy-pro-i2v"], + * allowedModels: ["lucy-pro-v2v", "lucy-restyle-v2v"], * constraints: { realtime: { maxSessionDuration: 120 } }, * }); * ``` diff --git a/packages/sdk/tests/e2e.test.ts b/packages/sdk/tests/e2e.test.ts index e5ca59c..93275f5 100644 --- a/packages/sdk/tests/e2e.test.ts +++ b/packages/sdk/tests/e2e.test.ts @@ -65,17 +65,6 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => { } describe("Process API - Image Models", () => { - it("lucy-pro-t2i: text-to-image", async () => { - const result = await client.process({ - model: models.image("lucy-pro-t2i"), - prompt: "A serene Japanese garden with cherry blossoms and a wooden bridge", - seed: 222, - orientation: "landscape", - }); - - await expectResult(result, "lucy-pro-t2i", ".png"); - }); - it("lucy-pro-i2i: image-to-image", async () => { const result = await client.process({ model: models.image("lucy-pro-i2i"), @@ -90,42 +79,6 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => { }); describe("Queue API - Video Models", () => { - it("lucy-pro-t2v: text-to-video", async () => { - const result = await client.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), - prompt: "A majestic eagle soaring through mountain peaks at sunset", - seed: 42, - resolution: "720p", - orientation: "landscape", - }); - - await expectResult(result, "lucy-pro-t2v", ".mp4"); - }); - - it("lucy-dev-i2v: image-to-video (dev)", async () => { - const result = await client.queue.submitAndPoll({ - model: models.video("lucy-dev-i2v"), - prompt: "The image comes to life with gentle movements", - data: imageBlob, - seed: 123, - resolution: "720p", - }); - - await expectResult(result, "lucy-dev-i2v", ".mp4"); - }); - - it("lucy-pro-i2v: image-to-video (pro)", async () => { - const result = await client.queue.submitAndPoll({ - model: models.video("lucy-pro-i2v"), - prompt: "Transform the image into a dynamic video scene", - data: imageBlob, - seed: 456, - resolution: "720p", - }); - - await expectResult(result, "lucy-pro-i2v", ".mp4"); - }); - it("lucy-pro-v2v: video-to-video", async () => { const result = await client.queue.submitAndPoll({ model: models.video("lucy-pro-v2v"), @@ -138,17 +91,6 @@ describe.concurrent("E2E Tests", { timeout: TIMEOUT, retry: 2 }, () => { await expectResult(result, "lucy-pro-v2v", ".mp4"); }); - it("lucy-fast-v2v: video-to-video (fast)", async () => { - const result = await client.queue.submitAndPoll({ - model: models.video("lucy-fast-v2v"), - prompt: "Watercolor painting style", - data: videoBlob, - seed: 888, - }); - - await expectResult(result, "lucy-fast-v2v", ".mp4"); - }); - it("lucy-restyle-v2v: video restyling (prompt)", async () => { const result = await client.queue.submitAndPoll({ model: models.video("lucy-restyle-v2v"), diff --git a/packages/sdk/tests/unit.test.ts b/packages/sdk/tests/unit.test.ts index ecfb468..26923a9 100644 --- a/packages/sdk/tests/unit.test.ts +++ b/packages/sdk/tests/unit.test.ts @@ -105,12 +105,15 @@ describe("Decart SDK", () => { }); describe("Model Processing", () => { - it("processes text-to-image", async () => { - server.use(createMockHandler("/v1/generate/lucy-pro-t2i")); + it("processes image-to-image", async () => { + server.use(createMockHandler("/v1/generate/lucy-pro-i2i")); + + const testBlob = new Blob(["test-image"], { type: "image/png" }); const result = await decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "A cat playing piano", + data: testBlob, seed: 42, }); @@ -121,11 +124,14 @@ describe("Decart SDK", () => { }); it("includes User-Agent header in requests", async () => { - server.use(createMockHandler("/v1/generate/lucy-pro-t2i")); + server.use(createMockHandler("/v1/generate/lucy-pro-i2i")); + + const testBlob = new Blob(["test-image"], { type: "image/png" }); await decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "Test prompt", + data: testBlob, }); const userAgent = lastRequest?.headers.get("user-agent"); @@ -140,11 +146,14 @@ describe("Decart SDK", () => { integration: "vercel-ai-sdk/3.0.0", }); - server.use(createMockHandler("/v1/generate/lucy-pro-t2i")); + server.use(createMockHandler("/v1/generate/lucy-pro-i2i")); + + const testBlob = new Blob(["test-image"], { type: "image/png" }); await decartWithIntegration.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "Test with integration", + data: testBlob, }); const userAgent = lastRequest?.headers.get("user-agent"); @@ -153,12 +162,15 @@ describe("Decart SDK", () => { expect(userAgent).toMatch(/^decart-js-sdk\/[\d.]+-?\w* lang\/js vercel-ai-sdk\/3\.0\.0 runtime\/[\w./]+$/); }); - it("processes text-to-image with resolution", async () => { - server.use(createMockHandler("/v1/generate/lucy-pro-t2i")); + it("processes image-to-image with resolution", async () => { + server.use(createMockHandler("/v1/generate/lucy-pro-i2i")); + + const testBlob = new Blob(["test-image"], { type: "image/png" }); const result = await decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "A beautiful landscape", + data: testBlob, seed: 123, resolution: "480p", }); @@ -170,7 +182,7 @@ describe("Decart SDK", () => { expect(lastFormData?.get("resolution")).toBe("480p"); }); - it("processes image-to-image", async () => { + it("processes image-to-image with enhance_prompt", async () => { server.use(createMockHandler("/v1/generate/lucy-pro-i2i")); const testBlob = new Blob(["test-image"], { type: "image/png" }); @@ -195,7 +207,7 @@ describe("Decart SDK", () => { const controller = new AbortController(); server.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, async () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return HttpResponse.arrayBuffer(MOCK_RESPONSE_DATA, { headers: { "Content-Type": "application/octet-stream" }, @@ -203,9 +215,12 @@ describe("Decart SDK", () => { }), ); + const testBlob = new Blob(["test-image"], { type: "image/png" }); + const processPromise = decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "test", + data: testBlob, signal: controller.signal, }); @@ -216,15 +231,6 @@ describe("Decart SDK", () => { }); describe("Input Validation", () => { - it("validates required inputs for text-to-image", async () => { - await expect( - decart.process({ - model: models.image("lucy-pro-t2i"), - // biome-ignore lint/suspicious/noExplicitAny: testing invalid input - } as any), - ).rejects.toThrow("Invalid inputs"); - }); - it("validates required inputs for image-to-image", async () => { await expect( decart.process({ @@ -236,10 +242,13 @@ describe("Decart SDK", () => { }); it("validates prompt max length is 1000 characters", async () => { + const testBlob = new Blob(["test-image"], { type: "image/png" }); + await expect( decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "a".repeat(1001), + data: testBlob, }), ).rejects.toThrow("expected string to have <=1000 characters"); }); @@ -248,15 +257,18 @@ describe("Decart SDK", () => { describe("Error Handling", () => { it("handles API errors", async () => { server.use( - http.post(`${BASE_URL}/v1/generate/lucy-pro-t2i`, () => { + http.post(`${BASE_URL}/v1/generate/lucy-pro-i2i`, () => { return HttpResponse.text("Internal Server Error", { status: 500 }); }), ); + const testBlob = new Blob(["test-image"], { type: "image/png" }); + await expect( decart.process({ - model: models.image("lucy-pro-t2i"), + model: models.image("lucy-pro-i2i"), prompt: "test", + data: testBlob, }), ).rejects.toThrow("Processing failed"); }); @@ -293,31 +305,6 @@ describe("Queue API", () => { }); describe("submit", () => { - it("submits text-to-video job", async () => { - server.use( - http.post("http://localhost/v1/jobs/lucy-pro-t2v", async ({ request }) => { - lastRequest = request; - lastFormData = await request.formData(); - return HttpResponse.json({ - job_id: "job_123", - status: "pending", - }); - }), - ); - - const result = await decart.queue.submit({ - model: models.video("lucy-pro-t2v"), - prompt: "A cat playing piano", - seed: 42, - }); - - expect(result.job_id).toBe("job_123"); - expect(result.status).toBe("pending"); - expect(lastRequest?.headers.get("x-api-key")).toBe("test-api-key"); - expect(lastFormData?.get("prompt")).toBe("A cat playing piano"); - expect(lastFormData?.get("seed")).toBe("42"); - }); - it("submits video-to-video job", async () => { server.use( http.post("http://localhost/v1/jobs/lucy-pro-v2v", async ({ request }) => { @@ -598,34 +585,6 @@ describe("Queue API", () => { ).rejects.toThrow("Invalid inputs"); }); - it("submits image-to-video job", async () => { - server.use( - http.post("http://localhost/v1/jobs/lucy-pro-i2v", async ({ request }) => { - lastRequest = request; - lastFormData = await request.formData(); - return HttpResponse.json({ - job_id: "job_i2v", - status: "pending", - }); - }), - ); - - const testBlob = new Blob(["test-image"], { type: "image/png" }); - - const result = await decart.queue.submit({ - model: models.video("lucy-pro-i2v"), - prompt: "The image comes to life", - data: testBlob, - }); - - expect(result.job_id).toBe("job_i2v"); - expect(result.status).toBe("pending"); - expect(lastFormData?.get("prompt")).toBe("The image comes to life"); - - const dataFile = lastFormData?.get("data") as File; - expect(dataFile).toBeInstanceOf(File); - }); - it("submits motion video job", async () => { server.use( http.post("http://localhost/v1/jobs/lucy-motion", async ({ request }) => { @@ -657,15 +616,6 @@ describe("Queue API", () => { expect(lastFormData?.get("trajectory")).toBeDefined(); }); - it("validates required inputs", async () => { - await expect( - decart.queue.submit({ - model: models.video("lucy-pro-t2v"), - // biome-ignore lint/suspicious/noExplicitAny: testing invalid input - } as any), - ).rejects.toThrow("Invalid inputs"); - }); - it("validates required inputs for video-to-video", async () => { await expect( decart.queue.submit({ @@ -694,15 +644,18 @@ describe("Queue API", () => { it("handles API errors", async () => { server.use( - http.post("http://localhost/v1/jobs/lucy-pro-t2v", () => { + http.post("http://localhost/v1/jobs/lucy-pro-v2v", () => { return HttpResponse.text("Internal Server Error", { status: 500 }); }), ); + const testBlob = new Blob(["test-video"], { type: "video/mp4" }); + await expect( decart.queue.submit({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "test", + data: testBlob, }), ).rejects.toThrow("Failed to submit job"); }); @@ -772,7 +725,7 @@ describe("Queue API", () => { const statusChanges: Array<{ job_id: string; status: string }> = []; server.use( - http.post("http://localhost/v1/jobs/lucy-pro-t2v", async ({ request }) => { + http.post("http://localhost/v1/jobs/lucy-pro-v2v", async ({ request }) => { lastFormData = await request.formData(); return HttpResponse.json({ job_id: "job_456", @@ -799,9 +752,12 @@ describe("Queue API", () => { }), ); + const testBlob = new Blob(["test-video"], { type: "video/mp4" }); + const result = await decart.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "A beautiful sunset", + data: testBlob, onStatusChange: (job) => { statusChanges.push({ job_id: job.job_id, status: job.status }); }, @@ -817,7 +773,7 @@ describe("Queue API", () => { it("returns failed status when job fails", async () => { server.use( - http.post("http://localhost/v1/jobs/lucy-pro-t2v", () => { + http.post("http://localhost/v1/jobs/lucy-pro-v2v", () => { return HttpResponse.json({ job_id: "job_789", status: "pending", @@ -831,9 +787,12 @@ describe("Queue API", () => { }), ); + const testBlob = new Blob(["test-video"], { type: "video/mp4" }); + const result = await decart.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "This will fail", + data: testBlob, }); expect(result.status).toBe("failed"); @@ -846,7 +805,7 @@ describe("Queue API", () => { const controller = new AbortController(); server.use( - http.post("http://localhost/v1/jobs/lucy-pro-t2v", () => { + http.post("http://localhost/v1/jobs/lucy-pro-v2v", () => { return HttpResponse.json({ job_id: "job_abort", status: "pending", @@ -861,9 +820,12 @@ describe("Queue API", () => { }), ); + const testBlob = new Blob(["test-video"], { type: "video/mp4" }); + const pollPromise = decart.queue.submitAndPoll({ - model: models.video("lucy-pro-t2v"), + model: models.video("lucy-pro-v2v"), prompt: "test", + data: testBlob, signal: controller.signal, }); @@ -1063,10 +1025,10 @@ describe("Tokens API", () => { }), ); - await decart.tokens.create({ allowedModels: ["lucy-pro-t2v", "lucy-pro-i2v"] }); + await decart.tokens.create({ allowedModels: ["lucy-pro-v2v", "lucy-restyle-v2v"] }); const body = await lastRequest?.json(); - expect(body).toEqual({ allowedModels: ["lucy-pro-t2v", "lucy-pro-i2v"] }); + expect(body).toEqual({ allowedModels: ["lucy-pro-v2v", "lucy-restyle-v2v"] }); }); it("sends constraints in request body", async () => { @@ -1100,7 +1062,7 @@ describe("Tokens API", () => { await decart.tokens.create({ metadata: { role: "viewer" }, expiresIn: 300, - allowedModels: ["lucy-pro-t2v"], + allowedModels: ["lucy-pro-v2v"], constraints: { realtime: { maxSessionDuration: 60 } }, }); @@ -1108,7 +1070,7 @@ describe("Tokens API", () => { expect(body).toEqual({ metadata: { role: "viewer" }, expiresIn: 300, - allowedModels: ["lucy-pro-t2v"], + allowedModels: ["lucy-pro-v2v"], constraints: { realtime: { maxSessionDuration: 60 } }, }); }); @@ -1120,19 +1082,19 @@ describe("Tokens API", () => { return HttpResponse.json({ apiKey: "ek_test123", expiresAt: "2024-12-15T12:15:00Z", - permissions: { models: ["lucy-pro-t2v", "lucy-pro-i2v"] }, + permissions: { models: ["lucy-pro-v2v", "lucy-restyle-v2v"] }, constraints: { realtime: { maxSessionDuration: 120 } }, }); }), ); const result = await decart.tokens.create({ - allowedModels: ["lucy-pro-t2v", "lucy-pro-i2v"], + allowedModels: ["lucy-pro-v2v", "lucy-restyle-v2v"], constraints: { realtime: { maxSessionDuration: 120 } }, }); expect(result.apiKey).toBe("ek_test123"); - expect(result.permissions).toEqual({ models: ["lucy-pro-t2v", "lucy-pro-i2v"] }); + expect(result.permissions).toEqual({ models: ["lucy-pro-v2v", "lucy-restyle-v2v"] }); expect(result.constraints).toEqual({ realtime: { maxSessionDuration: 120 } }); }); }); From 225d7874846614a4b5509a22f3caed9757b2b0d4 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Thu, 26 Mar 2026 15:46:44 +0200 Subject: [PATCH 2/5] fix: remove leftover text-to-image command from bun CLI validation Co-Authored-By: Claude Opus 4.6 --- examples/bun-cli/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/bun-cli/cli.ts b/examples/bun-cli/cli.ts index a683ce2..c985546 100755 --- a/examples/bun-cli/cli.ts +++ b/examples/bun-cli/cli.ts @@ -3,7 +3,7 @@ import { createDecartClient, models } from "@decartai/sdk"; const [command, prompt, inputPath] = process.argv.slice(2); -if ((command !== "image-edit" && command !== "text-to-image") || !prompt || !inputPath) { +if (command !== "image-edit" || !prompt || !inputPath) { console.error("Usage: decart image-edit "); process.exit(1); } From 8f12568d212c36c3ae1092e85d51052f0bf558db Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Thu, 26 Mar 2026 15:49:36 +0200 Subject: [PATCH 3/5] fix: use editing-style prompts in queue/process JSDoc examples The examples referenced generation prompts but lucy-pro-v2v is a video editing model. Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/index.ts | 6 +++--- packages/sdk/src/queue/client.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 5fd7686..0899489 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -222,7 +222,7 @@ export const createDecartClient = (options: DecartClientOptions = {}) => { * const client = createDecartClient({ apiKey: "your-api-key" }); * const result = await client.process({ * model: models.image("lucy-pro-i2i"), - * prompt: "A beautiful sunset over the ocean", + * prompt: "Transform into anime style", * data: imageBlob * }); * ``` @@ -240,7 +240,7 @@ export const createDecartClient = (options: DecartClientOptions = {}) => { * // Option 1: Submit and poll automatically * const result = await client.queue.submitAndPoll({ * model: models.video("lucy-pro-v2v"), - * prompt: "A beautiful sunset over the ocean", + * prompt: "Transform into anime style", * data: videoBlob, * onStatusChange: (job) => console.log(`Job ${job.job_id}: ${job.status}`) * }); @@ -248,7 +248,7 @@ export const createDecartClient = (options: DecartClientOptions = {}) => { * // Option 2: Submit and poll manually * const job = await client.queue.submit({ * model: models.video("lucy-pro-v2v"), - * prompt: "A beautiful sunset over the ocean", + * prompt: "Transform into anime style", * data: videoBlob * }); * diff --git a/packages/sdk/src/queue/client.ts b/packages/sdk/src/queue/client.ts index 7d822d0..330675d 100644 --- a/packages/sdk/src/queue/client.ts +++ b/packages/sdk/src/queue/client.ts @@ -25,7 +25,7 @@ export type QueueClient = { * ```ts * const job = await client.queue.submit({ * model: models.video("lucy-pro-v2v"), - * prompt: "A cat playing piano", + * prompt: "Make it look like a watercolor painting", * data: videoBlob * }); * console.log(job.job_id); // "job_abc123" @@ -64,7 +64,7 @@ export type QueueClient = { * ```ts * const result = await client.queue.submitAndPoll({ * model: models.video("lucy-pro-v2v"), - * prompt: "A beautiful sunset", + * prompt: "Transform into anime style", * data: videoBlob, * onStatusChange: (job) => { * console.log(`Job ${job.job_id}: ${job.status}`); From 1745bd4203d4f5ac02acc8f64bde77cffe7e1f56 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Thu, 26 Mar 2026 15:50:58 +0200 Subject: [PATCH 4/5] fix: use editing-style prompts in all docs and examples Proxy READMEs were also missing the required data param for lucy-pro-i2i, and several docs/examples still had generation-style prompts. Co-Authored-By: Claude Opus 4.6 --- examples/sdk-core/README.md | 2 +- packages/proxy/src/express/README.md | 3 ++- packages/proxy/src/nextjs/README.md | 3 ++- packages/sdk/README.md | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/sdk-core/README.md b/examples/sdk-core/README.md index 2cb2d91..f5995a8 100644 --- a/examples/sdk-core/README.md +++ b/examples/sdk-core/README.md @@ -79,7 +79,7 @@ const blob = await client.process({ // Automatic polling (video-to-video) const result = await client.queue.submitAndPoll({ model: models.video("lucy-pro-v2v"), - prompt: "A cat playing piano", + prompt: "Make it look like a watercolor painting", data: videoBlob, onStatusChange: (job) => console.log(job.status), }); diff --git a/packages/proxy/src/express/README.md b/packages/proxy/src/express/README.md index 1fb32db..c720f98 100644 --- a/packages/proxy/src/express/README.md +++ b/packages/proxy/src/express/README.md @@ -26,6 +26,7 @@ const client = createDecartClient({ proxy: "/api/decart" }); // Use the client as normal const result = await client.process({ model: models.image("lucy-pro-i2i"), - prompt: "A beautiful sunset", + prompt: "Make it look like a watercolor painting", + data: imageBlob, }); ``` diff --git a/packages/proxy/src/nextjs/README.md b/packages/proxy/src/nextjs/README.md index 080c5b3..f6f2275 100644 --- a/packages/proxy/src/nextjs/README.md +++ b/packages/proxy/src/nextjs/README.md @@ -36,7 +36,8 @@ const client = createDecartClient({ proxy: PROXY_ROUTE }); // Use the client as normal const result = await client.process({ model: models.image("lucy-pro-i2i"), - prompt: "A beautiful sunset", + prompt: "Make it look like a watercolor painting", + data: imageBlob, }); ``` diff --git a/packages/sdk/README.md b/packages/sdk/README.md index edc29f7..465f836 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -76,7 +76,7 @@ const client = createDecartClient({ // Submit and poll automatically const result = await client.queue.submitAndPoll({ model: models.video("lucy-pro-v2v"), - prompt: "A cat playing piano", + prompt: "Make it look like a watercolor painting", data: videoFile, onStatusChange: (job) => { console.log(`Status: ${job.status}`); @@ -96,7 +96,7 @@ Or manage the polling manually: // Submit the job const job = await client.queue.submit({ model: models.video("lucy-pro-v2v"), - prompt: "A cat playing piano", + prompt: "Make it look like a watercolor painting", data: videoFile }); console.log(`Job ID: ${job.job_id}`); From 1dd25f200fdc6bc9f4f8e39cf83e44fff1fcdc10 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Thu, 26 Mar 2026 15:58:22 +0200 Subject: [PATCH 5/5] fix: remove duplicate /api/image/transform endpoint in express example The generate and transform endpoints became identical after the model removal. Consolidated into a single /api/image/edit endpoint. Co-Authored-By: Claude Opus 4.6 --- examples/express-api/README.md | 16 +++++----------- examples/express-api/src/server.ts | 28 ++-------------------------- 2 files changed, 7 insertions(+), 37 deletions(-) diff --git a/examples/express-api/README.md b/examples/express-api/README.md index f8fbf5a..ec6d788 100644 --- a/examples/express-api/README.md +++ b/examples/express-api/README.md @@ -32,15 +32,10 @@ These editing endpoints expect `imageDataUrl` / `videoDataUrl` fields containing ### Image Editing ```bash -curl -X POST http://localhost:3000/api/image/generate \ - -H "Content-Type: application/json" \ - -d '{"prompt": "A beautiful sunset over mountains", "imageDataUrl": "data:image/png;base64,"}' \ - --output image.png - -curl -X POST http://localhost:3000/api/image/transform \ +curl -X POST http://localhost:3000/api/image/edit \ -H "Content-Type: application/json" \ -d '{"prompt": "Oil painting style", "imageDataUrl": "data:image/png;base64,"}' \ - --output transformed.png + --output image.png ``` ### Video Editing @@ -48,7 +43,7 @@ curl -X POST http://localhost:3000/api/image/transform \ ```bash curl -X POST http://localhost:3000/api/video/generate \ -H "Content-Type: application/json" \ - -d '{"prompt": "A cat playing piano", "videoDataUrl": "data:video/mp4;base64,"}' + -d '{"prompt": "Make it look like a watercolor painting", "videoDataUrl": "data:video/mp4;base64,"}' # Returns: {"jobId": "abc123", "status": "pending"} curl http://localhost:3000/api/video/status/abc123 @@ -58,7 +53,7 @@ curl http://localhost:3000/api/video/result/abc123 --output video.mp4 curl -X POST http://localhost:3000/api/video/generate-sync \ -H "Content-Type: application/json" \ - -d '{"prompt": "A timelapse of clouds moving", "videoDataUrl": "data:video/mp4;base64,"}' \ + -d '{"prompt": "Transform into anime style", "videoDataUrl": "data:video/mp4;base64,"}' \ --output video.mp4 ``` @@ -66,8 +61,7 @@ curl -X POST http://localhost:3000/api/video/generate-sync \ | Endpoint | Method | Description | |----------|--------|-------------| -| `/api/image/generate` | POST | Edit image from base64 data URL + prompt | -| `/api/image/transform` | POST | Transform image with prompt | +| `/api/image/edit` | POST | Edit image from base64 data URL + prompt | | `/api/video/generate` | POST | Submit video editing job | | `/api/video/status/:id` | GET | Check video job status | | `/api/video/result/:id` | GET | Get completed video | diff --git a/examples/express-api/src/server.ts b/examples/express-api/src/server.ts index 0c389b3..2c5b1db 100644 --- a/examples/express-api/src/server.ts +++ b/examples/express-api/src/server.ts @@ -28,30 +28,7 @@ function parseBase64DataUrl(dataUrl: unknown, mediaType: "image" | "video"): Blo } // Edit image (sync - returns immediately) -app.post("/api/image/generate", async (req, res) => { - try { - const { prompt, imageDataUrl } = req.body; - - if (!prompt || !imageDataUrl) { - return res.status(400).json({ error: "prompt and imageDataUrl are required" }); - } - - const blob = await client.process({ - model: models.image("lucy-pro-i2i"), - prompt, - data: parseBase64DataUrl(imageDataUrl, "image"), - }); - - const buffer = Buffer.from(await blob.arrayBuffer()); - res.setHeader("Content-Type", "image/png"); - res.send(buffer); - } catch (error) { - res.status(500).json({ error: String(error) }); - } -}); - -// Transform image (sync - returns immediately) -app.post("/api/image/transform", async (req, res) => { +app.post("/api/image/edit", async (req, res) => { try { const { prompt, imageDataUrl } = req.body; @@ -148,8 +125,7 @@ app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); console.log(""); console.log("Available endpoints:"); - console.log(" POST /api/image/generate - Edit image from base64 data URL + prompt"); - console.log(" POST /api/image/transform - Transform image"); + console.log(" POST /api/image/edit - Edit image from base64 data URL + prompt"); console.log(" POST /api/video/generate - Submit video edit job from base64 data URL + prompt"); console.log(" GET /api/video/status/:id - Check job status"); console.log(" GET /api/video/result/:id - Get video result");