Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions decart/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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(
...,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand Down
9 changes: 9 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
82 changes: 82 additions & 0 deletions tests/test_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading