diff --git a/decart/models.py b/decart/models.py index 1de9dba..cb55bb5 100644 --- a/decart/models.py +++ b/decart/models.py @@ -14,6 +14,7 @@ "lucy-pro-flf2v", "lucy-motion", "lucy-restyle-v2v", + "lucy-2-v2v", ] ImageModels = Literal["lucy-pro-t2i", "lucy-pro-i2i"] Model = Literal[RealTimeModels, VideoModels, ImageModels] @@ -125,6 +126,27 @@ def validate_prompt_or_reference_image(self) -> "VideoRestyleInput": return self +class VideoEdit2Input(DecartBaseModel): + """Input for lucy-2-v2v model. + + Must provide at least one of `prompt` or `reference_image`. + Both can be provided together. + """ + + prompt: Optional[str] = Field(default=None, min_length=1, max_length=1000) + reference_image: Optional[FileInput] = None + data: FileInput + seed: Optional[int] = None + resolution: Optional[str] = None + enhance_prompt: Optional[bool] = None + + @model_validator(mode="after") + def validate_prompt_or_reference_image(self) -> "VideoEdit2Input": + if self.prompt is None and self.reference_image is None: + raise ValueError("Must provide at least one of 'prompt' or 'reference_image'") + return self + + class TextToImageInput(BaseModel): prompt: str = Field( ..., @@ -256,6 +278,14 @@ class ImageToImageInput(DecartBaseModel): height=704, input_schema=VideoRestyleInput, ), + "lucy-2-v2v": ModelDefinition( + name="lucy-2-v2v", + url_path="/v1/generate/lucy-2-v2v", + fps=20, + width=1280, + height=720, + input_schema=VideoEdit2Input, + ), }, "image": { "lucy-pro-t2i": ModelDefinition( @@ -302,6 +332,7 @@ def video(model: VideoModels) -> VideoModelDefinition: - "lucy-fast-v2v" - Video-to-video (Fast quality) - "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) """ try: return _MODELS["video"][model] # type: ignore[return-value] diff --git a/tests/test_models.py b/tests/test_models.py index 0d0e0ef..31fe672 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -46,6 +46,15 @@ def test_image_models() -> None: assert model.url_path == "/v1/generate/lucy-pro-t2i" +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" + assert model.fps == 20 + assert model.width == 1280 + assert model.height == 720 + + def test_invalid_model() -> None: with pytest.raises(DecartSDKError): models.video("invalid-model") diff --git a/tests/test_queue.py b/tests/test_queue.py index 3d287ee..71d7acb 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -263,6 +263,88 @@ async def test_queue_includes_user_agent_header() -> None: assert headers["User-Agent"].startswith("decart-python-sdk/") +# Tests for lucy-2-v2v + + +@pytest.mark.asyncio +async def test_queue_lucy2_v2v_with_prompt() -> None: + client = DecartClient(api_key="test-key") + + with patch("decart.queue.client.submit_job") as mock_submit: + mock_submit.return_value = MagicMock(job_id="job-lucy2", status="pending") + + job = await client.queue.submit( + { + "model": models.video("lucy-2-v2v"), + "prompt": "Transform the scene", + "data": b"fake video data", + "enhance_prompt": True, + "seed": 42, + } + ) + + assert job.job_id == "job-lucy2" + assert job.status == "pending" + mock_submit.assert_called_once() + + +@pytest.mark.asyncio +async def test_queue_lucy2_v2v_with_reference_image_only() -> None: + client = DecartClient(api_key="test-key") + + with patch("decart.queue.client.submit_job") as mock_submit: + mock_submit.return_value = MagicMock(job_id="job-lucy2-ref", status="pending") + + job = await client.queue.submit( + { + "model": models.video("lucy-2-v2v"), + "reference_image": b"fake image data", + "data": b"fake video data", + } + ) + + assert job.job_id == "job-lucy2-ref" + assert job.status == "pending" + mock_submit.assert_called_once() + + +@pytest.mark.asyncio +async def test_queue_lucy2_v2v_with_both_prompt_and_reference_image() -> None: + client = DecartClient(api_key="test-key") + + with patch("decart.queue.client.submit_job") as mock_submit: + mock_submit.return_value = MagicMock(job_id="job-lucy2-both", status="pending") + + job = await client.queue.submit( + { + "model": models.video("lucy-2-v2v"), + "prompt": "Transform the scene", + "reference_image": b"fake image data", + "data": b"fake video data", + "seed": 123, + } + ) + + assert job.job_id == "job-lucy2-both" + assert job.status == "pending" + mock_submit.assert_called_once() + + +@pytest.mark.asyncio +async def test_queue_lucy2_v2v_rejects_neither_prompt_nor_reference_image() -> None: + client = DecartClient(api_key="test-key") + + with pytest.raises(DecartSDKError) as exc_info: + await client.queue.submit( + { + "model": models.video("lucy-2-v2v"), + "data": b"fake video data", + } + ) + + assert "at least one of 'prompt' or 'reference_image'" in str(exc_info.value).lower() + + # Tests for lucy-restyle-v2v with reference_image