Skip to content

Commit fc2be94

Browse files
authored
feat(video): Add lucy-2-v2v model (#33)
feat: add lucy-2-v2v model support
1 parent 0db5686 commit fc2be94

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

decart/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"lucy-pro-flf2v",
1515
"lucy-motion",
1616
"lucy-restyle-v2v",
17+
"lucy-2-v2v",
1718
]
1819
ImageModels = Literal["lucy-pro-t2i", "lucy-pro-i2i"]
1920
Model = Literal[RealTimeModels, VideoModels, ImageModels]
@@ -125,6 +126,27 @@ def validate_prompt_or_reference_image(self) -> "VideoRestyleInput":
125126
return self
126127

127128

129+
class VideoEdit2Input(DecartBaseModel):
130+
"""Input for lucy-2-v2v model.
131+
132+
Must provide at least one of `prompt` or `reference_image`.
133+
Both can be provided together.
134+
"""
135+
136+
prompt: Optional[str] = Field(default=None, min_length=1, max_length=1000)
137+
reference_image: Optional[FileInput] = None
138+
data: FileInput
139+
seed: Optional[int] = None
140+
resolution: Optional[str] = None
141+
enhance_prompt: Optional[bool] = None
142+
143+
@model_validator(mode="after")
144+
def validate_prompt_or_reference_image(self) -> "VideoEdit2Input":
145+
if self.prompt is None and self.reference_image is None:
146+
raise ValueError("Must provide at least one of 'prompt' or 'reference_image'")
147+
return self
148+
149+
128150
class TextToImageInput(BaseModel):
129151
prompt: str = Field(
130152
...,
@@ -256,6 +278,14 @@ class ImageToImageInput(DecartBaseModel):
256278
height=704,
257279
input_schema=VideoRestyleInput,
258280
),
281+
"lucy-2-v2v": ModelDefinition(
282+
name="lucy-2-v2v",
283+
url_path="/v1/generate/lucy-2-v2v",
284+
fps=20,
285+
width=1280,
286+
height=720,
287+
input_schema=VideoEdit2Input,
288+
),
259289
},
260290
"image": {
261291
"lucy-pro-t2i": ModelDefinition(
@@ -302,6 +332,7 @@ def video(model: VideoModels) -> VideoModelDefinition:
302332
- "lucy-fast-v2v" - Video-to-video (Fast quality)
303333
- "lucy-motion" - Image-to-motion-video
304334
- "lucy-restyle-v2v" - Video-to-video with prompt or reference image
335+
- "lucy-2-v2v" - Video-to-video editing (long-form, 720p)
305336
"""
306337
try:
307338
return _MODELS["video"][model] # type: ignore[return-value]

tests/test_models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ def test_image_models() -> None:
4646
assert model.url_path == "/v1/generate/lucy-pro-t2i"
4747

4848

49+
def test_lucy_2_v2v_model() -> None:
50+
model = models.video("lucy-2-v2v")
51+
assert model.name == "lucy-2-v2v"
52+
assert model.url_path == "/v1/generate/lucy-2-v2v"
53+
assert model.fps == 20
54+
assert model.width == 1280
55+
assert model.height == 720
56+
57+
4958
def test_invalid_model() -> None:
5059
with pytest.raises(DecartSDKError):
5160
models.video("invalid-model")

tests/test_queue.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,88 @@ async def test_queue_includes_user_agent_header() -> None:
263263
assert headers["User-Agent"].startswith("decart-python-sdk/")
264264

265265

266+
# Tests for lucy-2-v2v
267+
268+
269+
@pytest.mark.asyncio
270+
async def test_queue_lucy2_v2v_with_prompt() -> None:
271+
client = DecartClient(api_key="test-key")
272+
273+
with patch("decart.queue.client.submit_job") as mock_submit:
274+
mock_submit.return_value = MagicMock(job_id="job-lucy2", status="pending")
275+
276+
job = await client.queue.submit(
277+
{
278+
"model": models.video("lucy-2-v2v"),
279+
"prompt": "Transform the scene",
280+
"data": b"fake video data",
281+
"enhance_prompt": True,
282+
"seed": 42,
283+
}
284+
)
285+
286+
assert job.job_id == "job-lucy2"
287+
assert job.status == "pending"
288+
mock_submit.assert_called_once()
289+
290+
291+
@pytest.mark.asyncio
292+
async def test_queue_lucy2_v2v_with_reference_image_only() -> None:
293+
client = DecartClient(api_key="test-key")
294+
295+
with patch("decart.queue.client.submit_job") as mock_submit:
296+
mock_submit.return_value = MagicMock(job_id="job-lucy2-ref", status="pending")
297+
298+
job = await client.queue.submit(
299+
{
300+
"model": models.video("lucy-2-v2v"),
301+
"reference_image": b"fake image data",
302+
"data": b"fake video data",
303+
}
304+
)
305+
306+
assert job.job_id == "job-lucy2-ref"
307+
assert job.status == "pending"
308+
mock_submit.assert_called_once()
309+
310+
311+
@pytest.mark.asyncio
312+
async def test_queue_lucy2_v2v_with_both_prompt_and_reference_image() -> None:
313+
client = DecartClient(api_key="test-key")
314+
315+
with patch("decart.queue.client.submit_job") as mock_submit:
316+
mock_submit.return_value = MagicMock(job_id="job-lucy2-both", status="pending")
317+
318+
job = await client.queue.submit(
319+
{
320+
"model": models.video("lucy-2-v2v"),
321+
"prompt": "Transform the scene",
322+
"reference_image": b"fake image data",
323+
"data": b"fake video data",
324+
"seed": 123,
325+
}
326+
)
327+
328+
assert job.job_id == "job-lucy2-both"
329+
assert job.status == "pending"
330+
mock_submit.assert_called_once()
331+
332+
333+
@pytest.mark.asyncio
334+
async def test_queue_lucy2_v2v_rejects_neither_prompt_nor_reference_image() -> None:
335+
client = DecartClient(api_key="test-key")
336+
337+
with pytest.raises(DecartSDKError) as exc_info:
338+
await client.queue.submit(
339+
{
340+
"model": models.video("lucy-2-v2v"),
341+
"data": b"fake video data",
342+
}
343+
)
344+
345+
assert "at least one of 'prompt' or 'reference_image'" in str(exc_info.value).lower()
346+
347+
266348
# Tests for lucy-restyle-v2v with reference_image
267349

268350

0 commit comments

Comments
 (0)