-
Notifications
You must be signed in to change notification settings - Fork 230
feat: add MuAPI video generation tool #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import os | ||
| import time | ||
| import requests | ||
| from typing import Optional | ||
|
|
||
|
|
||
| PARAMS_CONFIG = { | ||
| "text_to_video": { | ||
| "model_name": { | ||
| "type": "string", | ||
| "description": "The muapi.ai model to use for text-to-video generation", | ||
| "default": "veo3-fast", | ||
| "enum": [ | ||
| "veo3", | ||
| "veo3-fast", | ||
| "kling-master", | ||
| "wan2.1", | ||
| "wan2.2", | ||
| "seedance-pro", | ||
| "seedance-pro-fast", | ||
| "runway", | ||
| "pixverse", | ||
| "hunyuan", | ||
| "minimax-hailuo-02-std", | ||
| "minimax-hailuo-02-pro", | ||
| ], | ||
| }, | ||
| "duration": { | ||
| "type": "integer", | ||
| "description": "Duration of the video in seconds", | ||
| "default": 5, | ||
| "minimum": 3, | ||
| "maximum": 60, | ||
| }, | ||
| "aspect_ratio": { | ||
| "type": "string", | ||
| "description": "Aspect ratio of the generated video", | ||
| "default": "16:9", | ||
| "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], | ||
| }, | ||
| }, | ||
| "image_to_video": { | ||
| "model_name": { | ||
| "type": "string", | ||
| "description": "The muapi.ai model to use for image-to-video generation", | ||
| "default": "kling-master", | ||
| "enum": [ | ||
| "kling-master", | ||
| "kling-v2.5-pro", | ||
| "wan2.1", | ||
| "wan2.2", | ||
| "seedance-pro", | ||
| "seedance-pro-fast", | ||
| "runway", | ||
| "pixverse", | ||
| "hunyuan", | ||
| "vidu", | ||
| ], | ||
| }, | ||
| "duration": { | ||
| "type": "integer", | ||
| "description": "Duration of the video in seconds", | ||
| "default": 5, | ||
| "minimum": 3, | ||
| "maximum": 60, | ||
| }, | ||
| "aspect_ratio": { | ||
| "type": "string", | ||
| "description": "Aspect ratio of the generated video", | ||
| "default": "16:9", | ||
| "enum": ["16:9", "9:16", "1:1", "4:3", "3:4"], | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| # Endpoints are the model IDs themselves for muapi | ||
| T2V_ENDPOINTS = { | ||
| "veo3": "veo3", | ||
| "veo3-fast": "veo3-fast", | ||
| "kling-master": "kling", | ||
| "wan2.1": "wan2.1", | ||
| "wan2.2": "wan2.2", | ||
| "seedance-pro": "seedance-pro", | ||
| "seedance-pro-fast": "seedance-pro-fast", | ||
| "runway": "runway", | ||
| "pixverse": "pixverse", | ||
| "hunyuan": "hunyuan", | ||
| "minimax-hailuo-02-std": "minimax-hailuo-02-std", | ||
| "minimax-hailuo-02-pro": "minimax-hailuo-02-pro", | ||
| } | ||
|
|
||
| I2V_ENDPOINTS = { | ||
| "kling-master": "kling-i2v", | ||
| "kling-v2.5-pro": "kling-v2.5-pro-i2v", | ||
| "wan2.1": "wan2.1-i2v", | ||
| "wan2.2": "wan2.2-i2v", | ||
| "seedance-pro": "seedance-pro-i2v", | ||
| "seedance-pro-fast": "seedance-pro-fast-i2v", | ||
| "runway": "runway-i2v", | ||
| "pixverse": "pixverse-i2v", | ||
| "hunyuan": "hunyuan-i2v", | ||
| "vidu": "vidu-i2v", | ||
| } | ||
|
|
||
|
|
||
| class MuApiVideoGenerationTool: | ||
| """Video generation tool using muapi.ai's 400+ model aggregator API.""" | ||
|
|
||
| BASE_URL = "https://api.muapi.ai/api/v1" | ||
| POLL_INTERVAL = 5 # seconds | ||
|
|
||
| def __init__(self, api_key: str): | ||
| if not api_key: | ||
| raise Exception( | ||
| "MUAPI_API_KEY not found. Get one at https://muapi.ai/dashboard/api-keys" | ||
| ) | ||
| self.api_key = api_key | ||
| self.session = requests.Session() | ||
| self.session.headers.update( | ||
| {"x-api-key": self.api_key, "Content-Type": "application/json"} | ||
| ) | ||
|
|
||
| def _submit(self, endpoint: str, payload: dict) -> str: | ||
| """Submit a generation request and return the request_id.""" | ||
| resp = self.session.post(f"{self.BASE_URL}/{endpoint}", json=payload, timeout=30) | ||
| resp.raise_for_status() | ||
| return resp.json()["request_id"] | ||
|
|
||
| def _poll(self, request_id: str, timeout: int = 600) -> str: | ||
| """Poll until the generation completes and return the output URL.""" | ||
| deadline = time.time() + timeout | ||
| while time.time() < deadline: | ||
| resp = self.session.get( | ||
| f"{self.BASE_URL}/predictions/{request_id}/result", timeout=15 | ||
| ) | ||
| resp.raise_for_status() | ||
| data = resp.json() | ||
| status = data.get("status", "pending") | ||
| if status == "completed": | ||
| outputs = data.get("outputs", []) | ||
| if not outputs: | ||
| raise Exception("Completed but no outputs returned") | ||
| return outputs[0] | ||
| if status in ("failed", "cancelled"): | ||
| raise Exception(f"Video generation {status}: {data.get('error', '')}") | ||
| time.sleep(self.POLL_INTERVAL) | ||
| raise Exception(f"Video generation timed out after {timeout}s") | ||
|
|
||
| def text_to_video( | ||
| self, prompt: str, save_at: str, duration: float, config: dict | ||
| ) -> dict: | ||
| """Generate a video from a text prompt using muapi.ai.""" | ||
| model_name = config.get("model_name", "veo3-fast") | ||
| endpoint = T2V_ENDPOINTS.get(model_name, model_name) | ||
|
|
||
| payload = { | ||
| "prompt": prompt, | ||
| "duration": int(duration), | ||
| "aspect_ratio": config.get("aspect_ratio", "16:9"), | ||
| } | ||
|
|
||
| try: | ||
| request_id = self._submit(endpoint, payload) | ||
| video_url = self._poll(request_id) | ||
| video_data = requests.get(video_url, timeout=120) | ||
| video_data.raise_for_status() | ||
| with open(save_at, "wb") as f: | ||
| f.write(video_data.content) | ||
| except Exception as e: | ||
| raise Exception(f"Error generating video: {type(e).__name__}: {str(e)}") | ||
|
|
||
| return {"status": "success", "video_path": save_at} | ||
|
Comment on lines
+162
to
+172
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Preserve exception chain with The re-raised exception loses the original traceback, making debugging harder. Using exception chaining preserves the full context. The same issue applies to Proposed fix for both methods except Exception as e:
- raise Exception(f"Error generating video: {type(e).__name__}: {str(e)}")
+ raise Exception(f"Error generating video: {type(e).__name__}: {e}") from eApply to both line 170 and line 202. 🧰 Tools🪛 Ruff (0.15.17)[warning] 169-169: Do not catch blind exception: (BLE001) [warning] 170-170: Within an (B904) [warning] 170-170: Use explicit conversion flag Replace with conversion flag (RUF010) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| def image_to_video( | ||
| self, | ||
| image_url: str, | ||
| save_at: str, | ||
| duration: float, | ||
| config: dict, | ||
| prompt: Optional[str] = None, | ||
| ) -> dict: | ||
| """Generate a video from an image URL using muapi.ai.""" | ||
| model_name = config.get("model_name", "kling-master") | ||
| endpoint = I2V_ENDPOINTS.get(model_name, f"{model_name}-i2v") | ||
|
|
||
| payload: dict = { | ||
| "image_url": image_url, | ||
| "duration": int(duration), | ||
| "aspect_ratio": config.get("aspect_ratio", "16:9"), | ||
| } | ||
| if prompt: | ||
| payload["prompt"] = prompt | ||
|
|
||
| try: | ||
| request_id = self._submit(endpoint, payload) | ||
| video_url = self._poll(request_id) | ||
| video_data = requests.get(video_url, timeout=120) | ||
| video_data.raise_for_status() | ||
| with open(save_at, "wb") as f: | ||
| f.write(video_data.content) | ||
| except Exception as e: | ||
| raise Exception(f"Error generating video: {type(e).__name__}: {str(e)}") | ||
|
|
||
| return {"status": "success", "video_path": save_at} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle missing
request_idin API response.If the API response doesn't contain the expected
request_idkey (e.g., due to an API change or unexpected error format), a rawKeyErroris raised without context. Adding explicit handling improves debuggability.Proposed fix
def _submit(self, endpoint: str, payload: dict) -> str: """Submit a generation request and return the request_id.""" resp = self.session.post(f"{self.BASE_URL}/{endpoint}", json=payload, timeout=30) resp.raise_for_status() - return resp.json()["request_id"] + data = resp.json() + if "request_id" not in data: + raise Exception(f"API response missing 'request_id': {data}") + return data["request_id"]📝 Committable suggestion
🤖 Prompt for AI Agents