Prechádzať zdrojové kódy

调整分镜剧本的表结构,新增接口

lh 4 mesiacov pred
rodič
commit
7ee98715d4

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

@@ -104,6 +104,14 @@ class AnimeController extends BaseController
         return $this->success(['success' => $result ? 1 : 0]);
     }
 
+    // 对话改图
+    public function chatChangeImg(Request $request) {
+        $data = $request->all();
+
+        $result = $this->AnimeService->chatChangeImg($data);
+        return $this->success(['img_url' => $result]);
+    }
+
     // public function scriptList(Request $request) {
     //     $data = $request->all();
     //     $result = $this->AnimeService->scriptList($data);

+ 8 - 0
app/Http/Controllers/DeepSeek/DeepSeekController.php

@@ -533,6 +533,14 @@ class DeepSeekController extends BaseController
         ]);
     }
 
+    // 对话改图
+    public function chatChangeImg(Request $request) {
+        $data = $request->all();
+
+        $result = $this->deepseekService->chatChangeImg($data);
+        return $this->success($result);
+    }
+
     public function testChat(Request $request) {
         $data = $request->all();
         $this->deepseekService->chat($data);

+ 28 - 0
app/Libs/Helpers.php

@@ -2645,11 +2645,39 @@ function handleEpisodeContent($originalContent) {
                 $segmentNumber = intval($segmentMatch[1]);
                 $segmentContent = trim($segmentMatch[2]);
                 
+                // 解析分镜详细信息
                 $segmentData = [
+                    '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' => '',
+                    'camera_movement' => '',
+                    'voice_actor' => '',
+                    'dialogue' => '',
+                    'frame_type' => ''
                 ];
                 
+                // 提取各个字段 - 兼容中文冒号和英文冒号
+                if (preg_match('/画面描述[::]\s*([^\n]+)/u', $segmentContent, $descMatch)) {
+                    $segmentData['description'] = trim($descMatch[1]);
+                }
+                if (preg_match('/构图设计[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
+                    $segmentData['composition'] = trim($compMatch[1]);
+                }
+                if (preg_match('/运镜调度[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
+                    $segmentData['camera_movement'] = trim($cameraMatch[1]);
+                }
+                if (preg_match('/配音角色[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
+                    $segmentData['voice_actor'] = trim($voiceMatch[1]);
+                }
+                if (preg_match('/台词内容[::]\s*([^\n]+)/u', $segmentContent, $dialogueMatch)) {
+                    $segmentData['dialogue'] = trim($dialogueMatch[1]);
+                }
+                if (preg_match('/画面类型[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
+                    $segmentData['frame_type'] = trim($frameMatch[1]);
+                }
+                
                 $segments[] = $segmentData;
             }
             

+ 1 - 0
app/Models/MpGeneratePicTask.php

@@ -11,6 +11,7 @@ class MpGeneratePicTask extends Model
 
     protected $fillable = [
         'task_id',
+        'alias_segment_id',
         'prompt',
         'width',
         'height',

+ 16 - 0
app/Services/AIGeneration/AIImageGenerationService.php

@@ -6,6 +6,7 @@ use App\Models\MpGeneratePicTask;
 use App\Services\VolcEngineService;
 use GuzzleHttp\Client;
 use GuzzleHttp\Exception\GuzzleException;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 class AIImageGenerationService
@@ -42,6 +43,7 @@ class AIImageGenerationService
         // 创建任务记录
         $task = MpGeneratePicTask::create([
             'task_id' => $taskId,
+            'alias_segment_id' => $params['alias_segment_id'] ?? 0,
             'prompt' => $params['prompt'] ?? '',
             'width' => $params['width'] ?? 2048,
             'height' => $params['height'] ?? 2048,
@@ -333,11 +335,25 @@ class AIImageGenerationService
                     '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'   => '已完成',
+                    ]);
+                }
             } elseif ($statusInfo['status'] === 'failed') {
                 $task->updateStatus(MpGeneratePicTask::STATUS_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'   => '失败',
+                    ]);
+                }
             }
             // 如果仍然是处理中状态,不做任何操作
             

+ 129 - 7
app/Services/Anime/AnimeService.php

@@ -5,6 +5,7 @@ namespace App\Services\Anime;
 use App\Consts\ErrorConst;
 use App\Facade\Site;
 use App\Libs\Utils;
+use App\Models\MpGeneratePicTask;
 use App\Services\AIGeneration\AIImageGenerationService;
 use Dflydev\DotAccessData\Util;
 use GuzzleHttp\Client;
@@ -45,13 +46,13 @@ class AnimeService
             $role_name = getProp($role, 'role');
             $description = getProp($role, 'description');
             // 参考图地址
-            $ref_img_url = getProp($role, 'ref_img_url');
+            $ref_img_url = getProp($role, 'url');
             
             // 此处调用AIImageGenerationService的生成图片任务接口: createImageGenerationTask(注意使用默认参数,prompt使用角色描述),并返回任务id
             try {
                 $params = [
                     'prompt' => $description,
-                    'ref_img_urls' => !empty($ref_img_url) ? $ref_img_url : []
+                    'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
                 ];
                 
                 $task = $this->aiImageGenerationService->createImageGenerationTask($params);
@@ -99,7 +100,7 @@ class AnimeService
             try {
                 $params = [
                     'prompt' => $description,
-                    'ref_img_urls' => !empty($ref_img_url) ? $ref_img_url : []
+                    'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
                 ];
                 
                 $task = $this->aiImageGenerationService->createImageGenerationTask($params);
@@ -238,12 +239,40 @@ class AnimeService
             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', 'title', 'intro', 'art_style', 'roles', 'scenes', 'acts')->first();
+        $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', 'title', 'intro', 'art_style', 'roles', 'scenes')->first();
         if (!$episode) Utils::throwError('20003: 该剧集不存在!');
         $episode = (array)$episode;
         $episode['roles'] = json_decode($episode['roles'], true);
         $episode['scenes'] = json_decode($episode['scenes'], true);
-        $episode['acts'] = json_decode($episode['acts'], true);
+        
+        // 获取分镜信息
+        $segments = DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', getProp($episode, 'episode_id'))->orderBy('segment_number')->select('act_number', 'act_title', 'segment_id', 'segment_number', 'segment_content')->get()->map(function ($value) {
+            return (array)$value;
+        })->toArray();
+        
+        $acts = [];
+        foreach($segments as $segment) {
+            if (isset($acts[getProp($segment, 'act_number')])) {
+                $acts[getProp($segment, 'act_number')]['segments'][] = [
+                    'segment_id'    => getProp($segment, 'segment_id'),
+                    'segment_number'    => getProp($segment, 'segment_number'),
+                    'segment_content'    => getProp($segment, 'segment_content'),
+                ];
+            }else {
+                $acts[getProp($segment, 'act_number')]= [
+                    'act_number' => getProp($segment, 'act_number'),
+                    'act_title' => getProp($segment, 'act_title'),
+                    'segments' => [
+                        [
+                            'segment_id'    => getProp($segment, 'segment_id'),
+                            'segment_number'    => getProp($segment, 'segment_number'),
+                            'segment_content'    => getProp($segment, 'segment_content'),
+                        ]
+                    ]
+                ];
+            }
+        }
+        $episode['acts'] = array_values($acts);
 
         return $episode;
     }
@@ -277,6 +306,22 @@ class AnimeService
                 Utils::throwError('20003:创建副本失败!');
             }
             $episode['episode_id'] = $new_episode_id;
+
+            // 获取分镜数据
+            $segments = DB::table('mp_episode_segments')->where('episode_id', $episode_id)->get()->map(function($item) use ($new_episode_id) {
+                $item = (array)$item;
+                unset($item['id']);
+                $item['episode_id'] = $new_episode_id;
+                $item['segment_id'] = date('YmdHis').mt_rand(1000,9999).str_pad($item['segment_number'], 3, "0", STR_PAD_LEFT);
+                $item['created_at'] = $item['updated_at'] = date('Y-m-d H:i:s');
+                return $item;
+            })->toArray();
+
+            // 写入分镜数据
+            $boolen2 = DB::table('mp_episode_segments')->insert($segments);
+            if (!$boolen2) {
+                Utils::throwError('20003:写入分镜数据失败!');
+            }
         }catch (\Exception $e) {
             DB::rollBack();
             Utils::throwError('20003:'.$e->getMessage());
@@ -294,7 +339,7 @@ class AnimeService
             Utils::throwError('1002:请选择剧集');
         }
 
-        $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->select('anime_id', 'episode_number', 'sequence', 'title', 'intro', 'art_style', 'roles', 'scenes', 'acts')->get()->map(function($value) {
+        $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->select('anime_id', 'episode_number', 'sequence', 'title', 'intro', 'art_style', 'roles', 'scenes')->get()->map(function($value) {
             return (array)$value;
         })->toArray();
         if (!$episodes) Utils::throwError('20003: 该剧集不存在!');
@@ -302,7 +347,6 @@ class AnimeService
             $episode['version'] = 'V'.$episode['sequence'];
             $episode['roles'] = json_decode($episode['roles'], true);
             $episode['scenes'] = json_decode($episode['scenes'], true);
-            $episode['acts'] = json_decode($episode['acts'], true);
         }
 
         return $episodes;
@@ -339,4 +383,82 @@ class AnimeService
 
         return true;
     }
+
+    public function chatChangeImg($data) {
+        $segment_id = getProp($data, 'segment_id');
+        $prompt = getProp($data, 'prompt');
+        if (!$prompt || !$segment_id) {
+            Utils::throwError('1002:请输入提示词,并且选择需修改的图片');
+        }
+        $segment = DB::table("mp_episode_segments")->where('segment_id', $segment_id)->first();
+        if (!$segment) Utils::throwError('20003:该分镜不存在!');
+        $ref_img_url = getProp($segment, 'img_url');
+        if (empty($ref_img_url)) {
+            $prompt = getProp($segment, 'segment_content');
+        }
+
+        try {
+            $params = [
+                'alias_segment_id' => $segment_id,
+                'prompt' => $prompt,
+                'ref_img_urls' => !empty($ref_img_url) ? [$ref_img_url] : []
+            ];
+            
+            $task = $this->aiImageGenerationService->createImageGenerationTask($params);
+            $task_id = $task->id;
+            
+            if (!$task_id) {
+                Utils::throwError("20003: 生成图片失败");
+            }
+            
+            // 更新任务ID
+            $boolen = 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 (!$boolen) Utils::throwError('20003:更新任务ID失败');
+        } catch (\Exception $e) {
+            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;
+        }
+
+        if (!$img_url) Utils::throwError('20003:图片生成中,请稍后刷新');
+        return $img_url;
+        
+    }
 }

+ 107 - 35
app/Services/DeepSeek/DeepSeekService.php

@@ -2301,6 +2301,7 @@ EOD;
                     4.2“分镜信息”需同一场景内的对话内容完成分镜(每一幕分镜尽量不得低于5个),需包含“画面描述”、“构图设计”、“运镜调度”、“配音音色”、“台词内容”和“画面类型”这几个部分,其中“画面描述”如果出现主体,只能选择上述主体列表中的一个,主体的名称也需要保持一致(不带主体具体描述)。
                     4.3分镜序号用1、2、3显示,如分镜1、分镜2,并且分镜序号需连续(跨分幕中的分镜序号需连续,但跨剧集中的不需要连续,如第2集的第1幕分镜序号从1开始)。
                     4.4“分镜信息”中的“台词内容”需与原文中的台词完全一致,不得修改或添加任何内容。
+                    4.5“分镜信息”中的“配音角色”需对应本集中的<主体列表>中的一个(非对话内容则统一固定为“旁白”角色),不得自己生成。
                 示例格式:\n\n
                 第1集:隐形的守护者
 
@@ -2541,35 +2542,6 @@ EOD;
             DB::beginTransaction();
 
             // 新建动漫剧集
-            // // 删除历史分集内容
-            // DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->delete();
-            
-            // $episodes  = [];
-            // $sequence = 1;
-            // foreach($episode_arr['acts'] as $act) {
-            //     foreach($act['segments'] as $segment) {
-            //         $episodes[] = [
-            //             'anime_id' => $anime_id,
-            //             'episode_number' => $episode_number,
-            //             'title' => $episode_arr['episode_title'],
-            //             // 'content' => $episode['content'],
-            //             'content'   => '',
-            //             'intro' => $episode_arr['intro'],
-            //             'art_style' => $episode_arr['art_style'],
-            //             'act_number' => $act['act_number'],
-            //             'act_title' => $act['act_title'],
-            //             'roles' => json_encode($episode_arr['roles'], 256),
-            //             'scenes' => json_encode($episode_arr['scenes'], 256),
-            //             'segment_number' => $segment['segment_number'],
-            //             'segment_content' => $segment['segment_content'],
-            //             'sequence' => $sequence,
-            //             'created_at' => date('Y-m-d H:i:s'),
-            //             'updated_at' => date('Y-m-d H:i:s')
-            //         ];
-            //         $sequence++;
-            //     }
-            // }
-
             $episode = [
                 'anime_id' => $anime_id,
                 'episode_number' => $episode_number,
@@ -2580,19 +2552,20 @@ EOD;
                 'art_style' => $episode_arr['art_style'],
                 'roles' => json_encode($episode_arr['roles'], 256),
                 'scenes' => json_encode($episode_arr['scenes'], 256),
-                'acts' => json_encode($episode_arr['acts'], 256),
                 'sequence' => 1,
                 'generate_status' => '待执行',
                 'generate_at' => date('Y-m-d H:i:s'),
                 'is_default' => 1,
-                'created_at' => date('Y-m-d H:i:s'),
+                'updated_at' => date('Y-m-d H:i:s'),
             ];
 
-            $episode_id = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->value('id');
-            if ($episode_id) {
-                $episode['updated_at'] = date('Y-m-d H:i:s');
-                $boolen2 = DB::table('mp_anime_episodes')->where('id', $episode_id)->update($episode);
+            $current_episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->select('id', 'sequence')->first();
+            if ($current_episode) {
+                $episode['sequence'] = getProp($current_episode, 'sequence');
+                $episode_id = getProp($current_episode, 'id');
+                $boolen2 = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('sequence', $episode['sequence'])->update($episode);
             }else {
+                $episode['created_at'] = date('Y-m-d H:i:s');
                 $episode_id = DB::table('mp_anime_episodes')->insertGetId($episode);
                 $boolen2 = $episode_id ? true : false;
             }
@@ -2602,6 +2575,38 @@ EOD;
                 Utils::throwError('20003:分集内容保存失败');
             }
 
+             // 删除历史分集内容
+            DB::table('mp_episode_segments')->where('anime_id', $anime_id)->where('episode_id', $episode_id)->delete();
+            $segments = [];
+            foreach($episode_arr['acts'] as $act) {
+                foreach($act['segments'] as $segment) {
+                    $segments[] = [
+                        'anime_id' => $anime_id,
+                        'episode_id' => $episode_id,
+                        'episode_number' => $episode_number,
+                        'act_number' => $act['act_number'],
+                        'act_title' => $act['act_title'],
+                        'segment_id' => getProp($segment, 'segment_id'),
+                        'segment_number' => getProp($segment, 'segment_number'),
+                        'segment_content' => getProp($segment, 'segment_content'),
+                        'description' => getProp($segment, 'description'),
+                        'composition' => getProp($segment, 'composition'),
+                        'camera_movement' => getProp($segment, 'camera_movement'),
+                        'voice_actor' => getProp($segment, 'voice_actor'),
+                        'dialogue' => getProp($segment, 'dialogue'),
+                        'frame_type' => getProp($segment, 'frame_type'),
+                        'created_at' => date('Y-m-d H:i:s'),
+                        'updated_at' => date('Y-m-d H:i:s')
+                    ];
+                }
+            }
+
+            $boolen3 = DB::table('mp_episode_segments')->insert($segments);
+            if (!$boolen3) {
+                dLog('deepseek')->info('分镜内容保存失败', $episode);
+                Utils::throwError('20003:分镜内容保存失败');
+            }
+
             // 保存对话记录
             $recrods = [
                 [
@@ -2656,6 +2661,73 @@ EOD;
         ];
     }
 
+    public function chatChangeImg($data) {
+        $segment = getProp($data, 'segment');
+        $segment_content = getProp($segment, 'segment_content');
+        $prompt = getProp($data, 'prompt');
+        $ref_img = getProp($data, 'ref_img');
+        $question = "请修改已有的内容{$segment_content}\n修改要求如下:\n{$prompt}";
+
+        $model = getProp($data, 'model', 'r1');
+        $model = $model == 'r1' ? 'deepseek-reasoner' : 'deepseek-chat';
+
+        $messages[] = 
+            [
+                'role'    => 'user',
+                'content' => $question
+            ];
+
+        $post_data = [
+            'model'             => $model,
+            'messages'          => $messages,
+            'max_tokens'        => 8192,
+            'temperature'       => 0.7,
+            'frequency_penalty' => 0,
+            'presence_penalty'  => 0,
+            'response_format'   => ['type' => 'text'],
+            'stream'            => false  // 是否启用流式输出
+        ];
+
+        // 调用流式输出的deepseek
+        $chatResult = $this->chatOnly($post_data);
+        if (is_array($chatResult)) {
+            extract($chatResult);
+        }else {
+            Utils::throwError('20003: 接口调用失败!');
+        }
+
+        return [
+            'type' => 'done',
+            // 'episode' => $episode,
+            'answer' => $fullContent,
+            'reasoning' => $fullReasoningContent,
+            'usage' => $usage
+        ];
+        
+    }
+
+    // 仅调用deepseek对话接口
+    private function chatOnly($post_data) {
+        $client = new Client(['timeout' => 1200, 'verify' => false]);
+        $result = $client->post($this->url, ['json' => $post_data, 'headers' => $this->headers]);
+        $response = $result->getBody()->getContents();
+        $response_arr = json_decode($response, true);
+        dLog('deepseek')->info('deepseek请求完成', ['response' => $response_arr]);
+        $fullContent = '';
+        $fullReasoningContent = [];
+        $usage = isset($response_arr['usage']) ? $response_arr['usage'] : [];
+        if (isset($response_arr['choices']) && count($response_arr['choices']) > 0) {
+            $fullContent = isset($response_arr['choices'][0]['message']['content']) ? $response_arr['choices'][0]['message']['content'] : '';
+            $fullReasoningContent = isset($response_arr['choices'][0]['message']['reasoning_content']) ? $response_arr['choices'][0]['message']['reasoning_content'] : '';
+        }
+
+        return [
+            'fullContent' => $fullContent,
+            'fullReasoningContent' => $fullReasoningContent,
+            'usage' => $usage
+        ];
+    }
+
     private function splitContent($content) {
         // 根据章节标题来拆分章节内容,返回包含标题和内容的二维数组
         $chapters = [];

+ 1 - 0
routes/api.php

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