|
|
@@ -5103,11 +5103,27 @@ class AnimeService
|
|
|
$audioPlan = $supportAudioReference
|
|
|
? $this->resolveActReferenceAudios($actContent, $products, $model)
|
|
|
: ['allowed' => [], 'excluded' => [], 'total_duration' => 0];
|
|
|
- $processResult = $this->processActContentWithProducts($actContent, $products, $supportAudioReference, $audioPlan['allowed']);
|
|
|
+ // 资产参考图同样先按模型上限(2.0=9张、2.5=30张)截断,保证提示词中的<图片N>与实际素材一一对应
|
|
|
+ $imagePlan = $this->resolveActReferenceImages($actContent, $products, $model);
|
|
|
+ $processResult = $this->processActContentWithProducts($actContent, $products, $supportAudioReference, $audioPlan['allowed'], $imagePlan['allowed']);
|
|
|
$fullPrompt = $processResult['content'];
|
|
|
// if ($art_style_type == '3D真人风格') $fullPrompt = "美术风格: $art_style\n\n".$fullPrompt;
|
|
|
- // 资产参考图与调用方传入的参考图合并后统一去重(保留资产参考图在前,不影响提示词中的<图片N>编号)
|
|
|
- $reference_images = array_values(array_unique(array_filter(array_merge($processResult['reference_images'], $reference_images))));
|
|
|
+
|
|
|
+ // 参考图合并:资产参考图(已按模型上限截断)优先,调用方显式传入的参考图按剩余额度补充,超出时优先截断调用方传入的图
|
|
|
+ $asset_reference_images = $processResult['reference_images'];
|
|
|
+ $frontend_reference_images_all = array_values(array_unique(array_filter($reference_images)));
|
|
|
+ $frontend_reference_images = $frontend_reference_images_all;
|
|
|
+ if (!empty($imagePlan['limit'])) {
|
|
|
+ $remaining = max(0, $imagePlan['limit'] - count($asset_reference_images));
|
|
|
+ $frontend_reference_images = array_slice($frontend_reference_images, 0, $remaining);
|
|
|
+ }
|
|
|
+ $reference_images = array_values(array_unique(array_filter(array_merge($asset_reference_images, $frontend_reference_images))));
|
|
|
+
|
|
|
+ // 任务记录用:截断前的完整合并数组(正文内出现的资产图 + 调用方传入的参考图),便于追溯前端入参
|
|
|
+ $reference_images_all = array_values(array_unique(array_filter(array_merge(
|
|
|
+ $processResult['reference_images_all'] ?? $asset_reference_images,
|
|
|
+ $frontend_reference_images_all
|
|
|
+ ))));
|
|
|
// 角色音色参考音频(顺序与提示词中的<音频N>标记一一对应)
|
|
|
$reference_audios = $supportAudioReference ? ($processResult['reference_audios'] ?? []) : [];
|
|
|
|
|
|
@@ -5264,10 +5280,15 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
if ($reference_images) {
|
|
|
- // 限制只传入9张参考图
|
|
|
- $reference_images = array_slice($reference_images, 0, 9);
|
|
|
+ // 参考图按模型上限截断(Seedance 2.0 系列=9张,2.5=30张)
|
|
|
+ $imageLimit = $this->getActReferenceImageLimit($model);
|
|
|
+ if ($imageLimit > 0) {
|
|
|
+ $reference_images = array_slice($reference_images, 0, $imageLimit);
|
|
|
+ }
|
|
|
|
|
|
- // 在处理之前先保存原始reference_images到任务参数中
|
|
|
+ // 任务记录用:保存截断前的完整参考图(含调用方传入),便于追溯前端入参
|
|
|
+ $unifiedParams['reference_images'] = $reference_images_all;
|
|
|
+ // 实际提交给大模型的参考图(截断后)
|
|
|
$unifiedParams['ref_image_url'] = json_encode($reference_images, 256);
|
|
|
$reference_images = $this->aiImageGenerationService->processReferenceImagesToAssets($reference_images, $unifiedParams['prompt']);
|
|
|
$parameters['reference_images'] = $reference_images;
|
|
|
@@ -5295,12 +5316,15 @@ class AnimeService
|
|
|
// 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片需要先上传为素材
|
|
|
$isKuaikuai = $this->isKuaikuaiSeedanceModel($model);
|
|
|
$isDreamina = $this->isOverseasBaiduSeedanceModel($model);
|
|
|
- // 保存原始参考图用于任务记录
|
|
|
- $videoParams['reference_images'] = $reference_images;
|
|
|
+ // 任务记录用:保存截断前的完整合并数组(正文内资产图 + 调用方传入的参考图),便于追溯前端入参
|
|
|
+ $videoParams['reference_images'] = $reference_images_all;
|
|
|
|
|
|
if ($reference_images) {
|
|
|
- // 限制只传入9张参考图
|
|
|
- $reference_images = array_slice($reference_images, 0, 9);
|
|
|
+ // 参考图按模型上限截断(Seedance 2.0 系列=9张,2.5=30张)
|
|
|
+ $imageLimit = $this->getActReferenceImageLimit($model);
|
|
|
+ if ($imageLimit > 0) {
|
|
|
+ $reference_images = array_slice($reference_images, 0, $imageLimit);
|
|
|
+ }
|
|
|
|
|
|
// 维护ref_image_url字段(参考图URL,JSON数组,与智帧分支保持一致)
|
|
|
$videoParams['ref_image_url'] = json_encode($reference_images, 256);
|
|
|
@@ -11314,11 +11338,12 @@ class AnimeService
|
|
|
* @param bool $supportAudioReference 模型是否支持参考音频;支持时角色音色优先使用 timbre_url
|
|
|
* @return array
|
|
|
*/
|
|
|
- public function processActContentWithProducts($actContent, $products, $supportAudioReference = false, $allowedAudioProducts = null) {
|
|
|
+ public function processActContentWithProducts($actContent, $products, $supportAudioReference = false, $allowedAudioProducts = null, $allowedImageProducts = null) {
|
|
|
if (empty($actContent) || empty($products)) {
|
|
|
return [
|
|
|
'content' => $actContent,
|
|
|
'reference_images' => [],
|
|
|
+ 'reference_images_all' => [],
|
|
|
'reference_audios' => [],
|
|
|
'audio_voice_fallbacks' => []
|
|
|
];
|
|
|
@@ -11344,6 +11369,7 @@ class AnimeService
|
|
|
preg_match_all('/\{([^}]+)\}/u', $actContent, $matches);
|
|
|
|
|
|
$reference_images = [];
|
|
|
+ $reference_images_all = []; // 正文中出现且有图的资产图(不受模型上限影响,用于任务记录)
|
|
|
$product_index_map = []; // 产品名称 => 图片序号的映射
|
|
|
$reference_audios = [];
|
|
|
$product_audio_index_map = []; // 产品名称 => 音频序号的映射(与图片序号互相独立)
|
|
|
@@ -11398,10 +11424,23 @@ class AnimeService
|
|
|
$voice_prompts[] = $product_name . ':' . $voice_prompt . "\n";
|
|
|
}
|
|
|
|
|
|
- // 只有URL非空才分配序号和添加到参考图片
|
|
|
+ // 只有URL非空且未被模型参考图上限截断,才分配序号并加入参考图片
|
|
|
if (!empty($url)) {
|
|
|
- // 检查URL是否已经在reference_images中(去重)
|
|
|
- if (!in_array($url, $reference_images)) {
|
|
|
+ // 任务记录用:正文中出现且有图的资产图全部记录(不受模型上限影响)
|
|
|
+ if (!in_array($url, $reference_images_all)) {
|
|
|
+ $reference_images_all[] = $url;
|
|
|
+ }
|
|
|
+
|
|
|
+ $image_allowed = true;
|
|
|
+ if (is_array($allowedImageProducts)) {
|
|
|
+ $image_allowed = isset($allowedImageProducts[$product_name])
|
|
|
+ && $allowedImageProducts[$product_name] === $url;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$image_allowed) {
|
|
|
+ // 超出模型参考图上限的资产不分配序号,提示词中不会出现<图片N>
|
|
|
+ $product_index_map[$product_name] = 0;
|
|
|
+ } elseif (!in_array($url, $reference_images)) {
|
|
|
$reference_images[] = $url;
|
|
|
$product_index_map[$product_name] = $current_index;
|
|
|
$current_index++;
|
|
|
@@ -11480,6 +11519,7 @@ class AnimeService
|
|
|
return [
|
|
|
'content' => $processedContent,
|
|
|
'reference_images' => $reference_images,
|
|
|
+ 'reference_images_all' => $reference_images_all,
|
|
|
'reference_audios' => $reference_audios,
|
|
|
'audio_voice_fallbacks' => $audio_voice_fallbacks
|
|
|
];
|
|
|
@@ -11632,6 +11672,102 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 获取模型对参考图片的数量上限
|
|
|
+ * Seedance 2.0(含 2.0 mini/fast)与智帧:9 张;Seedance 2.5:30 张
|
|
|
+ *
|
|
|
+ * @param string $model
|
|
|
+ * @return int 0 表示该模型不走参考图逻辑(不做限制)
|
|
|
+ */
|
|
|
+ public function getActReferenceImageLimit($model) {
|
|
|
+ if (!$this->isActReferenceAudioSupported($model)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (strpos($model, '2.5') !== false || strpos($model, '2-5') !== false) {
|
|
|
+ return 30;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 9;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成提示词之前解析可用的资产参考图
|
|
|
+ * 按正文中{资产名}首次出现的顺序取有图片的资产(相同图片URL复用同一编号);
|
|
|
+ * 超过模型上限的资产不分配图片编号,提示词中也不会出现<图片N>引用
|
|
|
+ * 调用方显式传入的 reference_images 不占用这里的额度,由调用方按剩余额度补充
|
|
|
+ *
|
|
|
+ * @param string $actContent 片段内容
|
|
|
+ * @param array $products 资产数组
|
|
|
+ * @param string $model 视频模型
|
|
|
+ * @return array ['allowed' => [产品名 => 图片URL], 'excluded' => [产品名 => 原因], 'limit' => 上限]
|
|
|
+ */
|
|
|
+ public function resolveActReferenceImages($actContent, $products, $model) {
|
|
|
+ $result = ['allowed' => [], 'excluded' => [], 'limit' => 0];
|
|
|
+
|
|
|
+ $limit = $this->getActReferenceImageLimit($model);
|
|
|
+ if ($limit <= 0 || empty($actContent) || empty($products)) {
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+ $result['limit'] = $limit;
|
|
|
+
|
|
|
+ // 产品名 => 图片(优先三视图,其次原图)
|
|
|
+ $imageMap = [];
|
|
|
+ foreach ($products as $product) {
|
|
|
+ $name = getProp($product, 'product_name');
|
|
|
+ if (!$name) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $url = trim((string)(getProp($product, 'three_view_image_url') ?: getProp($product, 'url')));
|
|
|
+ if ($url !== '') {
|
|
|
+ $imageMap[$name] = $url;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (empty($imageMap)) {
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ preg_match_all('/\{([^}]+)\}/u', $actContent, $matches);
|
|
|
+ $orderedNames = [];
|
|
|
+ foreach (($matches[1] ?? []) as $name) {
|
|
|
+ if (!isset($orderedNames[$name])) {
|
|
|
+ $orderedNames[$name] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $usedUrls = [];
|
|
|
+ foreach (array_keys($orderedNames) as $name) {
|
|
|
+ $url = $imageMap[$name] ?? '';
|
|
|
+ if ($url === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 相同图片复用同一编号,不重复计数
|
|
|
+ if (isset($usedUrls[$url])) {
|
|
|
+ $result['allowed'][$name] = $url;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (count($usedUrls) >= $limit) {
|
|
|
+ $result['excluded'][$name] = '超过参考图数量上限(' . $limit . '张)';
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $usedUrls[$url] = true;
|
|
|
+ $result['allowed'][$name] = $url;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($result['excluded'])) {
|
|
|
+ dLog('anime')->info('资产参考图超出模型限制,已截断', [
|
|
|
+ 'model' => $model,
|
|
|
+ 'limit' => $limit,
|
|
|
+ 'allowed' => array_keys($result['allowed']),
|
|
|
+ 'excluded' => $result['excluded'],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 获取模型对参考音频的限制
|
|
|
* Seedance 2.0(含 2.0 lite / 2.0 fast):最多3段,单个[2,15]s,总时长不超过15s
|
|
|
* Seedance 2.5:最多10段,单个[2,30]s,总时长不超过30s
|