""" 霜云(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()) # 前文摘要(取最近3章) 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: 章节大纲列表 """ # 如果有推理引擎,使用AI生成 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() # 匹配 "第X章" 模式 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 # 简单检查:角色在某章中被提及但不在involved列表中 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 '独特的能力'}。" )