From a651208db247267ed7b3f59b739327758d3aee46 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 7 Apr 2026 00:38:37 +0300 Subject: [PATCH 1/3] feat: canonical model names, lucy-2.1, and deprecation warnings Add canonical model names (lucy-2, lucy-clip, lucy-restyle-2, lucy-image-2, live-avatar, etc.) to match the updated API naming convention. Add new models lucy-2.1 (realtime + batch) and lucy-2.1-vton (realtime). Deprecated model names still work but now emit a DeprecationWarning guiding users to migrate. --- README.md | 6 +- decart/client.py | 6 +- decart/models.py | 191 +++++++++++++++++++++++++-- decart/queue/client.py | 4 +- decart/realtime/client.py | 2 +- decart/realtime/webrtc_connection.py | 2 +- decart/tokens/client.py | 4 +- examples/README.md | 4 +- examples/avatar_live.py | 2 +- examples/process_image.py | 2 +- examples/process_url.py | 2 +- examples/process_video.py | 2 +- examples/queue_example.py | 4 +- examples/realtime_file.py | 2 +- examples/realtime_synthetic.py | 2 +- examples/video_restyle.py | 4 +- playground/playground.py | 34 +++-- test_ui.py | 6 +- tests/test_models.py | 164 +++++++++++++++++++---- tests/test_process.py | 18 +-- tests/test_queue.py | 48 +++---- tests/test_realtime_unit.py | 66 ++++----- tests/test_tokens.py | 12 +- 23 files changed, 435 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index d6c6d2f..c41ac45 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ async def main(): async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: # Edit an image result = await client.process({ - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a painterly oil-on-canvas look while preserving the composition", "data": open("input.png", "rb"), }) @@ -53,7 +53,7 @@ For video editing jobs, use the queue API to submit jobs and poll for results: async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: # Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this footage with anime shading and vibrant neon highlights", "data": open("input.mp4", "rb"), "on_status_change": lambda job: print(f"Status: {job.status}"), @@ -72,7 +72,7 @@ Or manage the polling manually: async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: # Submit the job job = await client.queue.submit({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add cinematic teal-and-orange grading and gentle film grain", "data": open("input.mp4", "rb"), }) diff --git a/decart/client.py b/decart/client.py index 9f02b95..6280ebe 100644 --- a/decart/client.py +++ b/decart/client.py @@ -37,14 +37,14 @@ class DecartClient: # Image editing (sync) - use process() image = await client.process({ - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a painterly oil-on-canvas look while preserving the composition", "data": open("input.png", "rb"), }) # Video editing (async) - use queue result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this footage with anime shading and vibrant neon highlights", "data": open("input.mp4", "rb"), }) @@ -84,7 +84,7 @@ def queue(self) -> QueueClient: ```python # Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this footage with anime shading and vibrant neon highlights", "data": open("input.mp4", "rb"), }) diff --git a/decart/models.py b/decart/models.py index 7a3cbe7..56a917b 100644 --- a/decart/models.py +++ b/decart/models.py @@ -1,19 +1,75 @@ +import warnings from typing import Literal, Optional, List, Generic, TypeVar from pydantic import BaseModel, Field, ConfigDict, model_validator from .errors import ModelNotFoundError from .types import FileInput, MotionTrajectoryInput -RealTimeModels = Literal["mirage", "mirage_v2", "lucy_v2v_720p_rt", "lucy_2_rt", "live_avatar"] +RealTimeModels = Literal[ + # Canonical names + "lucy", + "lucy-2", + "lucy-2.1", + "lucy-2.1-vton", + "lucy-restyle", + "lucy-restyle-2", + "live-avatar", + # Deprecated names + "mirage", + "mirage_v2", + "lucy_v2v_720p_rt", + "lucy_2_rt", + "live_avatar", +] VideoModels = Literal[ - "lucy-pro-v2v", + # Canonical names + "lucy-clip", + "lucy-2", + "lucy-2.1", + "lucy-restyle-2", "lucy-motion", + # Deprecated names + "lucy-pro-v2v", "lucy-restyle-v2v", "lucy-2-v2v", ] -ImageModels = Literal["lucy-pro-i2i"] +ImageModels = Literal[ + # Canonical names + "lucy-image-2", + # Deprecated names + "lucy-pro-i2i", +] Model = Literal[RealTimeModels, VideoModels, ImageModels] +MODEL_ALIASES: dict[str, str] = { + # Realtime aliases + "mirage": "lucy-restyle", + "mirage_v2": "lucy-restyle-2", + "lucy_v2v_720p_rt": "lucy", + "lucy_2_rt": "lucy-2", + "live_avatar": "live-avatar", + # Video aliases + "lucy-pro-v2v": "lucy-clip", + "lucy-restyle-v2v": "lucy-restyle-2", + "lucy-2-v2v": "lucy-2", + # Image aliases + "lucy-pro-i2i": "lucy-image-2", +} + +_warned_aliases: set[str] = set() + + +def _warn_deprecated(model: str) -> None: + canonical = MODEL_ALIASES.get(model) + if canonical and model not in _warned_aliases: + _warned_aliases.add(model) + warnings.warn( + f'Model "{model}" is deprecated. Use "{canonical}" instead. ' + f"See https://docs.platform.decart.ai/models for details.", + DeprecationWarning, + stacklevel=3, + ) + # Type variable for model name ModelT = TypeVar("ModelT", bound=str) @@ -49,6 +105,7 @@ class VideoToVideoInput(DecartBaseModel): max_length=1000, ) data: FileInput + reference_image: Optional[FileInput] = None seed: Optional[int] = None resolution: Optional[str] = None enhance_prompt: Optional[bool] = None @@ -121,6 +178,64 @@ class ImageToImageInput(DecartBaseModel): _MODELS = { "realtime": { + # Canonical names + "lucy": ModelDefinition( + name="lucy", + url_path="/v1/stream", + fps=25, + width=1280, + height=704, + input_schema=BaseModel, + ), + "lucy-2": ModelDefinition( + name="lucy-2", + url_path="/v1/stream", + fps=20, + width=1280, + height=720, + input_schema=BaseModel, + ), + "lucy-2.1": ModelDefinition( + name="lucy-2.1", + url_path="/v1/stream", + fps=20, + width=1088, + height=624, + input_schema=BaseModel, + ), + "lucy-2.1-vton": ModelDefinition( + name="lucy-2.1-vton", + url_path="/v1/stream", + fps=20, + width=1088, + height=624, + input_schema=BaseModel, + ), + "lucy-restyle": ModelDefinition( + name="lucy-restyle", + url_path="/v1/stream", + fps=25, + width=1280, + height=704, + input_schema=BaseModel, + ), + "lucy-restyle-2": ModelDefinition( + name="lucy-restyle-2", + url_path="/v1/stream", + fps=22, + width=1280, + height=704, + input_schema=BaseModel, + ), + "live-avatar": ModelDefinition( + name="live-avatar", + url_path="/v1/stream", + fps=25, + width=1280, + height=720, + input_schema=BaseModel, + ), + # Deprecated names "mirage": ModelDefinition( name="mirage", url_path="/v1/stream", @@ -163,33 +278,67 @@ class ImageToImageInput(DecartBaseModel): ), }, "video": { - "lucy-pro-v2v": ModelDefinition( - name="lucy-pro-v2v", - url_path="/v1/generate/lucy-pro-v2v", + # Canonical names + "lucy-clip": ModelDefinition( + name="lucy-clip", + url_path="/v1/jobs/lucy-clip", fps=25, width=1280, height=704, input_schema=VideoToVideoInput, ), + "lucy-2": ModelDefinition( + name="lucy-2", + url_path="/v1/jobs/lucy-2", + fps=20, + width=1280, + height=720, + input_schema=VideoEdit2Input, + ), + "lucy-2.1": ModelDefinition( + name="lucy-2.1", + url_path="/v1/jobs/lucy-2.1", + fps=20, + width=1088, + height=624, + input_schema=VideoEdit2Input, + ), + "lucy-restyle-2": ModelDefinition( + name="lucy-restyle-2", + url_path="/v1/jobs/lucy-restyle-2", + fps=22, + width=1280, + height=704, + input_schema=VideoRestyleInput, + ), "lucy-motion": ModelDefinition( name="lucy-motion", - url_path="/v1/generate/lucy-motion", + url_path="/v1/jobs/lucy-motion", fps=25, width=1280, height=704, input_schema=ImageToMotionVideoInput, ), + # Deprecated names + "lucy-pro-v2v": ModelDefinition( + name="lucy-pro-v2v", + url_path="/v1/jobs/lucy-pro-v2v", + fps=25, + width=1280, + height=704, + input_schema=VideoToVideoInput, + ), "lucy-restyle-v2v": ModelDefinition( name="lucy-restyle-v2v", - url_path="/v1/generate/lucy-restyle-v2v", - fps=25, + url_path="/v1/jobs/lucy-restyle-v2v", + fps=22, width=1280, height=704, input_schema=VideoRestyleInput, ), "lucy-2-v2v": ModelDefinition( name="lucy-2-v2v", - url_path="/v1/generate/lucy-2-v2v", + url_path="/v1/jobs/lucy-2-v2v", fps=20, width=1280, height=720, @@ -197,6 +346,16 @@ class ImageToImageInput(DecartBaseModel): ), }, "image": { + # Canonical names + "lucy-image-2": ModelDefinition( + name="lucy-image-2", + url_path="/v1/generate/lucy-image-2", + fps=25, + width=1280, + height=704, + input_schema=ImageToImageInput, + ), + # Deprecated names "lucy-pro-i2i": ModelDefinition( name="lucy-pro-i2i", url_path="/v1/generate/lucy-pro-i2i", @@ -213,6 +372,7 @@ class Models: @staticmethod def realtime(model: RealTimeModels) -> RealTimeModelDefinition: """Get a realtime model definition for WebRTC streaming.""" + _warn_deprecated(model) try: return _MODELS["realtime"][model] # type: ignore[return-value] except KeyError: @@ -225,11 +385,13 @@ def video(model: VideoModels) -> VideoModelDefinition: Video models only support the queue API. Available models: - - "lucy-pro-v2v" - Video-to-video + - "lucy-clip" - Video-to-video + - "lucy-2" - Video editing with reference image support + - "lucy-2.1" - Video editing (newer, higher quality) + - "lucy-restyle-2" - Video restyling with prompt or reference image - "lucy-motion" - Image-to-motion-video - - "lucy-restyle-v2v" - Video-to-video with prompt or reference image - - "lucy-2-v2v" - Video-to-video editing (long-form, 720p) """ + _warn_deprecated(model) try: return _MODELS["video"][model] # type: ignore[return-value] except KeyError: @@ -242,8 +404,9 @@ def image(model: ImageModels) -> ImageModelDefinition: Image models only support the process (sync) API. Available models: - - "lucy-pro-i2i" - Image-to-image + - "lucy-image-2" - Image-to-image """ + _warn_deprecated(model) try: return _MODELS["image"][model] # type: ignore[return-value] except KeyError: diff --git a/decart/queue/client.py b/decart/queue/client.py index 43fb1c5..dfb79db 100644 --- a/decart/queue/client.py +++ b/decart/queue/client.py @@ -37,7 +37,7 @@ class QueueClient: # Option 1: Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this clip with anime shading and saturated colors", "data": open("input.mp4", "rb"), "on_status_change": lambda job: print(f"Status: {job.status}"), @@ -45,7 +45,7 @@ class QueueClient: # Option 2: Submit and poll manually job = await client.queue.submit({ - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add cinematic teal-and-orange grading and subtle film grain", "data": open("input.mp4", "rb"), }) diff --git a/decart/realtime/client.py b/decart/realtime/client.py index 3250c69..93f1394 100644 --- a/decart/realtime/client.py +++ b/decart/realtime/client.py @@ -113,7 +113,7 @@ async def connect( model_name: RealTimeModels = options.model.name # type: ignore[assignment] - is_avatar_live = model_name == "live_avatar" + is_avatar_live = model_name in ("live_avatar", "live-avatar") audio_stream_manager: Optional[AudioStreamManager] = None if is_avatar_live and local_track is None: diff --git a/decart/realtime/webrtc_connection.py b/decart/realtime/webrtc_connection.py index ecbf98d..ef5e0c5 100644 --- a/decart/realtime/webrtc_connection.py +++ b/decart/realtime/webrtc_connection.py @@ -246,7 +246,7 @@ async def on_ice_connection_state_change(): self._pc.addTransceiver("audio", direction="recvonly") logger.debug("Added video+audio transceivers (recvonly) for subscribe mode") else: - if model_name == "live_avatar": + if model_name in ("live_avatar", "live-avatar"): self._pc.addTransceiver("video", direction="recvonly") logger.debug("Added video transceiver (recvonly) for avatar-live mode") self._pc.addTrack(local_track) diff --git a/decart/tokens/client.py b/decart/tokens/client.py index aa36a80..9528c25 100644 --- a/decart/tokens/client.py +++ b/decart/tokens/client.py @@ -28,7 +28,7 @@ class TokensClient: # With expiry, model restrictions, and constraints: token = await client.tokens.create( expires_in=120, - allowed_models=["lucy_2_rt"], + allowed_models=["lucy-2"], constraints={"realtime": {"maxSessionDuration": 300}}, ) ``` @@ -70,7 +70,7 @@ async def create( token = await client.tokens.create( metadata={"role": "viewer"}, expires_in=120, - allowed_models=["lucy_2_rt"], + allowed_models=["lucy-2"], constraints={"realtime": {"maxSessionDuration": 300}}, ) ``` diff --git a/examples/README.md b/examples/README.md index efd8589..e5ab50b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,8 +20,8 @@ export DECART_API_KEY="your-api-key-here" ### Process API -- **`process_video.py`** - Edit a local video with `lucy-pro-v2v` -- **`process_image.py`** - Edit the bundled example image with `lucy-pro-i2i` +- **`process_video.py`** - Edit a local video with `lucy-clip` +- **`process_image.py`** - Edit the bundled example image with `lucy-image-2` - **`process_url.py`** - Transform videos from URLs - **`queue_image_example.py`** - Turn the bundled example image into motion with `lucy-motion` diff --git a/examples/avatar_live.py b/examples/avatar_live.py index 55c3dd2..03180fd 100644 --- a/examples/avatar_live.py +++ b/examples/avatar_live.py @@ -85,7 +85,7 @@ async def main(): print("\nCreating Decart client...") async with DecartClient(api_key=api_key) as client: - model = models.realtime("live_avatar") + model = models.realtime("live-avatar") print(f"Using model: {model.name}") frame_count = 0 diff --git a/examples/process_image.py b/examples/process_image.py index 23b734d..9296f22 100644 --- a/examples/process_image.py +++ b/examples/process_image.py @@ -16,7 +16,7 @@ async def main() -> None: print("Editing image...") result = await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply an impressionist oil-painting treatment while keeping the framing intact", "data": image_path, "enhance_prompt": True, diff --git a/examples/process_url.py b/examples/process_url.py index a93b4e2..bceece4 100644 --- a/examples/process_url.py +++ b/examples/process_url.py @@ -13,7 +13,7 @@ async def main() -> None: print("Transforming video from URL...") result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Watercolor painting style", "data": "https://docs.platform.decart.ai/assets/example-video.mp4", "on_status_change": lambda job: print(f" Status: {job.status}"), diff --git a/examples/process_video.py b/examples/process_video.py index 7113ec2..52ebcc3 100644 --- a/examples/process_video.py +++ b/examples/process_video.py @@ -21,7 +21,7 @@ async def main() -> None: print("Editing video...") result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this footage with anime shading, vibrant highlights, and crisp outlines", "data": video_path, "enhance_prompt": True, diff --git a/examples/queue_example.py b/examples/queue_example.py index 241fd46..e15eb6e 100644 --- a/examples/queue_example.py +++ b/examples/queue_example.py @@ -16,7 +16,7 @@ async def main() -> None: print("Submitting job with automatic polling...") result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Give this clip a cinematic dusk grade with cooler shadows and warm highlights", "data": video_path, "resolution": "480p", @@ -35,7 +35,7 @@ async def main() -> None: print("\nSubmitting job with manual polling...") job = await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle the scene to feel like stop-motion miniatures with soft practical lighting", "data": video_path, "resolution": "480p", diff --git a/examples/realtime_file.py b/examples/realtime_file.py index 9544613..a09a073 100644 --- a/examples/realtime_file.py +++ b/examples/realtime_file.py @@ -46,7 +46,7 @@ async def main(): print("Creating Decart client...") async with DecartClient(api_key=api_key) as client: - model = models.realtime("mirage_v2") + model = models.realtime("lucy-restyle-2") print(f"Using model: {model.name}") frame_count = 0 diff --git a/examples/realtime_synthetic.py b/examples/realtime_synthetic.py index de93686..ec93574 100644 --- a/examples/realtime_synthetic.py +++ b/examples/realtime_synthetic.py @@ -73,7 +73,7 @@ async def main(): print("Creating synthetic video track...") video_track = SyntheticVideoTrack() - model = models.realtime("lucy_2_rt") + model = models.realtime("lucy-2") print(f"Using model: {model.name}") print(f"Model config - FPS: {model.fps}, Size: {model.width}x{model.height}") diff --git a/examples/video_restyle.py b/examples/video_restyle.py index ab6edb6..2fd4c08 100644 --- a/examples/video_restyle.py +++ b/examples/video_restyle.py @@ -1,7 +1,7 @@ """ Video Restyle Example -This example demonstrates how to use the lucy-restyle-v2v model to restyle a video +This example demonstrates how to use the lucy-restyle-2 model to restyle a video using either a text prompt OR a reference image. Usage: @@ -93,7 +93,7 @@ async def main(): async with DecartClient(api_key=api_key) as client: # Build options options = { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "data": video_path, } diff --git a/playground/playground.py b/playground/playground.py index 9d0bc84..1211d67 100644 --- a/playground/playground.py +++ b/playground/playground.py @@ -13,10 +13,10 @@ Usage: python playground.py # Interactive mode - python playground.py --model mirage_v2 # Camera model - python playground.py --model mirage_v2 --prompt "Anime" # With initial prompt - python playground.py --model avatar-live --image face.png # Avatar mode - python playground.py --model avatar-live --image face.png --audio speech.mp3 + python playground.py --model lucy-restyle-2 # Camera model + python playground.py --model lucy-restyle-2 --prompt "Anime" # With initial prompt + python playground.py --model live-avatar --image face.png # Avatar mode + python playground.py --model live-avatar --image face.png --audio speech.mp3 Controls (while running): Type text + Enter → Send prompt to Decart @@ -91,9 +91,17 @@ def _check_deps() -> None: # ── Constants ──────────────────────────────────────────────────────────────── -REALTIME_MODELS = ["mirage", "mirage_v2", "lucy_v2v_720p_rt", "lucy_2_rt", "live_avatar"] -CAMERA_MODELS = {"mirage", "mirage_v2", "lucy_v2v_720p_rt", "lucy_2_rt"} -AVATAR_MODELS = {"live_avatar"} +REALTIME_MODELS = [ + "lucy", + "lucy-2", + "lucy-2.1", + "lucy-2.1-vton", + "lucy-restyle", + "lucy-restyle-2", + "live-avatar", +] +CAMERA_MODELS = {"lucy", "lucy-2", "lucy-2.1", "lucy-2.1-vton", "lucy-restyle", "lucy-restyle-2"} +AVATAR_MODELS = {"live-avatar"} BANNER = """ ╔══════════════════════════════════════╗ @@ -176,11 +184,11 @@ def parse_args() -> argparse.Namespace: formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ Examples: - %(prog)s --model mirage_v2 - %(prog)s --model mirage_v2 --prompt "Anime style" - %(prog)s --model avatar-live --image avatar.png - %(prog)s --model avatar-live --image avatar.png --audio speech.mp3 - %(prog)s --model lucy_2_rt --image ref.png --prompt "Lego World" + %(prog)s --model lucy-restyle-2 + %(prog)s --model lucy-restyle-2 --prompt "Anime style" + %(prog)s --model live-avatar --image avatar.png + %(prog)s --model live-avatar --image avatar.png --audio speech.mp3 + %(prog)s --model lucy-2 --image ref.png --prompt "Lego World" """, ) p.add_argument("--model", "-m", choices=REALTIME_MODELS, help="Model name") @@ -201,7 +209,7 @@ def select_model_interactive() -> str: note = "" if name in AVATAR_MODELS: note = " (requires --image)" - elif name in ("lucy_2_rt", "mirage_v2"): + elif name in ("lucy-2", "lucy-2.1", "lucy-2.1-vton", "lucy-restyle-2"): note = " (supports reference image)" print(f" {i}. {name}{note}") diff --git a/test_ui.py b/test_ui.py index 19aec05..4cf8bf7 100644 --- a/test_ui.py +++ b/test_ui.py @@ -46,7 +46,7 @@ async def process_image_to_image( client = get_client(api_key) options = { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": prompt, "data": Path(input_image), } @@ -82,7 +82,7 @@ async def process_video_v2v( client = get_client(api_key) options = { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": prompt, "data": Path(input_video), } @@ -134,7 +134,7 @@ async def process_video_restyle( client = get_client(api_key) options = { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "data": Path(input_video), } diff --git a/tests/test_models.py b/tests/test_models.py index 9e92c9b..95868db 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,56 +1,168 @@ +import warnings import pytest from decart import models, DecartSDKError +from decart.models import _warned_aliases -def test_realtime_models() -> None: - model = models.realtime("mirage") - assert model.name == "mirage" +def test_canonical_realtime_models() -> None: + model = models.realtime("lucy-restyle") + assert model.name == "lucy-restyle" assert model.fps == 25 assert model.width == 1280 assert model.height == 704 assert model.url_path == "/v1/stream" - model = models.realtime("mirage_v2") - assert model.name == "mirage_v2" + model = models.realtime("lucy-restyle-2") + assert model.name == "lucy-restyle-2" assert model.fps == 22 assert model.width == 1280 assert model.height == 704 - assert model.url_path == "/v1/stream" - # avatar-live model - model = models.realtime("live_avatar") - assert model.name == "live_avatar" + model = models.realtime("lucy") + assert model.name == "lucy" + assert model.fps == 25 + assert model.width == 1280 + assert model.height == 704 + + model = models.realtime("lucy-2") + assert model.name == "lucy-2" + assert model.fps == 20 + assert model.width == 1280 + assert model.height == 720 + + model = models.realtime("lucy-2.1") + assert model.name == "lucy-2.1" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + + model = models.realtime("lucy-2.1-vton") + assert model.name == "lucy-2.1-vton" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + + model = models.realtime("live-avatar") + assert model.name == "live-avatar" assert model.fps == 25 assert model.width == 1280 assert model.height == 720 - assert model.url_path == "/v1/stream" -def test_video_models() -> None: - model = models.video("lucy-pro-v2v") - assert model.name == "lucy-pro-v2v" - assert model.url_path == "/v1/generate/lucy-pro-v2v" +def test_deprecated_realtime_models() -> None: + _warned_aliases.clear() - # lucy-restyle-v2v model - model = models.video("lucy-restyle-v2v") - assert model.name == "lucy-restyle-v2v" - assert model.url_path == "/v1/generate/lucy-restyle-v2v" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.realtime("mirage") + assert model.name == "mirage" + assert len(w) == 1 + assert "deprecated" in str(w[0].message).lower() + assert "lucy-restyle" in str(w[0].message) + _warned_aliases.clear() -def test_image_models() -> None: - model = models.image("lucy-pro-i2i") - assert model.name == "lucy-pro-i2i" - assert model.url_path == "/v1/generate/lucy-pro-i2i" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.realtime("mirage_v2") + assert model.name == "mirage_v2" + assert len(w) == 1 + assert "lucy-restyle-2" in str(w[0].message) + _warned_aliases.clear() -def test_lucy_2_v2v_model() -> None: - model = models.video("lucy-2-v2v") - assert model.name == "lucy-2-v2v" - assert model.url_path == "/v1/generate/lucy-2-v2v" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.realtime("live_avatar") + assert model.name == "live_avatar" + assert len(w) == 1 + assert "live-avatar" in str(w[0].message) + + +def test_canonical_video_models() -> None: + model = models.video("lucy-clip") + assert model.name == "lucy-clip" + assert model.url_path == "/v1/jobs/lucy-clip" + + model = models.video("lucy-2") + assert model.name == "lucy-2" + assert model.url_path == "/v1/jobs/lucy-2" assert model.fps == 20 assert model.width == 1280 assert model.height == 720 + model = models.video("lucy-2.1") + assert model.name == "lucy-2.1" + assert model.url_path == "/v1/jobs/lucy-2.1" + 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" + + model = models.video("lucy-motion") + assert model.name == "lucy-motion" + assert model.url_path == "/v1/jobs/lucy-motion" + + +def test_deprecated_video_models() -> None: + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.video("lucy-pro-v2v") + assert model.name == "lucy-pro-v2v" + assert len(w) == 1 + assert "lucy-clip" in str(w[0].message) + + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.video("lucy-restyle-v2v") + assert model.name == "lucy-restyle-v2v" + assert len(w) == 1 + assert "lucy-restyle-2" in str(w[0].message) + + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.video("lucy-2-v2v") + assert model.name == "lucy-2-v2v" + assert len(w) == 1 + assert '"lucy-2"' in str(w[0].message) + + +def test_canonical_image_models() -> None: + model = models.image("lucy-image-2") + assert model.name == "lucy-image-2" + assert model.url_path == "/v1/generate/lucy-image-2" + + +def test_deprecated_image_models() -> None: + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = models.image("lucy-pro-i2i") + assert model.name == "lucy-pro-i2i" + assert len(w) == 1 + assert "lucy-image-2" in str(w[0].message) + + +def test_deprecation_warning_only_once() -> None: + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + models.realtime("mirage") + models.realtime("mirage") + models.realtime("mirage") + assert len(w) == 1 + def test_invalid_model() -> None: with pytest.raises(DecartSDKError): diff --git a/tests/test_process.py b/tests/test_process.py index 007c5b7..5c47a57 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -31,7 +31,7 @@ async def test_process_image_to_image() -> None: result = await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply an oil-painting treatment while preserving the composition", "data": b"fake input image", "enhance_prompt": True, @@ -62,7 +62,7 @@ async def test_process_image_to_image_with_reference_image() -> None: result = await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Add the object from the reference image", "data": b"fake input image", "reference_image": b"fake reference image", @@ -81,7 +81,7 @@ async def test_process_rejects_video_models() -> None: with pytest.raises(DecartSDKError) as exc_info: await client.process( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add cinematic teal-and-orange grading", } ) @@ -110,7 +110,7 @@ async def test_process_missing_required_field() -> None: with pytest.raises(DecartSDKError): await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), # Missing 'data' field which is required for i2i } ) @@ -123,12 +123,12 @@ async def test_process_max_prompt_length() -> None: with pytest.raises(DecartSDKError) as exception: await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": prompt, "data": b"fake image data", } ) - assert "Invalid inputs for lucy-pro-i2i: 1 validation error for ImageToImageInput" in str( + assert "Invalid inputs for lucy-image-2: 1 validation error for ImageToImageInput" in str( exception ) @@ -144,7 +144,7 @@ async def test_process_with_cancellation() -> None: with pytest.raises(asyncio.CancelledError): await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a high-contrast editorial treatment", "data": b"fake image data", "cancel_token": cancel_token, @@ -173,7 +173,7 @@ async def test_process_includes_user_agent_header() -> None: await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a soft watercolor treatment", "data": b"fake image data", } @@ -210,7 +210,7 @@ async def test_process_includes_integration_in_user_agent() -> None: await client.process( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a soft watercolor treatment", "data": b"fake image data", } diff --git a/tests/test_queue.py b/tests/test_queue.py index f09aef4..54e87b7 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -19,7 +19,7 @@ async def test_queue_submit_video_to_video_basic() -> None: job = await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Restyle this clip with a cinematic dusk grade", "data": b"fake video data", "seed": 42, @@ -41,7 +41,7 @@ async def test_queue_submit_video_to_video() -> None: job = await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Anime style", "data": b"fake video data", "enhance_prompt": True, @@ -60,7 +60,7 @@ async def test_queue_rejects_image_models() -> None: with pytest.raises(DecartSDKError) as exc_info: await client.queue.submit( { - "model": models.image("lucy-pro-i2i"), + "model": models.image("lucy-image-2"), "prompt": "Apply a painterly sunset color grade", } ) @@ -129,7 +129,7 @@ async def test_queue_submit_and_poll_completed() -> None: result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add anime shading and crisp outlines", "data": b"fake video data", } @@ -155,7 +155,7 @@ async def test_queue_submit_and_poll_failed() -> None: result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add anime shading and crisp outlines", "data": b"fake video data", } @@ -190,7 +190,7 @@ def on_status_change(job): await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Add anime shading and crisp outlines", "data": b"fake video data", "on_status_change": on_status_change, @@ -210,7 +210,7 @@ async def test_queue_submit_missing_required_field() -> None: with pytest.raises(DecartSDKError): await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), # Missing 'prompt' and 'data' which are required for v2v } ) @@ -225,13 +225,13 @@ async def test_queue_submit_max_prompt_length() -> None: with pytest.raises(DecartSDKError) as exception: await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": prompt, "data": b"fake video data", } ) - assert "Invalid inputs for lucy-pro-v2v" in str(exception) + assert "Invalid inputs for lucy-clip" in str(exception) @pytest.mark.asyncio @@ -255,7 +255,7 @@ async def test_queue_includes_user_agent_header() -> None: await client.queue.submit( { - "model": models.video("lucy-pro-v2v"), + "model": models.video("lucy-clip"), "prompt": "Apply a cinematic grade", "data": b"fake video data", } @@ -269,7 +269,7 @@ async def test_queue_includes_user_agent_header() -> None: assert headers["User-Agent"].startswith("decart-python-sdk/") -# Tests for lucy-2-v2v +# Tests for lucy-2 @pytest.mark.asyncio @@ -281,7 +281,7 @@ async def test_queue_lucy2_v2v_with_prompt() -> None: job = await client.queue.submit( { - "model": models.video("lucy-2-v2v"), + "model": models.video("lucy-2"), "prompt": "Restyle the scene with softer contrast and warmer highlights", "data": b"fake video data", "enhance_prompt": True, @@ -303,7 +303,7 @@ async def test_queue_lucy2_v2v_with_empty_prompt_and_reference_image() -> None: job = await client.queue.submit( { - "model": models.video("lucy-2-v2v"), + "model": models.video("lucy-2"), "prompt": "", "reference_image": b"fake image data", "data": b"fake video data", @@ -324,7 +324,7 @@ async def test_queue_lucy2_v2v_with_both_prompt_and_reference_image() -> None: job = await client.queue.submit( { - "model": models.video("lucy-2-v2v"), + "model": models.video("lucy-2"), "prompt": "Transform the scene", "reference_image": b"fake image data", "data": b"fake video data", @@ -337,12 +337,12 @@ async def test_queue_lucy2_v2v_with_both_prompt_and_reference_image() -> None: mock_submit.assert_called_once() -# Tests for lucy-restyle-v2v with reference_image +# Tests for lucy-restyle-2 with reference_image @pytest.mark.asyncio async def test_queue_restyle_with_prompt() -> None: - """Test lucy-restyle-v2v submission with text prompt.""" + """Test lucy-restyle-2 submission with text prompt.""" client = DecartClient(api_key="test-key") with patch("decart.queue.client.submit_job") as mock_submit: @@ -350,7 +350,7 @@ async def test_queue_restyle_with_prompt() -> None: job = await client.queue.submit( { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "prompt": "Make it look like anime", "data": b"fake video data", "enhance_prompt": True, @@ -364,7 +364,7 @@ async def test_queue_restyle_with_prompt() -> None: @pytest.mark.asyncio async def test_queue_restyle_with_reference_image() -> None: - """Test lucy-restyle-v2v submission with reference image.""" + """Test lucy-restyle-2 submission with reference image.""" client = DecartClient(api_key="test-key") with patch("decart.queue.client.submit_job") as mock_submit: @@ -372,7 +372,7 @@ async def test_queue_restyle_with_reference_image() -> None: job = await client.queue.submit( { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "reference_image": b"fake image data", "data": b"fake video data", } @@ -385,13 +385,13 @@ async def test_queue_restyle_with_reference_image() -> None: @pytest.mark.asyncio async def test_queue_restyle_rejects_both_prompt_and_reference_image() -> None: - """Test that lucy-restyle-v2v rejects both prompt and reference_image.""" + """Test that lucy-restyle-2 rejects both prompt and reference_image.""" client = DecartClient(api_key="test-key") with pytest.raises(DecartSDKError) as exc_info: await client.queue.submit( { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "prompt": "Make it anime", "reference_image": b"fake image data", "data": b"fake video data", @@ -403,13 +403,13 @@ async def test_queue_restyle_rejects_both_prompt_and_reference_image() -> None: @pytest.mark.asyncio async def test_queue_restyle_rejects_neither_prompt_nor_reference_image() -> None: - """Test that lucy-restyle-v2v rejects when neither prompt nor reference_image provided.""" + """Test that lucy-restyle-2 rejects when neither prompt nor reference_image provided.""" client = DecartClient(api_key="test-key") with pytest.raises(DecartSDKError) as exc_info: await client.queue.submit( { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "data": b"fake video data", } ) @@ -425,7 +425,7 @@ async def test_queue_restyle_rejects_enhance_prompt_with_reference_image() -> No with pytest.raises(DecartSDKError) as exc_info: await client.queue.submit( { - "model": models.video("lucy-restyle-v2v"), + "model": models.video("lucy-restyle-2"), "reference_image": b"fake image data", "data": b"fake video data", "enhance_prompt": True, diff --git a/tests/test_realtime_unit.py b/tests/test_realtime_unit.py index c4ab9de..2972b28 100644 --- a/tests/test_realtime_unit.py +++ b/tests/test_realtime_unit.py @@ -25,29 +25,29 @@ def test_realtime_client_available(): def test_realtime_models_available(): """Test that realtime models are available""" - model = models.realtime("mirage") - assert model.name == "mirage" + model = models.realtime("lucy-restyle") + assert model.name == "lucy-restyle" assert model.fps == 25 assert model.width == 1280 assert model.height == 704 assert model.url_path == "/v1/stream" - model2 = models.realtime("mirage_v2") - assert model2.name == "mirage_v2" + model2 = models.realtime("lucy-restyle-2") + assert model2.name == "lucy-restyle-2" assert model2.fps == 22 assert model2.width == 1280 assert model2.height == 704 assert model2.url_path == "/v1/stream" - model2 = models.realtime("lucy_v2v_720p_rt") - assert model2.name == "lucy_v2v_720p_rt" + model2 = models.realtime("lucy") + assert model2.name == "lucy" assert model2.fps == 25 assert model2.width == 1280 assert model2.height == 704 assert model2.url_path == "/v1/stream" - model2 = models.realtime("lucy_2_rt") - assert model2.name == "lucy_2_rt" + model2 = models.realtime("lucy-2") + assert model2.name == "lucy-2" assert model2.fps == 20 assert model2.width == 1280 assert model2.height == 720 @@ -86,7 +86,7 @@ async def test_realtime_client_creation_with_mock(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, initial_state=ModelState(prompt=Prompt(text="Test", enhance=True)), ), @@ -147,7 +147,7 @@ def register_prompt_wait(prompt): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -186,7 +186,7 @@ async def test_buffered_events_delivered_after_handler_registration(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -223,7 +223,7 @@ async def test_realtime_events(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -285,7 +285,7 @@ def register_prompt_wait(prompt): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -332,7 +332,7 @@ def register_prompt_wait(prompt): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -357,8 +357,8 @@ async def set_event(): def test_avatar_live_model_available(): """Test that avatar-live model is available""" - model = models.realtime("live_avatar") - assert model.name == "live_avatar" + model = models.realtime("live-avatar") + assert model.name == "live-avatar" assert model.fps == 25 assert model.width == 1280 assert model.height == 720 @@ -400,14 +400,14 @@ async def test_avatar_live_connect_with_initial_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("live_avatar"), + model=models.realtime("live-avatar"), on_remote_stream=lambda t: None, initial_state=ModelState(image=b"fake image bytes"), ), ) assert realtime_client is not None - assert realtime_client._model_name == "live_avatar" + assert realtime_client._model_name == "live-avatar" mock_image_to_b64.assert_called_once() # Verify initial_image was passed to connect mock_manager.connect.assert_called_once() @@ -448,7 +448,7 @@ async def test_avatar_live_set_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("live_avatar"), + model=models.realtime("live-avatar"), on_remote_stream=lambda t: None, ), ) @@ -491,7 +491,7 @@ async def test_set_image_works_for_any_model(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -528,7 +528,7 @@ async def test_set_image_null_clears_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -569,7 +569,7 @@ async def test_set_image_with_prompt_and_enhance(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -614,7 +614,7 @@ async def test_avatar_live_set_image_timeout(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("live_avatar"), + model=models.realtime("live-avatar"), on_remote_stream=lambda t: None, ), ) @@ -659,7 +659,7 @@ async def test_avatar_live_set_image_server_error(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("live_avatar"), + model=models.realtime("live-avatar"), on_remote_stream=lambda t: None, ), ) @@ -702,7 +702,7 @@ async def test_set_rejects_when_neither_prompt_nor_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -740,7 +740,7 @@ async def test_set_rejects_empty_prompt(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -778,7 +778,7 @@ async def test_set_sends_prompt_only(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -824,7 +824,7 @@ async def test_set_sends_prompt_with_enhance(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -873,7 +873,7 @@ async def test_set_sends_image_only(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -923,7 +923,7 @@ async def test_set_sends_prompt_and_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -972,7 +972,7 @@ async def test_set_converts_bytes_image(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, ), ) @@ -1021,7 +1021,7 @@ async def test_connect_with_initial_prompt(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, initial_state=ModelState(prompt=Prompt(text="Test prompt", enhance=False)), ), @@ -1127,7 +1127,7 @@ async def test_connect_without_initial_state_sends_passthrough(): api_key=client.api_key, local_track=mock_track, options=RealtimeConnectOptions( - model=models.realtime("mirage"), + model=models.realtime("lucy-restyle"), on_remote_stream=lambda t: None, # No initial_state — should trigger passthrough ), diff --git a/tests/test_tokens.py b/tests/test_tokens.py index 83a3f3d..fb8ecf5 100644 --- a/tests/test_tokens.py +++ b/tests/test_tokens.py @@ -158,10 +158,10 @@ async def test_create_token_with_allowed_models() -> None: ) with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)): - await client.tokens.create(allowed_models=["lucy_2_rt"]) + await client.tokens.create(allowed_models=["lucy-2"]) call_kwargs = mock_session.post.call_args - assert call_kwargs.kwargs["json"] == {"allowedModels": ["lucy_2_rt"]} + assert call_kwargs.kwargs["json"] == {"allowedModels": ["lucy-2"]} @pytest.mark.asyncio @@ -199,7 +199,7 @@ async def test_create_token_with_all_v2_fields() -> None: return_value={ "apiKey": "ek_test123", "expiresAt": "2024-12-15T12:10:00Z", - "permissions": {"models": ["lucy_2_rt"]}, + "permissions": {"models": ["lucy-2"]}, "constraints": {"realtime": {"maxSessionDuration": 120}}, } ) @@ -213,19 +213,19 @@ async def test_create_token_with_all_v2_fields() -> None: result = await client.tokens.create( metadata={"role": "viewer"}, expires_in=120, - allowed_models=["lucy_2_rt"], + allowed_models=["lucy-2"], constraints={"realtime": {"maxSessionDuration": 120}}, ) assert result.api_key == "ek_test123" assert result.expires_at == "2024-12-15T12:10:00Z" - assert result.permissions == {"models": ["lucy_2_rt"]} + assert result.permissions == {"models": ["lucy-2"]} assert result.constraints == {"realtime": {"maxSessionDuration": 120}} call_kwargs = mock_session.post.call_args assert call_kwargs.kwargs["json"] == { "metadata": {"role": "viewer"}, "expiresIn": 120, - "allowedModels": ["lucy_2_rt"], + "allowedModels": ["lucy-2"], "constraints": {"realtime": {"maxSessionDuration": 120}}, } From c999638287f53dfbf991fc52329e60f662ceb26a Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 7 Apr 2026 00:46:48 +0300 Subject: [PATCH 2/3] fix: format models.py with black --- decart/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/decart/models.py b/decart/models.py index 56a917b..b0d30c6 100644 --- a/decart/models.py +++ b/decart/models.py @@ -70,6 +70,7 @@ def _warn_deprecated(model: str) -> None: stacklevel=3, ) + # Type variable for model name ModelT = TypeVar("ModelT", bound=str) From 5a0c5e583611ffe3a34a6f1981b78680cb6b8d26 Mon Sep 17 00:00:00 2001 From: Adir Amsalem Date: Tue, 7 Apr 2026 01:26:00 +0300 Subject: [PATCH 3/3] feat: add -latest model aliases for server-side resolution Add lucy-latest, lucy-vton-latest, lucy-restyle-latest, lucy-clip-latest, lucy-motion-latest, and lucy-image-latest convenience aliases that always point to the current latest version of each model family. These are resolved server-side, so the SDK passes them through with no deprecation warnings. --- decart/models.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/decart/models.py b/decart/models.py index b0d30c6..0a7f8a6 100644 --- a/decart/models.py +++ b/decart/models.py @@ -14,6 +14,10 @@ "lucy-restyle", "lucy-restyle-2", "live-avatar", + # Latest aliases (server-side resolution) + "lucy-latest", + "lucy-vton-latest", + "lucy-restyle-latest", # Deprecated names "mirage", "mirage_v2", @@ -28,6 +32,11 @@ "lucy-2.1", "lucy-restyle-2", "lucy-motion", + # Latest aliases (server-side resolution) + "lucy-latest", + "lucy-restyle-latest", + "lucy-clip-latest", + "lucy-motion-latest", # Deprecated names "lucy-pro-v2v", "lucy-restyle-v2v", @@ -36,6 +45,8 @@ ImageModels = Literal[ # Canonical names "lucy-image-2", + # Latest alias (server-side resolution) + "lucy-image-latest", # Deprecated names "lucy-pro-i2i", ] @@ -236,6 +247,31 @@ class ImageToImageInput(DecartBaseModel): height=720, input_schema=BaseModel, ), + # Latest aliases (server-side resolution) + "lucy-latest": ModelDefinition( + name="lucy-latest", + url_path="/v1/stream", + fps=20, + width=1088, + height=624, + input_schema=BaseModel, + ), + "lucy-vton-latest": ModelDefinition( + name="lucy-vton-latest", + url_path="/v1/stream", + fps=20, + width=1088, + height=624, + input_schema=BaseModel, + ), + "lucy-restyle-latest": ModelDefinition( + name="lucy-restyle-latest", + url_path="/v1/stream", + fps=22, + width=1280, + height=704, + input_schema=BaseModel, + ), # Deprecated names "mirage": ModelDefinition( name="mirage", @@ -320,6 +356,39 @@ class ImageToImageInput(DecartBaseModel): height=704, input_schema=ImageToMotionVideoInput, ), + # Latest aliases (server-side resolution) + "lucy-latest": ModelDefinition( + name="lucy-latest", + url_path="/v1/jobs/lucy-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", + fps=22, + width=1280, + height=704, + input_schema=VideoRestyleInput, + ), + "lucy-clip-latest": ModelDefinition( + name="lucy-clip-latest", + url_path="/v1/jobs/lucy-clip-latest", + fps=25, + width=1280, + height=704, + input_schema=VideoToVideoInput, + ), + "lucy-motion-latest": ModelDefinition( + name="lucy-motion-latest", + url_path="/v1/jobs/lucy-motion-latest", + fps=25, + width=1280, + height=704, + input_schema=ImageToMotionVideoInput, + ), # Deprecated names "lucy-pro-v2v": ModelDefinition( name="lucy-pro-v2v", @@ -356,6 +425,15 @@ class ImageToImageInput(DecartBaseModel): height=704, input_schema=ImageToImageInput, ), + # Latest alias (server-side resolution) + "lucy-image-latest": ModelDefinition( + name="lucy-image-latest", + url_path="/v1/generate/lucy-image-latest", + fps=25, + width=1280, + height=704, + input_schema=ImageToImageInput, + ), # Deprecated names "lucy-pro-i2i": ModelDefinition( name="lucy-pro-i2i", diff --git a/tests/test_models.py b/tests/test_models.py index 95868db..7a845f7 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -164,6 +164,75 @@ def test_deprecation_warning_only_once() -> None: assert len(w) == 1 +def test_latest_realtime_models() -> None: + model = models.realtime("lucy-latest") + assert model.name == "lucy-latest" + assert model.url_path == "/v1/stream" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + + model = models.realtime("lucy-vton-latest") + assert model.name == "lucy-vton-latest" + assert model.url_path == "/v1/stream" + assert model.fps == 20 + assert model.width == 1088 + assert model.height == 624 + + model = models.realtime("lucy-restyle-latest") + assert model.name == "lucy-restyle-latest" + assert model.url_path == "/v1/stream" + assert model.fps == 22 + assert model.width == 1280 + assert model.height == 704 + + +def test_latest_video_models() -> None: + model = models.video("lucy-latest") + assert model.name == "lucy-latest" + assert model.url_path == "/v1/jobs/lucy-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" + assert model.fps == 22 + + model = models.video("lucy-clip-latest") + assert model.name == "lucy-clip-latest" + assert model.url_path == "/v1/jobs/lucy-clip-latest" + assert model.fps == 25 + + model = models.video("lucy-motion-latest") + assert model.name == "lucy-motion-latest" + assert model.url_path == "/v1/jobs/lucy-motion-latest" + assert model.fps == 25 + + +def test_latest_image_models() -> None: + model = models.image("lucy-image-latest") + assert model.name == "lucy-image-latest" + assert model.url_path == "/v1/generate/lucy-image-latest" + + +def test_latest_aliases_no_deprecation_warning() -> None: + _warned_aliases.clear() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + models.realtime("lucy-latest") + models.realtime("lucy-vton-latest") + models.realtime("lucy-restyle-latest") + models.video("lucy-latest") + models.video("lucy-restyle-latest") + models.video("lucy-clip-latest") + models.video("lucy-motion-latest") + models.image("lucy-image-latest") + assert len(w) == 0 + + def test_invalid_model() -> None: with pytest.raises(DecartSDKError): models.video("invalid-model")