From 3dd81ecb12fcaa8a0cc90a0d47902caea7a45261 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 14:39:30 +0800 Subject: [PATCH] again --- novel_generator.py | 243 ++++++++++++++++++++++++++------------------- ui.py | 165 +++++++++++++++++------------- 2 files changed, 238 insertions(+), 170 deletions(-) diff --git a/novel_generator.py b/novel_generator.py index 42fa110..77b4b56 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -5,7 +5,7 @@ import logging import re from typing import Dict, List, Optional try: - from typing import TypedDict # Python 3.8+ 直接可用;若是3.7可改用 typing_extensions + from typing import TypedDict except ImportError: from typing_extensions import TypedDict @@ -33,7 +33,15 @@ from prompt_definitions import ( from embedding_ollama import OllamaEmbeddings from chapter_directory_parser import get_chapter_info_from_directory -# ============ 适配接口的判断函数 ============ +# ============ 日志配置 ============ +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +def debug_log(prompt: str, response_content: str): + """打印Prompt与Response,可根据需要保留或去掉。""" + logging.info(f"\n[Prompt >>>] {prompt}\n") + logging.info(f"[Response >>>] {response_content}\n") + +# ============ 接口判断函数 ============ def is_using_ollama_api(interface_format: str, base_url: str) -> bool: """ 当 interface_format == "Ollama" 时返回 True @@ -60,13 +68,14 @@ def create_embeddings_object( """ 根据用户在UI中配置的参数,返回对应的 embeddings 对象。 - 当 interface_format = "Ollama" => OllamaEmbeddings(...) - - 当 interface_format = "OpenAI" => OpenAIEmbeddings - - 当 interface_format = "ML Studio" => OpenAIEmbeddings + - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings + - 其它情况可自行扩展 """ if is_using_ollama_api(interface_format, embed_url): # 使用 Ollama Embeddings return OllamaEmbeddings(model_name=embedding_model_name, base_url=embed_url) elif is_using_ml_studio_api(interface_format, base_url): + # 示例同用 OpenAIEmbeddings return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url) else: # 默认使用 OpenAIEmbeddings @@ -75,12 +84,7 @@ def create_embeddings_object( # ============ 日志配置 ============ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") -def debug_log(prompt: str, response_content: str): - """在控制台打印或记录下每次Prompt与Response,[调试]""" - logging.info(f"\n[Prompt >>>] {prompt}\n") - logging.info(f"[Response >>>] {response_content}\n") - -# ============ 向量检索相关 ============ +# ============ 向量库相关 ============ VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): @@ -106,20 +110,25 @@ def clear_vector_store(): logging.info("No vector store found to clear.") def init_vector_store( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - texts: List[str], - embedding_base_url: str = "" - ) -> Chroma: + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + texts: List[str], + embedding_base_url: str = "" +) -> Chroma: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 - 如果不存在该目录,会自动创建。 - 如果 embedding_base_url 不为空,则使用它做为embedding的base,否则默认base_url。 + embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url """ embed_url = embedding_base_url if embedding_base_url else base_url - embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, embedding_model_name) + embeddings = create_embeddings_object( + api_key=api_key, + base_url=base_url, + embed_url=embed_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name + ) documents = [Document(page_content=t) for t in texts] vectorstore = Chroma.from_documents( documents, @@ -130,35 +139,55 @@ def init_vector_store( return vectorstore def load_vector_store( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - embedding_base_url: str = "" - ) -> Optional[Chroma]: + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + embedding_base_url: str = "" +) -> Optional[Chroma]: """ 读取已存在的向量库。若不存在则返回 None。 - 同样支持可选的 embedding_base_url。 """ if not os.path.exists(VECTOR_STORE_DIR): return None embed_url = embedding_base_url if embedding_base_url else base_url - embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, embedding_model_name) + embeddings = create_embeddings_object( + api_key=api_key, + base_url=base_url, + embed_url=embed_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name + ) return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings) def update_vector_store( - api_key: str, - base_url: str, - new_chapter: str, - interface_format: str, - embedding_model_name: str, - embedding_base_url: str = "" - ) -> None: - """将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。""" - store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url) + api_key: str, + base_url: str, + new_chapter: str, + interface_format: str = "OpenAI", + embedding_model_name: str = "", + embedding_base_url: str = "" +) -> None: + """ + 将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。 + """ + store = load_vector_store( + api_key=api_key, + base_url=base_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name, + embedding_base_url=embedding_base_url + ) if not store: logging.info("Vector store does not exist. Initializing a new one...") - init_vector_store(api_key, base_url,interface_format, embedding_model_name, [new_chapter], embedding_base_url) + init_vector_store( + api_key=api_key, + base_url=base_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name, + texts=[new_chapter], + embedding_base_url=embedding_base_url + ) return new_doc = Document(page_content=new_chapter) @@ -166,19 +195,25 @@ def update_vector_store( store.persist() def get_relevant_context_from_vector_store( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - query: str, - k: int = 2, - embedding_base_url: str = "" - ) -> str: + api_key: str, + base_url: str, + query: str, + interface_format: str = "OpenAI", + embedding_model_name: str = "", + embedding_base_url: str = "", + k: int = 2 +) -> str: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 若向量库不存在则返回空字符串。 """ - store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url) + store = load_vector_store( + api_key=api_key, + base_url=base_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name, + embedding_base_url=embedding_base_url + ) if not store: logging.warning("Vector store not found. Returning empty context.") return "" @@ -186,6 +221,7 @@ def get_relevant_context_from_vector_store( combined = "\n".join([d.page_content for d in docs]) return combined + # ============ 多步生成:设置 & 目录 ============ class OverallState(TypedDict): @@ -324,7 +360,6 @@ def Novel_novel_directory_generate( filename_set = os.path.join(filepath, "Novel_setting.txt") filename_novel_directory = os.path.join(filepath, "Novel_directory.txt") - # 清理文本(可根据需要去除多余字符) def clean_text(txt: str) -> str: return txt.replace('#', '').replace('*', '') @@ -336,6 +371,7 @@ def Novel_novel_directory_generate( logging.info("Novel settings and directory generated successfully.") + # ============ 获取最近N章内容,生成短期摘要 ============ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int = 3) -> List[str]: @@ -356,17 +392,37 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int def summarize_recent_chapters(model, chapters_text_list: List[str]) -> str: """ 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 - 这里仅作示例,实际可传入 ChatOpenAI 或其他模型对象,以获取真实摘要。 + 如果没有可用的模型(model=None),则退化为简单截断示例。 """ if not chapters_text_list: return "" - # 模拟返回合并摘要,这里不做真实OpenAI调用 combined_text = "\n".join(chapters_text_list) - # 简单演示:直接返回合并后的文本,或你自己实现真正的摘要逻辑 - return f"【摘要】最近几章内容:\n{combined_text[:800]}..." # 截断示例 + # 如果未传入model,就做个简单的退化输出 + if not model: + return f"【摘要-演示】\n{combined_text[:800]}..." -# ============ 新增1:记录剧情要点/未解决冲突 ============ + # 构造一个提示词(Prompt),指示模型生成精简摘要 + prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容: +{combined_text} + +请你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点。 +1.请用中文输出,不超过500字。 +2.仅回复摘要内容,不需要其他信息。 +""" + + # 调用模型获取摘要 + response = model.invoke(prompt) + if not response or not response.content.strip(): + # 若模型无响应或空,返回简单截断 + return f"【摘要-演示】\n{combined_text[:800]}..." + + # 返回模型生成的摘要文本 + return response.content.strip() + + + +# ============ 新增:更新剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ 下面是新生成的章节内容: @@ -409,6 +465,7 @@ def update_plot_arcs( debug_log(prompt, response.content) return response.content.strip() + # ============ 生成章节草稿 & 定稿 ============ def generate_chapter_draft( @@ -436,13 +493,17 @@ def generate_chapter_draft( chapter_title = chapter_info["chapter_title"] chapter_brief = chapter_info["chapter_brief"] - # 1) 从向量库检索往期上下文 - # 在此示例中,如需独立的embedding url,可自行扩展 + # 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情") relevant_context = get_relevant_context_from_vector_store( - api_key, base_url, "回顾剧情", k=2 + api_key=api_key, + base_url=base_url, + query="回顾剧情", + interface_format="OpenAI", # 若需根据 UI 选择可再传参 + embedding_model_name="", # 同上 + embedding_base_url="", + k=2 ) - # 2) 生成大纲 model = ChatOpenAI( model=model_name, api_key=api_key, @@ -450,6 +511,7 @@ def generate_chapter_draft( temperature=temperature ) + # 2) 生成大纲 outline_prompt_text = chapter_outline_prompt.format( novel_setting=novel_settings, character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, @@ -458,18 +520,11 @@ def generate_chapter_draft( chapter_title=chapter_title, chapter_brief=chapter_brief ) - - outline_prompt_text += f"\n\n【本章目录标题与简述】\n标题:{chapter_title}\n简述:{chapter_brief}\n" - outline_prompt_text += f"\n【最近几章摘要】\n{recent_chapters_summary}" + outline_prompt_text += f"\n\n【最近几章摘要】\n{recent_chapters_summary}" outline_prompt_text += f"\n\n【用户指导】\n{user_guidance if user_guidance else '(无)'}" response_outline = model.invoke(outline_prompt_text) - if not response_outline: - logging.warning("generate_chapter_draft: outline no response.") - chapter_outline = "" - else: - debug_log(outline_prompt_text, response_outline.content) - chapter_outline = response_outline.content.strip() + chapter_outline = response_outline.content.strip() if response_outline else "" outlines_dir = os.path.join(filepath, "outlines") os.makedirs(outlines_dir, exist_ok=True) @@ -487,18 +542,11 @@ def generate_chapter_draft( chapter_title=chapter_title, chapter_brief=chapter_brief ) - - writing_prompt_text += f"\n\n【本章目录标题与简述】\n标题:{chapter_title}\n简述:{chapter_brief}\n" - writing_prompt_text += f"\n【最近几章摘要】\n{recent_chapters_summary}" + writing_prompt_text += f"\n\n【最近几章摘要】\n{recent_chapters_summary}" writing_prompt_text += f"\n\n【用户指导】\n{user_guidance if user_guidance else '(无)'}" response_chapter = model.invoke(writing_prompt_text) - if not response_chapter: - logging.warning("generate_chapter_draft: writing no response.") - chapter_content = "" - else: - debug_log(writing_prompt_text, response_chapter.content) - chapter_content = response_chapter.content.strip() + chapter_content = response_chapter.content.strip() if response_chapter else "" chapters_dir = os.path.join(filepath, "chapters") os.makedirs(chapters_dir, exist_ok=True) @@ -534,16 +582,15 @@ def finalize_chapter( logging.warning(f"Chapter {novel_number} is empty, cannot finalize.") return - # 读取角色状态 & 全局摘要 & 剧情要点 character_state_file = os.path.join(filepath, "character_state.txt") global_summary_file = os.path.join(filepath, "global_summary.txt") - plot_arcs_file = os.path.join(filepath, "plot_arcs.txt") # 新增文件 + plot_arcs_file = os.path.join(filepath, "plot_arcs.txt") old_char_state = read_file(character_state_file) old_global_summary = read_file(global_summary_file) old_plot_arcs = read_file(plot_arcs_file) - # 1) 先检查字数是否过少,若少于 80% 则调用 enrich 逻辑 + # 1) 若字数明显不足,做 enrich if len(chapter_text) < 0.8 * word_number: logging.info("Chapter text seems shorter than 80% of desired length. Attempting to enrich content...") chapter_text = enrich_chapter_text( @@ -554,7 +601,6 @@ def finalize_chapter( model_name=model_name, temperature=temperature ) - # 覆盖写回文件 clear_file_content(chapter_file) save_string_to_txt(chapter_text, chapter_file) logging.info("Chapter text has been enriched and updated.") @@ -573,11 +619,7 @@ def finalize_chapter( global_summary=old_summary ) response = model.invoke(prompt) - if not response: - logging.warning("update_global_summary: No response.") - return old_summary - debug_log(prompt, response.content) - return response.content.strip() + return response.content.strip() if response else old_summary new_global_summary = update_global_summary(chapter_text, old_global_summary) @@ -588,15 +630,11 @@ def finalize_chapter( old_state=old_state ) response = model.invoke(prompt) - if not response: - logging.warning("update_character_state: No response.") - return old_state - debug_log(prompt, response.content) - return response.content.strip() + return response.content.strip() if response else old_state new_char_state = update_character_state(chapter_text, old_char_state) - # ============ 新增2: 更新剧情要点 ============= + # 4) 更新剧情要点 new_plot_arcs = update_plot_arcs( chapter_text=chapter_text, old_plot_arcs=old_plot_arcs, @@ -606,7 +644,7 @@ def finalize_chapter( temperature=temperature ) - # 4) 覆盖写入角色状态文件、全局摘要文件、剧情要点文件 + # 5) 覆盖写入文件 clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) @@ -616,10 +654,16 @@ def finalize_chapter( clear_file_content(plot_arcs_file) save_string_to_txt(new_plot_arcs, plot_arcs_file) - # 5) 更新向量检索库 - update_vector_store(api_key, base_url, chapter_text) + # 6) 更新向量库 + update_vector_store( + api_key=api_key, + base_url=base_url, + new_chapter=chapter_text, + interface_format="OpenAI", + embedding_model_name="" + ) - logging.info(f"Chapter {novel_number} has been finalized (summary & state updated, plot arcs updated, vector store updated).") + logging.info(f"Chapter {novel_number} has been finalized.") def enrich_chapter_text( chapter_text: str, @@ -639,17 +683,14 @@ def enrich_chapter_text( base_url=base_url, temperature=temperature ) - prompt = f"""\ -以下是当前章节文本,可能篇幅较短,请在保持剧情连贯的前提下进行扩写,使其更充实、生动,并尽量靠近目标 {word_number} 字数。 + prompt = f"""以下是当前章节文本,可能篇幅较短,请在保持剧情连贯的前提下进行扩写,使其更充实、生动,并尽量靠近目标 {word_number} 字数。 原章节内容: -{chapter_text} -""" +{chapter_text}""" + response = model.invoke(prompt) if not response: - logging.warning("enrich_chapter_text: No response.") - return chapter_text # 无响应时就返回原文 - debug_log(prompt, response.content) + return chapter_text return response.content.strip() # ============ 导入外部知识文本 ============ diff --git a/ui.py b/ui.py index c485834..0832f7a 100644 --- a/ui.py +++ b/ui.py @@ -31,7 +31,7 @@ class NovelGeneratorGUI: # 窗口最大化 self.master.state("zoomed") # 配置窗口大小 - self.master.geometry("1344x896") + self.master.geometry("1428x960") # 配置持久化 self.config_file = "config.json" @@ -61,7 +61,7 @@ class NovelGeneratorGUI: self.tabview.pack(fill="both", expand=True) # 创建各个Tab - self.main_tab = self.tabview.add("主功能") + self.main_tab = self.tabview.add("Main Functions") self.setting_tab = self.tabview.add("Novel Settings") self.directory_tab = self.tabview.add("Novel Directory") self.character_tab = self.tabview.add("Character State") @@ -137,25 +137,31 @@ class NovelGeneratorGUI: 右侧用于显示配置与功能按钮。 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 + + 另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。 """ - # 第一行创建一个子 TabView 放置 AI 配置 & Embedding 配置 - self.config_tabview = ctk.CTkTabview(self.right_frame, width=350, height=300) - self.config_tabview.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=5, pady=5) + self.right_frame.grid_rowconfigure(0, weight=0) + self.right_frame.grid_rowconfigure(1, weight=0) + self.right_frame.grid_rowconfigure(2, weight=1) + self.right_frame.grid_columnconfigure(0, weight=0) + self.right_frame.grid_columnconfigure(1, weight=1) - self.ai_config_tab = self.config_tabview.add("AI接口配置") - self.embeddings_config_tab = self.config_tabview.add("Embedding配置") + # ========== 1. 配置项 TabView(AI接口配置 & Embedding配置) ========== + self.config_tabview = ctk.CTkTabview(self.right_frame) + self.config_tabview.grid(row=0, column=0, columnspan=2, sticky="we", padx=5, pady=5) - # 分别构建这两个 Tab 的布局 + self.ai_config_tab = self.config_tabview.add("LLM Model settings") + self.embeddings_config_tab = self.config_tabview.add("Embedding settings") + + # 构建这两个 Tab 的布局 self.build_ai_config_tab() self.build_embeddings_config_tab() - # 下面再放其他通用参数(主题、类型等) & 功能按钮 - row_base = 1 - # row_base + 1 处构建剩余输入,如topic, genre, etc. - self.build_novel_params_area(start_row=row_base+1) + # ========== 2. 与小说相关的参数区 ========== + self.build_novel_params_area(start_row=1) - # 最后放一些主功能按钮 - self.build_main_buttons_area(start_row=row_base+10) + # ========== 3. 放主要功能按钮 & “保存/加载配置”按钮区 ========== + self.build_main_buttons_area(start_row=10) def build_ai_config_tab(self): """ @@ -167,42 +173,55 @@ class NovelGeneratorGUI: - Temperature """ # 配置网格 - for i in range(6): + for i in range(5): self.ai_config_tab.grid_rowconfigure(i, weight=0) self.ai_config_tab.grid_columnconfigure(0, weight=0) self.ai_config_tab.grid_columnconfigure(1, weight=1) + # 回调:当接口格式下拉框发生变更时,如果 Base URL 为空,则根据接口类型自动填默认值 + def on_interface_format_changed(new_value): + # current_base = self.base_url_var.get().strip() + # if not current_base: + if new_value == "Ollama": + self.base_url_var.set("http://localhost:11434/v1") + elif new_value == "ML Studio": + self.base_url_var.set("http://localhost:1234/v1") + elif new_value == "OpenAI": + self.base_url_var.set("https://api.agicto.cn/v1") + # 1. API Key - api_key_label = ctk.CTkLabel(self.ai_config_tab, text="API Key:") + api_key_label = ctk.CTkLabel(self.ai_config_tab, text="API Key:", font=("Microsoft YaHei", 12)) api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e") - api_key_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.api_key_var) + api_key_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.api_key_var, font=("Microsoft YaHei", 12)) api_key_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") # 2. Base URL - base_url_label = ctk.CTkLabel(self.ai_config_tab, text="Base URL:") + base_url_label = ctk.CTkLabel(self.ai_config_tab, text="Base URL:", font=("Microsoft YaHei", 12)) base_url_label.grid(row=1, column=0, padx=5, pady=5, sticky="e") - base_url_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.base_url_var) + base_url_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.base_url_var, font=("Microsoft YaHei", 12)) base_url_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") # 3. 接口格式 - interface_label = ctk.CTkLabel(self.ai_config_tab, text="接口格式:") + interface_label = ctk.CTkLabel(self.ai_config_tab, text="接口格式:", font=("Microsoft YaHei", 12)) interface_label.grid(row=2, column=0, padx=5, pady=5, sticky="e") interface_options = ["OpenAI", "Ollama", "ML Studio", "Local"] interface_dropdown = ctk.CTkOptionMenu( self.ai_config_tab, values=interface_options, - variable=self.interface_format_var + variable=self.interface_format_var, + command=on_interface_format_changed, + font=("Microsoft YaHei", 12) ) interface_dropdown.grid(row=2, column=1, padx=5, pady=5, sticky="nsew") # 4. 模型名称 - model_name_label = ctk.CTkLabel(self.ai_config_tab, text="Model Name:") + model_name_label = ctk.CTkLabel(self.ai_config_tab, text="Model Name:", font=("Microsoft YaHei", 12)) model_name_label.grid(row=3, column=0, padx=5, pady=5, sticky="e") - model_name_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.model_name_var) + model_name_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.model_name_var, font=("Microsoft YaHei", 12)) model_name_entry.grid(row=3, column=1, padx=5, pady=5, sticky="nsew") # 5. Temperature - temp_label = ctk.CTkLabel(self.ai_config_tab, text="Temperature:") + temp_label = ctk.CTkLabel(self.ai_config_tab, text="Temperature:", font=("Microsoft YaHei", 12)) temp_label.grid(row=4, column=0, padx=5, pady=5, sticky="e") def update_temp_label(value): @@ -217,19 +236,9 @@ class NovelGeneratorGUI: ) temp_scale.grid(row=4, column=1, padx=5, pady=5, sticky="we") - self.temp_value_label = ctk.CTkLabel(self.ai_config_tab, text=f"{self.temperature_var.get():.2f}") + self.temp_value_label = ctk.CTkLabel(self.ai_config_tab, text=f"{self.temperature_var.get():.2f}", font=("Microsoft YaHei", 12)) self.temp_value_label.grid(row=4, column=2, padx=1, pady=1, sticky="w") - # 保存/加载配置按钮(与AI配置归一处) - config_frame = ctk.CTkFrame(self.ai_config_tab) - config_frame.grid(row=5, column=0, columnspan=3, sticky="nsew") - - save_config_btn = ctk.CTkButton(config_frame, text="保存配置", command=self.save_config_btn) - save_config_btn.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") - - load_config_btn = ctk.CTkButton(config_frame, text="加载配置", command=self.load_config_btn) - load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") - def build_embeddings_config_tab(self): """ 在 Embedding配置 子Tab 上放置: @@ -242,22 +251,22 @@ class NovelGeneratorGUI: self.embeddings_config_tab.grid_columnconfigure(1, weight=1) # 1. Embedding URL - embedding_url_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding URL:") + embedding_url_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding URL:", font=("Microsoft YaHei", 12)) embedding_url_label.grid(row=0, column=0, padx=5, pady=5, sticky="e") - embedding_url_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_url_var) + embedding_url_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_url_var, font=("Microsoft YaHei", 12)) embedding_url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") # 2. Embedding 模型名称 - emb_model_name_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding Model Name:") + emb_model_name_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding Model Name:", font=("Microsoft YaHei", 12)) emb_model_name_label.grid(row=1, column=0, padx=5, pady=5, sticky="e") - emb_model_name_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_model_name_var) + emb_model_name_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_model_name_var, font=("Microsoft YaHei", 12)) emb_model_name_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") def build_novel_params_area(self, start_row=2): """ 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 + 只使用两列:0列放Label,1列放输入框(可能加子Frame)。 """ - # row = start_row # 主题(Topic) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) topic_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="e") @@ -270,32 +279,37 @@ class NovelGeneratorGUI: genre_label = ctk.CTkLabel(self.right_frame, text="类型(Genre):", font=("Microsoft YaHei", 12)) genre_label.grid(row=start_row+1, column=0, padx=5, pady=5, sticky="e") genre_entry = ctk.CTkEntry(self.right_frame, textvariable=self.genre_var, font=("Microsoft YaHei", 12)) - genre_entry.grid(row=start_row+1, column=1, padx=5, pady=5, sticky="nsew") + genre_entry.grid(row=start_row+1, column=1, padx=5, pady=5, sticky="ew") # 章节数 num_chapters_label = ctk.CTkLabel(self.right_frame, text="章节数:", font=("Microsoft YaHei", 12)) num_chapters_label.grid(row=start_row+2, column=0, padx=5, pady=5, sticky="e") - num_chapters_entry = ctk.CTkEntry(self.right_frame, textvariable=self.num_chapters_var, width=80) + num_chapters_entry = ctk.CTkEntry(self.right_frame, textvariable=self.num_chapters_var, width=80, font=("Microsoft YaHei", 12)) num_chapters_entry.grid(row=start_row+2, column=1, padx=5, pady=5, sticky="w") # 每章字数 word_number_label = ctk.CTkLabel(self.right_frame, text="每章字数:", font=("Microsoft YaHei", 12)) word_number_label.grid(row=start_row+3, column=0, padx=5, pady=5, sticky="e") - word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80) + word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80, font=("Microsoft YaHei", 12)) word_number_entry.grid(row=start_row+3, column=1, padx=5, pady=5, sticky="w") - # 保存路径 + # 保存路径 (使用子Frame放置 Entry + Button) filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12)) filepath_label.grid(row=start_row+4, column=0, padx=5, pady=5, sticky="e") - filepath_entry = ctk.CTkEntry(self.right_frame, textvariable=self.filepath_var) - filepath_entry.grid(row=start_row+4, column=1, padx=5, pady=5, sticky="nsew") - browse_btn = ctk.CTkButton(self.right_frame, text="浏览...", command=self.browse_folder, width=60) - browse_btn.grid(row=start_row+4, column=2, padx=1, pady=1, sticky="w") + + self.filepath_frame = ctk.CTkFrame(self.right_frame) + self.filepath_frame.grid(row=start_row+4, column=1, padx=5, pady=5, sticky="nsew") + self.filepath_frame.columnconfigure(0, weight=1) + + filepath_entry = ctk.CTkEntry(self.filepath_frame, textvariable=self.filepath_var, font=("Microsoft YaHei", 12)) + filepath_entry.grid(row=0, column=0, padx=5, pady=5, sticky="ew") + browse_btn = ctk.CTkButton(self.filepath_frame, text="浏览...", command=self.browse_folder, width=60, font=("Microsoft YaHei", 12)) + browse_btn.grid(row=0, column=1, padx=5, pady=5, sticky="e") # 章节号 chapter_num_label = ctk.CTkLabel(self.right_frame, text="章节号:", font=("Microsoft YaHei", 12)) chapter_num_label.grid(row=start_row+5, column=0, padx=5, pady=5, sticky="e") - chapter_num_entry = ctk.CTkEntry(self.right_frame, textvariable=self.chapter_num_var, width=80) + chapter_num_entry = ctk.CTkEntry(self.right_frame, textvariable=self.chapter_num_var, width=80, font=("Microsoft YaHei", 12)) chapter_num_entry.grid(row=start_row+5, column=1, padx=5, pady=5, sticky="w") # 用户指导 @@ -306,46 +320,59 @@ class NovelGeneratorGUI: def build_main_buttons_area(self, start_row=10): """ - 主要功能按钮 + 主要功能按钮 + 配置保存/加载 按钮 """ + # 调整下布局:0,1列 + # 这里按行摆放多个按钮 self.right_frame.grid_rowconfigure(start_row, weight=0) - self.right_frame.grid_columnconfigure(0, weight=0) - self.right_frame.grid_columnconfigure(1, weight=0) + # 先放“保存配置”和“加载配置”按钮 + self.btn_frame_config = ctk.CTkFrame(self.right_frame) + self.btn_frame_config.grid(row=start_row, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_frame_config.columnconfigure(0, weight=1) + self.btn_frame_config.columnconfigure(1, weight=1) + + save_config_btn = ctk.CTkButton(self.btn_frame_config, text="保存配置", command=self.save_config_btn, font=("Microsoft YaHei", 12)) + save_config_btn.grid(row=0, column=0, padx=5, pady=5, sticky="ew") + + load_config_btn = ctk.CTkButton(self.btn_frame_config, text="加载配置", command=self.load_config_btn, font=("Microsoft YaHei", 12)) + load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + + # 下面是主要功能按钮 self.btn_generate_full = ctk.CTkButton( self.right_frame, text="Step1. 生成设定 & 目录", command=self.generate_full_novel, font=("Microsoft YaHei", 12) ) - self.btn_generate_full.grid(row=start_row, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_generate_full.grid(row=start_row+1, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_generate_chapter = ctk.CTkButton( self.right_frame, text="Step2. 生成章节草稿", command=self.generate_chapter_draft_ui, font=("Microsoft YaHei", 12) ) - self.btn_generate_chapter.grid(row=start_row+1, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_generate_chapter.grid(row=start_row+2, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_finalize_chapter = ctk.CTkButton( self.right_frame, text="Step3. 定稿当前章节", command=self.finalize_chapter_ui, font=("Microsoft YaHei", 12) ) - self.btn_finalize_chapter.grid(row=start_row+2, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_finalize_chapter.grid(row=start_row+3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_check_consistency = ctk.CTkButton( self.right_frame, text="[可选]一致性审校", command=self.do_consistency_check, font=("Microsoft YaHei", 12) ) - self.btn_check_consistency.grid(row=start_row+3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_check_consistency.grid(row=start_row+4, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_import_knowledge = ctk.CTkButton( self.right_frame, text="[可选]导入知识库", command=self.import_knowledge_handler, font=("Microsoft YaHei", 12) ) - self.btn_import_knowledge.grid(row=start_row+4, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_import_knowledge.grid(row=start_row+5, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_clear_vectorstore = ctk.CTkButton( self.right_frame, text="清空向量库", @@ -353,14 +380,14 @@ class NovelGeneratorGUI: command=self.clear_vectorstore_handler, font=("Microsoft YaHei", 12) ) - self.btn_clear_vectorstore.grid(row=start_row+5, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + self.btn_clear_vectorstore.grid(row=start_row+6, column=0, columnspan=2, padx=5, pady=5, sticky="ew") plot_arcs_btn = ctk.CTkButton( self.right_frame, text="[查看] 剧情要点", command=self.show_plot_arcs_ui, font=("Microsoft YaHei", 12) ) - plot_arcs_btn.grid(row=start_row+6, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + plot_arcs_btn.grid(row=start_row+7, column=0, columnspan=2, padx=5, pady=5, sticky="ew") # ------------------ Novel Settings Tab ------------------ def build_setting_tab(self): @@ -371,10 +398,10 @@ class NovelGeneratorGUI: self.setting_tab.rowconfigure(1, weight=1) self.setting_tab.columnconfigure(0, weight=1) - load_btn = ctk.CTkButton(self.setting_tab, text="加载 Novel_setting.txt", command=self.load_novel_setting) + load_btn = ctk.CTkButton(self.setting_tab, text="加载 Novel_setting.txt", command=self.load_novel_setting, font=("Microsoft YaHei", 12)) load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") - save_btn = ctk.CTkButton(self.setting_tab, text="保存修改", command=self.save_novel_setting) + save_btn = ctk.CTkButton(self.setting_tab, text="保存修改", command=self.save_novel_setting, font=("Microsoft YaHei", 12)) save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") self.setting_text = ctk.CTkTextbox(self.setting_tab, wrap="word", font=("Microsoft YaHei", 12)) @@ -412,10 +439,10 @@ class NovelGeneratorGUI: self.directory_tab.rowconfigure(1, weight=1) self.directory_tab.columnconfigure(0, weight=1) - load_btn = ctk.CTkButton(self.directory_tab, text="加载 Novel_directory.txt", command=self.load_novel_directory) + load_btn = ctk.CTkButton(self.directory_tab, text="加载 Novel_directory.txt", command=self.load_novel_directory, font=("Microsoft YaHei", 12)) load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") - save_btn = ctk.CTkButton(self.directory_tab, text="保存修改", command=self.save_novel_directory) + save_btn = ctk.CTkButton(self.directory_tab, text="保存修改", command=self.save_novel_directory, font=("Microsoft YaHei", 12)) save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") self.directory_text = ctk.CTkTextbox(self.directory_tab, wrap="word", font=("Microsoft YaHei", 12)) @@ -453,10 +480,10 @@ class NovelGeneratorGUI: self.character_tab.rowconfigure(1, weight=1) self.character_tab.columnconfigure(0, weight=1) - load_btn = ctk.CTkButton(self.character_tab, text="加载 character_state.txt", command=self.load_character_state) + load_btn = ctk.CTkButton(self.character_tab, text="加载 character_state.txt", command=self.load_character_state, font=("Microsoft YaHei", 12)) load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") - save_btn = ctk.CTkButton(self.character_tab, text="保存修改", command=self.save_character_state) + save_btn = ctk.CTkButton(self.character_tab, text="保存修改", command=self.save_character_state, font=("Microsoft YaHei", 12)) save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") self.character_text = ctk.CTkTextbox(self.character_tab, wrap="word", font=("Microsoft YaHei", 12)) @@ -494,10 +521,10 @@ class NovelGeneratorGUI: self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.columnconfigure(0, weight=1) - load_btn = ctk.CTkButton(self.summary_tab, text="加载 global_summary.txt", command=self.load_global_summary) + load_btn = ctk.CTkButton(self.summary_tab, text="加载 global_summary.txt", command=self.load_global_summary, font=("Microsoft YaHei", 12)) load_btn.grid(row=0, column=0, padx=5, pady=5, sticky="w") - save_btn = ctk.CTkButton(self.summary_tab, text="保存修改", command=self.save_global_summary) + save_btn = ctk.CTkButton(self.summary_tab, text="保存修改", command=self.save_global_summary, font=("Microsoft YaHei", 12)) save_btn.grid(row=0, column=0, padx=5, pady=5, sticky="e") self.summary_text = ctk.CTkTextbox(self.summary_tab, wrap="word", font=("Microsoft YaHei", 12)) @@ -670,7 +697,7 @@ class NovelGeneratorGUI: # 简易生成最近章节摘要(示例) recent_chapters_summary = summarize_recent_chapters( - None, # 此处仅示例 + model_name, # 此处仅示例 recent_3_texts ) @@ -865,7 +892,7 @@ class NovelGeneratorGUI: top.title("剧情要点/未解决冲突") top.geometry("600x400") - text_area = ctk.CTkTextbox(top, wrap="word") + text_area = ctk.CTkTextbox(top, wrap="word", font=("Microsoft YaHei", 12)) text_area.pack(fill="both", expand=True, padx=10, pady=10) text_area.insert("0.0", arcs_text)