aiVideoGenerationService = $aiVideoGenerationService; } /** * 创建视频生成任务(即梦AI) * * @param Request $request * @return JsonResponse */ public function createJimengTask(Request $request): JsonResponse { $data = $request->all(); // 首先验证基本参数 $baseRules = [ 'prompt' => 'required|string|max:1000', 'seed' => 'required|integer', 'video_duration' => 'required|in:5,10', 'video_resolution' => 'required|in:720P,1080P' ]; $baseMessages = [ 'prompt.required' => '提示词不能为空', 'prompt.max' => '提示词不能超过1000个字符', 'seed.required' => '随机种子不能为空', 'seed.integer' => '随机种子必须是整数', 'video_duration.required' => '请选择视频时长', 'video_duration.in' => '视频时长只能是5s或10s', 'video_resolution.required' => '请选择视频分辨率', 'video_resolution.in' => '视频分辨率只能是720P或1080P', ]; // 动态构建验证规则 $validationRules = $baseRules; $validationMessages = $baseMessages; // 检查是否有首帧URL if (isset($data['first_frame_url'])) { $validationRules['first_frame_url'] = 'url'; $validationMessages['first_frame_url.url'] = '首帧图片URL格式不正确'; } // 检查是否有尾帧URL if (isset($data['tail_frame_url'])) { $validationRules['tail_frame_url'] = 'url'; $validationMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确'; } // 检查是否有首帧文件 if ($request->hasFile('first_frame_file')) { $validationRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $validationMessages = array_merge($validationMessages, [ 'first_frame_file.image' => '首帧必须是图片文件', 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式', 'first_frame_file.max' => '首帧图片大小不能超过4.7MB', ]); } // 检查是否有尾帧文件 if ($request->hasFile('tail_frame_file')) { $validationRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $validationMessages = array_merge($validationMessages, [ 'tail_frame_file.image' => '尾帧必须是图片文件', 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式', 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB', ]); } if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) { Utils::throwError('1002:提供尾帧URL时必须提供首帧URL'); } if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) { Utils::throwError('1002:提供尾帧文件时必须提供首帧文件'); } $validator = Validator::make($data, $validationRules, $validationMessages); if ($validator->fails()) { Utils::throwError('1002:'.$validator->errors()->first()); } $firstFrame = null; $tailFrame = null; // 处理首帧图片(文件上传方式) if ($request->hasFile('first_frame_file')) { $firstFrameFile = $request->file('first_frame_file'); $firstFrame = $this->processFrameFile($firstFrameFile, 'first'); $data['first_frame_url'] = $firstFrame['url']; } // 处理首帧图片(URL方式) elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) { $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true); if (!$firstFrame) { Utils::throwError('1003:首帧图片获取失败'); } } // 处理尾帧图片(文件上传方式) if ($request->hasFile('tail_frame_file')) { $tailFrameFile = $request->file('tail_frame_file'); $tailFrame = $this->processFrameFile($tailFrameFile, 'tail'); $data['tail_frame_url'] = $tailFrame['url']; } // 处理尾帧图片(URL方式) elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) { $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true); if (!$tailFrame) { Utils::throwError('1003:尾帧图片获取失败'); } } // 验证首帧图片规格(如果存在) if ($firstFrame) { $this->validateFrameSpecs($firstFrame, '首帧'); } // 验证尾帧图片规格(如果存在) if ($tailFrame) { $this->validateFrameSpecs($tailFrame, '尾帧'); } // 如果首尾帧都存在,验证比例是否相同 if ($firstFrame && $tailFrame) { $firstRatio = round($firstFrame['width'] / $firstFrame['height'], 2); $tailRatio = round($tailFrame['width'] / $tailFrame['height'], 2); if ($firstRatio != $tailRatio) { Utils::throwError('1003:尾帧图片与首帧图片比例必须相同'); } } $task = $this->aiVideoGenerationService->createJimengTask($data); return $this->success(['task_id' => $task->id, 'status' => $task->status]); } /** * 查询视频任务状态 * * @param string $taskId * @return JsonResponse */ public function taskStatus(string $taskId): JsonResponse { $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first(); if (!$task) { return response()->json([ 'success' => false, 'message' => '任务不存在' ], 404); } return response()->json([ 'success' => true, 'data' => [ 'task_id' => $task->id, 'status' => $task->status, 'result_url' => $task->result_url, 'error_message' => $task->error_message, 'created_at' => $task->created_at, 'started_at' => $task->started_at, 'completed_at' => $task->completed_at ] ]); } /** * 处理帧图片文件 * * @param \Illuminate\Http\UploadedFile $file * @param string $frameType 帧类型(first/tail) * @return array */ private function processFrameFile($file, string $frameType): array { // 验证图片信息 $frameInfo = @getimagesize($file->getPathname()); if (!$frameInfo) { Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片格式无效'); } $frameData = [ 'width' => $frameInfo[0], 'height' => $frameInfo[1], 'size' => $file->getSize(), ]; // 上传图片到服务器 try { $frameData['url'] = uploadFileByTos('image', $file); } catch (\Exception $e) { Utils::throwError('1003:' . ($frameType === 'first' ? '首帧' : '尾帧') . '图片上传失败:' . $e->getMessage()); } return $frameData; } /** * 验证帧图片规格 * * @param array $frame * @param string $frameName * @return void */ private function validateFrameSpecs(array $frame, string $frameName): void { // 1.图片文件最大4.7MB if ($frame['size'] > 13 * 1024 * 1024) { Utils::throwError('1003:' . $frameName . '图片大小不能超过10MB'); } // 2.图片分辨率最大: 4096*4096, 最短边不低于320 $minDimension = min($frame['width'], $frame['height']); $maxDimension = max($frame['width'], $frame['height']); if ($maxDimension > 4096 || $minDimension < 320) { Utils::throwError('1003:' . $frameName . '图片分辨率不能超过4096*4096,且最短边不能低于320'); } // 3.图片长边与短边比例在3以内 if ($maxDimension / $minDimension > 3) { Utils::throwError('1003:' . $frameName . '图片长边与短边比例不能超过3'); } } /** * 创建 Seedance 1.5 Pro 视频生成任务 * * @param Request $request * @return JsonResponse */ public function createSeedanceTask(Request $request): JsonResponse { $data = $request->all(); // 验证基本参数 $baseRules = [ 'model' => 'required|string|in:doubao-seedance-1-5-pro-251215,doubao-seedance-1-0-pro-250528,doubao-seedance-1-0-pro-fast-251015', 'prompt' => 'required|max:1000', // 'content' => 'required|array|min:1', ]; $baseMessages = [ 'model.required' => '模型参数不能为空', 'model.in' => '模型只支持 doubao-seedance-1-5-pro-251215、doubao-seedance-1-0-pro-250528、doubao-seedance-1-0-pro-fast-251015', 'prompt.required' => '提示词不能为空', 'prompt.max' => '提示词不能超过1000个字符', // 'content.required' => '内容参数不能为空', // 'content.array' => '内容参数必须是数组', // 'content.min' => '内容参数至少包含一个元素', ]; // 可选参数验证 $optionalRules = [ 'callback_url' => 'nullable|url', 'return_last_frame' => 'nullable|boolean', 'service_tier' => 'nullable|in:default,flex', 'execution_expires_after' => 'nullable|integer|min:3600|max:259200', 'generate_audio' => 'nullable|boolean', 'draft' => 'nullable|boolean', 'video_resolution' => 'required|in:480P,720P,1080P', 'ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9,adaptive', 'video_duration' => 'required|integer|min:2|max:12', 'frames' => 'nullable|integer|min:29|max:289', 'seed' => 'nullable|integer|min:-1|max:4294967295', 'camera_fixed' => 'nullable|boolean', 'watermark' => 'nullable|boolean', ]; $optionalMessages = [ 'callback_url.url' => '回调地址格式不正确', 'service_tier.in' => '服务等级只能是 default 或 flex', 'execution_expires_after.integer' => '任务超时时间必须是整数', 'execution_expires_after.min' => '任务超时时间不能少于3600秒', 'execution_expires_after.max' => '任务超时时间不能超过259200秒', 'video_resolution.required' => '请选择视频分辨率', 'video_resolution.in' => '分辨率只能是 480P、720P 或 1080P', 'ratio.in' => '宽高比参数不正确', 'video_duration.required' => '请填写视频时长', 'video_duration.integer' => '视频时长必须是整数', 'video_duration.min' => '视频时长不能少于2秒', 'video_duration.max' => '视频时长不能超过12秒', 'frames.integer' => '帧数必须是整数', 'frames.min' => '帧数不能少于29', 'frames.max' => '帧数不能超过289', 'seed.integer' => '随机种子必须是整数', 'seed.min' => '随机种子不能小于-1', 'seed.max' => '随机种子不能超过4294967295', ]; $validationRules = array_merge($baseRules, $optionalRules); $validationMessages = array_merge($baseMessages, $optionalMessages); // 检查是否有首帧URL if (isset($data['first_frame_url'])) { $optionalRules['first_frame_url'] = 'url'; $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确'; } // 检查是否有尾帧URL if (isset($data['tail_frame_url'])) { $optionalRules['tail_frame_url'] = 'url'; $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确'; } // 检查是否有首帧文件 if ($request->hasFile('first_frame_file')) { $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $optionalMessages = array_merge($optionalMessages, [ 'first_frame_file.image' => '首帧必须是图片文件', 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式', 'first_frame_file.max' => '首帧图片大小不能超过4.7MB', ]); } // 检查是否有尾帧文件 if ($request->hasFile('tail_frame_file')) { $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $optionalMessages = array_merge($optionalMessages, [ 'tail_frame_file.image' => '尾帧必须是图片文件', 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式', 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB', ]); } if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) { Utils::throwError('1002:提供尾帧URL时必须提供首帧URL'); } if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) { Utils::throwError('1002:提供尾帧文件时必须提供首帧文件'); } $validator = Validator::make($data, $validationRules, $validationMessages); if ($validator->fails()) { Utils::throwError('1002:'.$validator->errors()->first()); } $data['content'] = [ [ 'type' => 'text', 'text' => $data['prompt'], ], ]; $firstFrame = null; $tailFrame = null; // 以下首尾帧做模型兼容: // doubao-seedance-1-0-pro-fast-251015只支持文生视频、图生视频-首帧 // 处理首帧图片(文件上传方式) if ($request->hasFile('first_frame_file')) { $firstFrameFile = $request->file('first_frame_file'); $firstFrame = $this->processFrameFile($firstFrameFile, 'first'); $data['first_frame_url'] = $firstFrame['url']; $data['content'][] = [ 'type' => 'image_url', 'image_url' => [ 'url' => $data['first_frame_url'], ], 'role' => 'first_frame', ]; } // 处理首帧图片(URL方式) elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) { $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true); if (!$firstFrame) { Utils::throwError('1003:首帧图片获取失败'); } $data['content'][] = [ 'type' => 'image_url', 'image_url' => [ 'url' => $data['first_frame_url'], ], 'role' => 'first_frame', ]; } // 处理尾帧图片(文件上传方式) if ($request->hasFile('tail_frame_file')) { $tailFrameFile = $request->file('tail_frame_file'); $tailFrame = $this->processFrameFile($tailFrameFile, 'tail'); if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) { $data['tail_frame_url'] = $tailFrame['url']; $data['content'][] = [ 'type' => 'image_url', 'image_url' => [ 'url' => $data['tail_frame_url'], ], 'role' => 'last_frame', ]; } } // 处理尾帧图片(URL方式) elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) { $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true); if (!$tailFrame) { Utils::throwError('1003:尾帧图片获取失败'); } if (!in_array($data['model'], ['doubao-seedance-1-0-pro-fast-251015'])) { $data['content'][] = [ 'type' => 'image_url', 'image_url' => [ 'url' => $data['tail_frame_url'], ], 'role' => 'last_frame', ]; }else { $data['tail_frame_url'] = ''; } } // 验证模型参数 if (!in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) { $data['generate_audio'] = false; $data['draft'] = false; } if (in_array($data['model'], ['doubao-seedance-1-5-pro-251215'])) { if ($data['video_duration'] < 4) $data['video_duration'] = 4; if (!isset($data['ratio'])) $data['ratio'] = 'adaptive'; } // 验证 content 数组内容 $this->validateSeedanceContent($data['content']); $task = $this->aiVideoGenerationService->createSeedanceTask($data); return $this->success(['task_id' => $task->id, 'status' => $task->status]); } public function seedanceCallback(Request $request) { $data = $request->all(); $result = $this->aiVideoGenerationService->seedanceCallback($data); return $this->success(['success'=> $result ? 1 : 0]); } /** * 查询 Seedance 1.5 Pro 任务状态 * * @param string $taskId * @return JsonResponse */ public function getSeedanceTaskStatus(string $taskId): JsonResponse { try { $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first(); if (!$task) { return response()->json([ 'success' => false, 'message' => '任务不存在' ], 404); } $result = $this->aiVideoGenerationService->querySeedanceTaskStatus($task); return $this->success($result); } catch (\Exception $e) { Utils::throwError('1001:'.$e->getMessage()); } } /** * 验证 Seedance content 内容 * * @param array $content * @return void */ private function validateSeedanceContent(array $content): void { $hasText = false; $hasFirstFrame = false; $hasLastFrame = false; $hasReferenceImage = false; $hasDraftTask = false; foreach ($content as $index => $item) { if (!isset($item['type'])) { Utils::throwError('1002:content[' . $index . '] 缺少 type 字段'); } switch ($item['type']) { case 'text': if (!isset($item['text']) || empty($item['text'])) { Utils::throwError('1002:text 类型的 content 必须包含非空的 text 字段'); } if (mb_strlen($item['text']) > 1000) { Utils::throwError('1002:文本提示词不能超过1000个字符'); } $hasText = true; break; case 'image_url': if (!isset($item['image_url']['url']) || empty($item['image_url']['url'])) { Utils::throwError('1002:image_url 类型的 content 必须包含 image_url.url 字段'); } $role = $item['role'] ?? null; if ($role === 'first_frame') { $hasFirstFrame = true; } elseif ($role === 'last_frame') { $hasLastFrame = true; } elseif ($role === 'reference_image') { $hasReferenceImage = true; } elseif (empty($role)) { // 默认为首帧 $hasFirstFrame = true; } break; case 'draft_task': if (!isset($item['draft_task']['id']) || empty($item['draft_task']['id'])) { Utils::throwError('1002:draft_task 类型的 content 必须包含 draft_task.id 字段'); } $hasDraftTask = true; break; default: Utils::throwError('1002:不支持的 content type: ' . $item['type']); } } // 验证组合规则 if ($hasLastFrame && !$hasFirstFrame) { Utils::throwError('1002:使用尾帧时必须同时提供首帧'); } if ($hasReferenceImage && ($hasFirstFrame || $hasLastFrame)) { Utils::throwError('1002:参考图模式不能与首帧/尾帧模式混用'); } if ($hasDraftTask && ($hasText || $hasFirstFrame || $hasLastFrame || $hasReferenceImage)) { Utils::throwError('1002:使用样片任务时不能同时提供其他内容'); } } /** * 创建可灵AI Omni视频生成任务 * * @param Request $request * @return JsonResponse */ public function createKelingTask(Request $request): JsonResponse { $data = $request->all(); // 验证基本参数 $baseRules = [ 'model' => 'required|string|in:kling-video-o1,kling-v3-omni', 'prompt' => 'required|max:2500', 'mode' => 'nullable|in:pro,std', 'aspect_ratio' => 'nullable|in:16:9,4:3,1:1,3:4,9:16,21:9', 'video_duration' => 'required|integer|min:3|max:15', ]; $baseMessages = [ 'model.required' => '模型参数不能为空', 'model.in' => '模型只支持 kling-video-o1、kling-v3-omni', 'prompt.required' => '提示词不能为空', 'prompt.max' => '提示词不能超过2500个字符', 'mode.in' => '模式只能是 pro 或 std', 'aspect_ratio.in' => '宽高比只能是 16:9、4:3、1:1、3:4、9:16、21:9', 'video_duration.required' => '请填写视频时长', 'video_duration.integer' => '视频时长必须是整数', 'video_duration.min' => '视频时长不能少于3秒', 'video_duration.max' => '视频时长不能超过15秒', ]; // 可选参数验证 $optionalRules = [ 'multi_prompt' => 'nullable|array', 'multi_prompt.*' => 'string|max:2500', 'shot_type' => 'nullable|in:customize,single_shot,multi_shot', 'generate_audio' => 'nullable|boolean', 'webhook_url' => 'nullable|url', 'seed' => 'nullable|integer|min:-1|max:4294967295', 'video_resolution' => 'nullable|in:480P,720P,1080P', ]; $optionalMessages = [ 'multi_prompt.array' => '多段提示词必须是数组', 'multi_prompt.*.string' => '每段提示词必须是字符串', 'multi_prompt.*.max' => '每段提示词不能超过2500个字符', 'shot_type.in' => '镜头类型只能是 customize、single_shot、multi_shot', 'webhook_url.url' => '回调地址格式不正确', 'seed.integer' => '随机种子必须是整数', 'seed.min' => '随机种子不能小于-1', 'seed.max' => '随机种子不能超过4294967295', 'video_resolution.in' => '分辨率只能是 480P、720P 或 1080P', ]; // 检查是否有首帧URL if (isset($data['first_frame_url'])) { $optionalRules['first_frame_url'] = 'url'; $optionalMessages['first_frame_url.url'] = '首帧图片URL格式不正确'; } // 检查是否有尾帧URL if (isset($data['tail_frame_url'])) { $optionalRules['tail_frame_url'] = 'url'; $optionalMessages['tail_frame_url.url'] = '尾帧图片URL格式不正确'; } // 检查是否有首帧文件 if ($request->hasFile('first_frame_file')) { $optionalRules['first_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $optionalMessages = array_merge($optionalMessages, [ 'first_frame_file.image' => '首帧必须是图片文件', 'first_frame_file.mimes' => '首帧图片必须是jpeg,png格式', 'first_frame_file.max' => '首帧图片大小不能超过4.7MB', ]); } // 检查是否有尾帧文件 if ($request->hasFile('tail_frame_file')) { $optionalRules['tail_frame_file'] = 'image|mimes:jpeg,png|max:4800'; $optionalMessages = array_merge($optionalMessages, [ 'tail_frame_file.image' => '尾帧必须是图片文件', 'tail_frame_file.mimes' => '尾帧图片必须是jpeg,png格式', 'tail_frame_file.max' => '尾帧图片大小不能超过4.7MB', ]); } if (isset($data['tail_frame_url']) && !isset($data['first_frame_url'])) { Utils::throwError('1002:提供尾帧URL时必须提供首帧URL'); } if ($request->hasFile('tail_frame_file') && !$request->has('first_frame_file')) { Utils::throwError('1002:提供尾帧文件时必须提供首帧文件'); } $validationRules = array_merge($baseRules, $optionalRules); $validationMessages = array_merge($baseMessages, $optionalMessages); // 处理图片相关参数 $validator = Validator::make($data, $validationRules, $validationMessages); if ($validator->fails()) { Utils::throwError('1002:'.$validator->errors()->first()); } $firstFrame = null; $tailFrame = null; $data['image_list'] = []; // 处理首帧图片(文件上传方式) if ($request->hasFile('first_frame_file')) { $firstFrameFile = $request->file('first_frame_file'); $firstFrame = $this->processFrameFile($firstFrameFile, 'first'); $data['first_frame_url'] = $firstFrame['url']; $data['image_list'][] = [ 'image_url' => $data['first_frame_url'], 'type' => 'first_frame', ]; } // 处理首帧图片(URL方式) elseif (isset($data['first_frame_url']) && !empty($data['first_frame_url'])) { $firstFrame = Utils::getRemoteImageInfo($data['first_frame_url'], true); if (!$firstFrame) { Utils::throwError('1003:首帧图片获取失败'); } $data['image_list'][] = [ 'image_url' => $data['first_frame_url'], 'type' => 'first_frame', ]; } // 处理尾帧图片(文件上传方式) if ($request->hasFile('tail_frame_file')) { $tailFrameFile = $request->file('tail_frame_file'); $tailFrame = $this->processFrameFile($tailFrameFile, 'tail'); $data['tail_frame_url'] = $tailFrame['url']; $data['image_list'][] = [ 'image_url' => $data['tail_frame_url'], 'type' => 'end_frame', ]; } // 处理尾帧图片(URL方式) elseif (isset($data['tail_frame_url']) && !empty($data['tail_frame_url'])) { $tailFrame = Utils::getRemoteImageInfo($data['tail_frame_url'], true); if (!$tailFrame) { Utils::throwError('1003:尾帧图片获取失败'); } $data['image_list'][] = [ 'image_url' => $data['tail_frame_url'], 'type' => 'end_frame', ]; } $task = $this->aiVideoGenerationService->createKelingOmniTask($data); return $this->success(['task_id' => $task->id, 'status' => $task->status]); } /** * 查询可灵AI Omni任务状态 * * @param string $taskId * @return JsonResponse */ public function getKelingTaskStatus(string $taskId): JsonResponse { try { $task = \App\Models\MpGenerateVideoTask::where('id', $taskId)->first(); if (!$task) { return response()->json([ 'success' => false, 'message' => '任务不存在' ], 404); } $result = $this->aiVideoGenerationService->queryKelingOmniTaskStatus($task); return $this->success($result); } catch (\Exception $e) { Utils::throwError('1001:'.$e->getMessage()); } } /** * 获取视频任务列表 * * @param Request $request * @return mixed */ public function taskList(Request $request) { $data = $request->all(); $result = $this->aiVideoGenerationService->getTaskList($data); return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']); } // ==================== 主体管理相关接口 ==================== /** * 创建主体 * * @param Request $request * @return JsonResponse */ public function createElement(Request $request): JsonResponse { try { // 验证请求参数 $validated = $request->validate([ 'element_name' => 'required|string|max:20', 'element_description' => 'required|string|max:100', 'reference_type' => 'required|in:image_refer,video_refer', 'element_image_list' => 'required_if:reference_type,image_refer|array', 'element_image_list.frontal_image' => 'required_if:reference_type,image_refer|string', 'element_image_list.refer_images' => 'required_if:reference_type,image_refer|array|min:1|max:3', 'element_image_list.refer_images.*.image_url' => 'required|string', 'element_video_list' => 'required_if:reference_type,video_refer|array', 'element_video_list.refer_videos' => 'required_if:reference_type,video_refer|array|size:1', 'element_video_list.refer_videos.*.video_url' => 'required|string', 'element_voice_id' => 'nullable|string', 'tag_list' => 'nullable|array', 'tag_list.*.tag_id' => 'required|in:o_101,o_102,o_103,o_104,o_105,o_106,o_107,o_108', 'callback_url' => 'nullable|url', 'external_task_id' => 'nullable|string', ]); // 调用服务层创建主体 $element = $this->aiVideoGenerationService->createElement($validated); return response()->json([ 'code' => 0, 'message' => '主体创建任务已提交', 'request_id' => uniqid(), 'data' => [ 'task_id' => $element->id, 'task_info' => [ 'external_task_id' => $element->external_task_id, ], 'task_status' => $element->task_status, 'created_at' => $element->created_at->timestamp * 1000, 'updated_at' => $element->updated_at->timestamp * 1000, ] ]); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'code' => 400, 'message' => '参数验证失败', 'request_id' => uniqid(), 'errors' => $e->errors() ], 400); } catch (\Exception $e) { dLog('generate')->error('创建主体失败: ' . $e->getMessage()); return response()->json([ 'code' => 500, 'message' => '创建主体失败: ' . $e->getMessage(), 'request_id' => uniqid(), ], 500); } } /** * 查询单个主体状态 * * @param string $taskId * @param Request $request * @return JsonResponse */ public function getElementStatus(string $taskId, Request $request): JsonResponse { try { $externalTaskId = $request->get('external_task_id'); // 先查询本地数据库 $query = AIElement::where('id', $taskId); if ($externalTaskId) { $query->orWhere('external_task_id', $externalTaskId); } $localElement = $query->first(); if (!$localElement) { return response()->json([ 'code' => 404, 'message' => '主体任务不存在', 'request_id' => uniqid(), ], 404); } // 如果任务还在处理中,查询最新状态 if ($localElement->task_status === AIElement::STATUS_PROCESSING) { $apiResult = $this->aiVideoGenerationService->queryElementStatus($taskId, $externalTaskId); if ($apiResult['success'] && isset($apiResult['data'])) { $localElement->refresh(); // 重新加载数据 } } // 构建响应数据 $responseData = [ 'task_id' => $localElement->id, 'task_status' => $localElement->task_status, 'task_status_msg' => $localElement->task_status_msg, 'task_info' => [ 'external_task_id' => $localElement->external_task_id, ], 'created_at' => $localElement->created_at->timestamp * 1000, 'updated_at' => $localElement->updated_at->timestamp * 1000, ]; // 如果任务成功,添加主体结果信息 if ($localElement->task_status === AIElement::STATUS_SUCCEED && $localElement->element_id) { $responseData['task_result'] = [ 'elements' => [ [ 'element_id' => $localElement->element_id, 'element_name' => $localElement->element_name, 'element_description' => $localElement->element_description, 'reference_type' => $localElement->reference_type, 'element_image_list' => $localElement->element_image_list, 'element_video_list' => $localElement->element_video_list, 'element_voice_info' => $localElement->element_voice_info, 'tag_list' => $localElement->tag_list, 'owned_by' => $localElement->owned_by, 'status' => $localElement->status, ] ] ]; $responseData['final_unit_deduction'] = $localElement->final_unit_deduction; } return response()->json([ 'code' => 0, 'message' => 'success', 'request_id' => uniqid(), 'data' => $responseData ]); } catch (\Exception $e) { dLog('generate')->error('查询主体状态失败: ' . $e->getMessage()); return response()->json([ 'code' => 500, 'message' => '查询主体状态失败: ' . $e->getMessage(), 'request_id' => uniqid(), ], 500); } } /** * 查询所有主体列表(包含自定义和官方) * * @param Request $request * @return JsonResponse */ public function getElementsList(Request $request): JsonResponse { try { $pageNum = (int)$request->get('pageNum', 1); $pageSize = (int)$request->get('pageSize', 30); $type = $request->get('type', 'all'); // all, custom, official // 限制分页参数范围 $pageNum = max(1, min($pageNum, 1000)); $pageSize = max(1, min($pageSize, 500)); $customElements = []; $officialElements = []; // 查询自定义主体 if (in_array($type, ['all', 'custom'])) { $customResult = $this->aiVideoGenerationService->queryCustomElementsList($pageNum, $pageSize); if ($customResult['success']) { $customElements = $customResult['data']; } } // 查询官方主体 if (in_array($type, ['all', 'official'])) { $officialResult = $this->aiVideoGenerationService->queryOfficialElementsList($pageNum, $pageSize); if ($officialResult['success']) { $officialElements = $officialResult['data']; } } // 合并结果 $allElements = array_merge($customElements, $officialElements); return response()->json([ 'code' => 0, 'message' => 'success', 'request_id' => uniqid(), 'data' => $allElements ]); } catch (\Exception $e) { dLog('generate')->error('查询主体列表失败: ' . $e->getMessage()); return response()->json([ 'code' => 500, 'message' => '查询主体列表失败: ' . $e->getMessage(), 'request_id' => uniqid(), ], 500); } } /** * 删除自定义主体 * * @param Request $request * @return JsonResponse */ public function deleteElement(Request $request): JsonResponse { try { // 验证请求参数 $validated = $request->validate([ 'element_id' => 'required|string', ]); // 检查主体是否存在且为自定义主体 $element = AIElement::where('element_id', $validated['element_id'])->first(); if (!$element) { return response()->json([ 'code' => 404, 'message' => '主体不存在', 'request_id' => uniqid(), ], 404); } if ($element->owned_by === 'kling') { return response()->json([ 'code' => 400, 'message' => '不能删除官方主体', 'request_id' => uniqid(), ], 400); } // 调用服务层删除主体 $result = $this->aiVideoGenerationService->deleteElement($validated['element_id']); if ($result['success']) { return response()->json([ 'code' => 0, 'message' => $result['message'], 'request_id' => uniqid(), 'data' => $result['data'] ]); } else { return response()->json([ 'code' => 500, 'message' => $result['message'], 'request_id' => uniqid(), ], 500); } } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'code' => 400, 'message' => '参数验证失败', 'request_id' => uniqid(), 'errors' => $e->errors() ], 400); } catch (\Exception $e) { dLog('generate')->error('删除主体失败: ' . $e->getMessage()); return response()->json([ 'code' => 500, 'message' => '删除主体失败: ' . $e->getMessage(), 'request_id' => uniqid(), ], 500); } } /** * 获取本地主体列表(用于内部管理) * * @param Request $request * @return JsonResponse */ public function getLocalElementsList(Request $request): JsonResponse { try { // 构建筛选条件 $filters = [ 'owned_by' => $request->get('owned_by'), 'reference_type' => $request->get('reference_type'), 'status' => $request->get('status'), 'task_status' => $request->get('task_status'), 'keyword' => $request->get('keyword'), 'page_size' => (int)$request->get('page_size', 20), ]; // 移除空值 $filters = array_filter($filters, function($value) { return $value !== null && $value !== ''; }); // 调用服务层获取列表 $elements = $this->aiVideoGenerationService->getLocalElementsList($filters); // 转换数据格式 $data = $elements->map(function ($element) { return [ 'task_id' => $element->id, 'external_task_id' => $element->external_task_id, 'task_status' => $element->task_status, 'task_status_msg' => $element->task_status_msg, 'element_id' => $element->element_id, 'element_name' => $element->element_name, 'element_description' => $element->element_description, 'reference_type' => $element->reference_type, 'element_image_list' => $element->element_image_list, 'element_video_list' => $element->element_video_list, 'element_voice_id' => $element->element_voice_id, 'element_voice_info' => $element->element_voice_info, 'tag_list' => $element->tag_list, 'tag_names' => $element->tag_names, // 使用模型的访问器 'owned_by' => $element->owned_by, 'status' => $element->status, 'final_unit_deduction' => $element->final_unit_deduction, 'created_at' => $element->created_at->format('Y-m-d H:i:s'), 'updated_at' => $element->updated_at->format('Y-m-d H:i:s'), ]; }); return response()->json([ 'code' => 0, 'message' => 'success', 'request_id' => uniqid(), 'data' => [ 'data' => $data, 'current_page' => $elements->currentPage(), 'last_page' => $elements->lastPage(), 'per_page' => $elements->perPage(), 'total' => $elements->total(), ] ]); } catch (\Exception $e) { dLog('generate')->error('获取本地主体列表失败: ' . $e->getMessage()); return response()->json([ 'code' => 500, 'message' => '获取本地主体列表失败: ' . $e->getMessage(), 'request_id' => uniqid(), ], 500); } } }