Sfoglia il codice sorgente

新增重新生成分镜功能

lh 5 mesi fa
parent
commit
55332f61cb

+ 11 - 0
app/Http/Controllers/Anime/AnimeController.php

@@ -139,6 +139,17 @@ class AnimeController extends BaseController
         return $this->success($result);
         return $this->success($result);
     }
     }
 
 
+    // 重新生成分镜
+    public function reGenerateSegment(Request $request) {
+        // 忽略所有超时限制
+        set_time_limit(0);
+        ini_set('max_execution_time', '0');
+
+        $data = $request->all();
+        $result = $this->AnimeService->reGenerateSegment($data);
+        return $this->success(['img_url'=>$result]);
+    }
+
     // public function scriptList(Request $request) {
     // public function scriptList(Request $request) {
     //     $data = $request->all();
     //     $data = $request->all();
     //     $result = $this->AnimeService->scriptList($data);
     //     $result = $this->AnimeService->scriptList($data);

+ 4 - 0
app/Libs/Helpers.php

@@ -2384,6 +2384,10 @@ function handleScriptContent($originalContent) {
     preg_match('/###场景列表\s*\n(.*?)(?=\n###|$)/s', $originalContent, $scenesMatch);
     preg_match('/###场景列表\s*\n(.*?)(?=\n###|$)/s', $originalContent, $scenesMatch);
     $scenesText = isset($scenesMatch[1]) ? trim($scenesMatch[1]) : '';
     $scenesText = isset($scenesMatch[1]) ? trim($scenesMatch[1]) : '';
     $parts['scenes'] = parseScenesFromText($scenesText);
     $parts['scenes'] = parseScenesFromText($scenesText);
+
+    // 提取分集详细内容
+    preg_match('/###分集详细内容\s*\n(.*?)(?=\n###|$)/s', $originalContent, $contentMatch);
+    $parts['content'] = isset($contentMatch[1]) ? trim($contentMatch[1]) : '';
         
         
     // 提取分集剧本并按分集拆分 - 兼容多种格式
     // 提取分集剧本并按分集拆分 - 兼容多种格式
     $fullScript = '';
     $fullScript = '';

+ 314 - 41
app/Services/Anime/AnimeService.php

@@ -448,53 +448,19 @@ class AnimeService
             Utils::throwError("20003:" . $e->getMessage());
             Utils::throwError("20003:" . $e->getMessage());
         }
         }
 
 
-        $start_count = 0;
-        $img_url = '';
-        while ($start_count <= 30) {
-            sleep(3);
-
-            $task = MpGeneratePicTask::where('id', $task_id)->first();
-            $statusInfo = $this->aiImageGenerationService->queryTaskStatus($task);
-
-            if ($statusInfo['status'] === 'success') {
-                $task->updateStatus('success', [
-                    'result_url'    => $statusInfo['result_url'],
-                    'result_json'   => $statusInfo['result_json'] ?? []
-                ]);
-
-                // 同步调整分镜图片状态和结果
-                if (getProp($task, 'alias_segment_id')) {
-                    DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
-                        'img_url'   => $statusInfo['result_url'][0],
-                        'pic_task_status'   => '已完成',
-                    ]);
-                }
-                $img_url = $statusInfo['result_url'][0];
-                break;
-            } elseif ($statusInfo['status'] === 'failed') {
-                $task->updateStatus('failed', [
-                    'error_message' => $statusInfo['error_message'],
-                    'result_json'   => $statusInfo['result_json'] ?? []
-                ]);
-
-                if (getProp($task, 'alias_segment_id')) {
-                    DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
-                        'pic_task_status'   => '失败',
-                    ]);
-                }
-                break;
-            }
-            $start_count+=3;
-        }
+        $img_url = $this->loopGetPicTaskResult($task_id);
 
 
         if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
         if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
         return $img_url;
         return $img_url;
-        
     }
     }
 
 
     public function batchSetSegmentTasks($data) {
     public function batchSetSegmentTasks($data) {
         $anime_id = getProp($data, 'anime_id');
         $anime_id = getProp($data, 'anime_id');
         $episode_number = getProp($data, 'episode_number');
         $episode_number = getProp($data, 'episode_number');
+        $ratio = getProp($data, 'ratio');
+        if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) {
+            Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9');
+        }
         
         
         if (!$anime_id || !$episode_number) {
         if (!$anime_id || !$episode_number) {
             Utils::throwError('1002:请选择剧集');
             Utils::throwError('1002:请选择剧集');
@@ -512,6 +478,33 @@ class AnimeService
             Utils::throwError('20003:该剧集不存在');
             Utils::throwError('20003:该剧集不存在');
         }
         }
 
 
+        // 根据画面比例给宽高赋值
+        switch ($ratio) {
+            case '1:1':
+                $width = 2048;
+                $height = 2048;
+                break;
+            case '4:3':
+                $width = 2304;
+                $height = 1728;
+                break;
+            case '3:2':
+                $width = 2496;
+                $height = 1664;
+                break;
+            case '16:9':
+                $width = 2560;
+                $height = 1440;
+                break;
+            case '21:9':
+                $width = 3024;
+                $height = 1296;
+                break;
+            default:
+                $width = 2048;
+                $height = 2048;
+        }
+
         // 获取动漫的角色信息(包含参考图)
         // 获取动漫的角色信息(包含参考图)
         $anime = DB::table('mp_animes')
         $anime = DB::table('mp_animes')
             ->where('id', $anime_id)
             ->where('id', $anime_id)
@@ -626,7 +619,9 @@ class AnimeService
                 $params = [
                 $params = [
                     'alias_segment_id' => $segment_id,
                     'alias_segment_id' => $segment_id,
                     'prompt' => $prompt,
                     'prompt' => $prompt,
-                    'ref_img_urls' => $ref_img_urls
+                    'ref_img_urls' => $ref_img_urls,
+                    'width' => $width,
+                    'height' => $height,
                 ];
                 ];
                 
                 
                 // $task_id = mt_rand(100000, 999999);
                 // $task_id = mt_rand(100000, 999999);
@@ -652,7 +647,7 @@ class AnimeService
                     ]);
                     ]);
 
 
                 // 如果是第一集的首帧图片,则存入待更新数组
                 // 如果是第一集的首帧图片,则存入待更新数组
-                if ((int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) {
+                if ($update_result && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) {
                     Redis::sadd('anime_first_frame_urls', $segment_id);
                     Redis::sadd('anime_first_frame_urls', $segment_id);
                 }
                 }
                 
                 
@@ -699,6 +694,230 @@ class AnimeService
         ];
         ];
     }
     }
 
 
+    public function reGenerateSegment($data) {
+        $segment_id = getProp($data, 'segment_id');
+        $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
+        $anime_id = getProp($segment, 'anime_id');
+        $episode_number = getProp($segment, 'episode_number');
+        $segment_content = getProp($data, 'segment_content');
+        $ratio = getProp($data, 'ratio');
+        if (!in_array($ratio, ['1:1', '4:3', '3:2', '16:9', '21:9'])) {
+            Utils::throwError('1002:画面比例选择不正确,只支持1:1、4:3、3:2、16:9、21:9');
+        }
+        
+        if (!$anime_id || !$episode_number) {
+            Utils::throwError('1002:请选择分镜');
+        }
+
+        // 根据画面比例给宽高赋值
+        switch ($ratio) {
+            case '1:1':
+                $width = 2048;
+                $height = 2048;
+                break;
+            case '4:3':
+                $width = 2304;
+                $height = 1728;
+                break;
+            case '3:2':
+                $width = 2496;
+                $height = 1664;
+                break;
+            case '16:9':
+                $width = 2560;
+                $height = 1440;
+                break;
+            case '21:9':
+                $width = 3024;
+                $height = 1296;
+                break;
+            default:
+                $width = 2048;
+                $height = 2048;
+        }
+
+        // 获取动漫的角色信息(包含参考图)
+        $anime = DB::table('mp_animes')
+            ->where('id', $anime_id)
+            ->select('roles', 'scenes')
+            ->first();
+            
+        if (!$anime || !$anime->roles) {
+            Utils::throwError('20003:未找到角色信息');
+        }
+
+        $roles = json_decode($anime->roles, true);
+        if (!$roles) {
+            Utils::throwError('20003:角色信息格式错误');
+        }
+        // 构建角色名称到参考图的映射
+        $role_map = [];
+        foreach ($roles as $role) {
+            $role_name = getProp($role, 'role');
+            $role_url = getProp($role, 'url');
+            if (!empty($role_name) && !empty($role_url)) {
+                $role_map[$role_name] = $role_url;
+            }
+        }
+
+        $scenes = json_decode($anime->scenes, true);
+        if (!$scenes) {
+            Utils::throwError('20003:角色信息格式错误');
+        }
+        // 构建角色名称到参考图的映射
+        $scene_map = [];
+        foreach ($scenes as $scene) {
+            $scene_name = getProp($scene, 'scene');
+            $scene_url = getProp($scene, 'url');
+            if (!empty($scene_name) && !empty($scene_url)) {
+                $scene_map[$scene_name] = $scene_url;
+            }
+        }
+
+        $segment_id = $segment->segment_id;
+        $segment_content = $segment->segment_content;
+        
+        if (empty($segment_content)) {
+            Utils::throwError('20003:分镜内容不存在,请填写内容后重试');
+        }
+
+        try {
+            // 分镜剧本数组
+            $update_data = [
+                'segment_content' => $segment_content,
+                'pic_task_status' => '生成中',
+                'updated_at' => date('Y-m-d H:i:s')
+            ];
+
+            // 解析分镜内容,提取画面描述作为主要提示词
+            $prompt = $segment_content;
+            $ref_img_urls = [];
+            // 分镜场景
+            $scene = getProp($data, 'scene');
+            if ($scene) {
+                $matched_scene = '';
+                if (isset($scene_map[$scene])) {
+                    $img_index = count($ref_img_urls) + 1;
+                    $prompt = "场景:$scene(图$img_index)\n$prompt";
+                    $ref_img_urls[] = $scene_map[$scene];
+                    $matched_scene = $scene;
+                }
+                $update_data['scene'] = $scene;
+            }
+
+            // 分镜角色
+            $characters = getProp($data, 'characters');
+            if ($characters) {
+                $update_data['characters'] = $characters;
+                $characters = str_replace(['、', ',', ';'], ',', $characters);
+                $characters = explode(',', $characters);
+                
+                // 从分镜内容中提取涉及的角色
+                $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $characters);
+                
+                // 获取匹配角色的参考图
+                foreach ($segment_characters as $character) {
+                    if (isset($role_map[$character])) {
+                        // 将$prompt中出现的角色替换成角色(图1、2、3,序号是$ref_img_urls的个数加1)
+                        $img_index = count($ref_img_urls) + 1;
+                        $prompt = str_replace($character, $character . '(图' . $img_index . ')', $prompt);
+                        $ref_img_urls[] = $role_map[$character];
+                    }
+                }
+            }
+            
+            
+            // 如果没有找到匹配的角色参考图,不使用参考图
+            if (empty($ref_img_urls)) {
+                $ref_img_urls = [];
+            }
+
+            // 准备生成任务参数
+            $params = [
+                'alias_segment_id' => $segment_id,
+                'prompt' => $prompt,
+                'ref_img_urls' => $ref_img_urls,
+                'width' => $width,
+                'height' => $height,
+            ];
+            
+            // 调用AI图片生成服务
+            $task = $this->aiImageGenerationService->createImageGenerationTask($params);
+            $task_id = $task->id;
+            
+            if (!$task_id) {
+                Utils::throwError('20003:创建生成任务失败!');
+            }
+            $update_data['pic_task_id'] = $task_id;
+            
+
+            // 更新分镜剧本信息
+            $this->handleEpisodeContent($update_data);
+            $boolen = DB::table('mp_episode_segments')
+                ->where('segment_id', $segment_id)
+                ->update($update_data);
+
+            // 如果是第一集的首帧图片,则存入待更新数组
+            if ($boolen && (int)getProp($segment, 'episode_number') === 1 && (int)getProp($segment, 'segment_number') === 1) {
+                Redis::sadd('anime_first_frame_urls', $segment_id);
+            }
+            
+        } catch (\Exception $e) {
+            $failed_segments[] = [
+                'segment_id' => $segment_id,
+                'error' => $e->getMessage()
+            ];
+        }
+
+        $img_url = $this->loopGetPicTaskResult($task_id);
+        if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
+        return $img_url;
+    }
+
+    // 轮训获取图片任务结果
+    private function loopGetPicTaskResult($task_id) {
+        $start_count = 0;
+        $img_url = '';
+        while ($start_count <= 30) {
+            sleep(3);
+
+            $task = MpGeneratePicTask::where('id', $task_id)->first();
+            $statusInfo = $this->aiImageGenerationService->queryTaskStatus($task);
+
+            if ($statusInfo['status'] === 'success') {
+                $task->updateStatus('success', [
+                    'result_url'    => $statusInfo['result_url'],
+                    'result_json'   => $statusInfo['result_json'] ?? []
+                ]);
+
+                // 同步调整分镜图片状态和结果
+                if (getProp($task, 'alias_segment_id')) {
+                    DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
+                        'img_url'   => $statusInfo['result_url'][0],
+                        'pic_task_status'   => '已完成',
+                    ]);
+                }
+                $img_url = $statusInfo['result_url'][0];
+                break;
+            } elseif ($statusInfo['status'] === 'failed') {
+                $task->updateStatus('failed', [
+                    'error_message' => $statusInfo['error_message'],
+                    'result_json'   => $statusInfo['result_json'] ?? []
+                ]);
+
+                if (getProp($task, 'alias_segment_id')) {
+                    DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
+                        'pic_task_status'   => '失败',
+                    ]);
+                }
+                break;
+            }
+            $start_count+=3;
+        }
+
+        return $img_url;
+    }
+
     /**
     /**
      * 从分镜内容中提取涉及的角色
      * 从分镜内容中提取涉及的角色
      */
      */
@@ -723,4 +942,58 @@ class AnimeService
         
         
         return array_unique($found_characters);
         return array_unique($found_characters);
     }
     }
+
+    // 从分镜剧本中提取关键字段
+    private function handleEpisodeContent(&$data) {
+        $segmentContent = getProp($data, 'segment_content');
+        // 解析分镜详细信息
+        $segmentData = [
+            'description'     => '',
+            'composition'     => '',
+            'camera_movement' => '',
+            'voice_actor'     => '',
+            'dialogue'        => '',
+            'frame_type'      => '',
+            'scene'           => '',  // 新增:场景
+            'characters'      => ''   // 新增:出镜角色
+        ];
+        
+        // 提取各个字段 - 兼容中文冒号和英文冒号,支持多种表达方式
+        if (preg_match('/(?:画面描述|镜头描述|场景描述)[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
+            $segmentData['description'] = trim($descMatch[1]);
+            $data['description'] = trim($descMatch[1]);
+        }
+        if (preg_match('/(?:构图设计|构图|镜头构图)[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
+            $segmentData['composition'] = trim($compMatch[1]);
+            $data['composition'] = trim($compMatch[1]);
+        }
+        if (preg_match('/(?:运镜调度|运镜|镜头运动|摄影机运动)[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
+            $segmentData['camera_movement'] = trim($cameraMatch[1]);
+            $data['camera_movement'] = trim($cameraMatch[1]);
+        }
+        if (preg_match('/(?:配音角色|配音|角色|声优)[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
+            $segmentData['voice_actor'] = trim($voiceMatch[1]);
+            $data['voice_actor'] = trim($voiceMatch[1]);
+        }
+        if (preg_match('/(?:台词内容|台词|对白|对话)[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) {
+            $segmentData['dialogue'] = trim($dialogueMatch[1]);
+            $data['dialogue'] = trim($dialogueMatch[1]);
+        }
+        if (preg_match('/(?:画面类型|镜头类型|类型)[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
+            $segmentData['frame_type'] = trim($frameMatch[1]);
+            $data['frame_type'] = trim($frameMatch[1]);
+        }
+        // 新增:场景字段
+        if (preg_match('/(?:场景|拍摄场景|背景场景|环境)[::]\s*([^\n]+)/u', $segmentContent, $sceneMatch)) {
+            $segmentData['scene'] = trim($sceneMatch[1]);
+            $data['scene'] = trim($sceneMatch[1]);
+        }
+        // 新增:出镜角色字段
+        if (preg_match('/(?:出镜角色|角色出镜|登场角色|人物)[::]\s*([^\n]+)/u', $segmentContent, $charactersMatch)) {
+            $segmentData['characters'] = trim($charactersMatch[1]);
+            $data['characters'] = trim($charactersMatch[1]);
+        }
+
+        return $segmentData;
+    }
 }
 }

File diff suppressed because it is too large
+ 163 - 13
app/Services/DeepSeek/DeepSeekService.php


+ 1 - 0
routes/api.php

@@ -143,6 +143,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
         Route::post('batchSetSceneImg', [AnimeController::class, 'batchSetSceneImg']);
         Route::post('batchSetSceneImg', [AnimeController::class, 'batchSetSceneImg']);
         Route::post('chatChangeImg', [AnimeController::class, 'chatChangeImg']);
         Route::post('chatChangeImg', [AnimeController::class, 'chatChangeImg']);
         Route::post('batchSetSegmentTasks', [AnimeController::class, 'batchSetSegmentTasks']);
         Route::post('batchSetSegmentTasks', [AnimeController::class, 'batchSetSegmentTasks']);
+        Route::post('reGenerateSegment', [AnimeController::class, 'reGenerateSegment']);
     });
     });
     
     
 });
 });