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..a571d27 --- /dev/null +++ b/examples/video_tryon.py @@ -0,0 +1,114 @@ +""" +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("--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 is not None: + 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 is not None: + 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" },