|
|
@@ -636,6 +636,14 @@ class AnimeService
|
|
|
$episode['acts'] = $segmentsStructure['acts'];
|
|
|
$episode['total_duration'] = $segmentsStructure['total_duration'];
|
|
|
|
|
|
+ // 获取mp_animes表中的视频参数
|
|
|
+ $anime = DB::table('mp_animes')->where('id', $anime_id)->first(['video_model', 'video_resolution', 'video_ratio']);
|
|
|
+ $episode['video_params'] = [
|
|
|
+ 'video_model' => getProp($anime, 'video_model'),
|
|
|
+ 'video_resolution' => getProp($anime, 'video_resolution'),
|
|
|
+ 'ratio' => getProp($anime, 'video_ratio'),
|
|
|
+ ];
|
|
|
+
|
|
|
return $episode;
|
|
|
}
|
|
|
|
|
|
@@ -1067,6 +1075,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_role_name = getProp($target_roles, 'role');
|
|
|
+ if (empty($target_role_name)) {
|
|
|
+ Utils::throwError('20003:角色名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -1297,6 +1308,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_scene_name = getProp($target_scenes, 'scene');
|
|
|
+ if (empty($target_scene_name)) {
|
|
|
+ Utils::throwError('20003:场景名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -1511,6 +1525,9 @@ class AnimeService
|
|
|
|
|
|
try {
|
|
|
$target_prop_name = getProp($target_props, 'prop');
|
|
|
+ if (empty($target_prop_name)) {
|
|
|
+ Utils::throwError('20003:道具名称不能为空');
|
|
|
+ }
|
|
|
|
|
|
// 如果是删除操作
|
|
|
if ($is_deleted == 1) {
|
|
|
@@ -3364,31 +3381,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
|
|
|
@@ -5095,6 +5125,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
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 预估生成所需积分(mode=chat/image/video,支持多数量总额)
|
|
|
*
|
|
|
* chat : count(默认1,AI对话单次10积分);批量分集可传 anime_id + generate_episode_number 按实际待创建集数估算
|
|
|
@@ -7905,7 +8075,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 = '';
|
|
|
|
|
|
@@ -7931,8 +8101,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([
|
|
|
@@ -7983,6 +8160,12 @@ class AnimeService
|
|
|
|
|
|
// 批量插入映射数据(使用 insertOrIgnore 避免重复插入)
|
|
|
$insertedCount = DB::table('mp_script_product_mappings')->insertOrIgnore($mappingRecords);
|
|
|
+
|
|
|
+ // 剧本已有资产,同步更新is_products标记
|
|
|
+ DB::table('mp_scripts')->where('id', $script_id)->update([
|
|
|
+ 'is_products' => 1,
|
|
|
+ 'updated_at' => $now
|
|
|
+ ]);
|
|
|
|
|
|
// 如果需要自动生成图片,在事务中创建图片生成任务
|
|
|
$productTaskMap = [];
|
|
|
@@ -8683,12 +8866,43 @@ class AnimeService
|
|
|
$ratio = getProp($data, 'ratio');
|
|
|
if (!$ratio) $ratio = '9:16';
|
|
|
|
|
|
- return DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
|
|
|
- 'video_model' => $model,
|
|
|
- 'video_resolution' => $video_resolution,
|
|
|
- 'ratio' => $ratio,
|
|
|
- 'updated_at' => date('Y-m-d H:i:s')
|
|
|
- ]);
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first(['anime_id']);
|
|
|
+ if (!$episode) {
|
|
|
+ Utils::throwError('20003:分集不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ DB::beginTransaction();
|
|
|
+
|
|
|
+ $result = DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
|
|
|
+ 'video_model' => $model,
|
|
|
+ 'video_resolution' => $video_resolution,
|
|
|
+ 'ratio' => $ratio,
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ]);
|
|
|
+ if ($result === false) {
|
|
|
+ Utils::throwError('20003:分集视频参数保存失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 同步更新mp_animes表
|
|
|
+ $anime_result = DB::table('mp_animes')->where('id', $episode->anime_id)->update([
|
|
|
+ 'video_model' => $model,
|
|
|
+ 'video_resolution' => $video_resolution,
|
|
|
+ 'video_ratio' => $ratio,
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ]);
|
|
|
+ if ($anime_result === false) {
|
|
|
+ Utils::throwError('20003:动漫视频参数保存失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
+ Utils::throwError('20003:'.$e->getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -8894,6 +9108,11 @@ class AnimeService
|
|
|
'title' => getProp($episode, 'title'),
|
|
|
'episode_number' => getProp($episode, 'episode_number'),
|
|
|
'is_generated' => getProp($episode, 'is_generated'),
|
|
|
+ 'video_params' => [
|
|
|
+ 'video_model' => getProp($episode, 'video_model'),
|
|
|
+ 'video_resolution' => getProp($episode, 'video_resolution'),
|
|
|
+ 'ratio' => getProp($episode, 'ratio'),
|
|
|
+ ],
|
|
|
'products' => $products,
|
|
|
'acts' => $return_acts
|
|
|
];
|