Просмотр исходного кода

优化提示词,优化剧集对话逻辑

lh 3 месяцев назад
Родитель
Сommit
af87a7e537
3 измененных файлов с 334 добавлено и 135 удалено
  1. 20 16
      app/Libs/Helpers.php
  2. 5 3
      app/Services/Anime/AnimeService.php
  3. 309 116
      app/Services/DeepSeek/DeepSeekService.php

Разница между файлами не показана из-за своего большого размера
+ 20 - 16
app/Libs/Helpers.php


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

@@ -55,11 +55,13 @@ class AnimeService
 
         if (!$anime_id) Utils::throwError('20003:请选择对话!');
 
-        $query = DB::table('mp_anime_records')->where('anime_id', $anime_id)->where('sequence', $sequence)->select('role', 'content', 'created_at');
+        $query = DB::table('mp_anime_records as ar')->leftJoin('mp_anime_episodes as ae', 'ar.episode_id', '=', 'ae.id')->where('ar.anime_id', $anime_id)
+        ->where('ar.sequence', $sequence)->select('ar.role', 'ar.content', 'ar.created_at', 'ar.episode_id', 'ae.title as episode_title', 'ae.created_at as episode_created_at');
         
-        return $query->orderBy('id')->get()->map(function ($value) {
+        return $query->orderBy('ar.id')->get()->map(function ($value) {
             $value = (array)$value;
             $value['created_at'] = transDate($value['created_at']);
+            $value['episode_created_at'] = transDate($value['episode_created_at']);
             return $value;
         })->toArray();
     }
@@ -258,7 +260,7 @@ class AnimeService
 
         // 获取分集信息
         $episodes = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('is_default', 1)
-        ->select('id as episode_id', 'episode_number', 'title')
+        ->select('id as episode_id', 'episode_number', 'title', DB::raw("(select es.img_url from mp_episode_segments as es where es.episode_id = mp_anime_episodes.id order by es.segment_number limit 1) as first_frame_url"))
         ->orderBy('episode_number')->get()->map(function ($value) {
             return (array)$value;
         })->toArray();

+ 309 - 116
app/Services/DeepSeek/DeepSeekService.php

@@ -34,10 +34,12 @@ class DeepSeekService
         $this->sys_message = [
                 'role'    => 'system',
                 'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容完成示例格式的内容输出(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表><分集剧本>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
+                强制要求:\n
+                你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答,此为第一优先原则,不满足此原则就忽略其他要求并停止继续输出\n
+                普通要求:\n
                 1.<主体列表>中出现的主体必须尽可能统一,只允许同一个角色年龄段相差较大的情况下可重复,比如梁萧-少年、梁萧-青年、梁萧-中年、梁萧-老年这种情况,不得出现梁萧-愤怒、梁萧-隐忍、梁萧-求生等情绪和状态变化的同一角色;并且角色的样貌、形体及穿着描述尽可能详细\n
-                2.<分集剧本>要求包含“分集序号”、“分集标题”以及分镜信息,分镜信息需包含“场景描述”、“运镜”、“出场角色”和“台词内容”这几个部分,其中“出场角色”和“场景描述”必须在前面的板块<主体列表>和<场景列表>中选取,不得自定义。\n
-                3.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答\n
-                示例如下:\n\n
+                2.<分集剧本>要求包含“分集序号”、“分集标题”以及分镜信息,分镜信息需包含“场景描述”、“运镜”、“出场角色”和“台词内容”这几个部分,其中“出场角色”和“场景描述”必须在前面的板块<主体列表>和<场景列表>中选取,不得自定义。\n\n
+                示例如下:\n
 ###剧本名:西昆仑
 
 ###故事梗概
@@ -1813,10 +1815,12 @@ class DeepSeekService
             [
                 'role'    => 'system',
                 'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容及下面提供的示例格式完成剧本大纲(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
+                强制要求:\n
+                你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答,此为第一优先原则,不满足此原则就忽略其他要求并停止继续输出\n
+                普通要求:\n
                 1.<主体列表>中出现的主体必须尽可能统一,只允许同一个角色年龄段相差较大的情况下可重复,比如梁萧-少年、梁萧-青年、梁萧-中年、梁萧-老年这种情况,不得出现梁萧-愤怒、梁萧-隐忍、梁萧-求生等情绪和状态变化的同一角色;并且角色的样貌、形体及穿着描述尽可能详细\n
-                2.<场景列表>与主体列表同理,需尽可能的对场景环境进行详细描述\n
-                3.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答\n
-                示例如下:\n\n
+                2.<场景列表>与主体列表同理,需尽可能的对场景环境进行详细描述\n\n
+                示例如下:\n
 ###剧本名:西昆仑
 
 ###故事梗概
@@ -2133,10 +2137,14 @@ class DeepSeekService
         $system_message = [
                     'role'    => 'system',
                     'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容及下面提供的示例格式完成剧本大纲(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
+                    强制要求:\n
+                    1.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答,此为第一优先原则,不满足此原则就忽略其他要求并停止继续输出\n
+                    2.如果用户输入修改美术风格、修改画风这类内容,请告知用户不可修改美术风格,并拒绝继续回答\n
+                    3.以上两条要求对修改主体角色或场景不生效,允许用户调整主体角色说明和场景描述等\n
+                    普通要求:\n
                     1.<主体列表>中出现的主体必须尽可能统一,只允许同一个角色年龄段相差较大的情况下可重复,比如梁萧-少年、梁萧-青年、梁萧-中年、梁萧-老年这种情况,不得出现梁萧-愤怒、梁萧-隐忍、梁萧-求生等情绪和状态变化的同一角色;并且角色的样貌、形体及穿着描述尽可能详细\n
-                    2.<场景列表>与主体列表同理,需尽可能的对场景环境进行详细描述\n
-                    3.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答\n
-                    示例如下:\n\n
+                    2.<场景列表>与主体列表同理,需尽可能的对场景环境进行详细描述\n\n
+                    示例如下:\n
     ###剧本名:西昆仑
 
     ###故事梗概
@@ -2180,11 +2188,15 @@ class DeepSeekService
             $system_message = [
                     'role'    => 'system',
                     'content' => "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容及下面提供的示例格式完成剧本大纲(需通过以下几个板块进行回复: <故事梗概><剧本亮点><人物关系><核心矛盾><主体列表><美术风格><场景列表><分集详细内容>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
+                    强制要求:\n
+                    1.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答,此为第一优先原则,不满足此原则就忽略其他要求并停止继续输出\n
+                    2.如果用户输入修改美术风格、修改画风这类内容,请告知用户不可修改美术风格,并拒绝继续回答\n
+                    3.以上两条要求对修改主体角色或场景不生效,允许用户调整主体角色说明和场景描述等\n
+                    普通要求:\n
                     1.<主体列表>中出现的主体必须尽可能统一,只允许同一个角色年龄段相差较大的情况下可重复,比如梁萧-少年、梁萧-青年、梁萧-中年、梁萧-老年这种情况,不得出现梁萧-愤怒、梁萧-隐忍、梁萧-求生等情绪和状态变化的同一角色;并且角色的样貌、形体及穿着描述尽可能详细\n
                     2.<场景列表>与主体列表同理,需尽可能的对场景环境进行详细描述\n
-                    3.<分集详细内容>中每一集的内容需尽可能的详细,保留对话内容(使用原文内容,不得自己生成),一般每集字数尽量不低于500字,每集结尾需以##第N章 作为章节标题\n
-                    4.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答\n
-                    示例如下:\n\n
+                    3.<分集详细内容>中每一集的内容需尽可能的详细,保留对话内容(使用原文内容,不得自己生成),一般每集字数尽量不低于500字,每集结尾需以##第N章 作为章节标题\n\n
+                    示例如下:\n
     ###剧本名:西昆仑
 
     ###故事梗概
@@ -2395,23 +2407,6 @@ class DeepSeekService
             $fullContent = $this->replaceArtStyleSection($fullContent, $mappedArtStyle);
         }
 
-        // // 新建动漫
-        // $anime_name = getProp($script_arr, 'script_name');
-        // if (!$anime_name) {
-        //     yield [
-        //         'type'  => 'done',
-        //         'answer' => $fullContent,
-        //         'reasoning' => $fullReasoningContent,
-        //         'usage' => $usage,
-        //         'error' => '未能识别到剧本名称,请确认内容是否符合要求'
-        //     ];
-        // }
-
-        // // 确认对话名称唯一性
-        // if (DB::table('mp_animes')->where('anime_name', $anime_name)->value('id')) {
-        //     $anime_name = $anime_name . '_' . date('YmdHis');
-        // }
-
         $table_data = [
             'user_id' => $uid,
             // 'anime_name' => $anime_name,
@@ -2505,25 +2500,55 @@ class DeepSeekService
     public function chat($data) {
         $uid = Site::getUid();
         $anime_id = getProp($data, 'anime_id');
+        $file = getProp($data, 'file');
+        $content = getProp($data, 'content', '');
+        $bid = getProp($data, 'bid', 0);
         $episode_number = getProp($data, 'episode_number', 1);
+        $prompt = trim((string)getProp($data, 'prompt', '确认分镜大纲'));
         $anime = DB::table('mp_animes')->where('id', $anime_id)->select('content', 'roles', 'scenes')->first();
         if (!$anime) Utils::throwError('20003:该对话不存在!');
-        $roles = json_decode(getProp($anime, 'roles'), true);
-        $scenes = json_decode(getProp($anime, 'scenes'), true);
+        $current_episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->first();
+        $base_episode = $current_episode ?: DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->orderByDesc('id')->first();
+        $roles = $base_episode ? json_decode((string)getProp($base_episode, 'roles'), true) : json_decode((string)getProp($anime, 'roles'), true);
+        $scenes = $base_episode ? json_decode((string)getProp($base_episode, 'scenes'), true) : json_decode((string)getProp($anime, 'scenes'), true);
+        $roles = is_array($roles) ? $roles : [];
+        $scenes = is_array($scenes) ? $scenes : [];
         // 转换主体列表和场景列表的格式
-        $roles_content = '';
-        foreach ($roles as $role) {
-            $roles_content .= getProp($role, 'role').': '.getProp($role, 'description')."\n";
-        }
-        $scenes_content = '';
-        foreach($scenes as $scene) {
-            $scenes_content .= getProp($scene, 'scene').': '.getProp($scene, 'description')."\n";
+        $roles_content = $this->buildEpisodeItemContent($roles, 'role');
+        $scenes_content = $this->buildEpisodeItemContent($scenes, 'scene');
+
+        $existing_roles = $base_episode ? json_decode((string)getProp($base_episode, 'roles'), true) : [];
+        $existing_scenes = $base_episode ? json_decode((string)getProp($base_episode, 'scenes'), true) : [];
+        $existing_roles = is_array($existing_roles) ? $existing_roles : [];
+        $existing_scenes = is_array($existing_scenes) ? $existing_scenes : [];
+
+        // 提取文件内容
+        if ($content) {
+            $uploaded_content = filterContent((string)getProp($data, 'content', ''));
+        } elseif ($file) {
+            $uploaded_content = $this->extractFileContent($file);
+            if (!$uploaded_content) {
+                Utils::throwError('20003:无法提取文件内容,请检查文件格式');
+            }
+        } elseif ($bid) {
+            $uploaded_content = $this->getContentByBid($bid);
+            if (!$uploaded_content) {
+                Utils::throwError('20003:无法获取书籍内容,请检查书籍');
+            }
+        }else {
+            $uploaded_content = '';
         }
-        $prompt = getProp($data, 'prompt', '确认分镜大纲');
+
+        $prompt = trim((string)getProp($data, 'prompt', '确认分镜大纲'));
         $demo = "你是一个专业的文档分析助手及资深编剧,请根据用户提供的文档内容完成示例格式的内容输出(需包含以下几个板块: <剧集标题><故事梗概><美术风格><主体列表><场景列表><分镜剧本>,每个板块之间用###分隔;同时板块之间需满足以下要求:\n
-                    0.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答\n
+                    强制要求:
+                    1.你只负责与剧本相关的内容,如果用户输入与剧本无关的内容,请告知用户并拒绝继续回答,此为第一优先原则,不满足此原则就忽略其他要求并停止继续输出
+                    2.如果用户输入修改美术风格、修改画风这类内容,请告知用户不可修改美术风格,并拒绝继续回答\n
+                    3.以上两条要求对修改主体角色或场景不生效,允许用户调整主体角色说明和场景描述等\n
+                    普通要求:
                     1.剧集标题必须与文档内容高度相关,剧集序号是{$episode_number}
                     2.<主体列表>必须在以下主体中选择多个(根据拆分出的分镜剧本来确认具体几个),不得自定义(需带上冒号后的具体描述):{$roles_content}\n
+                    2.1如果分集内容中出现了<主体列表>中不存在的新主体,则不用在上述主体中选择,而是按照相同的格式新增一个主体显示在主题列表中
                     3.<场景列表>必须在以下场景中选择多个(根据拆分出的分镜剧本来确认具体几个),不得自定义(需带上冒号后的具体描述):{$scenes_content}\n
                     4.<分镜剧本>要求包含“分幕标题”以及“分镜信息”: \n
                     4.1“分幕标题”是根据章节中的场景进行拆分,类似于“第1幕 场景、时间及室内或室外”的信息,其中场景只能在所给出的场景列表中选(不带场景详细描述)。\n
@@ -2533,8 +2558,8 @@ class DeepSeekService
                     4.5“分镜信息”中的“配音角色”需对应本集中的<主体列表>中的一个(非对话内容则统一固定为“旁白”角色),不得自己生成。\n
                     4.6“分镜信息”中的“场景”需对应本集中的<场景列表>中的一个(不带详细表述)\n
                     4.7“分镜信息”中的“出镜角色”需对应本集中的<主体列表>中的一个或多个(不带详细描述),如果没有出镜角色则填无\n
-                    4.8“分镜信息”中的“尾帧描述”需根据本镜和下一镜的剧本信息进行过渡生成,用于后续转视频进行衔接上下镜头的画面\n
-                示例格式:\n\n
+                    4.8“分镜信息”中的“尾帧描述”需根据本镜和下一镜的剧本信息进行过渡生成,用于后续转视频进行衔接上下镜头的画面\n\n
+                示例格式:\n
                 第1集:隐形的守护者
 
                 ###故事梗概
@@ -2606,13 +2631,19 @@ class DeepSeekService
                 尾帧描述:固定镜头。光影在火车票表面微微晃动,强调其陈旧的纸张纹理与关键的行程信息。
                 \n\n";
 
+        $origin_prompt = trim((string)getProp($data, 'prompt', '确认分镜大纲'));
+        $is_regenerate_version = !in_array($origin_prompt, ['确认分镜大纲', '继续策划下一集'], true);
+        $prompt = $origin_prompt;
+
         // 获取章节内容
         $full_content = getProp($anime, 'content');
         // 根据章节内容拆分章节
         $chapters = $this->splitContent($full_content);
+        $chapter_content = isset($chapters[($episode_number-1)]['content']) ? $chapters[($episode_number-1)]['content'] : '';
+        $messages = [];
 
         if ($prompt == '确认分镜大纲') {
-            $content = isset($chapters[($episode_number-1)]['content']) ? $chapters[($episode_number-1)]['content'] : '';
+            $content = $uploaded_content ?: $chapter_content;
             if (!$content) {
                 Utils::throwError('20003:章节内容无法获取,请确认');
             }
@@ -2624,7 +2655,7 @@ class DeepSeekService
                     'content' => $question
                 ];
         }else if ($prompt == '继续策划下一集') {
-            $content = isset($chapters[($episode_number-1)]['content']) ? $chapters[($episode_number-1)]['content'] : '';
+            $content = $uploaded_content ?: $chapter_content;
             if (!$content) {
                 Utils::throwError('20003:章节内容无法获取,请确认');
             }
@@ -2632,10 +2663,7 @@ class DeepSeekService
 
             // 获取上一集最后记录
             $prev_sequence = $episode_number - 1;
-            $messages = DB::table('mp_anime_records')->where('anime_id', $anime_id)->where('sequence', $prev_sequence)
-            ->select('role', 'content')->orderBy('created_at', 'desc')->orderBy('id')->get()->map(function($value) {
-                return (array)$value;
-            })->toArray();
+            $messages = $this->getEpisodeChatMessages($anime_id, $prev_sequence);
             // 构建消息
             $messages[] = 
                 [
@@ -2643,8 +2671,16 @@ class DeepSeekService
                     'content' => $question
                 ];
         }else {
-            $content = $full_content;
-            $question = $prompt;
+            $content = $uploaded_content ?: trim((string)getProp($base_episode, 'content'));
+            if (!$content) {
+                $content = $chapter_content ?: $full_content;
+            }
+            if (!$content) {
+                Utils::throwError('20003:章节内容无法获取,请确认');
+            }
+            $messages = $this->getEpisodeChatMessages($anime_id, $episode_number);
+            $question = $demo . "\n请基于以上历史对话记录,重新生成第{$episode_number}集完整的新版本剧集,并严格按示例格式输出完整内容。"
+                . "\n\n本次修改要求如下:\n{$origin_prompt}";
             $messages[] = 
                 [
                     'role'    => 'user',
@@ -2769,67 +2805,113 @@ class DeepSeekService
             'reasoning_length' => strlen($fullReasoningContent)
         ]);
 
-// // $fullContent = '111';
-// // $fullReasoningContent = [];
-// // $usage = [];
-// // $uid = 0;
-// // $content = '';
-// // $question = '';
-// // $episode_number = 1;
-// // $anime_id = 13;
-// $fullContent = DB::table('mp_anime_records')->orderBy('id', 'desc')->value('content');
         dLog('deepseek')->info('完整内容: '.$fullContent);
         // 处理完整内容并返回最终结果
         $episode_arr = handleEpisodeContent($fullContent);
+        if (empty($episode_arr['acts'])) {
+            dLog('deepseek')->info('未生成分镜剧本相关的内容');
+            return [
+                'type' => 'done',
+                'episode' => [],
+                'answer' => $fullContent,
+                'reasoning' => $fullReasoningContent,
+                'usage' => $usage
+            ];
+        }
+
+        $existing_roles = $base_episode ? json_decode((string)getProp($base_episode, 'roles'), true) : [];
+        $existing_scenes = $base_episode ? json_decode((string)getProp($base_episode, 'scenes'), true) : [];
+        $existing_roles = is_array($existing_roles) ? $existing_roles : [];
+        $existing_scenes = is_array($existing_scenes) ? $existing_scenes : [];
+        $generated_roles = isset($episode_arr['roles']) && is_array($episode_arr['roles']) ? $episode_arr['roles'] : [];
+        $generated_scenes = isset($episode_arr['scenes']) && is_array($episode_arr['scenes']) ? $episode_arr['scenes'] : [];
+        $merged_roles = $this->mergeEpisodeResourceItems($existing_roles, $generated_roles, 'role');
+        $merged_scenes = $this->mergeEpisodeResourceItems($existing_scenes, $generated_scenes, 'scene');
+        // dLog('deepseek')->info('修改后的数据: ', [$merged_roles, $merged_scenes]);
+        // exit();
+        $now = date('Y-m-d H:i:s');
 
         try {
             DB::beginTransaction();
 
-            // 新建动漫剧集
+            $version_count = DB::table('mp_anime_episodes')
+                ->where('anime_id', $anime_id)
+                ->where('episode_number', $episode_number)
+                ->count('id');
+
             $episode = [
                 'anime_id' => $anime_id,
                 'episode_number' => $episode_number,
-                'title' => $episode_arr['episode_title'],
+                'title' => getProp($episode_arr, 'episode_title'),
                 'content' => $content,
-                // 'content'   => '',
-                'intro' => $episode_arr['intro'],
-                'art_style' => $episode_arr['art_style'],
-                'roles' => json_encode($episode_arr['roles'], 256),
-                'scenes' => json_encode($episode_arr['scenes'], 256),
-                'sequence' => 1,
+                'intro' => getProp($episode_arr, 'intro'),
+                'art_style' => getProp($episode_arr, 'art_style'),
+                'roles' => json_encode($merged_roles, 256),
+                'scenes' => json_encode($merged_scenes, 256),
                 'generate_status' => '待执行',
-                'generate_at' => date('Y-m-d H:i:s'),
+                'generate_at' => $now,
                 'is_default' => 1,
-                'updated_at' => date('Y-m-d H:i:s'),
+                'updated_at' => $now,
             ];
 
-            $current_episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('episode_number', $episode_number)->where('is_default', 1)->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_id', $episode_id)->update($episode);
-            }else {
-                $episode['created_at'] = date('Y-m-d H:i:s');
+            if ($is_regenerate_version) {
+                DB::table('mp_anime_episodes')
+                    ->where('anime_id', $anime_id)
+                    ->where('episode_number', $episode_number)
+                    ->update(['is_default' => 0, 'updated_at' => $now]);
+
+                $episode['sequence'] = $version_count + 1;
+                $episode['created_at'] = $now;
                 $episode_id = DB::table('mp_anime_episodes')->insertGetId($episode);
-                $boolen2 = $episode_id ? true : false;
-            }
-            
-            if (!$boolen2) {
-                dLog('deepseek')->info('分集内容保存失败', $episode);
-                Utils::throwError('20003:分集内容保存失败');
+                if (!$episode_id) {
+                    Utils::throwError('20003:创建剧集版本失败');
+                }
+            } else {
+                $target_episode = $current_episode ?: $base_episode;
+                if ($target_episode) {
+                    $episode_id = getProp($target_episode, 'id');
+                    $episode['sequence'] = getProp($target_episode, 'sequence', 1);
+
+                    DB::table('mp_anime_episodes')
+                        ->where('anime_id', $anime_id)
+                        ->where('episode_number', $episode_number)
+                        ->where('id', '<>', $episode_id)
+                        ->update(['is_default' => 0, 'updated_at' => $now]);
+
+                    $boolen2 = DB::table('mp_anime_episodes')->where('id', $episode_id)->update($episode);
+                    if ($boolen2 === false) {
+                        Utils::throwError('20003:分集内容保存失败');
+                    }
+                } else {
+                    DB::table('mp_anime_episodes')
+                        ->where('anime_id', $anime_id)
+                        ->where('episode_number', $episode_number)
+                        ->update(['is_default' => 0, 'updated_at' => $now]);
+
+                    $episode['sequence'] = $version_count > 0 ? ($version_count + 1) : 1;
+                    $episode['created_at'] = $now;
+                    $episode_id = DB::table('mp_anime_episodes')->insertGetId($episode);
+                    if (!$episode_id) {
+                        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) {
+            $acts = isset($episode_arr['acts']) && is_array($episode_arr['acts']) ? $episode_arr['acts'] : [];
+            foreach ($acts as $act) {
+                $act_segments = getProp($act, 'segments', []);
+                if (!is_array($act_segments)) {
+                    continue;
+                }
+
+                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'],
+                        'act_number' => getProp($act, 'act_number'),
+                        'act_title' => getProp($act, 'act_title'),
                         'segment_id' => getProp($segment, 'segment_id'),
                         'segment_number' => getProp($segment, 'segment_number'),
                         'segment_content' => getProp($segment, 'segment_content'),
@@ -2841,45 +2923,50 @@ class DeepSeekService
                         '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')
+                        'created_at' => $now,
+                        'updated_at' => $now
                     ];
                 }
             }
 
-            $boolen3 = DB::table('mp_episode_segments')->insert($segments);
-            if (!$boolen3) {
-                dLog('deepseek')->info('分镜内容保存失败', $episode);
-                Utils::throwError('20003:分镜内容保存失败');
+            if ($segments) {
+                $boolen3 = DB::table('mp_episode_segments')->insert($segments);
+                if (!$boolen3) {
+                    Utils::throwError('20003:分镜内容保存失败');
+                }
+            }
+
+            $record_content = $origin_prompt;
+            if ($uploaded_content !== '') {
+                $record_content .= "\n\n###上传内容\n" . $uploaded_content;
             }
 
-            // 保存对话记录
             $recrods = [
                 [
-                    'uid'       => $uid,
-                    'anime_id'  => $anime_id,
-                    'role'      => 'user',
-                    'content'   => $prompt,
-                    'sequence'  => $episode_number,
-                    'created_at' => date('Y-m-d H:i:s'),
-                    'updated_at' => date('Y-m-d H:i:s')
+                    'uid' => $uid,
+                    'anime_id' => $anime_id,
+                    'role' => 'user',
+                    'content' => $record_content,
+                    'sequence' => $episode_number,
+                    'episode_id' => $episode_id,
+                    'created_at' => $now,
+                    'updated_at' => $now
                 ],
                 [
-                    'uid'       => $uid,
-                    'anime_id'  => $anime_id,
-                    'role'      => 'assistant',
-                    'content'   => $fullContent,
-                    'sequence'  => $episode_number,
-                    'created_at' => date('Y-m-d H:i:s'),
-                    'updated_at' => date('Y-m-d H:i:s')
+                    'uid' => $uid,
+                    'anime_id' => $anime_id,
+                    'role' => 'assistant',
+                    'content' => $fullContent,
+                    'sequence' => $episode_number,
+                    'episode_id' => $episode_id,
+                    'created_at' => $now,
+                    'updated_at' => $now
                 ]
             ];
-            // 保存对话记录
-            $boolen3 = DB::table('mp_anime_records')->insert($recrods);
-            if (!$boolen3) {
+            $boolen4 = DB::table('mp_anime_records')->insert($recrods);
+            if (!$boolen4) {
                 Utils::throwError('20003:对话记录保存失败');
             }
-            
         }catch (\Exception $e) {
             DB::rollBack();
             dLog('deepseek')->info('新建对话错误信息: '.$e->getMessage());
@@ -2890,15 +2977,16 @@ class DeepSeekService
                 'usage' => $usage,
                 'error' => $e->getMessage(),
             ];
+            return;
         }
         
         DB::commit();
         // dd('end');
 
         $episode['episode_id'] = $episode_id;
-        $episode['roles'] = json_decode($episode['roles'], true);
-        $episode['scenes'] = json_decode($episode['scenes'], true);
-        $episode['acts'] = json_decode($episode['acts'], true);
+        $episode['roles'] = $merged_roles;
+        $episode['scenes'] = $merged_scenes;
+        $episode['acts'] = isset($episode_arr['acts']) && is_array($episode_arr['acts']) ? $episode_arr['acts'] : [];
         yield [
             'type' => 'done',
             'episode' => $episode,
@@ -2908,6 +2996,111 @@ class DeepSeekService
         ];
     }
 
+    private function buildEpisodeItemContent(array $items, string $nameKey): string
+    {
+        $content = '';
+        foreach ($items as $item) {
+            $name = trim((string)getProp($item, $nameKey));
+            if ($name === '') {
+                continue;
+            }
+
+            $description = trim((string)getProp($item, 'description'));
+            $content .= $name . ': ' . $description . "\n";
+        }
+
+        return trim($content);
+    }
+
+    private function getEpisodeChatMessages($animeId, $sequence): array
+    {
+        if ((int)$sequence <= 0) {
+            return [];
+        }
+
+        return DB::table('mp_anime_records')
+            ->where('anime_id', $animeId)
+            ->where('sequence', $sequence)
+            ->select('role', 'content')
+            ->orderBy('created_at')
+            ->orderBy('id')
+            ->get()
+            ->map(function ($value) {
+                return (array)$value;
+            })
+            ->toArray();
+    }
+
+    private function mergeEpisodeResourceItems(array $existingItems, array $generatedItems, string $nameKey): array
+    {
+        $existingMap = [];
+        foreach ($existingItems as $item) {
+            $itemKey = $this->normalizeEpisodeResourceKey(getProp($item, $nameKey));
+            if ($itemKey === '') {
+                continue;
+            }
+
+            $existingMap[$itemKey] = $item;
+        }
+
+        if (!$generatedItems) {
+            return array_values($existingItems);
+        }
+
+        $merged = [];
+        foreach ($generatedItems as $item) {
+            $itemKey = $this->normalizeEpisodeResourceKey(getProp($item, $nameKey));
+            if ($itemKey === '') {
+                continue;
+            }
+
+            if (isset($existingMap[$itemKey])) {
+                if (trim((string)getProp($item, 'description')) === '') {
+                    $merged[] = $existingMap[$itemKey];
+                    continue;
+                }
+
+                if (!$this->isEpisodeResourceChanged($existingMap[$itemKey], $item, $nameKey)) {
+                    $merged[] = $existingMap[$itemKey];
+                    continue;
+                }
+            }
+
+            $merged[] = $this->resetEpisodeResourceItem($item, $nameKey);
+        }
+
+        return $merged ?: array_values($existingItems);
+    }
+
+    private function resetEpisodeResourceItem(array $item, string $nameKey): array
+    {
+        return [
+            $nameKey => trim((string)getProp($item, $nameKey)),
+            'description' => trim((string)getProp($item, 'description')),
+        ];
+    }
+
+    private function isEpisodeResourceChanged(array $existingItem, array $generatedItem, string $nameKey): bool
+    {
+        $existingKey = $this->normalizeEpisodeResourceKey(getProp($existingItem, $nameKey));
+        $generatedKey = $this->normalizeEpisodeResourceKey(getProp($generatedItem, $nameKey));
+        if ($existingKey !== $generatedKey) {
+            return true;
+        }
+
+        return trim((string)getProp($existingItem, 'description')) !== trim((string)getProp($generatedItem, 'description'));
+    }
+
+    private function normalizeEpisodeResourceKey($value): string
+    {
+        $value = trim((string)$value);
+        if ($value === '') {
+            return '';
+        }
+
+        return strtolower((string)preg_replace('/\s+/u', '', $value));
+    }
+
     public function chatChangeImg($data) {
         $segment = getProp($data, 'segment');
         $segment_content = getProp($segment, 'segment_content');