190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MiniMax API client - shared HTTP utilities for all MiniMax generation tasks.
|
|
|
|
Handles authentication, API calls, async task polling, and file downloads.
|
|
Base URL: https://api.minimax.io/v1
|
|
Auth: Bearer token via MINIMAX_API_KEY environment variable.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("Error: requests package not installed")
|
|
print("Install with: pip install requests")
|
|
sys.exit(1)
|
|
|
|
# Import centralized environment resolver
|
|
CLAUDE_ROOT = Path(__file__).parent.parent.parent.parent
|
|
sys.path.insert(0, str(CLAUDE_ROOT / 'scripts'))
|
|
try:
|
|
from resolve_env import resolve_env
|
|
CENTRALIZED_RESOLVER_AVAILABLE = True
|
|
except ImportError:
|
|
CENTRALIZED_RESOLVER_AVAILABLE = False
|
|
|
|
BASE_URL = "https://api.minimax.io/v1"
|
|
|
|
|
|
def find_minimax_api_key() -> Optional[str]:
|
|
"""Find MINIMAX_API_KEY using centralized resolver or environment."""
|
|
if CENTRALIZED_RESOLVER_AVAILABLE:
|
|
return resolve_env('MINIMAX_API_KEY', skill='ai-multimodal')
|
|
|
|
# Fallback: check environment and .env files
|
|
api_key = os.getenv('MINIMAX_API_KEY')
|
|
if api_key:
|
|
return api_key
|
|
|
|
# Check .env files in skill directory hierarchy
|
|
try:
|
|
from dotenv import load_dotenv
|
|
skill_dir = Path(__file__).parent.parent
|
|
for env_path in [skill_dir / '.env', skill_dir.parent / '.env']:
|
|
if env_path.exists():
|
|
load_dotenv(env_path, override=True)
|
|
api_key = os.getenv('MINIMAX_API_KEY')
|
|
if api_key:
|
|
return api_key
|
|
except ImportError:
|
|
pass
|
|
|
|
return None
|
|
|
|
|
|
def get_headers(api_key: str) -> Dict[str, str]:
|
|
"""Build authorization headers for MiniMax API."""
|
|
return {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
|
|
def api_post(endpoint: str, payload: Dict[str, Any], api_key: str,
|
|
verbose: bool = False, timeout: int = 120) -> Dict[str, Any]:
|
|
"""Make POST request to MiniMax API with error handling."""
|
|
url = f"{BASE_URL}/{endpoint}"
|
|
headers = get_headers(api_key)
|
|
|
|
if verbose:
|
|
print(f" POST {url}", file=sys.stderr)
|
|
|
|
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
|
|
|
if response.status_code != 200:
|
|
raise Exception(
|
|
f"MiniMax API error (HTTP {response.status_code}): {response.text}"
|
|
)
|
|
|
|
data = response.json()
|
|
|
|
# Check MiniMax-specific error codes
|
|
base_resp = data.get("base_resp", {})
|
|
status_code = base_resp.get("status_code", 0)
|
|
if status_code != 0:
|
|
raise Exception(
|
|
f"MiniMax API error (code {status_code}): "
|
|
f"{base_resp.get('status_msg', 'Unknown error')}"
|
|
)
|
|
|
|
return data
|
|
|
|
|
|
def api_get(endpoint: str, params: Dict[str, str], api_key: str,
|
|
verbose: bool = False) -> Dict[str, Any]:
|
|
"""Make GET request to MiniMax API."""
|
|
url = f"{BASE_URL}/{endpoint}"
|
|
headers = get_headers(api_key)
|
|
|
|
if verbose:
|
|
print(f" GET {url}", file=sys.stderr)
|
|
|
|
response = requests.get(url, headers=headers, params=params, timeout=60)
|
|
|
|
if response.status_code != 200:
|
|
raise Exception(
|
|
f"MiniMax API error (HTTP {response.status_code}): {response.text}"
|
|
)
|
|
|
|
return response.json()
|
|
|
|
|
|
def poll_async_task(task_id: str, task_type: str, api_key: str,
|
|
poll_interval: int = 10, max_wait: int = 600,
|
|
verbose: bool = False) -> Dict[str, Any]:
|
|
"""Poll async task (video/music) until completion.
|
|
|
|
Args:
|
|
task_id: The task ID returned from creation endpoint
|
|
task_type: 'video_generation' or 'music_generation'
|
|
poll_interval: Seconds between polls (default 10)
|
|
max_wait: Maximum wait time in seconds (default 600)
|
|
"""
|
|
elapsed = 0
|
|
while elapsed < max_wait:
|
|
result = api_get(
|
|
f"query/{task_type}",
|
|
{"task_id": task_id},
|
|
api_key,
|
|
verbose=False
|
|
)
|
|
|
|
status = result.get("status", "Unknown")
|
|
if verbose and elapsed > 0 and elapsed % 30 == 0:
|
|
print(f" Polling... {elapsed}s elapsed, status: {status}",
|
|
file=sys.stderr)
|
|
|
|
if status == "Success":
|
|
return result
|
|
elif status in ("Failed", "Error"):
|
|
raise Exception(f"Task failed: {json.dumps(result)}")
|
|
|
|
time.sleep(poll_interval)
|
|
elapsed += poll_interval
|
|
|
|
raise TimeoutError(f"Task {task_id} timed out after {max_wait}s")
|
|
|
|
|
|
def download_file(file_id: str, api_key: str, output_path: str,
|
|
verbose: bool = False) -> str:
|
|
"""Download file from MiniMax file service."""
|
|
result = api_get("files/retrieve", {"file_id": file_id}, api_key, verbose)
|
|
|
|
download_url = result.get("file", {}).get("download_url")
|
|
if not download_url:
|
|
raise Exception(f"No download URL in response: {json.dumps(result)}")
|
|
|
|
if verbose:
|
|
print(f" Downloading to: {output_path}", file=sys.stderr)
|
|
|
|
response = requests.get(download_url, stream=True, timeout=300)
|
|
response.raise_for_status()
|
|
|
|
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
with open(output_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
return output_path
|
|
|
|
|
|
def get_output_dir() -> Path:
|
|
"""Get project output directory for generated assets."""
|
|
script_dir = Path(__file__).parent
|
|
for parent in [script_dir] + list(script_dir.parents):
|
|
if (parent / '.git').exists() or (parent / '.claude').exists():
|
|
output_dir = parent / 'docs' / 'assets'
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
return output_dir
|
|
# Fallback
|
|
output_dir = script_dir.parent / 'assets'
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
return output_dir
|