Skip to content

Commit 53c4958

Browse files
committed
feat(api): enhance image and video processing with distinct APIs
- Updated DecartClient to differentiate between synchronous image processing and asynchronous video queueing. - Introduced ImageModelDefinition and VideoModelDefinition for better type safety. - Enhanced error handling to ensure only appropriate models are used with respective APIs. - Updated examples and tests to reflect the new processing structure and added validation for model types.
1 parent a7697fc commit 53c4958

File tree

7 files changed

+436
-145
lines changed

7 files changed

+436
-145
lines changed

decart/client.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from typing import Any, Optional
2+
import asyncio
23
import aiohttp
34
from pydantic import ValidationError
45
from .errors import InvalidAPIKeyError, InvalidBaseURLError, InvalidInputError
5-
from .models import ModelDefinition
6+
from .models import ImageModelDefinition, _MODELS
7+
from .types import FileInput
68
from .process.request import send_request
79
from .queue.client import QueueClient
810

@@ -27,7 +29,15 @@ class DecartClient:
2729
Example:
2830
```python
2931
client = DecartClient(api_key="your-key")
30-
result = await client.process({
32+
33+
# Image generation (sync) - use process()
34+
image = await client.process({
35+
"model": models.image("lucy-pro-t2i"),
36+
"prompt": "A serene lake at sunset",
37+
})
38+
39+
# Video generation (async) - use queue
40+
result = await client.queue.submit_and_poll({
3141
"model": models.video("lucy-pro-t2v"),
3242
"prompt": "A serene lake at sunset",
3343
})
@@ -55,7 +65,8 @@ def __init__(
5565
@property
5666
def queue(self) -> QueueClient:
5767
"""
58-
Queue client for async job-based video and image generation.
68+
Queue client for async job-based video generation.
69+
Only video models support the queue API.
5970
6071
Example:
6172
```python
@@ -97,22 +108,38 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
97108

98109
async def process(self, options: dict[str, Any]) -> bytes:
99110
"""
100-
Process video or image generation/transformation.
111+
Process image generation/transformation synchronously.
112+
Only image models support the process API.
113+
114+
For video generation, use the queue API instead:
115+
result = await client.queue.submit_and_poll({...})
101116
102117
Args:
103118
options: Processing options including model and inputs
119+
- model: ImageModelDefinition from models.image()
120+
- prompt: Text prompt for generation
121+
- Additional model-specific inputs
104122
105123
Returns:
106-
Generated/transformed media as bytes
124+
Generated/transformed image as bytes
107125
108126
Raises:
109-
InvalidInputError: If inputs are invalid
127+
InvalidInputError: If inputs are invalid or model is not an image model
110128
ProcessingError: If processing fails
111129
"""
112130
if "model" not in options:
113131
raise InvalidInputError("model is required")
114132

115-
model: ModelDefinition = options["model"]
133+
model: ImageModelDefinition = options["model"]
134+
135+
# Validate that this is an image model (check against registry)
136+
if model.name not in _MODELS["image"]:
137+
raise InvalidInputError(
138+
f"Model '{model.name}' is not supported by process(). "
139+
f"Only image models support sync processing. "
140+
f"For video models, use client.queue.submit_and_poll() instead."
141+
)
142+
116143
cancel_token = options.get("cancel_token")
117144

118145
inputs = {k: v for k, v in options.items() if k not in ("model", "cancel_token")}

decart/models.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Optional, List
1+
from typing import Literal, Optional, List, Generic, TypeVar
22
from pydantic import BaseModel, Field, ConfigDict
33
from .errors import ModelNotFoundError
44
from .types import FileInput, MotionTrajectoryInput
@@ -17,20 +17,34 @@
1717
ImageModels = Literal["lucy-pro-t2i", "lucy-pro-i2i"]
1818
Model = Literal[RealTimeModels, VideoModels, ImageModels]
1919

20+
# Type variable for model name
21+
ModelT = TypeVar("ModelT", bound=str)
22+
2023

2124
class DecartBaseModel(BaseModel):
2225
model_config = ConfigDict(arbitrary_types_allowed=True)
2326

2427

25-
class ModelDefinition(DecartBaseModel):
26-
name: str
28+
class ModelDefinition(DecartBaseModel, Generic[ModelT]):
29+
name: ModelT
2730
url_path: str
2831
fps: int = Field(ge=1)
2932
width: int = Field(ge=1)
3033
height: int = Field(ge=1)
3134
input_schema: type[BaseModel]
3235

3336

37+
# Type aliases for model definitions that support specific APIs
38+
ImageModelDefinition = ModelDefinition[ImageModels]
39+
"""Type alias for model definitions that support synchronous processing (process API)."""
40+
41+
VideoModelDefinition = ModelDefinition[VideoModels]
42+
"""Type alias for model definitions that support queue processing (queue API)."""
43+
44+
RealTimeModelDefinition = ModelDefinition[RealTimeModels]
45+
"""Type alias for model definitions that support realtime streaming."""
46+
47+
3448
class TextToVideoInput(BaseModel):
3549
prompt: str = Field(..., min_length=1, max_length=1000)
3650
seed: Optional[int] = None
@@ -212,23 +226,45 @@ class ImageToImageInput(DecartBaseModel):
212226

213227
class Models:
214228
@staticmethod
215-
def realtime(model: RealTimeModels) -> ModelDefinition:
229+
def realtime(model: RealTimeModels) -> RealTimeModelDefinition:
230+
"""Get a realtime model definition for WebRTC streaming."""
216231
try:
217-
return _MODELS["realtime"][model]
232+
return _MODELS["realtime"][model] # type: ignore[return-value]
218233
except KeyError:
219234
raise ModelNotFoundError(model)
220235

221236
@staticmethod
222-
def video(model: VideoModels) -> ModelDefinition:
237+
def video(model: VideoModels) -> VideoModelDefinition:
238+
"""
239+
Get a video model definition.
240+
Video models only support the queue API.
241+
242+
Available models:
243+
- "lucy-pro-t2v" - Text-to-video
244+
- "lucy-pro-i2v" - Image-to-video
245+
- "lucy-pro-v2v" - Video-to-video
246+
- "lucy-pro-flf2v" - First-last-frame-to-video
247+
- "lucy-dev-i2v" - Image-to-video (Dev quality)
248+
- "lucy-fast-v2v" - Video-to-video (Fast quality)
249+
- "lucy-motion" - Image-to-motion-video
250+
"""
223251
try:
224-
return _MODELS["video"][model]
252+
return _MODELS["video"][model] # type: ignore[return-value]
225253
except KeyError:
226254
raise ModelNotFoundError(model)
227255

228256
@staticmethod
229-
def image(model: ImageModels) -> ModelDefinition:
257+
def image(model: ImageModels) -> ImageModelDefinition:
258+
"""
259+
Get an image model definition.
260+
Image models only support the process (sync) API.
261+
262+
Available models:
263+
- "lucy-pro-t2i" - Text-to-image
264+
- "lucy-pro-i2i" - Image-to-image
265+
"""
230266
try:
231-
return _MODELS["image"][model]
267+
return _MODELS["image"][model] # type: ignore[return-value]
232268
except KeyError:
233269
raise ModelNotFoundError(model)
234270

decart/queue/client.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import aiohttp
55
from pydantic import ValidationError
66

7-
from ..models import ModelDefinition
7+
from ..models import VideoModelDefinition, _MODELS
88
from ..errors import InvalidInputError
99
from .request import submit_job, get_job_status, get_job_content
1010
from .types import (
@@ -25,7 +25,8 @@
2525

2626
class QueueClient:
2727
"""
28-
Queue client for async job-based video and image generation.
28+
Queue client for async job-based video generation.
29+
Only video models support the queue API.
2930
3031
Jobs are submitted and processed asynchronously, allowing you to
3132
poll for status and retrieve results when ready.
@@ -59,23 +60,35 @@ async def _get_session(self) -> aiohttp.ClientSession:
5960

6061
async def submit(self, options: dict[str, Any]) -> JobSubmitResponse:
6162
"""
62-
Submit a job to the queue for async processing.
63+
Submit a video generation job to the queue for async processing.
64+
Only video models are supported.
6365
Returns immediately with job_id and initial status.
6466
6567
Args:
6668
options: Submit options including model and inputs
69+
- model: VideoModelDefinition from models.video()
70+
- prompt: Text prompt for generation
71+
- Additional model-specific inputs
6772
6873
Returns:
6974
JobSubmitResponse with job_id and status
7075
7176
Raises:
72-
InvalidInputError: If inputs are invalid
77+
InvalidInputError: If inputs are invalid or model is not a video model
7378
QueueSubmitError: If submission fails
7479
"""
7580
if "model" not in options:
7681
raise InvalidInputError("model is required")
7782

78-
model: ModelDefinition = options["model"]
83+
model: VideoModelDefinition = options["model"]
84+
85+
# Validate that this is a video model (check against registry)
86+
if model.name not in _MODELS["video"]:
87+
raise InvalidInputError(
88+
f"Model '{model.name}' is not supported by queue API. "
89+
f"Only video models support async queue processing. "
90+
f"For image models, use client.process() instead."
91+
)
7992

8093
inputs = {k: v for k, v in options.items() if k not in ("model", "cancel_token")}
8194

examples/process_url.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""
2+
Video transformation from URL example using the Queue API.
3+
Video models only support async queue processing.
4+
"""
15
import asyncio
26
import os
37
from decart import DecartClient, models
@@ -6,18 +10,21 @@
610
async def main() -> None:
711
async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client:
812
print("Transforming video from URL...")
9-
result = await client.process(
13+
result = await client.queue.submit_and_poll(
1014
{
1115
"model": models.video("lucy-pro-v2v"),
1216
"prompt": "Watercolor painting style",
1317
"data": "https://docs.platform.decart.ai/assets/example-video.mp4",
18+
"on_status_change": lambda job: print(f" Status: {job.status}"),
1419
}
1520
)
1621

17-
with open("output_url.mp4", "wb") as f:
18-
f.write(result)
19-
20-
print("Video saved to output_url.mp4")
22+
if result.status == "completed":
23+
with open("output_url.mp4", "wb") as f:
24+
f.write(result.data)
25+
print("Video saved to output_url.mp4")
26+
else:
27+
print(f"Job failed: {result.error}")
2128

2229

2330
if __name__ == "__main__":

examples/process_video.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,53 @@
1+
"""
2+
Video generation example using the Queue API.
3+
Video models only support async queue processing.
4+
"""
15
import asyncio
26
import os
37
from decart import DecartClient, models
48

59

610
async def main() -> None:
711
async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client:
12+
# Text-to-video generation
813
print("Generating video from text...")
9-
result = await client.process(
14+
result = await client.queue.submit_and_poll(
1015
{
1116
"model": models.video("lucy-pro-t2v"),
1217
"prompt": "A serene lake at sunset with mountains in the background",
1318
"seed": 42,
19+
"on_status_change": lambda job: print(f" Status: {job.status}"),
1420
}
1521
)
1622

17-
with open("output_t2v.mp4", "wb") as f:
18-
f.write(result)
23+
if result.status == "completed":
24+
with open("output_t2v.mp4", "wb") as f:
25+
f.write(result.data)
26+
print("Video saved to output_t2v.mp4")
27+
else:
28+
print(f"Text-to-video failed: {result.error}")
29+
return
1930

20-
print("Video saved to output_t2v.mp4")
21-
22-
print("Transforming video...")
31+
# Video-to-video transformation
32+
print("\nTransforming video...")
2333
with open("output_t2v.mp4", "rb") as video_file:
24-
result = await client.process(
34+
result = await client.queue.submit_and_poll(
2535
{
2636
"model": models.video("lucy-pro-v2v"),
2737
"prompt": "Anime style with vibrant colors",
2838
"data": video_file,
2939
"enhance_prompt": True,
3040
"num_inference_steps": 50,
41+
"on_status_change": lambda job: print(f" Status: {job.status}"),
3142
}
3243
)
3344

34-
with open("output_v2v.mp4", "wb") as f:
35-
f.write(result)
36-
37-
print("Video saved to output_v2v.mp4")
45+
if result.status == "completed":
46+
with open("output_v2v.mp4", "wb") as f:
47+
f.write(result.data)
48+
print("Video saved to output_v2v.mp4")
49+
else:
50+
print(f"Video-to-video failed: {result.error}")
3851

3952

4053
if __name__ == "__main__":

0 commit comments

Comments
 (0)