| """ |
| 霜云(Shimokumo) - 小说创作模块 |
| |
| 提供世界观设定管理、角色管理、章节大纲生成、章节内容生成、 |
| 情节一致性检查和多种写作风格支持。 |
| |
| 基于AI模型驱动的自动化小说创作工具。 |
| """ |
|
|
| import json |
| import os |
| import re |
| import time |
| import uuid |
| from dataclasses import dataclass, field |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| from utils.logger import get_logger |
|
|
| logger = get_logger("Shimokumo.NovelCreator") |
|
|
|
|
| |
|
|
| @dataclass |
| class WorldSetting: |
| """世界观设定数据类""" |
| name: str = "" |
| """世界名称""" |
| genre: str = "奇幻" |
| """类型(奇幻/科幻/玄幻/言情/悬疑/日常/历史)""" |
| era: str = "" |
| """时代背景""" |
| geography: str = "" |
| """地理环境描述""" |
| magic_system: str = "" |
| """魔法/力量体系""" |
| technology_level: str = "" |
| """科技水平""" |
| social_structure: str = "" |
| """社会结构""" |
| history: str = "" |
| """历史背景""" |
| rules: List[str] = field(default_factory=list) |
| """世界规则列表""" |
| factions: List[str] = field(default_factory=list) |
| """势力/组织列表""" |
| custom_settings: Dict[str, str] = field(default_factory=dict) |
| """自定义设定""" |
|
|
| def to_prompt(self) -> str: |
| """生成世界观提示词(用于引导AI生成)""" |
| parts: List[str] = [f"【世界观设定:{self.name}】"] |
| if self.genre: |
| parts.append(f"类型:{self.genre}") |
| if self.era: |
| parts.append(f"时代:{self.era}") |
| if self.geography: |
| parts.append(f"地理:{self.geography}") |
| if self.magic_system: |
| parts.append(f"力量体系:{self.magic_system}") |
| if self.technology_level: |
| parts.append(f"科技水平:{self.technology_level}") |
| if self.social_structure: |
| parts.append(f"社会结构:{self.social_structure}") |
| if self.history: |
| parts.append(f"历史:{self.history}") |
| if self.rules: |
| parts.append("世界规则:") |
| for rule in self.rules: |
| parts.append(f" - {rule}") |
| if self.factions: |
| parts.append("势力/组织:") |
| for faction in self.factions: |
| parts.append(f" - {faction}") |
| if self.custom_settings: |
| parts.append("其他设定:") |
| for key, val in self.custom_settings.items(): |
| parts.append(f" - {key}: {val}") |
| return "\n".join(parts) |
|
|
| def to_dict(self) -> Dict[str, Any]: |
| """转为字典""" |
| return { |
| "name": self.name, |
| "genre": self.genre, |
| "era": self.era, |
| "geography": self.geography, |
| "magic_system": self.magic_system, |
| "technology_level": self.technology_level, |
| "social_structure": self.social_structure, |
| "history": self.history, |
| "rules": self.rules, |
| "factions": self.factions, |
| "custom_settings": self.custom_settings, |
| } |
|
|
|
|
| |
|
|
| @dataclass |
| class NovelCharacter: |
| """小说角色数据类""" |
| name: str = "" |
| """角色名""" |
| alias: List[str] = field(default_factory=list) |
| """别名列表""" |
| gender: str = "" |
| """性别""" |
| age: str = "" |
| """年龄""" |
| appearance: str = "" |
| """外观描述""" |
| personality: str = "" |
| """性格描述""" |
| background: str = "" |
| """背景故事""" |
| abilities: List[str] = field(default_factory=list) |
| """能力列表""" |
| weaknesses: List[str] = field(default_factory=list) |
| """弱点列表""" |
| relationships: Dict[str, str] = field(default_factory=dict) |
| """与其他角色的关系""" |
| speech_style: str = "" |
| """说话风格""" |
| goals: str = "" |
| """目标/动机""" |
| role_type: str = "配角" |
| """角色类型:主角/配角/反派/NPC""" |
|
|
| def to_prompt(self) -> str: |
| """生成角色提示词""" |
| parts: List[str] = [f"【角色:{self.name}】"] |
| if self.alias: |
| parts.append(f"别名:{'、'.join(self.alias)}") |
| if self.role_type: |
| parts.append(f"定位:{self.role_type}") |
| if self.gender: |
| parts.append(f"性别:{self.gender}") |
| if self.age: |
| parts.append(f"年龄:{self.age}") |
| if self.appearance: |
| parts.append(f"外观:{self.appearance}") |
| if self.personality: |
| parts.append(f"性格:{self.personality}") |
| if self.background: |
| parts.append(f"背景:{self.background}") |
| if self.abilities: |
| parts.append(f"能力:{'、'.join(self.abilities)}") |
| if self.weaknesses: |
| parts.append(f"弱点:{'、'.join(self.weaknesses)}") |
| if self.relationships: |
| parts.append("关系:") |
| for other, relation in self.relationships.items(): |
| parts.append(f" - {other}:{relation}") |
| if self.speech_style: |
| parts.append(f"说话风格:{self.speech_style}") |
| if self.goals: |
| parts.append(f"目标:{self.goals}") |
| return "\n".join(parts) |
|
|
| def to_dict(self) -> Dict[str, Any]: |
| """转为字典""" |
| return { |
| "name": self.name, |
| "alias": self.alias, |
| "gender": self.gender, |
| "age": self.age, |
| "appearance": self.appearance, |
| "personality": self.personality, |
| "background": self.background, |
| "abilities": self.abilities, |
| "weaknesses": self.weaknesses, |
| "relationships": self.relationships, |
| "speech_style": self.speech_style, |
| "goals": self.goals, |
| "role_type": self.role_type, |
| } |
|
|
|
|
| |
|
|
| @dataclass |
| class ChapterOutline: |
| """章节大纲""" |
| chapter_number: int = 0 |
| """章节编号""" |
| title: str = "" |
| """章节标题""" |
| summary: str = "" |
| """章节摘要""" |
| key_events: List[str] = field(default_factory=list) |
| """关键事件列表""" |
| characters_involved: List[str] = field(default_factory=list) |
| """涉及的角色""" |
| location: str = "" |
| """场景地点""" |
| mood: str = "" |
| """氛围/情感基调""" |
| cliffhanger: str = "" |
| """章末悬念""" |
|
|
| def to_prompt(self) -> str: |
| """生成章节大纲提示词""" |
| parts: List[str] = [f"【第{self.chapter_number}章:{self.title}】"] |
| parts.append(f"摘要:{self.summary}") |
| if self.key_events: |
| parts.append("关键事件:") |
| for event in self.key_events: |
| parts.append(f" - {event}") |
| if self.characters_involved: |
| parts.append(f"涉及角色:{'、'.join(self.characters_involved)}") |
| if self.location: |
| parts.append(f"地点:{self.location}") |
| if self.mood: |
| parts.append(f"基调:{self.mood}") |
| if self.cliffhanger: |
| parts.append(f"章末悬念:{self.cliffhanger}") |
| return "\n".join(parts) |
|
|
|
|
| @dataclass |
| class Chapter: |
| """章节内容""" |
| outline: ChapterOutline = field(default_factory=ChapterOutline) |
| """章节大纲""" |
| content: str = "" |
| """章节正文""" |
| word_count: int = 0 |
| """字数""" |
| status: str = "draft" |
| """状态:draft/complete/revised""" |
| created_at: float = field(default_factory=time.time) |
| """创建时间""" |
| revised_at: float = 0.0 |
| """修订时间""" |
|
|
| def count_words(self) -> int: |
| """统计字数""" |
| self.word_count = len(re.findall(r"[\u4e00-\u9fff]|[a-zA-Z]+", self.content)) |
| return self.word_count |
|
|
|
|
| |
|
|
| class NovelProject: |
| """小说项目管理类""" |
|
|
| def __init__(self, title: str = "", genre: str = "奇幻"): |
| """ |
| 初始化小说项目。 |
| |
| Args: |
| title: 小说标题 |
| genre: 类型 |
| """ |
| self.id = str(uuid.uuid4())[:8] |
| self.title = title |
| self.genre = genre |
| self.world_setting = WorldSetting(name=title) |
| self.characters: Dict[str, NovelCharacter] = {} |
| self.chapters: List[Chapter] = [] |
| self.plot_summary: str = "" |
| """整体情节梗概""" |
| self.style_guide: str = "" |
| """写作风格指南""" |
| self.notes: List[str] = [] |
| """创作笔记""" |
| self.created_at = time.time() |
| self.updated_at = time.time() |
|
|
| def add_character(self, character: NovelCharacter) -> None: |
| """添加角色""" |
| self.characters[character.name] = character |
| self.updated_at = time.time() |
|
|
| def get_character(self, name: str) -> Optional[NovelCharacter]: |
| """获取角色""" |
| return self.characters.get(name) |
|
|
| def add_chapter(self, chapter: Chapter) -> None: |
| """添加章节""" |
| self.chapters.append(chapter) |
| self.updated_at = time.time() |
|
|
| def get_chapter(self, index: int) -> Optional[Chapter]: |
| """获取指定章节""" |
| if 0 <= index < len(self.chapters): |
| return self.chapters[index] |
| return None |
|
|
| def get_total_word_count(self) -> int: |
| """获取总字数""" |
| return sum(ch.word_count for ch in self.chapters) |
|
|
| def build_context_prompt(self) -> str: |
| """ |
| 构建完整的上下文提示词(用于AI生成章节)。 |
| |
| Returns: |
| 包含世界观、角色、前文摘要的完整提示词 |
| """ |
| parts: List[str] = [] |
|
|
| |
| if self.world_setting.name: |
| parts.append(self.world_setting.to_prompt()) |
|
|
| |
| if self.plot_summary: |
| parts.append(f"\n【情节梗概】\n{self.plot_summary}") |
|
|
| |
| if self.style_guide: |
| parts.append(f"\n【写作风格】\n{self.style_guide}") |
|
|
| |
| if self.characters: |
| parts.append("\n【角色信息】") |
| for name, char in self.characters.items(): |
| parts.append(char.to_prompt()) |
|
|
| |
| recent_chapters = self.chapters[-3:] |
| if recent_chapters: |
| parts.append("\n【前文回顾】") |
| for ch in recent_chapters: |
| parts.append(f"第{ch.outline.chapter_number}章 {ch.outline.title}:{ch.outline.summary}") |
|
|
| return "\n\n".join(parts) |
|
|
| def save(self, save_path: str) -> bool: |
| """保存小说项目到文件""" |
| try: |
| data = { |
| "id": self.id, |
| "title": self.title, |
| "genre": self.genre, |
| "world_setting": self.world_setting.to_dict(), |
| "characters": {name: ch.to_dict() for name, ch in self.characters.items()}, |
| "chapters": [ |
| { |
| "outline": { |
| "chapter_number": ch.outline.chapter_number, |
| "title": ch.outline.title, |
| "summary": ch.outline.summary, |
| "key_events": ch.outline.key_events, |
| "characters_involved": ch.outline.characters_involved, |
| "location": ch.outline.location, |
| "mood": ch.outline.mood, |
| "cliffhanger": ch.outline.cliffhanger, |
| }, |
| "content": ch.content, |
| "word_count": ch.word_count, |
| "status": ch.status, |
| } |
| for ch in self.chapters |
| ], |
| "plot_summary": self.plot_summary, |
| "style_guide": self.style_guide, |
| "notes": self.notes, |
| } |
| os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) |
| with open(save_path, "w", encoding="utf-8") as f: |
| json.dump(data, f, ensure_ascii=False, indent=2) |
| logger.info(f"小说项目已保存: {save_path}") |
| return True |
| except Exception as e: |
| logger.error(f"保存小说项目失败: {e}") |
| return False |
|
|
| @classmethod |
| def load(cls, file_path: str) -> Optional["NovelProject"]: |
| """从文件加载小说项目""" |
| try: |
| with open(file_path, "r", encoding="utf-8") as f: |
| data = json.load(f) |
|
|
| project = cls(title=data.get("title", ""), genre=data.get("genre", "奇幻")) |
| project.id = data.get("id", project.id) |
|
|
| |
| ws = data.get("world_setting", {}) |
| project.world_setting = WorldSetting(**ws) |
|
|
| |
| for name, ch_data in data.get("characters", {}).items(): |
| project.characters[name] = NovelCharacter(**ch_data) |
|
|
| |
| for ch_data in data.get("chapters", []): |
| outline_data = ch_data.get("outline", {}) |
| outline = ChapterOutline(**outline_data) |
| chapter = Chapter( |
| outline=outline, |
| content=ch_data.get("content", ""), |
| word_count=ch_data.get("word_count", 0), |
| status=ch_data.get("status", "draft"), |
| ) |
| project.chapters.append(chapter) |
|
|
| project.plot_summary = data.get("plot_summary", "") |
| project.style_guide = data.get("style_guide", "") |
| project.notes = data.get("notes", []) |
|
|
| logger.info(f"小说项目已加载: {project.title}") |
| return project |
| except Exception as e: |
| logger.error(f"加载小说项目失败: {e}") |
| return None |
|
|
|
|
| |
|
|
| class NovelCreatorModule: |
| """小说创作模块 |
| |
| 提供完整的小说创作工作流: |
| 1. 创建项目并设定世界观和角色 |
| 2. 生成整体情节梗概 |
| 3. 生成章节大纲 |
| 4. 基于大纲生成章节内容 |
| 5. 情节一致性检查和修订 |
| |
| 用法: |
| creator = NovelCreatorModule(inference_engine) |
| project = creator.create_project("星辰物语", "奇幻") |
| creator.set_world(project, "魔幻大陆", "中世纪奇幻") |
| creator.add_character(project, NovelCharacter(name="艾尔", ...)) |
| outline = creator.generate_outline(project, total_chapters=30) |
| chapter = creator.generate_chapter(project, chapter_number=1) |
| """ |
|
|
| |
| STYLE_TEMPLATES = { |
| "轻小说": "轻松明快的语言风格,带有内心独白和吐槽,节奏紧凑,适合用对话推进剧情。", |
| "玄幻": "大气磅礴的叙事风格,注重世界观描写,战斗场景要有气势,修炼体系要详细。", |
| "科幻": "逻辑严谨的叙事,注重科技细节描写,未来感十足的想象,带有哲学思辨。", |
| "言情": "细腻入微的情感描写,注重心理活动和氛围营造,甜蜜与虐心交织。", |
| "悬疑": "层层递进的叙事,注重线索铺垫和悬念设置,反转要出人意料又合情合理。", |
| "日常": "温馨平淡的叙事,注重生活细节和人物互动,轻松治愈的氛围。", |
| "奇幻": "充满想象力的世界观,英雄史诗般的叙事,注重冒险和友情描写。", |
| "历史": "考据严谨的历史背景,古风韵味十足的语言,以小人物视角展现大时代。", |
| } |
|
|
| def __init__(self, inference_engine=None): |
| """ |
| 初始化小说创作模块。 |
| |
| Args: |
| inference_engine: 霜云推理引擎实例(为None时使用模板生成) |
| """ |
| self.inference = inference_engine |
| self.projects: Dict[str, NovelProject] = {} |
|
|
| def create_project(self, title: str, genre: str = "奇幻") -> NovelProject: |
| """ |
| 创建新的小说项目。 |
| |
| Args: |
| title: 小说标题 |
| genre: 类型 |
| |
| Returns: |
| NovelProject实例 |
| """ |
| project = NovelProject(title=title, genre=genre) |
| self.projects[project.id] = project |
|
|
| |
| if genre in self.STYLE_TEMPLATES: |
| project.style_guide = self.STYLE_TEMPLATES[genre] |
|
|
| logger.info(f"创建小说项目: 「{title}」 ({genre})") |
| return project |
|
|
| def set_world( |
| self, |
| project: NovelProject, |
| name: str, |
| genre: str = "", |
| era: str = "", |
| geography: str = "", |
| magic_system: str = "", |
| **kwargs, |
| ) -> WorldSetting: |
| """ |
| 设定世界观。 |
| |
| Args: |
| project: 小说项目 |
| name: 世界名称 |
| genre: 类型 |
| era: 时代 |
| geography: 地理 |
| magic_system: 力量体系 |
| **kwargs: 其他设定参数 |
| |
| Returns: |
| WorldSetting实例 |
| """ |
| project.world_setting.name = name |
| if genre: |
| project.world_setting.genre = genre |
| if era: |
| project.world_setting.era = era |
| if geography: |
| project.world_setting.geography = geography |
| if magic_system: |
| project.world_setting.magic_system = magic_system |
| for key, val in kwargs.items(): |
| if val: |
| project.world_setting.custom_settings[key] = str(val) |
|
|
| logger.info(f"设定世界观: {name}") |
| return project.world_setting |
|
|
| def add_character(self, project: NovelProject, character: NovelCharacter) -> None: |
| """ |
| 添加角色到项目。 |
| |
| Args: |
| project: 小说项目 |
| character: 角色对象 |
| """ |
| project.add_character(character) |
| logger.info(f"添加角色: {character.name} ({character.role_type})") |
|
|
| def generate_outline( |
| self, |
| project: NovelProject, |
| total_chapters: int = 10, |
| style: str = "", |
| ) -> List[ChapterOutline]: |
| """ |
| 生成小说章节大纲。 |
| |
| Args: |
| project: 小说项目 |
| total_chapters: 总章节数 |
| style: 额外风格要求 |
| |
| Returns: |
| 章节大纲列表 |
| """ |
| |
| if self.inference: |
| prompt = ( |
| f"请为一部{project.genre}类型、标题为「{project.title}」的小说," |
| f"生成{total_chapters}章的章节大纲。\n\n" |
| ) |
| prompt += project.build_context_prompt() |
| if style: |
| prompt += f"\n\n风格要求:{style}" |
| prompt += ( |
| f"\n\n请按以下格式生成每章大纲:\n" |
| f"第X章 标题 | 摘要 | 关键事件 | 涉及角色 | 地点 | 氛围 | 章末悬念" |
| ) |
|
|
| response = self.inference.generate(prompt) |
| return self._parse_outline_response(response, total_chapters) |
|
|
| |
| return self._generate_template_outline(project, total_chapters) |
|
|
| def _generate_template_outline( |
| self, project: NovelProject, total_chapters: int |
| ) -> List[ChapterOutline]: |
| """ |
| 基于模板生成章节大纲(无推理引擎时的回退方案)。 |
| |
| Args: |
| project: 小说项目 |
| total_chapters: 章节数 |
| |
| Returns: |
| 大纲列表 |
| """ |
| outlines: List[ChapterOutline] = [] |
|
|
| |
| act1_end = max(1, total_chapters // 4) |
| act2_end = max(act1_end + 1, total_chapters * 3 // 4) |
|
|
| for i in range(1, total_chapters + 1): |
| outline = ChapterOutline(chapter_number=i) |
|
|
| if i == 1: |
| outline.title = "序章:命运的开始" |
| outline.summary = "介绍世界背景和主角的日常生活,暗示即将到来的变故" |
| outline.mood = "平静中暗藏波澜" |
| outline.key_events = ["世界观介绍", "主角出场", "伏笔埋设"] |
| elif i <= act1_end: |
| outline.title = f"第{i}章:启程" |
| outline.summary = "主角遇到关键事件,被迫离开舒适区,踏上冒险旅程" |
| outline.mood = "紧张、期待" |
| outline.key_events = ["冲突发生", "获得同伴", "开始旅程"] |
| elif i <= act2_end: |
| outline.title = f"第{i}章:试炼" |
| outline.summary = "主角面对各种挑战和敌人,逐渐成长并获得力量" |
| outline.mood = "热血、悲壮" |
| outline.key_events = ["战斗场景", "获得新能力", "重要抉择"] |
| elif i < total_chapters: |
| outline.title = f"第{i}章:高潮" |
| outline.summary = "最终决战临近,所有线索汇聚,真相揭晓" |
| outline.mood = "悲壮、激昂" |
| outline.key_events = ["真相揭示", "最终决战", "牺牲与勇气"] |
| else: |
| outline.title = f"终章:新的开始" |
| outline.summary = "故事收束,主角达成目标,展望新的未来" |
| outline.mood = "温暖、感动" |
| outline.key_events = ["决战结局", "回归日常", "展望未来"] |
|
|
| outlines.append(outline) |
|
|
| return outlines |
|
|
| def _parse_outline_response( |
| self, response: str, total_chapters: int |
| ) -> List[ChapterOutline]: |
| """ |
| 解析AI生成的大纲响应。 |
| |
| Args: |
| response: AI生成的文本 |
| total_chapters: 期望的章节数 |
| |
| Returns: |
| 大纲列表 |
| """ |
| outlines: List[ChapterOutline] = [] |
|
|
| |
| lines = response.split("\n") |
| current_outline = None |
|
|
| for line in lines: |
| line = line.strip() |
| |
| chapter_match = re.match(r"第(\d+)[章回]?\s*[::]\s*(.+)", line) |
| if chapter_match: |
| if current_outline: |
| outlines.append(current_outline) |
| num = int(chapter_match.group(1)) |
| title = chapter_match.group(2).strip() |
| current_outline = ChapterOutline( |
| chapter_number=num, |
| title=title, |
| summary=line, |
| ) |
| elif current_outline and line: |
| current_outline.summary += "\n" + line |
|
|
| if current_outline: |
| outlines.append(current_outline) |
|
|
| |
| if not outlines: |
| paragraphs = [p.strip() for p in response.split("\n\n") if p.strip()] |
| for i, para in enumerate(paragraphs[:total_chapters], 1): |
| outlines.append(ChapterOutline( |
| chapter_number=i, |
| title=f"第{i}章", |
| summary=para, |
| )) |
|
|
| return outlines |
|
|
| def generate_chapter_content( |
| self, |
| project: NovelProject, |
| chapter_outline: ChapterOutline, |
| target_words: int = 3000, |
| ) -> Chapter: |
| """ |
| 基于大纲生成章节内容。 |
| |
| Args: |
| project: 小说项目 |
| chapter_outline: 章节大纲 |
| target_words: 目标字数 |
| |
| Returns: |
| Chapter对象(包含生成的正文) |
| """ |
| chapter = Chapter(outline=chapter_outline, status="draft") |
|
|
| if self.inference: |
| |
| prompt = project.build_context_prompt() |
| prompt += f"\n\n{chapter_outline.to_prompt()}" |
| prompt += ( |
| f"\n\n请根据以上设定和大纲,撰写第{chapter_outline.chapter_number}章的正文内容。" |
| f"目标字数约{target_words}字。注意保持与已有内容的一致性。" |
| f"请直接输出正文内容,不需要重复章节标题。" |
| ) |
|
|
| content = self.inference.generate( |
| prompt, |
| max_new_tokens=min(target_words * 3, 4096), |
| temperature=0.85, |
| top_p=0.92, |
| ) |
| chapter.content = content.strip() |
| else: |
| |
| chapter.content = self._generate_template_chapter(project, chapter_outline, target_words) |
|
|
| chapter.count_words() |
| chapter.status = "complete" |
| project.add_chapter(chapter) |
|
|
| logger.info( |
| f"生成第{chapter_outline.chapter_number}章「{chapter_outline.title}」" |
| f" - {chapter.word_count}字" |
| ) |
| return chapter |
|
|
| def _generate_template_chapter( |
| self, |
| project: NovelProject, |
| outline: ChapterOutline, |
| target_words: int, |
| ) -> str: |
| """生成模板章节内容(回退方案)""" |
| parts: List[str] = [ |
| f"# {outline.title}", |
| "", |
| ] |
|
|
| |
| parts.append(outline.summary) |
| parts.append("") |
|
|
| if outline.key_events: |
| for event in outline.key_events: |
| parts.append(f"\n围绕「{event}」这一关键事件展开叙述...\n") |
|
|
| |
| filler_scenes = [ |
| "风轻轻吹过,带来了远方若有若无的花香。", |
| "他抬起头,望向天边渐渐泛白的地平线。", |
| "一切似乎都在按预定的轨迹发展——至少表面上是这样。", |
| "远处传来了马蹄声,由远及近,打破了清晨的宁静。", |
| "她微微蹙眉,直觉告诉她,事情没有那么简单。", |
| ] |
|
|
| current_words = len("".join(parts)) |
| scene_idx = 0 |
| while current_words < target_words: |
| scene = filler_scenes[scene_idx % len(filler_scenes)] |
| parts.append(scene) |
| current_words += len(scene) |
| scene_idx += 1 |
|
|
| return "\n".join(parts) |
|
|
| def check_consistency(self, project: NovelProject) -> List[Dict[str, str]]: |
| """ |
| 情节一致性检查。 |
| |
| 检查项: |
| - 角色名称一致性 |
| - 时间线合理性 |
| - 设定冲突 |
| - 角色性格一致性 |
| |
| Args: |
| project: 小说项目 |
| |
| Returns: |
| 问题列表,每项包含 {"type": ..., "description": ..., "chapter": ...} |
| """ |
| issues: List[Dict[str, str]] = [] |
|
|
| |
| character_names = set(project.characters.keys()) |
| for ch in project.chapters: |
| |
| for name in character_names: |
| char = project.characters[name] |
| |
| for alias in char.alias: |
| if alias not in ch.content and name not in ch.content: |
| continue |
|
|
| |
| mentioned = set() |
| for name in character_names: |
| if name in ch.content: |
| mentioned.add(name) |
| involved_set = set(ch.outline.characters_involved) |
| unexpected = mentioned - involved_set |
| if unexpected and involved_set: |
| issues.append({ |
| "type": "角色一致性", |
| "description": f"第{ch.outline.chapter_number}章中出现了角色 {unexpected} 但不在涉及角色列表中", |
| "chapter": str(ch.outline.chapter_number), |
| }) |
|
|
| |
| locations: List[str] = [] |
| for ch in project.chapters: |
| if ch.outline.location: |
| locations.append(ch.outline.location) |
|
|
| |
| titles = [ch.outline.title for ch in project.chapters] |
| seen_titles: Dict[str, int] = {} |
| for title in titles: |
| seen_titles[title] = seen_titles.get(title, 0) + 1 |
| for title, count in seen_titles.items(): |
| if count > 1: |
| issues.append({ |
| "type": "重复标题", |
| "description": f"章节标题「{title}」出现了 {count} 次", |
| "chapter": "全部", |
| }) |
|
|
| logger.info(f"一致性检查完成: 发现 {len(issues)} 个潜在问题") |
| return issues |
|
|
| def generate_character_backstory( |
| self, |
| project: NovelProject, |
| character: NovelCharacter, |
| ) -> str: |
| """ |
| 为角色生成详细的背景故事。 |
| |
| Args: |
| project: 小说项目 |
| character: 角色 |
| |
| Returns: |
| 背景故事文本 |
| """ |
| if self.inference: |
| prompt = ( |
| f"请为以下角色生成详细的背景故事,要求与世界观设定一致:\n\n" |
| f"{project.world_setting.to_prompt()}\n\n" |
| f"{character.to_prompt()}\n\n" |
| f"请撰写详细的背景故事(约500-800字)," |
| f"包括童年经历、成长过程、关键转折点以及性格成因。" |
| ) |
| return self.inference.generate(prompt, max_new_tokens=1500, temperature=0.85) |
|
|
| |
| return ( |
| f"{character.name}是{project.world_setting.name}中的一名{character.role_type}。" |
| f"从小生活在{character.background or '一个普通的城镇'}中," |
| f"因为{character.goals or '某个重要的契机'}踏上了属于自己的旅程。" |
| f"ta的性格{character.personality or '坚毅而温柔'}," |
| f"拥有{'、'.join(character.abilities) if character.abilities else '独特的能力'}。" |
| ) |
|
|