|
|
@@ -642,6 +642,9 @@ class AIVideoGenerationService
|
|
|
{
|
|
|
// 分辨率统一规范化为小写(480p=原始档;sr_720p=480p生成+本地超分到720p)
|
|
|
$params['video_resolution'] = strtolower(trim((string)($params['video_resolution'] ?? '720p')));
|
|
|
+ $modelName = (string)($params['model'] ?? 'doubao-seedance-2.0');
|
|
|
+ // 海外百度Seedance(dreamina-* 真人素材空间)使用独立AK;系统内统一 baidu- 前缀名
|
|
|
+ $isDreaminaModel = strpos($modelName, 'baidu-dreamina-') === 0;
|
|
|
|
|
|
// 生成唯一的任务ID
|
|
|
$taskId = 'seedance20_' . time() . '_' . uniqid();
|
|
|
@@ -661,7 +664,7 @@ class AIVideoGenerationService
|
|
|
'status' => MpGenerateVideoTask::STATUS_PENDING,
|
|
|
'api_type' => 'seedance', // 复用Seedance任务轮询流程(提交/查询接口按extra_params区分网关)
|
|
|
'extra_params' => [
|
|
|
- 'model' => $params['model'] ?? 'doubao-seedance-2.0',
|
|
|
+ 'model' => $modelName,
|
|
|
'content' => $params['content'],
|
|
|
'reference_images' => $params['reference_images'] ?? [],
|
|
|
'reference_image_assets' => $params['reference_image_assets'] ?? [],
|
|
|
@@ -680,7 +683,9 @@ class AIVideoGenerationService
|
|
|
// 网关配置:默认百度AI网关;快快AI可传 https://ai-api.kkidc.com + gateway_path_prefix=/sd
|
|
|
'gateway_base_url' => $params['gateway_base_url'] ?? 'https://ai-gateway.baidubce.com',
|
|
|
'gateway_path_prefix' => $params['gateway_path_prefix'] ?? '',
|
|
|
- 'api_key_env' => $params['api_key_env'] ?? 'BAIDU_SEEDANCE_20_AK',
|
|
|
+ // 网关类型:baidu=国内百度、baidu_dreamina=海外百度真人素材空间、kuaikuai=快快AI
|
|
|
+ 'gateway_kind' => $params['gateway_kind'] ?? ($isDreaminaModel ? 'baidu_dreamina' : 'baidu'),
|
|
|
+ 'api_key_env' => $params['api_key_env'] ?? ($isDreaminaModel ? 'BAIDU_DREAMINA_AK' : 'BAIDU_SEEDANCE_20_AK'),
|
|
|
],
|
|
|
'charge_info' => $this->buildChargeInfo($params, 'seedance'),
|
|
|
]);
|
|
|
@@ -720,10 +725,16 @@ class AIVideoGenerationService
|
|
|
|
|
|
$content = $extraParams['content'] ?? [];
|
|
|
|
|
|
- // Seedance 2.0 类网关需要先把图片上传为素材(百度AI网关/快快AI)
|
|
|
+ // Seedance 2.0 类网关需要先把图片上传为素材(国内百度AI网关/海外百度真人素材空间/快快AI)
|
|
|
if (!empty($extraParams['gateway_base_url'])) {
|
|
|
+ $gatewayKind = $extraParams['gateway_kind'] ?? '';
|
|
|
+ $isDreaminaGateway = $gatewayKind === 'baidu_dreamina'
|
|
|
+ || strpos((string)($extraParams['model'] ?? ''), 'baidu-dreamina-') === 0;
|
|
|
if (strpos($gatewayBaseUrl, 'ai-api.kkidc.com') !== false || $gatewayPathPrefix === '/sd') {
|
|
|
$content = $this->convertKuaikuaiContentImagesToAssets($content);
|
|
|
+ } elseif ($isDreaminaGateway) {
|
|
|
+ // 海外百度真人素材需上传到 target-asset-space=dreamina-seedance-livenessface 资产空间
|
|
|
+ $content = $this->convertDreaminaContentImagesToAssets($content);
|
|
|
} else {
|
|
|
$content = $this->convertSeedance20ContentImagesToAssets($content);
|
|
|
}
|
|
|
@@ -812,8 +823,8 @@ class AIVideoGenerationService
|
|
|
if (!empty($extraParams['duration'])) {
|
|
|
$apiParams['duration'] = (int)$extraParams['duration'];
|
|
|
}
|
|
|
- // 百度 Seedance 2.5 带参考视频时,入参与快快 seed-2.5 一致:ratio=adaptive、duration=-1
|
|
|
- if ($requestModel === 'doubao-seedance-2-5-260628' && !empty($extraParams['reference_video_assets'])) {
|
|
|
+ // 百度 Seedance 2.5(含海外dreamina 2.5)带参考视频时,入参与快快 seed-2.5 一致:ratio=adaptive、duration=-1
|
|
|
+ if (in_array($requestModel, ['doubao-seedance-2-5-260628', 'dreamina-seedance-2-5-260628'], true) && !empty($extraParams['reference_video_assets'])) {
|
|
|
$apiParams['ratio'] = 'adaptive';
|
|
|
$apiParams['duration'] = -1;
|
|
|
}
|
|
|
@@ -1218,6 +1229,11 @@ class AIVideoGenerationService
|
|
|
'baidu-doubao-seedance-2-0-fast-260128' => 'doubao-seedance-2-0-fast-260128',
|
|
|
'baidu-doubao-seedance-2-0-mini-260615' => 'doubao-seedance-2-0-mini-260615',
|
|
|
'baidu-doubao-seedance-2-5-260628' => 'doubao-seedance-2-5-260628',
|
|
|
+ // 海外百度Seedance(dreamina)真人素材模型:系统内 baidu- 前缀,请求网关时去掉前缀
|
|
|
+ 'baidu-dreamina-seedance-2-0-260128' => 'dreamina-seedance-2-0-260128',
|
|
|
+ 'baidu-dreamina-seedance-2-0-fast-260128' => 'dreamina-seedance-2-0-fast-260128',
|
|
|
+ 'baidu-dreamina-seedance-2-0-mini-260615' => 'dreamina-seedance-2-0-mini-260615',
|
|
|
+ 'baidu-dreamina-seedance-2-5-260628' => 'dreamina-seedance-2-5-260628',
|
|
|
];
|
|
|
|
|
|
return $map[$model] ?? $model;
|
|
|
@@ -1904,6 +1920,621 @@ class AIVideoGenerationService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 将海外百度Seedance(dreamina)参考图上传为海外真人素材空间素材
|
|
|
+ * 失败处理与国内Seedance 2.0一致:删除参考图的同时清理提示词中的<图片N>标记
|
|
|
+ *
|
|
|
+ * @param array $referenceImages 参考图片URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败图片的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceImagesToBaiduDreaminaAssets(array $referenceImages, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ $assetIds = [];
|
|
|
+ $failedIndexes = []; // 记录失败的图片序号
|
|
|
+
|
|
|
+ foreach ($referenceImages as $index => $imageUrl) {
|
|
|
+ $imageIndex = $index + 1; // 图片序号从1开始
|
|
|
+
|
|
|
+ try {
|
|
|
+ dLog('generate')->info("开始处理海外百度Seedance参考图片 {$imageIndex}: {$imageUrl}");
|
|
|
+
|
|
|
+ // 上传素材到海外真人素材空间(已存在则直接复用,不重复上传)
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($imageUrl, "参考图片_{$imageIndex}_" . time());
|
|
|
+
|
|
|
+ // 网关素材异步处理为间歇性故障,失败后间隔重试,避免因网关临时故障误判
|
|
|
+ for ($retry = 1; $retry <= 5 && $createResult['code'] !== 0; $retry++) {
|
|
|
+ sleep($retry <= 3 ? 10 : 30);
|
|
|
+ dLog('generate')->warning("图片 {$imageIndex} 海外百度Seedance素材第 {$retry} 次重试上传");
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($imageUrl, "参考图片_{$imageIndex}_" . time());
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ $errorMessage = (string)($createResult['msg'] ?? '素材创建失败');
|
|
|
+ dLog('generate')->error("图片 {$imageIndex} 创建海外百度Seedance素材失败: " . $errorMessage);
|
|
|
+ logDB('generate', 'error', '海外百度Seedance创建素材失败', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'index' => $imageIndex,
|
|
|
+ 'error' => $errorMessage
|
|
|
+ ]);
|
|
|
+ // 素材不可用会直接影响视频效果,且任务会扣费,失败直接中断任务创建
|
|
|
+ Utils::throwError('20003:参考图片' . $imageIndex . '上传素材失败:' . $errorMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ $assetIds[] = $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info("图片 {$imageIndex} 海外百度Seedance素材上传成功,asset_id: " . $createResult['data']['asset_id']);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error("图片 {$imageIndex} 海外百度Seedance素材处理异常: " . $e->getMessage());
|
|
|
+ logDB('generate', 'error', '海外百度Seedance素材处理异常', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'index' => $imageIndex,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $imageIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理失败的图片:从 fullPrompt 中删除对应的标记并调整序号
|
|
|
+ if (!empty($failedIndexes)) {
|
|
|
+ dLog('generate')->info('开始处理海外百度Seedance失败图片的提示词调整,失败序号: ' . 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提示词图片序号调整完成');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info('海外百度Seedance参考图片转素材完成,成功数量: ' . count($assetIds) . ',失败数量: ' . count($failedIndexes));
|
|
|
+
|
|
|
+ // 格式化为可直接在API接口中使用的数组
|
|
|
+ foreach ($assetIds as &$assetId) {
|
|
|
+ $assetId = "asset://{$assetId}";
|
|
|
+ }
|
|
|
+ unset($assetId);
|
|
|
+
|
|
|
+ return $assetIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将海外百度Seedance(dreamina)参考视频上传为海外真人素材空间素材
|
|
|
+ *
|
|
|
+ * @param array $referenceVideos 参考视频URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败视频的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceVideosToBaiduDreaminaAssets(array $referenceVideos, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ return $this->processReferenceMediaToBaiduDreaminaAssets($referenceVideos, MpAsset::TYPE_VIDEO, '参考视频', '视频', $fullPrompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将海外百度Seedance(dreamina)参考音频上传为海外真人素材空间素材
|
|
|
+ *
|
|
|
+ * @param array $referenceAudios 参考音频URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败音频的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceAudiosToBaiduDreaminaAssets(array $referenceAudios, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ return $this->processReferenceMediaToBaiduDreaminaAssets($referenceAudios, MpAsset::TYPE_AUDIO, '参考音频', '音频', $fullPrompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将海外百度Seedance 参考视频/音频上传为海外真人素材空间素材(通用方法)
|
|
|
+ *
|
|
|
+ * @param array $urls 参考素材URL数组
|
|
|
+ * @param string $assetType 素材类型(Video/Audio)
|
|
|
+ * @param string $label 素材中文名(用于日志和素材命名)
|
|
|
+ * @param string $tagName 提示词标记名(视频/音频),用于 <{tagName}N> 标记的清理
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败素材的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ private function processReferenceMediaToBaiduDreaminaAssets(array $urls, string $assetType, string $label, string $tagName, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ $assetIds = [];
|
|
|
+ $failedIndexes = []; // 记录失败的素材序号
|
|
|
+
|
|
|
+ foreach ($urls as $index => $url) {
|
|
|
+ $mediaIndex = $index + 1; // 序号从1开始
|
|
|
+
|
|
|
+ try {
|
|
|
+ dLog('generate')->info("开始处理海外百度Seedance{$label} {$mediaIndex}: {$url}");
|
|
|
+
|
|
|
+ // 上传素材到海外真人素材空间(已存在则直接复用,不重复上传)
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($url, "{$label}_{$mediaIndex}_" . time(), $assetType);
|
|
|
+
|
|
|
+ // 网关素材异步处理为间歇性故障,失败后间隔重试,避免因网关临时故障误判
|
|
|
+ for ($retry = 1; $retry <= 5 && $createResult['code'] !== 0; $retry++) {
|
|
|
+ sleep($retry <= 3 ? 10 : 30);
|
|
|
+ dLog('generate')->warning("{$label} {$mediaIndex} 海外百度Seedance素材第 {$retry} 次重试上传");
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($url, "{$label}_{$mediaIndex}_" . time(), $assetType);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ $errorMessage = (string)($createResult['msg'] ?? '素材创建失败');
|
|
|
+ dLog('generate')->error("{$label} {$mediaIndex} 创建海外百度Seedance素材失败: " . $errorMessage);
|
|
|
+ logDB('generate', 'error', '海外百度Seedance创建素材失败', [
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
+ 'index' => $mediaIndex,
|
|
|
+ 'error' => $errorMessage
|
|
|
+ ]);
|
|
|
+ // 素材不可用会直接影响视频效果,且任务会扣费,失败直接中断任务创建
|
|
|
+ Utils::throwError('20003:' . $label . $mediaIndex . '上传素材失败:' . $errorMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ $assetIds[] = $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info("{$label} {$mediaIndex} 海外百度Seedance素材上传成功,asset_id: " . $createResult['data']['asset_id']);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error("{$label} {$mediaIndex} 海外百度Seedance素材处理异常: " . $e->getMessage());
|
|
|
+ logDB('generate', 'error', '海外百度Seedance素材处理异常', [
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
+ 'index' => $mediaIndex,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $mediaIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理失败的素材:从 fullPrompt 中删除对应的标记并调整序号(与参考图处理一致)
|
|
|
+ if (!empty($failedIndexes)) {
|
|
|
+ dLog('generate')->info("开始处理海外百度Seedance失败{$label}的提示词调整,失败序号: " . implode(',', $failedIndexes));
|
|
|
+
|
|
|
+ // 按序号从大到小排序,避免删除时影响后续序号
|
|
|
+ rsort($failedIndexes);
|
|
|
+
|
|
|
+ foreach ($failedIndexes as $failedIndex) {
|
|
|
+ // 删除失败素材的标记,例如 <视频1>、<音频2> 等
|
|
|
+ $pattern = "/<{$tagName}{$failedIndex}>/u";
|
|
|
+ $fullPrompt = preg_replace($pattern, '', $fullPrompt);
|
|
|
+ dLog('generate')->info("已从提示词中删除 <{$tagName}{$failedIndex}>");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新调整剩余素材的序号
|
|
|
+ $needAdjust = false;
|
|
|
+ foreach ($failedIndexes as $failedIndex) {
|
|
|
+ $needAdjust = true;
|
|
|
+ // 将所有大于失败序号的素材标记序号减1
|
|
|
+ for ($i = $failedIndex + 1; $i <= count($urls); $i++) {
|
|
|
+ $oldPattern = "/<{$tagName}{$i}>/u";
|
|
|
+ $newTag = "<{$tagName}" . ($i - 1) . ">";
|
|
|
+ $fullPrompt = preg_replace($oldPattern, $newTag, $fullPrompt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($needAdjust) {
|
|
|
+ dLog('generate')->info('海外百度Seedance提示词' . $label . '序号调整完成');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info("海外百度Seedance{$label}转素材完成,成功数量: " . count($assetIds) . ',失败数量: ' . count($failedIndexes));
|
|
|
+
|
|
|
+ // 格式化为可直接在API接口中使用的数组
|
|
|
+ foreach ($assetIds as &$assetId) {
|
|
|
+ $assetId = "asset://{$assetId}";
|
|
|
+ }
|
|
|
+ unset($assetId);
|
|
|
+
|
|
|
+ return $assetIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将海外百度Seedance 内容中的图片URL上传为海外真人素材空间素材
|
|
|
+ * 上传失败的图片直接从content中移除,不阻断整个任务
|
|
|
+ *
|
|
|
+ * @param array $content
|
|
|
+ * @return array 转换后的content(图片URL替换为 asset://{AssetId})
|
|
|
+ */
|
|
|
+ private function convertDreaminaContentImagesToAssets(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 = 'dreamina_' . time() . '_' . uniqid();
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($imageUrl, $name);
|
|
|
+
|
|
|
+ // 网关素材异步处理为间歇性故障,失败后间隔重试,避免因网关临时故障误判
|
|
|
+ for ($retry = 1; $retry <= 5 && $createResult['code'] !== 0; $retry++) {
|
|
|
+ sleep($retry <= 3 ? 10 : 30);
|
|
|
+ dLog('generate')->warning('海外百度Seedance 图片上传素材第 ' . $retry . ' 次重试: ' . $imageUrl);
|
|
|
+ $createResult = $this->createBaiduDreaminaAsset($imageUrl, $name);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ $errorMessage = (string)($createResult['msg'] ?? '素材创建失败');
|
|
|
+ dLog('generate')->error('海外百度Seedance 图片上传素材失败: ' . $imageUrl . ',原因: ' . $errorMessage);
|
|
|
+ logDB('generate', 'error', '海外百度Seedance图片转素材失败', [
|
|
|
+ 'image_url' => $imageUrl,
|
|
|
+ 'error' => $errorMessage
|
|
|
+ ]);
|
|
|
+ // 素材不可用会直接影响视频效果,且任务会扣费,失败直接中断任务创建
|
|
|
+ Utils::throwError('20003:图片上传素材失败:' . $errorMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ $item['image_url']['url'] = 'asset://' . $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info('海外百度Seedance 图片转素材成功: ' . $imageUrl . ' => asset://' . $createResult['data']['asset_id']);
|
|
|
+ }
|
|
|
+ unset($item);
|
|
|
+
|
|
|
+ return array_values($content);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传图片到海外百度AI网关真人素材空间并等待素材可用
|
|
|
+ * 上传前先检查mp_assets中是否已存在该url(source_type=baidu_dreamina),存在则直接复用素材ID,避免重复上传
|
|
|
+ *
|
|
|
+ * @param string $url 素材公网地址
|
|
|
+ * @param string $name 素材名称
|
|
|
+ * @param string $assetType 素材类型(Image/Video/Audio)
|
|
|
+ * @return array ['code'=>0, 'data'=>['asset_id'=>...]] 成功;['code'=>20003, 'msg'=>...] 失败
|
|
|
+ */
|
|
|
+ private function createBaiduDreaminaAsset(string $url, string $name, string $assetType = MpAsset::TYPE_IMAGE): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ if (empty($url)) {
|
|
|
+ return [
|
|
|
+ 'code' => 1,
|
|
|
+ 'msg' => '素材URL不能为空',
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先获取表中是否已有该url(仅限海外百度网关来源且状态可用的素材)
|
|
|
+ // 只复用海外百度网关上传成功的素材,避免复用 Failed 记录或其它网关(ark/kuaikuai/国内baidu)的素材 ID
|
|
|
+ $asset = DB::table('mp_assets')
|
|
|
+ ->where('url', $url)
|
|
|
+ ->where('source_type', 'baidu_dreamina')
|
|
|
+ ->where('status', MpAsset::STATUS_ACTIVE)
|
|
|
+ ->orderBy('id', 'desc')
|
|
|
+ ->first();
|
|
|
+ if ($asset) {
|
|
|
+ $extraParams = is_array($asset->extra_params) ? $asset->extra_params : json_decode((string)$asset->extra_params, true);
|
|
|
+ $isBaiduGatewayAsset = is_array($extraParams) && isset($extraParams['Result']);
|
|
|
+ if ($isBaiduGatewayAsset) {
|
|
|
+ dLog('generate')->info('海外百度Seedance 素材已存在,直接复用: ' . $url . ' => ' . $asset->asset_id);
|
|
|
+ return [
|
|
|
+ 'code' => 0,
|
|
|
+ 'msg' => '创建成功',
|
|
|
+ 'data' => [
|
|
|
+ 'asset_id' => $asset->asset_id,
|
|
|
+ 'group_id' => $asset->group_id,
|
|
|
+ 'status' => 'ready',
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $groupId = $this->ensureBaiduDreaminaAssetGroup();
|
|
|
+
|
|
|
+ $createAssetBody = [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ 'GroupId' => $groupId,
|
|
|
+ 'AssetType' => $assetType,
|
|
|
+ 'Name' => $name,
|
|
|
+ 'URL' => $url,
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ $responseData = $this->requestBaiduDreaminaGateway('CreateAsset', $createAssetBody);
|
|
|
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
|
+ $errorBody = $e->hasResponse() ? (string)$e->getResponse()->getBody() : '';
|
|
|
+ // 素材组空间失效/不存在(资产空间迁移或素材组被清理),创建新素材组并重试一次
|
|
|
+ $groupInvalid = strpos($errorBody, 'asset_space_mismatch') !== false
|
|
|
+ || strpos($errorBody, 'NotFound.group_id') !== false
|
|
|
+ || (stripos($errorBody, 'not found') !== false && stripos($errorBody, 'asset_group') !== false);
|
|
|
+ if (!$groupInvalid) {
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->warning('海外百度Seedance 素材组空间失效或不存在,创建新素材组重试: ' . $groupId);
|
|
|
+ logDB('generate', 'warning', '海外百度Seedance素材组空间失效或不存在', [
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ 'error' => $errorBody
|
|
|
+ ]);
|
|
|
+ $groupId = $this->createBaiduDreaminaAssetGroup();
|
|
|
+ $createAssetBody['GroupId'] = $groupId;
|
|
|
+ $responseData = $this->requestBaiduDreaminaGateway('CreateAsset', $createAssetBody);
|
|
|
+ }
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $assetId = $result['Id'] ?? $result['AssetId'] ?? '';
|
|
|
+ if (empty($assetId)) {
|
|
|
+ logDB('generate', 'error', '海外百度Seedance创建素材失败', ['url' => $url, 'response' => $responseData]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '创建海外百度Seedance素材失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存素材记录到数据库(海外百度素材类型:baidu_dreamina)
|
|
|
+ MpAsset::create([
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ 'group_name' => 'default',
|
|
|
+ 'name' => $name,
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
+ 'source_type' => 'baidu_dreamina',
|
|
|
+ 'status' => MpAsset::STATUS_PROCESSING,
|
|
|
+ 'extra_params' => $responseData,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 轮询查询素材状态(每3秒一次,最多100次即5分钟)
|
|
|
+ $maxAttempts = 100;
|
|
|
+ $assetStatus = null;
|
|
|
+ for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
|
|
+ sleep(3);
|
|
|
+
|
|
|
+ $assetInfo = $this->getBaiduDreaminaAsset($assetId);
|
|
|
+ $assetStatus = $assetInfo['Status'] ?? '';
|
|
|
+ dLog('generate')->info("海外百度Seedance 素材 {$assetId} 状态查询第 {$attempt} 次,状态: {$assetStatus}");
|
|
|
+
|
|
|
+ if ($assetStatus === 'Active') {
|
|
|
+ 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', '海外百度Seedance素材处理失败', [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'url' => $url,
|
|
|
+ 'error' => $errorMessage
|
|
|
+ ]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '海外百度Seedance素材处理失败: ' . $errorMessage,
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logDB('generate', 'error', '海外百度Seedance素材处理超时', [
|
|
|
+ 'asset_id' => $assetId,
|
|
|
+ 'url' => $url,
|
|
|
+ 'last_status' => $assetStatus
|
|
|
+ ]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '海外百度Seedance素材处理超时: ' . $assetId . ',最后状态: ' . ($assetStatus ?? 'unknown'),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error('海外百度Seedance创建素材异常: ' . $e->getMessage());
|
|
|
+ logDB('generate', 'error', '海外百度Seedance创建素材异常', ['url' => $url, 'error' => $e->getMessage()]);
|
|
|
+ return [
|
|
|
+ 'code' => 20003,
|
|
|
+ 'msg' => '创建海外百度Seedance素材失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询海外百度AI网关素材状态(优先查本地mp_assets,处理中/缺失时请求网关并同步状态)
|
|
|
+ *
|
|
|
+ * @param string $assetId
|
|
|
+ * @return array 素材信息(含Status字段)
|
|
|
+ */
|
|
|
+ private function getBaiduDreaminaAsset(string $assetId): array
|
|
|
+ {
|
|
|
+ // 先查本地素材表(仅限海外百度网关来源的素材)
|
|
|
+ $assetInfo = DB::table('mp_assets')->where('asset_id', $assetId)->where('source_type', 'baidu_dreamina')->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->requestBaiduDreaminaGateway('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_dreamina')
|
|
|
+ ->update($updateData);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可用的海外百度Seedance素材组ID
|
|
|
+ * 优先复用本地已有素材组,避免依赖网关ListAssetGroups接口(该接口近期返回400)
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ private function ensureBaiduDreaminaAssetGroup(): string
|
|
|
+ {
|
|
|
+ // 优先复用本地已有的海外百度素材组
|
|
|
+ $localGroup = DB::table('mp_assets')
|
|
|
+ ->where('source_type', 'baidu_dreamina')
|
|
|
+ ->whereNotNull('group_id')
|
|
|
+ ->where('group_id', '!=', '')
|
|
|
+ ->where('asset_id', 'not like', 'group-%')
|
|
|
+ ->orderBy('id', 'desc')
|
|
|
+ ->select('group_id')
|
|
|
+ ->first();
|
|
|
+ if ($localGroup && !empty($localGroup->group_id)) {
|
|
|
+ dLog('generate')->info('海外百度Seedance 复用本地素材组: ' . $localGroup->group_id);
|
|
|
+ return $localGroup->group_id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 本地无记录时再查询网关素材组
|
|
|
+ try {
|
|
|
+ $groups = $this->listBaiduDreaminaAssetGroups();
|
|
|
+ if (!empty($groups)) {
|
|
|
+ $groupId = $groups[0]['Id'] ?? $groups[0]['GroupId'] ?? '';
|
|
|
+ if (!empty($groupId)) {
|
|
|
+ return $groupId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->warning('海外百度Seedance ListAssetGroups查询失败,尝试直接创建素材组: ' . $e->getMessage());
|
|
|
+ logDB('generate', 'warning', '海外百度Seedance ListAssetGroups失败', ['error' => $e->getMessage()]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->createBaiduDreaminaAssetGroup();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询海外百度AI网关素材组列表
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ private function listBaiduDreaminaAssetGroups(): array
|
|
|
+ {
|
|
|
+ $responseData = $this->requestBaiduDreaminaGateway('ListAssetGroups', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $groups = $result['AssetGroups'] ?? $result['Groups'] ?? $result['Items'] ?? [];
|
|
|
+
|
|
|
+ return is_array($groups) ? $groups : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建海外百度Seedance默认素材组
|
|
|
+ *
|
|
|
+ * @return string 素材组ID
|
|
|
+ */
|
|
|
+ private function createBaiduDreaminaAssetGroup(): string
|
|
|
+ {
|
|
|
+ $responseData = $this->requestBaiduDreaminaGateway('CreateAssetGroup', [
|
|
|
+ 'ProjectName' => 'default',
|
|
|
+ 'Name' => 'default',
|
|
|
+ 'Description' => '海外百度Seedance 默认素材组',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $result = $responseData['Result'] ?? [];
|
|
|
+ $groupId = $result['GroupId'] ?? $result['Id'] ?? '';
|
|
|
+ if (empty($groupId)) {
|
|
|
+ throw new \Exception('创建海外百度Seedance素材组失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 持久化素材组记录(占位),避免后续每次都调用网关查询素材组
|
|
|
+ $groupRecordExists = DB::table('mp_assets')
|
|
|
+ ->where('group_id', $groupId)
|
|
|
+ ->where('source_type', 'baidu_dreamina')
|
|
|
+ ->where('asset_id', 'like', 'group-%')
|
|
|
+ ->exists();
|
|
|
+ if (!$groupRecordExists) {
|
|
|
+ MpAsset::create([
|
|
|
+ 'asset_id' => 'group-' . $groupId,
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ 'group_name' => 'default',
|
|
|
+ 'name' => $result['Name'] ?? 'default',
|
|
|
+ 'url' => 'baidu_dreamina_group://' . $groupId,
|
|
|
+ 'asset_type' => MpAsset::TYPE_IMAGE,
|
|
|
+ 'source_type' => 'baidu_dreamina',
|
|
|
+ 'status' => MpAsset::STATUS_ACTIVE,
|
|
|
+ 'extra_params' => $responseData,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $groupId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用海外百度AI网关真人素材接口(dreamina真人资产空间)
|
|
|
+ * 与国内百度素材库的区别:使用独立AK,且真人素材相关接口需携带 target-asset-space 请求头
|
|
|
+ *
|
|
|
+ * @param string $action 接口Action(CreateAssetGroup/CreateAsset/GetAsset等)
|
|
|
+ * @param array $body 请求体
|
|
|
+ * @return array 响应数组
|
|
|
+ */
|
|
|
+ private function requestBaiduDreaminaGateway(string $action, array $body): array
|
|
|
+ {
|
|
|
+ $apiKey = env('BAIDU_DREAMINA_AK');
|
|
|
+ if (empty($apiKey)) {
|
|
|
+ throw new \Exception('海外百度Seedance API KEY未配置,请检查环境变量BAIDU_DREAMINA_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',
|
|
|
+ 'target-asset-space' => 'dreamina-seedance-livenessface',
|
|
|
+ ],
|
|
|
+ 'json' => $body,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $responseData = json_decode($response->getBody(), true);
|
|
|
+ dLog('generate')->info('海外百度Seedance 网关 ' . $action . ' 响应: ', $responseData);
|
|
|
+
|
|
|
+ return $responseData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 调用快快AI聚合网关素材接口(真人素材库)
|
|
|
* 文档:https://ai.kkidc.com/api-docs/9207319m0
|
|
|
*
|