diff --git a/README.md b/README.md index dbfe4e8..d6c6d2f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ For complete documentation, guides, and examples, visit: ## Quick Start -### Process Files +### Image Editing (Process API) ```python import asyncio @@ -32,29 +32,30 @@ from decart import DecartClient, models async def main(): async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: - # Generate a video from text + # Edit an image result = await client.process({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat walking in a lego world", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a painterly oil-on-canvas look while preserving the composition", + "data": open("input.png", "rb"), }) - # Save the result - with open("output.mp4", "wb") as f: + with open("output.png", "wb") as f: f.write(result) asyncio.run(main()) ``` -### Async Processing (Queue API) +### Video Editing (Queue API) -For video generation jobs, use the queue API to submit jobs and poll for results: +For video editing jobs, use the queue API to submit jobs and poll for results: ```python async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: # Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this footage with anime shading and vibrant neon highlights", + "data": open("input.mp4", "rb"), "on_status_change": lambda job: print(f"Status: {job.status}"), }) @@ -71,8 +72,9 @@ Or manage the polling manually: async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client: # Submit the job job = await client.queue.submit({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add cinematic teal-and-orange grading and gentle film grain", + "data": open("input.mp4", "rb"), }) print(f"Job ID: {job.job_id}") @@ -147,8 +149,8 @@ python test_ui.py Then open http://localhost:7860 in your browser. The UI provides tabs for: -- **Image Generation** - Text-to-image and image-to-image transformations -- **Video Generation** - Text-to-video, image-to-video, and video-to-video +- **Image Editing** - Image-to-image edits +- **Video Editing** - Video-to-video edits - **Video Restyle** - Restyle videos using text prompts or reference images - **Tokens** - Create short-lived client tokens diff --git a/decart/client.py b/decart/client.py index a159357..10e0fad 100644 --- a/decart/client.py +++ b/decart/client.py @@ -19,7 +19,7 @@ class DecartClient: """ - Decart API client for video and image generation/transformation. + Decart API client for image editing, video editing, and realtime workflows. Args: api_key: Your Decart API key. Defaults to the DECART_API_KEY environment variable. @@ -35,16 +35,18 @@ class DecartClient: # Option 2: Using DECART_API_KEY environment variable client = DecartClient() - # Image generation (sync) - use process() + # Image editing (sync) - use process() image = await client.process({ - "model": models.image("lucy-pro-t2i"), - "prompt": "A serene lake at sunset", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a painterly oil-on-canvas look while preserving the composition", + "data": open("input.png", "rb"), }) - # Video generation (async) - use queue + # Video editing (async) - use queue result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake at sunset", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this footage with anime shading and vibrant neon highlights", + "data": open("input.mp4", "rb"), }) ``` """ @@ -75,15 +77,16 @@ def __init__( @property def queue(self) -> QueueClient: """ - Queue client for async job-based video generation. + Queue client for async video editing jobs. Only video models support the queue API. Example: ```python # Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this footage with anime shading and vibrant neon highlights", + "data": open("input.mp4", "rb"), }) # Or submit and poll manually @@ -135,16 +138,16 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): async def process(self, options: dict[str, Any]) -> bytes: """ - Process image generation/transformation synchronously. + Process image editing synchronously. Only image models support the process API. - For video generation, use the queue API instead: + For video editing, use the queue API instead: result = await client.queue.submit_and_poll({...}) Args: options: Processing options including model and inputs - model: ImageModelDefinition from models.image() - - prompt: Text prompt for generation + - prompt: Text instructions describing the requested edit - Additional model-specific inputs Returns: diff --git a/decart/models.py b/decart/models.py index 87e1dfd..a63cbe9 100644 --- a/decart/models.py +++ b/decart/models.py @@ -6,16 +6,12 @@ RealTimeModels = Literal["mirage", "mirage_v2", "lucy_v2v_720p_rt", "lucy_2_rt", "live_avatar"] VideoModels = Literal[ - "lucy-dev-i2v", - "lucy-fast-v2v", - "lucy-pro-t2v", - "lucy-pro-i2v", "lucy-pro-v2v", "lucy-motion", "lucy-restyle-v2v", "lucy-2-v2v", ] -ImageModels = Literal["lucy-pro-t2i", "lucy-pro-i2i"] +ImageModels = Literal["lucy-pro-i2i"] Model = Literal[RealTimeModels, VideoModels, ImageModels] # Type variable for model name @@ -46,24 +42,6 @@ class ModelDefinition(DecartBaseModel, Generic[ModelT]): """Type alias for model definitions that support realtime streaming.""" -class TextToVideoInput(BaseModel): - prompt: str = Field(..., min_length=1, max_length=1000) - seed: Optional[int] = None - resolution: Optional[str] = None - orientation: Optional[str] = None - - -class ImageToVideoInput(DecartBaseModel): - prompt: str = Field( - ..., - min_length=1, - max_length=1000, - ) - data: FileInput - seed: Optional[int] = None - resolution: Optional[str] = None - - class VideoToVideoInput(DecartBaseModel): prompt: str = Field( ..., @@ -128,17 +106,6 @@ class VideoEdit2Input(DecartBaseModel): enhance_prompt: Optional[bool] = None -class TextToImageInput(BaseModel): - prompt: str = Field( - ..., - min_length=1, - max_length=1000, - ) - seed: Optional[int] = None - resolution: Optional[str] = None - orientation: Optional[str] = None - - class ImageToImageInput(DecartBaseModel): prompt: str = Field( ..., @@ -195,38 +162,6 @@ class ImageToImageInput(DecartBaseModel): ), }, "video": { - "lucy-dev-i2v": ModelDefinition( - name="lucy-dev-i2v", - url_path="/v1/generate/lucy-dev-i2v", - fps=25, - width=1280, - height=704, - input_schema=ImageToVideoInput, - ), - "lucy-fast-v2v": ModelDefinition( - name="lucy-fast-v2v", - url_path="/v1/generate/lucy-fast-v2v", - fps=25, - width=1280, - height=704, - input_schema=VideoToVideoInput, - ), - "lucy-pro-t2v": ModelDefinition( - name="lucy-pro-t2v", - url_path="/v1/generate/lucy-pro-t2v", - fps=25, - width=1280, - height=704, - input_schema=TextToVideoInput, - ), - "lucy-pro-i2v": ModelDefinition( - name="lucy-pro-i2v", - url_path="/v1/generate/lucy-pro-i2v", - fps=25, - width=1280, - height=704, - input_schema=ImageToVideoInput, - ), "lucy-pro-v2v": ModelDefinition( name="lucy-pro-v2v", url_path="/v1/generate/lucy-pro-v2v", @@ -261,14 +196,6 @@ class ImageToImageInput(DecartBaseModel): ), }, "image": { - "lucy-pro-t2i": ModelDefinition( - name="lucy-pro-t2i", - url_path="/v1/generate/lucy-pro-t2i", - fps=25, - width=1280, - height=704, - input_schema=TextToImageInput, - ), "lucy-pro-i2i": ModelDefinition( name="lucy-pro-i2i", url_path="/v1/generate/lucy-pro-i2i", @@ -297,11 +224,7 @@ def video(model: VideoModels) -> VideoModelDefinition: Video models only support the queue API. Available models: - - "lucy-pro-t2v" - Text-to-video - - "lucy-pro-i2v" - Image-to-video - "lucy-pro-v2v" - Video-to-video - - "lucy-dev-i2v" - Image-to-video (Dev quality) - - "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) @@ -318,7 +241,6 @@ def image(model: ImageModels) -> ImageModelDefinition: Image models only support the process (sync) API. Available models: - - "lucy-pro-t2i" - Text-to-image - "lucy-pro-i2i" - Image-to-image """ try: diff --git a/decart/queue/client.py b/decart/queue/client.py index c06e0ec..f85686b 100644 --- a/decart/queue/client.py +++ b/decart/queue/client.py @@ -25,7 +25,7 @@ class QueueClient: """ - Queue client for async job-based video generation. + Queue client for async job-based video editing. Only video models support the queue API. Jobs are submitted and processed asynchronously, allowing you to @@ -37,15 +37,17 @@ class QueueClient: # Option 1: Submit and poll automatically result = await client.queue.submit_and_poll({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this clip with anime shading and saturated colors", + "data": open("input.mp4", "rb"), "on_status_change": lambda job: print(f"Status: {job.status}"), }) # Option 2: Submit and poll manually job = await client.queue.submit({ - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add cinematic teal-and-orange grading and subtle film grain", + "data": open("input.mp4", "rb"), }) status = await client.queue.status(job.job_id) result = await client.queue.result(job.job_id) @@ -60,14 +62,14 @@ async def _get_session(self) -> aiohttp.ClientSession: async def submit(self, options: dict[str, Any]) -> JobSubmitResponse: """ - Submit a video generation job to the queue for async processing. + Submit a video editing job to the queue for async processing. Only video models are supported. Returns immediately with job_id and initial status. Args: options: Submit options including model and inputs - model: VideoModelDefinition from models.video() - - prompt: Text prompt for generation + - prompt: Text instructions describing the requested edit - Additional model-specific inputs Returns: diff --git a/examples/README.md b/examples/README.md index 4733171..efd8589 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,9 +20,10 @@ export DECART_API_KEY="your-api-key-here" ### Process API -- **`process_video.py`** - Generate and transform videos -- **`process_image.py`** - Generate and transform images +- **`process_video.py`** - Edit a local video with `lucy-pro-v2v` +- **`process_image.py`** - Edit the bundled example image with `lucy-pro-i2i` - **`process_url.py`** - Transform videos from URLs +- **`queue_image_example.py`** - Turn the bundled example image into motion with `lucy-motion` ### Realtime API @@ -37,13 +38,19 @@ pip install decart[realtime] ### Running Examples +`process_image.py` and `queue_image_example.py` use the bundled `examples/files/image.png` asset. +`process_video.py` expects you to place a local video at `examples/assets/example_video.mp4` first. + ```bash -# Generate and transform videos +# Edit a local video (requires examples/assets/example_video.mp4) python examples/process_video.py -# Generate and transform images +# Edit the bundled example image python examples/process_image.py +# Turn the bundled example image into motion +python examples/queue_image_example.py + # Transform video from URL python examples/process_url.py diff --git a/examples/process_image.py b/examples/process_image.py index f1096a6..23b734d 100644 --- a/examples/process_image.py +++ b/examples/process_image.py @@ -1,36 +1,28 @@ import asyncio import os +from pathlib import Path from decart import DecartClient, models async def main() -> None: async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client: - print("Generating image from text...") + # Image-to-image editing + image_path = Path(__file__).parent / "files" / "image.png" + + if not image_path.exists(): + print(f"Missing bundled example image at: {image_path}") + return + + print("Editing image...") result = await client.process( { - "model": models.image("lucy-pro-t2i"), - "prompt": "A futuristic cityscape at night with neon lights", - "seed": 42, - "orientation": "portrait", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply an impressionist oil-painting treatment while keeping the framing intact", + "data": image_path, + "enhance_prompt": True, } ) - with open("output_t2i.png", "wb") as f: - f.write(result) - - print("Image saved to output_t2i.png") - - print("\nTransforming image...") - with open("output_t2i.png", "rb") as image_file: - result = await client.process( - { - "model": models.image("lucy-pro-i2i"), - "prompt": "Oil painting style with impressionist brushstrokes", - "data": image_file, - "enhance_prompt": True, - } - ) - with open("output_i2i.png", "wb") as f: f.write(result) diff --git a/examples/process_video.py b/examples/process_video.py index a6d7940..7113ec2 100644 --- a/examples/process_video.py +++ b/examples/process_video.py @@ -1,47 +1,34 @@ """ -Video generation example using the Queue API. +Video editing example using the Queue API. Video models only support async queue processing. """ import asyncio import os +from pathlib import Path from decart import DecartClient, models async def main() -> None: async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client: - # Text-to-video generation - print("Generating video from text...") + # Video-to-video editing + video_path = Path(__file__).parent / "assets" / "example_video.mp4" + + if not video_path.exists(): + print(f"Please add a video at: {video_path}") + return + + print("Editing video...") result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake at sunset with mountains in the background", - "seed": 42, + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this footage with anime shading, vibrant highlights, and crisp outlines", + "data": video_path, + "enhance_prompt": True, "on_status_change": lambda job: print(f" Status: {job.status}"), } ) - if result.status == "completed": - with open("output_t2v.mp4", "wb") as f: - f.write(result.data) - print("Video saved to output_t2v.mp4") - else: - print(f"Text-to-video failed: {result.error}") - return - - # Video-to-video transformation - print("\nTransforming video...") - with open("output_t2v.mp4", "rb") as video_file: - result = await client.queue.submit_and_poll( - { - "model": models.video("lucy-pro-v2v"), - "prompt": "Anime style with vibrant colors", - "data": video_file, - "enhance_prompt": True, - "on_status_change": lambda job: print(f" Status: {job.status}"), - } - ) - if result.status == "completed": with open("output_v2v.mp4", "wb") as f: f.write(result.data) diff --git a/examples/queue_example.py b/examples/queue_example.py index 20b3d04..241fd46 100644 --- a/examples/queue_example.py +++ b/examples/queue_example.py @@ -1,16 +1,24 @@ import asyncio import os +from pathlib import Path from decart import DecartClient, models async def main() -> None: + video_path = Path(__file__).parent / "assets" / "example_video.mp4" + + if not video_path.exists(): + print(f"Please add a video at: {video_path}") + return + async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client: # Automatic polling - submits and waits for completion print("Submitting job with automatic polling...") result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake at sunset with mountains in the background", + "model": models.video("lucy-pro-v2v"), + "prompt": "Give this clip a cinematic dusk grade with cooler shadows and warm highlights", + "data": video_path, "resolution": "480p", "on_status_change": lambda job: print(f"Job {job.job_id}: {job.status}"), } @@ -27,8 +35,9 @@ async def main() -> None: print("\nSubmitting job with manual polling...") job = await client.queue.submit( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat playing piano in a cozy living room", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle the scene to feel like stop-motion miniatures with soft practical lighting", + "data": video_path, "resolution": "480p", } ) diff --git a/examples/queue_image_example.py b/examples/queue_image_example.py index 0e53bc3..06708fe 100644 --- a/examples/queue_image_example.py +++ b/examples/queue_image_example.py @@ -5,24 +5,28 @@ async def main() -> None: - # Load image from assets folder - image_path = Path(__file__).parent / "assets" / "example_asset.png" + # Load the bundled example image + image_path = Path(__file__).parent / "files" / "image.png" if not image_path.exists(): - print(f"Please add an image at: {image_path}") + print(f"Missing bundled example image at: {image_path}") return async with DecartClient(api_key=os.getenv("DECART_API_KEY", "your-api-key-here")) as client: print(f"Loading image: {image_path}") # Manual polling - submit and poll yourself - print("Submitting job...") + print("Submitting image-to-motion job...") job = await client.queue.submit( { - "model": models.video("lucy-pro-i2v"), - "prompt": "The image comes to life with gentle motion", + "model": models.video("lucy-motion"), "data": image_path, "resolution": "480p", + "trajectory": [ + {"frame": 0, "x": 0.35, "y": 0.5}, + {"frame": 30, "x": 0.5, "y": 0.45}, + {"frame": 60, "x": 0.65, "y": 0.5}, + ], } ) print(f"Job submitted: {job.job_id}") @@ -39,9 +43,9 @@ async def main() -> None: if status.status == "completed": print("Fetching result...") data = await client.queue.result(job.job_id) - with open("output_i2v.mp4", "wb") as f: + with open("output_motion.mp4", "wb") as f: f.write(data) - print("Video saved to output_i2v.mp4") + print("Video saved to output_motion.mp4") else: print("Job failed") diff --git a/examples/video_restyle.py b/examples/video_restyle.py index dd639d1..ab6edb6 100644 --- a/examples/video_restyle.py +++ b/examples/video_restyle.py @@ -6,7 +6,7 @@ Usage: # With text prompt: - DECART_API_KEY=your-key python video_restyle.py input.mp4 --prompt "anime style" + DECART_API_KEY=your-key python video_restyle.py input.mp4 --prompt "add anime shading and neon highlights" # With reference image: DECART_API_KEY=your-key python video_restyle.py input.mp4 --reference style.png @@ -32,7 +32,7 @@ async def main(): parser.add_argument( "--prompt", "-p", - help="Text prompt describing the style (e.g., 'anime style', 'oil painting')", + help="Text prompt describing the edit (e.g., 'add anime shading', 'apply an oil-painting look')", ) parser.add_argument("--reference", "-r", help="Path to reference image for style transfer") parser.add_argument("--output", "-o", help="Output file path (default: output_restyle.mp4)") diff --git a/test_ui.py b/test_ui.py index ab8e6b4..19aec05 100644 --- a/test_ui.py +++ b/test_ui.py @@ -31,34 +31,6 @@ def get_client(api_key: str) -> DecartClient: # ============================================================================ -async def process_text_to_image( - api_key: str, - prompt: str, - seed: Optional[int], - resolution: str, - orientation: str, -) -> tuple[Optional[bytes], str]: - """Generate an image from text prompt.""" - try: - client = get_client(api_key) - - options = { - "model": models.image("lucy-pro-t2i"), - "prompt": prompt, - } - if seed: - options["seed"] = seed - if resolution and resolution != "default": - options["resolution"] = resolution - if orientation and orientation != "default": - options["orientation"] = orientation - - result = await client.process(options) - return result, f"Success! Generated image from prompt: '{prompt[:50]}...'" - except Exception as e: - return None, f"Error: {str(e)}" - - async def process_image_to_image( api_key: str, prompt: str, @@ -66,7 +38,7 @@ async def process_image_to_image( seed: Optional[int], strength: float, ) -> tuple[Optional[bytes], str]: - """Transform an image with a prompt.""" + """Edit an image with text instructions.""" try: if not input_image: return None, "Please upload an image" @@ -84,7 +56,7 @@ async def process_image_to_image( options["strength"] = strength result = await client.process(options) - return result, f"Success! Transformed image with prompt: '{prompt[:50]}...'" + return result, f"Success! Edited image with instructions: '{prompt[:50]}...'" except Exception as e: return None, f"Error: {str(e)}" @@ -94,52 +66,6 @@ async def process_image_to_image( # ============================================================================ -async def process_video_t2v( - api_key: str, - prompt: str, - seed: Optional[int], - enhance_prompt: bool, - progress=gr.Progress(), -) -> tuple[Optional[str], str]: - """Generate a video from text prompt.""" - try: - client = get_client(api_key) - - options = { - "model": models.video("lucy-pro-t2v"), - "prompt": prompt, - } - if seed: - options["seed"] = seed - if enhance_prompt is not None: - options["enhance_prompt"] = enhance_prompt - - progress(0.1, desc="Submitting job...") - - def on_status_change(job): - if job.status == "pending": - progress(0.2, desc="Job pending...") - elif job.status == "processing": - progress(0.5, desc="Processing video...") - - options["on_status_change"] = on_status_change - - result = await client.queue.submit_and_poll(options) - - if result.status == "failed": - return None, f"Job failed: {result.error}" - - progress(0.9, desc="Saving video...") - - # Save to temp file - with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f: - f.write(result.data) - return f.name, f"Success! Generated video from prompt: '{prompt[:50]}...'" - - except Exception as e: - return None, f"Error: {str(e)}" - - async def process_video_v2v( api_key: str, prompt: str, @@ -148,7 +74,7 @@ async def process_video_v2v( enhance_prompt: bool, progress=gr.Progress(), ) -> tuple[Optional[str], str]: - """Transform a video with a prompt.""" + """Edit a video with text instructions.""" try: if not input_video: return None, "Please upload a video" @@ -184,7 +110,7 @@ def on_status_change(job): with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f: f.write(result.data) - return f.name, f"Success! Transformed video with prompt: '{prompt[:50]}...'" + return f.name, f"Success! Edited video with instructions: '{prompt[:50]}...'" except Exception as e: return None, f"Error: {str(e)}" @@ -252,56 +178,6 @@ def on_status_change(job): return None, f"Error: {str(e)}" -async def process_video_i2v( - api_key: str, - prompt: str, - input_image: str, - seed: Optional[int], - enhance_prompt: bool, - progress=gr.Progress(), -) -> tuple[Optional[str], str]: - """Generate a video from an image.""" - try: - if not input_image: - return None, "Please upload an image" - - client = get_client(api_key) - - options = { - "model": models.video("lucy-pro-i2v"), - "prompt": prompt, - "data": Path(input_image), - } - if seed: - options["seed"] = seed - if enhance_prompt is not None: - options["enhance_prompt"] = enhance_prompt - - progress(0.1, desc="Submitting job...") - - def on_status_change(job): - if job.status == "pending": - progress(0.2, desc="Job pending...") - elif job.status == "processing": - progress(0.5, desc="Processing video...") - - options["on_status_change"] = on_status_change - - result = await client.queue.submit_and_poll(options) - - if result.status == "failed": - return None, f"Job failed: {result.error}" - - progress(0.9, desc="Saving video...") - - with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f: - f.write(result.data) - return f.name, "Success! Generated video from image" - - except Exception as e: - return None, f"Error: {str(e)}" - - # ============================================================================ # Tokens API # ============================================================================ @@ -354,46 +230,14 @@ def create_ui(): # ================================================================ # Image Processing Tab # ================================================================ - with gr.TabItem("Image Generation"): - gr.Markdown("### Text to Image") - with gr.Row(): - with gr.Column(): - t2i_prompt = gr.Textbox( - label="Prompt", - placeholder="A beautiful sunset over mountains", - lines=3, - ) - with gr.Row(): - t2i_seed = gr.Number(label="Seed (optional)", precision=0) - t2i_resolution = gr.Dropdown( - label="Resolution", - choices=["default", "720p", "1080p"], - value="default", - ) - t2i_orientation = gr.Dropdown( - label="Orientation", - choices=["default", "landscape", "portrait", "square"], - value="default", - ) - t2i_btn = gr.Button("Generate Image", variant="primary") - with gr.Column(): - t2i_output = gr.Image(label="Generated Image", type="filepath") - t2i_status = gr.Textbox(label="Status", interactive=False) - - t2i_btn.click( - fn=lambda *args: asyncio.run(process_text_to_image(*args)), - inputs=[api_key, t2i_prompt, t2i_seed, t2i_resolution, t2i_orientation], - outputs=[t2i_output, t2i_status], - ) - - gr.Markdown("---") + with gr.TabItem("Image Editing"): gr.Markdown("### Image to Image") with gr.Row(): with gr.Column(): i2i_input = gr.Image(label="Input Image", type="filepath") i2i_prompt = gr.Textbox( label="Prompt", - placeholder="Make it look like anime", + placeholder="Apply a painterly anime key-art treatment", lines=2, ) with gr.Row(): @@ -405,9 +249,9 @@ def create_ui(): value=0.75, step=0.05, ) - i2i_btn = gr.Button("Transform Image", variant="primary") + i2i_btn = gr.Button("Edit Image", variant="primary") with gr.Column(): - i2i_output = gr.Image(label="Transformed Image", type="filepath") + i2i_output = gr.Image(label="Edited Image", type="filepath") i2i_status = gr.Textbox(label="Status", interactive=False) i2i_btn.click( @@ -419,69 +263,22 @@ def create_ui(): # ================================================================ # Video Processing Tab # ================================================================ - with gr.TabItem("Video Generation"): - gr.Markdown("### Text to Video") - with gr.Row(): - with gr.Column(): - t2v_prompt = gr.Textbox( - label="Prompt", - placeholder="A cat walking in a park", - lines=3, - ) - with gr.Row(): - t2v_seed = gr.Number(label="Seed (optional)", precision=0) - t2v_enhance = gr.Checkbox(label="Enhance Prompt", value=True) - t2v_btn = gr.Button("Generate Video", variant="primary") - with gr.Column(): - t2v_output = gr.Video(label="Generated Video") - t2v_status = gr.Textbox(label="Status", interactive=False) - - t2v_btn.click( - fn=lambda *args: asyncio.run(process_video_t2v(*args)), - inputs=[api_key, t2v_prompt, t2v_seed, t2v_enhance], - outputs=[t2v_output, t2v_status], - ) - - gr.Markdown("---") - gr.Markdown("### Image to Video") - with gr.Row(): - with gr.Column(): - i2v_input = gr.Image(label="Input Image", type="filepath") - i2v_prompt = gr.Textbox( - label="Prompt", - placeholder="The scene comes to life", - lines=2, - ) - with gr.Row(): - i2v_seed = gr.Number(label="Seed (optional)", precision=0) - i2v_enhance = gr.Checkbox(label="Enhance Prompt", value=True) - i2v_btn = gr.Button("Generate Video", variant="primary") - with gr.Column(): - i2v_output = gr.Video(label="Generated Video") - i2v_status = gr.Textbox(label="Status", interactive=False) - - i2v_btn.click( - fn=lambda *args: asyncio.run(process_video_i2v(*args)), - inputs=[api_key, i2v_prompt, i2v_input, i2v_seed, i2v_enhance], - outputs=[i2v_output, i2v_status], - ) - - gr.Markdown("---") + with gr.TabItem("Video Editing"): gr.Markdown("### Video to Video") with gr.Row(): with gr.Column(): v2v_input = gr.Video(label="Input Video") v2v_prompt = gr.Textbox( label="Prompt", - placeholder="Make it look like Lego world", + placeholder="Restyle this footage with toy-like miniatures and warm practical lighting", lines=2, ) with gr.Row(): v2v_seed = gr.Number(label="Seed (optional)", precision=0) v2v_enhance = gr.Checkbox(label="Enhance Prompt", value=True) - v2v_btn = gr.Button("Transform Video", variant="primary") + v2v_btn = gr.Button("Edit Video", variant="primary") with gr.Column(): - v2v_output = gr.Video(label="Transformed Video") + v2v_output = gr.Video(label="Edited Video") v2v_status = gr.Textbox(label="Status", interactive=False) v2v_btn.click( @@ -513,7 +310,7 @@ def create_ui(): ) restyle_prompt = gr.Textbox( label="Prompt", - placeholder="Make it look like anime", + placeholder="Add anime shading with vivid neon accents", lines=2, visible=True, ) diff --git a/tests/test_models.py b/tests/test_models.py index d4dfd67..9e92c9b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -27,12 +27,9 @@ def test_realtime_models() -> None: def test_video_models() -> None: - model = models.video("lucy-pro-t2v") - assert model.name == "lucy-pro-t2v" - assert model.url_path == "/v1/generate/lucy-pro-t2v" - model = models.video("lucy-pro-v2v") assert model.name == "lucy-pro-v2v" + assert model.url_path == "/v1/generate/lucy-pro-v2v" # lucy-restyle-v2v model model = models.video("lucy-restyle-v2v") @@ -41,9 +38,9 @@ def test_video_models() -> None: def test_image_models() -> None: - model = models.image("lucy-pro-t2i") - assert model.name == "lucy-pro-t2i" - assert model.url_path == "/v1/generate/lucy-pro-t2i" + model = models.image("lucy-pro-i2i") + assert model.name == "lucy-pro-i2i" + assert model.url_path == "/v1/generate/lucy-pro-i2i" def test_lucy_2_v2v_model() -> None: diff --git a/tests/test_process.py b/tests/test_process.py index e479378..736bd7e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,6 +1,6 @@ """ Tests for the process API. -Note: process() only supports image models (t2i, i2i). +Note: process() only supports image models (i2i). Video models must use the queue API. """ @@ -10,35 +10,6 @@ from decart import DecartClient, models, DecartSDKError -@pytest.mark.asyncio -async def test_process_text_to_image() -> None: - """Test text-to-image generation with process API.""" - client = DecartClient(api_key="test-key") - - with patch("aiohttp.ClientSession") as mock_session_cls: - mock_response = MagicMock() - mock_response.ok = True - mock_response.read = AsyncMock(return_value=b"fake image data") - - mock_session = MagicMock() - mock_session.__aenter__ = AsyncMock(return_value=mock_session) - mock_session.__aexit__ = AsyncMock(return_value=None) - mock_session.post = MagicMock() - mock_session.post.return_value.__aenter__ = AsyncMock(return_value=mock_response) - mock_session.post.return_value.__aexit__ = AsyncMock(return_value=None) - - mock_session_cls.return_value = mock_session - - result = await client.process( - { - "model": models.image("lucy-pro-t2i"), - "prompt": "A cat walking", - } - ) - - assert result == b"fake image data" - - @pytest.mark.asyncio async def test_process_image_to_image() -> None: """Test image-to-image transformation with process API.""" @@ -61,7 +32,7 @@ async def test_process_image_to_image() -> None: result = await client.process( { "model": models.image("lucy-pro-i2i"), - "prompt": "Oil painting style", + "prompt": "Apply an oil-painting treatment while preserving the composition", "data": b"fake input image", "enhance_prompt": True, } @@ -78,8 +49,8 @@ async def test_process_rejects_video_models() -> None: with pytest.raises(DecartSDKError) as exc_info: await client.process( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat walking", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add cinematic teal-and-orange grading", } ) @@ -94,7 +65,7 @@ async def test_process_missing_model() -> None: with pytest.raises(DecartSDKError): await client.process( { - "prompt": "A cat walking", + "prompt": "Apply an editorial color grade", } ) @@ -120,11 +91,12 @@ async def test_process_max_prompt_length() -> None: with pytest.raises(DecartSDKError) as exception: await client.process( { - "model": models.image("lucy-pro-t2i"), + "model": models.image("lucy-pro-i2i"), "prompt": prompt, + "data": b"fake image data", } ) - assert "Invalid inputs for lucy-pro-t2i: 1 validation error for TextToImageInput" in str( + assert "Invalid inputs for lucy-pro-i2i: 1 validation error for ImageToImageInput" in str( exception ) @@ -140,8 +112,9 @@ async def test_process_with_cancellation() -> None: with pytest.raises(asyncio.CancelledError): await client.process( { - "model": models.image("lucy-pro-t2i"), - "prompt": "An image that will be cancelled", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a high-contrast editorial treatment", + "data": b"fake image data", "cancel_token": cancel_token, } ) @@ -168,8 +141,9 @@ async def test_process_includes_user_agent_header() -> None: await client.process( { - "model": models.image("lucy-pro-t2i"), - "prompt": "Test prompt", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a soft watercolor treatment", + "data": b"fake image data", } ) @@ -204,8 +178,9 @@ async def test_process_includes_integration_in_user_agent() -> None: await client.process( { - "model": models.image("lucy-pro-t2i"), - "prompt": "Test prompt", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a soft watercolor treatment", + "data": b"fake image data", } ) diff --git a/tests/test_queue.py b/tests/test_queue.py index dd04aac..f09aef4 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -10,8 +10,8 @@ @pytest.mark.asyncio -async def test_queue_submit_text_to_video() -> None: - """Test text-to-video submission with queue API.""" +async def test_queue_submit_video_to_video_basic() -> None: + """Test video-to-video submission with queue API.""" client = DecartClient(api_key="test-key") with patch("decart.queue.client.submit_job") as mock_submit: @@ -19,8 +19,9 @@ async def test_queue_submit_text_to_video() -> None: job = await client.queue.submit( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A cat walking in a park", + "model": models.video("lucy-pro-v2v"), + "prompt": "Restyle this clip with a cinematic dusk grade", + "data": b"fake video data", "seed": 42, } ) @@ -59,8 +60,8 @@ async def test_queue_rejects_image_models() -> None: with pytest.raises(DecartSDKError) as exc_info: await client.queue.submit( { - "model": models.image("lucy-pro-t2i"), - "prompt": "A beautiful sunset", + "model": models.image("lucy-pro-i2i"), + "prompt": "Apply a painterly sunset color grade", } ) @@ -76,7 +77,7 @@ async def test_queue_missing_model() -> None: with pytest.raises(DecartSDKError): await client.queue.submit( { - "prompt": "A cat walking", + "prompt": "Apply a cinematic grade", } ) @@ -128,8 +129,9 @@ async def test_queue_submit_and_poll_completed() -> None: result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add anime shading and crisp outlines", + "data": b"fake video data", } ) @@ -153,8 +155,9 @@ async def test_queue_submit_and_poll_failed() -> None: result = await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add anime shading and crisp outlines", + "data": b"fake video data", } ) @@ -187,8 +190,9 @@ def on_status_change(job): await client.queue.submit_and_poll( { - "model": models.video("lucy-pro-t2v"), - "prompt": "A serene lake", + "model": models.video("lucy-pro-v2v"), + "prompt": "Add anime shading and crisp outlines", + "data": b"fake video data", "on_status_change": on_status_change, } ) @@ -221,12 +225,13 @@ async def test_queue_submit_max_prompt_length() -> None: with pytest.raises(DecartSDKError) as exception: await client.queue.submit( { - "model": models.video("lucy-pro-t2v"), + "model": models.video("lucy-pro-v2v"), "prompt": prompt, + "data": b"fake video data", } ) - assert "Invalid inputs for lucy-pro-t2v" in str(exception) + assert "Invalid inputs for lucy-pro-v2v" in str(exception) @pytest.mark.asyncio @@ -250,8 +255,9 @@ async def test_queue_includes_user_agent_header() -> None: await client.queue.submit( { - "model": models.video("lucy-pro-t2v"), - "prompt": "Test prompt", + "model": models.video("lucy-pro-v2v"), + "prompt": "Apply a cinematic grade", + "data": b"fake video data", } ) @@ -276,7 +282,7 @@ async def test_queue_lucy2_v2v_with_prompt() -> None: job = await client.queue.submit( { "model": models.video("lucy-2-v2v"), - "prompt": "Transform the scene", + "prompt": "Restyle the scene with softer contrast and warmer highlights", "data": b"fake video data", "enhance_prompt": True, "seed": 42, diff --git a/uv.lock b/uv.lock index 23179f7..a6b90af 100644 --- a/uv.lock +++ b/uv.lock @@ -597,7 +597,7 @@ wheels = [ [[package]] name = "decart" -version = "0.0.28" +version = "0.0.29" source = { editable = "." } dependencies = [ { name = "aiofiles" },