| | """ |
| | Video Service |
| | |
| | High-level service for video generation. |
| | Abstracts all API complexity from the UI layer. |
| | """ |
| |
|
| | from typing import Callable, Optional, List |
| | from dataclasses import dataclass |
| |
|
| | from ..api.client import StackNetClient |
| |
|
| |
|
| | @dataclass |
| | class GeneratedVideo: |
| | """Generated video result.""" |
| | video_url: str |
| | video_path: Optional[str] = None |
| | thumbnail_url: Optional[str] = None |
| | duration: Optional[float] = None |
| | prompt: Optional[str] = None |
| |
|
| |
|
| | class VideoService: |
| | """ |
| | Service for video generation. |
| | |
| | Provides clean interfaces for: |
| | - Text-to-video generation |
| | - Image-to-video animation |
| | """ |
| |
|
| | def __init__(self, client: Optional[StackNetClient] = None): |
| | self.client = client or StackNetClient() |
| |
|
| | async def generate_video( |
| | self, |
| | prompt: str, |
| | duration: int = 10, |
| | style: Optional[str] = None, |
| | on_progress: Optional[Callable[[float, str], None]] = None |
| | ) -> List[GeneratedVideo]: |
| | """ |
| | Generate video from a text prompt using generate_video_2 tool. |
| | |
| | Args: |
| | prompt: Description of desired video |
| | duration: Target duration in seconds |
| | style: Style preset (Cinematic, Animation, etc.) |
| | on_progress: Callback for progress updates |
| | |
| | Returns: |
| | List of generated videos |
| | """ |
| | full_prompt = prompt |
| | if style and style != "Cinematic": |
| | full_prompt = f"{prompt}, {style.lower()} style" |
| |
|
| | result = await self.client.submit_tool_task( |
| | tool_name="generate_video_2", |
| | parameters={ |
| | "prompt": full_prompt, |
| | "duration": duration |
| | }, |
| | on_progress=on_progress |
| | ) |
| |
|
| | if not result.success: |
| | raise Exception(result.error or "Video generation failed") |
| |
|
| | return self._parse_video_result(result.data, prompt) |
| |
|
| | async def animate_image( |
| | self, |
| | image_url: str, |
| | motion_prompt: str, |
| | duration: int = 5, |
| | on_progress: Optional[Callable[[float, str], None]] = None |
| | ) -> List[GeneratedVideo]: |
| | """ |
| | Animate a static image into video using generate_image_to_video_2 tool. |
| | |
| | Args: |
| | image_url: URL to source image |
| | motion_prompt: Description of desired motion |
| | duration: Target duration in seconds |
| | on_progress: Progress callback |
| | |
| | Returns: |
| | List of animated videos |
| | """ |
| | result = await self.client.submit_tool_task( |
| | tool_name="generate_image_to_video_2", |
| | parameters={ |
| | "prompt": motion_prompt, |
| | "image_url": image_url, |
| | "duration": duration |
| | }, |
| | on_progress=on_progress |
| | ) |
| |
|
| | if not result.success: |
| | raise Exception(result.error or "Image animation failed") |
| |
|
| | return self._parse_video_result(result.data, motion_prompt) |
| |
|
| | def _parse_video_result(self, data: dict, prompt: str) -> List[GeneratedVideo]: |
| | """Parse API response into GeneratedVideo objects.""" |
| | videos = [] |
| |
|
| | |
| | raw_videos = data.get("videos", []) |
| |
|
| | if not raw_videos: |
| | |
| | video_url = ( |
| | data.get("video_url") or |
| | data.get("videoUrl") or |
| | data.get("url") or |
| | data.get("content") |
| | ) |
| | if video_url: |
| | raw_videos = [{"url": video_url}] |
| |
|
| | for vid_data in raw_videos: |
| | if isinstance(vid_data, str): |
| | video_url = vid_data |
| | else: |
| | video_url = ( |
| | vid_data.get("url") or |
| | vid_data.get("video_url") or |
| | vid_data.get("videoUrl") |
| | ) |
| |
|
| | if video_url: |
| | videos.append(GeneratedVideo( |
| | video_url=video_url, |
| | thumbnail_url=vid_data.get("thumbnail") if isinstance(vid_data, dict) else None, |
| | duration=vid_data.get("duration") if isinstance(vid_data, dict) else None, |
| | prompt=prompt |
| | )) |
| |
|
| | return videos |
| |
|
| | async def download_video(self, video: GeneratedVideo) -> str: |
| | """Download a video to local file.""" |
| | if video.video_path: |
| | return video.video_path |
| |
|
| | |
| | url = video.video_url |
| | if ".webm" in url: |
| | ext = ".webm" |
| | elif ".mov" in url: |
| | ext = ".mov" |
| | else: |
| | ext = ".mp4" |
| |
|
| | filename = f"video_{hash(url) % 10000}{ext}" |
| | video.video_path = await self.client.download_file(url, filename) |
| | return video.video_path |
| |
|
| | def cleanup(self): |
| | """Clean up temporary files.""" |
| | self.client.cleanup() |
| |
|