Skip to content

Commit 39c73fa

Browse files
authored
feat(api): enhance image and video processing with distinct APIs (#14)
* 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. * black format * ruff
1 parent fb130f2 commit 39c73fa

7 files changed

Lines changed: 444 additions & 145 deletions

File tree

decart/client.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import aiohttp
33
from pydantic import ValidationError
44
from .errors import InvalidAPIKeyError, InvalidBaseURLError, InvalidInputError
5-
from .models import ModelDefinition
5+
from .models import ImageModelDefinition, _MODELS
66
from .process.request import send_request
77
from .queue.client import QueueClient
88

@@ -27,7 +27,15 @@ class DecartClient:
2727
Example:
2828
```python
2929
client = DecartClient(api_key="your-key")
30-
result = await client.process({
30+
31+
# Image generation (sync) - use process()
32+
image = await client.process({
33+
"model": models.image("lucy-pro-t2i"),
34+
"prompt": "A serene lake at sunset",
35+
})
36+
37+
# Video generation (async) - use queue
38+
result = await client.queue.submit_and_poll({
3139
"model": models.video("lucy-pro-t2v"),
3240
"prompt": "A serene lake at sunset",
3341
})
@@ -55,7 +63,8 @@ def __init__(
5563
@property
5664
def queue(self) -> QueueClient:
5765
"""
58-
Queue client for async job-based video and image generation.
66+
Queue client for async job-based video generation.
67+
Only video models support the queue API.
5968
6069
Example:
6170
```python
@@ -97,22 +106,38 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
97106

98107
async def process(self, options: dict[str, Any]) -> bytes:
99108
"""
100-
Process video or image generation/transformation.
109+
Process image generation/transformation synchronously.
110+
Only image models support the process API.
111+
112+
For video generation, use the queue API instead:
113+
result = await client.queue.submit_and_poll({...})
101114
102115
Args:
103116
options: Processing options including model and inputs
117+
- model: ImageModelDefinition from models.image()
118+
- prompt: Text prompt for generation
119+
- Additional model-specific inputs
104120
105121
Returns:
106-
Generated/transformed media as bytes
122+
Generated/transformed image as bytes
107123
108124
Raises:
109-
InvalidInputError: If inputs are invalid
125+
InvalidInputError: If inputs are invalid or model is not an image model
110126
ProcessingError: If processing fails
111127
"""
112128
if "model" not in options:
113129
raise InvalidInputError("model is required")
114130

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

118143
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: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Video transformation from URL example using the Queue API.
3+
Video models only support async queue processing.
4+
"""
5+
16
import asyncio
27
import os
38
from decart import DecartClient, models
@@ -6,18 +11,21 @@
611
async def main() -> None:
712
async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client:
813
print("Transforming video from URL...")
9-
result = await client.process(
14+
result = await client.queue.submit_and_poll(
1015
{
1116
"model": models.video("lucy-pro-v2v"),
1217
"prompt": "Watercolor painting style",
1318
"data": "https://docs.platform.decart.ai/assets/example-video.mp4",
19+
"on_status_change": lambda job: print(f" Status: {job.status}"),
1420
}
1521
)
1622

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

2230

2331
if __name__ == "__main__":

examples/process_video.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,54 @@
1+
"""
2+
Video generation example using the Queue API.
3+
Video models only support async queue processing.
4+
"""
5+
16
import asyncio
27
import os
38
from decart import DecartClient, models
49

510

611
async def main() -> None:
712
async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client:
13+
# Text-to-video generation
814
print("Generating video from text...")
9-
result = await client.process(
15+
result = await client.queue.submit_and_poll(
1016
{
1117
"model": models.video("lucy-pro-t2v"),
1218
"prompt": "A serene lake at sunset with mountains in the background",
1319
"seed": 42,
20+
"on_status_change": lambda job: print(f" Status: {job.status}"),
1421
}
1522
)
1623

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

22-
print("Transforming video...")
32+
# Video-to-video transformation
33+
print("\nTransforming video...")
2334
with open("output_t2v.mp4", "rb") as video_file:
24-
result = await client.process(
35+
result = await client.queue.submit_and_poll(
2536
{
2637
"model": models.video("lucy-pro-v2v"),
2738
"prompt": "Anime style with vibrant colors",
2839
"data": video_file,
2940
"enhance_prompt": True,
3041
"num_inference_steps": 50,
42+
"on_status_change": lambda job: print(f" Status: {job.status}"),
3143
}
3244
)
3345

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

3953

4054
if __name__ == "__main__":

0 commit comments

Comments
 (0)