|
|
@@ -37,6 +37,7 @@ class AIVideoGenerationService
|
|
|
'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创建任务
|
|
|
@@ -261,12 +262,15 @@ class AIVideoGenerationService
|
|
|
foreach ($tasks as $task) {
|
|
|
$api_type = getProp($task, 'api_type');
|
|
|
switch($api_type) {
|
|
|
- case 'seedance_1_5_pro':
|
|
|
+ case 'seedance':
|
|
|
$this->updateSeedanceTask($task);
|
|
|
break;
|
|
|
- case 'jimeng_ai':
|
|
|
+ case 'jimeng':
|
|
|
$this->updateJimengTask($task);
|
|
|
break;
|
|
|
+ case 'keling':
|
|
|
+ $this->updateKelingOmniTask($task);
|
|
|
+ break;
|
|
|
default:
|
|
|
break;
|
|
|
}
|
|
|
@@ -331,9 +335,8 @@ class AIVideoGenerationService
|
|
|
'frames' => $params['video_duration'] ? ($params['video_duration'] * 24 + 1) : null,
|
|
|
'video_resolution' => $params['video_resolution'] ?? '720P',
|
|
|
'status' => MpGenerateVideoTask::STATUS_PENDING,
|
|
|
- 'api_type' => 'seedance_1_5_pro', // 标识为 Seedance 1.5 Pro API
|
|
|
+ 'api_type' => 'seedance', // 标识为 Seedance 1.5 Pro API
|
|
|
'extra_params' => [
|
|
|
- 'api_type' => 'seedance_1_5_pro',
|
|
|
'model' => $params['model'] ?? 'doubao-seedance-1-5-pro-251215',
|
|
|
'content' => $params['content'],
|
|
|
'callback_url' => $params['callback_url'] ?? '',
|
|
|
@@ -641,6 +644,342 @@ class AIVideoGenerationService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 生成可灵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'] = $extraParams['element_list'];
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // 调用可灵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);
|
|
|
+
|
|
|
+ if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) {
|
|
|
+ // API返回错误
|
|
|
+ $errorMessage = $responseData['error']['message'] ?? $responseData['message'] ?? 'API Error';
|
|
|
+ $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) {
|
|
|
+ // 记录错误
|
|
|
+ $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);
|
|
|
+
|
|
|
+ if (isset($responseData['error']) || (isset($responseData['code']) && $responseData['code'] !== 0)) {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ 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') {
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
|
|
|
+ } elseif ($statusInfo['status'] === 'failed') {
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => $statusInfo['error_message'],
|
|
|
+ 'result_json' => $statusInfo['result_json'] ?? []
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ // 如果仍然是处理中状态,不做任何操作
|
|
|
+
|
|
|
+ // 处理:如果任务处理超过24小时,标记为失败
|
|
|
+ $processingTime = now()->diffInHours($task->created_at);
|
|
|
+ if ($processingTime > 24) {
|
|
|
+ $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
+ 'error_message' => '任务处理超时(超过24小时)'
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 映射任务状态到前端显示状态
|
|
|
*
|
|
|
* @param string $taskStatus
|