""" KlingAI (可灵 AI) API 路由 ========================== 提供视频生成、图像生成、对口型等功能。 """ import logging from typing import Any from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from app.ai.providers.klingai_provider import KlingAIProvider from app.config import get_settings from app.core.config_loader import get_config_loader from app.schemas.common import ApiResponse, success_response logger = logging.getLogger(__name__) router = APIRouter(prefix="/klingai", tags=["KlingAI"]) # ============ 请求/响应 Schema ============ class OmniVideoRequest(BaseModel): """Omni-Video 视频生成请求(kling-v3-omni / kling-video-o1)""" prompt: str = Field( ..., description="视频描述提示词(不超过2500字符),支持 <<>>/<<>>/<<>> 引用语法", example="一只<<>>在花园里奔跑,阳光明媚", ) model: str | None = Field("kling-v3-omni", description="模型: kling-v3-omni, kling-video-o1") duration: int = Field(5, ge=3, le=15, description="视频时长(秒): 3/5/10/15,Omni支持3-15秒") aspect_ratio: str = Field("16:9", description="宽高比: 16:9, 9:16, 1:1") mode: str = Field("pro", description="生成模式: pro(高质量) 或 std(标准)") sound: str = Field("on", description="声音控制: on=音画同出, off=无声") negative_prompt: str | None = Field(None, description="负面提示词") # 多镜头参数 multi_shot: bool = Field(False, description="是否启用多镜头模式") shot_type: str | None = Field( None, description="分镜方式: customize=自定义, intelligence=智能分镜" ) multi_prompt: list[dict | None] = Field( None, description="多镜头提示词列表,每个元素包含 index, prompt, duration,最多6个分镜", ) # 参考资源 image_list: list[dict | None] = Field(None, description="参考图片列表,最多4张") element_list: list[dict | None] = Field( None, description="主体参考列表,格式: [{'elementId': 123}], 最多7个" ) video_list: list[dict | None] = Field(None, description="参考视频列表") callback_url: str | None = Field(None, description="回调通知地址") external_task_id: str | None = Field(None, description="自定义任务ID") class VideoGenerateRequest(BaseModel): """文生视频请求""" prompt: str = Field( ..., description="视频描述提示词(不超过2500字符)", example="一只猫在花园里玩耍", ) model: str | None = Field("kling-v2.6", description="视频模型: kling-v2.6, kling-v2.5-turbo") duration: int = Field(5, ge=5, le=10, description="视频时长(秒): 5 或 10") aspect_ratio: str = Field("16:9", description="宽高比: 16:9, 9:16, 1:1, 4:3, 3:4") mode: str = Field("pro", description="生成模式: pro(高质量) 或 std(标准)") negative_prompt: str | None = Field(None, description="负面提示词") callback_url: str | None = Field(None, description="回调通知地址") class VideoGenerateResponse(BaseModel): """视频生成响应""" task_id: str task_status: str created_at: int updated_at: int class Image2VideoRequest(BaseModel): """图生视频请求""" image_url: str = Field(..., description="输入图片 URL") prompt: str | None = Field(None, description="视频运动描述提示词") model: str | None = Field("kling-v2.6", description="视频模型") duration: int = Field(5, ge=5, le=10, description="视频时长(秒)") aspect_ratio: str | None = Field(None, description="宽高比") mode: str = Field("pro", description="生成模式") callback_url: str | None = Field(None, description="回调通知地址") class IdentifyFaceRequest(BaseModel): """人脸识别请求""" video_id: str | None = Field(None, description="KlingAI 生成的视频 ID") video_url: str | None = Field(None, description="上传的视频 URL(与 videoId 二选一") class IdentifyFaceResponse(BaseModel): """人脸识别响应""" session_id: str face_data: list[dict[str, Any]] class FaceChooseItem(BaseModel): """新版对口型人脸配置""" face_id: str = Field(..., description="人脸ID,由 identify-face 接口返回") audio_id: str | None = Field(None, description="通过TTS生成的音频ID(与 sound_file 二选一)") sound_file: str | None = Field(None, description="音频文件URL或Base64(与 audio_id 二选一)") sound_start_time: int = Field(0, description="音频裁剪起点时间(ms)") sound_end_time: int = Field(..., description="音频裁剪终点时间(ms)") sound_insert_time: int = Field(0, description="裁剪后音频插入时间(ms)") class AdvancedLipSyncRequest(BaseModel): """新版对口型请求(advanced-lip-sync)""" session_id: str = Field(..., description="人脸识别返回的会话ID") face_choose: list[FaceChooseItem] = Field(..., description="人脸对口型配置列表") callback_url: str | None = Field(None, description="回调通知地址") class OmniImageRequest(BaseModel): """Omni-Image 图像生成请求""" prompt: str = Field(..., description="图像描述提示词") model: str | None = Field("kling-image-o1", description="模型: kling-image-o1, kling-v3-omni") aspect_ratio: str | None = Field("9:16", description="宽高比: 16:9/9:16/1:1/4:3/3:4") resolution: str | None = Field("1k", description="清晰度: 1k/2k/4k") result_type: str | None = Field("single", description="结果类型: single/series") n: int | None = Field(1, description="生成数量 1-9") element_list: list[dict[str, Any]] | None = Field(None, description="主体参考列表") image_list: list[dict[str, Any]] | None = Field(None, description="参考图列表") callback_url: str | None = Field(None, description="回调通知地址") class ImageGenerateRequest(BaseModel): """图像生成请求""" prompt: str = Field(..., description="图像描述提示词") model: str | None = Field("kolors-v1", description="图像模型: kolors-v1") width: int = Field(1024, description="图像宽度") height: int = Field(1024, description="图像高度") negative_prompt: str | None = Field(None, description="负面提示词") callback_url: str | None = Field(None, description="回调通知地址") class TaskStatusResponse(BaseModel): """任务状态响应""" task_id: str task_status: str # submitted, processing, succeed, failed created_at: int updated_at: int video_url: str | None = None image_url: str | None = None error_message: str | None = None class VirtualTryonRequest(BaseModel): """虚拟试穿请求""" person_image_url: str = Field(..., description="人物图片 URL") cloth_image_url: str = Field(..., description="衣服图片 URL") callback_url: str | None = Field(None, description="回调通知地址") # ============ 自定义音色 Schema ============ class CreateCustomVoiceRequest(BaseModel): """创建自定义音色请求""" voice_name: str = Field(..., description="音色名称(最多20字符)", example="我的音色") audio_url: str | None = Field(None, description="音频文件URL(mp3/wav/mp4/mov)") video_url: str | None = Field(None, description="视频文件URL") video_id: str | None = Field( None, description="历史作品ID(v2.6/sound=on/数字人/对口型生成的视频)" ) callback_url: str | None = Field(None, description="回调通知地址") external_task_id: str | None = Field(None, description="自定义任务ID") class ElementImage(BaseModel): """主体参考图片(对应 KlingAI 官方格式 imageUrl)""" image_url: str = Field(..., description="图片URL") name: str | None = Field(None, description="图片名称") class ElementVideo(BaseModel): """主体参考视频(对应 KlingAI 官方格式 videoUrl)""" video_url: str = Field(..., description="视频URL") name: str | None = Field(None, description="视频名称") class CreateElementRequest(BaseModel): """创建主体请求""" element_name: str = Field(..., description="主体名称(最多20字符)", example="我的小猫") element_description: str = Field( ..., description="主体描述(最多100字符)", example="一只橘色的小猫,毛茸茸的" ) reference_type: str = Field("image_refer", description="参考类型: image_refer 或 video_refer") element_image_list: list[ElementImage] | None = Field( None, description="图片参考列表(图片定制时必填,第一个作为正面图)" ) element_video_list: list[ElementVideo] | None = Field( None, description="视频参考列表(视频定制时必填,第一个作为正面视频)" ) element_voice_id: str | None = Field(None, description="音色ID,绑定音色到主体") callback_url: str | None = Field(None, description="回调通知地址") class ElementResponse(BaseModel): """主体响应""" element_id: int | None = None element_name: str | None = None element_description: str | None = None element_type: str | None = None # image_refer / video_refer status: str | None = None task_id: str | None = None task_status: str | None = None created_at: int | None = None updated_at: int | None = None element_image_list: dict | None = None element_video_list: dict | None = None element_voice_info: dict | None = None owned_by: str | None = None class CreateCustomVoiceResponse(BaseModel): """创建自定义音色响应""" task_id: str task_status: str created_at: int updated_at: int class VoiceInfo(BaseModel): """音色信息""" voice_id: str voice_name: str trial_url: str | None = None owned_by: str | None = None status: str | None = None # ============ 辅助函数 ============ async def get_klingai_provider() -> KlingAIProvider: """获取 KlingAI Provider 实例 API Key 从 Settings 读取(符合配置规范) """ settings = get_settings() config_loader = get_config_loader() platform = config_loader.get_platform("klingai") if not platform: raise HTTPException(status_code=404, detail="KlingAI 平台未配置") # 从 Settings 读取 AK/SK(符合配置规范:.env → Settings → 服务层) access_key = settings.KLINGAI_ACCESS_KEY secret_key = settings.KLINGAI_SECRET_KEY if not access_key or not secret_key: raise HTTPException( status_code=400, detail="KlingAI Access Key 或 Secret Key 未配置,请设置环境变量 KLINGAI_ACCESS_KEY 和 KLINGAI_SECRET_KEY", ) # 从 YAML 读取 base_url(模型配置) base_url = platform.base_url if platform else None return KlingAIProvider( { "access_key": access_key, "secret_key": secret_key, "base_url": base_url or "https://api-beijing.klingai.com", } ) # ============ API 路由 ============ @router.post("/videos/omni", response_model=ApiResponse[VideoGenerateResponse]) async def create_omni_video(data: OmniVideoRequest): """ Omni-Video 多模态视频生成 支持文本、图片、主体、视频等多种输入方式组合生成视频。 适用于 kling-v3-omni 和 kling-video-o1 模型。 **特性:** - 支持 3-15 秒视频生成 - 支持多镜头和智能分镜 (shotType=intelligence) - 支持引用主体、图片、视频作为参考 - 支持 <<>>/<<>>/<<>> 语法引用提示词中的资源 """ try: provider = await get_klingai_provider() result = await provider.generate_video_omni( prompt=data.prompt, model=data.model, mode=data.mode, aspect_ratio=data.aspect_ratio, duration=data.duration, sound=data.sound, negative_prompt=data.negative_prompt, multi_shot=data.multi_shot, shot_type=data.shot_type, multi_prompt=data.multi_prompt, image_list=data.image_list, element_list=data.element_list, video_list=data.video_list, callback_url=data.callback_url, external_task_id=data.external_task_id, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"Omni-Video 生成失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/videos/omni/{task_id}", response_model=ApiResponse[TaskStatusResponse]) async def get_omni_video_task(task_id: str): """ 查询 Omni-Video 任务状态 查询指定 Omni-Video 任务的执行状态和结果。 """ try: provider = await get_klingai_provider() result = await provider.get_omni_video_task(task_id) return success_response(data=TaskStatusResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"查询 Omni-Video 任务失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/videos/omni", response_model=ApiResponse[dict]) async def list_omni_video_tasks( page: int = 1, page_size: int = 30, ): """ 查询 Omni-Video 任务列表 查询历史 Omni-Video 任务列表。 """ try: provider = await get_klingai_provider() result = await provider.list_omni_video_tasks( page=page, page_size=page_size, ) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"查询 Omni-Video 任务列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/videos/image2video", response_model=ApiResponse[VideoGenerateResponse]) async def create_image_to_video(data: Image2VideoRequest): """ 图生视频 根据输入图片生成视频。 """ try: provider = await get_klingai_provider() result = await provider.generate_video_from_image( image_url=data.image_url, prompt=data.prompt, model=data.model, duration=data.duration, aspect_ratio=data.aspect_ratio, mode=data.mode, callback_url=data.callback_url, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"图生视频失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/videos/extend", response_model=ApiResponse[VideoGenerateResponse]) async def extend_video( video_id: str, prompt: str | None = None, duration: int = 5, callback_url: str | None = None, ): """ 视频延长 延长现有视频的时长。 """ try: provider = await get_klingai_provider() result = await provider.extend_video( video_id=video_id, prompt=prompt, duration=duration, callback_url=callback_url, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"视频延长失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/videos/identify-face", response_model=ApiResponse[IdentifyFaceResponse]) async def identify_face(data: IdentifyFaceRequest): """ 对口型前置:人脸识别 分析视频中的人脸信息,返回 sessionId 和 faceId,用于后续的 advanced-lip-sync。 """ try: if not data.videoId and not data.videoUrl: raise HTTPException(status_code=400, detail="必须提供 videoId 或 videoUrl") provider = await get_klingai_provider() result = await provider.identify_face(video_id=data.videoId, video_url=data.videoUrl) return success_response(data=IdentifyFaceResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"人脸识别失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/videos/advanced-lip-sync", response_model=ApiResponse[VideoGenerateResponse]) async def create_advanced_lip_sync(data: AdvancedLipSyncRequest): """ 新版对口型视频生成 基于 KlingAI advanced-lip-sync 接口,先调用 /videos/identify-face 获取 sessionId 和 faceId, 再传入本接口生成对口型视频。 支持 audio_id(TTS 生成)或 soundFile(外部音频 URL)驱动口型。 """ try: provider = await get_klingai_provider() face_choose = [item.model_dump() for item in data.faceChoose] result = await provider.advanced_lip_sync( session_id=data.sessionId, face_choose=face_choose, callback_url=data.callbackUrl, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"对口型生成失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/videos/advanced-lip-sync/{taskId}", response_model=ApiResponse[TaskStatusResponse]) async def get_advanced_lip_sync_task(taskId: str): """ 查询新版对口型任务状态 """ try: provider = await get_klingai_provider() result = await provider.get_advanced_lip_sync_task(taskId) taskStatus = result.get("taskStatus", "unknown") videos = result.get("task_result", {}).get("videos", []) videoUrl = videos[0].get("url") if videos else None return success_response( data=TaskStatusResponse( taskId=result.get("taskId", taskId), taskStatus=taskStatus, createdAt=result.get("createdAt", 0), updatedAt=result.get("updatedAt", 0), videoUrl=videoUrl, errorMessage=result.get("taskStatus_msg"), ) ) except HTTPException: raise except Exception as e: logger.error(f"查询对口型任务失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/images/omni", response_model=ApiResponse[VideoGenerateResponse]) async def create_omni_image(data: OmniImageRequest): """ Omni-Image 图像生成 支持文本、主体、参考图等多种输入方式组合生成图像。 """ try: provider = await get_klingai_provider() result = await provider.generate_omni_image( prompt=data.prompt, model=data.model, aspect_ratio=data.aspect_ratio, resolution=data.resolution, result_type=data.result_type, n=data.n, element_list=data.element_list, image_list=data.image_list, callback_url=data.callback_url, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"Omni-Image 生成失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/images/omni/{task_id}", response_model=ApiResponse[TaskStatusResponse]) async def get_omni_image_task(task_id: str): """ 查询 Omni-Image 任务状态 """ try: provider = await get_klingai_provider() result = await provider.get_omni_image_task(task_id) task_status = result.get("task_status", "unknown") images = result.get("task_result", {}).get("images", []) image_url = images[0].get("url") if images else None return success_response( data=TaskStatusResponse( task_id=result.get("task_id", task_id), task_status=task_status, created_at=result.get("created_at", 0), updated_at=result.get("updated_at", 0), image_url=image_url, error_message=result.get("task_status_msg"), ) ) except HTTPException: raise except Exception as e: logger.error(f"查询 Omni-Image 任务失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/images/generations", response_model=ApiResponse[VideoGenerateResponse]) async def create_image(data: ImageGenerateRequest): """ 文生图 根据文本描述生成图像。 """ try: provider = await get_klingai_provider() result = await provider.generate_image( prompt=data.prompt, model=data.model, width=data.width, height=data.height, negative_prompt=data.negativePrompt, callback_url=data.callbackUrl, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"图像生成失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/virtual-tryon", response_model=ApiResponse[VideoGenerateResponse]) async def create_virtual_tryon(data: VirtualTryonRequest): """ 虚拟试穿 将衣服虚拟试穿到人物身上。 """ try: provider = await get_klingai_provider() result = await provider.virtual_tryon( person_image_url=data.person_imageUrl, cloth_image_url=data.cloth_imageUrl, callback_url=data.callbackUrl, ) return success_response(data=VideoGenerateResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"虚拟试穿失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/tasks/{taskId}", response_model=ApiResponse[TaskStatusResponse]) async def get_taskStatus( taskId: str, task_type: str = "video", ): """ 查询任务状态 查询指定任务的执行状态和结果。 Args: taskId: 任务 ID task_type: 任务类型 (video, image2video, image, lip-sync, virtual-tryon) """ try: provider = await get_klingai_provider() result = await provider.get_taskStatus( task_id=taskId, task_type=task_type, ) return success_response(data=TaskStatusResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"查询任务状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/tasks", response_model=ApiResponse[dict]) async def list_tasks( task_type: str = "video", page: int = 1, page_size: int = 10, ): """ 查询任务列表 查询历史任务列表。 """ try: provider = await get_klingai_provider() result = await provider.list_tasks( task_type=task_type, page=page, page_size=page_size, ) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"查询任务列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============ 主体管理 API ============ @router.post("/elements", response_model=ApiResponse[ElementResponse]) async def create_element(data: CreateElementRequest): """ 创建主体(自定义元素) 通过上传图片或视频创建可复用的主体,用于视频/图像生成时保持角色一致性。 图片要求: - 格式:jpg, jpeg, png - 大小:≤10MB - 数量:正面图 + 1-3张其他角度 视频要求: - 格式:mp4, mov - 时长:3-8秒 - 分辨率:1080P - 大小:≤200MB """ try: provider = await get_klingai_provider() imageList = None if data.element_imageList: imageList = { "frontalImage": data.element_imageList[0].imageUrl, "referImages": [ {"imageUrl": img.imageUrl, "name": img.name} for img in data.element_imageList[1:] if img.imageUrl ], } videoList = None if data.element_videoList: videoList = { "frontal_video": data.element_videoList[0].videoUrl, "referVideos": [ {"videoUrl": vid.videoUrl, "name": vid.name} for vid in data.element_videoList[1:] if vid.videoUrl ], } result = await provider.create_element( element_name=data.elementName, element_description=data.elementDescription, reference_type=data.referenceType, element_image_list=imageList, element_video_list=videoList, element_voice_id=data.element_voiceId, callback_url=data.callbackUrl, ) return success_response(data=ElementResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"创建主体失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/elements", response_model=ApiResponse[list[ElementResponse]]) async def list_elements(): """ 查询主体列表 获取所有已创建的主体(自定义元素)列表。 """ try: provider = await get_klingai_provider() result = await provider.list_elements() elements = [ElementResponse(**item) for item in result if isinstance(item, dict)] return success_response(data=elements) except HTTPException: raise except Exception as e: logger.error(f"查询主体列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/elements/{elementId}", response_model=ApiResponse[ElementResponse]) async def get_element(elementId: str): """ 查询单个主体详情 获取指定主体的详细信息。 """ try: provider = await get_klingai_provider() result = await provider.get_element(elementId) return success_response(data=ElementResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"查询主体详情失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/elements/{elementId}", response_model=ApiResponse[dict]) async def delete_element(elementId: str): """ 删除主体 删除不再使用的主体(自定义元素)。 """ try: provider = await get_klingai_provider() result = await provider.delete_element(elementId) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"删除主体失败: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============ 智能补全主体图 API ============ class AiMultiShotRequest(BaseModel): """智能补全主体图请求""" frontal_image: str = Field( ..., description="主体正面参考图 URL", example="https://example.com/front.jpg" ) callback_url: str | None = Field(None, description="回调通知地址") class AiMultiShotResponse(BaseModel): """智能补全主体图响应""" task_id: str task_status: str created_at: int updated_at: int @router.post("/elements/ai-multi-shot", response_model=ApiResponse[AiMultiShotResponse]) async def ai_multiShot(data: AiMultiShotRequest): """ 智能补全主体不同角度图片 通过主体正面图,自动推理出该主体其他角度图片。 每次可生成3组结果供选择,每次扣减0.5积分。 使用流程: 1. 调用此接口传入正面图 2. 轮询查询任务状态 3. 获取生成的多组角度图片 4. 选择合适的图片创建主体 """ try: provider = await get_klingai_provider() result = await provider.ai_multiShot( frontal_image=data.frontalImage, callback_url=data.callbackUrl, ) return success_response(data=AiMultiShotResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"智能补全主体图失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/elements/ai-multi-shot/{taskId}", response_model=ApiResponse[dict]) async def get_ai_multiShot_task(taskId: str): """ 查询智能补全主体图任务状态 获取指定任务的执行状态和生成的多角度图片结果。 """ try: provider = await get_klingai_provider() result = await provider.get_ai_multiShot_task(taskId) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"查询智能补全任务失败: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============ 自定义音色 API ============ @router.post("/voices/custom", response_model=ApiResponse[CreateCustomVoiceResponse]) async def create_custom_voice(data: CreateCustomVoiceRequest): """ 创建自定义音色 通过上传音频文件或引用历史视频创建自定义音色,用于对口型视频。 音频要求: - 格式:mp3, wav, mp4, mov - 时长:5-30 秒 - 人声干净、无杂音、单一人声 """ try: provider = await get_klingai_provider() result = await provider.create_custom_voice( voice_name=data.voiceName, audio_url=data.audioUrl, video_url=data.videoUrl, video_id=data.videoId, callback_url=data.callbackUrl, external_task_id=data.externalTaskId, ) return success_response(data=CreateCustomVoiceResponse(**result)) except HTTPException: raise except Exception as e: logger.error(f"创建自定义音色失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/voices/custom", response_model=ApiResponse[list[VoiceInfo]]) async def list_custom_voices(): """ 查询自定义音色列表 获取所有已创建的自定义音色。 """ try: provider = await get_klingai_provider() result = await provider.list_custom_voices() voices = [] for item in result: if isinstance(item, dict) and "task_result" in item: task_result = item.get("task_result", {}) voices_data = task_result.get("voices", []) for voice in voices_data: voices.append(VoiceInfo(**voice)) return success_response(data=voices) except HTTPException: raise except Exception as e: logger.error(f"查询自定义音色列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/voices/custom/{voiceId}", response_model=ApiResponse[dict]) async def get_custom_voice(voiceId: str): """ 查询单个自定义音色 获取指定自定义音色的详细信息。 """ try: provider = await get_klingai_provider() result = await provider.get_custom_voice(voiceId) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"查询自定义音色失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/voices/presets", response_model=ApiResponse[list[VoiceInfo]]) async def list_preset_voices(): """ 查询官方预设音色列表 获取 KlingAI 提供的官方音色列表。 """ try: provider = await get_klingai_provider() result = await provider.list_preset_voices() voices = [] for item in result: if isinstance(item, dict) and "task_result" in item: task_result = item.get("task_result", {}) voices_data = task_result.get("voices", []) for voice in voices_data: voices.append(VoiceInfo(**voice)) return success_response(data=voices) except HTTPException: raise except Exception as e: logger.error(f"查询官方音色列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/voices/custom/{voiceId}", response_model=ApiResponse[dict]) async def delete_custom_voice(voiceId: str): """ 删除自定义音色 删除不再使用的自定义音色。 """ try: provider = await get_klingai_provider() result = await provider.delete_custom_voice(voiceId) return success_response(data=result) except HTTPException: raise except Exception as e: logger.error(f"删除自定义音色失败: {e}") raise HTTPException(status_code=500, detail=str(e))