|
|
@@ -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输出和模板提示词
|