فهرست منبع

新增一键生成剧集中的分镜图片接口

lh 5 ماه پیش
والد
کامیت
a579147771

+ 5 - 5
app/Console/Anime/AnimeCheckImgUrlCommand.php

@@ -29,14 +29,14 @@ class AnimeCheckImgUrlCommand extends Command
     {
         dLog('generate')->info('====================开始检查动漫对话角色和场景图片生成状态====================');
         $time_expire_datetime = date('Y-m-d H:i:s', strtotime('-12 hours'));
-        DB::table('mp_anime_episodes')->where('generate_status', '执行中')->where('generate_at', '<', $time_expire_datetime)->update([
+        DB::table('mp_animes')->where('generate_status', '执行中')->where('generate_at', '<', $time_expire_datetime)->update([
             'generate_status' => '失败',
             'updated_at'=>date('Y-m-d H:i:s'),
         ]);
         $time_start = microtime(true);
 
         // 获取待执行的动漫对话数据
-        $data = DB::table('mp_anime_episodes')->where('generate_status', '执行中')->select('id', 'roles', 'scenes')->get();
+        $data = DB::table('mp_animes')->where('generate_status', '执行中')->select('id', 'roles', 'scenes')->get();
         
         foreach ($data as $item) {
             $id = getProp($item, 'id');
@@ -55,7 +55,7 @@ class AnimeCheckImgUrlCommand extends Command
 
             if ($role_success_count == $role_count && $scene_success_count == $scene_count) {
                 // 更新状态为已完成
-                DB::table('mp_anime_episodes')->where('id', $id)->update(['generate_status' => '已完成', 'updated_at'=>date('Y-m-d H:i:s')]);
+                DB::table('mp_animes')->where('id', $id)->update(['generate_status' => '已完成', 'updated_at'=>date('Y-m-d H:i:s')]);
                 dLog('generate')->info('动漫对话图片已全部生成完毕');
             }
         }
@@ -88,7 +88,7 @@ class AnimeCheckImgUrlCommand extends Command
         }
 
         if ($role_success_count > 0) {
-            DB::table('mp_anime_episodes')->where('id', $id)->update(['roles' => json_encode($roles, 256)]);
+            DB::table('mp_animes')->where('id', $id)->update(['roles' => json_encode($roles, 256)]);
         }
 
         return $role_success_count;
@@ -117,7 +117,7 @@ class AnimeCheckImgUrlCommand extends Command
         }
 
         if ($scene_success_count > 0) {
-            DB::table('mp_anime_episodes')->where('id', $id)->update(['scenes' => json_encode($scenes, 256)]);
+            DB::table('mp_animes')->where('id', $id)->update(['scenes' => json_encode($scenes, 256)]);
         }
 
         return $scene_success_count;

+ 1 - 0
app/Console/Test/TestCommand.php

@@ -50,6 +50,7 @@ class TestCommand extends Command
      */
     public function handle()
     {
+        handleEpisodeContent('');
         // dd(testStoryboardParsing(''));
         // dd(handleEpisodeContent(''));
         

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

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

+ 32 - 14
app/Libs/Helpers.php

@@ -2503,34 +2503,42 @@ function handleEpisodeContent($originalContent) {
 // ##第1幕:徐家老旧厨房 白天 室内
 // 分镜1
 // 画面描述:许芸-校服装站在厨房门口,书包背在肩上,徐母手持扫帚,正对着许芸的背部猛烈挥打。卞大伟站在锅炉后方,双手抱胸,阴沉的目光落在许芸身上。
+// 场景:徐家老旧厨房
 // 构图设计:全景,低角度仰视
 // 运镜调度:手持拍摄
 // 配音角色:旁白
+// 出镜角色:许芸-校服装、徐母
 // 台词内容:我一到家,徐母的扫帚就落在我身上,嘴里一个劲地骂我,
 // 画面类型:普通画面
 
 // 分镜2
 // 画面描述:徐母面容扭曲,嘴唇快速开合,扫帚挥舞在空中。
+// 场景:徐家老旧厨房
 // 构图设计:近景,徐母面部特写
 // 运镜调度:固定镜头
 // 配音角色:徐母
+// 出镜角色:徐母
 // 台词内容:问我夜不归宿死哪儿去了。
 // 画面类型:对口型
 
 // ##第2幕:徐家简陋客厅 黄昏 室内
 // 分镜3
 // 画面描述:客厅窗户透出昏黄的夕阳余晖,空荡荡的客厅中央只有几把木凳和一张方桌,显得异常安静。
+// 场景:徐家简陋客厅
 // 构图设计:全景,景深虚化
 // 运镜调度:固定镜头
 // 配音角色:旁白
+// 出镜角色:无
 // 台词内容:可我没想到,半个月后,徐母居然再一次外出,
 // 画面类型:普通画面
 
 // 分镜4
 // 画面描述:一张泛黄的火车票特写,上面显示着隔壁市的站名和日期。
+// 场景:徐家简陋客厅
 // 构图设计:特写,火车票
 // 运镜调度:固定镜头
 // 配音角色:旁白
+// 出镜角色:无
 // 台词内容:这次是去隔壁市看望她生病的好姐妹。
 // 画面类型:普通画面
 // \n\n';
@@ -2647,36 +2655,46 @@ function handleEpisodeContent($originalContent) {
                 
                 // 解析分镜详细信息
                 $segmentData = [
-                    'segment_id'    => date('YmdHis').mt_rand(1000,9999).str_pad($segmentNumber, 3, "0", STR_PAD_LEFT),   //生成唯一ID(后续可在生成任务里查看历史版本)
-                    'segment_number' => $segmentNumber,
+                    'segment_id'      => date('YmdHis') . mt_rand(1000, 9999) . str_pad($segmentNumber, 3, "0", STR_PAD_LEFT), // 生成唯一ID(后续可在生成任务里查看历史版本)
+                    'segment_number'  => $segmentNumber,
                     'segment_content' => $segmentContent,
-                    'description' => '',
-                    'composition' => '',
+                    'description'     => '',
+                    'composition'     => '',
                     'camera_movement' => '',
-                    'voice_actor' => '',
-                    'dialogue' => '',
-                    'frame_type' => ''
+                    'voice_actor'     => '',
+                    'dialogue'        => '',
+                    'frame_type'      => '',
+                    'scene'           => '',  // 新增:场景
+                    'characters'      => ''   // 新增:出镜角色
                 ];
                 
-                // 提取各个字段 - 兼容中文冒号和英文冒号
-                if (preg_match('/画面描述[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
+                // 提取各个字段 - 兼容中文冒号和英文冒号,支持多种表达方式
+                if (preg_match('/(?:画面描述|镜头描述|场景描述)[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
                     $segmentData['description'] = trim($descMatch[1]);
                 }
-                if (preg_match('/构图设计[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
+                if (preg_match('/(?:构图设计|构图|镜头构图)[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
                     $segmentData['composition'] = trim($compMatch[1]);
                 }
-                if (preg_match('/运镜调度[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
+                if (preg_match('/(?:运镜调度|运镜|镜头运动|摄影机运动)[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
                     $segmentData['camera_movement'] = trim($cameraMatch[1]);
                 }
-                if (preg_match('/配音角色[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
+                if (preg_match('/(?:配音角色|配音|角色|声优)[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
                     $segmentData['voice_actor'] = trim($voiceMatch[1]);
                 }
-                if (preg_match('/台词内容[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) {
+                if (preg_match('/(?:台词内容|台词|对白|对话)[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) {
                     $segmentData['dialogue'] = trim($dialogueMatch[1]);
                 }
-                if (preg_match('/画面类型[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
+                if (preg_match('/(?:画面类型|镜头类型|类型)[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
                     $segmentData['frame_type'] = trim($frameMatch[1]);
                 }
+                // 新增:场景字段
+                if (preg_match('/(?:场景|拍摄场景|背景场景|环境)[::]\s*([^\n]+)/u', $segmentContent, $sceneMatch)) {
+                    $segmentData['scene'] = trim($sceneMatch[1]);
+                }
+                // 新增:出镜角色字段
+                if (preg_match('/(?:出镜角色|角色出镜|登场角色|人物)[::]\s*([^\n]+)/u', $segmentContent, $charactersMatch)) {
+                    $segmentData['characters'] = trim($charactersMatch[1]);
+                }
                 
                 $segments[] = $segmentData;
             }

+ 236 - 10
app/Services/Anime/AnimeService.php

@@ -32,15 +32,12 @@ class AnimeService
         ];
     }
 
-    public function uploadImg($data) {
-
-    }
-
     public function batchSetRoleImg($data) {
         $anime_id = getProp($data, 'anime_id');
         $episode_number = getProp($data, 'episode_number');
         if (!$anime_id) Utils::throwError('1002:请选择对话');
-        $roles = getProp($data, 'roles');
+        $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
+        $roles = json_decode(getProp($anime, 'roles'), true);
         
         foreach ($roles as &$role) {
             $role_name = getProp($role, 'role');
@@ -70,10 +67,11 @@ class AnimeService
         }
         
         // 保存角色信息
-        $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
+        // $boolen = DB::table('mp_animes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
+        $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
             'roles'             => json_encode($roles, 256),
             'generate_status'   => '执行中',
-            'generate_status'   => date('Y-m-d H:i:s'),
+            'generate_at'       => date('Y-m-d H:i:s'),
             'updated_at'        => date('Y-m-d H:i:s')
         ]);
         if (!$boolen) {
@@ -88,7 +86,8 @@ class AnimeService
         $anime_id = getProp($data, 'anime_id');
         if (!$anime_id) Utils::throwError('1002:请选择对话');
         $episode_number = getProp($data, 'episode_number');
-        $scenes = getProp($data, 'scenes');
+        $anime = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
+        $scenes = json_decode(getProp($anime, 'scenes'), true);
         
         foreach ($scenes as &$scene) {
             $scene_name = getProp($scene, 'scene');
@@ -118,10 +117,11 @@ class AnimeService
         }
         
         // 保存角色信息
-        $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
+        // $boolen = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->update([
+        $boolen = DB::table('mp_animes')->where('id', $anime_id)->update([
             'scenes'             => json_encode($scenes, 256),
             'generate_status'   => '执行中',
-            'generate_status'   => date('Y-m-d H:i:s'),
+            'generate_at'       => date('Y-m-d H:i:s'),
             'updated_at'        => date('Y-m-d H:i:s')
         ]);
         if (!$boolen) {
@@ -461,4 +461,230 @@ class AnimeService
         return $img_url;
         
     }
+
+    public function batchSetSegmentTasks($data) {
+        $anime_id = getProp($data, 'anime_id');
+        $episode_number = getProp($data, 'episode_number');
+        
+        if (!$anime_id || !$episode_number) {
+            Utils::throwError('1002:请选择剧集');
+        }
+
+        // 获取剧集信息
+        $episode = DB::table('mp_anime_episodes')
+            ->where('anime_id', $anime_id)
+            ->where('episode_number', $episode_number)
+            ->where('is_default', 1)
+            ->select('id as episode_id', 'anime_id', 'episode_number')
+            ->first();
+            
+        if (!$episode) {
+            Utils::throwError('20003:该剧集不存在');
+        }
+
+        // 获取动漫的角色信息(包含参考图)
+        $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)) {
+                // // 处理角色名称,去除装扮后缀(如:许芸-校服装 -> 许芸)- 修复中文乱码问题(弃用,容易产生重复图片)
+                // $base_name = preg_replace('/[--].*$/u', '', $role_name);
+                // $role_map[$base_name] = $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;
+            }
+        }
+
+        // 获取所有分镜信息
+        $segments = DB::table('mp_episode_segments')
+            ->where('anime_id', $anime_id)
+            ->where('episode_id', $episode->episode_id)
+            ->orderBy('segment_number')
+            ->select('segment_id', 'segment_number', 'segment_content', 'scene', 'characters')
+            ->limit(3)
+            ->get()
+            ->toArray();
+
+        if (empty($segments)) {
+            Utils::throwError('20003:未找到分镜信息');
+        }
+
+        $updated_segments = [];
+        $failed_segments = [];
+
+        // 批量为每个分镜生成图片任务
+        foreach ($segments as $segment) {
+            $segment_id = $segment->segment_id;
+            $segment_content = $segment->segment_content;
+            
+            if (empty($segment_content)) {
+                $failed_segments[] = [
+                    'segment_id' => $segment_id,
+                    'error' => '分镜内容为空'
+                ];
+                continue;
+            }
+
+            try {
+                // 解析分镜内容,提取画面描述作为主要提示词
+                $prompt = $segment_content;
+                $ref_img_urls = [];
+                // 分镜场景
+                $scene = getProp($segment, '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;
+                }
+
+                // 分镜角色
+                $characters = getProp($segment, 'characters');
+                $characters = str_replace(['、', ',', ';'], ',', $characters);
+                $roles = explode(',', $characters);
+                
+                // 从分镜内容中提取涉及的角色
+                $segment_characters = $this->extractCharactersFromSegment($segment_content, array_keys($role_map), $roles);
+                
+                // 获取匹配角色的参考图
+                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
+                ];
+                
+                // $task_id = mt_rand(100000, 999999);
+                // 调用AI图片生成服务
+                $task = $this->aiImageGenerationService->createImageGenerationTask($params);
+                $task_id = $task->id;
+                
+                if (!$task_id) {
+                    $failed_segments[] = [
+                        'segment_id' => $segment_id,
+                        'error' => '创建生成任务失败'
+                    ];
+                    continue;
+                }
+                
+                // 更新分镜的任务信息
+                $update_result = DB::table('mp_episode_segments')
+                    ->where('segment_id', $segment_id)
+                    ->update([
+                        'pic_task_id' => $task_id,
+                        'pic_task_status' => '生成中',
+                        'updated_at' => date('Y-m-d H:i:s')
+                    ]);
+                
+                if ($update_result) {
+                    $updated_segments[] = [
+                        'segment_id' => $segment_id,
+                        'task_id' => $task_id,
+                        'status' => '生成中',
+                        'matched_scenes' => $scene,
+                        'matched_roles' => $segment_characters,
+                        'ref_urls_count' => count($ref_img_urls)
+                    ];
+                } else {
+                    $failed_segments[] = [
+                        'segment_id' => $segment_id,
+                        'error' => '更新分镜任务状态失败'
+                    ];
+                }
+                
+            } catch (\Exception $e) {
+                $failed_segments[] = [
+                    'segment_id' => $segment_id,
+                    'error' => $e->getMessage()
+                ];
+            }
+        }
+
+        // // 更新剧集生成状态
+        // if (!empty($updated_segments)) {
+        //     DB::table('mp_anime_episodes')
+        //         ->where('id', $episode->episode_id)
+        //         ->update([
+        //             'generate_status' => '执行中',
+        //             'updated_at' => date('Y-m-d H:i:s')
+        //         ]);
+        // }
+
+        return [
+            'success_count' => count($updated_segments),
+            'failed_count' => count($failed_segments),
+            'success_segments' => $updated_segments,
+            'failed_segments' => $failed_segments,
+            'total_segments' => count($segments)
+        ];
+    }
+
+    /**
+     * 从分镜内容中提取涉及的角色
+     */
+    private function extractCharactersFromSegment($segment_content, $available_characters, $roles) {
+        $found_characters = [];
+        
+        foreach ($available_characters as $character) {
+            // 检查角色名称是否在分镜内容中出现
+            if (strpos($segment_content, $character) !== false) {
+                $found_characters[] = $character;
+            }
+        }
+
+        if (!$found_characters) {
+            foreach ($roles as $character) {
+                // 检查角色名称是否在分镜内容中出现
+                if (strpos($segment_content, $character) !== false) {
+                    $found_characters[] = $character;
+                }
+            }
+        }
+        
+        return array_unique($found_characters);
+    }
 }

+ 13 - 0
app/Services/DeepSeek/DeepSeekService.php

@@ -2302,6 +2302,8 @@ EOD;
                     4.3分镜序号用1、2、3显示,如分镜1、分镜2,并且分镜序号需连续(跨分幕中的分镜序号需连续,但跨剧集中的不需要连续,如第2集的第1幕分镜序号从1开始)。
                     4.4“分镜信息”中的“台词内容”需与原文中的台词完全一致,不得修改或添加任何内容。
                     4.5“分镜信息”中的“配音角色”需对应本集中的<主体列表>中的一个(非对话内容则统一固定为“旁白”角色),不得自己生成。
+                    4.6“分镜信息”中的“场景”需对应本集中的<场景列表>中的一个(不带详细表述)
+                    4.7“分镜信息”中的“出镜角色”需对应本集中的<主体列表>中的一个或多个(不带详细描述),如果没有出镜角色则填无
                 示例格式:\n\n
                 第1集:隐形的守护者
 
@@ -2330,34 +2332,42 @@ EOD;
                 ##第1幕:徐家老旧厨房 白天 室内
                 分镜1
                 画面描述:许芸-校服装站在厨房门口,书包背在肩上,徐母手持扫帚,正对着许芸的背部猛烈挥打。卞大伟站在锅炉后方,双手抱胸,阴沉的目光落在许芸身上。
+                场景:徐家老旧厨房
                 构图设计:全景,低角度仰视
                 运镜调度:手持拍摄
                 配音角色:旁白
+                出镜角色:许芸-校服装、徐母
                 台词内容:我一到家,徐母的扫帚就落在我身上,嘴里一个劲地骂我,
                 画面类型:普通画面
 
                 分镜2
                 画面描述:徐母面容扭曲,嘴唇快速开合,扫帚挥舞在空中。
+                场景:徐家老旧厨房
                 构图设计:近景,徐母面部特写
                 运镜调度:固定镜头
                 配音角色:徐母
+                出镜角色:徐母
                 台词内容:问我夜不归宿死哪儿去了。
                 画面类型:对口型
                 
                 ##第2幕:徐家简陋客厅 黄昏 室内
                 分镜3
                 画面描述:客厅窗户透出昏黄的夕阳余晖,空荡荡的客厅中央只有几把木凳和一张方桌,显得异常安静。
+                场景:徐家简陋客厅
                 构图设计:全景,景深虚化
                 运镜调度:固定镜头
                 配音角色:旁白
+                出镜角色:无
                 台词内容:可我没想到,半个月后,徐母居然再一次外出,
                 画面类型:普通画面
 
                 分镜4
                 画面描述:一张泛黄的火车票特写,上面显示着隔壁市的站名和日期。
+                场景:徐家简陋客厅
                 构图设计:特写,火车票
                 运镜调度:固定镜头
                 配音角色:旁白
+                出镜角色:无
                 台词内容:这次是去隔壁市看望她生病的好姐妹。
                 画面类型:普通画面
                 \n\n";
@@ -2595,6 +2605,8 @@ EOD;
                         'voice_actor' => getProp($segment, 'voice_actor'),
                         'dialogue' => getProp($segment, 'dialogue'),
                         'frame_type' => getProp($segment, 'frame_type'),
+                        'scene' => getProp($segment, 'scene'),
+                        'characters' => getProp($segment, 'characters'),
                         'created_at' => date('Y-m-d H:i:s'),
                         'updated_at' => date('Y-m-d H:i:s')
                     ];
@@ -2647,6 +2659,7 @@ EOD;
         }
         
         DB::commit();
+        // dd('end');
 
         $episode['episode_id'] = $episode_id;
         $episode['roles'] = json_decode($episode['roles'], true);

+ 1 - 0
routes/api.php

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