1327 lines
46 KiB
Python
1327 lines
46 KiB
Python
"""
|
||
KlingAI (可灵 AI) API Provider
|
||
|
||
API 域名: https://api-beijing.klingai.com
|
||
认证方式: JWT (AK + SK)
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.core.token_manager import JWTTokenStrategy, TokenManager
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class KlingAIConfig(BaseModel):
|
||
"""Kling AI Provider 配置"""
|
||
|
||
access_key: str = Field(default="", description="Access Key")
|
||
secret_key: str = Field(default="", description="Secret Key")
|
||
base_url: str = Field(default="https://api-beijing.klingai.com", description="API Base URL")
|
||
|
||
|
||
class KlingPromptBuilder:
|
||
"""Kling Prompt 构建器
|
||
|
||
将业务语义(scene, voiceover, human_id)转换为 Kling API 专用语法。
|
||
所有 Kling 语法(<<<element_1>>>, <<<voice_1>>>)仅限此类内部使用。
|
||
"""
|
||
|
||
@staticmethod
|
||
def omni_segment(scene: str, voiceover: str, human_id: str | None = None) -> str:
|
||
"""构建 Omni-Video 分镜提示词"""
|
||
return f'<<<element_1>>>{scene},说:"{voiceover}"'
|
||
|
||
@staticmethod
|
||
def empty_shot(scene: str, voiceover: str) -> str:
|
||
"""构建空镜图生视频提示词"""
|
||
if voiceover:
|
||
return f'{scene}。<<<voice_1>>>画外音:"{voiceover}"'
|
||
return scene
|
||
|
||
|
||
class KlingAIProvider:
|
||
"""
|
||
KlingAI API Provider
|
||
|
||
官方音色ID:
|
||
- 829824295735410756: 钓系女友
|
||
- 829826751244537879: 温柔女声
|
||
- 829826792415842333: 播报男声
|
||
- 829826834144964676: 盐系少年
|
||
- 829826884271091753: 撒娇女友
|
||
"""
|
||
|
||
provider_id = "klingai"
|
||
DEFAULT_BASE_URL = "https://api-beijing.klingai.com"
|
||
|
||
# 官方预设音色
|
||
PRESET_VOICES = {
|
||
"829824295735410756": "钓系女友",
|
||
"829826751244537879": "温柔女声",
|
||
"829826792415842333": "播报男声",
|
||
"829826834144964676": "盐系少年",
|
||
"829826884271091753": "撒娇女友",
|
||
}
|
||
|
||
def __init__(self, config: dict[str, Any] = None):
|
||
self.config = config or {}
|
||
self.access_key = self.config.get("access_key", "")
|
||
self.secret_key = self.config.get("secret_key", "")
|
||
self.base_url = self.config.get("base_url", self.DEFAULT_BASE_URL).rstrip("/")
|
||
|
||
# 初始化 Token 策略
|
||
self._token_strategy: JWTTokenStrategy | None = None
|
||
if self.access_key and self.secret_key:
|
||
self._token_strategy = JWTTokenStrategy(
|
||
access_key=self.access_key,
|
||
secret_key=self.secret_key,
|
||
expires_in=1800, # 30分钟
|
||
)
|
||
|
||
async def _get_headers(self) -> dict[str, str]:
|
||
"""获取请求头(使用 TokenManager 缓存)"""
|
||
if not self._token_strategy:
|
||
raise ValueError("KlingAI access_key and secret_key are required")
|
||
|
||
token_info = await TokenManager.get_instance().get_token(self._token_strategy)
|
||
return {
|
||
"Authorization": f"Bearer {token_info.token}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
# ==================== 文生视频 ====================
|
||
|
||
# ==================== Omni 视频生成 ====================
|
||
|
||
async def generate_video_omni(
|
||
self,
|
||
prompt: str,
|
||
model: str = "kling-v3-omni",
|
||
mode: str = "pro",
|
||
aspect_ratio: str = "9:16",
|
||
duration: int | None = None,
|
||
sound: str = "on",
|
||
negative_prompt: str | None = None,
|
||
multi_shot: bool = False,
|
||
shot_type: str | None = None,
|
||
multi_prompt: list[dict | None] = None,
|
||
image_list: list[dict | None] = None,
|
||
element_list: list[dict | None] = None,
|
||
video_list: list[dict | None] = None,
|
||
voice_list: list[dict | None] = None,
|
||
callback_url: str | None = None,
|
||
external_task_id: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""Omni-Video 视频生成(多模态视频生成)
|
||
|
||
POST /v1/videos/omni-video
|
||
|
||
支持文本、图片、主体、视频多种输入方式组合生成视频。
|
||
是 kling-v3-omni 和 kling-video-o1 模型的专用接口。
|
||
|
||
Args:
|
||
prompt: 正向提示词(≤2500字符),支持 <<<element_1>>>/<<<image_1>>>/<<<video_1>>> 引用语法
|
||
model: 模型名称,kling-v3-omni 或 kling-video-o1
|
||
mode: 生成模式,pro=1080p(默认), std=720p
|
||
aspect_ratio: 宽高比,可选 16:9/9:16/1:1
|
||
duration: 时长,3/5/10/15(秒),Omni 支持 3-15s
|
||
sound: 声音控制,on=音画同出, off=无声
|
||
negative_prompt: 负向提示词
|
||
multi_shot: 是否多镜头模式
|
||
shot_type: 分镜方式,customize=自定义, intelligence=智能分镜
|
||
multi_prompt: 多镜头提示词列表,每个元素包含 index, prompt, duration
|
||
image_list: 参考图片列表,最多 4 张
|
||
element_list: 主体参考列表,最多 7 个,格式:[{"element_id": 123}]
|
||
video_list: 参考视频列表
|
||
callback_url: 回调地址
|
||
external_task_id: 自定义任务ID
|
||
|
||
Returns:
|
||
包含 task_id 和任务状态的字典
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/omni-video"
|
||
|
||
payload = {
|
||
"model_name": model,
|
||
"prompt": prompt,
|
||
"mode": mode,
|
||
"aspect_ratio": aspect_ratio,
|
||
"sound": sound,
|
||
}
|
||
if duration is not None:
|
||
payload["duration"] = str(duration)
|
||
|
||
if negative_prompt:
|
||
payload["negative_prompt"] = negative_prompt
|
||
|
||
if multi_shot:
|
||
payload["multi_shot"] = True
|
||
if shot_type:
|
||
payload["shot_type"] = shot_type
|
||
if multi_prompt:
|
||
payload["multi_prompt"] = multi_prompt
|
||
|
||
if image_list:
|
||
payload["image_list"] = image_list
|
||
if element_list:
|
||
payload["element_list"] = element_list
|
||
if video_list:
|
||
payload["video_list"] = video_list
|
||
if voice_list:
|
||
payload["voice_list"] = voice_list
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
if external_task_id:
|
||
payload["external_task_id"] = external_task_id
|
||
|
||
# 记录请求参数(调试用)
|
||
logger.info(
|
||
f"[KlingAI] omni-video 请求: {json.dumps(payload, ensure_ascii=False, indent=2)}"
|
||
)
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"[KlingAI] omni-video 响应: {json.dumps(data, ensure_ascii=False)}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_omni_video_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询 Omni-Video 任务状态
|
||
|
||
GET /v1/videos/omni-video/{task_id}
|
||
|
||
Args:
|
||
task_id: 任务ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/omni-video/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def list_omni_video_tasks(
|
||
self, page: int = 1, page_size: int = 30, **kwargs
|
||
) -> dict[str, Any]:
|
||
"""查询 Omni-Video 任务列表
|
||
|
||
GET /v1/videos/omni-video?pageNum={page}&pageSize={page_size}
|
||
|
||
Args:
|
||
page: 页码
|
||
page_size: 每页数量
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/omni-video?pageNum={page}&pageSize={page_size}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 文生视频 ====================
|
||
|
||
async def generate_video_text2video(
|
||
self,
|
||
prompt: str,
|
||
model: str = "kling-v3",
|
||
mode: str = "pro",
|
||
aspect_ratio: str = "9:16",
|
||
duration: int | None = None,
|
||
sound: str = "on",
|
||
negative_prompt: str | None = None,
|
||
multi_shot: bool = False,
|
||
shot_type: str | None = None,
|
||
multi_prompt: list[dict | None] = None,
|
||
element_list: list[dict | None] = None,
|
||
voice_list: list[dict | None] = None,
|
||
callback_url: str | None = None,
|
||
external_task_id: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""文生视频
|
||
|
||
POST /v1/videos/text2video
|
||
|
||
Args:
|
||
prompt: 正向提示词(≤2500字符),支持 <<<element_1>>> 引用语法
|
||
model: 模型名称,可选 kling-v1/kling-v1-6/kling-v2-master/kling-v2-1-master/kling-v2-5-turbo/kling-v2-6/kling-v3
|
||
mode: 生成模式,pro=1080p(默认), std=720p
|
||
aspect_ratio: 宽高比,可选 16:9/9:16/1:1/4:3/3:4
|
||
duration: 时长,可选 3/5/10(秒)
|
||
sound: 声音控制,on/off(kling-v3支持)
|
||
negative_prompt: 负向提示词
|
||
multi_shot: 是否多镜头
|
||
shot_type: 分镜方式,customize/intelligence
|
||
multi_prompt: 多镜头提示词列表,每个元素包含 index, prompt, duration
|
||
element_list: 主体参考列表,格式: [{"element_id": 123}], kling-v3支持
|
||
voice_list: 音色列表,格式: [{"voice_id": "xxx"}], 配合 prompt 中 <<<voice_1>>> 使用
|
||
callback_url: 回调地址
|
||
external_task_id: 自定义任务ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/text2video"
|
||
|
||
payload = {
|
||
"model_name": model,
|
||
"prompt": prompt,
|
||
"mode": mode,
|
||
"aspect_ratio": aspect_ratio,
|
||
"sound": sound,
|
||
}
|
||
if duration is not None:
|
||
payload["duration"] = str(duration)
|
||
|
||
if negative_prompt:
|
||
payload["negative_prompt"] = negative_prompt
|
||
|
||
if multi_shot:
|
||
payload["multi_shot"] = True
|
||
if shot_type:
|
||
payload["shot_type"] = shot_type
|
||
if multi_prompt:
|
||
payload["multi_prompt"] = multi_prompt
|
||
|
||
if element_list:
|
||
payload["element_list"] = element_list
|
||
if voice_list:
|
||
payload["voice_list"] = voice_list
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
if external_task_id:
|
||
payload["external_task_id"] = external_task_id
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI text2video response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
# ==================== 图生视频 ====================
|
||
|
||
async def generate_video_image2video(
|
||
self,
|
||
prompt: str,
|
||
image_url: str,
|
||
model: str = "kling-v3",
|
||
mode: str = "pro",
|
||
duration: int | str | None = None,
|
||
image_tail_url: str | None = None,
|
||
camera_control: dict | None = None,
|
||
static_mask: str | None = None,
|
||
dynamic_masks: list[dict | None] = None,
|
||
voice_list: list[dict | None] = None,
|
||
sound: str = "on",
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""图生视频
|
||
|
||
Args:
|
||
prompt: 视频运动描述
|
||
image_url: 首帧图像URL或Base64
|
||
model: 模型名称
|
||
mode: 生成模式,pro=1080p(默认), std=720p
|
||
duration: 视频时长
|
||
image_tail_url: 尾帧图像URL或Base64
|
||
camera_control: 相机控制参数,仅kling-v1模型支持
|
||
static_mask: 静态笔刷涂抹区域
|
||
dynamic_masks: 动态笔刷配置列表,包含mask和trajectories
|
||
voice_list: 音色列表,格式: [{"voice_id": "xxx"}], 配合 prompt 中 <<<voice_1>>> 使用
|
||
sound: 声音控制,on/off(kling-v2-6支持)
|
||
callback_url: 回调地址
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/image2video"
|
||
|
||
payload = {
|
||
"model_name": model,
|
||
"image": image_url,
|
||
"prompt": prompt,
|
||
"mode": mode,
|
||
"sound": sound,
|
||
}
|
||
if duration is not None:
|
||
payload["duration"] = str(duration)
|
||
|
||
if image_tail_url:
|
||
payload["image_tail"] = image_tail_url
|
||
if camera_control:
|
||
payload["camera_control"] = camera_control
|
||
if static_mask:
|
||
payload["static_mask"] = static_mask
|
||
if dynamic_masks:
|
||
payload["dynamic_masks"] = dynamic_masks
|
||
if voice_list:
|
||
payload["voice_list"] = voice_list
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
# 记录请求参数(调试用)
|
||
logger.info(
|
||
f"[KlingAI] image2video 请求: {json.dumps(payload, ensure_ascii=False, indent=2)}"
|
||
)
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"[KlingAI] image2video 响应: {json.dumps(data, ensure_ascii=False)}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 视频延长 ====================
|
||
|
||
async def extend_video(
|
||
self,
|
||
video_id: str,
|
||
prompt: str,
|
||
duration: int = 5,
|
||
model: str = "kling-v1-6",
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""视频延长
|
||
|
||
Args:
|
||
video_id: 要延长的视频ID
|
||
prompt: 延长部分的提示词
|
||
duration: 延长时间(5或10秒)
|
||
model: 模型名称
|
||
callback_url: 回调地址
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/extend"
|
||
|
||
payload = {
|
||
"model_name": model,
|
||
"video_id": video_id,
|
||
"prompt": prompt,
|
||
"duration": duration,
|
||
}
|
||
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 人脸识别 ====================
|
||
|
||
async def identify_face(
|
||
self, video_id: str | None = None, video_url: str | None = None, **kwargs
|
||
) -> dict[str, Any]:
|
||
"""人脸识别(对口型前置步骤)
|
||
|
||
POST /v1/videos/identify-face
|
||
|
||
Args:
|
||
video_id: KlingAI生成的视频ID
|
||
video_url: 上传的视频URL(二选一)
|
||
|
||
Returns:
|
||
包含 session_id 和 face_data 的字典
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/identify-face"
|
||
|
||
payload = {}
|
||
if video_id:
|
||
payload["video_id"] = video_id
|
||
elif video_url:
|
||
payload["video_url"] = video_url
|
||
else:
|
||
raise ValueError("必须提供 video_id 或 video_url")
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 对口型(新版 advanced-lip-sync)====================
|
||
|
||
async def advanced_lip_sync(
|
||
self,
|
||
session_id: str,
|
||
face_choose: list[dict[str, Any]],
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""新版对口型视频生成
|
||
|
||
POST /v1/videos/advanced-lip-sync
|
||
|
||
Args:
|
||
session_id: 人脸识别返回的会话ID
|
||
face_choose: 人脸选择配置列表,每项包含:
|
||
- face_id: 人脸ID(必填)
|
||
- audio_id: 通过TTS试听接口生成的音频ID(与sound_file二选一)
|
||
- sound_file: 音频文件URL或Base64(与audio_id二选一)
|
||
- sound_start_time: 音频裁剪起点时间(ms,必填)
|
||
- sound_end_time: 音频裁剪终点时间(ms,必填)
|
||
- sound_insert_time: 裁剪后音频插入时间(ms,必填)
|
||
callback_url: 回调地址
|
||
|
||
Returns:
|
||
包含 task_id 的任务信息
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/advanced-lip-sync"
|
||
|
||
payload = {
|
||
"session_id": session_id,
|
||
"face_choose": face_choose,
|
||
}
|
||
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_advanced_lip_sync_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询新版对口型任务状态
|
||
|
||
GET /v1/videos/advanced-lip-sync/{task_id}
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/videos/advanced-lip-sync/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 自定义音色 ====================
|
||
|
||
async def create_custom_voice(
|
||
self,
|
||
voice_name: str,
|
||
audio_url: str | None = None,
|
||
video_url: str | None = None,
|
||
video_id: str | None = None,
|
||
callback_url: str | None = None,
|
||
external_task_id: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""创建自定义音色
|
||
|
||
Args:
|
||
voice_name: 音色名称(≤20字符)
|
||
audio_url: 音频文件URL(5-30秒,mp3/wav格式)
|
||
video_url: 视频文件URL(可选)
|
||
video_id: 历史作品ID(可选)
|
||
callback_url: 回调地址
|
||
external_task_id: 自定义任务ID
|
||
|
||
音频要求: 人声干净、无杂音、单一人声、5-30秒
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/custom-voices"
|
||
|
||
payload = {
|
||
"voice_name": voice_name,
|
||
}
|
||
|
||
# 三者至少填一个 (KlingAI API 使用 voice_url 参数名)
|
||
if audio_url:
|
||
payload["voice_url"] = audio_url
|
||
elif video_url:
|
||
payload["voice_url"] = video_url # KlingAI 使用 voice_url 而不是 video_url
|
||
elif video_id:
|
||
payload["video_id"] = video_id
|
||
else:
|
||
raise ValueError("必须提供 audio_url、video_url 或 video_id 之一")
|
||
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
if external_task_id:
|
||
payload["external_task_id"] = external_task_id
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
async def list_custom_voices(self, **kwargs) -> list[dict[str, Any]]:
|
||
"""查询自定义音色列表
|
||
|
||
Returns:
|
||
自定义音色列表,每个音色包含 voice_id, voice_name, status 等字段
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/custom-voices"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI list_custom_voices response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
# KlingAI API 返回的是任务列表,每个任务包含 task_result.voices
|
||
result_data = data.get("data", [])
|
||
voices = []
|
||
if isinstance(result_data, list):
|
||
for task in result_data:
|
||
if isinstance(task, dict) and task.get("task_status") == "succeed":
|
||
task_result = task.get("task_result", {})
|
||
if isinstance(task_result, dict):
|
||
voice_list = task_result.get("voices", [])
|
||
if isinstance(voice_list, list):
|
||
voices.extend(voice_list)
|
||
return voices
|
||
|
||
async def list_preset_voices(self, **kwargs) -> list[dict[str, Any]]:
|
||
"""查询官方音色列表
|
||
|
||
KlingAI API 返回的是任务列表格式,每个任务包含音色信息:
|
||
{
|
||
'code': 0,
|
||
'data': [
|
||
{
|
||
'task_id': '...',
|
||
'task_status': 'succeed',
|
||
'task_result': {'voices': [{'voice_id': '...', 'voice_name': '...'}]}
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/presets-voices"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
|
||
# 解析任务列表格式的响应
|
||
voices = []
|
||
task_list = data.get("data", [])
|
||
for task in task_list:
|
||
if task.get("task_status") == "succeed":
|
||
task_result = task.get("task_result", {})
|
||
voice_list = task_result.get("voices", [])
|
||
voices.extend(voice_list)
|
||
|
||
return voices
|
||
|
||
async def delete_custom_voice(self, voice_id: str, **kwargs) -> dict[str, Any]:
|
||
"""删除自定义音色
|
||
|
||
Args:
|
||
voice_id: 音色ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/delete-voices"
|
||
|
||
payload = {"voice_id": voice_id}
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 任务查询 ====================
|
||
|
||
async def get_video_task(
|
||
self, task_id: str, task_type: str = "text2video", **kwargs
|
||
) -> dict[str, Any]:
|
||
"""查询视频任务状态
|
||
|
||
Args:
|
||
task_id: 任务ID
|
||
task_type: 任务类型,可选 text2video/image2video/lip-sync
|
||
|
||
Returns:
|
||
任务详情,包含 task_status 字段:
|
||
- submitted: 已提交
|
||
- processing: 处理中
|
||
- succeed: 成功
|
||
- failed: 失败
|
||
"""
|
||
import aiohttp
|
||
|
||
endpoint_map = {
|
||
"text2video": "videos/text2video",
|
||
"image2video": "videos/image2video",
|
||
"lip-sync": "videos/lip-sync",
|
||
"extend": "videos/extend",
|
||
}
|
||
|
||
endpoint = endpoint_map.get(task_type, "videos/text2video")
|
||
url = f"{self.base_url}/v1/{endpoint}/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
async def list_video_tasks(
|
||
self,
|
||
task_type: str = "text2video",
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""查询视频任务列表
|
||
|
||
Args:
|
||
task_type: 任务类型
|
||
page: 页码
|
||
page_size: 每页数量
|
||
"""
|
||
import aiohttp
|
||
|
||
endpoint_map = {
|
||
"text2video": "videos/text2video",
|
||
"image2video": "videos/image2video",
|
||
}
|
||
|
||
endpoint = endpoint_map.get(task_type, "videos/text2video")
|
||
url = f"{self.base_url}/v1/{endpoint}?page={page}&page_size={page_size}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
async def get_custom_voice_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询自定义音色任务状态"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/custom-voices/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== TTS 语音合成 ====================
|
||
|
||
async def generate_tts(
|
||
self,
|
||
text: str,
|
||
voice_id: str,
|
||
voice_language: str = "zh",
|
||
voice_speed: float = 1.0,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
语音合成 (TTS)
|
||
|
||
POST /v1/audio/tts
|
||
|
||
Args:
|
||
text: 要合成的文本
|
||
voice_id: 音色ID(官方预设或自定义音色)
|
||
voice_language: 语言 (zh/en)
|
||
voice_speed: 语速 (0.8-2.0)
|
||
|
||
Returns:
|
||
包含音频URL和任务信息的字典
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/audio/tts"
|
||
|
||
payload = {
|
||
"text": text,
|
||
"voice_id": voice_id,
|
||
"voice_language": voice_language,
|
||
"voice_speed": str(voice_speed),
|
||
}
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI TTS API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
async def get_tts_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询 TTS 任务状态"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/audio/tts/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
async def list_tts_tasks(self, page: int = 1, page_size: int = 30, **kwargs) -> dict[str, Any]:
|
||
"""
|
||
查询 TTS 任务列表
|
||
|
||
Args:
|
||
page: 页码
|
||
page_size: 每页数量
|
||
|
||
Returns:
|
||
任务列表数据
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/audio/tts?pageNum={page}&pageSize={page_size}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 主体管理 (Element) ====================
|
||
|
||
async def create_element(
|
||
self,
|
||
element_name: str,
|
||
element_description: str,
|
||
reference_type: str = "image_refer",
|
||
element_image_list: dict[str, Any | None] = None,
|
||
element_video_list: dict[str, Any | None] = None,
|
||
element_voice_id: str | None = None,
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""创建主体(自定义元素)
|
||
|
||
POST /v1/general/advanced-custom-elements
|
||
|
||
Args:
|
||
element_name: 主体名称(≤20字符)
|
||
element_description: 主体描述(≤100字符)
|
||
reference_type: 参考类型,image_refer(图片参考) / video_refer(视频参考)
|
||
element_image_list: 图片参考对象,图片定制时必填
|
||
格式: {
|
||
"frontal_image": "正面图URL",
|
||
"refer_images": [{"image_url": "其他角度URL1"}, ...]
|
||
}
|
||
要求:正面图+1-3张其他角度,jpg/jpeg/png,≤10MB
|
||
element_video_list: 视频参考对象,视频定制时必填
|
||
格式: {
|
||
"frontal_video": "正面视频URL",
|
||
"refer_videos": [{"video_url": "其他角度URL"}]
|
||
}
|
||
要求:3s~8s,MP4/MOV,高度720px~2160px,≤200MB
|
||
限制:仅支持写实风格、人形主体
|
||
element_voice_id: 音色ID,绑定音色到主体
|
||
callback_url: 回调地址
|
||
|
||
Returns:
|
||
包含 task_id 和任务状态的字典
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/advanced-custom-elements"
|
||
|
||
payload = {
|
||
"element_name": element_name,
|
||
"element_description": element_description,
|
||
"reference_type": reference_type,
|
||
}
|
||
|
||
# 修正 element_image_list 内部格式:refer_images 数组元素使用 image_url 而非 url
|
||
if element_image_list and isinstance(element_image_list, dict):
|
||
formatted_image_list = {"frontal_image": element_image_list.get("frontal_image", "")}
|
||
refer_images = element_image_list.get("refer_images", [])
|
||
if refer_images:
|
||
formatted_image_list["refer_images"] = [
|
||
(
|
||
{"image_url": img.get("url", img.get("image_url", ""))}
|
||
if isinstance(img, dict)
|
||
else {"image_url": img}
|
||
)
|
||
for img in refer_images
|
||
]
|
||
payload["element_image_list"] = formatted_image_list
|
||
# 修正 element_video_list 内部格式
|
||
# element_video_list 是对象格式,不是数组
|
||
# 正确格式: {"refer_videos": [{"video_url": "..."}]}
|
||
if element_video_list and isinstance(element_video_list, dict):
|
||
formatted_video_list = {}
|
||
|
||
# 处理 refer_videos 数组
|
||
refer_videos = element_video_list.get("refer_videos", [])
|
||
if refer_videos:
|
||
formatted_video_list["refer_videos"] = [
|
||
(
|
||
{"video_url": vid.get("url", vid.get("video_url", ""))}
|
||
if isinstance(vid, dict)
|
||
else {"video_url": vid}
|
||
)
|
||
for vid in refer_videos
|
||
]
|
||
|
||
# 处理 frontal_video(如果提供了)- 转为 refer_videos 格式
|
||
frontal_video = element_video_list.get("frontal_video", "")
|
||
if frontal_video:
|
||
if "refer_videos" not in formatted_video_list:
|
||
formatted_video_list["refer_videos"] = []
|
||
formatted_video_list["refer_videos"].append({"video_url": frontal_video})
|
||
|
||
if formatted_video_list:
|
||
# API 要求 element_video_list 是对象格式
|
||
payload["element_video_list"] = formatted_video_list
|
||
if element_voice_id:
|
||
payload["element_voice_id"] = element_voice_id
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI create_element response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def list_elements(self, **kwargs) -> list[dict[str, Any]]:
|
||
"""查询主体列表
|
||
|
||
GET /v1/general/advanced-custom-elements
|
||
|
||
KlingAI API 返回任务列表格式,每个任务包含 task_result.elements 数组。
|
||
需要从任务列表中提取所有主体信息。
|
||
|
||
Returns:
|
||
主体列表,每个主体包含 element_id, element_name 等字段
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/advanced-custom-elements"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI list_elements response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
|
||
# KlingAI 返回任务列表,每个任务包含 task_result.elements
|
||
task_list = data.get("data", [])
|
||
elements = []
|
||
|
||
for task in task_list:
|
||
if isinstance(task, dict) and task.get("task_status") == "succeed":
|
||
task_result = task.get("task_result", {})
|
||
if isinstance(task_result, dict):
|
||
element_list = task_result.get("elements", [])
|
||
if isinstance(element_list, list):
|
||
for element in element_list:
|
||
# 添加任务信息到主体数据中
|
||
element["task_id"] = task.get("task_id")
|
||
element["task_status"] = task.get("task_status")
|
||
element["created_at"] = task.get("created_at")
|
||
element["updated_at"] = task.get("updated_at")
|
||
elements.append(element)
|
||
|
||
return elements
|
||
|
||
async def get_element_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询创建主体任务状态
|
||
|
||
GET /v1/general/advanced-custom-elements/{task_id}
|
||
|
||
Args:
|
||
task_id: 创建任务的ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/advanced-custom-elements/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI get_element_task response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_element(self, element_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询单个主体详情
|
||
|
||
GET /v1/general/advanced-custom-elements/{element_id}
|
||
|
||
Args:
|
||
element_id: 主体ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/advanced-custom-elements/{element_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI get_element response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def delete_element(self, element_id: str, **kwargs) -> dict[str, Any]:
|
||
"""删除主体
|
||
|
||
POST /v1/general/delete-elements
|
||
|
||
Args:
|
||
element_id: 主体ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/delete-elements"
|
||
|
||
payload = {"element_id": element_id}
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI delete_element response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
# ==================== 智能补全主体图 ====================
|
||
|
||
async def ai_multi_shot(
|
||
self, frontal_image: str, callback_url: str | None = None, **kwargs
|
||
) -> dict[str, Any]:
|
||
"""智能补全主体不同角度图片(AI Multi Shot)
|
||
|
||
POST /v1/general/ai-multi-shot
|
||
|
||
通过主体正面图,自动推理出该主体其他角度图片。
|
||
每次可生成3组结果供选择,每次扣减0.5积分。
|
||
|
||
Args:
|
||
frontal_image: 主体正面参考图 URL
|
||
callback_url: 回调地址
|
||
|
||
Returns:
|
||
包含生成结果的任务信息
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/ai-multi-shot"
|
||
|
||
payload = {
|
||
"element_frontal_image": frontal_image,
|
||
}
|
||
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"KlingAI ai_multi_shot response: {data}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_ai_multi_shot_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询智能补全主体图任务状态
|
||
|
||
GET /v1/general/ai-multi-shot/{task_id}
|
||
|
||
Args:
|
||
task_id: 任务ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/general/ai-multi-shot/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== Omni-Image 图像生成 ====================
|
||
|
||
async def generate_omni_image(
|
||
self,
|
||
prompt: str,
|
||
model: str = "kling-image-o1",
|
||
aspect_ratio: str = "9:16",
|
||
resolution: str = "1k",
|
||
result_type: str = "single",
|
||
n: int = 1,
|
||
element_list: list[dict[str, Any]] | None = None,
|
||
image_list: list[dict[str, Any]] | None = None,
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""Omni-Image 图像生成
|
||
|
||
POST /v1/images/omni-image
|
||
|
||
Args:
|
||
prompt: 正向提示词
|
||
model: 模型名称,kling-image-o1 或 kling-v3-omni
|
||
aspect_ratio: 宽高比,可选 16:9/9:16/1:1/4:3/3:4
|
||
resolution: 清晰度,1k/2k/4k
|
||
result_type: 结果类型,single/series
|
||
n: 生成图片数量(1-9)
|
||
element_list: 主体参考列表
|
||
image_list: 参考图列表
|
||
callback_url: 回调地址
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/images/omni-image"
|
||
|
||
payload: dict[str, Any] = {
|
||
"model_name": model,
|
||
"prompt": prompt,
|
||
"aspect_ratio": aspect_ratio,
|
||
"resolution": resolution,
|
||
"result_type": result_type,
|
||
"n": n,
|
||
}
|
||
|
||
if element_list:
|
||
payload["element_list"] = element_list
|
||
if image_list:
|
||
payload["image_list"] = image_list
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
logger.info(
|
||
f"[KlingAI] omni-image 请求: {json.dumps(payload, ensure_ascii=False, indent=2)}"
|
||
)
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"[KlingAI] omni-image 响应: {json.dumps(data, ensure_ascii=False)}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_omni_image_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询 Omni-Image 任务状态
|
||
|
||
GET /v1/images/omni-image/{task_id}
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/images/omni-image/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
# ==================== 文生图 ====================
|
||
|
||
async def generate_image(
|
||
self,
|
||
prompt: str,
|
||
model: str = "kling-v3",
|
||
aspect_ratio: str = "9:16",
|
||
negative_prompt: str | None = None,
|
||
callback_url: str | None = None,
|
||
**kwargs,
|
||
) -> dict[str, Any]:
|
||
"""文生图
|
||
|
||
POST /v1/images/generations
|
||
|
||
Args:
|
||
prompt: 正向提示词
|
||
model: 模型名称,kling-v3
|
||
aspect_ratio: 宽高比,可选 16:9/9:16/1:1/4:3/3:4
|
||
negative_prompt: 负向提示词
|
||
callback_url: 回调地址
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/images/generations"
|
||
|
||
payload = {
|
||
"model_name": model,
|
||
"prompt": prompt,
|
||
"aspect_ratio": aspect_ratio,
|
||
}
|
||
|
||
if negative_prompt:
|
||
payload["negative_prompt"] = negative_prompt
|
||
if callback_url:
|
||
payload["callback_url"] = callback_url
|
||
|
||
# 记录请求参数(调试用)
|
||
logger.info(
|
||
f"[KlingAI] generate_image 请求: {json.dumps(payload, ensure_ascii=False, indent=2)}"
|
||
)
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.post(url, json=payload, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
logger.info(f"[KlingAI] generate_image 响应: {json.dumps(data, ensure_ascii=False)}")
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
result_data = data.get("data", {})
|
||
if isinstance(result_data, list) and len(result_data) > 0:
|
||
return result_data[0] if isinstance(result_data[0], dict) else {}
|
||
return result_data if isinstance(result_data, dict) else {}
|
||
|
||
async def get_image_task(self, task_id: str, **kwargs) -> dict[str, Any]:
|
||
"""查询文生图任务状态
|
||
|
||
GET /v1/images/generations/{task_id}
|
||
|
||
Args:
|
||
task_id: 任务ID
|
||
"""
|
||
import aiohttp
|
||
|
||
url = f"{self.base_url}/v1/images/generations/{task_id}"
|
||
|
||
async with (
|
||
aiohttp.ClientSession() as session,
|
||
session.get(url, headers=await self._get_headers()) as resp,
|
||
):
|
||
data = await resp.json()
|
||
if data.get("code") != 0:
|
||
raise Exception(f"KlingAI API error: {data.get('message')}")
|
||
return data.get("data", {})
|
||
|
||
|
||
# 导出 Provider 类
|
||
__all__ = ["KlingAIProvider", "KlingAIConfig"]
|