lh há 1 semana atrás
pai
commit
62ecd40d0a

+ 636 - 5
app/Services/AIGeneration/AIVideoGenerationService.php

@@ -642,6 +642,9 @@ class AIVideoGenerationService
     {
     {
         // 分辨率统一规范化为小写(480p=原始档;sr_720p=480p生成+本地超分到720p)
         // 分辨率统一规范化为小写(480p=原始档;sr_720p=480p生成+本地超分到720p)
         $params['video_resolution'] = strtolower(trim((string)($params['video_resolution'] ?? '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
         // 生成唯一的任务ID
         $taskId = 'seedance20_' . time() . '_' . uniqid();
         $taskId = 'seedance20_' . time() . '_' . uniqid();
@@ -661,7 +664,7 @@ class AIVideoGenerationService
             'status' => MpGenerateVideoTask::STATUS_PENDING,
             'status' => MpGenerateVideoTask::STATUS_PENDING,
             'api_type' => 'seedance', // 复用Seedance任务轮询流程(提交/查询接口按extra_params区分网关)
             'api_type' => 'seedance', // 复用Seedance任务轮询流程(提交/查询接口按extra_params区分网关)
             'extra_params' => [
             'extra_params' => [
-                'model' => $params['model'] ?? 'doubao-seedance-2.0',
+                'model' => $modelName,
                 'content' => $params['content'],
                 'content' => $params['content'],
                 'reference_images' => $params['reference_images'] ?? [],
                 'reference_images' => $params['reference_images'] ?? [],
                 'reference_image_assets' => $params['reference_image_assets'] ?? [],
                 'reference_image_assets' => $params['reference_image_assets'] ?? [],
@@ -680,7 +683,9 @@ class AIVideoGenerationService
                 // 网关配置:默认百度AI网关;快快AI可传 https://ai-api.kkidc.com + gateway_path_prefix=/sd
                 // 网关配置:默认百度AI网关;快快AI可传 https://ai-api.kkidc.com + gateway_path_prefix=/sd
                 'gateway_base_url' => $params['gateway_base_url'] ?? 'https://ai-gateway.baidubce.com',
                 'gateway_base_url' => $params['gateway_base_url'] ?? 'https://ai-gateway.baidubce.com',
                 'gateway_path_prefix' => $params['gateway_path_prefix'] ?? '',
                 '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'),
             'charge_info' => $this->buildChargeInfo($params, 'seedance'),
         ]);
         ]);
@@ -720,10 +725,16 @@ class AIVideoGenerationService
 
 
             $content = $extraParams['content'] ?? [];
             $content = $extraParams['content'] ?? [];
 
 
-            // Seedance 2.0 类网关需要先把图片上传为素材(百度AI网关/快快AI)
+            // Seedance 2.0 类网关需要先把图片上传为素材(国内百度AI网关/海外百度真人素材空间/快快AI)
             if (!empty($extraParams['gateway_base_url'])) {
             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') {
                 if (strpos($gatewayBaseUrl, 'ai-api.kkidc.com') !== false || $gatewayPathPrefix === '/sd') {
                     $content = $this->convertKuaikuaiContentImagesToAssets($content);
                     $content = $this->convertKuaikuaiContentImagesToAssets($content);
+                } elseif ($isDreaminaGateway) {
+                    // 海外百度真人素材需上传到 target-asset-space=dreamina-seedance-livenessface 资产空间
+                    $content = $this->convertDreaminaContentImagesToAssets($content);
                 } else {
                 } else {
                     $content = $this->convertSeedance20ContentImagesToAssets($content);
                     $content = $this->convertSeedance20ContentImagesToAssets($content);
                 }
                 }
@@ -812,8 +823,8 @@ class AIVideoGenerationService
             if (!empty($extraParams['duration'])) {
             if (!empty($extraParams['duration'])) {
                 $apiParams['duration'] = (int)$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['ratio'] = 'adaptive';
                 $apiParams['duration'] = -1;
                 $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-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-0-mini-260615' => 'doubao-seedance-2-0-mini-260615',
             'baidu-doubao-seedance-2-5-260628' => 'doubao-seedance-2-5-260628',
             '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;
         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聚合网关素材接口(真人素材库)
      * 调用快快AI聚合网关素材接口(真人素材库)
      * 文档:https://ai.kkidc.com/api-docs/9207319m0
      * 文档:https://ai.kkidc.com/api-docs/9207319m0
      *
      *

+ 47 - 112
app/Services/Anime/AnimeService.php

@@ -4685,7 +4685,7 @@ class AnimeService
         
         
         // 如果有首帧图片,添加到content中
         // 如果有首帧图片,添加到content中
         if ($hasImage) {
         if ($hasImage) {
-            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615'])) {
+            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-0-260128', 'baidu-dreamina-seedance-2-0-fast-260128', 'baidu-dreamina-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-5-260628'])) {
                 $videoParams['content'][] = [
                 $videoParams['content'][] = [
                     'type' => 'image_url',
                     'type' => 'image_url',
                     'image_url' => [
                     'image_url' => [
@@ -4825,7 +4825,7 @@ class AnimeService
                     $videoParams['video_duration'] = max(4, min(12, ceil($audioDuration)));
                     $videoParams['video_duration'] = max(4, min(12, ceil($audioDuration)));
                 }
                 }
                 $task = $this->aiVideoGenerationService->createKuaikuaiVideoTask($videoParams);
                 $task = $this->aiVideoGenerationService->createKuaikuaiVideoTask($videoParams);
-            } elseif ($canUseAudio && in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615'])) {
+            } elseif ($canUseAudio && in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-0-260128', 'baidu-dreamina-seedance-2-0-fast-260128', 'baidu-dreamina-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-5-260628'])) {
             // 检查是否为 Seedance 2.0 模型,如果是则需要处理配音音频
             // 检查是否为 Seedance 2.0 模型,如果是则需要处理配音音频
                 // 添加配音音频到content中
                 // 添加配音音频到content中
                 $videoParams['content'][] = [
                 $videoParams['content'][] = [
@@ -4893,7 +4893,24 @@ class AnimeService
     }
     }
 
 
     /**
     /**
-     * 是否为 Seedance 2.0 系列模型(百度 doubao-seedance-2.0 / baidu-* / 快快AI seed-2 系列)
+     * 是否为海外百度Seedance(dreamina真人素材空间)模型
+     * 系统内使用 baidu-dreamina- 前缀名,请求网关时映射为 dreamina-* 原名
+     *
+     * @param string $model
+     * @return bool
+     */
+    public function isOverseasBaiduSeedanceModel(string $model): bool
+    {
+        return in_array($model, [
+            'baidu-dreamina-seedance-2-0-260128',
+            'baidu-dreamina-seedance-2-0-fast-260128',
+            'baidu-dreamina-seedance-2-0-mini-260615',
+            'baidu-dreamina-seedance-2-5-260628',
+        ], true);
+    }
+
+    /**
+     * 是否为 Seedance 2.0 系列模型(百度 doubao-seedance-2.0 / baidu-doubao-* / baidu-dreamina-* / 快快AI seed-2 系列)
      * 供单条与批量生成入口统一判断,避免各入口各自维护模型白名单
      * 供单条与批量生成入口统一判断,避免各入口各自维护模型白名单
      *
      *
      * @param string $model
      * @param string $model
@@ -4903,7 +4920,8 @@ class AnimeService
     {
     {
         return strpos($model, 'doubao-seedance-2.0') !== false
         return strpos($model, 'doubao-seedance-2.0') !== false
             || $this->isKuaikuaiSeedanceModel($model)
             || $this->isKuaikuaiSeedanceModel($model)
-            || $this->isBaiduSeedanceModel($model);
+            || $this->isBaiduSeedanceModel($model)
+            || $this->isOverseasBaiduSeedanceModel($model);
     }
     }
 
 
     public function createActVideoTask($data) {
     public function createActVideoTask($data) {
@@ -5082,7 +5100,7 @@ class AnimeService
         
         
         // 如果有首帧图片,添加到content中
         // 如果有首帧图片,添加到content中
         if ($hasImage) {
         if ($hasImage) {
-            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'doubao-seedance-2.0', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615'])) {
+            if (in_array($model, ['doubao-seedance-2-0-260128', 'doubao-seedance-2-0-fast-260128', 'doubao-seedance-2.0', 'baidu-doubao-seedance-2-5-260628', 'baidu-doubao-seedance-2-0-260128', 'baidu-doubao-seedance-2-0-fast-260128', 'baidu-doubao-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-0-260128', 'baidu-dreamina-seedance-2-0-fast-260128', 'baidu-dreamina-seedance-2-0-mini-260615', 'baidu-dreamina-seedance-2-5-260628'])) {
                 $videoParams['content'][] = [
                 $videoParams['content'][] = [
                     'type' => 'image_url',
                     'type' => 'image_url',
                     'image_url' => [
                     'image_url' => [
@@ -5190,6 +5208,7 @@ class AnimeService
         } elseif ($this->isSeedance20SeriesModel($model)) {
         } elseif ($this->isSeedance20SeriesModel($model)) {
             // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片需要先上传为素材
             // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片需要先上传为素材
             $isKuaikuai = $this->isKuaikuaiSeedanceModel($model);
             $isKuaikuai = $this->isKuaikuaiSeedanceModel($model);
+            $isDreamina = $this->isOverseasBaiduSeedanceModel($model);
             // 保存原始参考图用于任务记录
             // 保存原始参考图用于任务记录
             $videoParams['reference_images'] = $reference_images;
             $videoParams['reference_images'] = $reference_images;
 
 
@@ -5200,10 +5219,12 @@ class AnimeService
                 // 维护ref_image_url字段(参考图URL,JSON数组,与智帧分支保持一致)
                 // 维护ref_image_url字段(参考图URL,JSON数组,与智帧分支保持一致)
                 $videoParams['ref_image_url'] = json_encode($reference_images, 256);
                 $videoParams['ref_image_url'] = json_encode($reference_images, 256);
 
 
-                // 参考图上传为素材(百度网关/快快AI),上传失败时同步清理提示词中的<图片N>标记
+                // 参考图上传为素材(国内百度/海外百度dreamina/快快AI),上传失败时同步清理提示词中的<图片N>标记
                 $reference_image_assets = $isKuaikuai
                 $reference_image_assets = $isKuaikuai
                     ? $this->aiVideoGenerationService->processReferenceImagesToKuaikuaiAssets($reference_images, $fullPrompt)
                     ? $this->aiVideoGenerationService->processReferenceImagesToKuaikuaiAssets($reference_images, $fullPrompt)
-                    : $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $fullPrompt);
+                    : ($isDreamina
+                        ? $this->aiVideoGenerationService->processReferenceImagesToBaiduDreaminaAssets($reference_images, $fullPrompt)
+                        : $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $fullPrompt));
 
 
                 // 素材上传失败后提示词可能被清理,同步更新视频参数中的提示词
                 // 素材上传失败后提示词可能被清理,同步更新视频参数中的提示词
                 $videoParams['prompt'] = trim($fullPrompt);
                 $videoParams['prompt'] = trim($fullPrompt);
@@ -5421,6 +5442,7 @@ class AnimeService
         } elseif ($this->isSeedance20SeriesModel($model)) {
         } elseif ($this->isSeedance20SeriesModel($model)) {
             // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片/视频/音频需要先上传为素材
             // 豆包Seedance 2.0(百度AI网关)/ 快快AI Seedance 2.0/2.5:图片/视频/音频需要先上传为素材
             $isKuaikuai = $this->isKuaikuaiSeedanceModel($model);
             $isKuaikuai = $this->isKuaikuaiSeedanceModel($model);
+            $isDreamina = $this->isOverseasBaiduSeedanceModel($model);
             $videoParams['reference_images'] = $reference_images;
             $videoParams['reference_images'] = $reference_images;
 
 
             // 维护ref_image_url字段(参考图+参考视频+参考音频URL,JSON数组)
             // 维护ref_image_url字段(参考图+参考视频+参考音频URL,JSON数组)
@@ -5430,26 +5452,32 @@ class AnimeService
             }
             }
 
 
             if ($reference_images) {
             if ($reference_images) {
-                // 参考图上传为素材(百度网关/快快AI)
+                // 参考图上传为素材(国内百度/海外百度dreamina/快快AI)
                 $reference_image_assets = $isKuaikuai
                 $reference_image_assets = $isKuaikuai
                     ? $this->aiVideoGenerationService->processReferenceImagesToKuaikuaiAssets($reference_images, $videoParams['prompt'])
                     ? $this->aiVideoGenerationService->processReferenceImagesToKuaikuaiAssets($reference_images, $videoParams['prompt'])
-                    : $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $videoParams['prompt']);
+                    : ($isDreamina
+                        ? $this->aiVideoGenerationService->processReferenceImagesToBaiduDreaminaAssets($reference_images, $videoParams['prompt'])
+                        : $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $videoParams['prompt']));
                 $videoParams['reference_image_assets'] = $reference_image_assets;
                 $videoParams['reference_image_assets'] = $reference_image_assets;
             }
             }
 
 
-            // 参考视频上传为素材(百度网关/快快AI)
+            // 参考视频上传为素材(国内百度/海外百度dreamina/快快AI)
             if ($reference_videos) {
             if ($reference_videos) {
                 $reference_video_assets = $isKuaikuai
                 $reference_video_assets = $isKuaikuai
                     ? $this->aiVideoGenerationService->processReferenceVideosToKuaikuaiAssets($reference_videos, $videoParams['prompt'])
                     ? $this->aiVideoGenerationService->processReferenceVideosToKuaikuaiAssets($reference_videos, $videoParams['prompt'])
-                    : $this->aiVideoGenerationService->processReferenceVideosToSeedance20Assets($reference_videos, $videoParams['prompt']);
+                    : ($isDreamina
+                        ? $this->aiVideoGenerationService->processReferenceVideosToBaiduDreaminaAssets($reference_videos, $videoParams['prompt'])
+                        : $this->aiVideoGenerationService->processReferenceVideosToSeedance20Assets($reference_videos, $videoParams['prompt']));
                 $videoParams['reference_video_assets'] = $reference_video_assets;
                 $videoParams['reference_video_assets'] = $reference_video_assets;
             }
             }
 
 
-            // 参考音频上传为素材(百度网关/快快AI)
+            // 参考音频上传为素材(国内百度/海外百度dreamina/快快AI)
             if ($reference_audios) {
             if ($reference_audios) {
                 $reference_audio_assets = $isKuaikuai
                 $reference_audio_assets = $isKuaikuai
                     ? $this->aiVideoGenerationService->processReferenceAudiosToKuaikuaiAssets($reference_audios, $videoParams['prompt'])
                     ? $this->aiVideoGenerationService->processReferenceAudiosToKuaikuaiAssets($reference_audios, $videoParams['prompt'])
-                    : $this->aiVideoGenerationService->processReferenceAudiosToSeedance20Assets($reference_audios, $videoParams['prompt']);
+                    : ($isDreamina
+                        ? $this->aiVideoGenerationService->processReferenceAudiosToBaiduDreaminaAssets($reference_audios, $videoParams['prompt'])
+                        : $this->aiVideoGenerationService->processReferenceAudiosToSeedance20Assets($reference_audios, $videoParams['prompt']));
                 $videoParams['reference_audio_assets'] = $reference_audio_assets;
                 $videoParams['reference_audio_assets'] = $reference_audio_assets;
             }
             }
 
 
@@ -6168,39 +6196,8 @@ class AnimeService
         $inner_prompt_type = getProp($data, 'inner_prompt_type', 0);
         $inner_prompt_type = getProp($data, 'inner_prompt_type', 0);
         $imageNum = (int)getProp($data, 'image_num', 1);
         $imageNum = (int)getProp($data, 'image_num', 1);
         
         
-        // 定义分辨率和宽高比对应的尺寸映射表
-        $sizeMap = [
-            '2k' => [
-                '1:1' => ['width' => 2048, 'height' => 2048],
-                '4:3' => ['width' => 2304, 'height' => 1728],
-                '3:4' => ['width' => 1728, 'height' => 2304],
-                '16:9' => ['width' => 2848, 'height' => 1600],
-                '9:16' => ['width' => 1600, 'height' => 2848],
-                '3:2' => ['width' => 2496, 'height' => 1664],
-                '2:3' => ['width' => 1664, 'height' => 2496],
-                '21:9' => ['width' => 3136, 'height' => 1344],
-            ],
-            '3k' => [
-                '1:1' => ['width' => 3072, 'height' => 3072],
-                '4:3' => ['width' => 3456, 'height' => 2592],
-                '3:4' => ['width' => 2592, 'height' => 3456],
-                '16:9' => ['width' => 4096, 'height' => 2304],
-                '9:16' => ['width' => 2304, 'height' => 4096],
-                '2:3' => ['width' => 2496, 'height' => 3744],
-                '3:2' => ['width' => 3744, 'height' => 2496],
-                '21:9' => ['width' => 4704, 'height' => 2016],
-            ],
-            '4k' => [
-                '1:1' => ['width' => 4096, 'height' => 4096],
-                '3:4' => ['width' => 3520, 'height' => 4704],
-                '4:3' => ['width' => 4704, 'height' => 3520],
-                '16:9' => ['width' => 5504, 'height' => 3040],
-                '9:16' => ['width' => 3040, 'height' => 5504],
-                '2:3' => ['width' => 3328, 'height' => 4992],
-                '3:2' => ['width' => 4992, 'height' => 3328],
-                '21:9' => ['width' => 6240, 'height' => 2656],
-            ],
-        ];
+        // 分辨率档位与尺寸映射统一从 BaseConst 获取
+        $sizeMap = BaseConst::IMAGE_RESOLUTION_SIZE_MAP;
         
         
         // 参数验证
         // 参数验证
         $resolution = strtolower($resolution);
         $resolution = strtolower($resolution);
@@ -7948,39 +7945,8 @@ class AnimeService
         $ratio = getProp($data, 'ratio', '16:9');
         $ratio = getProp($data, 'ratio', '16:9');
         $resolution = getProp($data, 'resolution', '2k');
         $resolution = getProp($data, 'resolution', '2k');
 
 
-        // 定义分辨率和宽高比对应的尺寸映射表
-        $sizeMap = [
-            '2k' => [
-                '1:1' => ['width' => 2048, 'height' => 2048],
-                '4:3' => ['width' => 2304, 'height' => 1728],
-                '3:4' => ['width' => 1728, 'height' => 2304],
-                '16:9' => ['width' => 2848, 'height' => 1600],
-                '9:16' => ['width' => 1600, 'height' => 2848],
-                '3:2' => ['width' => 2496, 'height' => 1664],
-                '2:3' => ['width' => 1664, 'height' => 2496],
-                '21:9' => ['width' => 3136, 'height' => 1344],
-            ],
-            '3k' => [
-                '1:1' => ['width' => 3072, 'height' => 3072],
-                '4:3' => ['width' => 3456, 'height' => 2592],
-                '3:4' => ['width' => 2592, 'height' => 3456],
-                '16:9' => ['width' => 4096, 'height' => 2304],
-                '9:16' => ['width' => 2304, 'height' => 4096],
-                '2:3' => ['width' => 2496, 'height' => 3744],
-                '3:2' => ['width' => 3744, 'height' => 2496],
-                '21:9' => ['width' => 4704, 'height' => 2016],
-            ],
-            '4k' => [
-                '1:1' => ['width' => 4096, 'height' => 4096],
-                '3:4' => ['width' => 3520, 'height' => 4704],
-                '4:3' => ['width' => 4704, 'height' => 3520],
-                '16:9' => ['width' => 5504, 'height' => 3040],
-                '9:16' => ['width' => 3040, 'height' => 5504],
-                '2:3' => ['width' => 3328, 'height' => 4992],
-                '3:2' => ['width' => 4992, 'height' => 3328],
-                '21:9' => ['width' => 6240, 'height' => 2656],
-            ],
-        ];
+        // 分辨率档位与尺寸映射统一从 BaseConst 获取
+        $sizeMap = BaseConst::IMAGE_RESOLUTION_SIZE_MAP;
         
         
         // 参数验证
         // 参数验证
         $resolution = strtolower($resolution);
         $resolution = strtolower($resolution);
@@ -8354,39 +8320,8 @@ class AnimeService
         $ratio = getProp($data, 'ratio', '9:16');
         $ratio = getProp($data, 'ratio', '9:16');
         $resolution = getProp($data, 'resolution', '2k');
         $resolution = getProp($data, 'resolution', '2k');
 
 
-        // 定义分辨率和宽高比对应的尺寸映射表
-        $sizeMap = [
-            '2k' => [
-                '1:1' => ['width' => 2048, 'height' => 2048],
-                '4:3' => ['width' => 2304, 'height' => 1728],
-                '3:4' => ['width' => 1728, 'height' => 2304],
-                '16:9' => ['width' => 2848, 'height' => 1600],
-                '9:16' => ['width' => 1600, 'height' => 2848],
-                '3:2' => ['width' => 2496, 'height' => 1664],
-                '2:3' => ['width' => 1664, 'height' => 2496],
-                '21:9' => ['width' => 3136, 'height' => 1344],
-            ],
-            '3k' => [
-                '1:1' => ['width' => 3072, 'height' => 3072],
-                '4:3' => ['width' => 3456, 'height' => 2592],
-                '3:4' => ['width' => 2592, 'height' => 3456],
-                '16:9' => ['width' => 4096, 'height' => 2304],
-                '9:16' => ['width' => 2304, 'height' => 4096],
-                '2:3' => ['width' => 2496, 'height' => 3744],
-                '3:2' => ['width' => 3744, 'height' => 2496],
-                '21:9' => ['width' => 4704, 'height' => 2016],
-            ],
-            '4k' => [
-                '1:1' => ['width' => 4096, 'height' => 4096],
-                '3:4' => ['width' => 3520, 'height' => 4704],
-                '4:3' => ['width' => 4704, 'height' => 3520],
-                '16:9' => ['width' => 5504, 'height' => 3040],
-                '9:16' => ['width' => 3040, 'height' => 5504],
-                '2:3' => ['width' => 3328, 'height' => 4992],
-                '3:2' => ['width' => 4992, 'height' => 3328],
-                '21:9' => ['width' => 6240, 'height' => 2656],
-            ],
-        ];
+        // 分辨率档位与尺寸映射统一从 BaseConst 获取
+        $sizeMap = BaseConst::IMAGE_RESOLUTION_SIZE_MAP;
         
         
         // 参数验证
         // 参数验证
         $resolution = strtolower($resolution);
         $resolution = strtolower($resolution);