Pārlūkot izejas kodu

1.新增全能模式2.支持分段剧本3.新增分段剧本解析公共方法

lh 1 mēnesi atpakaļ
vecāks
revīzija
8907fe6aec

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4 - 0
app/Console/Test/TestCommand.php


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

@@ -741,6 +741,78 @@ class DeepSeekController extends BaseController
         ]);
     }
 
+    public function chatForAce(Request $request) {
+        // 忽略所有超时限制
+        set_time_limit(0);
+        ini_set('max_execution_time', '0');
+
+        $data = $request->all();
+        return response()->stream(function () use ($data) {
+            // 禁用所有输出缓冲
+            if (ob_get_level()) {
+                ob_end_clean();
+            }
+            
+            // 设置输出缓冲为关闭
+            ini_set('output_buffering', 'off');
+            ini_set('zlib.output_compression', 'off');
+            
+            // 立即刷新
+            if (function_exists('apache_setenv')) {
+                apache_setenv('no-gzip', '1');
+            }
+
+            try {
+                $generator = $this->deepseekService->chatForAce($data);
+                
+                foreach ($generator as $chunk) {
+                    // 发送 SSE 格式的数据
+                    $jsonData = json_encode($chunk, JSON_UNESCAPED_UNICODE);
+                    echo "data: {$jsonData}\n\n";
+                    
+                    // 强制刷新输出缓冲区
+                    if (ob_get_level() > 0) {
+                        ob_flush();
+                    }
+                    flush();
+                    
+                    // 检查客户端是否断开连接
+                    if (connection_aborted()) {
+                        break;
+                    }
+                }
+                
+                // 发送结束标记
+                echo "data: [DONE]\n\n";
+                if (ob_get_level() > 0) {
+                    ob_flush();
+                }
+                flush();
+                
+            } catch (\Exception $e) {
+                // 发送错误信息
+                $error = [
+                    'type' => 'error',
+                    'msg' => $e->getMessage(),
+                    'code' => $e->getCode()
+                ];
+                $jsonError = json_encode($error, JSON_UNESCAPED_UNICODE);
+                echo "data: {$jsonError}\n\n";
+                if (ob_get_level() > 0) {
+                    ob_flush();
+                }
+                flush();
+            }
+        }, 200, [
+            'Content-Type' => 'text/event-stream',
+            'Cache-Control' => 'no-cache',
+            'Connection' => 'keep-alive',
+            'X-Accel-Buffering' => 'no', // 防止 Nginx 缓冲
+            'Access-Control-Allow-Origin' => '*',
+            'Access-Control-Allow-Credentials' => 'true'
+        ]);
+    }
+
     // 对话改图
     public function chatChangeImg(Request $request) {
         $data = $request->all();

+ 423 - 0
app/Libs/Helpers.php

@@ -2870,6 +2870,429 @@ function handleEpisodeContent($originalContent) {
     return $result;
 }
 
+function handleEpisodeContentForAce($originalContent) {
+    if (!$originalContent) return [];
+    
+    // 解析剧集内容
+    $result = [
+        'episode_title' => '',
+        'intro' => '',
+        'art_style' => '',
+        'roles' => [],
+        'scenes' => [],
+        'acts' => []
+    ];
+    
+    // 提取剧集标题 - 匹配"第xx集:标题"或"第xx集 标题"格式,排除###标记
+    if (preg_match('/第(\d+)集[::\s]+([^#\n]+?)(?=\s*###|\s*$|\n)/u', $originalContent, $titleMatch)) {
+        $result['episode_title'] = '第' . $titleMatch[1] . '集:' . trim($titleMatch[2]);
+    }
+    
+    // 提取故事梗概 - 兼容多种格式:###故事梗概、### 故事梗概、###     故事梗概
+    if (preg_match('/###\s*故事梗概\s*\n(.*?)(?=\n\s*###[^#]|\z)/s', $originalContent, $summaryMatch)) {
+        $result['intro'] = trim($summaryMatch[1]);
+    }
+    
+    // 提取美术风格 - 兼容多种格式
+    if (preg_match('/###\s*美术风格\s*\n(.*?)(?=\n\s*###[^#]|\z)/s', $originalContent, $styleMatch)) {
+        $result['art_style'] = trim($styleMatch[1]);
+    }
+    
+    // 提取主体列表 - 兼容多种格式
+    if (preg_match('/###\s*主体列表\s*\n(.*?)(?=\n\s*###[^#]|\z)/s', $originalContent, $charactersMatch)) {
+        $charactersText = trim($charactersMatch[1]);
+        $characterLines = explode("\n", $charactersText);
+        
+        foreach ($characterLines as $line) {
+            $line = trim($line);
+            if (empty($line)) continue;
+            
+            // 兼容中文冒号:和英文冒号:,同时提取音色信息
+            if (preg_match('/^([^::]+)[::](.+)$/u', $line, $charMatch)) {
+                $role = trim($charMatch[1]);
+                $description = trim($charMatch[2]);
+                $timbreName = null;
+                
+                // 检查描述末尾是否有{主体图片提示词}{{音色名}}格式
+                $picPrompt = null;
+                $timbrePrompt = null;
+                
+                // 修复正则:使用更精确的匹配,支持花括号嵌套
+                // 格式1: 描述{pic_prompt} {{voice_prompt}}
+                if (preg_match('/^(.*?)\{(.+?)\}\s*\{\{(.+?)\}\}\s*$/u', $description, $fullMatch)) {
+                    // 匹配格式:主体描述{主体图片提示词}{{音色名}}
+                    $description = trim($fullMatch[1]);
+                    $picPrompt = trim($fullMatch[2]);
+                    $timbrePrompt = trim($fullMatch[3]);
+                } 
+                // 格式2: 描述{{voice_prompt}}(兼容旧格式,只有音色)
+                elseif (preg_match('/^(.*?)\{\{(.+?)\}\}\s*$/u', $description, $timbreMatch)) {
+                    // 兼容旧格式:主体描述{{音色名}}
+                    $description = trim($timbreMatch[1]);
+                    $timbrePrompt = trim($timbreMatch[2]);
+                }
+                // 格式3: 描述{pic_prompt}(只有图片提示词)
+                elseif (preg_match('/^(.*?)\{(.+?)\}\s*$/u', $description, $picMatch)) {
+                    $description = trim($picMatch[1]);
+                    $picPrompt = trim($picMatch[2]);
+                }
+                
+                $roleData = [
+                    'role' => $role,
+                    'description' => $description
+                ];
+                
+                // 如果有主体图片提示词,添加到数组中
+                if ($picPrompt) {
+                    // 检查是否包含"姿态:"或"姿态:"
+                    if (preg_match('/姿态[::]/u', $picPrompt)) {
+                        // 如果有姿态描述,统一替换为"姿态:站立"
+                        $picPrompt = preg_replace('/姿态[::][^.。\n]+/u', '姿态:站立', $picPrompt);
+                    } else {
+                        // 如果没有姿态描述,在末尾添加"姿态:站立"
+                        $picPrompt = rtrim($picPrompt, '。,, ') . '。姿态:站立。';
+                    }
+                    
+                    $roleData['pic_prompt'] = $picPrompt;
+                }
+                
+                if ($timbrePrompt) $roleData['voice_prompt'] = $timbrePrompt;
+                
+                $result['roles'][] = $roleData;
+            }
+        }
+
+        // // 加入旁白角色(如果不存在)
+        // $hasNarrator = false;
+        // foreach ($result['roles'] as $role) {
+        //     if (isset($role['role']) && $role['role'] === '旁白') {
+        //         $hasNarrator = true;
+        //         break;
+        //     }
+        // }
+        
+        // if (!$hasNarrator) {
+        //     $result['roles'][] = [
+        //         'role' => '旁白',
+        //         'description' => '负责叙述剧情、补充说明和情感渲染的非视觉角色。',
+        //         'pic_prompt' => '',
+        //         'voice_name' => '旁白',
+        //         'voice_type' => 'zh_male_linjiananhai_moon_bigtts',
+        //         'voice_audio_url' => 'https://zw-audiobook.tos-cn-beijing.volces.com/demonstrate/zh_male_linjiananhai_moon_bigtts.wav'
+        //     ];
+        // }
+    }
+    
+    // 提取场景列表 - 兼容多种格式
+    if (preg_match('/###\s*场景列表\s*\n(.*?)(?=\n\s*###[^#]|\z)/s', $originalContent, $scenesMatch)) {
+        $scenesText = trim($scenesMatch[1]);
+        $sceneLines = explode("\n", $scenesText);
+        
+        foreach ($sceneLines as $line) {
+            $line = trim($line);
+            if (empty($line)) continue;
+            
+            // 兼容中文冒号:和英文冒号:
+            if (preg_match('/^([^::]+)[::](.+)$/u', $line, $sceneMatch)) {
+                $scene = trim($sceneMatch[1]);
+                $description = trim($sceneMatch[2]);
+                $picPrompt = null;
+                
+                // 检查描述末尾是否有{场景图片提示词}格式
+                if (preg_match('/^(.*?)\{([^}]+)\}\s*$/u', $description, $promptMatch)) {
+                    // 匹配格式:场景描述{场景图片提示词}
+                    $description = trim($promptMatch[1]);
+                    $picPrompt = trim($promptMatch[2]);
+                }
+                
+                $sceneData = [
+                    'scene' => $scene,
+                    'description' => $description
+                ];
+                
+                // 如果有场景图片提示词,添加到数组中
+                if ($picPrompt) {
+                    $sceneData['pic_prompt'] = $picPrompt;
+                }
+                
+                $result['scenes'][] = $sceneData;
+            }
+        }
+    }
+    
+    // 提取分镜剧本 - 兼容多种格式(旧格式:###分镜剧本,新格式:###分段剧本)
+    $storyboardPattern = '/###\s*(?:分镜剧本|分段剧本)\s*\n(.*?)(?=\n\s*###[^#]|\z)/s';
+    if (preg_match($storyboardPattern, $originalContent, $storyboardMatch)) {
+        $storyboardText = trim($storyboardMatch[1]);
+        
+        // 提取旁白音色(新格式独有)
+        $narratorVoice = '';
+        if (preg_match('/旁白音色[::]\s*([^\n]+)/u', $storyboardText, $narratorMatch)) {
+            $narratorVoice = trim($narratorMatch[1]);
+            
+            // 将旁白加入到roles数组
+            $hasNarrator = false;
+            foreach ($result['roles'] as $role) {
+                if (isset($role['role']) && $role['role'] === '旁白') {
+                    $hasNarrator = true;
+                    break;
+                }
+            }
+            
+            if (!$hasNarrator) {
+                $result['roles'][] = [
+                    'role' => '旁白',
+                    'description' => '负责叙述剧情、补充说明和情感渲染的非视觉角色。',
+                    'pic_prompt' => '',
+                    'voice_prompt' => $narratorVoice
+                ];
+            }
+        }
+        
+        // 按幕/片段分割 - 兼容"第X幕"和"片段X"两种格式
+        $acts = [];
+        
+        // 先在开头添加换行符,确保第1幕/片段1也能被正确分割
+        $normalizedText = "\n" . $storyboardText;
+        $parts = preg_split('/\n\s*##/', $normalizedText);
+        
+        foreach ($parts as $part) {
+            $part = trim($part);
+            if (empty($part)) continue;
+            
+            // 兼容"第X幕"和"片段X"两种格式
+            if (!preg_match('/^(?:第\d+幕|片段\d+)/', $part)) {
+                continue;
+            }
+            
+            // 分离标题和内容
+            $lines = explode("\n", $part, 2);
+            $actTitle = trim($lines[0]);
+            $actContent = isset($lines[1]) ? trim($lines[1]) : '';
+            
+            // 解析标题,提取序号和详细信息
+            if (preg_match('/^第(\d+)幕[::]?\s*(.*)$/u', $actTitle, $actTitleMatch)) {
+                // 旧格式:第X幕
+                $actNumber = intval($actTitleMatch[1]);
+                $actDetails = trim($actTitleMatch[2]);
+                if (empty($actDetails) || $actDetails === ':' || $actDetails === ':') {
+                    $actDetails = $actTitle;
+                }
+            } elseif (preg_match('/^片段(\d+)\s*$/u', $actTitle, $segmentTitleMatch)) {
+                // 新格式:片段X
+                $actNumber = intval($segmentTitleMatch[1]);
+                $actDetails = $actTitle;
+            } else {
+                $actNumber = count($acts) + 1;
+                $actDetails = $actTitle;
+            }
+            
+            // 提取时长(新格式独有,仅保留数字)
+            $actDuration = '';
+            if (preg_match('/时长[::]\s*([^\n]+)/u', $actContent, $actDurationMatch)) {
+                $actDurationStr = trim($actDurationMatch[1]);
+                // 提取数字部分(支持整数和小数)
+                if (preg_match('/([-+]?[0-9]*\.?[0-9]+)/', $actDurationStr, $numMatch)) {
+                    $actDuration = $numMatch[1];
+                }
+            }
+            
+            // 解析该幕/片段下的分镜
+            $segments = [];
+            $segmentPattern = '/分镜(\d+)\s*\n(.*?)(?=\n+\s*分镜\d+|\z)/s';
+            preg_match_all($segmentPattern, $actContent, $segmentMatches, PREG_SET_ORDER);
+            
+            foreach ($segmentMatches as $segmentMatch) {
+                $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),
+                    'segment_number'  => $segmentNumber,
+                    'segment_content' => $segmentContent,
+                    'description'     => '',
+                    'composition'     => '',
+                    'camera_movement' => '',
+                    'voice_actor'     => '',
+                    'dialogue'        => '',
+                    'frame_type'      => '',
+                    'scene'           => '',
+                    'characters'      => '',
+                    'tail_frame'      => '',
+                    'emotion'         => '中性',
+                    'gender'          => '0',
+                    'speed_ratio'     => 0,
+                    'loudness_ratio'  => 0,
+                    'emotion_scale'   => 4,
+                    'pitch'           => 0,
+                ];
+                
+                // 提取场景
+                if (preg_match('/(?:场景|拍摄场景|背景场景|环境)[::]\s*([^\n]+)/u', $segmentContent, $sceneMatch)) {
+                    $segmentData['scene'] = trim($sceneMatch[1]);
+                }
+                
+                // 提取画面描述 - 兼容新格式的方括号标注
+                if (preg_match('/(?:画面|画面描述|镜头描述|场景描述)[::]\s*(\[.*?\])?\s*([^\n]+)/u', $segmentContent, $descMatch)) {
+                    $frameTypeInDesc = isset($descMatch[1]) ? trim($descMatch[1], '[]') : '';
+                    $description = trim($descMatch[2]);
+                    
+                    if (!empty($frameTypeInDesc)) {
+                        $segmentData['frame_type'] = $frameTypeInDesc;
+                    }
+                    $segmentData['description'] = $description;
+                }
+                
+                // 提取运镜
+                if (preg_match('/(?:运镜|运镜调度|镜头运动|摄影机运动)[::]\s*([^\n]+)/u', $segmentContent, $cameraMatch)) {
+                    $segmentData['camera_movement'] = trim($cameraMatch[1]);
+                }
+                
+                // 提取配音台词 - 兼容新格式"中文配音:[角色] 台词"
+                // 修复:排除背景音效等非台词内容
+                if (preg_match('/配音台词[::]\s*(?:中文配音[::]?)?\s*(?:\[([^\]]+)\])?\s*([^\n]*?)(?=\n|$)/u', $segmentContent, $dialogueMatch)) {
+                    $voiceActor = isset($dialogueMatch[1]) ? trim($dialogueMatch[1]) : '';
+                    $dialogue = isset($dialogueMatch[2]) ? trim($dialogueMatch[2]) : '';
+                    
+                    // 排除非台词内容(如果匹配到的是空或者是其他字段标记)
+                    if (!empty($dialogue) && preg_match('/^(?:背景音效|音效|场景|画面|运镜|构图)[::]/u', $dialogue)) {
+                        $dialogue = ''; // 如果匹配到的是其他字段,清空
+                    }
+                    
+                    if (!empty($voiceActor)) {
+                        $segmentData['voice_actor'] = $voiceActor;
+                    }
+                    
+                    if ($dialogue && $dialogue !== '无') {
+                        // 确保台词使用中文左右双引号
+                        $dialogue = preg_replace('/^[""]/u', '"', $dialogue);
+                        $dialogue = preg_replace('/[""]$/u', '"', $dialogue);
+                        if (!preg_match('/^["]/', $dialogue)) {
+                            $dialogue = '"' . $dialogue;
+                        }
+                        if (!preg_match('/["]$/', $dialogue)) {
+                            $dialogue .= '"';
+                        }
+                        $segmentData['dialogue'] = $dialogue;
+                    }
+                }
+                
+                // 兼容旧格式的台词字段
+                // 修复:排除背景音效等非台词内容
+                if (empty($segmentData['dialogue']) && preg_match('/(?:台词内容|台词|对白|对话)[::]\s*([^\n]*?)(?=\n|$)/u', $segmentContent, $oldDialogueMatch)) {
+                    $dialogue = trim($oldDialogueMatch[1]);
+                    
+                    // 排除非台词内容
+                    if (!empty($dialogue) && preg_match('/^(?:背景音效|音效|场景|画面|运镜|构图)[::]/u', $dialogue)) {
+                        $dialogue = ''; // 如果匹配到的是其他字段,清空
+                    }
+                    
+                    if ($dialogue && $dialogue !== '无') {
+                        $dialogue = preg_replace('/^[""]/u', '"', $dialogue);
+                        $dialogue = preg_replace('/[""]$/u', '"', $dialogue);
+                        if (!preg_match('/^["]/', $dialogue)) {
+                            $dialogue = '"' . $dialogue;
+                        }
+                        if (!preg_match('/["]$/', $dialogue)) {
+                            $dialogue .= '"';
+                        }
+                        $segmentData['dialogue'] = $dialogue;
+                    }
+                }
+                
+                // 兼容旧格式的配音角色字段
+                if (empty($segmentData['voice_actor']) && preg_match('/(?:配音角色|配音|角色|声优)[::]\s*([^\n]+)/u', $segmentContent, $voiceMatch)) {
+                    $segmentData['voice_actor'] = trim($voiceMatch[1]);
+                }
+                
+                // 提取构图
+                if (preg_match('/(?:构图设计|构图|镜头构图)[::]\s*([^\n]+)/u', $segmentContent, $compMatch)) {
+                    $segmentData['composition'] = trim($compMatch[1]);
+                }
+                
+                // 提取画面类型
+                if (empty($segmentData['frame_type']) && preg_match('/(?:画面类型|镜头类型|类型)[::]\s*([^\n]+)/u', $segmentContent, $frameMatch)) {
+                    $segmentData['frame_type'] = trim($frameMatch[1]);
+                }
+                
+                // 提取出镜角色
+                if (preg_match('/(?:出镜角色|角色出镜|登场角色|人物)[::]\s*([^\n]+)/u', $segmentContent, $charactersMatch)) {
+                    $segmentData['characters'] = trim($charactersMatch[1]);
+                }
+                
+                // 提取尾帧描述
+                if (preg_match('/(?:尾帧描述|尾帧|结束帧|最后一帧|结尾画面|结束画面)[::]\s*([^\n]+)/u', $segmentContent, $tailFrameMatch)) {
+                    $segmentData['tail_frame'] = trim($tailFrameMatch[1]);
+                }
+                
+                $replaceEmptyArr = [];
+                
+                // 提取情感
+                if (preg_match('/(?:情感|情绪|感情)[::]\s*([^\n]+)/u', $segmentContent, $emotionMatch)) {
+                    $replaceEmptyArr[] = trim($emotionMatch[0]);
+                    $segmentData['emotion'] = trim($emotionMatch[1]);
+                }
+                
+                // 提取性别
+                if (preg_match('/(?:性别)[::]\s*([^\n]+)/u', $segmentContent, $genderMatch)) {
+                    $replaceEmptyArr[] = trim($genderMatch[0]);
+                    $genderStr = trim($genderMatch[1]);
+                    if (strpos($genderStr, '男') !== false || $genderStr === '1') {
+                        $segmentData['gender'] = '1';
+                    } elseif (strpos($genderStr, '女') !== false || $genderStr === '2') {
+                        $segmentData['gender'] = '2';
+                    } else {
+                        $segmentData['gender'] = '0';
+                    }
+                }
+                
+                // 提取语速
+                if (preg_match('/(?:语速|说话速度)[::]\s*([-+]?[0-9]*\.?[0-9]+)/u', $segmentContent, $speedMatch)) {
+                    $replaceEmptyArr[] = trim($speedMatch[0]);
+                    $segmentData['speed_ratio'] = (float)trim($speedMatch[1]);
+                }
+                
+                // 提取音量
+                if (preg_match('/(?:音量|声音大小)[::]\s*([-+]?[0-9]*\.?[0-9]+)/u', $segmentContent, $loudnessMatch)) {
+                    $replaceEmptyArr[] = trim($loudnessMatch[0]);
+                    $segmentData['loudness_ratio'] = (float)trim($loudnessMatch[1]);
+                }
+                
+                // 提取情感强度
+                if (preg_match('/(?:情感强度|情绪强度)[::]\s*([0-9]+)/u', $segmentContent, $scaleMatch)) {
+                    $replaceEmptyArr[] = trim($scaleMatch[0]);
+                    $segmentData['emotion_scale'] = (int)trim($scaleMatch[1]);
+                }
+                
+                // 提取音调
+                if (preg_match('/(?:音调|音高)[::]\s*([-+]?[0-9]+)/u', $segmentContent, $pitchMatch)) {
+                    $replaceEmptyArr[] = trim($pitchMatch[0]);
+                    $segmentData['pitch'] = (int)trim($pitchMatch[1]);
+                }
+
+                $segmentData['segment_content'] = str_replace($replaceEmptyArr, '', $segmentContent);
+                $segmentData['segment_content'] = preg_replace('/\n{2,}/', "\n", $segmentData['segment_content']);
+                $segmentData['segment_content'] = trim($segmentData['segment_content']);
+                
+                $segments[] = $segmentData;
+            }
+            
+            $acts[] = [
+                'act_number' => $actNumber,
+                'act_title' => $actTitle,
+                'act_details' => $actDetails,
+                'act_duration' => $actDuration,
+                'segments' => $segments
+            ];
+        }
+        
+        $result['acts'] = $acts;
+    }
+    
+    return $result;
+}
+
 
 /**
  * 远程图片压缩(尽可能保持原图 fidelity,压缩到不超过 maxBytes 字节)

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 650 - 10
app/Services/DeepSeek/DeepSeekService.php


+ 1 - 0
routes/api.php

@@ -156,6 +156,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             Route::get('chatList', [AnimeController::class, 'chatList']);                           // 动漫列表
             Route::get('chatHistory', [AnimeController::class, 'chatHistory']);                     // 动漫对话历史
             Route::post('chat', [DeepSeekController::class, 'chat']);                               // 动漫对话
+            Route::post('chatForAce', [DeepSeekController::class, 'chatForAce']);                         // 动漫对话(全能模式)
             Route::get('detail', [AnimeController::class, 'animeDetail']);                          // 动漫大纲
             Route::get('episodeInfo', [AnimeController::class, 'episodeInfo']);                     // 动漫剧集
             Route::get('segmentInfo', [AnimeController::class, 'segmentInfo']);                     // 分镜信息