|
|
@@ -4,6 +4,7 @@ namespace App\Services\AIGeneration;
|
|
|
|
|
|
use App\Facade\Site;
|
|
|
use App\Libs\Utils;
|
|
|
+use App\Models\MpAsset;
|
|
|
use App\Models\MpGenerateVideoTask;
|
|
|
use App\Models\AIElement;
|
|
|
use App\Services\PointsService;
|
|
|
@@ -560,6 +561,57 @@ class AIVideoGenerationService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 创建 Seedance 2.0(百度AI网关)视频生成任务
|
|
|
+ *
|
|
|
+ * @param array $params
|
|
|
+ * @return MpGenerateVideoTask
|
|
|
+ */
|
|
|
+ public function createSeedance20Task(array $params): MpGenerateVideoTask
|
|
|
+ {
|
|
|
+ // 生成唯一的任务ID
|
|
|
+ $taskId = 'seedance20_' . time() . '_' . uniqid();
|
|
|
+
|
|
|
+ // 创建任务记录,复用现有表结构
|
|
|
+ $task = MpGenerateVideoTask::create([
|
|
|
+ 'task_id' => $taskId,
|
|
|
+ 'alias_segment_id' => $params['alias_segment_id'] ?? '',
|
|
|
+ 'alias_act_id' => $params['alias_act_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任务轮询流程(提交/查询接口按extra_params区分网关)
|
|
|
+ 'extra_params' => [
|
|
|
+ 'model' => $params['model'] ?? 'doubao-seedance-2.0',
|
|
|
+ 'content' => $params['content'],
|
|
|
+ 'reference_images' => $params['reference_images'] ?? [],
|
|
|
+ 'reference_image_assets' => $params['reference_image_assets'] ?? [],
|
|
|
+ 'callback_url' => $params['callback_url'] ?? '',
|
|
|
+ 'return_last_frame' => $params['return_last_frame'] ?? true,
|
|
|
+ 'execution_expires_after' => $params['execution_expires_after'] ?? 172800,
|
|
|
+ 'video_resolution' => $params['video_resolution'] ?? '720P',
|
|
|
+ '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,
|
|
|
+ // 百度AI网关配置
|
|
|
+ 'gateway_base_url' => 'https://ai-gateway.baidubce.com',
|
|
|
+ 'api_key_env' => 'BAIDU_SEEDANCE_20_AK',
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 提交任务到百度AI网关
|
|
|
+ $this->submitSeedanceTaskToApi($task);
|
|
|
+
|
|
|
+ return $task;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 提交 Seedance 任务到火山引擎 API
|
|
|
*
|
|
|
* @param MpGenerateVideoTask $task
|
|
|
@@ -569,22 +621,49 @@ class AIVideoGenerationService
|
|
|
{
|
|
|
$client = new Client(['verify'=>false,'timeout'=>120]);
|
|
|
try {
|
|
|
+ $extraParams = $task->extra_params;
|
|
|
+
|
|
|
+ // 网关配置:Seedance 2.0 走百度AI网关,其余默认走火山引擎
|
|
|
+ $gatewayBaseUrl = $extraParams['gateway_base_url'] ?? 'https://ark.cn-beijing.volces.com';
|
|
|
+ $apiKeyEnv = $extraParams['api_key_env'] ?? 'VOLC_AI_API_KEY';
|
|
|
+
|
|
|
// 验证环境变量配置
|
|
|
- $apiKey = env('VOLC_AI_API_KEY');
|
|
|
-
|
|
|
+ $apiKey = env($apiKeyEnv);
|
|
|
+
|
|
|
if (empty($apiKey)) {
|
|
|
$task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
|
|
|
- 'error_message' => '火山引擎访问密钥未配置,请检查环境变量API Key'
|
|
|
+ 'error_message' => $apiKeyEnv . ' 未配置,请检查环境变量API Key'
|
|
|
]);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- $extraParams = $task->extra_params;
|
|
|
+ $content = $extraParams['content'] ?? [];
|
|
|
+
|
|
|
+ // Seedance 2.0(百度AI网关)需要先把图片上传为素材
|
|
|
+ if (!empty($extraParams['gateway_base_url'])) {
|
|
|
+ $content = $this->convertSeedance20ContentImagesToAssets($content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 追加参考图素材(已由processReferenceImagesToSeedance20Assets上传并清理失败的图片标记)
|
|
|
+ if (!empty($extraParams['gateway_base_url']) && !empty($extraParams['reference_image_assets'])) {
|
|
|
+ foreach ($extraParams['reference_image_assets'] as $assetUrl) {
|
|
|
+ if (empty($assetUrl)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'image_url',
|
|
|
+ 'image_url' => [
|
|
|
+ 'url' => $assetUrl,
|
|
|
+ ],
|
|
|
+ 'role' => 'reference_image',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 构建火山引擎 Seedance API 请求参数
|
|
|
$apiParams = [
|
|
|
'model' => isset(\App\Consts\BaseConst::MODELS_MAP[$extraParams['model']]) ? \App\Consts\BaseConst::MODELS_MAP[$extraParams['model']] : $extraParams['model'],
|
|
|
- 'content' => $extraParams['content'],
|
|
|
+ 'content' => $content,
|
|
|
];
|
|
|
|
|
|
// 添加可选参数
|
|
|
@@ -594,7 +673,7 @@ class AIVideoGenerationService
|
|
|
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'])) {
|
|
|
+ if (!empty($extraParams['service_tier']) && !in_array($extraParams['model'], ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'doubao-seedance-2.0'])) {
|
|
|
$apiParams['service_tier'] = $extraParams['service_tier'];
|
|
|
}
|
|
|
if (!empty($extraParams['execution_expires_after'])) {
|
|
|
@@ -628,11 +707,11 @@ class AIVideoGenerationService
|
|
|
$apiParams['watermark'] = $extraParams['watermark'];
|
|
|
}
|
|
|
|
|
|
- dLog('generate')->info('Seedance 1.5 Pro 视频生成参数: ', $apiParams);
|
|
|
- logDB('generate', 'info', 'Seedance视频生成任务提交', ['id' => $task->id, 'params' => $apiParams]);
|
|
|
+ dLog('generate')->info('Seedance 视频生成参数: ', $apiParams);
|
|
|
+ logDB('generate', 'info', 'Seedance视频生成任务提交', ['id' => $task->id, 'model' => $apiParams['model'], 'params' => $apiParams]);
|
|
|
|
|
|
- // 调用火山引擎 Seedance API
|
|
|
- $response = $client->post('https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', [
|
|
|
+ // 调用 Seedance API
|
|
|
+ $response = $client->post($gatewayBaseUrl . '/api/v3/contents/generations/tasks', [
|
|
|
'headers' => [
|
|
|
'Authorization' => 'Bearer ' . $apiKey,
|
|
|
'Content-Type' => 'application/json',
|
|
|
@@ -642,7 +721,7 @@ class AIVideoGenerationService
|
|
|
|
|
|
|
|
|
$responseData = json_decode($response->getBody(), true);
|
|
|
- dLog('generate')->info('Seedance 1.5 Pro API 响应: ', $responseData);
|
|
|
+ dLog('generate')->info('Seedance API 响应: ', $responseData);
|
|
|
logDB('generate', 'info', 'Seedance视频生成API响应', ['id' => $task->id, 'response' => $responseData]);
|
|
|
|
|
|
if (isset($responseData['error'])) {
|
|
|
@@ -674,6 +753,413 @@ class AIVideoGenerationService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将 Seedance 2.0 参考图上传为百度AI网关素材
|
|
|
+ * 失败处理与智真AI的processReferenceImagesToAssets一致:删除参考图的同时清理提示词中的<图片N>标记
|
|
|
+ *
|
|
|
+ * @param array $referenceImages 参考图片URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败图片的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceImagesToSeedance20Assets(array $referenceImages, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ $assetIds = [];
|
|
|
+ $failedIndexes = []; // 记录失败的图片序号
|
|
|
+
|
|
|
+ foreach ($referenceImages as $index => $imageUrl) {
|
|
|
+ $imageIndex = $index + 1; // 图片序号从1开始
|
|
|
+
|
|
|
+ try {
|
|
|
+ dLog('generate')->info("开始处理Seedance 2.0参考图片 {$imageIndex}: {$imageUrl}");
|
|
|
+
|
|
|
+ // 上传素材(已存在则直接复用,不重复上传)
|
|
|
+ $createResult = $this->createSeedance20Asset($imageUrl, "参考图片_{$imageIndex}_" . time());
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ dLog('generate')->warning("图片 {$imageIndex} 创建Seedance 2.0素材失败: " . $createResult['msg']);
|
|
|
+ logDB('generate', 'warning', 'Seedance2.0创建素材失败', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'index' => $imageIndex,
|
|
|
+ 'error' => $createResult['msg']
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $imageIndex;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $assetIds[] = $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info("图片 {$imageIndex} Seedance 2.0素材上传成功,asset_id: " . $createResult['data']['asset_id']);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error("图片 {$imageIndex} Seedance 2.0素材处理异常: " . $e->getMessage());
|
|
|
+ logDB('generate', 'error', 'Seedance2.0素材处理异常', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'index' => $imageIndex,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $imageIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理失败的图片:从 fullPrompt 中删除对应的标记并调整序号
|
|
|
+ if (!empty($failedIndexes)) {
|
|
|
+ dLog('generate')->info('开始处理Seedance 2.0失败图片的提示词调整,失败序号: ' . implode(',', $failedIndexes));
|
|
|
+
|
|
|
+ // 按序号从大到小排序,避免删除时影响后续序号
|
|
|
+ rsort($failedIndexes);
|
|
|
+
|
|
|
+ foreach ($failedIndexes as $failedIndex) {
|
|
|
+ // 删除失败图片的标记,例如 <图片1>、<图片2> 等
|
|
|
+ $pattern = "/<图片{$failedIndex}>/u";
|
|
|
+ $fullPrompt = preg_replace($pattern, '', $fullPrompt);
|
|
|
+ dLog('generate')->info("已从提示词中删除 <图片{$failedIndex}>");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新调整剩余图片的序号
|
|
|
+ $needAdjust = false;
|
|
|
+ foreach ($failedIndexes as $failedIndex) {
|
|
|
+ $needAdjust = true;
|
|
|
+ // 将所有大于失败序号的图片标记序号减1
|
|
|
+ for ($i = $failedIndex + 1; $i <= count($referenceImages); $i++) {
|
|
|
+ $oldPattern = "/<图片{$i}>/u";
|
|
|
+ $newTag = "<图片" . ($i - 1) . ">";
|
|
|
+ $fullPrompt = preg_replace($oldPattern, $newTag, $fullPrompt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($needAdjust) {
|
|
|
+ dLog('generate')->info('Seedance 2.0提示词图片序号调整完成');
|
|
|
+ logDB('generate', 'info', 'Seedance2.0提示词调整', [
|
|
|
+ 'failed_indexes' => $failedIndexes,
|
|
|
+ 'adjusted_prompt' => $fullPrompt
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info('Seedance 2.0参考图片转素材完成,成功数量: ' . count($assetIds) . ',失败数量: ' . count($failedIndexes));
|
|
|
+
|
|
|
+ // 格式化为可直接在API接口中使用的数组
|
|
|
+ foreach ($assetIds as &$assetId) {
|
|
|
+ $assetId = "asset://{$assetId}";
|
|
|
+ }
|
|
|
+
|
|
|
+ return $assetIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 Seedance 2.0 内容中的图片URL上传为百度AI网关素材
|
|
|
+ * 上传失败的图片直接从content中移除,不阻断整个任务
|
|
|
+ *
|
|
|
+ * @param array $content
|
|
|
+ * @return array 转换后的content(图片URL替换为 asset://{AssetId})
|
|
|
+ */
|
|
|
+ private function convertSeedance20ContentImagesToAssets(array $content): array
|
|
|
+ {
|
|
|
+ foreach ($content as $index => &$item) {
|
|
|
+ if (($item['type'] ?? '') !== 'image_url') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $imageUrl = $item['image_url']['url'] ?? '';
|
|
|
+ if (empty($imageUrl) || strpos($imageUrl, 'asset://') === 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $name = 'seedance20_' . time() . '_' . uniqid();
|
|
|
+ $createResult = $this->createSeedance20Asset($imageUrl, $name);
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ // 上传失败:移除该图片,继续处理其他图片
|
|
|
+ dLog('generate')->warning('Seedance 2.0 图片上传素材失败,已从content中移除: ' . $imageUrl . ',原因: ' . $createResult['msg']);
|
|
|
+ logDB('generate', 'warning', 'Seedance2.0图片转素材失败', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'error' => $createResult['msg']
|
|
|
+ ]);
|
|
|
+ unset($content[$index]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $item['image_url']['url'] = 'asset://' . $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info('Seedance 2.0 图片转素材成功: ' . $imageUrl . ' => asset://' . $createResult['data']['asset_id']);
|
|
|
+ }
|
|
|
+ unset($item);
|
|
|
+
|
|
|
+ return array_values($content);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传图片到百度AI网关素材库并等待素材可用
|
|
|
+ * 上传前先检查mp_assets中是否已存在该url(source_type=baidu),存在则直接复用素材ID,避免重复上传
|
|
|
+ *
|
|
|
+ * @param string $url 图片公网地址
|
|
|
+ * @param string $name 素材名称
|
|
|
+ * @return array ['code'=>0, 'data'=>['asset_id'=>...]] 成功;['code'=>20003, 'msg'=>...] 失败
|
|
|
+ */
|
|
|
+ private function createSeedance20Asset(string $url, string $name): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ if (empty($url)) {
|
|
|
+ return [
|
|
|
+ 'code' => 1,
|
|
|
+ 'msg' => '素材URL不能为空',
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先获取表中是否已有该url(仅限百度网关来源的素材)
|
|
|
+ $asset = DB::table('mp_assets')->where('url', $url)->where('source_type', 'baidu')->first();
|
|
|
+ if ($asset) {
|
|
|
+ dLog('generate')->info('Seedance 2.0 素材已存在,直接复用: ' . $url . ' => ' . $asset->asset_id);
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'msg' => '创建成功',
|
|
|
+ 'data' => [
|
|
|
+ 'asset_id' => $asset->asset_id,
|
|
|
+ 'group_id' => $asset->group_id,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $groupId = $this->ensureSeedance20AssetGroup();
|
|
|
+
|
|
|
+ $responseData = $this->requestSeedance20Gateway('CreateAsset', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ 'GroupId' => $groupId,
|
|
|
+ 'AssetType' => 'Image',
|
|
|
+ 'Name' => $name,
|
|
|
+ 'URL' => $url,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $assetId = $result['Id'] ?? $result['AssetId'] ?? '';
|
|
|
+ if (empty($assetId)) {
|
|
|
+ logDB('generate', 'error', 'Seedance2.0创建素材失败', ['url' => $url, 'response' => $responseData]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '创建Seedance 2.0素材失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存素材记录到数据库
|
|
|
+ MpAsset::create([
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ 'group_name' => 'default',
|
|
|
+ 'name' => $name,
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => MpAsset::TYPE_IMAGE,
|
|
|
+ 'source_type' => 'baidu',
|
|
|
+ 'status' => MpAsset::STATUS_PROCESSING,
|
|
|
+ 'extra_params' => $responseData,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 轮询查询素材状态(每3秒一次,最多100次即5分钟)
|
|
|
+ $maxAttempts = 100;
|
|
|
+ $assetStatus = null;
|
|
|
+ for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
|
|
+ sleep(3);
|
|
|
+
|
|
|
+ $assetInfo = $this->getSeedance20Asset($assetId);
|
|
|
+ $assetStatus = $assetInfo['Status'] ?? '';
|
|
|
+ dLog('generate')->info("Seedance 2.0 素材 {$assetId} 状态查询第 {$attempt} 次,状态: {$assetStatus}");
|
|
|
+
|
|
|
+ if ($assetStatus === 'Active') {
|
|
|
+ logDB('generate', 'info', 'Seedance2.0素材处理成功', [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'url' => $url
|
|
|
+ ]);
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'msg' => '创建成功',
|
|
|
+ 'data' => [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($assetStatus === 'Failed') {
|
|
|
+ $errorMessage = $assetInfo['Error']['Message'] ?? json_encode($assetInfo, JSON_UNESCAPED_UNICODE);
|
|
|
+ logDB('generate', 'error', 'Seedance2.0素材处理失败', [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'url' => $url,
|
|
|
+ 'error' => $errorMessage
|
|
|
+ ]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => 'Seedance 2.0素材处理失败: ' . $errorMessage,
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logDB('generate', 'error', 'Seedance2.0素材处理超时', [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'url' => $url,
|
|
|
+ 'last_status' => $assetStatus
|
|
|
+ ]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => 'Seedance 2.0素材处理超时: ' . $assetId . ',最后状态: ' . ($assetStatus ?? 'unknown'),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error('Seedance 2.0创建素材异常: ' . $e->getMessage());
|
|
|
+ logDB('generate', 'error', 'Seedance2.0创建素材异常', ['url' => $url, 'error' => $e->getMessage()]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '创建Seedance 2.0素材失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询百度AI网关素材状态(优先查本地mp_assets,处理中/缺失时请求网关并同步状态)
|
|
|
+ *
|
|
|
+ * @param string $assetId
|
|
|
+ * @return array 素材信息(含Status字段)
|
|
|
+ */
|
|
|
+ private function getSeedance20Asset(string $assetId): array
|
|
|
+ {
|
|
|
+ // 先查本地素材表(仅限百度网关来源的素材)
|
|
|
+ $assetInfo = DB::table('mp_assets')->where('asset_id', $assetId)->where('source_type', 'baidu')->first();
|
|
|
+ if ($assetInfo) {
|
|
|
+ $assetInfo = (array)$assetInfo;
|
|
|
+ $status = getProp($assetInfo, 'status');
|
|
|
+ if (in_array($status, [MpAsset::STATUS_ACTIVE, MpAsset::STATUS_FAILED])) {
|
|
|
+ $result = [
|
|
|
+ 'Id' => $assetInfo['asset_id'],
|
|
|
+ 'Status' => $status,
|
|
|
+ 'URL' => $assetInfo['url'] ?? '',
|
|
|
+ ];
|
|
|
+ if ($status === MpAsset::STATUS_FAILED && !empty($assetInfo['error_message'])) {
|
|
|
+ $result['Error'] = ['Message' => $assetInfo['error_message']];
|
|
|
+ }
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $responseData = $this->requestSeedance20Gateway('GetAsset', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ 'Id' => $assetId,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ // 兼容不同返回结构
|
|
|
+ if (isset($result['Asset']) && is_array($result['Asset'])) {
|
|
|
+ $result = $result['Asset'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询失败时快速结束,避免长时间轮询
|
|
|
+ if (empty($result) && isset($responseData['Error'])) {
|
|
|
+ $result['Status'] = 'Failed';
|
|
|
+ $result['Error'] = $responseData['Error'];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 同步更新本地素材状态
|
|
|
+ if (!empty($result['Status'])) {
|
|
|
+ $updateData = ['status' => $result['Status']];
|
|
|
+ if (!empty($result['Error']['Message'])) {
|
|
|
+ $updateData['error_message'] = $result['Error']['Message'];
|
|
|
+ }
|
|
|
+ DB::table('mp_assets')
|
|
|
+ ->where('asset_id', $assetId)
|
|
|
+ ->where('source_type', 'baidu')
|
|
|
+ ->update($updateData);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可用的Seedance 2.0素材组ID(不存在则创建默认素材组)
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ private function ensureSeedance20AssetGroup(): string
|
|
|
+ {
|
|
|
+ $groups = $this->listSeedance20AssetGroups();
|
|
|
+ if (!empty($groups)) {
|
|
|
+ $groupId = $groups[0]['Id'] ?? $groups[0]['GroupId'] ?? '';
|
|
|
+ if (!empty($groupId)) {
|
|
|
+ return $groupId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->createSeedance20AssetGroup();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询百度AI网关素材组列表
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function listSeedance20AssetGroups(): array
|
|
|
+ {
|
|
|
+ $responseData = $this->requestSeedance20Gateway('ListAssetGroups', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $groups = $result['AssetGroups'] ?? $result['Groups'] ?? $result['Items'] ?? [];
|
|
|
+
|
|
|
+ return is_array($groups) ? $groups : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建Seedance 2.0默认素材组
|
|
|
+ *
|
|
|
+ * @return string 素材组ID
|
|
|
+ */
|
|
|
+ private function createSeedance20AssetGroup(): string
|
|
|
+ {
|
|
|
+ $responseData = $this->requestSeedance20Gateway('CreateAssetGroup', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ 'Name' => 'default',
|
|
|
+ 'Description' => 'Seedance 2.0 默认素材组',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $groupId = $result['GroupId'] ?? $result['Id'] ?? '';
|
|
|
+ if (empty($groupId)) {
|
|
|
+ throw new \Exception('创建Seedance 2.0素材组失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE));
|
|
|
+ }
|
|
|
+
|
|
|
+ logDB('generate', 'info', 'Seedance2.0素材组创建成功', ['group_id' => $groupId]);
|
|
|
+ return $groupId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用百度AI网关素材接口
|
|
|
+ *
|
|
|
+ * @param string $action 接口Action(CreateAssetGroup/CreateAsset/GetAsset等)
|
|
|
+ * @param array $body 请求体
|
|
|
+ * @return array 响应数组
|
|
|
+ */
|
|
|
+ private function requestSeedance20Gateway(string $action, array $body): array
|
|
|
+ {
|
|
|
+ $apiKey = env('BAIDU_SEEDANCE_20_AK');
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ throw new \Exception('百度Seedance 2.0 API KEY未配置,请检查环境变量BAIDU_SEEDANCE_20_AK');
|
|
|
+ }
|
|
|
+
|
|
|
+ $client = new Client(['verify' => false, 'timeout' => 120]);
|
|
|
+ $response = $client->post('https://ai-gateway.baidubce.com/?Action=' . $action . '&Version=2024-01-01', [
|
|
|
+ 'headers' => [
|
|
|
+ 'Authorization' => 'Bearer ' . $apiKey,
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ ],
|
|
|
+ 'json' => $body,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody(), true);
|
|
|
+ dLog('generate')->info('Seedance 2.0 网关 ' . $action . ' 响应: ', $responseData);
|
|
|
+ logDB('generate', 'info', 'Seedance2.0网关' . $action, ['params' => $body, 'response' => $responseData]);
|
|
|
+
|
|
|
+ return $responseData;
|
|
|
+ }
|
|
|
+
|
|
|
// 更新Seedance视频任务状态
|
|
|
private function updateSeedanceTask($task) {
|
|
|
$statusInfo = $this->querySeedanceTaskStatus($task);
|
|
|
@@ -816,19 +1302,23 @@ class AIVideoGenerationService
|
|
|
];
|
|
|
}
|
|
|
|
|
|
- // 调用火山引擎 API 查询任务状态
|
|
|
- $apiKey = env('VOLC_AI_API_KEY');
|
|
|
-
|
|
|
+ // 调用 API 查询任务状态(Seedance 2.0走百度AI网关,其余走火山引擎)
|
|
|
+ $extraParams = is_array($task->extra_params) ? $task->extra_params : json_decode($task->extra_params, true);
|
|
|
+ $gatewayBaseUrl = $extraParams['gateway_base_url'] ?? 'https://ark.cn-beijing.volces.com';
|
|
|
+ $apiKeyEnv = $extraParams['api_key_env'] ?? 'VOLC_AI_API_KEY';
|
|
|
+
|
|
|
+ $apiKey = env($apiKeyEnv);
|
|
|
+
|
|
|
if (empty($apiKey)) {
|
|
|
return [
|
|
|
'status' => MpGenerateVideoTask::STATUS_FAILED,
|
|
|
'result_json' => [],
|
|
|
- 'error_message' => '火山引擎访问密钥未配置'
|
|
|
+ 'error_message' => $apiKeyEnv . ' 未配置'
|
|
|
];
|
|
|
}
|
|
|
|
|
|
// 构建查询 URL
|
|
|
- $url = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/' . $task->task_id;
|
|
|
+ $url = $gatewayBaseUrl . '/api/v3/contents/generations/tasks/' . $task->task_id;
|
|
|
|
|
|
$response = $client->get($url, [
|
|
|
'headers' => [
|