Преглед изворни кода

新建剧本资产时增加集数和章节连续性校验

lh пре 1 месец
родитељ
комит
7c962fceac
1 измењених фајлова са 185 додато и 0 уклоњено
  1. 185 0
      app/Services/DeepSeek/DeepSeekService.php

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

@@ -310,6 +310,8 @@ class DeepSeekService
 
     public function createGenerateText($data) {
         $file = getProp($data, 'file');
+        $episode_num = getProp($data, 'episode_num');
+
         $uid = Site::getUid();
         $cpid = Site::getCpid();
         
@@ -322,6 +324,11 @@ class DeepSeekService
         if (!$content) {
             Utils::throwError('20003:无法提取文件内容,请检查文件格式');
         }
+
+        // 拆分章节并校验章节总数、标题连续性
+        if ($episode_num) {
+            $this->validateGenerateTextChapters($content, (int)$episode_num);
+        }
         
         // 获取原始文件名(不含扩展名)作为script_name
         // 注意:不能用pathinfo()取FILENAME,PHP在部分Linux环境下对中文等
@@ -357,6 +364,184 @@ class DeepSeekService
             'script_name' => $script_name
         ];
     }
+
+    /**
+     * 拆分剧本内容并校验章节总数与标题连续性
+     *
+     * @param string $content
+     * @param int $episodeNum
+     * @return void
+     */
+    private function validateGenerateTextChapters($content, $episodeNum)
+    {
+        $chapters = splitContent($content);
+
+        if (count($chapters) !== $episodeNum) {
+            Utils::throwError('20003:章节总数与集数不一致,请检查上传内容');
+        }
+
+        if (empty($chapters)) {
+            return;
+        }
+
+        // 先尝试从标题正则中解析序号,确认是否从1开始连续递增
+        $numbers = [];
+        $unparsedIndex = null;
+        foreach ($chapters as $index => $chapter) {
+            $num = $this->parseChapterNumber($chapter['title']);
+            if ($num === null) {
+                $unparsedIndex = $index;
+                break;
+            }
+            $numbers[] = $num;
+        }
+
+        if ($unparsedIndex === null) {
+            $expected = 1;
+            foreach ($numbers as $index => $num) {
+                if ($num !== $expected) {
+                    $title = mb_substr(trim((string)$chapters[$index]['title']), 0, 40);
+                    Utils::throwError('20003:章节序号不连续:第' . ($index + 1) . '个章节“' . $title . '”序号为' . $num . ',预期为' . $expected);
+                }
+                $expected++;
+            }
+            return;
+        }
+
+        // 标题格式无法用正则确认时,交给模型分析
+        $titles = array_column($chapters, 'title');
+        $aiResult = $this->validateChapterSequenceByAI($titles);
+        if ($aiResult['valid'] === false) {
+            $badIndex = $aiResult['invalid_index'] ?: $unparsedIndex + 1;
+            if (isset($chapters[$badIndex - 1])) {
+                $title = mb_substr(trim((string)$chapters[$badIndex - 1]['title']), 0, 40);
+                Utils::throwError('20003:章节序号不连续:第' . $badIndex . '个章节“' . $title . '”序号异常,请检查');
+            }
+            Utils::throwError('20003:章节序号不连续,请检查章节标题格式');
+        }
+        if ($aiResult['valid'] === null) {
+            if (isset($chapters[$unparsedIndex])) {
+                $title = mb_substr(trim((string)$chapters[$unparsedIndex]['title']), 0, 40);
+                Utils::throwError('20003:无法确认第' . ($unparsedIndex + 1) . '个章节“' . $title . '”的章节序号,请检查标题格式');
+            }
+            Utils::throwError('20003:无法确认章节标题连续性,请检查章节标题格式');
+        }
+    }
+
+    /**
+     * 从章节标题中解析序号,支持阿拉伯数字和中文数字
+     *
+     * @param string $title
+     * @return int|null
+     */
+    private function parseChapterNumber($title)
+    {
+        if (!preg_match('/第\s*([一二两三四五六七八九十百千万0-9]+)\s*[章节集幕场回话]/u', $title, $match)) {
+            return null;
+        }
+
+        return $this->convertChineseNumber($match[1]);
+    }
+
+    /**
+     * 中文数字转阿拉伯数字(支持到千)
+     *
+     * @param string $numStr
+     * @return int
+     */
+    private function convertChineseNumber($numStr)
+    {
+        if (preg_match('/^\d+$/', $numStr)) {
+            return (int)$numStr;
+        }
+
+        $map = [
+            '零' => 0,
+            '一' => 1,
+            '二' => 2,
+            '两' => 2,
+            '三' => 3,
+            '四' => 4,
+            '五' => 5,
+            '六' => 6,
+            '七' => 7,
+            '八' => 8,
+            '九' => 9
+        ];
+
+        $total = 0;
+        $section = 0;
+        $length = mb_strlen($numStr);
+        for ($i = 0; $i < $length; $i++) {
+            $char = mb_substr($numStr, $i, 1);
+            if (isset($map[$char])) {
+                $section = $map[$char];
+            } elseif ($char === '十') {
+                $total += $section === 0 ? 10 : $section * 10;
+                $section = 0;
+            } elseif ($char === '百') {
+                $total += $section === 0 ? 100 : $section * 100;
+                $section = 0;
+            } elseif ($char === '千') {
+                $total += $section === 0 ? 1000 : $section * 1000;
+                $section = 0;
+            }
+        }
+
+        return $total + $section;
+    }
+
+    /**
+     * 用 deepseek-v4-flash 非流式判断章节标题是否从1开始连续编号
+     *
+     * @param array $titles
+     * @return array{valid: bool|null, invalid_index: int|null}
+     */
+    private function validateChapterSequenceByAI($titles)
+    {
+        $titleLines = [];
+        foreach (array_slice($titles, 0, 200) as $index => $title) {
+            $titleLines[] = ($index + 1) . '. ' . mb_substr(trim((string)$title), 0, 80);
+        }
+
+        $result = [
+            'valid' => null,
+            'invalid_index' => null
+        ];
+
+        $post_data = [
+            'model'             => 'deepseek-v4-flash',
+            'messages'          => [
+                [
+                    'role'    => 'system',
+                    'content' => '你是剧本章节校验助手。用户会给出章节标题列表,请判断这些标题中的编号是否从第1章/第1集开始连续递增,支持阿拉伯数字和中文数字。只输出 JSON,格式为 {"valid":true};若不连续则输出 {"valid":false,"invalid_index":N},其中 N 是第一个序号异常的章节序号(从1开始),不要输出其他内容。',
+                ],
+                [
+                    'role'    => 'user',
+                    'content' => implode("\n", $titleLines),
+                ],
+            ],
+            'temperature'       => 0,
+            'frequency_penalty' => 0,
+            'presence_penalty'  => 0,
+            'stream'            => false,
+        ];
+
+        try {
+            $result = $this->chatOnly($post_data, 8);
+            $content = trim((string)($result['fullContent'] ?? ''));
+            if (preg_match('/"valid"\s*:\s*(true|false)/i', $content, $match)) {
+                $result['valid'] = strtolower($match[1]) === 'true';
+            }
+            if (preg_match('/"invalid_index"\s*:\s*(\d+)/i', $content, $match)) {
+                $result['invalid_index'] = (int)$match[1];
+            }
+        } catch (\Exception $e) {
+            dLog('deepseek')->error('章节连续性AI校验失败: ' . $e->getMessage());
+        }
+
+        return $result;
+    }
     
     /**
      * 通用文生文方法(非流式版本)- 支持多模型、图片输入、JSON输出和模板提示词