|
|
@@ -1071,6 +1071,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_role_name = getProp($target_roles, 'role');
|
|
|
+ if (empty($target_role_name)) {
|
|
|
+ Utils::throwError('20003:角色名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -1301,6 +1304,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_scene_name = getProp($target_scenes, 'scene');
|
|
|
+ if (empty($target_scene_name)) {
|
|
|
+ Utils::throwError('20003:场景名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -1515,6 +1521,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_prop_name = getProp($target_props, 'prop');
|
|
|
+ if (empty($target_prop_name)) {
|
|
|
+ Utils::throwError('20003:道具名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -3368,31 +3377,44 @@ class AnimeService
|
|
|
if ($episode) {
|
|
|
$roles = json_decode(getProp($episode, 'roles', '[]'), true) ?: [];
|
|
|
$scenes = json_decode(getProp($episode, 'scenes', '[]'), true) ?: [];
|
|
|
+ $props = json_decode(getProp($episode, 'props', '[]'), true) ?: [];
|
|
|
$extra_products = json_decode(getProp($episode, 'extra_products', '[]'), true) ?: [];
|
|
|
|
|
|
- // 先合并roles和scenes
|
|
|
+ // 先合并roles、scenes和props
|
|
|
// 处理角色数组
|
|
|
foreach ($roles as $role) {
|
|
|
$product = $role;
|
|
|
// 将role字段重命名为product_name
|
|
|
- if (isset($product['role'])) {
|
|
|
+ if (is_array($product) && !empty($product['role'] ?? null)) {
|
|
|
$product['product_name'] = $product['role'];
|
|
|
unset($product['role']);
|
|
|
+ $product['product'] = 1; // 标记类型为角色
|
|
|
+ $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
|
|
|
}
|
|
|
- $product['product'] = 1; // 标记类型为角色
|
|
|
- $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
|
|
|
}
|
|
|
|
|
|
// 处理场景数组
|
|
|
foreach ($scenes as $scene) {
|
|
|
$product = $scene;
|
|
|
// 将scene字段重命名为product_name
|
|
|
- if (isset($product['scene'])) {
|
|
|
+ if (is_array($product) && !empty($product['scene'] ?? null)) {
|
|
|
$product['product_name'] = $product['scene'];
|
|
|
unset($product['scene']);
|
|
|
+ $product['product'] = 2; // 标记类型为场景
|
|
|
+ $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理道具数组
|
|
|
+ foreach ($props as $prop) {
|
|
|
+ $product = $prop;
|
|
|
+ // 将prop字段重命名为product_name
|
|
|
+ if (is_array($product) && !empty($product['prop'] ?? null)) {
|
|
|
+ $product['product_name'] = $product['prop'];
|
|
|
+ unset($product['prop']);
|
|
|
+ $product['product'] = 3; // 标记类型为道具
|
|
|
+ $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
|
|
|
}
|
|
|
- $product['product'] = 2; // 标记类型为场景
|
|
|
- $products[$product['product_name']] = $product; // 使用product_name作为key便于后续覆盖
|
|
|
}
|
|
|
|
|
|
// extra_products优先级更高,直接覆盖同名的roles和scenes
|
|
|
@@ -5068,6 +5090,146 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 文生视频通用方法
|
|
|
+ * 支持传入视频参数、提示词、参考图等信息,不支持products入参
|
|
|
+ * 用户提示词不做任何处理,直接组装参数后根据模型调用
|
|
|
+ *
|
|
|
+ * @param array $data 请求参数
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public function generateVideo($data) {
|
|
|
+ $prompt = getProp($data, 'prompt');
|
|
|
+ if (empty($prompt)) {
|
|
|
+ Utils::throwError('20003:提示词不能为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ $model = getProp($data, 'model');
|
|
|
+ if (!$model) {
|
|
|
+ $model = 'zhizhen-20';
|
|
|
+ }
|
|
|
+ // 验证模型是否在可用模型表中
|
|
|
+ if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
|
|
|
+ Utils::throwError('20003:该模型已不可用,请更换!');
|
|
|
+ }
|
|
|
+
|
|
|
+ $video_duration = getProp($data, 'video_duration', 5);
|
|
|
+ $video_resolution = getProp($data, 'video_resolution', '480p');
|
|
|
+ $ratio = getProp($data, 'ratio', '9:16');
|
|
|
+ $generate_audio = getProp($data, 'generate_audio', 1);
|
|
|
+ $seed = getProp($data, 'seed', -1);
|
|
|
+ $first_frame_url = getProp($data, 'first_frame_url');
|
|
|
+ $tail_frame_url = getProp($data, 'tail_frame_url');
|
|
|
+
|
|
|
+ // 参考图(支持数组或JSON字符串,最多9张)
|
|
|
+ $reference_images = getProp($data, 'reference_images', []);
|
|
|
+ if (is_string($reference_images)) {
|
|
|
+ $reference_images = json_decode($reference_images, true) ?: [];
|
|
|
+ }
|
|
|
+ if (!is_array($reference_images)) {
|
|
|
+ Utils::throwError('20003:参考图格式不正确');
|
|
|
+ }
|
|
|
+ $reference_images = array_values(array_unique(array_filter($reference_images)));
|
|
|
+ $reference_images = array_slice($reference_images, 0, 9);
|
|
|
+
|
|
|
+ // 构建视频生成参数(用户参数直接透传,提示词不做处理)
|
|
|
+ $videoParams = [
|
|
|
+ 'model' => $model,
|
|
|
+ 'prompt' => $prompt,
|
|
|
+ 'video_duration' => $video_duration,
|
|
|
+ 'video_resolution' => $video_resolution,
|
|
|
+ 'seed' => $seed,
|
|
|
+ 'ratio' => $ratio,
|
|
|
+ 'generate_audio' => (int)$generate_audio === 1 ? true : false,
|
|
|
+ 'draft' => getProp($data, 'draft', false),
|
|
|
+ 'watermark' => getProp($data, 'watermark', false),
|
|
|
+ 'camera_fixed' => getProp($data, 'camera_fixed', false),
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($first_frame_url) $videoParams['first_frame_url'] = $first_frame_url;
|
|
|
+ if ($tail_frame_url) $videoParams['tail_frame_url'] = $tail_frame_url;
|
|
|
+
|
|
|
+ // 构建content数组
|
|
|
+ $videoParams['content'] = [
|
|
|
+ [
|
|
|
+ 'type' => 'text',
|
|
|
+ 'text' => $prompt,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 根据模型选择不同的视频生成方法
|
|
|
+ if (strpos($model, 'jimeng') !== false) {
|
|
|
+ // 即梦模型参数调整
|
|
|
+ unset($videoParams['content']); // 即梦不使用content格式
|
|
|
+ $task = $this->aiVideoGenerationService->createJimengTask($videoParams);
|
|
|
+ } elseif (strpos($model, 'kling') !== false) {
|
|
|
+ // 可灵模型参数调整
|
|
|
+ $videoParams['aspect_ratio'] = $videoParams['ratio']; // 可灵使用aspect_ratio
|
|
|
+ unset($videoParams['ratio']);
|
|
|
+ unset($videoParams['content']); // 可灵不使用content格式
|
|
|
+ $task = $this->aiVideoGenerationService->createKelingOmniTask($videoParams);
|
|
|
+ } elseif (strpos($model, 'zhizhen') !== false) {
|
|
|
+ // 智帧等新模型使用统一API
|
|
|
+ $unifiedParams = [
|
|
|
+ 'model_code' => $model,
|
|
|
+ 'prompt' => $prompt,
|
|
|
+ 'video_duration' => $video_duration,
|
|
|
+ 'video_resolution' => strtolower($video_resolution),
|
|
|
+ 'video_ratio' => $ratio,
|
|
|
+ 'seed' => $seed,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 构建parameters参数
|
|
|
+ $parameters = [
|
|
|
+ 'seconds' => $unifiedParams['video_duration'],
|
|
|
+ 'resolution' => $unifiedParams['video_resolution'],
|
|
|
+ 'ratio' => $ratio,
|
|
|
+ 'generate_audio' => (int)$generate_audio === 1 ? true : false,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 首帧和尾帧
|
|
|
+ if ($first_frame_url) $parameters['first_frame_url'] = $first_frame_url;
|
|
|
+ if ($tail_frame_url) $parameters['last_frame_url'] = $tail_frame_url;
|
|
|
+
|
|
|
+ // 参考图处理
|
|
|
+ if ($reference_images) {
|
|
|
+ $unifiedParams['ref_image_url'] = json_encode($reference_images, 256);
|
|
|
+ $reference_images = $this->aiImageGenerationService->processReferenceImagesToAssets($reference_images, $unifiedParams['prompt']);
|
|
|
+ $parameters['reference_images'] = $reference_images;
|
|
|
+ $parameters['video_mode'] = 'multi_image';
|
|
|
+ $parameters['mode'] = 'multi_image';
|
|
|
+ }
|
|
|
+
|
|
|
+ $unifiedParams['parameters'] = $parameters;
|
|
|
+
|
|
|
+ // 调用统一API创建任务
|
|
|
+ $task = $this->aiVideoGenerationService->createUnifiedApiTask($unifiedParams);
|
|
|
+ } elseif (strpos($model, 'doubao-seedance-2.0') !== false) {
|
|
|
+ // 豆包Seedance 2.0(百度AI网关):图片需要先上传为素材
|
|
|
+ $videoParams['reference_images'] = $reference_images;
|
|
|
+
|
|
|
+ if ($reference_images) {
|
|
|
+ // 参考图上传为百度网关素材
|
|
|
+ $reference_image_assets = $this->aiVideoGenerationService->processReferenceImagesToSeedance20Assets($reference_images, $videoParams['prompt']);
|
|
|
+ $videoParams['content'][0]['text'] = $videoParams['prompt'];
|
|
|
+ $videoParams['reference_image_assets'] = $reference_image_assets;
|
|
|
+ }
|
|
|
+
|
|
|
+ $task = $this->aiVideoGenerationService->createSeedance20Task($videoParams);
|
|
|
+ } else {
|
|
|
+ $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'task_id' => $task->id,
|
|
|
+ 'status' => $task->status,
|
|
|
+ 'model' => $model,
|
|
|
+ 'video_duration' => $video_duration,
|
|
|
+ 'video_resolution' => $video_resolution,
|
|
|
+ 'ratio' => $ratio
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 根据提示词内容智能计算最优视频时长
|
|
|
*
|
|
|
* @param string $segmentContent 分镜内容
|
|
|
@@ -7755,7 +7917,7 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
// 根据动态查找的列索引提取数据
|
|
|
- $product_name = isset($row[$nameColumnIndex]) ? trim($row[$nameColumnIndex]) : '';
|
|
|
+ $product_name = isset($row[$nameColumnIndex]) ? trim((string)$row[$nameColumnIndex]) : '';
|
|
|
$sequence_str = isset($row[$sequenceColumnIndex]) ? trim($row[$sequenceColumnIndex]) : '';
|
|
|
$pic_prompt = '';
|
|
|
|
|
|
@@ -7781,8 +7943,15 @@ class AnimeService
|
|
|
|
|
|
// 处理product_name两边的符号
|
|
|
$product_name = preg_replace('/^[\s*\[\]【】[]{}{}\x{3000}]+|[\s*\[\]【】[]{}{}\x{3000}]+$/u', '', $product_name);
|
|
|
- // 如果名称不存在则跳过
|
|
|
- if (!$product_name) continue;
|
|
|
+ // 如果名称不存在或仅剩占位符号,则跳过
|
|
|
+ if (!$product_name || preg_match('/^[-—_]+$/u', $product_name)) {
|
|
|
+ dLog('anime')->warning('资产名称无效,跳过保存', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'type' => $type,
|
|
|
+ 'product_name' => $product_name
|
|
|
+ ]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
// 直接插入新资产到 mp_products 表(parent_id 指向对应类型的根文件夹)
|
|
|
$product_id = DB::table('mp_products')->insertGetId([
|