|
|
@@ -579,6 +579,7 @@ class AIVideoGenerationService
|
|
|
'alias_segment_id' => $params['alias_segment_id'] ?? '',
|
|
|
'alias_act_id' => $params['alias_act_id'] ?? '',
|
|
|
'prompt' => $params['prompt'] ?? '',
|
|
|
+ 'ref_image_url' => $params['ref_image_url'] ?? json_encode([], 256),
|
|
|
'first_frame_url' => $params['first_frame_url'] ?? '',
|
|
|
'tail_frame_url' => $params['tail_frame_url'] ?? '',
|
|
|
'seed' => $params['seed'] ?? -1,
|
|
|
@@ -627,6 +628,7 @@ class AIVideoGenerationService
|
|
|
'alias_segment_id' => $params['alias_segment_id'] ?? '',
|
|
|
'alias_act_id' => $params['alias_act_id'] ?? '',
|
|
|
'prompt' => $params['prompt'] ?? '',
|
|
|
+ 'ref_image_url' => $params['ref_image_url'] ?? json_encode([], 256),
|
|
|
'first_frame_url' => $params['first_frame_url'] ?? '',
|
|
|
'tail_frame_url' => $params['tail_frame_url'] ?? '',
|
|
|
'seed' => $params['seed'] ?? -1,
|
|
|
@@ -639,6 +641,8 @@ class AIVideoGenerationService
|
|
|
'content' => $params['content'],
|
|
|
'reference_images' => $params['reference_images'] ?? [],
|
|
|
'reference_image_assets' => $params['reference_image_assets'] ?? [],
|
|
|
+ 'reference_video_assets' => $params['reference_video_assets'] ?? [],
|
|
|
+ 'reference_audio_assets' => $params['reference_audio_assets'] ?? [],
|
|
|
'callback_url' => $params['callback_url'] ?? '',
|
|
|
'return_last_frame' => $params['return_last_frame'] ?? true,
|
|
|
'execution_expires_after' => $params['execution_expires_after'] ?? 172800,
|
|
|
@@ -710,6 +714,38 @@ class AIVideoGenerationService
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 追加参考视频素材(已由processReferenceVideosToSeedance20Assets上传)
|
|
|
+ if (!empty($extraParams['gateway_base_url']) && !empty($extraParams['reference_video_assets'])) {
|
|
|
+ foreach ($extraParams['reference_video_assets'] as $assetUrl) {
|
|
|
+ if (empty($assetUrl)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'video_url',
|
|
|
+ 'video_url' => [
|
|
|
+ 'url' => $assetUrl,
|
|
|
+ ],
|
|
|
+ 'role' => 'reference_video',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 追加参考音频素材(已由processReferenceAudiosToSeedance20Assets上传)
|
|
|
+ if (!empty($extraParams['gateway_base_url']) && !empty($extraParams['reference_audio_assets'])) {
|
|
|
+ foreach ($extraParams['reference_audio_assets'] as $assetUrl) {
|
|
|
+ if (empty($assetUrl)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $content[] = [
|
|
|
+ 'type' => 'audio_url',
|
|
|
+ 'audio_url' => [
|
|
|
+ 'url' => $assetUrl,
|
|
|
+ ],
|
|
|
+ 'role' => 'reference_audio',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 构建火山引擎 Seedance API 请求参数
|
|
|
$apiParams = [
|
|
|
@@ -806,7 +842,7 @@ class AIVideoGenerationService
|
|
|
|
|
|
/**
|
|
|
* 将 Seedance 2.0 参考图上传为百度AI网关素材
|
|
|
- * 失败处理与智真AI的processReferenceImagesToAssets一致:删除参考图的同时清理提示词中的<图片N>标记
|
|
|
+ * 失败处理与智帧AI的processReferenceImagesToAssets一致:删除参考图的同时清理提示词中的<图片N>标记
|
|
|
*
|
|
|
* @param array $referenceImages 参考图片URL数组
|
|
|
* @param string &$fullPrompt 完整提示词(引用传递,用于删除失败图片的标记)
|
|
|
@@ -892,6 +928,130 @@ class AIVideoGenerationService
|
|
|
foreach ($assetIds as &$assetId) {
|
|
|
$assetId = "asset://{$assetId}";
|
|
|
}
|
|
|
+ unset($assetId);
|
|
|
+
|
|
|
+ return $assetIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 Seedance 2.0 参考视频上传为百度AI网关素材
|
|
|
+ * 失败处理与参考图一致:删除参考视频的同时清理提示词中的<视频N>标记
|
|
|
+ *
|
|
|
+ * @param array $referenceVideos 参考视频URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败视频的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceVideosToSeedance20Assets(array $referenceVideos, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ return $this->processReferenceMediaToSeedance20Assets($referenceVideos, MpAsset::TYPE_VIDEO, '参考视频', '视频', $fullPrompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 Seedance 2.0 参考音频上传为百度AI网关素材
|
|
|
+ * 失败处理与参考图一致:删除参考音频的同时清理提示词中的<音频N>标记
|
|
|
+ *
|
|
|
+ * @param array $referenceAudios 参考音频URL数组
|
|
|
+ * @param string &$fullPrompt 完整提示词(引用传递,用于删除失败音频的标记)
|
|
|
+ * @return array 返回素材ID数组(asset://格式)
|
|
|
+ */
|
|
|
+ public function processReferenceAudiosToSeedance20Assets(array $referenceAudios, string &$fullPrompt): array
|
|
|
+ {
|
|
|
+ return $this->processReferenceMediaToSeedance20Assets($referenceAudios, MpAsset::TYPE_AUDIO, '参考音频', '音频', $fullPrompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 Seedance 2.0 参考视频/音频上传为百度AI网关素材(通用方法)
|
|
|
+ *
|
|
|
+ * @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 processReferenceMediaToSeedance20Assets(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 2.0{$label} {$mediaIndex}: {$url}");
|
|
|
+
|
|
|
+ // 上传素材(已存在则直接复用,不重复上传)
|
|
|
+ $createResult = $this->createSeedance20Asset($url, "{$label}_{$mediaIndex}_" . time(), $assetType);
|
|
|
+
|
|
|
+ if ($createResult['code'] !== 0) {
|
|
|
+ dLog('generate')->warning("{$label} {$mediaIndex} 创建Seedance 2.0素材失败: " . $createResult['msg']);
|
|
|
+ logDB('generate', 'warning', 'Seedance2.0创建素材失败', [
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
+ 'index' => $mediaIndex,
|
|
|
+ 'error' => $createResult['msg']
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $mediaIndex;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $assetIds[] = $createResult['data']['asset_id'];
|
|
|
+ dLog('generate')->info("{$label} {$mediaIndex} Seedance 2.0素材上传成功,asset_id: " . $createResult['data']['asset_id']);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->error("{$label} {$mediaIndex} Seedance 2.0素材处理异常: " . $e->getMessage());
|
|
|
+ logDB('generate', 'error', 'Seedance2.0素材处理异常', [
|
|
|
+ 'url' => $url,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
+ 'index' => $mediaIndex,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ $failedIndexes[] = $mediaIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理失败的素材:从 fullPrompt 中删除对应的标记并调整序号(与参考图处理一致)
|
|
|
+ if (!empty($failedIndexes)) {
|
|
|
+ dLog('generate')->info("开始处理Seedance 2.0失败{$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 2.0提示词' . $label . '序号调整完成');
|
|
|
+ logDB('generate', 'info', 'Seedance2.0提示词调整', [
|
|
|
+ 'failed_indexes' => $failedIndexes,
|
|
|
+ 'adjusted_prompt' => $fullPrompt
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ dLog('generate')->info("Seedance 2.0{$label}转素材完成,成功数量: " . count($assetIds) . ',失败数量: ' . count($failedIndexes));
|
|
|
+
|
|
|
+ // 格式化为可直接在API接口中使用的数组
|
|
|
+ foreach ($assetIds as &$assetId) {
|
|
|
+ $assetId = "asset://{$assetId}";
|
|
|
+ }
|
|
|
+ unset($assetId);
|
|
|
|
|
|
return $assetIds;
|
|
|
}
|
|
|
@@ -941,11 +1101,12 @@ class AIVideoGenerationService
|
|
|
* 上传图片到百度AI网关素材库并等待素材可用
|
|
|
* 上传前先检查mp_assets中是否已存在该url(source_type=baidu),存在则直接复用素材ID,避免重复上传
|
|
|
*
|
|
|
- * @param string $url 图片公网地址
|
|
|
+ * @param string $url 素材公网地址
|
|
|
* @param string $name 素材名称
|
|
|
+ * @param string $assetType 素材类型(Image/Video/Audio)
|
|
|
* @return array ['code'=>0, 'data'=>['asset_id'=>...]] 成功;['code'=>20003, 'msg'=>...] 失败
|
|
|
*/
|
|
|
- private function createSeedance20Asset(string $url, string $name): array
|
|
|
+ private function createSeedance20Asset(string $url, string $name, string $assetType = MpAsset::TYPE_IMAGE): array
|
|
|
{
|
|
|
try {
|
|
|
if (empty($url)) {
|
|
|
@@ -972,13 +1133,32 @@ class AIVideoGenerationService
|
|
|
|
|
|
$groupId = $this->ensureSeedance20AssetGroup();
|
|
|
|
|
|
- $responseData = $this->requestSeedance20Gateway('CreateAsset', [
|
|
|
+ $createAssetBody = [
|
|
|
'ProjectName' => 'default',
|
|
|
'GroupId' => $groupId,
|
|
|
- 'AssetType' => 'Image',
|
|
|
+ 'AssetType' => $assetType,
|
|
|
'Name' => $name,
|
|
|
'URL' => $url,
|
|
|
- ]);
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ $responseData = $this->requestSeedance20Gateway('CreateAsset', $createAssetBody);
|
|
|
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
|
+ $errorBody = $e->hasResponse() ? (string)$e->getResponse()->getBody() : '';
|
|
|
+ if (strpos($errorBody, 'asset_space_mismatch') === false) {
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 素材组空间失效(网关资产空间迁移),创建新素材组并重试一次
|
|
|
+ dLog('generate')->warning('Seedance 2.0 素材组空间失效,创建新素材组重试: ' . $groupId);
|
|
|
+ logDB('generate', 'warning', 'Seedance2.0素材组空间失效', [
|
|
|
+ 'group_id' => $groupId,
|
|
|
+ 'error' => $errorBody
|
|
|
+ ]);
|
|
|
+ $groupId = $this->createSeedance20AssetGroup();
|
|
|
+ $createAssetBody['GroupId'] = $groupId;
|
|
|
+ $responseData = $this->requestSeedance20Gateway('CreateAsset', $createAssetBody);
|
|
|
+ }
|
|
|
|
|
|
$result = $responseData['Result'] ?? [];
|
|
|
$assetId = $result['Id'] ?? $result['AssetId'] ?? '';
|
|
|
@@ -998,7 +1178,7 @@ class AIVideoGenerationService
|
|
|
'group_name' => 'default',
|
|
|
'name' => $name,
|
|
|
'url' => $url,
|
|
|
- 'asset_type' => MpAsset::TYPE_IMAGE,
|
|
|
+ 'asset_type' => $assetType,
|
|
|
'source_type' => 'baidu',
|
|
|
'status' => MpAsset::STATUS_PROCESSING,
|
|
|
'extra_params' => $responseData,
|
|
|
@@ -1124,18 +1304,38 @@ class AIVideoGenerationService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取可用的Seedance 2.0素材组ID(不存在则创建默认素材组)
|
|
|
+ * 获取可用的Seedance 2.0素材组ID
|
|
|
+ * 优先复用本地已有素材组,避免依赖网关ListAssetGroups接口(该接口近期返回400)
|
|
|
*
|
|
|
* @return string
|
|
|
*/
|
|
|
private function ensureSeedance20AssetGroup(): string
|
|
|
{
|
|
|
- $groups = $this->listSeedance20AssetGroups();
|
|
|
- if (!empty($groups)) {
|
|
|
- $groupId = $groups[0]['Id'] ?? $groups[0]['GroupId'] ?? '';
|
|
|
- if (!empty($groupId)) {
|
|
|
- return $groupId;
|
|
|
+ // 优先复用本地已有的百度素材组
|
|
|
+ $localGroup = DB::table('mp_assets')
|
|
|
+ ->where('source_type', 'baidu')
|
|
|
+ ->whereNotNull('group_id')
|
|
|
+ ->where('group_id', '!=', '')
|
|
|
+ ->orderBy('id', 'desc')
|
|
|
+ ->select('group_id')
|
|
|
+ ->first();
|
|
|
+ if ($localGroup && !empty($localGroup->group_id)) {
|
|
|
+ dLog('generate')->info('Seedance 2.0 复用本地素材组: ' . $localGroup->group_id);
|
|
|
+ return $localGroup->group_id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 本地无记录时再查询网关素材组
|
|
|
+ try {
|
|
|
+ $groups = $this->listSeedance20AssetGroups();
|
|
|
+ if (!empty($groups)) {
|
|
|
+ $groupId = $groups[0]['Id'] ?? $groups[0]['GroupId'] ?? '';
|
|
|
+ if (!empty($groupId)) {
|
|
|
+ return $groupId;
|
|
|
+ }
|
|
|
}
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('generate')->warning('Seedance 2.0 ListAssetGroups查询失败,尝试直接创建素材组: ' . $e->getMessage());
|
|
|
+ logDB('generate', 'warning', 'Seedance2.0 ListAssetGroups失败', ['error' => $e->getMessage()]);
|
|
|
}
|
|
|
|
|
|
return $this->createSeedance20AssetGroup();
|
|
|
@@ -1177,6 +1377,26 @@ class AIVideoGenerationService
|
|
|
throw new \Exception('创建Seedance 2.0素材组失败: ' . json_encode($responseData, JSON_UNESCAPED_UNICODE));
|
|
|
}
|
|
|
|
|
|
+ // 持久化素材组记录(占位),避免后续每次都调用网关查询素材组
|
|
|
+ $groupRecordExists = DB::table('mp_assets')
|
|
|
+ ->where('group_id', $groupId)
|
|
|
+ ->where('source_type', 'baidu')
|
|
|
+ ->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_group://' . $groupId,
|
|
|
+ 'asset_type' => MpAsset::TYPE_IMAGE,
|
|
|
+ 'source_type' => 'baidu',
|
|
|
+ 'status' => MpAsset::STATUS_ACTIVE,
|
|
|
+ 'extra_params' => $responseData,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
logDB('generate', 'info', 'Seedance2.0素材组创建成功', ['group_id' => $groupId]);
|
|
|
return $groupId;
|
|
|
}
|