فهرست منبع

1.新增片段转视频接口2.新增处理片段内容转AI提示词公共方法3.前端改版

lh 1 ماه پیش
والد
کامیت
8891c21847

+ 8 - 1
app/Http/Controllers/Anime/AnimeController.php

@@ -694,7 +694,7 @@ class AnimeController extends BaseController
         $data = $request->all();
         
         // 创建视频生成任务
-        $result = $this->AnimeService->createSegmentVideoTask($data);
+        $result = $this->AnimeService->createActVideoTask($data);
         $taskId = $result['task_id'];
         
         // 设置 SSE 响应头
@@ -2455,4 +2455,11 @@ class AnimeController extends BaseController
             }
         }
     }
+
+    // 确认片段
+    public function confirmActs(Request $request) {
+        $data = $request->all();
+        $result = $this->AnimeService->confirmActs($data);
+        return $this->success($result);
+    }
 }

+ 302 - 3
app/Services/Anime/AnimeService.php

@@ -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
+        ];
+    }
 }
 

+ 2 - 2
app/Services/DeepSeek/DeepSeekService.php

@@ -5712,7 +5712,7 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
                 foreach ($act_segments as $seg) {
                     $seg_num = getProp($seg, 'segment_number');
                     $seg_content = getProp($seg, 'segment_content');
-                    $merged_parts[] = "【镜{$seg_num}】\n{$seg_content}\n";
+                    $merged_parts[] = "【镜{$seg_num}】\n{$seg_content}\n";
                 }
                 $segments[] = [
                     'anime_id' => $anime_id,
@@ -5817,7 +5817,7 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
                 foreach ($act_segments as $seg) {
                     $seg_num = getProp($seg, 'segment_number');
                     $seg_content = getProp($seg, 'segment_content');
-                    $merged_parts[] = "【镜{$seg_num}】{$seg_content}\n";
+                    $merged_parts[] = "【镜{$seg_num}】{$seg_content}\n";
                 }
                 $restructured_acts[] = [
                     'anime_id' => $anime_id,

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
resources/views/animeIndex.blade.php


+ 3 - 2
routes/api.php

@@ -188,13 +188,14 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             Route::get('segmentChatHistory', [AnimeController::class, 'segmentChatHistory']);           // 分镜对话历史记录
             Route::post('createSegmentVideoTask', [AnimeController::class, 'createSegmentVideoTask']);  // 分镜图片转视频
             Route::post('batchSetSegmentVideos', [AnimeController::class, 'batchSetSegmentVideos']);    // 一键转视频
-            Route::post('createActVideoTask', [AnimeController::class, 'createActVideoTask']);          // 片段转视频
-            Route::post('batchSetActVideos', [AnimeController::class, 'batchSetActVideos']);            // 片段一键转视频
             Route::post('applyAudioData', [AnimeController::class, 'applyAudioData']);                  // 修改分镜音频参数
             Route::post('previewAudio', [AnimeController::class, 'previewAudio']);                      // 音频试听
             Route::get('episodePicsInfo', [AnimeController::class, 'episodePicsInfo']);                 // 获取主体和场景图片生成信息
             Route::get('clipSegmentVideo', [AnimeController::class, 'clipSegmentVideo']);                       // 裁剪视频
             Route::get('actChatHistory', [AnimeController::class, 'actChatHistory']);                   // 片段对话历史记录
+            Route::get('confirmActs', [AnimeController::class, 'confirmActs']);                        // 确认片段分镜
+            Route::post('createActVideoTask', [AnimeController::class, 'createActVideoTask']);          // 片段转视频
+            Route::post('batchSetActVideos', [AnimeController::class, 'batchSetActVideos']);            // 片段一键转视频
             
             // 完整视频合成任务
             Route::get('createCompleteVideoTask', [AnimeController::class, 'createCompleteVideoTask']);