Przeglądaj źródła

1.创建视频任务新增图片数量按模型验证
2.增加图片数量截断优先级(资产图>参考图)

lh 1 dzień temu
rodzic
commit
9162e803e1

+ 31 - 9
app/Http/Controllers/Anime/AnimeController.php

@@ -888,6 +888,9 @@ class AnimeController extends BaseController
                 // 智能选择视频时长
                 // $videoDuration = $this->AnimeService->calculateOptimalVideoDuration($segmentContent, $tailFrame);
                 $videoDuration = -1;
+
+                // 分镜模式仅使用分镜首帧图,不传资产参考图;显式初始化为空数组,避免未定义变量
+                $reference_images = [];
                 
                 // 构建视频生成参数
                 $videoParams = [
@@ -1015,12 +1018,16 @@ class AnimeController extends BaseController
                     // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片需要先上传为素材
                     $isKuaikuai = $this->AnimeService->isKuaikuaiSeedanceModel($model);
                     $isDreamina = $this->AnimeService->isOverseasBaiduSeedanceModel($model);
-                    // 保存原始参考图用于任务记录
-                    $videoParams['reference_images'] = $reference_images;
+                    // 任务记录用:保存截断前的参考图(分镜模式的参考图来自分镜数据,不经过资产上限截断前的合并)
+                    $reference_images_all = $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->AnimeService->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);
@@ -3151,9 +3158,13 @@ class AnimeController extends BaseController
                 $audioPlan = $supportAudioReference
                     ? $this->AnimeService->resolveActReferenceAudios($actContent, $products, $model)
                     : ['allowed' => [], 'excluded' => [], 'total_duration' => 0];
-                $processResult = $this->AnimeService->processActContentWithProducts($actContent, $products, $supportAudioReference, $audioPlan['allowed']);
+                // 资产参考图按模型上限(2.0=9张、2.5=30张)截断,保证提示词中的<图片N>与实际素材一一对应
+                $imagePlan = $this->AnimeService->resolveActReferenceImages($actContent, $products, $model);
+                $processResult = $this->AnimeService->processActContentWithProducts($actContent, $products, $supportAudioReference, $audioPlan['allowed'], $imagePlan['allowed']);
                 $fullPrompt = $processResult['content'];
                 $reference_images = $processResult['reference_images'];
+                // 任务记录用:正文内出现的全部资产参考图(按模型上限截断之前)
+                $reference_images_all = $processResult['reference_images_all'] ?? $reference_images;
                 // 角色音色参考音频(顺序与提示词中的<音频N>标记一一对应)
                 $reference_audios = $supportAudioReference ? ($processResult['reference_audios'] ?? []) : [];
                 // 参考音频素材不可用时用于兜底的角色音色提示词
@@ -3267,6 +3278,14 @@ class AnimeController extends BaseController
                     }
 
                     if ($reference_images) {
+                        // 参考图按模型上限截断(智帧沿用 Seedance 2.0 的 9 张上限)
+                        $imageLimit = $this->AnimeService->getActReferenceImageLimit($model);
+                        if ($imageLimit > 0) {
+                            $reference_images = array_slice($reference_images, 0, $imageLimit);
+                        }
+
+                        // 任务记录用:保存截断前的完整参考图,便于追溯
+                        $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;
@@ -3294,12 +3313,15 @@ class AnimeController extends BaseController
                     // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:参考图/配音音频需要先上传为素材
                     $isKuaikuai = $this->AnimeService->isKuaikuaiSeedanceModel($model);
                     $isDreamina = $this->AnimeService->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->AnimeService->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);

+ 2 - 0
app/Services/AIGeneration/AIVideoGenerationService.php

@@ -5489,6 +5489,8 @@ class AIVideoGenerationService
                 'video_duration' => $params['video_duration'] ?? 5,
                 'video_ratio' => $params['video_ratio'] ?? '9:16',
                 'parameters' => $params['parameters'] ?? [],
+                // 任务记录用:截断前的完整参考图(便于追溯前端入参)
+                'reference_images' => $params['reference_images'] ?? [],
             ],
             'charge_info' => $this->buildChargeInfo($params, 'zzengine'),
         ]);

+ 150 - 14
app/Services/Anime/AnimeService.php

@@ -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