Files
meijiaka-zy/python-api/app/ai/providers/klingai_provider.py
T

1327 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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/offkling-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/offkling-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: 音频文件URL5-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~8sMP4/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"]