|
|
@@ -3997,7 +3997,9 @@ class AnimeService
|
|
|
$first_frame_url = getProp($data, 'first_frame_url');
|
|
|
$tail_frame_url = getProp($data, 'tail_frame_url');
|
|
|
$generate_audio = getProp($data, 'generate_audio', 1);
|
|
|
- $reference_images = getProp($data, 'reference_images', []);
|
|
|
+ $products = getProp($data, 'products', []);
|
|
|
+ // $reference_images = getProp($data, 'reference_images', []);
|
|
|
+ $reference_images = [];
|
|
|
|
|
|
if (!$act_id) {
|
|
|
Utils::throwError('1002:片段ID不能为空');
|
|
|
@@ -4016,13 +4018,16 @@ class AnimeService
|
|
|
if (!$ratio) $ratio = '9:16';
|
|
|
|
|
|
// 获取分镜内容
|
|
|
- $actContent = getProp($act, 'act_content', '');
|
|
|
+ $actContent = getProp($act, 'act_show_content', '');
|
|
|
|
|
|
// 音效同出(默认使用)
|
|
|
$current_generate_audio = $generate_audio ? 1 : 0;
|
|
|
|
|
|
// 构建完整的提示词
|
|
|
- $fullPrompt = $actContent;
|
|
|
+ $processResult = $this->processActContentWithProducts($actContent, $products);
|
|
|
+ $fullPrompt = $processResult['content'];
|
|
|
+ $reference_images = array_merge($reference_images, $processResult['reference_images']);
|
|
|
+ dd($fullPrompt, $reference_images);
|
|
|
|
|
|
// 去除多余的换行符(将连续多个换行符替换为单个换行符)
|
|
|
$fullPrompt = preg_replace('/\n{2,}/', "\n", $fullPrompt);
|
|
|
@@ -5971,5 +5976,299 @@ class AnimeService
|
|
|
Utils::throwError('20003:' . $e->getMessage());
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理act_content中的{}标记,替换为product_name(图+序号)格式,并收集参考图片
|
|
|
+ * @param string $actContent 原始内容
|
|
|
+ * @param array $products 产品数组(包含product_name和url)
|
|
|
+ * @return array ['content' => 处理后的内容, 'reference_images' => 参考图片数组]
|
|
|
+ */
|
|
|
+ private function processActContentWithProducts($actContent, $products) {
|
|
|
+ if (empty($actContent) || empty($products)) {
|
|
|
+ return [
|
|
|
+ 'content' => $actContent,
|
|
|
+ 'reference_images' => []
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建产品名称到产品信息的映射
|
|
|
+ $product_dict = [];
|
|
|
+ foreach ($products as $product) {
|
|
|
+ $product_name = getProp($product, 'product_name');
|
|
|
+ if ($product_name) {
|
|
|
+ $product_dict[$product_name] = [
|
|
|
+ 'url' => getProp($product, 'url'),
|
|
|
+ 'voice_prompt' => getProp($product, 'voice_prompt')
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第一步:查找内容中所有 {产品名称} 格式的标记(按出现顺序,去重)
|
|
|
+ preg_match_all('/\{([^}]+)\}/u', $actContent, $matches);
|
|
|
+
|
|
|
+ $reference_images = [];
|
|
|
+ $product_index_map = []; // 产品名称 => 图片序号的映射
|
|
|
+ $voice_prompts = []; // 存储需要添加到开头的voice_prompt信息
|
|
|
+ $narrator_voice_prompt = ''; // 单独存储旁白的voice_prompt
|
|
|
+ $current_index = 1;
|
|
|
+
|
|
|
+ if (!empty($matches[1])) {
|
|
|
+ // 按照在内容中第一次出现的顺序,为每个产品分配序号
|
|
|
+ $seen_products = []; // 用于去重
|
|
|
+
|
|
|
+ foreach ($matches[1] as $product_name) {
|
|
|
+ // 跳过已处理的产品
|
|
|
+ if (isset($seen_products[$product_name])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $seen_products[$product_name] = true;
|
|
|
+
|
|
|
+ // 检查产品是否在字典中
|
|
|
+ if (isset($product_dict[$product_name])) {
|
|
|
+ $url = $product_dict[$product_name]['url'];
|
|
|
+ $voice_prompt = $product_dict[$product_name]['voice_prompt'];
|
|
|
+
|
|
|
+ // 处理voice_prompt(旁白单独处理,放在最后)
|
|
|
+ if (!empty($voice_prompt)) {
|
|
|
+ if ($product_name === '旁白') {
|
|
|
+ $narrator_voice_prompt = $product_name . ':' . $voice_prompt . "\n";
|
|
|
+ } else {
|
|
|
+ $voice_prompts[] = $product_name . ':' . $voice_prompt . "\n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只有URL非空才分配序号和添加到参考图片
|
|
|
+ if (!empty($url)) {
|
|
|
+ // 检查URL是否已经在reference_images中(去重)
|
|
|
+ if (!in_array($url, $reference_images)) {
|
|
|
+ $reference_images[] = $url;
|
|
|
+ $product_index_map[$product_name] = $current_index;
|
|
|
+ $current_index++;
|
|
|
+ } else {
|
|
|
+ // URL重复,找到已有的序号
|
|
|
+ $existing_index = array_search($url, $reference_images) + 1;
|
|
|
+ $product_index_map[$product_name] = $existing_index;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // URL为空,不分配序号(后续直接去掉{})
|
|
|
+ $product_index_map[$product_name] = 0;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 产品不在字典中,不分配序号
|
|
|
+ $product_index_map[$product_name] = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二步:替换内容中的 {产品名称}
|
|
|
+ $processedContent = $actContent;
|
|
|
+ foreach ($product_index_map as $product_name => $index) {
|
|
|
+ $escaped_name = preg_quote($product_name, '/');
|
|
|
+
|
|
|
+ if ($index > 0) {
|
|
|
+ // 有图片,替换为:产品名称(图X)
|
|
|
+ $replacement = $product_name . '(图' . $index . ')';
|
|
|
+ } else {
|
|
|
+ // 无图片,只保留产品名称
|
|
|
+ $replacement = $product_name;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 替换所有 {产品名称} 为处理后的格式
|
|
|
+ $processedContent = preg_replace('/\{' . $escaped_name . '\}/u', $replacement, $processedContent);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第三步:将voice_prompt信息添加到内容最前面
|
|
|
+ $voice_prompt_prefix = '';
|
|
|
+ if (!empty($voice_prompts)) {
|
|
|
+ $voice_prompt_prefix .= implode('', $voice_prompts);
|
|
|
+ }
|
|
|
+ // 旁白的voice_prompt放在最后
|
|
|
+ if (!empty($narrator_voice_prompt)) {
|
|
|
+ $voice_prompt_prefix .= $narrator_voice_prompt;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有voice_prompt信息,添加到内容开头
|
|
|
+ if (!empty($voice_prompt_prefix)) {
|
|
|
+ $processedContent = "人物音色:\n" . $voice_prompt_prefix . "\n" . $processedContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'content' => $processedContent,
|
|
|
+ 'reference_images' => $reference_images
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function confirmActs($data) {
|
|
|
+ $episode_id = getProp($data, 'episode_id');
|
|
|
+
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
|
|
|
+ if (!$episode) {
|
|
|
+ Utils::throwError('20003:分集不存在');
|
|
|
+ }
|
|
|
+ $roles = json_decode(getProp($episode, 'roles'), true) ?: [];
|
|
|
+ $scenes = json_decode(getProp($episode, 'scenes'), true) ?: [];
|
|
|
+
|
|
|
+ // 获取所有片段数据
|
|
|
+ $acts = DB::table('mp_episode_segments')->where('episode_id', $episode_id)->orderBy('act_number')->get();
|
|
|
+
|
|
|
+ // 构建角色和场景名称数组
|
|
|
+ $role_names = [];
|
|
|
+ $scene_names = [];
|
|
|
+
|
|
|
+ foreach ($roles as $role) {
|
|
|
+ $role_name = getProp($role, 'role');
|
|
|
+ if ($role_name && $role_name != '旁白') {
|
|
|
+ $role_names[] = $role_name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($scenes as $scene) {
|
|
|
+ $scene_name = getProp($scene, 'scene');
|
|
|
+ if ($scene_name) {
|
|
|
+ $scene_names[] = $scene_name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按照字符长度降序排序,确保先匹配长的名称(避免部分匹配问题)
|
|
|
+ usort($role_names, function($a, $b) {
|
|
|
+ return mb_strlen($b) - mb_strlen($a);
|
|
|
+ });
|
|
|
+ usort($scene_names, function($a, $b) {
|
|
|
+ return mb_strlen($b) - mb_strlen($a);
|
|
|
+ });
|
|
|
+
|
|
|
+ $count = 0;
|
|
|
+ try {
|
|
|
+ DB::beginTransaction();
|
|
|
+ // 处理每个片段
|
|
|
+ foreach ($acts as $act) {
|
|
|
+ $act_content = getProp($act, 'act_content');
|
|
|
+ if (!$act_content) continue;
|
|
|
+ $count++;
|
|
|
+ if ($count > 1) continue;
|
|
|
+
|
|
|
+ $processed_content = $act_content;
|
|
|
+
|
|
|
+ // 标记已处理的位置,避免重复标记
|
|
|
+ $marked_positions = [];
|
|
|
+
|
|
|
+ // 先匹配场景(在"场景:"后面)
|
|
|
+ foreach ($scene_names as $scene_name) {
|
|
|
+ // 转义特殊字符
|
|
|
+ $escaped_scene = preg_quote($scene_name, '/');
|
|
|
+ // 匹配场景:后面的场景名称(可能换行,也可能不换行)
|
|
|
+ $pattern = '/(场景:[\s\r\n]*)(' . $escaped_scene . ')/u';
|
|
|
+
|
|
|
+ $processed_content = preg_replace_callback($pattern, function($matches) use (&$marked_positions) {
|
|
|
+ $full_match = $matches[0];
|
|
|
+ $prefix = $matches[1];
|
|
|
+ $scene_name = $matches[2];
|
|
|
+
|
|
|
+ // 检查是否已经被标记
|
|
|
+ if (strpos($scene_name, '{') === false && strpos($scene_name, '}') === false) {
|
|
|
+ return $prefix . '{' . $scene_name . '}';
|
|
|
+ }
|
|
|
+ return $full_match;
|
|
|
+ }, $processed_content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 再匹配角色名称(只在"画面:"部分)
|
|
|
+ foreach ($role_names as $role_name) {
|
|
|
+ // 转义特殊字符
|
|
|
+ $escaped_role = preg_quote($role_name, '/');
|
|
|
+
|
|
|
+ // 只匹配"画面:"开始到换行之前的内容
|
|
|
+ $pattern = '/(画面:[^\n]*)/u';
|
|
|
+ $processed_content = preg_replace_callback($pattern, function($matches) use ($escaped_role) {
|
|
|
+ $line = $matches[1]; // 整行"画面:xxxxx"
|
|
|
+
|
|
|
+ // 在这一行中匹配所有角色名(避免重复标记)
|
|
|
+ $new_line = preg_replace(
|
|
|
+ '/(?<![{])(' . $escaped_role . ')(?![}])/u',
|
|
|
+ '{$1}',
|
|
|
+ $line
|
|
|
+ );
|
|
|
+
|
|
|
+ return $new_line;
|
|
|
+ }, $processed_content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新数据库
|
|
|
+ $boolen = DB::table('mp_episode_segments')
|
|
|
+ ->where('id', $act->id)
|
|
|
+ ->update([
|
|
|
+ 'act_show_content' => $processed_content,
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ]);
|
|
|
+ if (!$boolen) {
|
|
|
+ Utils::throwError('20003:片段处理失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
+ Utils::throwError('20003:'.$e->getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ $boolen1 = DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
|
|
|
+ 'is_generated' => 1,
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ]);
|
|
|
+ if (!$boolen1) {
|
|
|
+ DB::rollBack();
|
|
|
+ Utils::throwError('20003:分集处理失败!');
|
|
|
+ }
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ // 合并角色和场景作为products,统一字段名为product_name
|
|
|
+ $products = [];
|
|
|
+
|
|
|
+ // 处理角色数组
|
|
|
+ foreach ($roles as $role) {
|
|
|
+ $product = $role;
|
|
|
+ // 将role字段重命名为product_name
|
|
|
+ if (isset($product['role'])) {
|
|
|
+ $product['product_name'] = $product['role'];
|
|
|
+ unset($product['role']);
|
|
|
+ }
|
|
|
+ $product['product'] = 1; // 标记类型为角色
|
|
|
+ $products[] = $product;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理场景数组
|
|
|
+ foreach ($scenes as $scene) {
|
|
|
+ $product = $scene;
|
|
|
+ // 将scene字段重命名为product_name
|
|
|
+ if (isset($product['scene'])) {
|
|
|
+ $product['product_name'] = $product['scene'];
|
|
|
+ unset($product['scene']);
|
|
|
+ }
|
|
|
+ $product['product'] = 2; // 标记类型为场景
|
|
|
+ $products[] = $product;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建返回的acts数组
|
|
|
+ $return_acts = [];
|
|
|
+ foreach ($acts as $act) {
|
|
|
+ $return_acts[] = [
|
|
|
+ 'act_id' => getProp($act, 'id'),
|
|
|
+ 'act_number' => getProp($act, 'act_number'),
|
|
|
+ 'act_duration' => getProp($act, 'act_duration'),
|
|
|
+ 'act_show_content' => getProp($act, 'act_show_content'),
|
|
|
+ 'video_url' => getProp($act, 'video_url'),
|
|
|
+ 'video_duration' => getProp($act, 'video_duration'),
|
|
|
+ 'video_time_point_start' => getProp($act, 'video_time_point_start'),
|
|
|
+ 'video_time_point_end' => getProp($act, 'video_time_point_end'),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'anime_id' => getProp($episode, 'anime_id'),
|
|
|
+ 'episode_id' => $episode_id,
|
|
|
+ 'products' => $products,
|
|
|
+ 'acts' => $return_acts
|
|
|
+ ];
|
|
|
+ }
|
|
|
}
|
|
|
|