diff --git a/chapter_directory_parser.py b/chapter_directory_parser.py index 796b9f5..696cbb8 100644 --- a/chapter_directory_parser.py +++ b/chapter_directory_parser.py @@ -21,13 +21,19 @@ def parse_chapter_blueprint(blueprint_text: str): chunks = re.split(r'\n\s*\n', blueprint_text.strip()) results = [] - chapter_number_pattern = re.compile(r'^第\s*(\d+)\s*章\s*-\s*\[(.*?)\]') # 捕获章号与标题 - role_pattern = re.compile(r'^本章定位:\s*(.*)$') - purpose_pattern = re.compile(r'^核心作用:\s*(.*)$') - suspense_pattern = re.compile(r'^悬念密度:\s*(.*)$') - foreshadow_pattern = re.compile(r'^伏笔操作:\s*(.*)$') - twist_pattern = re.compile(r'^认知颠覆:\s*(.*)$') - summary_pattern = re.compile(r'^本章简述:\s*\[(.*)\]$') + # 兼容是否使用方括号包裹章节标题 + # 例如: + # 第1章 - 紫极光下的预兆 + # 或 + # 第1章 - [紫极光下的预兆] + chapter_number_pattern = re.compile(r'^第\s*(\d+)\s*章\s*-\s*\[?(.*?)\]?$') + + role_pattern = re.compile(r'^本章定位:\s*\[?(.*)\]?$') + purpose_pattern = re.compile(r'^核心作用:\s*\[?(.*)\]?$') + suspense_pattern = re.compile(r'^悬念密度:\s*\[?(.*)\]?$') + foreshadow_pattern = re.compile(r'^伏笔操作:\s*\[?(.*)\]?$') + twist_pattern = re.compile(r'^认知颠覆:\s*\[?(.*)\]?$') + summary_pattern = re.compile(r'^本章简述:\s*\[?(.*)\]?$') for chunk in chunks: lines = chunk.strip().splitlines() @@ -44,9 +50,9 @@ def parse_chapter_blueprint(blueprint_text: str): chapter_summary = "" # 先匹配第一行(或前几行),找到章号和标题 - header_match = chapter_number_pattern.match(lines[0].strip()) if lines else None + header_match = chapter_number_pattern.match(lines[0].strip()) if not header_match: - # 不符合格式,跳过 + # 不符合“第X章 - 标题”的格式,跳过 continue chapter_number = int(header_match.group(1)) diff --git a/novel_generator.py b/novel_generator.py index f29076c..ebe885c 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -431,8 +431,6 @@ def Chapter_blueprint_generate( return # 从内容中尽量提取 number_of_chapters - # 如果之前已经存储了 number_of_chapters,可以在外面传入,这里做简化: - # 这里用正则或者其他逻辑提取,但演示时直接写 10 也可 match_chaps = re.search(r'约(\d+)章', architecture_text) if match_chaps: number_of_chapters = int(match_chaps.group(1)) @@ -440,10 +438,7 @@ def Chapter_blueprint_generate( number_of_chapters = 10 # fallback # 提取三幕式文本 - # 在写入时,我们将 4) 三幕式情节架构 作为传给 prompt 的核心 - # 这里做一个简易匹配 plot_arch_text = "" - # 假设 "#=== 4) 三幕式情节架构 ===" 是分隔点 pat_plot = r'#=== 4\) 三幕式情节架构 ===\n([\s\S]+)$' m = re.search(pat_plot, architecture_text) if m: @@ -556,7 +551,6 @@ def update_plot_arcs( # ========== 3) 生成章节草稿 ========== - def generate_chapter_draft( api_key: str, base_url: str, @@ -570,6 +564,10 @@ def generate_chapter_draft( key_items: str, scene_location: str, time_constraint: str, + embedding_api_key: str, + embedding_url: str, + embedding_interface_format: str, + embedding_model_name: str, embedding_retrieval_k: int = 2 ) -> str: """ @@ -577,7 +575,7 @@ def generate_chapter_draft( - novel_architecture 取自 Novel_architecture.txt - blueprint 取自 Novel_directory.txt - global_summary, character_state 分别取自全局摘要、角色状态文件 - - 向量库检索上下文 + - 从向量库检索上下文(embedding_*参数) - 用户还可以额外提供四个可选元素:核心人物、关键道具、空间坐标、时间压力 """ @@ -609,25 +607,21 @@ def generate_chapter_draft( recent_3_texts = get_last_n_chapters_text(chapters_dir, novel_number, n=3) merged_query_str = "回顾剧情:\n" + "\n".join(recent_3_texts) + "\n" + user_guidance - # 4) 检索向量库上下文 + # 4) 检索向量库上下文 (使用embedding_*参数) relevant_context = get_relevant_context_from_vector_store( - api_key=api_key, - base_url=base_url, + api_key=embedding_api_key, + base_url=embedding_url, query=merged_query_str, - embedding_model_name=model_name, + interface_format=embedding_interface_format, + embedding_model_name=embedding_model_name, filepath=filepath, k=embedding_retrieval_k ) - if not relevant_context.strip(): relevant_context = "(无检索到的上下文)" - # 5) 构造prompt,调用 scene_dynamics_prompt - # 在这里,我们拆分架构文本,以便给模型提供: - # - “世界观”与“小说设定”可以从 arch_file 中的相应片段读取 - # 这里为了简化,直接把 novel_architecture_text 整体塞入 novel_setting - # 也可更精细地拆分 "#=== 3) 世界观 ===" 片段给 world_building - # 下方仅作示例。 + # 5) 构造prompt + # 拆分 world_building_text, novel_architecture_text 等等 world_building_text = "" match_world = re.search(r'#=== 3\) 世界观 ===\n([\s\S]+?)\n#===', novel_architecture_text) if match_world: @@ -658,9 +652,8 @@ def generate_chapter_draft( character_state=character_state_text ) - # 因为我们还想让模型了解向量库检索到的上下文,可以合并到最后 + # 合并检索到的上下文和用户指导 prompt_text += f"\n\n【检索到的上下文】\n{relevant_context}" - # 也可合并用户指导 prompt_text += f"\n\n【用户指导】\n{user_guidance}\n" model = ChatOpenAI( @@ -674,8 +667,7 @@ def generate_chapter_draft( if not chapter_content.strip(): logging.warning("Generated chapter draft is empty.") - # 6) 写入 chapters 目录 - chapters_dir = os.path.join(filepath, "chapters") + # 6) 写入 chapters os.makedirs(chapters_dir, exist_ok=True) chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") @@ -695,6 +687,9 @@ def finalize_chapter( model_name: str, temperature: float, filepath: str, + embedding_api_key: str, + embedding_url: str, + embedding_interface_format: str, embedding_model_name: str ): """ @@ -707,7 +702,7 @@ def finalize_chapter( logging.warning(f"Chapter {novel_number} is empty, cannot finalize.") return - # 如果长度比目标少很多,可考虑在此扩写 + # 若篇幅过短,可尝试扩写 if len(chapter_text) < 0.6 * word_number: chapter_text = enrich_chapter_text(chapter_text, word_number, api_key, base_url, model_name, temperature) clear_file_content(chapter_file) @@ -750,12 +745,13 @@ def finalize_chapter( clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) - # 3) 更新向量库 + # 3) 更新向量库 (embedding相关) update_vector_store( - api_key=api_key, - base_url=base_url, + api_key=embedding_api_key, + base_url=embedding_url, new_chapter=chapter_text, - model_name=embedding_model_name, # 用于embedding + interface_format=embedding_interface_format, + embedding_model_name=embedding_model_name, filepath=filepath ) @@ -827,15 +823,14 @@ def advanced_split_content(content: str, return final_segments def import_knowledge_file( - api_key: str, - base_url: str, - interface_format: str, + embedding_api_key: str, + embedding_url: str, + embedding_interface_format: str, embedding_model_name: str, file_path: str, - embedding_base_url: str, filepath: str ): - logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}") + logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {embedding_interface_format}, 模型: {embedding_model_name}") if not os.path.exists(file_path): logging.warning(f"知识库文件不存在: {file_path}") return @@ -847,20 +842,20 @@ def import_knowledge_file( paragraphs = advanced_split_content(content) - # 若向量库不存在则初始化,否则追加 + # 尝试加载已有的向量库 store = load_vector_store( - api_key=api_key, - base_url=base_url if base_url else "http://localhost:11434/v1", - interface_format=interface_format, + api_key=embedding_api_key, + base_url=embedding_url if embedding_url else "http://localhost:11434/v1", + interface_format=embedding_interface_format, embedding_model_name=embedding_model_name, filepath=filepath ) if not store: logging.info("Vector store does not exist. Initializing a new one for knowledge import...") init_vector_store( - api_key=api_key, - base_url=base_url if base_url else "http://localhost:11434/v1", - interface_format=interface_format, + api_key=embedding_api_key, + base_url=embedding_url if embedding_url else "http://localhost:11434/v1", + interface_format=embedding_interface_format, embedding_model_name=embedding_model_name, texts=paragraphs, filepath=filepath diff --git a/ui.py b/ui.py index 2a4fb1d..425dd40 100644 --- a/ui.py +++ b/ui.py @@ -597,11 +597,13 @@ class NovelGeneratorGUI: def task(): self.disable_button_safe(self.btn_generate_chapter) try: + # LLM相关 api_key = self.api_key_var.get().strip() base_url = self.base_url_var.get().strip() model_name = self.model_name_var.get().strip() temperature = self.temperature_var.get() + # 章节信息 chap_num = self.safe_get_int(self.chapter_num_var, 1) word_number = self.safe_get_int(self.word_number_var, 3000) user_guidance = self.user_guide_text.get("0.0", "end").strip() @@ -612,6 +614,10 @@ class NovelGeneratorGUI: scene_loc = self.scene_location_var.get().strip() time_constr = self.time_constraint_var.get().strip() + # Embedding相关 + embedding_api_key = self.embedding_api_key_var.get().strip() + embedding_url = self.embedding_url_var.get().strip() + embedding_interface_format = self.embedding_interface_format_var.get().strip() embedding_model_name = self.embedding_model_name_var.get().strip() embedding_k = self.safe_get_int(self.embedding_retrieval_k_var, 4) @@ -629,6 +635,10 @@ class NovelGeneratorGUI: key_items=key_items, scene_location=scene_loc, time_constraint=time_constr, + embedding_api_key=embedding_api_key, + embedding_url=embedding_url, + embedding_interface_format=embedding_interface_format, + embedding_model_name=embedding_model_name, embedding_retrieval_k=embedding_k ) if draft_text: @@ -659,13 +669,19 @@ class NovelGeneratorGUI: def task(): self.disable_button_safe(self.btn_finalize_chapter) try: + # LLM相关 api_key = self.api_key_var.get().strip() base_url = self.base_url_var.get().strip() model_name = self.model_name_var.get().strip() temperature = self.temperature_var.get() + # Embedding相关 + embedding_api_key = self.embedding_api_key_var.get().strip() + embedding_url = self.embedding_url_var.get().strip() + embedding_interface_format = self.embedding_interface_format_var.get().strip() embedding_model_name = self.embedding_model_name_var.get().strip() + # 章节参数 chap_num = self.safe_get_int(self.chapter_num_var, 1) word_number = self.safe_get_int(self.word_number_var, 3000) @@ -686,6 +702,9 @@ class NovelGeneratorGUI: model_name=model_name, temperature=temperature, filepath=filepath, + embedding_api_key=embedding_api_key, + embedding_url=embedding_url, + embedding_interface_format=embedding_interface_format, embedding_model_name=embedding_model_name ) self.safe_log(f"✅ 第{chap_num}章定稿完成(已更新全局摘要、角色状态、向量库)。") @@ -754,19 +773,18 @@ class NovelGeneratorGUI: def task(): self.disable_button_safe(self.btn_import_knowledge) try: - api_key = self.embedding_api_key_var.get().strip() - base_url = self.embedding_url_var.get().strip() - interface_format = self.embedding_interface_format_var.get().strip() - embedding_model_name = self.embedding_model_name_var.get().strip() + emb_api_key = self.embedding_api_key_var.get().strip() + emb_url = self.embedding_url_var.get().strip() + emb_format = self.embedding_interface_format_var.get().strip() + emb_model = self.embedding_model_name_var.get().strip() self.safe_log(f"开始导入知识库文件: {selected_file}") import_knowledge_file( - api_key=api_key, - base_url=base_url, - interface_format=interface_format, - embedding_model_name=embedding_model_name, + embedding_api_key=emb_api_key, + embedding_url=emb_url, + embedding_interface_format=emb_format, + embedding_model_name=emb_model, file_path=selected_file, - embedding_base_url=base_url, filepath=self.filepath_var.get().strip() ) self.safe_log("✅ 知识库文件导入完成。")