From f2dc98744d25b1def291018b0187183491d4352f Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 12:59:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=BA=8C=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/Novel_directory.txt | 83 ++++++++++++- Src/Novel_setting.txt | 67 ++++++++++- embedding_ollama.py | 8 +- novel_generator.py | 94 ++++++++++++--- ui.py | 256 ++++++++++++++++++++++++++-------------- 5 files changed, 401 insertions(+), 107 deletions(-) diff --git a/Src/Novel_directory.txt b/Src/Novel_directory.txt index bdbd913..3a9cc6f 100644 --- a/Src/Novel_directory.txt +++ b/Src/Novel_directory.txt @@ -98,4 +98,85 @@ 第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 -第100章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局 \ No newline at end of file +第100章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局 + +嗯,我需要按照用户提供的《小说设定》来生成100章的目录。首先,我得仔细阅读并理解这个设定,确保每一章都紧扣主题和叙事结构。 + + 世界观设定:暗影之歌 + +在提瓦特大陆上,暗影力量正在苏醒,带来了新的挑战和阴谋。主要角色包括星辰、艾莎、凯文,以及暗影力量本身。故事探讨了力量与责任、身份与命运、信任与背叛等主题。 + + 目录模板 +第1章: - <简述> +... +第100章: - <简述> + +接下来,我需要规划每一章的内容,确保涵盖起始点、初识艾莎、凯文的动机、危机初现、真相浮现、高潮对决以及开放式结局。 + + 起始点 +星辰被选中成为暗影力量的继承者,但她对自己的能力感到不安,同时怀疑“暗影力量”是否真是保护大陆的存在。这一部分需要引入角色和设定,铺垫后续的冲突。 + + 初识艾莎 +艾莎以刺客的身份接近星辰,试图利用她的暗影能力。两人合作中发现彼此的共同点和隐藏的冲突,这可以增加角色的互动和紧张感。 + + 凯文的动机 +凯文表面上支持星辰和艾莎,但实际上有自己的秘密。他可能是“暗影力量”的后代,或者另有隐情。这部分需要揭示凯文的多面性和潜在威胁。 + + 危机初现 +随着暗影力量的觉醒,大陆出现诡异事件。暗影力量失控,普通民众受害。这可以是故事转折点,展示力量带来的危险。 + + 真相浮现 +通过对凯文、艾莎和星辰的深入探索,揭示他们的过去与命运联系。他们之间存在更大的联系,关系到暗影力量的未来。 + + 高潮对决 +关键时刻,星辰和艾莎必须共同面临强敌。这场对决是力量的较量,更是信任与背叛的考验,增加戏剧性和紧张感。 + + 结局模糊 +故事留下开放结局,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路,或者被力量吞噬。 + +在规划每一章时,我需要确保情节连贯,角色发展合理,同时埋下伏笔,为后续章节做好铺垫。例如,在初识艾莎的阶段,可以设置她的多面性和潜在的秘密,为后期冲突做准备。 + +此外,每章的标题应简洁明了,能够反映主要情节或角色的发展,并附上简短的提示性简述,帮助读者了解每一章的内容。 + +现在,我需要将这些思考整理成一个100章的目录,确保每一章都紧扣设定中的叙事结构和角色发展,同时埋下足够的暗线伏笔。 + + +第1章 :星辰的选择 - 星辰被选中为暗影力量的继承者,但内心充满疑惑与不安。 + +第2章 :暗影之息 - 星辰在练习中意外触发暗影力量,引发混乱,开始探索自己的能力边界。 + +第3章 :艾莎的来临 - 艾莎以刺客身份接近星辰,试图获取更多关于暗影力量的信息,同时对星辰产生复杂情感。 + +第4章 :凯文的真实意图 - 凯文表面上支持星辰和艾莎,但逐渐显露出多面性和隐藏的秘密,引起星辰的警觉。 + +第5章:暗影失控 - 随着暗影力量的强化,大陆出现一系列诡异事件,普通民众受害,暗影力量开始失控。 + +第6章:艾莎的内心挣扎 - 艾莎在完成任务后,发现自己逐渐被暗影力量控制,对自己的选择感到恐慌和纠结。 + +第7章:凯文的秘密揭露 - 凯文被迫公开他的多面性和隐藏的秘密,星辰和艾莎联手调查他的真实意图。 + +第8章:命运的交错 - 通过对凯文、艾莎和星辰的深入了解,逐渐揭示他们之间存在更大的联系关系到暗影力量的未来走向。 + +第9章:危机加剧 - 凯文与暗影力量的势力开始对星辰和艾莎构成直接威胁,两人必须面临更为严峻的挑战。 + +第10章:信任的考验 - 在关键时刻,星辰和艾莎之间的信任面临突破,但也隐藏着潜在的背叛阴谋。 + +第11章:暗影力量的真相 - 星辰通过回忆和资料,逐渐揭开暗影力量的起源和其对世界的影响。 + +第12章:凯文的最终动向 - 凯文的双重身份和真实意图被彻底揭露,他的选择将对暗影力量的未来产生重大影响。 + +第13章:星辰的觉醒 - 星辰在与艾莎的合作中逐渐意识到自己肩负的责任,开始寻找解决问题的方法。 + +第14章:艾莎的挣扎 - 艾莎被迫面对自己的过去和现在的选择,她是否能站出来对抗暗影力量? + +第15章:凯文的覆灭 - 凯文最终的命运如何?他的计划是否能够成功,或者他将成为阻碍? + +第16章:暗影之歌 - 星辰和艾莎联手面临最后的危机,他们需要找到一个办法来控制暗影力量。 + +第17章:力量与责任的抉择 - 在关键时刻,星辰和艾莎必须做出艰难的决定,选择自己的道路还是被力量吞噬。 + +第18章:命运的终结 - 故事进入高潮,暗影力量的威胁更加严峻,星辰和艾莎必须团结起来对抗它。 + +第19章:最后的对决 - 星辰和艾莎面临最终的对决,他们的能力和信任将决定世界的命运。 + +第20章:暗影之光 - 故事情节以开放式结束,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路? \ No newline at end of file diff --git a/Src/Novel_setting.txt b/Src/Novel_setting.txt index 5767994..340c5cc 100644 --- a/Src/Novel_setting.txt +++ b/Src/Novel_setting.txt @@ -30,4 +30,69 @@ 结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 -世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 \ No newline at end of file +世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 + 世界观设定:暗影之歌 + + 引言 +在一个名为提瓦特的大陆上,力量如同黑暗中的呼吸,无声无息地渗透着一切。这片大陆的历史被称为“暗影年代”,那时期充满了权力斗争和悲剧命运。如今,随着“暗影力量”的再次苏醒,新的阴谋与挑战正逐渐显露。 + + 主要角色 +1. 星辰(亦名:斯琴) + - 背景:一位被称为“暗影之心”的天才少女,拥有操纵暗影的特殊能力。她的力量是暗影之力最强者之一,但同时也让她背负着沉重的责任。 + +2. 艾莎(亦名:艾丽莎) + - 背景:一位冷酷无情的刺客,曾为“暗影力量”效力。她的身世与“暗影力量”密不可分,性格多变,隐藏着巨大的秘密。 + +3. 凯文(亦名:克莱文) + - 背景:自封为提瓦特大陆的守护者,实则心中藏着大不明小谋。他表面上支持星辰和艾莎,但背后暗藏自己的秘密计划。 + +4. 暗影力量 + - 背景:一股强大的力量源于远古,能够操控暗影与黑暗。它的真实目的尚未完全被揭示,既有守护大陆的传说,也有毁灭一切的预言。 + + 世界观亮点 + +1. 力量的双面性 + - 暗影力量是世界的核心,但同时也是最大的危险。掌握它的人往往会被力量所控制,经历深刻的心理变化。 + +2. 身份的迷失 + - 星辰和艾莎都背负着来自“暗影力量”的阴影,他们的身份与命运与力量紧密相连,这种联系既是他们的优势,也是他们的诅咒。 + +3. 权力真空 + - 提瓦特大陆在“暗影年代”后期逐渐走向新的权力真空,各种势力争夺资源和权利,形成了复杂多变的政治格局。 + +4. 宿命的重遇 + - 星辰和艾莎被暗影力量联系起来,这种宿命使他们在面对危机时不得不合作,但同时也让彼此之间充满了矛盾与误解。 + +5. 未知的威胁 + - “暗影力量”的真实目标尚未完全明确,可能是为了大陆的繁荣,也可能是为了彻底摧毁一切。这种不确定性为故事增添了紧张感和悬念。 + + 故事情节展开 + +1. 起始点 + - 星辰被选中成为暗影力量的继承者,但她对自己的能力感到不安,同时也怀疑“暗影力量”是否真的是保护大陆的存在。 + +2. 初识艾莎 + - 艾莎以刺客的身份接近星辰,试图利用她的暗影能力。但两人在合作中逐渐发现彼此之间的共同点,以及隐藏的冲突。 + +3. 凯文的动机 + - 凯文表面上是支持星辰和艾莎,但实际上他有自己的秘密。他可能是“暗影力量”的后代,或者背后另有hidden agenda。 + +4. 危机初现 + - 随着暗影力量的觉醒,大陆出现了一系列诡异的事件。暗影力量开始失控,普通大民被笼罩在阴影中,生存成为一件艰难的事。 + +5. 真相浮现 + - 通过对凯文、艾莎和星辰的深入探索,逐渐揭示他们各自的过去与命运联系。最终,他们会发现自己之间存在着更大的联系,而这可能关系到整个暗影力量的未来。 + +6. 高潮对决 + - 在一次关键时刻,星辰和艾莎必须共同面临一个强大的敌人。这场对决不仅是力量的较量,更是信任与背叛的考验。 + +7. 结局的模糊性 + - 故事情节留下了开放的结局,让读者思考暗影力量最终会带来什么样的未来。星辰和艾莎是否能找到自己的道路,或者他们将被暗影力量彻底吞噬。 + + 主题 +- 力量与责任:暗影力量赋予了一些人巨大的能力,但同时也带来了沉重的责任。 +- 身份与命运:每个人的过去决定了他们的未来,他们是否能超越自己的宿命? +- 信任与背叛:在危难时刻,真实的朋友和潜在的敌人是如何区分的? + + 总结 +“暗影之歌”是一个充满矛盾与复杂性的故事,它探讨了力量、身份、信任等多个深刻主题。通过星辰和艾莎两位主角的视角,读者可以感受到在黑暗与光明之间挣扎的人性,以及他们如何面对命运给予的挑战。 \ No newline at end of file diff --git a/embedding_ollama.py b/embedding_ollama.py index 4bbd8e7..36ee973 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -10,7 +10,13 @@ class OllamaEmbeddings: def __init__(self, model_name: str, base_url: str): self.model_name = model_name self.base_url = base_url - + + def embed(self, texts: List[str]) -> List[List[float]]: + embeddings = [] + for text in texts: + embeddings.append(self.embed_single_document(text)) + return embeddings + def embed_documents(self, texts: List[str]) -> List[List[float]]: """ 将多段文本转换为向量列表 diff --git a/novel_generator.py b/novel_generator.py index 66807a4..42fa110 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -30,10 +30,48 @@ from prompt_definitions import ( summary_prompt, update_character_state_prompt, chapter_outline_prompt, chapter_write_prompt ) - -# ============ 新增:导入 chapter_directory_parser ============ +from embedding_ollama import OllamaEmbeddings from chapter_directory_parser import get_chapter_info_from_directory +# ============ 适配接口的判断函数 ============ +def is_using_ollama_api(interface_format: str, base_url: str) -> bool: + """ + 当 interface_format == "Ollama" 时返回 True + """ + if interface_format.lower() == "ollama": + return True + return False + +def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: + """ + 如果用户在下拉里选择了 ML Studio + """ + if interface_format.lower() == "ml studio": + return True + return False + +def create_embeddings_object( + api_key: str, + base_url: str, + embed_url: str, + interface_format: str, + embedding_model_name: str +): + """ + 根据用户在UI中配置的参数,返回对应的 embeddings 对象。 + - 当 interface_format = "Ollama" => OllamaEmbeddings(...) + - 当 interface_format = "OpenAI" => OpenAIEmbeddings + - 当 interface_format = "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): + return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url) + else: + # 默认使用 OpenAIEmbeddings + return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url) + # ============ 日志配置 ============ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") @@ -67,17 +105,21 @@ def clear_vector_store(): else: logging.info("No vector store found to clear.") -def init_vector_store(api_key: str, base_url: str, texts: List[str], embedding_base_url: str = "") -> Chroma: +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: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 如果不存在该目录,会自动创建。 如果 embedding_base_url 不为空,则使用它做为embedding的base,否则默认base_url。 """ embed_url = embedding_base_url if embedding_base_url else base_url - embeddings = OpenAIEmbeddings( - openai_api_key=api_key, - openai_api_base=embed_url - ) + embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, embedding_model_name) documents = [Document(page_content=t) for t in texts] vectorstore = Chroma.from_documents( documents, @@ -87,7 +129,13 @@ def init_vector_store(api_key: str, base_url: str, texts: List[str], embedding_b vectorstore.persist() return vectorstore -def load_vector_store(api_key: str, base_url: str, embedding_base_url: str = "") -> Optional[Chroma]: +def load_vector_store( + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + embedding_base_url: str = "" + ) -> Optional[Chroma]: """ 读取已存在的向量库。若不存在则返回 None。 同样支持可选的 embedding_base_url。 @@ -95,30 +143,42 @@ def load_vector_store(api_key: str, base_url: str, embedding_base_url: str = "") if not os.path.exists(VECTOR_STORE_DIR): return None embed_url = embedding_base_url if embedding_base_url else base_url - embeddings = OpenAIEmbeddings( - openai_api_key=api_key, - openai_api_base=embed_url - ) + embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, 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, embedding_base_url: str = "") -> None: +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, embedding_base_url) + store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url) if not store: logging.info("Vector store does not exist. Initializing a new one...") - init_vector_store(api_key, base_url, [new_chapter], embedding_base_url) + init_vector_store(api_key, base_url,interface_format, embedding_model_name, [new_chapter], embedding_base_url) return new_doc = Document(page_content=new_chapter) store.add_documents([new_doc]) store.persist() -def get_relevant_context_from_vector_store(api_key: str, base_url: str, query: str, k: int = 2, embedding_base_url: str = "") -> str: +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: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 若向量库不存在则返回空字符串。 """ - store = load_vector_store(api_key, base_url, embedding_base_url) + store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url) if not store: logging.warning("Vector store not found. Returning empty context.") return "" diff --git a/ui.py b/ui.py index 4bfa638..c485834 100644 --- a/ui.py +++ b/ui.py @@ -38,12 +38,14 @@ class NovelGeneratorGUI: self.loaded_config = load_config(self.config_file) # ========== 主要的属性变量 ========== - # 右侧参数区 - 各种输入 + # 注意:配置项较多,此处增加 embedding_model_name_var 以支持 Embedding 模型名称 self.api_key_var = ctk.StringVar(value=self.loaded_config.get("api_key", "")) self.base_url_var = ctk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1")) self.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI")) self.model_name_var = ctk.StringVar(value=self.loaded_config.get("model_name", "gpt-4o-mini")) - self.embedding_url_var = ctk.StringVar(value=self.loaded_config.get("embedding_url", "")) # 新增:可选Embedding模型URL + + self.embedding_url_var = ctk.StringVar(value=self.loaded_config.get("embedding_url", "")) + self.embedding_model_name_var = ctk.StringVar(value=self.loaded_config.get("embedding_model_name", "")) self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7)) self.topic_default = self.loaded_config.get("topic", "") @@ -83,7 +85,7 @@ class NovelGeneratorGUI: # ------------------ 主功能 Tab ------------------ def build_main_tab(self): """ - 主Tab: 左侧显示日志 / 本章内容, 右侧显示主要功能操作区 + 主Tab: 左侧显示章节草稿/日志, 右侧是功能区 & 配置区 """ self.main_tab.rowconfigure(0, weight=1) self.main_tab.columnconfigure(0, weight=1) @@ -104,8 +106,8 @@ class NovelGeneratorGUI: def build_left_layout(self): """ 左侧包含两个区域: - 1. 输出日志(下半部分) - 2. 本章内容(上半部分) + 1. 本章草稿内容(可编辑) + 2. 输出日志(只读) """ self.left_frame.grid_rowconfigure(0, weight=3) self.left_frame.grid_rowconfigure(1, weight=1) @@ -115,177 +117,250 @@ class NovelGeneratorGUI: chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容", font=("Microsoft YaHei", 14)) chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w") + # 章节草稿:可编辑 self.chapter_result = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 14)) self.chapter_result.grid(row=0, column=0, sticky="nsew", padx=5, pady=(0, 5)) # 输出日志 - log_label = ctk.CTkLabel(self.left_frame, text="输出日志", font=("Microsoft YaHei", 14)) + log_label = ctk.CTkLabel(self.left_frame, text="输出日志 (只读)", font=("Microsoft YaHei", 14)) log_label.grid(row=1, column=0, padx=5, pady=(5, 0), sticky="w") + # 日志:只读 self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12)) self.log_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) + # 设置只读 + self.log_text.configure(state="disabled") + def build_right_layout(self): """ - 右侧用于显示一系列参数输入和功能按钮。 + 右侧用于显示配置与功能按钮。 + 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 + 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 + """ + # 第一行创建一个子 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.ai_config_tab = self.config_tabview.add("AI接口配置") + self.embeddings_config_tab = self.config_tabview.add("Embedding配置") + + # 分别构建这两个 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) + + # 最后放一些主功能按钮 + self.build_main_buttons_area(start_row=row_base+10) + + def build_ai_config_tab(self): + """ + 在 AI接口配置 子Tab 上放置: + - API Key + - Base URL + - 接口格式 + - 模型名称 + - Temperature """ # 配置网格 - for i in range(25): - self.right_frame.grid_rowconfigure(i, weight=0) - self.right_frame.grid_columnconfigure(0, weight=0) - self.right_frame.grid_columnconfigure(1, weight=1) + for i in range(6): + 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) # 1. API Key - api_key_label = ctk.CTkLabel(self.right_frame, text="API Key:") + api_key_label = ctk.CTkLabel(self.ai_config_tab, text="API Key:") api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e") - api_key_entry = ctk.CTkEntry(self.right_frame, textvariable=self.api_key_var) - api_key_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + api_key_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.api_key_var) + api_key_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") # 2. Base URL - base_url_label = ctk.CTkLabel(self.right_frame, text="Base URL:") + base_url_label = ctk.CTkLabel(self.ai_config_tab, text="Base URL:") base_url_label.grid(row=1, column=0, padx=5, pady=5, sticky="e") - base_url_entry = ctk.CTkEntry(self.right_frame, textvariable=self.base_url_var) - base_url_entry.grid(row=1, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + base_url_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.base_url_var) + base_url_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") - # 2.1 接口格式 下拉菜单 - interface_label = ctk.CTkLabel(self.right_frame, text="接口格式:") + # 3. 接口格式 + interface_label = ctk.CTkLabel(self.ai_config_tab, text="接口格式:") 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.right_frame, values=interface_options, variable=self.interface_format_var) - interface_dropdown.grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + interface_dropdown = ctk.CTkOptionMenu( + self.ai_config_tab, + values=interface_options, + variable=self.interface_format_var + ) + interface_dropdown.grid(row=2, column=1, padx=5, pady=5, sticky="nsew") - # 3. Model Name - model_name_label = ctk.CTkLabel(self.right_frame, text="Model Name:") + # 4. 模型名称 + model_name_label = ctk.CTkLabel(self.ai_config_tab, text="Model Name:") model_name_label.grid(row=3, column=0, padx=5, pady=5, sticky="e") - model_name_entry = ctk.CTkEntry(self.right_frame, textvariable=self.model_name_var) - model_name_entry.grid(row=3, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + model_name_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.model_name_var) + model_name_entry.grid(row=3, column=1, padx=5, pady=5, sticky="nsew") - # 3.1 Embedding Model URL (可选) - embedding_url_label = ctk.CTkLabel(self.right_frame, text="Embedding URL:") - embedding_url_label.grid(row=4, column=0, padx=5, pady=5, sticky="e") - embedding_url_entry = ctk.CTkEntry(self.right_frame, textvariable=self.embedding_url_var) - embedding_url_entry.grid(row=4, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + # 5. Temperature + temp_label = ctk.CTkLabel(self.ai_config_tab, text="Temperature:") + temp_label.grid(row=4, column=0, padx=5, pady=5, sticky="e") - # 4. Temperature - temp_label = ctk.CTkLabel(self.right_frame, text="Temperature:") - temp_label.grid(row=5, column=0, padx=5, pady=5, sticky="e") - def update_temp_label(value): self.temp_value_label.configure(text=f"{float(value):.2f}") - temp_scale = ctk.CTkSlider(self.right_frame, from_=0.0, to=1.0, number_of_steps=100, - command=update_temp_label, variable=self.temperature_var) - temp_scale.grid(row=5, column=1, padx=5, pady=5, sticky="we") - - self.temp_value_label = ctk.CTkLabel(self.right_frame, text=f"{self.temperature_var.get():.2f}") - self.temp_value_label.grid(row=5, column=2, padx=1, pady=1, sticky="w") - # 5. 主题(Topic) 多行输入 + temp_scale = ctk.CTkSlider( + self.ai_config_tab, + from_=0.0, to=1.0, + number_of_steps=100, + command=update_temp_label, + variable=self.temperature_var + ) + 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.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 上放置: + - Embedding URL + - Embedding 模型名称 + """ + for i in range(2): + self.embeddings_config_tab.grid_rowconfigure(i, weight=0) + self.embeddings_config_tab.grid_columnconfigure(0, weight=0) + 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.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.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.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.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") + + def build_novel_params_area(self, start_row=2): + """ + 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 + """ + # row = start_row + # 主题(Topic) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) - topic_label.grid(row=6, column=0, padx=5, pady=5, sticky="e") + topic_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="e") self.topic_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12)) - self.topic_text.grid(row=6, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + self.topic_text.grid(row=start_row, column=1, padx=5, pady=5, sticky="nsew") if self.topic_default: self.topic_text.insert("0.0", self.topic_default) - # 6. 类型(Genre) + # 类型(Genre) genre_label = ctk.CTkLabel(self.right_frame, text="类型(Genre):", font=("Microsoft YaHei", 12)) - genre_label.grid(row=7, column=0, padx=5, pady=5, sticky="e") + 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=7, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + genre_entry.grid(row=start_row+1, column=1, padx=5, pady=5, sticky="nsew") - # 7. 章节数 + # 章节数 num_chapters_label = ctk.CTkLabel(self.right_frame, text="章节数:", font=("Microsoft YaHei", 12)) - num_chapters_label.grid(row=8, column=0, padx=5, pady=5, sticky="e") + 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.grid(row=8, column=1, padx=5, pady=5, sticky="w") + num_chapters_entry.grid(row=start_row+2, column=1, padx=5, pady=5, sticky="w") - # 8. 每章字数 + # 每章字数 word_number_label = ctk.CTkLabel(self.right_frame, text="每章字数:", font=("Microsoft YaHei", 12)) - word_number_label.grid(row=9, column=0, padx=5, pady=5, sticky="e") + 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.grid(row=9, column=1, padx=5, pady=5, sticky="w") + word_number_entry.grid(row=start_row+3, column=1, padx=5, pady=5, sticky="w") - # 9. 文件保存路径 + # 保存路径 filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12)) - filepath_label.grid(row=10, column=0, padx=5, pady=5, sticky="e") + 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=10, column=1, padx=5, pady=5, sticky="nsew") - browse_btn = ctk.CTkButton(self.right_frame, text="浏览...", command=self.browse_folder, width=60, font=("Microsoft YaHei", 12)) - browse_btn.grid(row=10, column=2, padx=1, pady=1, sticky="w") + 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") - # 保存/加载配置按钮 - config_frame = ctk.CTkFrame(self.right_frame) - config_frame.grid(row=11, column=1, columnspan=2, sticky="nsew") - - save_config_btn = ctk.CTkButton(config_frame, text="保存配置", command=self.save_config_btn, font=("Microsoft YaHei", 12)) - 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, font=("Microsoft YaHei", 12)) - load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") - - # 10. 章节号 + # 章节号 chapter_num_label = ctk.CTkLabel(self.right_frame, text="章节号:", font=("Microsoft YaHei", 12)) - chapter_num_label.grid(row=12, column=0, padx=5, pady=5, sticky="e") + 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.grid(row=12, column=1, padx=5, pady=5, sticky="w") + chapter_num_entry.grid(row=start_row+5, column=1, padx=5, pady=5, sticky="w") - # 11. “用户指导” 多行输入 + # 用户指导 guide_label = ctk.CTkLabel(self.right_frame, text="本章指导:", font=("Microsoft YaHei", 12)) - guide_label.grid(row=13, column=0, padx=5, pady=5, sticky="ne") + guide_label.grid(row=start_row+6, column=0, padx=5, pady=5, sticky="ne") self.user_guide_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12)) - self.user_guide_text.grid(row=13, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") + self.user_guide_text.grid(row=start_row+6, column=1, padx=5, pady=5, sticky="nsew") + + def build_main_buttons_area(self, start_row=10): + """ + 主要功能按钮 + """ + 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) - # 功能按钮区域 - row_base = 14 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=row_base, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_generate_full.grid(row=start_row, 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=row_base+1, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_generate_chapter.grid(row=start_row+1, 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=row_base+2, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_finalize_chapter.grid(row=start_row+2, 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=row_base+3, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_check_consistency.grid(row=start_row+3, 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=row_base+4, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_import_knowledge.grid(row=start_row+4, column=0, columnspan=2, padx=5, pady=5, sticky="ew") self.btn_clear_vectorstore = ctk.CTkButton( self.right_frame, text="清空向量库", - fg_color="red", # 让按钮显眼一些 + fg_color="red", command=self.clear_vectorstore_handler, font=("Microsoft YaHei", 12) ) - self.btn_clear_vectorstore.grid(row=row_base+5, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + self.btn_clear_vectorstore.grid(row=start_row+5, 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=row_base+6, column=0, columnspan=3, padx=5, pady=5, sticky="ew") + plot_arcs_btn.grid(row=start_row+6, column=0, columnspan=2, padx=5, pady=5, sticky="ew") # ------------------ Novel Settings Tab ------------------ def build_setting_tab(self): @@ -460,6 +535,7 @@ class NovelGeneratorGUI: self.interface_format_var.set(cfg.get("interface_format", "OpenAI")) self.model_name_var.set(cfg.get("model_name", "")) self.embedding_url_var.set(cfg.get("embedding_url", "")) + self.embedding_model_name_var.set(cfg.get("embedding_model_name", "")) self.temperature_var.set(cfg.get("temperature", 0.7)) self.genre_var.set(cfg.get("genre", "")) self.num_chapters_var.set(cfg.get("num_chapters", 10)) @@ -481,6 +557,7 @@ class NovelGeneratorGUI: "interface_format": self.interface_format_var.get(), "model_name": self.model_name_var.get(), "embedding_url": self.embedding_url_var.get(), + "embedding_model_name": self.embedding_model_name_var.get(), "temperature": self.temperature_var.get(), "topic": self.topic_text.get("0.0", "end").strip(), "genre": self.genre_var.get(), @@ -501,8 +578,12 @@ class NovelGeneratorGUI: # ------------------ 日志输出 ------------------ def log(self, message: str): + # 临时切换为可写 + self.log_text.configure(state="normal") self.log_text.insert("end", message + "\n") self.log_text.see("end") + # 再切回只读 + self.log_text.configure(state="disabled") # ------------------ 功能区 -------------------- def disable_button(self, btn): @@ -587,9 +668,9 @@ class NovelGeneratorGUI: chapters_dir = os.path.join(filepath, "chapters") recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3) - # 用当前模型生成一个较为详细的最近剧情摘要 + # 简易生成最近章节摘要(示例) recent_chapters_summary = summarize_recent_chapters( - None, # 这里只是示例,实际可根据你的需求传入对应的模型 + None, # 此处仅示例 recent_3_texts ) @@ -610,7 +691,7 @@ class NovelGeneratorGUI: filepath=filepath ) if draft_text: - self.log(f"✅ 第{chap_num}章草稿生成完成。请在下方查看。") + self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") self.chapter_result.delete("0.0", "end") self.chapter_result.insert("0.0", draft_text) self.chapter_result.see("end") @@ -741,7 +822,8 @@ class NovelGeneratorGUI: import_knowledge_file( api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip(), # 新增:传入embedding url + # 传入 embedding_url + embedding_model_name + embedding_base_url=self.embedding_url_var.get().strip(), file_path=selected_file ) self.log("✅ 知识库文件导入完成。")