|
|
@@ -305,6 +305,9 @@ class AIVideoGenerationService
|
|
|
case 'keling':
|
|
|
$this->updateKelingOmniTask($task);
|
|
|
break;
|
|
|
+ case 'zzengine':
|
|
|
+ $this->updateUnifiedApiTask($task);
|
|
|
+ break;
|
|
|
default:
|
|
|
break;
|
|
|
}
|
|
|
@@ -2088,5 +2091,412 @@ class AIVideoGenerationService
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建统一API视频生成任务(智帧20等新模型)
|
|
|
+ *
|
|
|
+ * @param array $params
|
|
|
+ * @return MpGenerateVideoTask
|
|
|
+ */
|
|
|
+ public function createUnifiedApiTask(array $params): MpGenerateVideoTask
|
|
|
+ {
|
|
|
+ // 生成唯一的任务ID
|
|
|
+ $taskId = 'unified_' . 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['last_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' => 'zzengine',
|
|
|
+ 'extra_params' => [
|
|
|
+ 'task_type' => 'video',
|
|
|
+ 'model_code' => $params['model_code'] ?? 'zhizhen-20',
|
|
|
+ 'callback_url' => $params['callback_url'] ?? '',
|
|
|
+ 'video_duration' => $params['video_duration'] ?? 5,
|
|
|
+ 'video_ratio' => $params['video_ratio'] ?? '9:16',
|
|
|
+ 'parameters' => $params['parameters'] ?? [],
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 提交任务到统一API
|
|
|
+ $this->submitUnifiedApiTaskToApi($task);
|
|
|
+
|
|
|
+ return $task;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交任务到统一API
|
|
|
+ *
|
|
|
+ * @param MpGenerateVideoTask $task
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ private function submitUnifiedApiTaskToApi(MpGenerateVideoTask $task): void
|
|
|
+ {
|
|
|
+ $client = new Client(['verify' => false, 'timeout' => 120]);
|
|
|
+ try {
|
|
|
+ // 验证环境变量配置
|
|
|
+ $apiKey = env('ZHIZHEN_AI_SK');
|
|
|
+ $apiUrl = 'https://model-relay.zzengine.net/api/v1/task/submit';
|
|
|
+
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => '统一视频API密钥未配置,请检查环境变量UNIFIED_VIDEO_API_KEY'
|
|
|
+ ]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $extraParams = $task->extra_params;
|
|
|
+
|
|
|
+ // 构建统一API请求参数
|
|
|
+ $apiParams = [
|
|
|
+ 'task_type' => 'video',
|
|
|
+ 'model_code' => $extraParams['model_code'] ?? 'zhizhen-20',
|
|
|
+ 'prompt' => $task->prompt,
|
|
|
+ 'video_duration' => $extraParams['video_duration'] ?? 5,
|
|
|
+ 'video_resolution' => $task->video_resolution,
|
|
|
+ 'video_ratio' => $extraParams['video_ratio'] ?? '9:16',
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 添加回调地址
|
|
|
+ if (!empty($extraParams['callback_url'])) {
|
|
|
+ $apiParams['callback_url'] = $extraParams['callback_url'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加parameters参数
|
|
|
+ if (!empty($extraParams['parameters'])) {
|
|
|
+ $apiParams['parameters'] = $extraParams['parameters'];
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info('统一API视频生成参数: ', $apiParams);
|
|
|
+ logDB('generate', 'info', '统一API视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]);
|
|
|
+
|
|
|
+ // 调用统一API
|
|
|
+ $response = $client->post($apiUrl, [
|
|
|
+ 'headers' => [
|
|
|
+ 'X-Api-Key' => $apiKey,
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ ],
|
|
|
+ 'json' => $apiParams
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody(), true);
|
|
|
+ dLog('generate')->info('统一API响应: ', $responseData);
|
|
|
+ logDB('generate', 'info', '统一API视频生成API响应', ['id' => $task->id, 'response' => $responseData]);
|
|
|
+
|
|
|
+ if (isset($responseData['error']) || !isset($responseData['data']['task_uid'])) {
|
|
|
+ // API返回错误
|
|
|
+ $errorMsg = $responseData['error']['message'] ?? $responseData['message'] ?? 'API Error';
|
|
|
+ logDB('generate', 'error', '统一API视频生成任务提交失败', ['id' => $task->id, 'error' => $errorMsg]);
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => $errorMsg,
|
|
|
+ 'extra_params' => array_merge($task->extra_params, ['api_response' => $responseData])
|
|
|
+ ]);
|
|
|
+ } elseif (isset($responseData['data']['task_uid'])) {
|
|
|
+ // 任务提交成功,更新状态为处理中
|
|
|
+ $updateData = [
|
|
|
+ 'task_id' => $responseData['data']['task_uid'], // 使用API返回的task_uid
|
|
|
+ 'extra_params' => array_merge($task->extra_params, [
|
|
|
+ 'task_uid' => $responseData['data']['task_uid'],
|
|
|
+ 'estimated_amount' => $responseData['data']['estimated_amount'] ?? 0,
|
|
|
+ 'account_uid' => $responseData['data']['account_uid'] ?? '',
|
|
|
+ 'key_uid' => $responseData['data']['key_uid'] ?? '',
|
|
|
+ '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 (\GuzzleHttp\Exception\RequestException $e) {
|
|
|
+ $errorMsg = 'API请求失败';
|
|
|
+ if ($e->hasResponse()) {
|
|
|
+ $errorResponse = json_decode($e->getResponse()->getBody()->getContents(), true);
|
|
|
+ $errorMsg .= ': ' . ($errorResponse['message'] ?? $e->getMessage());
|
|
|
+ logDB('generate', 'error', '统一API请求失败', ['id' => $task->id, 'error' => $errorResponse]);
|
|
|
+ } else {
|
|
|
+ $errorMsg .= ': ' . $e->getMessage();
|
|
|
+ logDB('generate', 'error', '统一API请求失败', ['id' => $task->id, 'error' => $e->getMessage()]);
|
|
|
+ }
|
|
|
+
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => $errorMsg
|
|
|
+ ]);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ logDB('generate', 'error', '统一API任务提交异常', ['id' => $task->id, 'error' => $e->getMessage()]);
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => 'API请求失败: ' . $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询统一API任务状态
|
|
|
+ *
|
|
|
+ * @param MpGenerateVideoTask $task
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public function queryUnifiedApiTaskStatus(MpGenerateVideoTask $task): array
|
|
|
+ {
|
|
|
+ $client = new Client(['verify' => false, 'timeout' => 120]);
|
|
|
+ try {
|
|
|
+ // 获取API任务UID
|
|
|
+ $extraParams = $task->extra_params;
|
|
|
+ $taskUid = $extraParams['task_uid'] ?? $task->task_id;
|
|
|
+
|
|
|
+ if (!$taskUid) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => 'API任务UID不存在',
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证环境变量配置
|
|
|
+ $apiKey = env('ZHIZHEN_AI_SK');
|
|
|
+ $apiUrl = 'https://model-relay.zzengine.net';
|
|
|
+ $queryUrl = rtrim($apiUrl, '/') . '/api/v1/task/query';
|
|
|
+
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => '统一视频API密钥未配置',
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info('查询统一API任务状态: ', ['task_uid' => $taskUid]);
|
|
|
+
|
|
|
+ // 调用统一API查询接口(GET请求)
|
|
|
+ $response = $client->get($queryUrl, [
|
|
|
+ 'headers' => [
|
|
|
+ 'X-Api-Key' => $apiKey,
|
|
|
+ ],
|
|
|
+ 'query' => [
|
|
|
+ 'task_uid' => $taskUid
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody(), true);
|
|
|
+ dLog('generate')->info('统一API任务状态查询响应: ', $responseData);
|
|
|
+ logDB('generate', 'info', '统一API任务状态查询', ['id' => $task->id, 'response' => $responseData]);
|
|
|
+
|
|
|
+ // 检查响应code
|
|
|
+ if (!isset($responseData['code']) || $responseData['code'] !== 0) {
|
|
|
+ $errorMsg = $responseData['msg'] ?? $responseData['message'] ?? 'API查询失败';
|
|
|
+ logDB('generate', 'error', '统一API任务状态查询失败', ['id' => $task->id, 'error' => $errorMsg]);
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => $errorMsg,
|
|
|
+ 'result_url' => null,
|
|
|
+ 'result_json' => $responseData ?? []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析API响应
|
|
|
+ $taskData = $responseData['data']['task'] ?? [];
|
|
|
+ if (empty($taskData)) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => '任务数据为空',
|
|
|
+ 'result_url' => null,
|
|
|
+ 'result_json' => $responseData ?? []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 任务状态映射:0=排队中,1=执行中,2=成功,3=失败
|
|
|
+ $taskStatus = $taskData['status'] ?? 3;
|
|
|
+ $failReason = $taskData['fail_reason'] ?? '';
|
|
|
+
|
|
|
+ $returnData = [
|
|
|
+ 'status' => 'processing',
|
|
|
+ 'error_message' => null,
|
|
|
+ 'result_url' => null,
|
|
|
+ 'result_json' => $responseData ?? []
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 根据status判断任务状态
|
|
|
+ if ($taskStatus === 2) {
|
|
|
+ // 任务成功
|
|
|
+ $detail = $taskData['detail'] ?? [];
|
|
|
+ $result = $detail['result'] ?? [];
|
|
|
+
|
|
|
+ // 获取视频URL
|
|
|
+ $videoUrl = $result['video'] ?? null;
|
|
|
+ if (!$videoUrl && isset($result['videos']) && is_array($result['videos']) && count($result['videos']) > 0) {
|
|
|
+ $videoUrl = $result['videos'][0];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($videoUrl) {
|
|
|
+ // 保存视频到TOS
|
|
|
+ try {
|
|
|
+ $video_name = 'ai_generation_' . time() . '_' . uniqid();
|
|
|
+ $video_ext = getVideoExtFromUrl($videoUrl);
|
|
|
+ $video_name = $video_name . $video_ext;
|
|
|
+ $url = uploadStreamByTos('video', file_get_contents($videoUrl), $video_name);
|
|
|
+ $returnData['result_url'] = $url;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error('保存视频到TOS失败', ['error' => $e->getMessage()]);
|
|
|
+ $returnData['result_url'] = $videoUrl; // 如果保存失败,使用原URL
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $returnData['status'] = 'failed';
|
|
|
+ $returnData['error_message'] = '视频URL不存在';
|
|
|
+ return $returnData;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取视频时长
|
|
|
+ $video_duration = $detail['duration'] ?? 0;
|
|
|
+ if ($video_duration > 0) {
|
|
|
+ $returnData['video_duration'] = (int)$video_duration;
|
|
|
+ $returnData['video_time_point_start'] = 0;
|
|
|
+ $returnData['video_time_point_end'] = (int)$video_duration;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取尾帧
|
|
|
+ if (isset($result['last_frame_url'])) {
|
|
|
+ $returnData['last_frame_url'] = $result['last_frame_url'];
|
|
|
+ }
|
|
|
+
|
|
|
+ $returnData['status'] = 'success';
|
|
|
+ } elseif ($taskStatus === 3) {
|
|
|
+ // 任务失败
|
|
|
+ $returnData['status'] = 'failed';
|
|
|
+ $returnData['error_message'] = $failReason ?: '任务执行失败';
|
|
|
+ } elseif (in_array($taskStatus, [0, 1])) {
|
|
|
+ // 0=排队中,1=执行中
|
|
|
+ $returnData['status'] = 'processing';
|
|
|
+ } else {
|
|
|
+ $returnData['status'] = 'failed';
|
|
|
+ $returnData['error_message'] = '未知的任务状态: ' . $taskStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $returnData;
|
|
|
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
|
+ $errorMsg = 'API查询失败';
|
|
|
+ if ($e->hasResponse()) {
|
|
|
+ $errorResponse = json_decode($e->getResponse()->getBody()->getContents(), true);
|
|
|
+ $errorMsg .= ': ' . ($errorResponse['msg'] ?? $errorResponse['message'] ?? $e->getMessage());
|
|
|
+ } else {
|
|
|
+ $errorMsg .= ': ' . $e->getMessage();
|
|
|
+ }
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => $errorMsg,
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return [
|
|
|
+ 'status' => 'failed',
|
|
|
+ 'error_message' => $e->getMessage(),
|
|
|
+ 'result_url' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新统一API视频任务状态
|
|
|
+ *
|
|
|
+ * @param MpGenerateVideoTask $task
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ private function updateUnifiedApiTask($task): void
|
|
|
+ {
|
|
|
+ $statusInfo = $this->queryUnifiedApiTaskStatus($task);
|
|
|
+ if (!isset($statusInfo['status'])) return;
|
|
|
+
|
|
|
+ if ($statusInfo['status'] === 'success') {
|
|
|
+ logDB('generate', 'info', '统一API视频生成任务成功', ['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 = [
|
|
|
+ 'origin_video_url' => $statusInfo['result_url'],
|
|
|
+ 'video_task_status' => '已完成',
|
|
|
+ 'current_type' => 2,
|
|
|
+ 'last_frame_url' => $statusInfo['last_frame_url'] ?? '',
|
|
|
+ 'updated_at' => $now
|
|
|
+ ];
|
|
|
+ $compressed_video_url = compressVideo($statusInfo['result_url']);
|
|
|
+ $segmentUpdateData['video_url'] = $compressed_video_url ?: $statusInfo['result_url'];
|
|
|
+
|
|
|
+ // 只有当video_duration存在且大于0时才更新
|
|
|
+ if (isset($statusInfo['video_duration']) && $statusInfo['video_duration'] > 0) {
|
|
|
+ $segmentUpdateData['video_duration'] = $statusInfo['video_duration'];
|
|
|
+ $segmentUpdateData['video_time_point_start'] = 0;
|
|
|
+ $audio_duration = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('audio_duration');
|
|
|
+ $segmentUpdateData['video_time_point_end'] = $audio_duration ?? 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新分镜表
|
|
|
+ DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update($segmentUpdateData);
|
|
|
+
|
|
|
+ // 添加视频生成对话记录
|
|
|
+ $this->addVideoGenerationRecords($segment_id, $statusInfo['result_url']);
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
+ dLog('generate')->error('统一API视频任务处理失败', [
|
|
|
+ 'segment_id' => $segment_id,
|
|
|
+ 'task_id' => $task->id,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } elseif ($statusInfo['status'] === 'failed') {
|
|
|
+ logDB('generate', 'error', '统一API视频生成任务失败', ['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', '统一API视频生成任务超时', ['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')
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|