volcEngineService = $volcEngineService; } /** * 异步创建视频生成任务 * * @param array $params * @return MpGenerateVideoTask */ public function createJimengTask(array $params): MpGenerateVideoTask { // 生成唯一的任务ID $taskId = 'vid_gen_' . time() . '_' . uniqid(); // 创建任务记录 $task = MpGenerateVideoTask::create([ 'task_id' => $taskId, 'prompt' => $params['prompt'] ?? '', 'first_frame_url' => $params['first_frame_url'] ?? null, 'tail_frame_url' => $params['tail_frame_url'] ?? null, 'seed' => $params['seed'] ?? -1, 'frames' => $params['video_duration'] ? ($params['video_duration'] * 24 + 1) : 121, 'video_resolution' => $params['video_resolution'] ?? '720P', 'status' => MpGenerateVideoTask::STATUS_PENDING, 'api_type' => 'jimeng', ]); // 发送异步请求到即梦AI API创建任务 $this->submitTaskToApi($task); return $task; } /** * 提交任务到即梦AI API * * @param MpGenerateVideoTask $task * @return void */ private function submitTaskToApi(MpGenerateVideoTask $task): void { try { // 验证环境变量配置 $accessKey = env('VOLC_AI_AK'); $secretKey = env('VOLC_AI_SK'); if (empty($accessKey) || empty($secretKey)) { $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '火山引擎访问密钥未配置,请检查环境变量AK和SK' ]); return; } // 根据任务类型选择对应的req_key if ($task->first_frame_url && $task->tail_frame_url) { $req_key = $task->video_resolution == '720P' ? 'jimeng_i2v_first_tail_v30' : 'jimeng_i2v_first_tail_v30_1080'; }else if($task->first_frame_url){ $req_key = $task->video_resolution == '720P' ? 'jimeng_i2v_first_v30' : 'jimeng_i2v_first_v30_1080'; }else{ $req_key = $task->video_resolution == '720P' ? 'jimeng_t2v_v30' : 'jimeng_t2v_v30_1080p'; } // 构建即梦AI 3.0 视频生成API请求参数 $apiParams = [ 'req_key' => $req_key, 'prompt' => $task->prompt, 'seed' => (int)$task->seed, 'frames' => $task->frames, ]; // 添加图片URL参数 if ($task->first_frame_url) { $apiParams['image_urls'] = [ $task->first_frame_url ]; if ($task->tail_frame_url) $apiParams['image_urls'][] = $task->tail_frame_url; } dLog('generate')->info('图生视频参数: ', $apiParams); logDB('generate', 'info', '即梦AI视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]); // 调用即梦AI 3.0 视频生成API提交任务 $response = $this->volcEngineService->request( 'POST', 'visual.volcengineapi.com', // 即梦AI API域名 '/', // 使用根路径 [], // Query参数 json_encode($apiParams), $accessKey, $secretKey, 'cv', // 服务标识 'cn-north-1', // 区域 'CVSync2AsyncSubmitTask', '2022-08-31', 'application/json' ); $responseData = json_decode($response['body'], true); dLog('generate')->info('即梦AI 3.0 视频生成API提交任务响应: ', $responseData); logDB('generate', 'info', '即梦AI视频生成API响应', ['id' => $task->id, 'response' => $responseData]); if ($responseData['code'] !== 10000) { // API返回错误 logDB('generate', 'error', '即梦AI视频生成任务提交失败', ['id' => $task->id, 'error' => $responseData['message'] ?? 'API Error']); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => $responseData['message'] ?? 'API Error', 'extra_params' => $responseData['data'] ?? [] ]); } else { // 任务提交成功,更新状态为处理中 $updateData = []; // 存储API返回的任务ID if (isset($responseData['data']['task_id'])) { $updateData['task_id'] = $responseData['data']['task_id']; $updateData['extra_params'] = $responseData['data'] ?? []; $updateData['extra_params']['req_key'] = $req_key; $updateData['extra_params']['request_id'] = isset($responseData['request_id']) ? $responseData['request_id'] : ''; } $task->updateStatus(MpGenerateVideoTask::STATUS_PROCESSING, $updateData); } } catch (\Exception $e) { // 记录错误 $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => 'API请求失败: ' . $e->getMessage() ]); Utils::throwError('1001:'.$e->getMessage()); } } /** * 查询任务状态(即梦AI) * * @param MpGenerateVideoTask $task * @return array */ public function queryJimengTaskStatus(MpGenerateVideoTask $task): array { try { // 获取API任务ID $apiTaskId = $task->task_id ?? null; if (!$apiTaskId) { return [ 'status' => 'failed', 'error_message' => 'API任务ID不存在', 'result_url' => null ]; } // 根据任务类型选择对应的req_key if ($task->first_frame_url && $task->tail_frame_url) { $req_key = $task->video_resolution == '720P' ? 'jimeng_i2v_first_tail_v30' : 'jimeng_i2v_first_tail_v30_1080'; }else if($task->first_frame_url){ $req_key = $task->video_resolution == '720P' ? 'jimeng_i2v_first_v30' : 'jimeng_i2v_first_v30_1080'; }else{ $req_key = $task->video_resolution == '720P' ? 'jimeng_t2v_v30' : 'jimeng_t2v_v30_1080p'; } // 构建查询参数 $apiParams = [ 'req_key' => $req_key, 'task_id' => $apiTaskId, // 'req_json' => '{"return_url":true}' // 返回URL格式 ]; // 调用即梦AI 3.0 视频生成API查询任务状态 $response = $this->volcEngineService->request( 'POST', 'visual.volcengineapi.com', '/', // 使用根路径 [], // Query参数 json_encode($apiParams), env('VOLC_AI_AK'), env('VOLC_AI_SK'), 'cv', 'cn-north-1', 'CVSync2AsyncGetResult', '2022-08-31', 'application/json' ); $responseData = json_decode($response['body'], true); dLog('generate')->info('即梦AI 3.0 视频生成API查询任务状态响应: ', $responseData); logDB('generate', 'info', '即梦AI视频任务状态查询', ['id' => $task->id, 'response' => $responseData]); if ($responseData['code'] !== 10000) { // API返回错误 logDB('generate', 'error', '即梦AI视频任务状态查询失败', ['id' => $task->id, 'error' => $responseData['message'] ?? 'API Error']); return [ 'status' => 'failed', 'error_message' => $responseData['message'] ?? 'API Error', 'result_url' => null, 'result_json' => $responseData ?? [] ]; } // 解析API响应 $result = $responseData['data'] ?? []; $taskStatus = $result['status'] ?? 'failed'; $returnData = [ 'status' => $taskStatus, 'error_message' => null, 'result_url' => null, 'result_json' => $responseData ?? [] ]; // 如果任务成功,获取结果URL if ($taskStatus === 'done' && isset($result['video_url'])) { if (isset($result['video_url'])) { $video_name = 'ai_generation_' . time() . '_' . uniqid(); $video_ext = getVideoExtFromUrl($result['video_url']); $video_name = $video_name . $video_ext; // 将视频另存到tos $url = uploadStreamByTos('video', file_get_contents($result['video_url']), $video_name); $returnData['result_url'] = $url; } // 计算视频时长 $video_duration = 0; if (isset($result['duration'])) { $video_duration = (int)$result['duration']; } elseif (isset($result['frames']) && isset($result['framespersecond'])) { $frames = (int)$result['frames']; $fps = (int)$result['framespersecond']; if ($fps > 0) { $video_duration = (int)floor($frames / $fps); } } // 如果没有计算出时长,使用任务表中的设定值 if ($video_duration <= 0 && !empty($task->extra_params)) { $extraParams = is_array($task->extra_params) ? $task->extra_params : json_decode($task->extra_params, true); if (isset($extraParams['duration'])) { $video_duration = (int)$extraParams['duration']; } } // 只有当video_duration > 0时才添加到返回数据中 if ($video_duration > 0) { $returnData['video_duration'] = $video_duration; $returnData['video_time_point_start'] = 0; $returnData['video_time_point_end'] = $video_duration; } $returnData['status'] = 'success'; } elseif (in_array($taskStatus, ['not_found', 'expired'])) { $returnData['status'] = 'failed'; $returnData['error_message'] = '任务未找到或已过期'; } elseif ($taskStatus === 'failed') { $returnData['status'] = 'failed'; $returnData['error_message'] = '任务执行失败'; } // in_queue 或 generating状态保持原status return $returnData; } catch (\Exception $e) { return [ 'status' => 'failed', 'error_message' => $e->getMessage(), 'result_url' => null ]; } } /** * 更新所有待处理任务的状态 * * @return void */ public function updatePendingTasks(): void { // 获取所有处理中的任务 $tasks = MpGenerateVideoTask::where('status', MpGenerateVideoTask::STATUS_PROCESSING) ->get(); foreach ($tasks as $task) { $api_type = getProp($task, 'api_type'); switch($api_type) { case 'seedance': $this->updateSeedanceTask($task); break; case 'jimeng': $this->updateJimengTask($task); break; case 'keling': $this->updateKelingOmniTask($task); break; default: break; } } } // 更新即梦AI视频任务状态 private function updateJimengTask($task) { $statusInfo = $this->queryJimengTaskStatus($task); if (!isset($statusInfo['status'])) return; if ($statusInfo['status'] === 'success') { logDB('generate', 'info', '即梦AI视频生成任务成功', ['id' => $task->id, 'result_url' => $statusInfo['result_url']]); $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo); // 同步调整分镜视频状态和结果 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id && isset($statusInfo['result_url'])) { try { DB::beginTransaction(); $now = date('Y-m-d H:i:s'); // 构建更新数据 $segmentUpdateData = [ 'video_url' => $statusInfo['result_url'], 'video_task_status' => '已完成', 'last_frame_url' => $statusInfo['last_frame_url'] ?? '', 'updated_at' => $now ]; // 只有当video_duration存在且大于0时才更新 if (isset($statusInfo['video_duration']) && $statusInfo['video_duration'] > 0) { $segmentUpdateData['video_duration'] = $statusInfo['video_duration']; $segmentUpdateData['video_time_point_start'] = 0; $segmentUpdateData['video_time_point_end'] = $statusInfo['video_duration']; } // 更新分镜表 DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update($segmentUpdateData); DB::commit(); } catch (\Exception $e) { DB::rollBack(); dLog('generate')->error('即梦视频任务处理失败', [ 'segment_id' => $segment_id, 'task_id' => $task->id, 'error' => $e->getMessage() ]); } } } elseif ($statusInfo['status'] === 'failed') { logDB('generate', 'error', '即梦AI视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, $statusInfo); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } // 如果仍然是处理中状态,不做任何操作 // 处理:如果任务处理超过12小时,标记为失败 $processingTime = now()->diffInHours($task->created_at); if ($processingTime > 12) { logDB('generate', 'warning', '即梦AI视频生成任务超时', ['id' => $task->id, 'processing_hours' => $processingTime]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '任务处理超时(超过12小时)' ]); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } } public function getTaskList($data) { $task_id = getProp($data, 'task_id'); $status = getProp($data, 'status'); $query = MpGenerateVideoTask::select('*'); if ($task_id) { $query->where('id', $task_id); } if ($status) { $query->where('status', $status); } return $query->orderBy('created_at', 'desc')->paginate(); } /** * 创建 Seedance 1.5 Pro 视频生成任务 * * @param array $params * @return MpGenerateVideoTask */ public function createSeedanceTask(array $params): MpGenerateVideoTask { // 生成唯一的任务ID $taskId = 'seedance_' . time() . '_' . uniqid(); // 创建任务记录,复用现有表结构 $task = MpGenerateVideoTask::create([ 'task_id' => $taskId, 'alias_segment_id' => $params['alias_segment_id'] ?? '', 'prompt' => $params['prompt'] ?? '', 'first_frame_url' => $params['first_frame_url'] ?? '', 'tail_frame_url' => $params['tail_frame_url'] ?? '', 'seed' => $params['seed'] ?? -1, 'frames' => $params['video_duration'] ? ($params['video_duration'] * 24 + 1) : null, 'video_resolution' => $params['video_resolution'] ?? '720P', 'status' => MpGenerateVideoTask::STATUS_PENDING, 'api_type' => 'seedance', // 标识为 Seedance 1.5 Pro API 'extra_params' => [ 'model' => $params['model'] ?? 'doubao-seedance-1-5-pro-251215', 'content' => $params['content'], 'callback_url' => $params['callback_url'] ?? '', 'return_last_frame' => $params['return_last_frame'] ?? true, 'service_tier' => $params['service_tier'] ?? 'default', 'execution_expires_after' => $params['execution_expires_after'] ?? 172800, 'generate_audio' => $params['generate_audio'] ?? false, 'draft' => $params['draft'] ?? false, 'ratio' => $params['ratio'] ?? '9:16', 'duration' => $params['video_duration'] ?? 5, 'camera_fixed' => $params['camera_fixed'] ?? false, 'watermark' => $params['watermark'] ?? false, ] ]); // 提交任务到火山引擎 API $this->submitSeedanceTaskToApi($task); return $task; } /** * 提交 Seedance 任务到火山引擎 API * * @param MpGenerateVideoTask $task * @return void */ private function submitSeedanceTaskToApi(MpGenerateVideoTask $task): void { $client = new Client(['verify'=>false,'timeout'=>120]); try { // 验证环境变量配置 $apiKey = env('VOLC_AI_API_KEY'); if (empty($apiKey)) { $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '火山引擎访问密钥未配置,请检查环境变量API Key' ]); return; } $extraParams = $task->extra_params; // 构建火山引擎 Seedance API 请求参数 $apiParams = [ 'model' => isset(\App\Consts\BaseConst::MODELS_MAP[$extraParams['model']]) ? \App\Consts\BaseConst::MODELS_MAP[$extraParams['model']] : $extraParams['model'], 'content' => $extraParams['content'], ]; // 添加可选参数 if (!empty($extraParams['callback_url'])) { $apiParams['callback_url'] = $extraParams['callback_url']; } if (isset($extraParams['return_last_frame'])) { $apiParams['return_last_frame'] = $extraParams['return_last_frame']; } if (!empty($extraParams['service_tier']) && !in_array($extraParams['model'], ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128'])) { $apiParams['service_tier'] = $extraParams['service_tier']; } if (!empty($extraParams['execution_expires_after'])) { $apiParams['execution_expires_after'] = $extraParams['execution_expires_after']; } if (isset($extraParams['generate_audio'])) { $apiParams['generate_audio'] = $extraParams['generate_audio']; } if (isset($extraParams['draft'])) { $apiParams['draft'] = $extraParams['draft']; } if (!empty($extraParams['video_resolution'])) { $apiParams['resolution'] = strtolower($task->video_resolution); } if (!empty($extraParams['ratio'])) { $apiParams['ratio'] = $extraParams['ratio']; } if (!empty($extraParams['duration'])) { $apiParams['duration'] = (int)$extraParams['duration']; } if (isset($task->frames)) { // $apiParams['frames'] = $task->frames; } if ($task->seed !== -1) { $apiParams['seed'] = (int)$task->seed; } if (isset($extraParams['camera_fixed'])) { $apiParams['camera_fixed'] = $extraParams['camera_fixed']; } if (isset($extraParams['watermark'])) { $apiParams['watermark'] = $extraParams['watermark']; } dLog('generate')->info('Seedance 1.5 Pro 视频生成参数: ', $apiParams); logDB('generate', 'info', 'Seedance视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]); // 调用火山引擎 Seedance API $response = $client->post('https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', [ 'headers' => [ 'Authorization' => 'Bearer ' . $apiKey, 'Content-Type' => 'application/json', ], 'json' => $apiParams ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('Seedance 1.5 Pro API 响应: ', $responseData); logDB('generate', 'info', 'Seedance视频生成API响应', ['id' => $task->id, 'response' => $responseData]); if (isset($responseData['error'])) { // API返回错误 logDB('generate', 'error', 'Seedance视频生成任务提交失败', ['id' => $task->id, 'error' => $responseData['error']['message'] ?? 'API Error']); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => $responseData['error']['message'] ?? 'API Error', 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]); } elseif (isset($responseData['id'])) { // 任务提交成功,更新状态为处理中 $updateData = [ 'task_id' => $responseData['id'], // 使用 API 返回的任务 ID 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]; $task->updateStatus(MpGenerateVideoTask::STATUS_PROCESSING, $updateData); } else { $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '未知的 API 响应格式', 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]); } } catch (\Exception $e) { // 记录错误 $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => 'API请求失败: ' . $e->getMessage() ]); Utils::throwError('1001:'.$e->getMessage()); } } // 更新Seedance视频任务状态 private function updateSeedanceTask($task) { $statusInfo = $this->querySeedanceTaskStatus($task); if (!isset($statusInfo['status'])) return; if ($statusInfo['status'] === 'success') { logDB('generate', 'info', 'Seedance视频生成任务成功', ['id' => $task->id, 'result_url' => $statusInfo['result_url']]); $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo); // 同步调整分镜视频状态和结果 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id && isset($statusInfo['result_url'])) { try { DB::beginTransaction(); $now = date('Y-m-d H:i:s'); // 构建更新数据 $segmentUpdateData = [ 'video_url' => $statusInfo['result_url'], 'video_task_status' => '已完成', 'last_frame_url' => $statusInfo['last_frame_url'] ?? '', 'updated_at' => $now ]; // 只有当video_duration存在且大于0时才更新 if (isset($statusInfo['video_duration']) && $statusInfo['video_duration'] > 0) { $segmentUpdateData['video_duration'] = $statusInfo['video_duration']; $segmentUpdateData['video_time_point_start'] = 0; $segmentUpdateData['video_time_point_end'] = $statusInfo['video_duration']; } // 更新分镜表 DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update($segmentUpdateData); DB::commit(); } catch (\Exception $e) { DB::rollBack(); dLog('generate')->error('Seedance视频任务处理失败', [ 'segment_id' => $segment_id, 'task_id' => $task->id, 'error' => $e->getMessage() ]); } } } elseif ($statusInfo['status'] === 'failed') { logDB('generate', 'error', 'Seedance视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => $statusInfo['error_message'], 'result_json' => $statusInfo['result_json'] ?? [] ]); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } // 如果仍然是处理中状态,不做任何操作 // 处理:如果任务处理超过48小时,标记为失败 $processingTime = now()->diffInHours($task->created_at); if ($processingTime > 48) { logDB('generate', 'warning', 'Seedance视频生成任务超时', ['id' => $task->id, 'processing_hours' => $processingTime]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '任务处理超时(超过48小时)' ]); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } } /** * 查询 Seedance 任务状态 * * @param MpGenerateVideoTask $task * @return array */ public function querySeedanceTaskStatus(MpGenerateVideoTask $task): array { $client = new Client(['verify'=>false,'timeout'=>120]); try { // 如果任务已完成,直接返回结果 if (in_array($task->status, [MpGenerateVideoTask::STATUS_SUCCESS, MpGenerateVideoTask::STATUS_FAILED])) { return [ 'success' => true, 'data' => [ 'id' => $task->id, 'status' => $this->mapTaskStatus($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 ] ]; } // 调用火山引擎 API 查询任务状态 $apiKey = env('VOLC_AI_API_KEY'); if (empty($apiKey)) { return [ 'status' => MpGenerateVideoTask::STATUS_FAILED, 'result_json' => [], 'error_message' => '火山引擎访问密钥未配置' ]; } // 构建查询 URL $url = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/' . $task->task_id; $response = $client->get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $apiKey, 'Content-Type' => 'application/json', ] ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('Seedance 任务状态查询响应: ', $responseData); logDB('generate', 'info', 'Seedance任务状态查询', ['id' => $task->id, 'response' => $responseData]); if (isset($responseData['error'])) { logDB('generate', 'error', 'Seedance任务状态查询失败', ['id' => $task->id, 'error' => $responseData['error']['message'] ?? 'API查询失败']); return [ 'status' => MpGenerateVideoTask::STATUS_FAILED, 'result_json' => $responseData, 'error_message' => $responseData['error']['message'] ?? 'API查询失败' ]; } // 解析任务状态 $apiStatus = $responseData['status'] ?? 'unknown'; $taskStatus = $this->mapApiStatusToTaskStatus($apiStatus); $updateData = ['result_json' => $responseData]; if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS) { $updateData['status'] = MpGenerateVideoTask::STATUS_SUCCESS; // 任务成功,处理结果 if (isset($responseData['content']['video_url'])) { $video_url = $responseData['content']['video_url']; // 下载并保存视频 $video_name = 'ai_generation_' . time() . '_' . uniqid(); $video_ext = getVideoExtFromUrl($video_url); $video_name = $video_name . $video_ext; $url = uploadStreamByTos('video', file_get_contents($video_url), $video_name); $updateData['result_url'] = $url; } // 计算视频时长 $video_duration = 0; if (isset($responseData['duration'])) { // 如果直接返回duration,使用该值 $video_duration = (int)$responseData['duration']; } elseif (isset($responseData['frames']) && isset($responseData['framespersecond'])) { // 如果返回frames和framespersecond,计算时长 $frames = (int)$responseData['frames']; $fps = (int)$responseData['framespersecond']; if ($fps > 0) { $video_duration = (int)floor($frames / $fps); } } // 如果没有计算出时长,使用任务表中的设定值 if ($video_duration <= 0 && !empty($task->extra_params)) { $extraParams = is_array($task->extra_params) ? $task->extra_params : json_decode($task->extra_params, true); if (isset($extraParams['duration'])) { $video_duration = (int)$extraParams['duration']; } } // 只有当video_duration > 0时才添加到更新数据中 if ($video_duration > 0) { $updateData['video_duration'] = $video_duration; $updateData['video_time_point_start'] = 0; $updateData['video_time_point_end'] = $video_duration; } // 处理尾帧图片(如果启用了 return_last_frame) if (isset($responseData['content']['last_frame_url']) && !empty($responseData['content']['last_frame_url'])) { $pic_url = $responseData['content']['last_frame_url']; $pic_name = 'ai_generation_' . time() . '_' . uniqid(); $pic_ext = getImgExtFromUrl($pic_url); $pic_name = $pic_name . $pic_ext; if ($pic_ext === '.png') { $comporessed_data = compressRemoteImageUrlToSize($pic_url); $url = uploadStreamByTos('image', $comporessed_data, $pic_name); }else { // 将图片另存到tos $url = uploadStreamByTos('image', file_get_contents($pic_url), $pic_name); } $updateData['last_frame_url'] = $url; } return $updateData; } elseif ($taskStatus === MpGenerateVideoTask::STATUS_FAILED) { $updateData['status'] = MpGenerateVideoTask::STATUS_FAILED; $updateData['error_message'] = $responseData['error_message'] ?? '任务执行失败'; return $updateData; } // 其他状态保持不变 return [ 'success' => true, 'data' => [ 'id' => $task->id, 'status' => $this->mapTaskStatus($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, 'api_response' => $responseData ] ]; } catch (\Exception $e) { dLog('generate')->info('Seedance 任务状态查询异常: '.$e->getMessage()); return [ 'success' => false, 'message' => '查询失败: ' . $e->getMessage() ]; } } /** * 映射 API 状态到任务状态 * * @param string $apiStatus * @return string */ private function mapApiStatusToTaskStatus(string $apiStatus): string { switch ($apiStatus) { case 'queued': case 'running': return MpGenerateVideoTask::STATUS_PROCESSING; case 'succeeded': return MpGenerateVideoTask::STATUS_SUCCESS; case 'failed': case 'expired': return MpGenerateVideoTask::STATUS_FAILED; default: return MpGenerateVideoTask::STATUS_PROCESSING; } } public function seedanceCallback($data) { try { dLog('generate')->info('Seedance 回调数据: ', $data); logDB('generate', 'info', 'Seedance回调接收', ['task_id' => $data['id'] ?? null, 'status' => $data['status'] ?? null]); // 从回调数据中获取任务ID $taskId = $data['id'] ?? null; if (!$taskId) { dLog('generate')->error('Seedance 回调缺少任务ID'); logDB('generate', 'error', 'Seedance回调缺少任务ID', ['data' => $data]); return ['success' => false, 'message' => '缺少任务ID']; } // 查找对应的任务记录 $task = MpGenerateVideoTask::where('task_id', $taskId)->first(); if (!$task) { dLog('generate')->error('Seedance 回调找不到任务: ' . $taskId); logDB('generate', 'error', 'Seedance回调找不到任务', ['task_id' => $taskId]); return ['success' => false, 'message' => '任务不存在']; } // 解析任务状态 $apiStatus = $data['status'] ?? 'unknown'; $taskStatus = $this->mapApiStatusToTaskStatus($apiStatus); // 准备更新数据 $updateData = [ 'result_json' => $data, 'updated_at' => now() ]; if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS) { $updateData['status'] = MpGenerateVideoTask::STATUS_SUCCESS; $updateData['completed_at'] = now(); // 处理视频结果 if (isset($data['content']['video_url'])) { $video_url = $data['content']['video_url']; try { // 下载并保存视频 $video_name = 'ai_generation_' . time() . '_' . uniqid(); $video_ext = getVideoExtFromUrl($video_url); $video_name = $video_name . $video_ext; $url = uploadStreamByTos('video', file_get_contents($video_url), $video_name); $updateData['result_url'] = $url; dLog('generate')->info('Seedance 视频保存成功: ' . $url); } catch (\Exception $e) { dLog('generate')->error('Seedance 视频保存失败: ' . $e->getMessage()); // 如果保存失败,直接使用原始URL $updateData['result_url'] = $video_url; } } // 处理尾帧图片(如果启用了 return_last_frame) if (isset($data['content']['last_frame_url']) && !empty($data['content']['last_frame_url'])) { $pic_url = $data['content']['last_frame_url']; try { $pic_name = 'ai_generation_' . time() . '_' . uniqid(); $pic_ext = getImgExtFromUrl($pic_url); $pic_name = $pic_name . $pic_ext; if ($pic_ext === '.png') { $compressed_data = compressRemoteImageUrlToSize($pic_url); $url = uploadStreamByTos('image', $compressed_data, $pic_name); } else { $url = uploadStreamByTos('image', file_get_contents($pic_url), $pic_name); } $updateData['last_frame_url'] = $url; dLog('generate')->info('Seedance 尾帧图片保存成功: ' . $url); } catch (\Exception $e) { dLog('generate')->error('Seedance 尾帧图片保存失败: ' . $e->getMessage()); // 如果保存失败,直接使用原始URL $updateData['last_frame_url'] = $pic_url; } } } elseif ($taskStatus === MpGenerateVideoTask::STATUS_FAILED) { $updateData['status'] = MpGenerateVideoTask::STATUS_FAILED; $updateData['completed_at'] = now(); $updateData['error_message'] = $data['error_message'] ?? $data['failure_reason'] ?? '任务执行失败'; } else { // 处理中状态 $updateData['status'] = $taskStatus; if ($taskStatus === MpGenerateVideoTask::STATUS_PROCESSING && !$task->started_at) { $updateData['started_at'] = now(); } } // 更新任务记录 $task->update($updateData); // 更新分镜表的视频任务状态 $this->updateSegmentVideoStatus($task, $taskStatus, $updateData); dLog('generate')->info('Seedance 回调处理完成: ' . $taskId . ', 状态: ' . $taskStatus); logDB('generate', 'info', 'Seedance回调处理完成', ['task_id' => $taskId, 'status' => $taskStatus]); return [ 'success' => true, 'message' => '回调处理成功', 'task_id' => $taskId, 'status' => $taskStatus ]; } catch (\Exception $e) { dLog('generate')->error('Seedance 回调处理异常: ' . $e->getMessage()); logDB('generate', 'error', 'Seedance回调处理异常', ['error' => $e->getMessage()]); return [ 'success' => false, 'message' => '回调处理失败: ' . $e->getMessage() ]; } } /** * 更新分镜表的视频任务状态 * * @param MpGenerateVideoTask $task * @param string $taskStatus * @param array $updateData */ private function updateSegmentVideoStatus(MpGenerateVideoTask $task, string $taskStatus, array $updateData) { try { // 查找关联的分镜记录 $segment = DB::table('mp_episode_segments') ->where('video_task_id', $task->id) ->first(); if (!$segment) { dLog('generate')->info('未找到关联的分镜记录: ' . $task->id); return; } $segmentUpdateData = ['updated_at' => date('Y-m-d H:i:s')]; if (isset($updateData['last_frame_url']) && $updateData['last_frame_url']) { $segmentUpdateData['last_frame_url'] = $updateData['last_frame_url']; } if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS) { $segmentUpdateData['video_task_status'] = '已完成'; if (isset($updateData['result_url'])) { $segmentUpdateData['video_url'] = $updateData['result_url']; } } elseif ($taskStatus === MpGenerateVideoTask::STATUS_FAILED) { $segmentUpdateData['video_task_status'] = '失败'; } elseif ($taskStatus === MpGenerateVideoTask::STATUS_PROCESSING) { $segmentUpdateData['video_task_status'] = '生成中'; } // 更新分镜表 DB::table('mp_episode_segments') ->where('video_task_id', $task->id) ->update($segmentUpdateData); dLog('generate')->info('分镜视频状态更新成功: ' . $segment->segment_id . ', 状态: ' . ($segmentUpdateData['video_task_status'] ?? '未知')); } catch (\Exception $e) { dLog('generate')->error('更新分镜视频状态失败: ' . $e->getMessage()); } } /** * 生成可灵AI的JWT Token * * @return string */ private function generateKelingJwtToken(): string { $ak = env('KELING_AK'); $sk = env('KELING_SK'); if (empty($ak) || empty($sk)) { throw new \Exception('可灵AI访问密钥未配置,请检查环境变量KELING_AK和KELING_SK'); } // JWT头部 $header = [ 'alg' => 'HS256', 'typ' => 'JWT' ]; // JWT载荷 $payload = [ 'iss' => $ak, // 签发者,使用Access Key 'exp' => time() + 1800, // 过期时间,30分钟后过期 'nbf' => time() - 5 // 生效时间,5秒前开始生效 ]; // Base64URL编码 $headerEncoded = $this->base64UrlEncode(json_encode($header)); $payloadEncoded = $this->base64UrlEncode(json_encode($payload)); // 创建签名 $signature = hash_hmac('sha256', $headerEncoded . '.' . $payloadEncoded, $sk, true); $signatureEncoded = $this->base64UrlEncode($signature); // 返回完整的JWT token return $headerEncoded . '.' . $payloadEncoded . '.' . $signatureEncoded; } /** * Base64URL编码(JWT标准编码) * * @param string $data * @return string */ private function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } /** * 创建可灵AI Omni视频生成任务 * * @param array $params * @return MpGenerateVideoTask */ public function createKelingOmniTask(array $params): MpGenerateVideoTask { // 生成唯一的任务ID $taskId = 'keling_omni_' . time() . '_' . uniqid(); // 创建任务记录,复用现有表结构 $task = MpGenerateVideoTask::create([ 'task_id' => $taskId, 'prompt' => $params['prompt'] ?? '', 'first_frame_url' => $params['first_frame_url'] ?? '', 'tail_frame_url' => $params['tail_frame_url'] ?? '', 'seed' => $params['seed'] ?? -1, 'frames' => $params['video_duration'] ? ($params['video_duration'] * 24 + 1) : null, 'video_resolution' => $params['video_resolution'] ?? '720P', 'status' => MpGenerateVideoTask::STATUS_PENDING, 'api_type' => 'keling', // 标识为可灵AI Omni API 'extra_params' => [ 'api_type' => 'keling', 'model' => $params['model'] ?? 'kling-video-o1', 'prompt' => $params['prompt'] ?? '', 'multi_prompt' => $params['multi_prompt'] ?? [], 'shot_type' => $params['shot_type'] ?? 'customize', 'image_list' => $params['image_list'] ?? [], 'element_list' => $params['element_list'] ?? [], 'sound' => $params['sound'] ?? 'off', // 'aspect_ratio' => $params['aspect_ratio'] ?? '16:9', 'duration' => $params['video_duration'] ?? 5, 'mode' => $params['mode'] ?? 'std', 'callback_url' => $params['callback_url'] ?? '', ] ]); // 提交任务到可灵AI Omni API $this->submitKelingOmniTaskToApi($task); return $task; } /** * 提交可灵AI Omni任务到API * * @param MpGenerateVideoTask $task * @return void */ private function submitKelingOmniTaskToApi(MpGenerateVideoTask $task): void { $client = new Client(['verify'=>false,'timeout'=>120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); $extraParams = $task->extra_params; // 构建可灵AI Omni API请求参数 $apiParams = [ 'model_name' => $extraParams['model'], 'prompt' => $extraParams['prompt'], 'mode' => $extraParams['mode'], // 'aspect_ratio' => $extraParams['aspect_ratio'], 'duration' => (int)$extraParams['duration'], ]; // 添加可选参数 if (!empty($extraParams['multi_prompt'])) { $apiParams['multi_prompt'] = $extraParams['multi_prompt']; } if (!empty($extraParams['shot_type'])) { $apiParams['shot_type'] = $extraParams['shot_type']; } if (!empty($extraParams['image_list'])) { $apiParams['image_list'] = $extraParams['image_list']; } if (!empty($extraParams['element_list'])) { $apiParams['element_list'] = json_decode($extraParams['element_list'], true); } if (isset($extraParams['sound'])) { $apiParams['sound'] = $extraParams['sound']; } // if (!empty($extraParams['callback_url'])) { // $apiParams['callback_url'] = $extraParams['callback_url']; // } dLog('generate')->info('可灵AI Omni视频生成参数: ', $apiParams); logDB('generate', 'info', '可灵AI Omni视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]); // 调用可灵AI Omni API $response = $client->post('https://api-beijing.klingai.com/v1/videos/omni-video', [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ], 'json' => $apiParams ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI Omni API响应: ', $responseData); logDB('generate', 'info', '可灵AI Omni API响应', ['id' => $task->id, 'response' => $responseData]); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { // API返回错误 $errorMessage = $responseData['error']['message'] ?? $responseData['message'] ?? 'API Error'; logDB('generate', 'error', '可灵AI Omni任务提交失败', ['id' => $task->id, 'error' => $errorMessage]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => $errorMessage, 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]); } elseif (isset($responseData['data']['task_id'])) { // 任务提交成功,更新状态为处理中 $updateData = [ 'task_id' => $responseData['data']['task_id'], // 使用API返回的任务ID 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]; $task->updateStatus(MpGenerateVideoTask::STATUS_PROCESSING, $updateData); } else { $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '未知的API响应格式', 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData]) ]); } } catch (\Exception $e) { // 记录错误 logDB('generate', 'error', '可灵AI Omni任务异常', ['id' => $task->id, 'error' => $e->getMessage()]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => 'API请求失败: ' . $e->getMessage() ]); dLog('generate')->error('可灵AI Omni API请求异常: ' . $e->getMessage()); } } /** * 查询可灵AI Omni任务状态 * * @param MpGenerateVideoTask $task * @return array */ public function queryKelingOmniTaskStatus(MpGenerateVideoTask $task): array { $client = new Client(['verify'=>false,'timeout'=>120]); try { // 如果任务已完成,直接返回结果 if (in_array($task->status, [MpGenerateVideoTask::STATUS_SUCCESS, MpGenerateVideoTask::STATUS_FAILED])) { return [ 'success' => true, 'data' => [ 'task_id' => $task->task_id, 'task_status' => $this->mapTaskStatusToKelingStatus($task->status), 'result_url' => $task->result_url, 'error_message' => $task->error_message, 'created_at' => $task->created_at, 'updated_at' => $task->updated_at ] ]; } // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建查询URL $url = 'https://api-beijing.klingai.com/v1/videos/omni-video/' . $task->task_id; $response = $client->get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ] ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI Omni任务状态查询响应: ', $responseData); logDB('generate', 'info', '可灵AI Omni任务状态查询', ['id' => $task->id, 'response' => $responseData]); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { logDB('generate', 'error', '可灵AI Omni任务状态查询失败', ['id' => $task->id, 'error' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API查询失败']); return [ 'status' => 'failed', 'error_message' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API查询失败', 'result_json' => $responseData ]; } // 解析任务状态 $taskData = $responseData['data'] ?? []; $taskStatus = $taskData['task_status'] ?? 'unknown'; $updateData = ['result_json' => $responseData]; if ($taskStatus === 'succeed') { $updateData['status'] = 'success'; // 任务成功,处理结果 if (isset($taskData['task_result']['videos']) && !empty($taskData['task_result']['videos'])) { $video = $taskData['task_result']['videos'][0]; $video_url = $video['url']; // 下载并保存视频 $video_name = 'ai_generation_' . time() . '_' . uniqid(); $video_ext = getVideoExtFromUrl($video_url); $video_name = $video_name . $video_ext; $url = uploadStreamByTos('video', file_get_contents($video_url), $video_name); $updateData['result_url'] = $url; // 计算视频时长 $video_duration = 0; if (isset($video['duration'])) { $video_duration = (int)$video['duration']; } elseif (isset($taskData['duration'])) { $video_duration = (int)$taskData['duration']; } elseif (isset($video['frames']) && isset($video['framespersecond'])) { $frames = (int)$video['frames']; $fps = (int)$video['framespersecond']; if ($fps > 0) { $video_duration = (int)floor($frames / $fps); } } // 如果没有计算出时长,使用任务表中的设定值 if ($video_duration <= 0 && !empty($task->extra_params)) { $extraParams = is_array($task->extra_params) ? $task->extra_params : json_decode($task->extra_params, true); if (isset($extraParams['duration'])) { $video_duration = (int)$extraParams['duration']; } } // 只有当video_duration > 0时才添加到更新数据中 if ($video_duration > 0) { $updateData['video_duration'] = $video_duration; $updateData['video_time_point_start'] = 0; $updateData['video_time_point_end'] = $video_duration; } } return $updateData; } elseif ($taskStatus === 'failed') { $updateData = [ 'status' => 'failed', 'error_message' => '任务执行失败', 'result_json' => $responseData ]; return $updateData; } // 其他状态(processing等)保持不变 return [ 'success' => true, 'data' => [ 'task_id' => $task->task_id, 'task_status' => $taskStatus, 'result_url' => $task->result_url, 'error_message' => $task->error_message, 'created_at' => $task->created_at, 'updated_at' => $task->updated_at, 'api_response' => $responseData ] ]; } catch (\Exception $e) { dLog('generate')->error('可灵AI Omni任务状态查询异常: ' . $e->getMessage()); return [ 'success' => false, 'message' => '查询失败: ' . $e->getMessage() ]; } } /** * 映射任务状态到可灵AI状态格式 * * @param string $taskStatus * @return string */ private function mapTaskStatusToKelingStatus(string $taskStatus): string { switch ($taskStatus) { case MpGenerateVideoTask::STATUS_PENDING: return 'submitted'; case MpGenerateVideoTask::STATUS_PROCESSING: return 'processing'; case MpGenerateVideoTask::STATUS_SUCCESS: return 'succeed'; case MpGenerateVideoTask::STATUS_FAILED: return 'failed'; default: return 'unknown'; } } /** * 更新可灵AI Omni视频任务状态 * * @param MpGenerateVideoTask $task * @return void */ private function updateKelingOmniTask($task): void { $statusInfo = $this->queryKelingOmniTaskStatus($task); if (!isset($statusInfo['status'])) return; if ($statusInfo['status'] === 'success') { logDB('generate', 'info', '可灵AI Omni视频生成任务成功', ['id' => $task->id, 'result_url' => $statusInfo['result_url']]); $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo); // 同步调整分镜视频状态和结果 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id && isset($statusInfo['result_url'])) { try { DB::beginTransaction(); $now = date('Y-m-d H:i:s'); // 构建更新数据 $segmentUpdateData = [ 'video_url' => $statusInfo['result_url'], 'video_task_status' => '已完成', 'last_frame_url' => $statusInfo['last_frame_url'] ?? '', 'updated_at' => $now ]; // 只有当video_duration存在且大于0时才更新 if (isset($statusInfo['video_duration']) && $statusInfo['video_duration'] > 0) { $segmentUpdateData['video_duration'] = $statusInfo['video_duration']; $segmentUpdateData['video_time_point_start'] = 0; $segmentUpdateData['video_time_point_end'] = $statusInfo['video_duration']; } // 更新分镜表 DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update($segmentUpdateData); DB::commit(); } catch (\Exception $e) { DB::rollBack(); dLog('generate')->error('可灵视频任务处理失败', [ 'segment_id' => $segment_id, 'task_id' => $task->id, 'error' => $e->getMessage() ]); } } } elseif ($statusInfo['status'] === 'failed') { $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => $statusInfo['error_message'], 'result_json' => $statusInfo['result_json'] ?? [] ]); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } // 如果仍然是处理中状态,不做任何操作 // 处理:如果任务处理超过24小时,标记为失败 $processingTime = now()->diffInHours($task->created_at); if ($processingTime > 24) { logDB('generate', 'warning', '可灵AI Omni视频生成任务超时', ['id' => $task->id, 'processing_hours' => $processingTime]); $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [ 'error_message' => '任务处理超时(超过24小时)' ]); // 同步更新分镜表状态为失败 $segment_id = getProp($task, 'alias_segment_id'); if ($segment_id) { DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([ 'video_task_status' => '失败', 'updated_at' => date('Y-m-d H:i:s') ]); } } } /** * 映射任务状态到前端显示状态 * * @param string $taskStatus * @return string */ private function mapTaskStatus(string $taskStatus): string { switch ($taskStatus) { case MpGenerateVideoTask::STATUS_PENDING: return 'queued'; case MpGenerateVideoTask::STATUS_PROCESSING: return 'running'; case MpGenerateVideoTask::STATUS_SUCCESS: return 'succeeded'; case MpGenerateVideoTask::STATUS_FAILED: return 'failed'; default: return 'unknown'; } } // ==================== 主体管理相关方法 ==================== /** * 创建主体 * * @param array $params * @return AIElement */ public function createElement(array $params): AIElement { // 生成唯一的任务ID $taskId = 'element_' . time() . '_' . uniqid(); // 创建主体记录 $element = AIElement::create([ 'task_id' => $taskId, 'external_task_id' => $params['external_task_id'] ?? null, 'element_name' => $params['element_name'], 'element_description' => $params['element_description'], 'reference_type' => $params['reference_type'], 'element_image_list' => $params['element_image_list'] ?? null, 'element_video_list' => $params['element_video_list'] ?? null, 'element_voice_id' => $params['element_voice_id'] ?? null, 'tag_list' => $params['tag_list'] ?? null, 'callback_url' => $params['callback_url'] ?? null, 'task_status' => AIElement::STATUS_SUBMITTED, ]); // 提交到可灵AI API $this->submitElementToKelingApi($element); return $element; } /** * 提交主体创建任务到可灵AI API * * @param AIElement $element * @return void */ private function submitElementToKelingApi(AIElement $element): void { $client = new Client(['verify' => false, 'timeout' => 120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建API请求参数 $apiParams = [ 'element_name' => $element->element_name, 'element_description' => $element->element_description, 'reference_type' => $element->reference_type, ]; // 添加图片参考 if ($element->reference_type === AIElement::REFERENCE_TYPE_IMAGE && $element->element_image_list) { $apiParams['element_image_list'] = $element->element_image_list; } // 添加视频参考 if ($element->reference_type === AIElement::REFERENCE_TYPE_VIDEO && $element->element_video_list) { $apiParams['element_video_list'] = $element->element_video_list; } // 添加音色ID if ($element->element_voice_id) { $apiParams['element_voice_id'] = $element->element_voice_id; } // 添加标签 if ($element->tag_list) { $apiParams['tag_list'] = $element->tag_list; } // 添加回调URL if ($element->callback_url) { $apiParams['callback_url'] = $element->callback_url; } // 添加外部任务ID if ($element->external_task_id) { $apiParams['external_task_id'] = $element->external_task_id; } dLog('generate')->info('可灵AI创建主体参数: ', $apiParams); // 调用可灵AI创建主体API $response = $client->post('https://api-beijing.klingai.com/v1/general/advanced-custom-elements', [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ], 'json' => $apiParams ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI创建主体API响应: ', $responseData); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { // API返回错误 $errorMessage = $responseData['error']['message'] ?? $responseData['message'] ?? 'API Error'; $element->update([ 'task_status' => AIElement::STATUS_FAILED, 'task_status_msg' => $errorMessage, ]); } elseif (isset($responseData['data']['task_id'])) { // 任务提交成功,更新状态为处理中 $element->update([ 'task_id' => $responseData['data']['task_id'], // 使用API返回的任务ID 'task_status' => AIElement::STATUS_PROCESSING, 'created_at' => isset($responseData['data']['created_at']) ? date('Y-m-d H:i:s', $responseData['data']['created_at'] / 1000) : now(), 'updated_at' => isset($responseData['data']['updated_at']) ? date('Y-m-d H:i:s', $responseData['data']['updated_at'] / 1000) : now(), ]); } else { $element->update([ 'task_status' => AIElement::STATUS_FAILED, 'task_status_msg' => '未知的API响应格式', ]); } } catch (\Exception $e) { // 记录错误 $element->update([ 'task_status' => AIElement::STATUS_FAILED, 'task_status_msg' => 'API请求失败: ' . $e->getMessage(), ]); dLog('generate')->error('可灵AI创建主体API请求异常: ' . $e->getMessage()); } } /** * 查询单个主体状态 * * @param string $taskId * @param string|null $externalTaskId * @return array */ public function queryElementStatus(string $taskId, string $externalTaskId = null): array { $client = new Client(['verify' => false, 'timeout' => 120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建查询URL $url = 'https://api-beijing.klingai.com/v1/general/advanced-custom-elements/' . $taskId; $queryParams = []; if ($externalTaskId) { $queryParams['external_task_id'] = $externalTaskId; } if (!empty($queryParams)) { $url .= '?' . http_build_query($queryParams); } $response = $client->get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ] ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI查询主体状态响应: ', $responseData); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { return [ 'success' => false, 'message' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API查询失败', 'data' => null ]; } // 更新本地数据库记录 if (isset($responseData['data'])) { $this->updateLocalElementFromApi($responseData['data']); } return [ 'success' => true, 'data' => $responseData['data'] ]; } catch (\Exception $e) { dLog('generate')->error('可灵AI查询主体状态异常: ' . $e->getMessage()); return [ 'success' => false, 'message' => '查询失败: ' . $e->getMessage(), 'data' => null ]; } } /** * 根据API响应更新本地主体记录 * * @param array $apiData * @return void */ private function updateLocalElementFromApi(array $apiData): void { $element = AIElement::where('task_id', $apiData['task_id'])->first(); if (!$element) { return; } $updateData = [ 'task_status' => $apiData['task_status'], 'task_status_msg' => $apiData['task_status_msg'] ?? null, 'final_unit_deduction' => $apiData['final_unit_deduction'] ?? null, 'updated_at' => isset($apiData['updated_at']) ? date('Y-m-d H:i:s', $apiData['updated_at'] / 1000) : now(), ]; // 如果任务成功,更新主体信息 if ($apiData['task_status'] === AIElement::STATUS_SUCCEED && isset($apiData['task_result']['elements'])) { $elementData = $apiData['task_result']['elements'][0]; $updateData = array_merge($updateData, [ 'element_id' => $elementData['element_id'], 'element_voice_info' => $elementData['element_voice_info'] ?? null, 'owned_by' => $elementData['owned_by'] ?? 'custom', 'status' => $elementData['status'] ?? AIElement::ELEMENT_STATUS_SUCCEED, ]); } $element->update($updateData); } /** * 查询自定义主体列表 * * @param int $pageNum * @param int $pageSize * @return array */ public function queryCustomElementsList(int $pageNum = 1, int $pageSize = 30): array { $client = new Client(['verify' => false, 'timeout' => 120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建查询URL $url = 'https://api-beijing.klingai.com/v1/general/advanced-custom-elements'; $queryParams = [ 'pageNum' => $pageNum, 'pageSize' => $pageSize, ]; $url .= '?' . http_build_query($queryParams); $response = $client->get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ] ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI查询自定义主体列表响应: ', $responseData); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { return [ 'success' => false, 'message' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API查询失败', 'data' => [] ]; } // 更新本地数据库记录 if (isset($responseData['data']) && is_array($responseData['data'])) { foreach ($responseData['data'] as $elementData) { $this->updateLocalElementFromApi($elementData); } } return [ 'success' => true, 'data' => $responseData['data'] ?? [] ]; } catch (\Exception $e) { dLog('generate')->error('可灵AI查询自定义主体列表异常: ' . $e->getMessage()); return [ 'success' => false, 'message' => '查询失败: ' . $e->getMessage(), 'data' => [] ]; } } /** * 查询官方主体列表 * * @param int $pageNum * @param int $pageSize * @return array */ public function queryOfficialElementsList(int $pageNum = 1, int $pageSize = 30): array { $client = new Client(['verify' => false, 'timeout' => 120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建查询URL $url = 'https://api-beijing.klingai.com/v1/general/advanced-presets-elements'; $queryParams = [ 'pageNum' => $pageNum, 'pageSize' => $pageSize, ]; $url .= '?' . http_build_query($queryParams); $response = $client->get($url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ] ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI查询官方主体列表响应: ', $responseData); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { return [ 'success' => false, 'message' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API查询失败', 'data' => [] ]; } return [ 'success' => true, 'data' => $responseData['data'] ?? [] ]; } catch (\Exception $e) { dLog('generate')->error('可灵AI查询官方主体列表异常: ' . $e->getMessage()); return [ 'success' => false, 'message' => '查询失败: ' . $e->getMessage(), 'data' => [] ]; } } /** * 删除自定义主体 * * @param string $elementId * @return array */ public function deleteElement(string $elementId): array { $client = new Client(['verify' => false, 'timeout' => 120]); try { // 生成JWT token $jwtToken = $this->generateKelingJwtToken(); // 构建API请求参数 $apiParams = [ 'element_id' => $elementId, ]; dLog('generate')->info('可灵AI删除主体参数: ', $apiParams); // 调用可灵AI删除主体API $response = $client->post('https://api-beijing.klingai.com/v1/general/delete-elements', [ 'headers' => [ 'Authorization' => 'Bearer ' . $jwtToken, 'Content-Type' => 'application/json', ], 'json' => $apiParams ]); $responseData = json_decode($response->getBody(), true); dLog('generate')->info('可灵AI删除主体API响应: ', $responseData); if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) { // API返回错误 return [ 'success' => false, 'message' => $responseData['error']['message'] ?? $responseData['message'] ?? 'API Error' ]; } else { // 删除成功,更新本地记录状态 AIElement::where('element_id', $elementId)->update([ 'status' => AIElement::ELEMENT_STATUS_DELETED, 'updated_at' => now(), ]); return [ 'success' => true, 'message' => '主体删除成功', 'data' => $responseData['data'] ?? [] ]; } } catch (\Exception $e) { dLog('generate')->error('可灵AI删除主体API请求异常: ' . $e->getMessage()); return [ 'success' => false, 'message' => 'API请求失败: ' . $e->getMessage() ]; } } /** * 获取本地主体列表(包含自定义和官方) * * @param array $filters * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function getLocalElementsList(array $filters = []) { $query = AIElement::query(); // 筛选条件 if (isset($filters['owned_by'])) { $query->where('owned_by', $filters['owned_by']); } if (isset($filters['reference_type'])) { $query->where('reference_type', $filters['reference_type']); } if (isset($filters['status'])) { $query->where('status', $filters['status']); } if (isset($filters['task_status'])) { $query->where('task_status', $filters['task_status']); } // 搜索关键词 if (isset($filters['keyword']) && !empty($filters['keyword'])) { $query->where(function ($q) use ($filters) { $q->where('element_name', 'like', '%' . $filters['keyword'] . '%') ->orWhere('element_description', 'like', '%' . $filters['keyword'] . '%'); }); } return $query->orderBy('created_at', 'desc') ->paginate($filters['page_size'] ?? 20); } }