From 8803c6fc4efee72f02d6538ab2a112a7a31a9fbe Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 14 Apr 2026 08:26:10 +0300 Subject: [PATCH 1/3] feat: add lucy-2.1-vton batch (queue) API support --- decart/models.py | 18 ++++++ examples/video_tryon.py | 124 ++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 15 +++++ uv.lock | 2 +- 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 examples/video_tryon.py diff --git a/decart/models.py b/decart/models.py index 0a7f8a6..f8a6563 100644 --- a/decart/models.py +++ b/decart/models.py @@ -30,10 +30,12 @@ "lucy-clip", "lucy-2", "lucy-2.1", + "lucy-2.1-vton", "lucy-restyle-2", "lucy-motion", # Latest aliases (server-side resolution) "lucy-latest", + "lucy-vton-latest", "lucy-restyle-latest", "lucy-clip-latest", "lucy-motion-latest", @@ -340,6 +342,14 @@ class ImageToImageInput(DecartBaseModel): height=624, input_schema=VideoEdit2Input, ), + "lucy-2.1-vton": ModelDefinition( + name="lucy-2.1-vton", + url_path="/v1/jobs/lucy-2.1-vton", + fps=20, + width=1088, + height=624, + input_schema=VideoEdit2Input, + ), "lucy-restyle-2": ModelDefinition( name="lucy-restyle-2", url_path="/v1/jobs/lucy-restyle-2", @@ -365,6 +375,14 @@ class ImageToImageInput(DecartBaseModel): height=624, input_schema=VideoEdit2Input, ), + "lucy-vton-latest": ModelDefinition( + name="lucy-vton-latest", + url_path="/v1/jobs/lucy-vton-latest", + fps=20, + width=1088, + height=624, + input_schema=VideoEdit2Input, + ), "lucy-restyle-latest": ModelDefinition( name="lucy-restyle-latest", url_path="/v1/jobs/lucy-restyle-latest", diff --git a/examples/video_tryon.py b/examples/video_tryon.py new file mode 100644 index 0000000..30dd728 --- /dev/null +++ b/examples/video_tryon.py @@ -0,0 +1,124 @@ +""" +Virtual Try-On Example + +This example demonstrates how to use the lucy-2.1-vton model to perform +virtual try-on on a video using a reference garment image. + +Usage: + # With reference image and prompt: + DECART_API_KEY=your-key python video_tryon.py input.mp4 --reference garment.png --prompt "wear this outfit" + + # With reference image only (empty prompt): + DECART_API_KEY=your-key python video_tryon.py input.mp4 --reference garment.png + +Requirements: + pip install decart +""" + +import asyncio +import argparse +import os +import sys +from pathlib import Path + +from decart import DecartClient, models + + +async def main(): + parser = argparse.ArgumentParser( + description="Virtual try-on: apply a garment from a reference image onto a person in a video" + ) + parser.add_argument("video", help="Path to input video file") + parser.add_argument( + "--reference", "-r", required=True, help="Path to reference garment image" + ) + parser.add_argument( + "--prompt", "-p", default="", help="Text prompt (default: empty string)" + ) + parser.add_argument("--output", "-o", help="Output file path (default: output_tryon.mp4)") + parser.add_argument("--seed", "-s", type=int, help="Random seed for reproducibility") + parser.add_argument( + "--enhance", + action="store_true", + default=True, + help="Enhance the prompt (default: True)", + ) + parser.add_argument("--no-enhance", action="store_true", help="Disable prompt enhancement") + + args = parser.parse_args() + + api_key = os.getenv("DECART_API_KEY") + if not api_key: + print("Error: DECART_API_KEY environment variable not set") + sys.exit(1) + + video_path = Path(args.video) + if not video_path.exists(): + print(f"Error: Video file not found: {video_path}") + sys.exit(1) + + ref_path = Path(args.reference) + if not ref_path.exists(): + print(f"Error: Reference image not found: {ref_path}") + sys.exit(1) + + output_path = args.output or f"output_tryon_{video_path.stem}.mp4" + + print("=" * 50) + print("Virtual Try-On") + print("=" * 50) + print(f"Input video: {video_path}") + print(f"Reference image: {ref_path}") + if args.prompt: + print(f"Prompt: '{args.prompt}'") + print(f"Enhance prompt: {not args.no_enhance}") + print(f"Output: {output_path}") + if args.seed: + print(f"Seed: {args.seed}") + print("=" * 50) + + async with DecartClient(api_key=api_key) as client: + options = { + "model": models.video("lucy-2.1-vton"), + "data": video_path, + "prompt": args.prompt, + "reference_image": ref_path, + } + + if args.prompt: + options["enhance_prompt"] = not args.no_enhance + + if args.seed: + options["seed"] = args.seed + + def on_status_change(job): + status_emoji = { + "pending": "ā³", + "processing": "šŸ”„", + "completed": "āœ…", + "failed": "āŒ", + } + emoji = status_emoji.get(job.status, "•") + print(f"{emoji} Status: {job.status}") + + options["on_status_change"] = on_status_change + + print("\nSubmitting job...") + result = await client.queue.submit_and_poll(options) + + if result.status == "failed": + print(f"\nāŒ Job failed: {result.error}") + sys.exit(1) + + print("\nāœ… Job completed!") + print(f"šŸ’¾ Saving to {output_path}...") + + with open(output_path, "wb") as f: + f.write(result.data) + + print(f"āœ“ Video saved to {output_path}") + print(f" Size: {len(result.data) / 1024 / 1024:.2f} MB") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/test_models.py b/tests/test_models.py index 7a845f7..0d3ec66 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -98,6 +98,13 @@ def test_canonical_video_models() -> None: assert model.width == 1088 assert model.height == 624 + model = models.video("lucy-2.1-vton") + assert model.name == "lucy-2.1-vton" + assert model.url_path == "/v1/jobs/lucy-2.1-vton" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + model = models.video("lucy-restyle-2") assert model.name == "lucy-restyle-2" assert model.url_path == "/v1/jobs/lucy-restyle-2" @@ -195,6 +202,13 @@ def test_latest_video_models() -> None: assert model.width == 1088 assert model.height == 624 + model = models.video("lucy-vton-latest") + assert model.name == "lucy-vton-latest" + assert model.url_path == "/v1/jobs/lucy-vton-latest" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + model = models.video("lucy-restyle-latest") assert model.name == "lucy-restyle-latest" assert model.url_path == "/v1/jobs/lucy-restyle-latest" @@ -226,6 +240,7 @@ def test_latest_aliases_no_deprecation_warning() -> None: models.realtime("lucy-vton-latest") models.realtime("lucy-restyle-latest") models.video("lucy-latest") + models.video("lucy-vton-latest") models.video("lucy-restyle-latest") models.video("lucy-clip-latest") models.video("lucy-motion-latest") diff --git a/uv.lock b/uv.lock index a6b90af..ceff5ff 100644 --- a/uv.lock +++ b/uv.lock @@ -597,7 +597,7 @@ wheels = [ [[package]] name = "decart" -version = "0.0.29" +version = "0.0.33" source = { editable = "." } dependencies = [ { name = "aiofiles" }, From 77bed011a8ea5954b9aa011c75a161d1fdda57a0 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 14 Apr 2026 08:27:08 +0300 Subject: [PATCH 2/3] style: format video_tryon.py with black --- examples/video_tryon.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/video_tryon.py b/examples/video_tryon.py index 30dd728..31cee30 100644 --- a/examples/video_tryon.py +++ b/examples/video_tryon.py @@ -29,12 +29,8 @@ async def main(): description="Virtual try-on: apply a garment from a reference image onto a person in a video" ) parser.add_argument("video", help="Path to input video file") - parser.add_argument( - "--reference", "-r", required=True, help="Path to reference garment image" - ) - parser.add_argument( - "--prompt", "-p", default="", help="Text prompt (default: empty string)" - ) + parser.add_argument("--reference", "-r", required=True, help="Path to reference garment image") + parser.add_argument("--prompt", "-p", default="", help="Text prompt (default: empty string)") parser.add_argument("--output", "-o", help="Output file path (default: output_tryon.mp4)") parser.add_argument("--seed", "-s", type=int, help="Random seed for reproducibility") parser.add_argument( From e436436853abe07e9085b6fe778df6aeaf6922fc Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 14 Apr 2026 08:33:49 +0300 Subject: [PATCH 3/3] fix: handle seed=0 and remove unused --enhance flag in video_tryon example --- examples/video_tryon.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/examples/video_tryon.py b/examples/video_tryon.py index 31cee30..a571d27 100644 --- a/examples/video_tryon.py +++ b/examples/video_tryon.py @@ -33,12 +33,6 @@ async def main(): parser.add_argument("--prompt", "-p", default="", help="Text prompt (default: empty string)") parser.add_argument("--output", "-o", help="Output file path (default: output_tryon.mp4)") parser.add_argument("--seed", "-s", type=int, help="Random seed for reproducibility") - parser.add_argument( - "--enhance", - action="store_true", - default=True, - help="Enhance the prompt (default: True)", - ) parser.add_argument("--no-enhance", action="store_true", help="Disable prompt enhancement") args = parser.parse_args() @@ -69,7 +63,7 @@ async def main(): print(f"Prompt: '{args.prompt}'") print(f"Enhance prompt: {not args.no_enhance}") print(f"Output: {output_path}") - if args.seed: + if args.seed is not None: print(f"Seed: {args.seed}") print("=" * 50) @@ -84,7 +78,7 @@ async def main(): if args.prompt: options["enhance_prompt"] = not args.no_enhance - if args.seed: + if args.seed is not None: options["seed"] = args.seed def on_status_change(job):