feat: Implement video generation functionality and integrate with Kling API.

This commit is contained in:
xds
2026-02-12 10:27:07 +03:00
parent d1f67c773f
commit 32ff77e04b
13 changed files with 405 additions and 5 deletions

165
adapters/kling_adapter.py Normal file
View File

@@ -0,0 +1,165 @@
import logging
import time
import asyncio
from typing import Optional, Dict, Any
import httpx
import jwt
logger = logging.getLogger(__name__)
KLING_API_BASE = "https://api.klingai.com"
class KlingApiException(Exception):
pass
class KlingAdapter:
def __init__(self, access_key: str, secret_key: str):
if not access_key or not secret_key:
raise ValueError("Kling API credentials are missing")
self.access_key = access_key
self.secret_key = secret_key
def _generate_token(self) -> str:
"""Generate a JWT token for Kling API authentication."""
now = int(time.time())
payload = {
"iss": self.access_key,
"exp": now + 1800, # 30 minutes
"iat": now - 5, # небольшой запас назад
"nbf": now - 5,
}
return jwt.encode(payload, self.secret_key, algorithm="HS256",
headers={"typ": "JWT", "alg": "HS256"})
def _headers(self) -> dict:
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._generate_token()}"
}
async def create_video_task(
self,
image_url: str,
prompt: str = "",
negative_prompt: str = "",
model_name: str = "kling-v2-6",
duration: int = 5,
mode: str = "std",
cfg_scale: float = 0.5,
aspect_ratio: str = "16:9",
callback_url: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create an image-to-video generation task.
Returns the full task data dict including task_id.
"""
body: Dict[str, Any] = {
"model_name": model_name,
"image": image_url,
"prompt": prompt,
"negative_prompt": negative_prompt,
"duration": str(duration),
"mode": mode,
"cfg_scale": cfg_scale,
"aspect_ratio": aspect_ratio,
}
if callback_url:
body["callback_url"] = callback_url
logger.info(f"Creating Kling video task. Model: {model_name}, Duration: {duration}s, Mode: {mode}")
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{KLING_API_BASE}/v1/videos/image2video",
headers=self._headers(),
json=body,
)
data = response.json()
logger.info(f"Kling create task response: code={data.get('code')}, message={data.get('message')}")
if response.status_code != 200 or data.get("code") != 0:
error_msg = data.get("message", "Unknown Kling API error")
raise KlingApiException(f"Failed to create video task: {error_msg} (code={data.get('code')})")
task_data = data.get("data", {})
task_id = task_data.get("task_id")
if not task_id:
raise KlingApiException("No task_id returned from Kling API")
logger.info(f"Kling video task created: task_id={task_id}")
return task_data
async def get_task_status(self, task_id: str) -> Dict[str, Any]:
"""
Query the status of a video generation task.
Returns the full task data dict.
"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{KLING_API_BASE}/v1/videos/image2video/{task_id}",
headers=self._headers(),
)
data = response.json()
if response.status_code != 200 or data.get("code") != 0:
error_msg = data.get("message", "Unknown error")
raise KlingApiException(f"Failed to query task {task_id}: {error_msg}")
return data.get("data", {})
async def wait_for_completion(
self,
task_id: str,
poll_interval: int = 10,
timeout: int = 600,
progress_callback=None,
) -> Dict[str, Any]:
"""
Poll the task status until completion.
Args:
task_id: Kling task ID
poll_interval: seconds between polls
timeout: max seconds to wait
progress_callback: async callable(progress_pct: int) to report progress
Returns:
Final task data dict with video URL on success.
Raises:
KlingApiException on failure or timeout.
"""
start = time.time()
attempt = 0
while True:
elapsed = time.time() - start
if elapsed > timeout:
raise KlingApiException(f"Video generation timed out after {timeout}s for task {task_id}")
task_data = await self.get_task_status(task_id)
status = task_data.get("task_status")
logger.info(f"Kling task {task_id}: status={status}, elapsed={elapsed:.0f}s")
if status == "succeed":
logger.info(f"Kling task {task_id} completed successfully")
return task_data
if status == "failed":
fail_reason = task_data.get("task_status_msg", "Unknown failure")
raise KlingApiException(f"Video generation failed: {fail_reason}")
# Report progress estimate (linear approximation based on typical time)
if progress_callback:
# Estimate: typical gen is ~120s, cap at 90%
estimated_progress = min(int((elapsed / 120) * 90), 90)
attempt += 1
await progress_callback(estimated_progress)
await asyncio.sleep(poll_interval)