From bcd13b8ca388d49973f865d33d3fe6e5bd90c339 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 11:55:49 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=B8=80=E4=B8=8B?= =?UTF-8?q?=EF=BC=8C=E4=BE=BF=E4=BA=8E=E5=BE=85=E4=BC=9A=E6=A0=B8=E5=AF=B9?= =?UTF-8?q?=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/Novel_directory.txt | 101 +++++++ Src/Novel_setting.txt | 33 +++ embedding_ollama.py | 45 +++ main.spec | 4 +- novel_generator.py | 53 ++-- ui.py | 621 +++++++++++++++++++++++++++++----------- 6 files changed, 664 insertions(+), 193 deletions(-) create mode 100644 Src/Novel_directory.txt create mode 100644 Src/Novel_setting.txt create mode 100644 embedding_ollama.py diff --git a/Src/Novel_directory.txt b/Src/Novel_directory.txt new file mode 100644 index 0000000..bdbd913 --- /dev/null +++ b/Src/Novel_directory.txt @@ -0,0 +1,101 @@ + +第1章 :星河坠落提瓦特 - 穿越与初遇派蒙,危机初现端倪 +第2章 :蒙德的初印象 - 初探蒙德城,与丽莎的首次相遇 +第3章 :酒馆的闲谈 - 了解提瓦特局势,派蒙展现惊人一面 +第4章 :丽莎的试探 - 星河展现科技实力,引来丽莎的警惕 +第5章 :委托与冒险 - 接受委托,初次展现星穹铁道科技的运用 +第6章 :风魔龙的挑战 - 运用科技对抗风魔龙,展现策略与智慧 +第7章 :派蒙的异常 - 派蒙出现记忆碎片,暗示其不寻常身份 +第8章 :丽莎的秘密魔法 - 丽莎使用禁忌魔法,展现其神秘一面 +第9章 :旅行者的出现 - 与原神旅行者初次相遇,产生摩擦与合作 +第10章 :蒙德的阴谋 - 发现蒙德隐藏的危机,与丽莎深入合作 +第11章 :追捕令的阴影 - 星河收到星穹铁道追捕令,内心挣扎 +第12章 :深入调查 - 调查蒙德的危机,发现与丽莎的秘密组织有关 +第13章 :钟离的现身 - 钟离初次出现,对星河进行暗示与引导 +第14章 :失落科技的线索 - 发现关于提瓦特失落科技的线索 +第15章 :前往璃月 - 踏上前往璃月的旅程,面对新的挑战 +第16章 :璃月港的繁华 - 体验璃月港的文化,了解璃月局势 +第17章 :往生堂的委托 - 接受往生堂的委托,深入了解璃月历史 +第18章 :岩王帝君的考验 - 钟离对星河进行考验,揭示其真实目的 +第19章 :地脉异常 - 发现璃月地脉异常,与旅行者合作调查 +第20章 :黄金屋的秘密 - 探索黄金屋的秘密,发现与失落科技有关 +第21章 :刻晴的质疑 - 刻晴对星河的科技持有怀疑态度 +第22章 :对抗岩龙蜥 - 运用科技与策略对抗强大的岩龙蜥 +第23章 :派蒙的回忆 - 派蒙回忆片段增多,身份逐渐清晰 +第24章 :丽莎的引导 - 丽莎引导星河学习提瓦特魔法 +第25章 :星河的抉择 - 星河面临道德困境,做出艰难选择 +第26章 :前往稻妻 - 前往稻妻,面对新的挑战与危机 +第27章 :稻妻的雷电 - 体验稻妻的文化与习俗,对抗雷电将军的威压 +第28章 :八重神子的试探 - 八重神子对星河进行试探,考验其能力 +第29章 :影向地的秘密 - 探索影向地的秘密,发现与提瓦特历史有关 +第30章 :反抗军与合作 - 与稻妻反抗军合作,对抗幕后黑手 +第31章 :永恒的执政官 - 与雷电将军正面交锋,展现智慧与实力 +第32章 :派蒙的过去 - 派蒙的过去逐渐揭开,与提瓦特古老文明有关 +第33章 :丽莎的组织 - 丽莎所属组织的真相逐渐浮出水面 +第34章 :前往须弥 - 前往须弥,面对新的挑战与文化 +第35章 :须弥的智慧 - 体验须弥的文化,了解须弥的智慧与科技 +第36章 :草神的大智慧 - 与草神进行交流,探讨生命的意义 +第37章 :森林的危机 - 须弥森林面临危机,星河运用科技解决问题 +第38章 :沙漠的秘密 - 探索须弥沙漠的秘密,发现远古文明的遗迹 +第39章 :散兵的阴谋 - 散兵的阴谋逐渐浮出水面,星河需要应对 +第40章 :与散兵的对抗 - 与散兵展开对抗,展现星穹铁道科技的优势 +第41章 :前往枫丹 - 前往枫丹,面对新的挑战与科技 +第42章 :枫丹的审判 - 体验枫丹的法律与秩序,面对复杂的案件 +第43章 :水神的神秘 - 与水神进行互动,了解枫丹的秘密 +第44章 :机械岛的危机 - 枫丹机械岛面临危机,星河运用科技解决问题 +第45章 :潜入深海 - 潜入深海,探索深海的秘密与科技 +第46章 :派蒙的预言 - 派蒙预言即将到来的巨大危机 +第47章 :丽莎的抉择 - 丽莎面临艰难的抉择,需要做出牺牲 +第48章 :前往纳塔 - 前往纳塔,面对新的挑战与文化 +第49章 :纳塔的冰雪 - 体验纳塔的严寒环境,与冰雪元素对抗 +第50章 :雪山之巅的秘密 - 探索雪山之巅的秘密,发现远古文明的遗迹 +第51章 :冰神的考验 - 冰神对星河进行考验,考验其心性与毅力 +第52章 :与旅行者的合作 - 与旅行者深入合作,对抗共同的敌人 +第53章 :深渊的威胁 - 深渊的威胁日益逼近,星河需要寻找应对之策 +第54章 :钟离的计划 - 钟离的计划逐渐显露,其真实目的更加清晰 +第55章 :星河的过去 - 星河过去的阴影再次浮现,影响其与他人的关系 +第56章 :派蒙的觉醒 - 派蒙觉醒,展现其强大的力量 +第57章 :丽莎的牺牲 - 丽莎为了保护星河做出牺牲 +第58章 :前往至冬 - 前往至冬,面对新的挑战与阴谋 +第59章 :至冬的阴谋 - 揭露至冬的阴谋,对抗幕后黑手 +第60章 :女皇的算计 - 女皇对星河进行算计,考验其智慧与勇气 +第61章 :机械生命的危机 - 至冬的机械生命失控,引发巨大的危机 +第62章 :星河的策略 - 星河运用策略,化解机械生命的危机 +第63章 :派蒙的秘密力量 - 派蒙展现其强大的秘密力量 +第64章 :丽莎的遗产 - 丽莎留下宝贵的遗产,帮助星河对抗危机 +第65章 :对抗深渊使徒 - 对抗强大的深渊使徒,展现星穹铁道科技的威力 +第66章 :旅行者的选择 - 旅行者面临艰难的选择,需要做出决定 +第67章 :钟离的帮助 - 钟离给予星河关键的帮助,但其目的依旧不明 +第68章 :星河的成长 - 星河在经历种种磨难后得到成长 +第69章 :提瓦特的命运 - 提瓦特的命运掌握在星河手中 +第70章 :最终决战的序幕 - 最终决战的序幕拉开,危机全面爆发 +第71章 :与幕后黑手的对抗 - 与幕后黑手展开激烈的对抗 +第72章 :星河的牺牲 - 星河为了拯救提瓦特做出牺牲 +第73章 :派蒙的付出 - 派蒙为了帮助星河付出一切 +第74章 :丽莎的回归 - 丽莎以另一种方式回归,继续守护蒙德 +第75章 :旅行者的抉择 - 旅行者做出最终的抉择 +第76章 :钟离的真相 - 钟离的真实目的最终揭晓 +第77章 :星河的复活 - 星河以意想不到的方式复活 +第78章 :新的危机 - 新的危机出现,提瓦特面临新的挑战 +第79章 :星河的抉择 - 星河需要做出新的抉择,选择自己的未来 +第80章 :派蒙的未来 - 派蒙的未来充满希望与未知 +第81章 :丽莎的守护 - 丽莎继续守护蒙德,为提瓦特贡献力量 +第82章 :旅行者的旅程 - 旅行者继续自己的旅程,探索提瓦特的奥秘 +第83章 :钟离的隐退 - 钟离选择隐退,默默守护提瓦特 +第84章 :星河的归宿 - 星河思考自己的归宿,是留下还是离开 +第85章 :与伙伴的告别 - 与伙伴们告别,回忆共同经历的冒险 +第86章 :返回星穹铁道? - 星河考虑返回星穹铁道,但心中仍有牵挂 +第87章 :提瓦特的未来 - 提瓦特的未来充满希望与挑战 +第88章 :科技与魔法的融合 - 星穹铁道科技与提瓦特魔法开始融合 +第89章 :新的时代 - 提瓦特进入新的时代,科技与魔法共存 +第90章 :星河的遗产 - 星河留下宝贵的遗产,影响着提瓦特的未来 +第91章 :派蒙的传承 - 派蒙将自己的智慧与经验传承下去 +第92章 :丽莎的教导 - 丽莎继续教导年轻一代,传承魔法的艺术 +第93章 :旅行者的故事 - 旅行者的故事继续,探索更多未知的领域 +第94章 :钟离的箴言 - 钟离留下意味深长的箴言,引导提瓦特的未来 +第95章 :星河的回归 - 星河最终做出决定,选择自己的归宿 +第96章 :告别的时刻 - 与提瓦特的伙伴们告别,心中充满不舍 +第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 +第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 +第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 +第100章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局 \ No newline at end of file diff --git a/Src/Novel_setting.txt b/Src/Novel_setting.txt new file mode 100644 index 0000000..5767994 --- /dev/null +++ b/Src/Novel_setting.txt @@ -0,0 +1,33 @@ + +小说设定:《星河挽歌:提瓦特星轨》 + +小说类型:玄幻穿越,史诗级冒险,轻松幽默与细腻情感并存,整体偏正剧,但避免过于沉重。 + +字数:约400万字(100章 x 4000字/章) + +视角:第三人称,穿插主角第一人称内心独白。 + +世界观:基于原神世界线平行宇宙的提瓦特大陆,地理环境与原神基本一致,但部分区域势力分布及历史事件有所调整,以适应主角的到来和剧情发展。七国结构保留,但政治格局和社会风貌因主角介入而变。科技与魔法并存,魔法更为显著,主角的星穹铁道科技作为“外来力量”融入其中,引发冲突和变革。重要历史事件保留原神设定,并添加主角引发的新的历史事件。 + +核心内容梗概:主角星河在星穹铁道执行任务时意外穿越到提瓦特大陆,发现大陆面临史无前例的巨大危机。凭借星穹铁道科技力量及经验,星河成为对抗危机的关键人物。在拯救提瓦特过程中,他将与原神角色相遇,建立深厚情谊或产生冲突,最终完成使命,并决定自己的归宿。采用英雄之旅叙事结构,主线为拯救提瓦特,副线为主角与原神角色的情感纠葛。 + +主要角色: + +1. 星河:坚毅、责任感强、略带孤傲,内心温柔渴望归宿。拥有强大的星穹铁道科技力量,但更倾向于智慧和策略解决问题。拥有丰富战斗经验和冷静判断力,但也有过去任务的阴影和对未来命运的迷茫,偶尔冲动,也可能因自身能力产生优越感。他的目标是拯救提瓦特,找到归宿,弥补过去遗憾。他的隐藏秘密是:在星穹铁道的一次任务中,他导致一位重要伙伴牺牲,这个秘密影响着他与他人的关系,并在与原神角色建立深厚感情时成为关键转折点。 + +2. 派蒙:活泼、贪吃、略显成熟,更能理解星河内心世界,给予更深层次的支持。积极参与冒险,但鲁莽会带来麻烦,是重要的喜剧元素。对提瓦特知识渊博,但对星穹铁道科技一无所知。她的目标是帮助星河拯救提瓦特,探索自身存在秘密,与星河建立更深厚羁绊。她的隐藏秘密是:她并非简单的导航精灵,拥有远超常人的智慧和感知能力,与提瓦特大陆的过去有着神秘联系。这会在中段通过关键事件逐步揭示。 她的“记忆碎片”——偶尔失神,喃喃自语,对特定场景反应强烈,甚至出现幻觉,这些会逐渐清晰,暗示她与提瓦特古老文明甚至超越提瓦特的力量有关。 + +3. 丽莎:慵懒、智慧,强大的魔法能力,比原神设定中更成熟、更富有责任感。对星穹铁道科技好奇又警惕,将星河视为竞争对手和值得培养的对象。她会引导主角,但在关键时刻设置考验。她的目标是保护蒙德,培养星河,并寻求突破自身瓶颈。她的隐藏秘密是:她拥有不为人知的秘密身份,与某个神秘组织有关,该组织与即将到来的危机密切相关。她的“禁忌魔法”——偶尔使用威力远超平时能力的魔法,使用后疲惫甚至身体异样,她会刻意隐藏,这与她的神秘组织联系紧密。 + +4. 原神旅行者:与星河互动,产生冲突与合作。两人对提瓦特大陆的理解不同,可能会产生矛盾,但在对抗危机时携手合作。 + +5. 钟离:神秘幕后人物,引导星河完成使命,但目的和动机不明,会带来考验和帮助,也可能成为潜在敌人。他的“真实目的”——对星河的帮助看似无私,但始终保持距离,言行举止充满深意,他的行动与提瓦特危机有关,目的可能并非简单帮助主角,而是为了实现某种更宏大的计划。 + +角色关系与冲突:星河与派蒙(依赖与独立的矛盾),星河与丽莎(竞争与合作),星河与原神旅行者(身份认同冲突),星河与钟离(考验与引导)。 + +暗线与伏笔:派蒙的记忆碎片,丽莎的禁忌魔法,星河的星穹铁道追捕令,钟离的真实目的,提瓦特大陆的失落科技。这些暗线相互交织,推动剧情发展,并为角色成长和故事走向增添可能性和悬念。 + +结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 + + +世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 \ No newline at end of file diff --git a/embedding_ollama.py b/embedding_ollama.py new file mode 100644 index 0000000..4bbd8e7 --- /dev/null +++ b/embedding_ollama.py @@ -0,0 +1,45 @@ +# embedding_ollama.py +import requests +from typing import List + +class OllamaEmbeddings: + """ + Ollama 本地服务提供 /api/embeddings 接口,响应中包含 {"embedding": [...]}。 + """ + + def __init__(self, model_name: str, base_url: str): + self.model_name = model_name + self.base_url = base_url + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """ + 将多段文本转换为向量列表 + """ + embeddings = [] + for text in texts: + emb = self.embed_single_document(text) + embeddings.append(emb) + return embeddings + + def embed_query(self, query: str) -> List[float]: + """ + 将单条 query 转换为 embedding 向量 + """ + return self.embed_single_document(query) + + def embed_single_document(self, text: str) -> List[float]: + """ + 调用 Ollama 本地服务接口,获取文本的 embedding + """ + url = f"{self.base_url}/api/embeddings" + data = { + "model": self.model_name, + "prompt": text + } + try: + response = requests.post(url, json=data) + response.raise_for_status() + result = response.json() + return result["embedding"] + except requests.exceptions.RequestException as e: + raise Exception(f"Ollama embeddings request error: {e}") diff --git a/main.spec b/main.spec index 93ae127..cf52c4c 100644 --- a/main.spec +++ b/main.spec @@ -40,7 +40,7 @@ exe = EXE( a.scripts, [], exclude_binaries=True, - name='AI_NovelGenerator_V1.2.3', + name='AI_NovelGenerator_V1.2.4', debug=True, bootloader_ignore_signals=False, strip=False, @@ -60,5 +60,5 @@ coll = COLLECT( strip=False, upx=True, upx_exclude=[], - name='AI_NovelGenerator_V1.2.3' + name='AI_NovelGenerator_V1.2.4' ) diff --git a/novel_generator.py b/novel_generator.py index a5d62bf..66807a4 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -51,7 +51,6 @@ if not os.path.exists(VECTOR_STORE_DIR): def clear_vector_store(): """ 清空本地向量库(删除 vectorstore 文件夹内的内容)。 - 需要在UI中加一个二次确认弹窗,防止误删。 """ if os.path.exists(VECTOR_STORE_DIR): try: @@ -68,14 +67,16 @@ 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]) -> Chroma: +def init_vector_store(api_key: str, base_url: 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=base_url + openai_api_base=embed_url ) documents = [Document(page_content=t) for t in texts] vectorstore = Chroma.from_documents( @@ -86,34 +87,38 @@ def init_vector_store(api_key: str, base_url: str, texts: List[str]) -> Chroma: vectorstore.persist() return vectorstore -def load_vector_store(api_key: str, base_url: str) -> Optional[Chroma]: - """读取已存在的向量库。若不存在则返回 None。""" +def load_vector_store(api_key: str, base_url: 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 = OpenAIEmbeddings( openai_api_key=api_key, - openai_api_base=base_url + openai_api_base=embed_url ) return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings) -def update_vector_store(api_key: str, base_url: str, new_chapter: str) -> None: +def update_vector_store(api_key: str, base_url: str, new_chapter: str, embedding_base_url: str = "") -> None: """将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。""" - store = load_vector_store(api_key, base_url) + store = load_vector_store(api_key, 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, [new_chapter]) + init_vector_store(api_key, base_url, [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) -> str: +def get_relevant_context_from_vector_store(api_key: str, base_url: str, query: str, k: int = 2, embedding_base_url: str = "") -> str: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 若向量库不存在则返回空字符串。 """ - store = load_vector_store(api_key, base_url) + store = load_vector_store(api_key, base_url, embedding_base_url) if not store: logging.warning("Vector store not found. Returning empty context.") return "" @@ -288,25 +293,18 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int texts.append(text) return texts -def summarize_recent_chapters(model: ChatOpenAI, chapters_text_list: List[str]) -> str: +def summarize_recent_chapters(model, chapters_text_list: List[str]) -> str: """ 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 + 这里仅作示例,实际可传入 ChatOpenAI 或其他模型对象,以获取真实摘要。 """ if not chapters_text_list: return "" + # 模拟返回合并摘要,这里不做真实OpenAI调用 combined_text = "\n".join(chapters_text_list) - prompt = f"""\ -这是最近几章的故事内容,请生成一份详细的短期内容摘要(不少于一章篇幅的细节),用于帮助后续创作时回顾细节。 -请着重强调发生的事件、角色的心理和关系变化、冲突或悬念等。 - -{combined_text} -""" - response = model.invoke(prompt) - if not response: - return "" - debug_log(prompt, response.content) - return response.content.strip() + # 简单演示:直接返回合并后的文本,或你自己实现真正的摘要逻辑 + return f"【摘要】最近几章内容:\n{combined_text[:800]}..." # 截断示例 # ============ 新增1:记录剧情要点/未解决冲突 ============ @@ -379,6 +377,7 @@ def generate_chapter_draft( chapter_brief = chapter_info["chapter_brief"] # 1) 从向量库检索往期上下文 + # 在此示例中,如需独立的embedding url,可自行扩展 relevant_context = get_relevant_context_from_vector_store( api_key, base_url, "回顾剧情", k=2 ) @@ -595,7 +594,7 @@ def enrich_chapter_text( # ============ 导入外部知识文本 ============ -def import_knowledge_file(api_key: str, base_url: str, file_path: str) -> None: +def import_knowledge_file(api_key: str, base_url: str, file_path: str, embedding_base_url: str = "") -> None: """ 将用户选定的文本文件导入到向量库,以便在写作时检索。 """ @@ -610,10 +609,10 @@ def import_knowledge_file(api_key: str, base_url: str, file_path: str) -> None: paragraphs = advanced_split_content(content) - store = load_vector_store(api_key, base_url) + store = load_vector_store(api_key, base_url, embedding_base_url) if not store: logging.info("Vector store does not exist. Initializing a new one for knowledge import...") - init_vector_store(api_key, base_url, paragraphs) + init_vector_store(api_key, base_url, paragraphs, embedding_base_url) return docs = [Document(page_content=p) for p in paragraphs] @@ -627,7 +626,7 @@ def advanced_split_content(content: str, """ 将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 """ - nltk.download('punkt_tab', quiet=True) + nltk.download('punkt_tab', quiet=True) # 如有需求,可改成 'punkt' sentences = nltk.sent_tokenize(content) if not sentences: diff --git a/ui.py b/ui.py index d5d79ca..4bfa638 100644 --- a/ui.py +++ b/ui.py @@ -1,12 +1,13 @@ # ui.py # -*- coding: utf-8 -*- + import os -import tkinter as tk -from tkinter import ttk, filedialog, scrolledtext, messagebox import threading +import customtkinter as ctk +from tkinter import filedialog, messagebox from config_manager import load_config, save_config -from utils import read_file +from utils import read_file, save_string_to_txt from novel_generator import ( Novel_novel_directory_generate, generate_chapter_draft, @@ -18,181 +19,456 @@ from novel_generator import ( ) from consistency_checker import check_consistency +# 设置全局主题和颜色 +ctk.set_appearance_mode("System") +ctk.set_default_color_theme("blue") + class NovelGeneratorGUI: def __init__(self, master): self.master = master - self.master.title("Novel Generator GUI") + self.master.title("Novel Generator GUI (CustomTkinter)") + self.master.iconbitmap("icon.ico") + # 窗口最大化 + self.master.state("zoomed") + # 配置窗口大小 + self.master.geometry("1344x896") # 配置持久化 self.config_file = "config.json" self.loaded_config = load_config(self.config_file) - # 使用 PanedWindow 实现左右布局 - main_pane = ttk.PanedWindow(master, orient="horizontal") - main_pane.pack(fill="both", expand=True) + # ========== 主要的属性变量 ========== + # 右侧参数区 - 各种输入 + 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.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7)) + self.topic_default = self.loaded_config.get("topic", "") + self.genre_var = ctk.StringVar(value=self.loaded_config.get("genre", "玄幻")) + self.num_chapters_var = ctk.IntVar(value=self.loaded_config.get("num_chapters", 10)) + self.word_number_var = ctk.IntVar(value=self.loaded_config.get("word_number", 3000)) + self.filepath_var = ctk.StringVar(value=self.loaded_config.get("filepath", "")) - # 左侧:显示区(上下分区) - self.left_frame = ttk.Frame(main_pane) - main_pane.add(self.left_frame, weight=3) + self.chapter_num_var = ctk.IntVar(value=1) - # 右侧:参数输入区 - self.right_frame = ttk.Frame(main_pane, padding="10 10 10 10") - main_pane.add(self.right_frame, weight=1) + # ========== 主容器使用 TabView ========== + self.tabview = ctk.CTkTabview(self.master, width=1200, height=800) + self.tabview.pack(fill="both", expand=True) - # 左侧布局:日志区 + 章节内容 + # 创建各个Tab + self.main_tab = self.tabview.add("主功能") + self.setting_tab = self.tabview.add("Novel Settings") + self.directory_tab = self.tabview.add("Novel Directory") + self.character_tab = self.tabview.add("Character State") + self.summary_tab = self.tabview.add("Global Summary") + + # 构建主功能Tab的布局 + self.build_main_tab() + + # 构建“Novel Settings”编辑Tab + self.build_setting_tab() + + # 构建“Novel Directory”编辑Tab + self.build_directory_tab() + + # 角色状态 Tab + self.build_character_tab() + + # 全局摘要 Tab + self.build_summary_tab() + + # ------------------ 主功能 Tab ------------------ + def build_main_tab(self): + """ + 主Tab: 左侧显示日志 / 本章内容, 右侧显示主要功能操作区 + """ + self.main_tab.rowconfigure(0, weight=1) + self.main_tab.columnconfigure(0, weight=1) + self.main_tab.columnconfigure(1, weight=0) + + # 左侧Frame + self.left_frame = ctk.CTkFrame(self.main_tab) + self.left_frame.grid(row=0, column=0, sticky="nsew", padx=2, pady=2) + # 右侧Frame + self.right_frame = ctk.CTkFrame(self.main_tab) + self.right_frame.grid(row=0, column=1, sticky="nsew", padx=2, pady=2) + + # 左侧布局 self.build_left_layout() - # 右侧布局:参数输入区 + # 右侧布局 self.build_right_layout() def build_left_layout(self): - self.left_frame.rowconfigure(0, weight=1) - self.left_frame.rowconfigure(1, weight=1) - self.left_frame.columnconfigure(0, weight=1) + """ + 左侧包含两个区域: + 1. 输出日志(下半部分) + 2. 本章内容(上半部分) + """ + self.left_frame.grid_rowconfigure(0, weight=3) + self.left_frame.grid_rowconfigure(1, weight=1) + self.left_frame.grid_columnconfigure(0, weight=1) - # 日志区 - log_frame = ttk.LabelFrame(self.left_frame, text="输出日志") - log_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) - log_frame.rowconfigure(0, weight=1) - log_frame.columnconfigure(0, weight=1) + # 本章内容 + 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.log_text = scrolledtext.ScrolledText(log_frame, width=80, height=10) - self.log_text.grid(row=0, column=0, sticky="nsew") + 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)) - # 章节内容区 - chapter_frame = ttk.LabelFrame(self.left_frame, text="本章内容") - chapter_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) - chapter_frame.rowconfigure(0, weight=1) - chapter_frame.columnconfigure(0, weight=1) + # 输出日志 + 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.chapter_result = scrolledtext.ScrolledText(chapter_frame, width=80, height=10, foreground="blue") - self.chapter_result.grid(row=0, column=0, sticky="nsew") + 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)) def build_right_layout(self): - # 行列配置 - for i in range(20): - self.right_frame.rowconfigure(i, weight=0) - self.right_frame.columnconfigure(1, weight=1) + """ + 右侧用于显示一系列参数输入和功能按钮。 + """ + # 配置网格 + 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) # 1. API Key - ttk.Label(self.right_frame, text="API Key:").grid(row=0, column=0, padx=5, pady=5, sticky="e") - self.api_key_var = tk.StringVar(value=self.loaded_config.get("api_key", "")) - ttk.Entry(self.right_frame, textvariable=self.api_key_var, width=32).grid(row=0, column=1, padx=5, pady=5, sticky="w") + api_key_label = ctk.CTkLabel(self.right_frame, 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") # 2. Base URL - ttk.Label(self.right_frame, text="Base URL:").grid(row=1, column=0, padx=5, pady=5, sticky="e") - self.base_url_var = tk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1")) - ttk.Entry(self.right_frame, textvariable=self.base_url_var, width=32).grid(row=1, column=1, padx=5, pady=5, sticky="w") + base_url_label = ctk.CTkLabel(self.right_frame, 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") + + # 2.1 接口格式 下拉菜单 + interface_label = ctk.CTkLabel(self.right_frame, 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") # 3. Model Name - ttk.Label(self.right_frame, text="Model Name:").grid(row=2, column=0, padx=5, pady=5, sticky="e") - self.model_name_var = tk.StringVar(value=self.loaded_config.get("model_name", "gpt-4o-mini")) - ttk.Entry(self.right_frame, textvariable=self.model_name_var, width=32).grid(row=2, column=1, padx=5, pady=5, sticky="w") + model_name_label = ctk.CTkLabel(self.right_frame, 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") + + # 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") # 4. Temperature - ttk.Label(self.right_frame, text="Temperature:").grid(row=3, column=0, padx=5, pady=5, sticky="e") - self.temperature_var = tk.DoubleVar(value=self.loaded_config.get("temperature", 0.7)) - self.temp_value_label = ttk.Label(self.right_frame, text=f"{self.temperature_var.get():.2f}") - self.temp_value_label.grid(row=3, column=2, padx=5, pady=5, sticky="w") - - temp_scale = ttk.Scale(self.right_frame, from_=0.0, to=1.0, orient=tk.HORIZONTAL, variable=self.temperature_var) - temp_scale.grid(row=3, column=1, padx=5, pady=5, sticky="we") - def update_temp_label(*args): - self.temp_value_label.config(text=f"{self.temperature_var.get():.2f}") - self.temperature_var.trace("w", update_temp_label) + 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) 多行输入 - ttk.Label(self.right_frame, text="主题(Topic):").grid(row=4, column=0, padx=5, pady=5, sticky="ne") - self.topic_text = scrolledtext.ScrolledText(self.right_frame, width=32, height=4) - self.topic_text.grid(row=4, column=1, padx=5, pady=5, sticky="w") - topic_default = self.loaded_config.get("topic", "") - if topic_default: - self.topic_text.insert(tk.END, topic_default) + 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") + 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") + if self.topic_default: + self.topic_text.insert("0.0", self.topic_default) # 6. 类型(Genre) - ttk.Label(self.right_frame, text="类型(Genre):").grid(row=5, column=0, padx=5, pady=5, sticky="e") - self.genre_var = tk.StringVar(value=self.loaded_config.get("genre", "玄幻")) - ttk.Entry(self.right_frame, textvariable=self.genre_var, width=32).grid(row=5, column=1, padx=5, pady=5, sticky="w") + 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_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") # 7. 章节数 - ttk.Label(self.right_frame, text="章节数:").grid(row=6, column=0, padx=5, pady=5, sticky="e") - self.num_chapters_var = tk.IntVar(value=self.loaded_config.get("num_chapters", 10)) - ttk.Entry(self.right_frame, textvariable=self.num_chapters_var, width=8).grid(row=6, column=1, padx=5, pady=5, sticky="w") + 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_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") # 8. 每章字数 - ttk.Label(self.right_frame, text="每章字数:").grid(row=7, column=0, padx=5, pady=5, sticky="e") - self.word_number_var = tk.IntVar(value=self.loaded_config.get("word_number", 3000)) - ttk.Entry(self.right_frame, textvariable=self.word_number_var, width=8).grid(row=7, 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=9, 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") # 9. 文件保存路径 - ttk.Label(self.right_frame, text="保存路径:").grid(row=8, column=0, padx=5, pady=5, sticky="e") - self.filepath_var = tk.StringVar(value=self.loaded_config.get("filepath", "")) - ttk.Entry(self.right_frame, textvariable=self.filepath_var, width=32).grid(row=8, column=1, padx=5, pady=5, sticky="w") - ttk.Button(self.right_frame, text="浏览...", command=self.browse_folder).grid(row=8, column=2, padx=5, pady=5, sticky="w") + 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_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") # 保存/加载配置按钮 - config_frame = ttk.Frame(self.right_frame) - config_frame.grid(row=9, column=1, sticky="w") - ttk.Button(config_frame, text="保存配置", command=self.save_config_btn).grid(row=0, column=0, padx=5) - ttk.Button(config_frame, text="加载配置", command=self.load_config_btn).grid(row=0, column=1, padx=5) + 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. 章节号 - ttk.Label(self.right_frame, text="章节号:").grid(row=10, column=0, sticky="e") - self.chapter_num_var = tk.IntVar(value=1) - ttk.Entry(self.right_frame, textvariable=self.chapter_num_var, width=6).grid(row=10, column=1, padx=5, pady=5, sticky="w") + 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_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") # 11. “用户指导” 多行输入 - ttk.Label(self.right_frame, text="本章指导:").grid(row=11, column=0, padx=5, pady=5, sticky="ne") - self.user_guide_text = scrolledtext.ScrolledText(self.right_frame, width=32, height=4) - self.user_guide_text.grid(row=11, column=1, padx=5, pady=5, sticky="w") + 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") + 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") - row_base = 12 - # ============ 功能按钮 ============ - - # (1) 生成设定 & 目录 - self.btn_generate_full = ttk.Button(self.right_frame, text="Step1. 生成设定 & 目录", command=self.generate_full_novel) - self.btn_generate_full.grid(row=row_base, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (2) 生成章节草稿 - self.btn_generate_chapter = ttk.Button(self.right_frame, text="Step2. 生成章节草稿", command=self.generate_chapter_draft_ui) - self.btn_generate_chapter.grid(row=row_base+1, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (3) 定稿当前章节 - self.btn_finalize_chapter = ttk.Button(self.right_frame, text="Step3. 定稿当前章节", command=self.finalize_chapter_ui) - self.btn_finalize_chapter.grid(row=row_base+2, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (4) 一致性审校 - self.btn_check_consistency = ttk.Button(self.right_frame, text="[可选]一致性审校", command=self.do_consistency_check) - self.btn_check_consistency.grid(row=row_base+3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (5) 导入知识库文件 - self.btn_import_knowledge = ttk.Button(self.right_frame, text="[可选]导入知识库", command=self.import_knowledge_handler) - self.btn_import_knowledge.grid(row=row_base+4, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (6) 清空向量库 - self.btn_clear_vectorstore = ttk.Button(self.right_frame, text="清空向量库", command=self.clear_vectorstore_handler) - self.btn_clear_vectorstore.grid(row=row_base+5, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - # (7) 查看剧情要点 - ttk.Button(self.right_frame, text="[查看] 剧情要点", command=self.show_plot_arcs_ui).grid( - row=row_base+6, column=0, columnspan=2, padx=5, pady=5, sticky="ew" + # 功能按钮区域 + 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_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_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_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_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_clear_vectorstore = ctk.CTkButton( + self.right_frame, text="清空向量库", + 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") + + 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") + + # ------------------ Novel Settings Tab ------------------ + def build_setting_tab(self): + """ + 可查看/编辑 Novel_setting.txt 并保存 + """ + self.setting_tab.rowconfigure(0, weight=0) + 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.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.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)) + self.setting_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + + def load_novel_setting(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + setting_file = os.path.join(filepath, "Novel_setting.txt") + content = read_file(setting_file) + self.setting_text.delete("0.0", "end") + self.setting_text.insert("0.0", content) + self.log("已加载 Novel_setting.txt 内容到编辑区。") + + def save_novel_setting(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + content = self.setting_text.get("0.0", "end").strip() + setting_file = os.path.join(filepath, "Novel_setting.txt") + save_string_to_txt(content, setting_file) + self.log("已保存对 Novel_setting.txt 的修改。") + + # ------------------ Novel Directory Tab ------------------ + def build_directory_tab(self): + """ + 可查看/编辑 Novel_directory.txt 并保存 + """ + self.directory_tab.rowconfigure(0, weight=0) + 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.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.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)) + self.directory_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + + def load_novel_directory(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + directory_file = os.path.join(filepath, "Novel_directory.txt") + content = read_file(directory_file) + self.directory_text.delete("0.0", "end") + self.directory_text.insert("0.0", content) + self.log("已加载 Novel_directory.txt 内容到编辑区。") + + def save_novel_directory(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + content = self.directory_text.get("0.0", "end").strip() + directory_file = os.path.join(filepath, "Novel_directory.txt") + save_string_to_txt(content, directory_file) + self.log("已保存对 Novel_directory.txt 的修改。") + + # ------------------ Character State Tab ------------------ + def build_character_tab(self): + """ + 查看/编辑 character_state.txt + """ + self.character_tab.rowconfigure(0, weight=0) + 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.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.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)) + self.character_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + + def load_character_state(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + char_file = os.path.join(filepath, "character_state.txt") + content = read_file(char_file) + self.character_text.delete("0.0", "end") + self.character_text.insert("0.0", content) + self.log("已加载 character_state.txt 内容到编辑区。") + + def save_character_state(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + content = self.character_text.get("0.0", "end").strip() + char_file = os.path.join(filepath, "character_state.txt") + save_string_to_txt(content, char_file) + self.log("已保存对 character_state.txt 的修改。") + + # ------------------ Global Summary Tab ------------------ + def build_summary_tab(self): + """ + 查看/编辑 global_summary.txt + """ + self.summary_tab.rowconfigure(0, weight=0) + 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.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.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)) + self.summary_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + + def load_global_summary(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + summary_file = os.path.join(filepath, "global_summary.txt") + content = read_file(summary_file) + self.summary_text.delete("0.0", "end") + self.summary_text.insert("0.0", content) + self.log("已加载 global_summary.txt 内容到编辑区。") + + def save_global_summary(self): + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + + content = self.summary_text.get("0.0", "end").strip() + summary_file = os.path.join(filepath, "global_summary.txt") + save_string_to_txt(content, summary_file) + self.log("已保存对 global_summary.txt 的修改。") + + # ------------------ 配置管理 ------------------ def load_config_btn(self): cfg = load_config(self.config_file) if cfg: self.api_key_var.set(cfg.get("api_key", "")) self.base_url_var.set(cfg.get("base_url", "")) + 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.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)) self.word_number_var.set(cfg.get("word_number", 3000)) self.filepath_var.set(cfg.get("filepath", "")) - # 多行文本 - self.topic_text.delete("1.0", tk.END) - self.topic_text.insert(tk.END, cfg.get("topic", "")) + # 主题 + self.topic_text.delete("0.0", "end") + self.topic_text.insert("0.0", cfg.get("topic", "")) self.log("已加载配置。") else: @@ -202,9 +478,11 @@ class NovelGeneratorGUI: config_data = { "api_key": self.api_key_var.get(), "base_url": self.base_url_var.get(), + "interface_format": self.interface_format_var.get(), "model_name": self.model_name_var.get(), + "embedding_url": self.embedding_url_var.get(), "temperature": self.temperature_var.get(), - "topic": self.topic_text.get("1.0", tk.END).strip(), + "topic": self.topic_text.get("0.0", "end").strip(), "genre": self.genre_var.get(), "num_chapters": self.num_chapters_var.get(), "word_number": self.word_number_var.get(), @@ -221,17 +499,17 @@ class NovelGeneratorGUI: if selected_dir: self.filepath_var.set(selected_dir) - # -------------- 日志输出 -------------- + # ------------------ 日志输出 ------------------ def log(self, message: str): - self.log_text.insert(tk.END, message + "\n") - self.log_text.see(tk.END) + self.log_text.insert("end", message + "\n") + self.log_text.see("end") - # -------------- 功能 -------------- + # ------------------ 功能区 -------------------- def disable_button(self, btn): - btn.config(state=tk.DISABLED) + btn.configure(state="disabled") def enable_button(self, btn): - btn.config(state=tk.NORMAL) + btn.configure(state="normal") def generate_full_novel(self): """生成小说设定 & 目录""" @@ -241,7 +519,7 @@ class NovelGeneratorGUI: api_key = self.api_key_var.get().strip() base_url = self.base_url_var.get().strip() model_name = self.model_name_var.get().strip() - topic = self.topic_text.get("1.0", tk.END).strip() + topic = self.topic_text.get("0.0", "end").strip() genre = self.genre_var.get().strip() num_chapters = self.num_chapters_var.get() word_number = self.word_number_var.get() @@ -284,6 +562,10 @@ class NovelGeneratorGUI: temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() + if not filepath: + self.log("请先配置保存文件路径。") + return + novel_settings_file = os.path.join(filepath, "Novel_setting.txt") novel_settings = read_file(novel_settings_file) if not novel_settings.strip(): @@ -299,15 +581,17 @@ class NovelGeneratorGUI: chap_num = self.chapter_num_var.get() word_number = self.word_number_var.get() - user_guidance = self.user_guide_text.get("1.0", tk.END).strip() + user_guidance = self.user_guide_text.get("0.0", "end").strip() - # 获取最近3章文本,生成短期摘要 + # 获取最近3章文本 chapters_dir = os.path.join(filepath, "chapters") recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3) # 用当前模型生成一个较为详细的最近剧情摘要 - model_obj = self.get_llm_model(model_name, api_key, base_url, temperature) - recent_chapters_summary = summarize_recent_chapters(model_obj, recent_3_texts) + recent_chapters_summary = summarize_recent_chapters( + None, # 这里只是示例,实际可根据你的需求传入对应的模型 + recent_3_texts + ) self.log(f"开始生成第{chap_num}章草稿...") draft_text = generate_chapter_draft( @@ -327,9 +611,9 @@ class NovelGeneratorGUI: ) if draft_text: self.log(f"✅ 第{chap_num}章草稿生成完成。请在下方查看。") - self.chapter_result.delete("1.0", tk.END) - self.chapter_result.insert(tk.END, draft_text) - self.chapter_result.see(tk.END) + self.chapter_result.delete("0.0", "end") + self.chapter_result.insert("0.0", draft_text) + self.chapter_result.see("end") else: self.log("⚠️ 本章草稿生成失败或无内容。") @@ -352,6 +636,10 @@ class NovelGeneratorGUI: temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() + if not filepath: + self.log("请先配置保存文件路径。") + return + chap_num = self.chapter_num_var.get() word_number = self.word_number_var.get() @@ -370,9 +658,9 @@ class NovelGeneratorGUI: # 读取定稿后的文本显示 chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") final_text = read_file(chap_file) - self.chapter_result.delete("1.0", tk.END) - self.chapter_result.insert(tk.END, final_text) - self.chapter_result.see(tk.END) + self.chapter_result.delete("0.0", "end") + self.chapter_result.insert("0.0", final_text) + self.chapter_result.see("end") except Exception as e: self.log(f"❌ 定稿章节时出错: {e}") @@ -393,16 +681,19 @@ class NovelGeneratorGUI: temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() - # 读取关键文件 + if not filepath: + self.log("请先配置保存文件路径。") + return + novel_settings_file = os.path.join(filepath, "Novel_setting.txt") 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") novel_setting = read_file(novel_settings_file) character_state = read_file(character_state_file) global_summary = read_file(global_summary_file) - plot_arcs = read_file(plot_arcs_file) # 新增 + plot_arcs = read_file(plot_arcs_file) # 获取当前章节文本 chap_num = self.chapter_num_var.get() @@ -423,7 +714,7 @@ class NovelGeneratorGUI: base_url=base_url, model_name=model_name, temperature=temperature, - plot_arcs=plot_arcs # 新增传入 + plot_arcs=plot_arcs ) self.log("审校结果:") self.log(result) @@ -450,6 +741,7 @@ 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 file_path=selected_file ) self.log("✅ 知识库文件导入完成。") @@ -462,42 +754,43 @@ class NovelGeneratorGUI: thread.start() def clear_vectorstore_handler(self): - """ - 清空向量库按钮:弹出二次确认,若确认则执行 clear_vector_store()。 - """ - def confirmed_clear(): + """清空向量库按钮:弹出二次确认。""" + first_confirm = messagebox.askyesno("警告", "确定要清空本地向量库吗?此操作不可恢复!") + if first_confirm: second_confirm = messagebox.askyesno("二次确认", "你确定真的要删除所有向量数据吗?此操作不可恢复!") if second_confirm: clear_vector_store() self.log("已清空向量库。") - first_confirm = messagebox.askyesno("警告", "确定要清空本地向量库吗?此操作不可恢复!") - if first_confirm: - confirmed_clear() - - # =========== 新增:在 UI 中查看当前剧情要点 ============= def show_plot_arcs_ui(self): + """[查看]当前剧情要点""" filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先在主Tab中设置保存文件路径") + return + plot_arcs_file = os.path.join(filepath, "plot_arcs.txt") if not os.path.exists(plot_arcs_file): messagebox.showinfo("剧情要点", "当前还未生成任何剧情要点或未解决冲突。") return + arcs_text = read_file(plot_arcs_file).strip() if not arcs_text: arcs_text = "当前没有记录的剧情要点或冲突。" - # 弹出一个简单的弹窗显示 - top = tk.Toplevel(self.master) - top.title("剧情要点/未解决冲突") - text_area = scrolledtext.ScrolledText(top, width=60, height=20) - text_area.pack(fill="both", expand=True) - text_area.insert(tk.END, arcs_text) - text_area.config(state=tk.DISABLED) - def get_llm_model(self, model_name, api_key, base_url, temperature): - from langchain_openai import ChatOpenAI - return ChatOpenAI( - model=model_name, - api_key=api_key, - base_url=base_url, - temperature=temperature - ) + # 弹窗显示 + top = ctk.CTkToplevel(self.master) + top.title("剧情要点/未解决冲突") + top.geometry("600x400") + + text_area = ctk.CTkTextbox(top, wrap="word") + text_area.pack(fill="both", expand=True, padx=10, pady=10) + + text_area.insert("0.0", arcs_text) + text_area.configure(state="disabled") + + +if __name__ == "__main__": + app = ctk.CTk() + gui = NovelGeneratorGUI(app) + app.mainloop() From f2dc98744d25b1def291018b0187183491d4352f Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 12:59:13 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=BA=8C=E6=AC=A1?= =?UTF-8?q?=E6=9B=B4=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("✅ 知识库文件导入完成。") From 3dd81ecb12fcaa8a0cc90a0d47902caea7a45261 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 14:39:30 +0800 Subject: [PATCH 03/11] 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) From 7741345ee13e172a5d2c922c17c780f3ca947565 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 15:50:19 +0800 Subject: [PATCH 04/11] 11 --- embedding_ollama.py | 13 +++++--- novel_generator.py | 81 ++++++++++++++++++++++++++++++++------------- ui.py | 17 +++++----- 3 files changed, 74 insertions(+), 37 deletions(-) diff --git a/embedding_ollama.py b/embedding_ollama.py index 36ee973..b755ddd 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -4,13 +4,15 @@ from typing import List class OllamaEmbeddings: """ - Ollama 本地服务提供 /api/embeddings 接口,响应中包含 {"embedding": [...]}。 + Ollama 本地服务提供的 Embedding 接口, + 本需求里我们最终拼出形如: http://localhost:11434/api/embed + 即 base_url + "/embed" """ def __init__(self, model_name: str, base_url: str): self.model_name = model_name - self.base_url = base_url - + self.base_url = base_url # 这里应形如 http://localhost:11434/api (不再含 /v1) + def embed(self, texts: List[str]) -> List[List[float]]: embeddings = [] for text in texts: @@ -35,9 +37,10 @@ class OllamaEmbeddings: def embed_single_document(self, text: str) -> List[float]: """ - 调用 Ollama 本地服务接口,获取文本的 embedding + 调用 Ollama 本地服务接口,获取文本的 embedding。 + 这里统一改为请求: [base_url]/embed """ - url = f"{self.base_url}/api/embeddings" + url = f"{self.base_url}/embed" data = { "model": self.model_name, "prompt": text diff --git a/novel_generator.py b/novel_generator.py index 77b4b56..a9d53ca 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -37,9 +37,10 @@ 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") + """打印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: @@ -58,6 +59,7 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: return True return False + def create_embeddings_object( api_key: str, base_url: str, @@ -68,14 +70,20 @@ def create_embeddings_object( """ 根据用户在UI中配置的参数,返回对应的 embeddings 对象。 - 当 interface_format = "Ollama" => OllamaEmbeddings(...) + (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed) - 当 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) + # 去除末尾斜杠 + fixed_url = embed_url.rstrip("/") + # 如果包含 /v1 则替换为 /api + fixed_url = fixed_url.replace("/v1", "/api") + return OllamaEmbeddings( + model_name=embedding_model_name, + base_url=fixed_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 @@ -85,7 +93,6 @@ def create_embeddings_object( logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") # ============ 向量库相关 ============ - VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): os.makedirs(VECTOR_STORE_DIR) @@ -119,7 +126,7 @@ def init_vector_store( ) -> Chroma: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 - embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url + embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。 """ embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -164,8 +171,8 @@ def update_vector_store( api_key: str, base_url: str, new_chapter: str, - interface_format: str = "OpenAI", - embedding_model_name: str = "", + interface_format: str, + embedding_model_name: str, embedding_base_url: str = "" ) -> None: """ @@ -198,8 +205,8 @@ def get_relevant_context_from_vector_store( api_key: str, base_url: str, query: str, - interface_format: str = "OpenAI", - embedding_model_name: str = "", + interface_format: str, + embedding_model_name: str, embedding_base_url: str = "", k: int = 2 ) -> str: @@ -389,11 +396,24 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int texts.append(text) return texts -def summarize_recent_chapters(model, chapters_text_list: List[str]) -> str: +def summarize_recent_chapters( + llm_model: str, + api_key: str, + base_url: str, + temperature: float, + chapters_text_list: List[str] + ) -> str: """ 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 如果没有可用的模型(model=None),则退化为简单截断示例。 """ + model = ChatOpenAI( + model=llm_model, + api_key=api_key, + base_url=base_url, + temperature=temperature + ) + if not chapters_text_list: return "" @@ -410,7 +430,6 @@ def summarize_recent_chapters(model, chapters_text_list: List[str]) -> str: 1.请用中文输出,不超过500字。 2.仅回复摘要内容,不需要其他信息。 """ - # 调用模型获取摘要 response = model.invoke(prompt) if not response or not response.content.strip(): @@ -421,7 +440,6 @@ def summarize_recent_chapters(model, chapters_text_list: List[str]) -> str: return response.content.strip() - # ============ 新增:更新剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ @@ -498,8 +516,8 @@ def generate_chapter_draft( api_key=api_key, base_url=base_url, query="回顾剧情", - interface_format="OpenAI", # 若需根据 UI 选择可再传参 - embedding_model_name="", # 同上 + interface_format="OpenAI", + embedding_model_name="", embedding_base_url="", k=2 ) @@ -562,6 +580,8 @@ def finalize_chapter( word_number: int, api_key: str, base_url: str, + interface_format: str, + embedding_model_name: str, model_name: str, temperature: float, filepath: str @@ -659,8 +679,8 @@ def finalize_chapter( api_key=api_key, base_url=base_url, new_chapter=chapter_text, - interface_format="OpenAI", - embedding_model_name="" + interface_format=interface_format, + embedding_model_name=embedding_model_name ) logging.info(f"Chapter {novel_number} has been finalized.") @@ -695,10 +715,18 @@ def enrich_chapter_text( # ============ 导入外部知识文本 ============ -def import_knowledge_file(api_key: str, base_url: str, file_path: str, embedding_base_url: str = "") -> None: +def import_knowledge_file( + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + file_path: str, + embedding_base_url: str = "" + ) -> None: """ 将用户选定的文本文件导入到向量库,以便在写作时检索。 """ + logging.info(f"开始导入知识库文件: {file_path},当前接口格式: {interface_format},当前模型: {embedding_model_name}") if not os.path.exists(file_path): logging.warning(f"知识库文件不存在: {file_path}") return @@ -710,10 +738,17 @@ def import_knowledge_file(api_key: str, base_url: str, file_path: str, embedding paragraphs = advanced_split_content(content) - 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 for knowledge import...") - init_vector_store(api_key, base_url, paragraphs, embedding_base_url) + init_vector_store( + api_key, + base_url, + interface_format, + embedding_model_name, + paragraphs, + embedding_base_url + ) return docs = [Document(page_content=p) for p in paragraphs] @@ -727,7 +762,7 @@ def advanced_split_content(content: str, """ 将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 """ - nltk.download('punkt_tab', quiet=True) # 如有需求,可改成 'punkt' + nltk.download('punkt_tab', quiet=True) sentences = nltk.sent_tokenize(content) if not sentences: diff --git a/ui.py b/ui.py index 0832f7a..6bfc0e8 100644 --- a/ui.py +++ b/ui.py @@ -180,14 +180,12 @@ class NovelGeneratorGUI: # 回调:当接口格式下拉框发生变更时,如果 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") + 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:", font=("Microsoft YaHei", 12)) @@ -849,7 +847,8 @@ class NovelGeneratorGUI: import_knowledge_file( api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), - # 传入 embedding_url + embedding_model_name + interface_format=self.interface_format_var.get().strip(), + embedding_base_url=self.embedding_url_var.get().strip(), embedding_base_url=self.embedding_url_var.get().strip(), file_path=selected_file ) From aca9e4868339ac93f3d152a3bbb6962eaee28014 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 17:41:16 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E4=B8=8D=E7=9F=A5=E9=81=93=E5=87=BA?= =?UTF-8?q?=E4=BA=86=E4=BB=80=E4=B9=88=E9=97=AE=E9=A2=98=EF=BC=8C=E5=85=88?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=B8=80=E4=B8=8B=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/Novel_directory.txt | 182 ---------------------------------- Src/Novel_setting.txt | 98 ------------------ embedding_ollama.py | 33 ++++-- novel_generator.py | 215 ++++++++++++++++++++-------------------- ui.py | 71 ++++++------- 5 files changed, 168 insertions(+), 431 deletions(-) delete mode 100644 Src/Novel_directory.txt delete mode 100644 Src/Novel_setting.txt diff --git a/Src/Novel_directory.txt b/Src/Novel_directory.txt deleted file mode 100644 index 3a9cc6f..0000000 --- a/Src/Novel_directory.txt +++ /dev/null @@ -1,182 +0,0 @@ - -第1章 :星河坠落提瓦特 - 穿越与初遇派蒙,危机初现端倪 -第2章 :蒙德的初印象 - 初探蒙德城,与丽莎的首次相遇 -第3章 :酒馆的闲谈 - 了解提瓦特局势,派蒙展现惊人一面 -第4章 :丽莎的试探 - 星河展现科技实力,引来丽莎的警惕 -第5章 :委托与冒险 - 接受委托,初次展现星穹铁道科技的运用 -第6章 :风魔龙的挑战 - 运用科技对抗风魔龙,展现策略与智慧 -第7章 :派蒙的异常 - 派蒙出现记忆碎片,暗示其不寻常身份 -第8章 :丽莎的秘密魔法 - 丽莎使用禁忌魔法,展现其神秘一面 -第9章 :旅行者的出现 - 与原神旅行者初次相遇,产生摩擦与合作 -第10章 :蒙德的阴谋 - 发现蒙德隐藏的危机,与丽莎深入合作 -第11章 :追捕令的阴影 - 星河收到星穹铁道追捕令,内心挣扎 -第12章 :深入调查 - 调查蒙德的危机,发现与丽莎的秘密组织有关 -第13章 :钟离的现身 - 钟离初次出现,对星河进行暗示与引导 -第14章 :失落科技的线索 - 发现关于提瓦特失落科技的线索 -第15章 :前往璃月 - 踏上前往璃月的旅程,面对新的挑战 -第16章 :璃月港的繁华 - 体验璃月港的文化,了解璃月局势 -第17章 :往生堂的委托 - 接受往生堂的委托,深入了解璃月历史 -第18章 :岩王帝君的考验 - 钟离对星河进行考验,揭示其真实目的 -第19章 :地脉异常 - 发现璃月地脉异常,与旅行者合作调查 -第20章 :黄金屋的秘密 - 探索黄金屋的秘密,发现与失落科技有关 -第21章 :刻晴的质疑 - 刻晴对星河的科技持有怀疑态度 -第22章 :对抗岩龙蜥 - 运用科技与策略对抗强大的岩龙蜥 -第23章 :派蒙的回忆 - 派蒙回忆片段增多,身份逐渐清晰 -第24章 :丽莎的引导 - 丽莎引导星河学习提瓦特魔法 -第25章 :星河的抉择 - 星河面临道德困境,做出艰难选择 -第26章 :前往稻妻 - 前往稻妻,面对新的挑战与危机 -第27章 :稻妻的雷电 - 体验稻妻的文化与习俗,对抗雷电将军的威压 -第28章 :八重神子的试探 - 八重神子对星河进行试探,考验其能力 -第29章 :影向地的秘密 - 探索影向地的秘密,发现与提瓦特历史有关 -第30章 :反抗军与合作 - 与稻妻反抗军合作,对抗幕后黑手 -第31章 :永恒的执政官 - 与雷电将军正面交锋,展现智慧与实力 -第32章 :派蒙的过去 - 派蒙的过去逐渐揭开,与提瓦特古老文明有关 -第33章 :丽莎的组织 - 丽莎所属组织的真相逐渐浮出水面 -第34章 :前往须弥 - 前往须弥,面对新的挑战与文化 -第35章 :须弥的智慧 - 体验须弥的文化,了解须弥的智慧与科技 -第36章 :草神的大智慧 - 与草神进行交流,探讨生命的意义 -第37章 :森林的危机 - 须弥森林面临危机,星河运用科技解决问题 -第38章 :沙漠的秘密 - 探索须弥沙漠的秘密,发现远古文明的遗迹 -第39章 :散兵的阴谋 - 散兵的阴谋逐渐浮出水面,星河需要应对 -第40章 :与散兵的对抗 - 与散兵展开对抗,展现星穹铁道科技的优势 -第41章 :前往枫丹 - 前往枫丹,面对新的挑战与科技 -第42章 :枫丹的审判 - 体验枫丹的法律与秩序,面对复杂的案件 -第43章 :水神的神秘 - 与水神进行互动,了解枫丹的秘密 -第44章 :机械岛的危机 - 枫丹机械岛面临危机,星河运用科技解决问题 -第45章 :潜入深海 - 潜入深海,探索深海的秘密与科技 -第46章 :派蒙的预言 - 派蒙预言即将到来的巨大危机 -第47章 :丽莎的抉择 - 丽莎面临艰难的抉择,需要做出牺牲 -第48章 :前往纳塔 - 前往纳塔,面对新的挑战与文化 -第49章 :纳塔的冰雪 - 体验纳塔的严寒环境,与冰雪元素对抗 -第50章 :雪山之巅的秘密 - 探索雪山之巅的秘密,发现远古文明的遗迹 -第51章 :冰神的考验 - 冰神对星河进行考验,考验其心性与毅力 -第52章 :与旅行者的合作 - 与旅行者深入合作,对抗共同的敌人 -第53章 :深渊的威胁 - 深渊的威胁日益逼近,星河需要寻找应对之策 -第54章 :钟离的计划 - 钟离的计划逐渐显露,其真实目的更加清晰 -第55章 :星河的过去 - 星河过去的阴影再次浮现,影响其与他人的关系 -第56章 :派蒙的觉醒 - 派蒙觉醒,展现其强大的力量 -第57章 :丽莎的牺牲 - 丽莎为了保护星河做出牺牲 -第58章 :前往至冬 - 前往至冬,面对新的挑战与阴谋 -第59章 :至冬的阴谋 - 揭露至冬的阴谋,对抗幕后黑手 -第60章 :女皇的算计 - 女皇对星河进行算计,考验其智慧与勇气 -第61章 :机械生命的危机 - 至冬的机械生命失控,引发巨大的危机 -第62章 :星河的策略 - 星河运用策略,化解机械生命的危机 -第63章 :派蒙的秘密力量 - 派蒙展现其强大的秘密力量 -第64章 :丽莎的遗产 - 丽莎留下宝贵的遗产,帮助星河对抗危机 -第65章 :对抗深渊使徒 - 对抗强大的深渊使徒,展现星穹铁道科技的威力 -第66章 :旅行者的选择 - 旅行者面临艰难的选择,需要做出决定 -第67章 :钟离的帮助 - 钟离给予星河关键的帮助,但其目的依旧不明 -第68章 :星河的成长 - 星河在经历种种磨难后得到成长 -第69章 :提瓦特的命运 - 提瓦特的命运掌握在星河手中 -第70章 :最终决战的序幕 - 最终决战的序幕拉开,危机全面爆发 -第71章 :与幕后黑手的对抗 - 与幕后黑手展开激烈的对抗 -第72章 :星河的牺牲 - 星河为了拯救提瓦特做出牺牲 -第73章 :派蒙的付出 - 派蒙为了帮助星河付出一切 -第74章 :丽莎的回归 - 丽莎以另一种方式回归,继续守护蒙德 -第75章 :旅行者的抉择 - 旅行者做出最终的抉择 -第76章 :钟离的真相 - 钟离的真实目的最终揭晓 -第77章 :星河的复活 - 星河以意想不到的方式复活 -第78章 :新的危机 - 新的危机出现,提瓦特面临新的挑战 -第79章 :星河的抉择 - 星河需要做出新的抉择,选择自己的未来 -第80章 :派蒙的未来 - 派蒙的未来充满希望与未知 -第81章 :丽莎的守护 - 丽莎继续守护蒙德,为提瓦特贡献力量 -第82章 :旅行者的旅程 - 旅行者继续自己的旅程,探索提瓦特的奥秘 -第83章 :钟离的隐退 - 钟离选择隐退,默默守护提瓦特 -第84章 :星河的归宿 - 星河思考自己的归宿,是留下还是离开 -第85章 :与伙伴的告别 - 与伙伴们告别,回忆共同经历的冒险 -第86章 :返回星穹铁道? - 星河考虑返回星穹铁道,但心中仍有牵挂 -第87章 :提瓦特的未来 - 提瓦特的未来充满希望与挑战 -第88章 :科技与魔法的融合 - 星穹铁道科技与提瓦特魔法开始融合 -第89章 :新的时代 - 提瓦特进入新的时代,科技与魔法共存 -第90章 :星河的遗产 - 星河留下宝贵的遗产,影响着提瓦特的未来 -第91章 :派蒙的传承 - 派蒙将自己的智慧与经验传承下去 -第92章 :丽莎的教导 - 丽莎继续教导年轻一代,传承魔法的艺术 -第93章 :旅行者的故事 - 旅行者的故事继续,探索更多未知的领域 -第94章 :钟离的箴言 - 钟离留下意味深长的箴言,引导提瓦特的未来 -第95章 :星河的回归 - 星河最终做出决定,选择自己的归宿 -第96章 :告别的时刻 - 与提瓦特的伙伴们告别,心中充满不舍 -第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 -第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 -第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 -第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 deleted file mode 100644 index 340c5cc..0000000 --- a/Src/Novel_setting.txt +++ /dev/null @@ -1,98 +0,0 @@ - -小说设定:《星河挽歌:提瓦特星轨》 - -小说类型:玄幻穿越,史诗级冒险,轻松幽默与细腻情感并存,整体偏正剧,但避免过于沉重。 - -字数:约400万字(100章 x 4000字/章) - -视角:第三人称,穿插主角第一人称内心独白。 - -世界观:基于原神世界线平行宇宙的提瓦特大陆,地理环境与原神基本一致,但部分区域势力分布及历史事件有所调整,以适应主角的到来和剧情发展。七国结构保留,但政治格局和社会风貌因主角介入而变。科技与魔法并存,魔法更为显著,主角的星穹铁道科技作为“外来力量”融入其中,引发冲突和变革。重要历史事件保留原神设定,并添加主角引发的新的历史事件。 - -核心内容梗概:主角星河在星穹铁道执行任务时意外穿越到提瓦特大陆,发现大陆面临史无前例的巨大危机。凭借星穹铁道科技力量及经验,星河成为对抗危机的关键人物。在拯救提瓦特过程中,他将与原神角色相遇,建立深厚情谊或产生冲突,最终完成使命,并决定自己的归宿。采用英雄之旅叙事结构,主线为拯救提瓦特,副线为主角与原神角色的情感纠葛。 - -主要角色: - -1. 星河:坚毅、责任感强、略带孤傲,内心温柔渴望归宿。拥有强大的星穹铁道科技力量,但更倾向于智慧和策略解决问题。拥有丰富战斗经验和冷静判断力,但也有过去任务的阴影和对未来命运的迷茫,偶尔冲动,也可能因自身能力产生优越感。他的目标是拯救提瓦特,找到归宿,弥补过去遗憾。他的隐藏秘密是:在星穹铁道的一次任务中,他导致一位重要伙伴牺牲,这个秘密影响着他与他人的关系,并在与原神角色建立深厚感情时成为关键转折点。 - -2. 派蒙:活泼、贪吃、略显成熟,更能理解星河内心世界,给予更深层次的支持。积极参与冒险,但鲁莽会带来麻烦,是重要的喜剧元素。对提瓦特知识渊博,但对星穹铁道科技一无所知。她的目标是帮助星河拯救提瓦特,探索自身存在秘密,与星河建立更深厚羁绊。她的隐藏秘密是:她并非简单的导航精灵,拥有远超常人的智慧和感知能力,与提瓦特大陆的过去有着神秘联系。这会在中段通过关键事件逐步揭示。 她的“记忆碎片”——偶尔失神,喃喃自语,对特定场景反应强烈,甚至出现幻觉,这些会逐渐清晰,暗示她与提瓦特古老文明甚至超越提瓦特的力量有关。 - -3. 丽莎:慵懒、智慧,强大的魔法能力,比原神设定中更成熟、更富有责任感。对星穹铁道科技好奇又警惕,将星河视为竞争对手和值得培养的对象。她会引导主角,但在关键时刻设置考验。她的目标是保护蒙德,培养星河,并寻求突破自身瓶颈。她的隐藏秘密是:她拥有不为人知的秘密身份,与某个神秘组织有关,该组织与即将到来的危机密切相关。她的“禁忌魔法”——偶尔使用威力远超平时能力的魔法,使用后疲惫甚至身体异样,她会刻意隐藏,这与她的神秘组织联系紧密。 - -4. 原神旅行者:与星河互动,产生冲突与合作。两人对提瓦特大陆的理解不同,可能会产生矛盾,但在对抗危机时携手合作。 - -5. 钟离:神秘幕后人物,引导星河完成使命,但目的和动机不明,会带来考验和帮助,也可能成为潜在敌人。他的“真实目的”——对星河的帮助看似无私,但始终保持距离,言行举止充满深意,他的行动与提瓦特危机有关,目的可能并非简单帮助主角,而是为了实现某种更宏大的计划。 - -角色关系与冲突:星河与派蒙(依赖与独立的矛盾),星河与丽莎(竞争与合作),星河与原神旅行者(身份认同冲突),星河与钟离(考验与引导)。 - -暗线与伏笔:派蒙的记忆碎片,丽莎的禁忌魔法,星河的星穹铁道追捕令,钟离的真实目的,提瓦特大陆的失落科技。这些暗线相互交织,推动剧情发展,并为角色成长和故事走向增添可能性和悬念。 - -结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 - - -世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 - 世界观设定:暗影之歌 - - 引言 -在一个名为提瓦特的大陆上,力量如同黑暗中的呼吸,无声无息地渗透着一切。这片大陆的历史被称为“暗影年代”,那时期充满了权力斗争和悲剧命运。如今,随着“暗影力量”的再次苏醒,新的阴谋与挑战正逐渐显露。 - - 主要角色 -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 b755ddd..5764e61 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -1,14 +1,9 @@ -# embedding_ollama.py +import logging import requests from typing import List +import traceback class OllamaEmbeddings: - """ - Ollama 本地服务提供的 Embedding 接口, - 本需求里我们最终拼出形如: http://localhost:11434/api/embed - 即 base_url + "/embed" - """ - def __init__(self, model_name: str, base_url: str): self.model_name = model_name self.base_url = base_url # 这里应形如 http://localhost:11434/api (不再含 /v1) @@ -49,6 +44,28 @@ class OllamaEmbeddings: response = requests.post(url, json=data) response.raise_for_status() result = response.json() + print(result) + + # 检查返回结果是否包含 'embedding' 字段 + if "embedding" not in result: + logging.warning(f"No 'embedding' field in response. Returning empty embedding.") + return [] # 返回空列表 return result["embedding"] + except requests.exceptions.RequestException as e: - raise Exception(f"Ollama embeddings request error: {e}") + logging.error(f"Ollama embeddings request error: {e}") + logging.error(f"Request URL: {url}") + logging.error(f"Request Data: {data}") + logging.error("Full error details:\n" + traceback.format_exc()) + return [] + + except ValueError as e: + logging.error(f"Invalid response structure: {e}") + logging.error(f"Response content: {response.text}") + logging.error("Full error details:\n" + traceback.format_exc()) + return [] + + except Exception as e: + logging.error(f"Unexpected error: {e}") + logging.error("Full error details:\n" + traceback.format_exc()) + return [] diff --git a/novel_generator.py b/novel_generator.py index a9d53ca..71343a2 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -4,17 +4,12 @@ import os import logging import re from typing import Dict, List, Optional -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict - +from typing import TypedDict from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.docstore.document import Document - import nltk import math from sentence_transformers import SentenceTransformer @@ -34,15 +29,15 @@ 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") + 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,6 +55,8 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: return False +# ============ 创建 Embeddings 对象 ============ + def create_embeddings_object( api_key: str, base_url: str, @@ -72,27 +69,25 @@ def create_embeddings_object( - 当 interface_format = "Ollama" => OllamaEmbeddings(...) (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed) - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings - - 其它情况可自行扩展 + - 其它情况视需求可扩展 """ if is_using_ollama_api(interface_format, embed_url): - # 去除末尾斜杠 fixed_url = embed_url.rstrip("/") - # 如果包含 /v1 则替换为 /api fixed_url = fixed_url.replace("/v1", "/api") return OllamaEmbeddings( model_name=embedding_model_name, base_url=fixed_url ) elif is_using_ml_studio_api(interface_format, base_url): + # ML Studio / OpenAI 兼容 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") # ============ 向量库相关 ============ + VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): os.makedirs(VECTOR_STORE_DIR) @@ -102,8 +97,8 @@ def clear_vector_store(): 清空本地向量库(删除 vectorstore 文件夹内的内容)。 """ if os.path.exists(VECTOR_STORE_DIR): + import shutil try: - import shutil for filename in os.listdir(VECTOR_STORE_DIR): file_path = os.path.join(VECTOR_STORE_DIR, filename) if os.path.isfile(file_path) or os.path.islink(file_path): @@ -126,7 +121,6 @@ def init_vector_store( ) -> Chroma: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 - embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。 """ embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -156,6 +150,7 @@ def load_vector_store( 读取已存在的向量库。若不存在则返回 None。 """ if not os.path.exists(VECTOR_STORE_DIR): + logging.info("Vector store not found. Initializing a new one...") return None embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -185,8 +180,10 @@ def update_vector_store( 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...") + logging.info("Vector store does not exist. Initializing a new one for new chapter...") init_vector_store( api_key=api_key, base_url=base_url, @@ -200,6 +197,7 @@ def update_vector_store( new_doc = Document(page_content=new_chapter) store.add_documents([new_doc]) store.persist() + logging.info("Vector store updated with the new chapter.") def get_relevant_context_from_vector_store( api_key: str, @@ -212,7 +210,7 @@ def get_relevant_context_from_vector_store( ) -> str: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 - 若向量库不存在则返回空字符串。 + 若向量库不存在或没有足够的内容,则返回空字符串。 """ store = load_vector_store( api_key=api_key, @@ -221,10 +219,19 @@ def get_relevant_context_from_vector_store( embedding_model_name=embedding_model_name, embedding_base_url=embedding_base_url ) + + # 如果向量库为空,直接返回空字符串 if not store: - logging.warning("Vector store not found. Returning empty context.") + logging.info("No vector store found. Returning empty context.") return "" + + # 向量库存在,但没有足够的内容时也避免索引错误 docs = store.similarity_search(query, k=k) + + if not docs: + logging.info(f"No relevant documents found for query '{query}'. Returning empty context.") + return "" + combined = "\n".join([d.page_content for d in docs]) return combined @@ -256,7 +263,6 @@ def Novel_novel_directory_generate( """ 使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。 """ - # 确保文件夹存在 os.makedirs(filepath, exist_ok=True) model = ChatOpenAI( @@ -327,7 +333,6 @@ def Novel_novel_directory_generate( debug_log(prompt, response.content) return {"novel_directory": response.content.strip()} - # 构建状态图 graph = StateGraph(OverallState) graph.add_node("generate_base_setting", generate_base_setting) graph.add_node("generate_character_setting", generate_character_setting) @@ -363,7 +368,6 @@ def Novel_novel_directory_generate( logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。") return - # 写入文件 filename_set = os.path.join(filepath, "Novel_setting.txt") filename_novel_directory = os.path.join(filepath, "Novel_directory.txt") @@ -375,7 +379,6 @@ def Novel_novel_directory_generate( append_text_to_file(final_novel_setting_cleaned, filename_set) append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) - logging.info("Novel settings and directory generated successfully.") @@ -394,19 +397,25 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int text = read_file(chap_file).strip() if text: texts.append(text) + if len(texts) < n: + texts = [''] * (n - len(texts)) + texts return texts + def summarize_recent_chapters( - llm_model: str, - api_key: str, - base_url: str, - temperature: float, - chapters_text_list: List[str] - ) -> str: + llm_model: str, + api_key: str, + base_url: str, + temperature: float, + chapters_text_list: List[str] +) -> str: """ - 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 - 如果没有可用的模型(model=None),则退化为简单截断示例。 + 将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。 """ + if not chapters_text_list: + return "" + if chapters_text_list==['', '', '']: + return "暂无摘要。" model = ChatOpenAI( model=llm_model, api_key=api_key, @@ -414,33 +423,19 @@ def summarize_recent_chapters( temperature=temperature ) - if not chapters_text_list: - return "" - combined_text = "\n".join(chapters_text_list) - # 如果未传入model,就做个简单的退化输出 - if not model: - return f"【摘要-演示】\n{combined_text[:800]}..." - - # 构造一个提示词(Prompt),指示模型生成精简摘要 - prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容: + prompt = f"""你是一名资深长篇小说写作辅助AI,下面是最近几章的合并文本: {combined_text} -请你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点。 -1.请用中文输出,不超过500字。 -2.仅回复摘要内容,不需要其他信息。 -""" - # 调用模型获取摘要 +请用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点:""" + response = model.invoke(prompt) if not response or not response.content.strip(): - # 若模型无响应或空,返回简单截断 - return f"【摘要-演示】\n{combined_text[:800]}..." - - # 返回模型生成的摘要文本 + return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text return response.content.strip() -# ============ 新增:更新剧情要点/未解决冲突 ============ +# ============ 新增:剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ 下面是新生成的章节内容: @@ -449,9 +444,9 @@ PLOT_ARCS_PROMPT = """\ 这里是已记录的剧情要点/未解决冲突(可能为空): {old_plot_arcs} -请基于新的章节内容,提炼出本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 +请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。 -最终输出一份更新后的剧情要点列表,以帮助后续保持故事的整体一致性和悬念延续。 +最终输出更新后的剧情要点列表,以帮助后续保持故事整体的一致性和悬念延续。 """ def update_plot_arcs( @@ -462,10 +457,6 @@ def update_plot_arcs( model_name: str, temperature: float ) -> str: - """ - 利用模型分析最新章节文本,提炼或更新“未解决冲突或剧情要点”。 - 并返回更新后的字符串。 - """ model = ChatOpenAI( model=model_name, api_key=api_key, @@ -480,7 +471,6 @@ def update_plot_arcs( if not response: logging.warning("update_plot_arcs: No response.") return old_plot_arcs - debug_log(prompt, response.content) return response.content.strip() @@ -499,28 +489,44 @@ def generate_chapter_draft( word_number: int, temperature: float, novel_novel_directory: str, - filepath: str + filepath: str, + interface_format: str, + embedding_model_name: str, + embedding_base_url: str ) -> str: """ - 仅生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 - 并将生成的内容写到 "chapter_{novel_number}.txt" 覆盖写入。 - 同时生成 "outline_{novel_number}.txt" 存储大纲内容。 + 生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 """ - # 0) 根据 novel_number 从 novel_novel_directory 中获取本章标题及简述 + # 根据目录信息获取本章标题、简介 chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number) chapter_title = chapter_info["chapter_title"] chapter_brief = chapter_info["chapter_brief"] - # 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情") - relevant_context = get_relevant_context_from_vector_store( - api_key=api_key, - base_url=base_url, - query="回顾剧情", - interface_format="OpenAI", - embedding_model_name="", - embedding_base_url="", - k=2 - ) + # 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并) + queries = [] + if user_guidance.strip(): + queries.append(user_guidance) + if chapter_brief.strip(): + queries.append(chapter_brief) + # 也可加一句“回顾剧情”之类 + queries.append("回顾剧情") + + relevant_context = "" + for q in queries: + partial_context = get_relevant_context_from_vector_store( + api_key=api_key, + base_url=base_url, + query=q, + interface_format=interface_format, + embedding_model_name=embedding_model_name, + embedding_base_url=embedding_base_url, + k=2 + ) + if partial_context.strip(): + relevant_context += "\n" + partial_context + # 如果检索结果为空,使用默认值(如空字符串) + if not relevant_context: + relevant_context = "暂无相关内容。" model = ChatOpenAI( model=model_name, @@ -529,10 +535,10 @@ def generate_chapter_draft( temperature=temperature ) - # 2) 生成大纲 + # 1) 生成本章大纲 outline_prompt_text = chapter_outline_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, + character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, global_summary=global_summary, novel_number=novel_number, chapter_title=chapter_title, @@ -550,10 +556,10 @@ def generate_chapter_draft( clear_file_content(outline_file) save_string_to_txt(chapter_outline, outline_file) - # 3) 生成正文草稿 + # 2) 生成正文草稿 writing_prompt_text = chapter_write_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, + character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, global_summary=global_summary, chapter_outline=chapter_outline, word_number=word_number, @@ -588,13 +594,12 @@ def finalize_chapter( ): """ 对当前章节进行定稿: - 1. 读取 chapter_{novel_number}.txt 的最终内容; - 2. 更新全局摘要、角色状态文件; - 3. 如果字数明显少于 word_number 的 80%,则自动调用 enrich_chapter_text 再次扩写; - 4. 更新向量库; - 5. 新增:更新剧情要点/未解决冲突 -> plot_arcs.txt + 1. 读取草稿文本 + 2. 若字数太短则再次扩写 + 3. 更新全局摘要、角色状态 + 4. 更新剧情要点 + 5. 更新向量库 """ - # 读取当前章节内容 chapters_dir = os.path.join(filepath, "chapters") chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") chapter_text = read_file(chapter_file).strip() @@ -610,9 +615,9 @@ def finalize_chapter( old_global_summary = read_file(global_summary_file) old_plot_arcs = read_file(plot_arcs_file) - # 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...") + logging.info("Chapter text is shorter than 80% of desired length. Enriching...") chapter_text = enrich_chapter_text( chapter_text=chapter_text, word_number=word_number, @@ -623,9 +628,8 @@ def finalize_chapter( ) clear_file_content(chapter_file) save_string_to_txt(chapter_text, chapter_file) - logging.info("Chapter text has been enriched and updated.") - # 2) 更新全局摘要 + # 更新全局摘要 model = ChatOpenAI( model=model_name, api_key=api_key, @@ -643,7 +647,7 @@ def finalize_chapter( new_global_summary = update_global_summary(chapter_text, old_global_summary) - # 3) 更新角色状态 + # 更新角色状态 def update_character_state(chapter_text: str, old_state: str) -> str: prompt = update_character_state_prompt.format( chapter_text=chapter_text, @@ -654,7 +658,7 @@ def finalize_chapter( new_char_state = update_character_state(chapter_text, old_char_state) - # 4) 更新剧情要点 + # 更新剧情要点 new_plot_arcs = update_plot_arcs( chapter_text=chapter_text, old_plot_arcs=old_plot_arcs, @@ -664,7 +668,7 @@ def finalize_chapter( temperature=temperature ) - # 5) 覆盖写入文件 + # 写回文件 clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) @@ -674,10 +678,10 @@ def finalize_chapter( clear_file_content(plot_arcs_file) save_string_to_txt(new_plot_arcs, plot_arcs_file) - # 6) 更新向量库 + # 更新向量库 update_vector_store( - api_key=api_key, - base_url=base_url, + api_key=api_key, + base_url=base_url, new_chapter=chapter_text, interface_format=interface_format, embedding_model_name=embedding_model_name @@ -695,7 +699,6 @@ def enrich_chapter_text( ) -> str: """ 当章节篇幅不足时,调用此函数对章节文本进行二次扩写。 - 可以让模型补充场景描写、角色心理等,保证与现有文本风格一致。 """ model = ChatOpenAI( model=model_name, @@ -713,20 +716,21 @@ def enrich_chapter_text( return chapter_text return response.content.strip() + # ============ 导入外部知识文本 ============ def import_knowledge_file( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - file_path: str, - embedding_base_url: str = "" - ) -> None: + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + file_path: str, + embedding_base_url: str = "" +) -> None: """ 将用户选定的文本文件导入到向量库,以便在写作时检索。 """ - logging.info(f"开始导入知识库文件: {file_path},当前接口格式: {interface_format},当前模型: {embedding_model_name}") + logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}") if not os.path.exists(file_path): logging.warning(f"知识库文件不存在: {file_path}") return @@ -760,11 +764,12 @@ def advanced_split_content(content: str, similarity_threshold: float = 0.7, max_length: int = 500) -> List[str]: """ - 将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 + 将文本先按句子切分,然后根据语义相似度进行合并,最后按max_length二次切分。 """ - nltk.download('punkt_tab', quiet=True) - sentences = nltk.sent_tokenize(content) + # 纠正下载punkt包:'punkt' 而非 'punkt_tab' + nltk.download('punkt', quiet=True) + sentences = nltk.sent_tokenize(content) if not sentences: return [] diff --git a/ui.py b/ui.py index 6bfc0e8..dd787c5 100644 --- a/ui.py +++ b/ui.py @@ -1,11 +1,12 @@ # ui.py # -*- coding: utf-8 -*- +import logging import os import threading import customtkinter as ctk from tkinter import filedialog, messagebox - +import traceback from config_manager import load_config, save_config from utils import read_file, save_string_to_txt from novel_generator import ( @@ -19,6 +20,12 @@ from novel_generator import ( ) from consistency_checker import check_consistency +def log_error(message: str): + """ + 用于打印详细的错误信息和堆栈信息。 + """ + logging.error(f"{message}\n{traceback.format_exc()}") + # 设置全局主题和颜色 ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") @@ -38,7 +45,6 @@ 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")) @@ -46,7 +52,7 @@ class NovelGeneratorGUI: 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", "") self.genre_var = ctk.StringVar(value=self.loaded_config.get("genre", "玄幻")) @@ -128,8 +134,6 @@ class NovelGeneratorGUI: # 日志:只读 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): @@ -137,8 +141,6 @@ class NovelGeneratorGUI: 右侧用于显示配置与功能按钮。 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 - - 另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。 """ self.right_frame.grid_rowconfigure(0, weight=0) self.right_frame.grid_rowconfigure(1, weight=0) @@ -172,14 +174,13 @@ class NovelGeneratorGUI: - 模型名称 - Temperature """ - # 配置网格 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): + # 如果用户切换接口格式,可根据需要修改BaseURL为默认值 if new_value == "Ollama": self.base_url_var.set("http://localhost:11434/v1") elif new_value == "ML Studio": @@ -263,7 +264,6 @@ class NovelGeneratorGUI: def build_novel_params_area(self, start_row=2): """ 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 - 只使用两列:0列放Label,1列放输入框(可能加子Frame)。 """ # 主题(Topic) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) @@ -291,7 +291,7 @@ class NovelGeneratorGUI: 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") @@ -320,11 +320,8 @@ class NovelGeneratorGUI: """ 主要功能按钮 + 配置保存/加载 按钮 """ - # 调整下布局:0,1列 - # 这里按行摆放多个按钮 self.right_frame.grid_rowconfigure(start_row, 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) @@ -336,7 +333,6 @@ class NovelGeneratorGUI: 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, @@ -389,9 +385,6 @@ class NovelGeneratorGUI: # ------------------ Novel Settings Tab ------------------ def build_setting_tab(self): - """ - 可查看/编辑 Novel_setting.txt 并保存 - """ self.setting_tab.rowconfigure(0, weight=0) self.setting_tab.rowconfigure(1, weight=1) self.setting_tab.columnconfigure(0, weight=1) @@ -430,9 +423,6 @@ class NovelGeneratorGUI: # ------------------ Novel Directory Tab ------------------ def build_directory_tab(self): - """ - 可查看/编辑 Novel_directory.txt 并保存 - """ self.directory_tab.rowconfigure(0, weight=0) self.directory_tab.rowconfigure(1, weight=1) self.directory_tab.columnconfigure(0, weight=1) @@ -471,9 +461,6 @@ class NovelGeneratorGUI: # ------------------ Character State Tab ------------------ def build_character_tab(self): - """ - 查看/编辑 character_state.txt - """ self.character_tab.rowconfigure(0, weight=0) self.character_tab.rowconfigure(1, weight=1) self.character_tab.columnconfigure(0, weight=1) @@ -512,9 +499,6 @@ class NovelGeneratorGUI: # ------------------ Global Summary Tab ------------------ def build_summary_tab(self): - """ - 查看/编辑 global_summary.txt - """ self.summary_tab.rowconfigure(0, weight=0) self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.columnconfigure(0, weight=1) @@ -603,11 +587,9 @@ 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") # ------------------ 功能区 -------------------- @@ -650,6 +632,7 @@ class NovelGeneratorGUI: ) self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") except Exception as e: + logging.error(f"生成小说设定 & 目录时出错: {e}") self.log(f"❌ 生成小说设定 & 目录时出错: {e}") finally: self.enable_button(self.btn_generate_full) @@ -693,10 +676,13 @@ 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( - model_name, # 此处仅示例 - recent_3_texts + llm_model=model_name, + api_key=api_key, + base_url=base_url, + temperature=temperature, + chapters_text_list=recent_3_texts ) self.log(f"开始生成第{chap_num}章草稿...") @@ -713,7 +699,10 @@ class NovelGeneratorGUI: word_number=word_number, temperature=temperature, novel_novel_directory=novel_directory, - filepath=filepath + filepath=filepath, + interface_format=self.interface_format_var.get().strip(), + embedding_model_name=self.embedding_model_name_var.get().strip(), + embedding_base_url=self.embedding_url_var.get().strip() ) if draft_text: self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") @@ -724,6 +713,7 @@ class NovelGeneratorGUI: self.log("⚠️ 本章草稿生成失败或无内容。") except Exception as e: + logging.error(f"生成章节草稿时出错: {e}") self.log(f"❌ 生成章节草稿时出错: {e}") finally: self.enable_button(self.btn_generate_chapter) @@ -741,6 +731,8 @@ class NovelGeneratorGUI: model_name = self.model_name_var.get().strip() temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() + interface_format = self.interface_format_var.get().strip() + embedding_model_name = self.embedding_model_name_var.get().strip() if not filepath: self.log("请先配置保存文件路径。") @@ -755,6 +747,8 @@ class NovelGeneratorGUI: word_number=word_number, api_key=api_key, base_url=base_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name, model_name=model_name, temperature=temperature, filepath=filepath @@ -769,6 +763,7 @@ class NovelGeneratorGUI: self.chapter_result.see("end") except Exception as e: + logging.error(f"定稿章节时出错: {e}") self.log(f"❌ 定稿章节时出错: {e}") finally: self.enable_button(self.btn_finalize_chapter) @@ -801,7 +796,6 @@ class NovelGeneratorGUI: global_summary = read_file(global_summary_file) plot_arcs = read_file(plot_arcs_file) - # 获取当前章节文本 chap_num = self.chapter_num_var.get() chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") chapter_text = read_file(chap_file) @@ -826,6 +820,7 @@ class NovelGeneratorGUI: self.log(result) except Exception as e: + logging.error(f"审校时出错: {e}") self.log(f"❌ 审校时出错: {e}") finally: self.enable_button(self.btn_check_consistency) @@ -848,12 +843,13 @@ class NovelGeneratorGUI: api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), interface_format=self.interface_format_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip(), - file_path=selected_file + embedding_model_name=self.embedding_model_name_var.get().strip(), + file_path=selected_file, + embedding_base_url=self.embedding_url_var.get().strip() ) self.log("✅ 知识库文件导入完成。") except Exception as e: + logging.error(f"导入知识库时出错: {e}") self.log(f"❌ 导入知识库时出错: {e}") finally: self.enable_button(self.btn_import_knowledge) @@ -886,7 +882,6 @@ class NovelGeneratorGUI: if not arcs_text: arcs_text = "当前没有记录的剧情要点或冲突。" - # 弹窗显示 top = ctk.CTkToplevel(self.master) top.title("剧情要点/未解决冲突") top.geometry("600x400") From 9822f3b3ffd841490da894907381fc64506fdf69 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 17:46:29 +0800 Subject: [PATCH 06/11] =?UTF-8?q?Revert=20"=E4=B8=8D=E7=9F=A5=E9=81=93?= =?UTF-8?q?=E5=87=BA=E4=BA=86=E4=BB=80=E4=B9=88=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=85=88=E8=AE=B0=E5=BD=95=E4=B8=80=E4=B8=8B=E6=9B=B4=E6=96=B0?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit aca9e4868339ac93f3d152a3bbb6962eaee28014. --- Src/Novel_directory.txt | 182 ++++++++++++++++++++++++++++++++++ Src/Novel_setting.txt | 98 ++++++++++++++++++ embedding_ollama.py | 33 ++---- novel_generator.py | 215 ++++++++++++++++++++-------------------- ui.py | 71 +++++++------ 5 files changed, 431 insertions(+), 168 deletions(-) create mode 100644 Src/Novel_directory.txt create mode 100644 Src/Novel_setting.txt diff --git a/Src/Novel_directory.txt b/Src/Novel_directory.txt new file mode 100644 index 0000000..3a9cc6f --- /dev/null +++ b/Src/Novel_directory.txt @@ -0,0 +1,182 @@ + +第1章 :星河坠落提瓦特 - 穿越与初遇派蒙,危机初现端倪 +第2章 :蒙德的初印象 - 初探蒙德城,与丽莎的首次相遇 +第3章 :酒馆的闲谈 - 了解提瓦特局势,派蒙展现惊人一面 +第4章 :丽莎的试探 - 星河展现科技实力,引来丽莎的警惕 +第5章 :委托与冒险 - 接受委托,初次展现星穹铁道科技的运用 +第6章 :风魔龙的挑战 - 运用科技对抗风魔龙,展现策略与智慧 +第7章 :派蒙的异常 - 派蒙出现记忆碎片,暗示其不寻常身份 +第8章 :丽莎的秘密魔法 - 丽莎使用禁忌魔法,展现其神秘一面 +第9章 :旅行者的出现 - 与原神旅行者初次相遇,产生摩擦与合作 +第10章 :蒙德的阴谋 - 发现蒙德隐藏的危机,与丽莎深入合作 +第11章 :追捕令的阴影 - 星河收到星穹铁道追捕令,内心挣扎 +第12章 :深入调查 - 调查蒙德的危机,发现与丽莎的秘密组织有关 +第13章 :钟离的现身 - 钟离初次出现,对星河进行暗示与引导 +第14章 :失落科技的线索 - 发现关于提瓦特失落科技的线索 +第15章 :前往璃月 - 踏上前往璃月的旅程,面对新的挑战 +第16章 :璃月港的繁华 - 体验璃月港的文化,了解璃月局势 +第17章 :往生堂的委托 - 接受往生堂的委托,深入了解璃月历史 +第18章 :岩王帝君的考验 - 钟离对星河进行考验,揭示其真实目的 +第19章 :地脉异常 - 发现璃月地脉异常,与旅行者合作调查 +第20章 :黄金屋的秘密 - 探索黄金屋的秘密,发现与失落科技有关 +第21章 :刻晴的质疑 - 刻晴对星河的科技持有怀疑态度 +第22章 :对抗岩龙蜥 - 运用科技与策略对抗强大的岩龙蜥 +第23章 :派蒙的回忆 - 派蒙回忆片段增多,身份逐渐清晰 +第24章 :丽莎的引导 - 丽莎引导星河学习提瓦特魔法 +第25章 :星河的抉择 - 星河面临道德困境,做出艰难选择 +第26章 :前往稻妻 - 前往稻妻,面对新的挑战与危机 +第27章 :稻妻的雷电 - 体验稻妻的文化与习俗,对抗雷电将军的威压 +第28章 :八重神子的试探 - 八重神子对星河进行试探,考验其能力 +第29章 :影向地的秘密 - 探索影向地的秘密,发现与提瓦特历史有关 +第30章 :反抗军与合作 - 与稻妻反抗军合作,对抗幕后黑手 +第31章 :永恒的执政官 - 与雷电将军正面交锋,展现智慧与实力 +第32章 :派蒙的过去 - 派蒙的过去逐渐揭开,与提瓦特古老文明有关 +第33章 :丽莎的组织 - 丽莎所属组织的真相逐渐浮出水面 +第34章 :前往须弥 - 前往须弥,面对新的挑战与文化 +第35章 :须弥的智慧 - 体验须弥的文化,了解须弥的智慧与科技 +第36章 :草神的大智慧 - 与草神进行交流,探讨生命的意义 +第37章 :森林的危机 - 须弥森林面临危机,星河运用科技解决问题 +第38章 :沙漠的秘密 - 探索须弥沙漠的秘密,发现远古文明的遗迹 +第39章 :散兵的阴谋 - 散兵的阴谋逐渐浮出水面,星河需要应对 +第40章 :与散兵的对抗 - 与散兵展开对抗,展现星穹铁道科技的优势 +第41章 :前往枫丹 - 前往枫丹,面对新的挑战与科技 +第42章 :枫丹的审判 - 体验枫丹的法律与秩序,面对复杂的案件 +第43章 :水神的神秘 - 与水神进行互动,了解枫丹的秘密 +第44章 :机械岛的危机 - 枫丹机械岛面临危机,星河运用科技解决问题 +第45章 :潜入深海 - 潜入深海,探索深海的秘密与科技 +第46章 :派蒙的预言 - 派蒙预言即将到来的巨大危机 +第47章 :丽莎的抉择 - 丽莎面临艰难的抉择,需要做出牺牲 +第48章 :前往纳塔 - 前往纳塔,面对新的挑战与文化 +第49章 :纳塔的冰雪 - 体验纳塔的严寒环境,与冰雪元素对抗 +第50章 :雪山之巅的秘密 - 探索雪山之巅的秘密,发现远古文明的遗迹 +第51章 :冰神的考验 - 冰神对星河进行考验,考验其心性与毅力 +第52章 :与旅行者的合作 - 与旅行者深入合作,对抗共同的敌人 +第53章 :深渊的威胁 - 深渊的威胁日益逼近,星河需要寻找应对之策 +第54章 :钟离的计划 - 钟离的计划逐渐显露,其真实目的更加清晰 +第55章 :星河的过去 - 星河过去的阴影再次浮现,影响其与他人的关系 +第56章 :派蒙的觉醒 - 派蒙觉醒,展现其强大的力量 +第57章 :丽莎的牺牲 - 丽莎为了保护星河做出牺牲 +第58章 :前往至冬 - 前往至冬,面对新的挑战与阴谋 +第59章 :至冬的阴谋 - 揭露至冬的阴谋,对抗幕后黑手 +第60章 :女皇的算计 - 女皇对星河进行算计,考验其智慧与勇气 +第61章 :机械生命的危机 - 至冬的机械生命失控,引发巨大的危机 +第62章 :星河的策略 - 星河运用策略,化解机械生命的危机 +第63章 :派蒙的秘密力量 - 派蒙展现其强大的秘密力量 +第64章 :丽莎的遗产 - 丽莎留下宝贵的遗产,帮助星河对抗危机 +第65章 :对抗深渊使徒 - 对抗强大的深渊使徒,展现星穹铁道科技的威力 +第66章 :旅行者的选择 - 旅行者面临艰难的选择,需要做出决定 +第67章 :钟离的帮助 - 钟离给予星河关键的帮助,但其目的依旧不明 +第68章 :星河的成长 - 星河在经历种种磨难后得到成长 +第69章 :提瓦特的命运 - 提瓦特的命运掌握在星河手中 +第70章 :最终决战的序幕 - 最终决战的序幕拉开,危机全面爆发 +第71章 :与幕后黑手的对抗 - 与幕后黑手展开激烈的对抗 +第72章 :星河的牺牲 - 星河为了拯救提瓦特做出牺牲 +第73章 :派蒙的付出 - 派蒙为了帮助星河付出一切 +第74章 :丽莎的回归 - 丽莎以另一种方式回归,继续守护蒙德 +第75章 :旅行者的抉择 - 旅行者做出最终的抉择 +第76章 :钟离的真相 - 钟离的真实目的最终揭晓 +第77章 :星河的复活 - 星河以意想不到的方式复活 +第78章 :新的危机 - 新的危机出现,提瓦特面临新的挑战 +第79章 :星河的抉择 - 星河需要做出新的抉择,选择自己的未来 +第80章 :派蒙的未来 - 派蒙的未来充满希望与未知 +第81章 :丽莎的守护 - 丽莎继续守护蒙德,为提瓦特贡献力量 +第82章 :旅行者的旅程 - 旅行者继续自己的旅程,探索提瓦特的奥秘 +第83章 :钟离的隐退 - 钟离选择隐退,默默守护提瓦特 +第84章 :星河的归宿 - 星河思考自己的归宿,是留下还是离开 +第85章 :与伙伴的告别 - 与伙伴们告别,回忆共同经历的冒险 +第86章 :返回星穹铁道? - 星河考虑返回星穹铁道,但心中仍有牵挂 +第87章 :提瓦特的未来 - 提瓦特的未来充满希望与挑战 +第88章 :科技与魔法的融合 - 星穹铁道科技与提瓦特魔法开始融合 +第89章 :新的时代 - 提瓦特进入新的时代,科技与魔法共存 +第90章 :星河的遗产 - 星河留下宝贵的遗产,影响着提瓦特的未来 +第91章 :派蒙的传承 - 派蒙将自己的智慧与经验传承下去 +第92章 :丽莎的教导 - 丽莎继续教导年轻一代,传承魔法的艺术 +第93章 :旅行者的故事 - 旅行者的故事继续,探索更多未知的领域 +第94章 :钟离的箴言 - 钟离留下意味深长的箴言,引导提瓦特的未来 +第95章 :星河的回归 - 星河最终做出决定,选择自己的归宿 +第96章 :告别的时刻 - 与提瓦特的伙伴们告别,心中充满不舍 +第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 +第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 +第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 +第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 new file mode 100644 index 0000000..340c5cc --- /dev/null +++ b/Src/Novel_setting.txt @@ -0,0 +1,98 @@ + +小说设定:《星河挽歌:提瓦特星轨》 + +小说类型:玄幻穿越,史诗级冒险,轻松幽默与细腻情感并存,整体偏正剧,但避免过于沉重。 + +字数:约400万字(100章 x 4000字/章) + +视角:第三人称,穿插主角第一人称内心独白。 + +世界观:基于原神世界线平行宇宙的提瓦特大陆,地理环境与原神基本一致,但部分区域势力分布及历史事件有所调整,以适应主角的到来和剧情发展。七国结构保留,但政治格局和社会风貌因主角介入而变。科技与魔法并存,魔法更为显著,主角的星穹铁道科技作为“外来力量”融入其中,引发冲突和变革。重要历史事件保留原神设定,并添加主角引发的新的历史事件。 + +核心内容梗概:主角星河在星穹铁道执行任务时意外穿越到提瓦特大陆,发现大陆面临史无前例的巨大危机。凭借星穹铁道科技力量及经验,星河成为对抗危机的关键人物。在拯救提瓦特过程中,他将与原神角色相遇,建立深厚情谊或产生冲突,最终完成使命,并决定自己的归宿。采用英雄之旅叙事结构,主线为拯救提瓦特,副线为主角与原神角色的情感纠葛。 + +主要角色: + +1. 星河:坚毅、责任感强、略带孤傲,内心温柔渴望归宿。拥有强大的星穹铁道科技力量,但更倾向于智慧和策略解决问题。拥有丰富战斗经验和冷静判断力,但也有过去任务的阴影和对未来命运的迷茫,偶尔冲动,也可能因自身能力产生优越感。他的目标是拯救提瓦特,找到归宿,弥补过去遗憾。他的隐藏秘密是:在星穹铁道的一次任务中,他导致一位重要伙伴牺牲,这个秘密影响着他与他人的关系,并在与原神角色建立深厚感情时成为关键转折点。 + +2. 派蒙:活泼、贪吃、略显成熟,更能理解星河内心世界,给予更深层次的支持。积极参与冒险,但鲁莽会带来麻烦,是重要的喜剧元素。对提瓦特知识渊博,但对星穹铁道科技一无所知。她的目标是帮助星河拯救提瓦特,探索自身存在秘密,与星河建立更深厚羁绊。她的隐藏秘密是:她并非简单的导航精灵,拥有远超常人的智慧和感知能力,与提瓦特大陆的过去有着神秘联系。这会在中段通过关键事件逐步揭示。 她的“记忆碎片”——偶尔失神,喃喃自语,对特定场景反应强烈,甚至出现幻觉,这些会逐渐清晰,暗示她与提瓦特古老文明甚至超越提瓦特的力量有关。 + +3. 丽莎:慵懒、智慧,强大的魔法能力,比原神设定中更成熟、更富有责任感。对星穹铁道科技好奇又警惕,将星河视为竞争对手和值得培养的对象。她会引导主角,但在关键时刻设置考验。她的目标是保护蒙德,培养星河,并寻求突破自身瓶颈。她的隐藏秘密是:她拥有不为人知的秘密身份,与某个神秘组织有关,该组织与即将到来的危机密切相关。她的“禁忌魔法”——偶尔使用威力远超平时能力的魔法,使用后疲惫甚至身体异样,她会刻意隐藏,这与她的神秘组织联系紧密。 + +4. 原神旅行者:与星河互动,产生冲突与合作。两人对提瓦特大陆的理解不同,可能会产生矛盾,但在对抗危机时携手合作。 + +5. 钟离:神秘幕后人物,引导星河完成使命,但目的和动机不明,会带来考验和帮助,也可能成为潜在敌人。他的“真实目的”——对星河的帮助看似无私,但始终保持距离,言行举止充满深意,他的行动与提瓦特危机有关,目的可能并非简单帮助主角,而是为了实现某种更宏大的计划。 + +角色关系与冲突:星河与派蒙(依赖与独立的矛盾),星河与丽莎(竞争与合作),星河与原神旅行者(身份认同冲突),星河与钟离(考验与引导)。 + +暗线与伏笔:派蒙的记忆碎片,丽莎的禁忌魔法,星河的星穹铁道追捕令,钟离的真实目的,提瓦特大陆的失落科技。这些暗线相互交织,推动剧情发展,并为角色成长和故事走向增添可能性和悬念。 + +结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 + + +世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 + 世界观设定:暗影之歌 + + 引言 +在一个名为提瓦特的大陆上,力量如同黑暗中的呼吸,无声无息地渗透着一切。这片大陆的历史被称为“暗影年代”,那时期充满了权力斗争和悲剧命运。如今,随着“暗影力量”的再次苏醒,新的阴谋与挑战正逐渐显露。 + + 主要角色 +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 5764e61..b755ddd 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -1,9 +1,14 @@ -import logging +# embedding_ollama.py import requests from typing import List -import traceback class OllamaEmbeddings: + """ + Ollama 本地服务提供的 Embedding 接口, + 本需求里我们最终拼出形如: http://localhost:11434/api/embed + 即 base_url + "/embed" + """ + def __init__(self, model_name: str, base_url: str): self.model_name = model_name self.base_url = base_url # 这里应形如 http://localhost:11434/api (不再含 /v1) @@ -44,28 +49,6 @@ class OllamaEmbeddings: response = requests.post(url, json=data) response.raise_for_status() result = response.json() - print(result) - - # 检查返回结果是否包含 'embedding' 字段 - if "embedding" not in result: - logging.warning(f"No 'embedding' field in response. Returning empty embedding.") - return [] # 返回空列表 return result["embedding"] - except requests.exceptions.RequestException as e: - logging.error(f"Ollama embeddings request error: {e}") - logging.error(f"Request URL: {url}") - logging.error(f"Request Data: {data}") - logging.error("Full error details:\n" + traceback.format_exc()) - return [] - - except ValueError as e: - logging.error(f"Invalid response structure: {e}") - logging.error(f"Response content: {response.text}") - logging.error("Full error details:\n" + traceback.format_exc()) - return [] - - except Exception as e: - logging.error(f"Unexpected error: {e}") - logging.error("Full error details:\n" + traceback.format_exc()) - return [] + raise Exception(f"Ollama embeddings request error: {e}") diff --git a/novel_generator.py b/novel_generator.py index 71343a2..a9d53ca 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -4,12 +4,17 @@ import os import logging import re from typing import Dict, List, Optional -from typing import TypedDict +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict + from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.docstore.document import Document + import nltk import math from sentence_transformers import SentenceTransformer @@ -29,15 +34,15 @@ 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): - logging.info(f"\n[Prompt >>>] {prompt}\n") - logging.info(f"[Response >>>] {response_content}\n") + """打印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 @@ -55,8 +60,6 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: return False -# ============ 创建 Embeddings 对象 ============ - def create_embeddings_object( api_key: str, base_url: str, @@ -69,25 +72,27 @@ def create_embeddings_object( - 当 interface_format = "Ollama" => OllamaEmbeddings(...) (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed) - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings - - 其它情况视需求可扩展 + - 其它情况可自行扩展 """ if is_using_ollama_api(interface_format, embed_url): + # 去除末尾斜杠 fixed_url = embed_url.rstrip("/") + # 如果包含 /v1 则替换为 /api fixed_url = fixed_url.replace("/v1", "/api") return OllamaEmbeddings( model_name=embedding_model_name, base_url=fixed_url ) elif is_using_ml_studio_api(interface_format, base_url): - # ML Studio / OpenAI 兼容 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") # ============ 向量库相关 ============ - VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): os.makedirs(VECTOR_STORE_DIR) @@ -97,8 +102,8 @@ def clear_vector_store(): 清空本地向量库(删除 vectorstore 文件夹内的内容)。 """ if os.path.exists(VECTOR_STORE_DIR): - import shutil try: + import shutil for filename in os.listdir(VECTOR_STORE_DIR): file_path = os.path.join(VECTOR_STORE_DIR, filename) if os.path.isfile(file_path) or os.path.islink(file_path): @@ -121,6 +126,7 @@ def init_vector_store( ) -> Chroma: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 + embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。 """ embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -150,7 +156,6 @@ def load_vector_store( 读取已存在的向量库。若不存在则返回 None。 """ if not os.path.exists(VECTOR_STORE_DIR): - logging.info("Vector store not found. Initializing a new one...") return None embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -180,10 +185,8 @@ def update_vector_store( 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 for new chapter...") + logging.info("Vector store does not exist. Initializing a new one...") init_vector_store( api_key=api_key, base_url=base_url, @@ -197,7 +200,6 @@ def update_vector_store( new_doc = Document(page_content=new_chapter) store.add_documents([new_doc]) store.persist() - logging.info("Vector store updated with the new chapter.") def get_relevant_context_from_vector_store( api_key: str, @@ -210,7 +212,7 @@ def get_relevant_context_from_vector_store( ) -> str: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 - 若向量库不存在或没有足够的内容,则返回空字符串。 + 若向量库不存在则返回空字符串。 """ store = load_vector_store( api_key=api_key, @@ -219,19 +221,10 @@ def get_relevant_context_from_vector_store( embedding_model_name=embedding_model_name, embedding_base_url=embedding_base_url ) - - # 如果向量库为空,直接返回空字符串 if not store: - logging.info("No vector store found. Returning empty context.") + logging.warning("Vector store not found. Returning empty context.") return "" - - # 向量库存在,但没有足够的内容时也避免索引错误 docs = store.similarity_search(query, k=k) - - if not docs: - logging.info(f"No relevant documents found for query '{query}'. Returning empty context.") - return "" - combined = "\n".join([d.page_content for d in docs]) return combined @@ -263,6 +256,7 @@ def Novel_novel_directory_generate( """ 使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。 """ + # 确保文件夹存在 os.makedirs(filepath, exist_ok=True) model = ChatOpenAI( @@ -333,6 +327,7 @@ def Novel_novel_directory_generate( debug_log(prompt, response.content) return {"novel_directory": response.content.strip()} + # 构建状态图 graph = StateGraph(OverallState) graph.add_node("generate_base_setting", generate_base_setting) graph.add_node("generate_character_setting", generate_character_setting) @@ -368,6 +363,7 @@ def Novel_novel_directory_generate( logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。") return + # 写入文件 filename_set = os.path.join(filepath, "Novel_setting.txt") filename_novel_directory = os.path.join(filepath, "Novel_directory.txt") @@ -379,6 +375,7 @@ def Novel_novel_directory_generate( append_text_to_file(final_novel_setting_cleaned, filename_set) append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) + logging.info("Novel settings and directory generated successfully.") @@ -397,25 +394,19 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int text = read_file(chap_file).strip() if text: texts.append(text) - if len(texts) < n: - texts = [''] * (n - len(texts)) + texts return texts - def summarize_recent_chapters( - llm_model: str, - api_key: str, - base_url: str, - temperature: float, - chapters_text_list: List[str] -) -> str: + llm_model: str, + api_key: str, + base_url: str, + temperature: float, + chapters_text_list: List[str] + ) -> str: """ - 将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。 + 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 + 如果没有可用的模型(model=None),则退化为简单截断示例。 """ - if not chapters_text_list: - return "" - if chapters_text_list==['', '', '']: - return "暂无摘要。" model = ChatOpenAI( model=llm_model, api_key=api_key, @@ -423,19 +414,33 @@ def summarize_recent_chapters( temperature=temperature ) + if not chapters_text_list: + return "" + combined_text = "\n".join(chapters_text_list) - prompt = f"""你是一名资深长篇小说写作辅助AI,下面是最近几章的合并文本: + # 如果未传入model,就做个简单的退化输出 + if not model: + return f"【摘要-演示】\n{combined_text[:800]}..." + + # 构造一个提示词(Prompt),指示模型生成精简摘要 + prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容: {combined_text} -请用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点:""" - +请你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点。 +1.请用中文输出,不超过500字。 +2.仅回复摘要内容,不需要其他信息。 +""" + # 调用模型获取摘要 response = model.invoke(prompt) if not response or not response.content.strip(): - return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text + # 若模型无响应或空,返回简单截断 + return f"【摘要-演示】\n{combined_text[:800]}..." + + # 返回模型生成的摘要文本 return response.content.strip() -# ============ 新增:剧情要点/未解决冲突 ============ +# ============ 新增:更新剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ 下面是新生成的章节内容: @@ -444,9 +449,9 @@ PLOT_ARCS_PROMPT = """\ 这里是已记录的剧情要点/未解决冲突(可能为空): {old_plot_arcs} -请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 +请基于新的章节内容,提炼出本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。 -最终输出更新后的剧情要点列表,以帮助后续保持故事整体的一致性和悬念延续。 +最终输出一份更新后的剧情要点列表,以帮助后续保持故事的整体一致性和悬念延续。 """ def update_plot_arcs( @@ -457,6 +462,10 @@ def update_plot_arcs( model_name: str, temperature: float ) -> str: + """ + 利用模型分析最新章节文本,提炼或更新“未解决冲突或剧情要点”。 + 并返回更新后的字符串。 + """ model = ChatOpenAI( model=model_name, api_key=api_key, @@ -471,6 +480,7 @@ def update_plot_arcs( if not response: logging.warning("update_plot_arcs: No response.") return old_plot_arcs + debug_log(prompt, response.content) return response.content.strip() @@ -489,44 +499,28 @@ def generate_chapter_draft( word_number: int, temperature: float, novel_novel_directory: str, - filepath: str, - interface_format: str, - embedding_model_name: str, - embedding_base_url: str + filepath: str ) -> str: """ - 生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 + 仅生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 + 并将生成的内容写到 "chapter_{novel_number}.txt" 覆盖写入。 + 同时生成 "outline_{novel_number}.txt" 存储大纲内容。 """ - # 根据目录信息获取本章标题、简介 + # 0) 根据 novel_number 从 novel_novel_directory 中获取本章标题及简述 chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number) chapter_title = chapter_info["chapter_title"] chapter_brief = chapter_info["chapter_brief"] - # 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并) - queries = [] - if user_guidance.strip(): - queries.append(user_guidance) - if chapter_brief.strip(): - queries.append(chapter_brief) - # 也可加一句“回顾剧情”之类 - queries.append("回顾剧情") - - relevant_context = "" - for q in queries: - partial_context = get_relevant_context_from_vector_store( - api_key=api_key, - base_url=base_url, - query=q, - interface_format=interface_format, - embedding_model_name=embedding_model_name, - embedding_base_url=embedding_base_url, - k=2 - ) - if partial_context.strip(): - relevant_context += "\n" + partial_context - # 如果检索结果为空,使用默认值(如空字符串) - if not relevant_context: - relevant_context = "暂无相关内容。" + # 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情") + relevant_context = get_relevant_context_from_vector_store( + api_key=api_key, + base_url=base_url, + query="回顾剧情", + interface_format="OpenAI", + embedding_model_name="", + embedding_base_url="", + k=2 + ) model = ChatOpenAI( model=model_name, @@ -535,10 +529,10 @@ def generate_chapter_draft( temperature=temperature ) - # 1) 生成本章大纲 + # 2) 生成大纲 outline_prompt_text = chapter_outline_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, + character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, global_summary=global_summary, novel_number=novel_number, chapter_title=chapter_title, @@ -556,10 +550,10 @@ def generate_chapter_draft( clear_file_content(outline_file) save_string_to_txt(chapter_outline, outline_file) - # 2) 生成正文草稿 + # 3) 生成正文草稿 writing_prompt_text = chapter_write_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, + character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, global_summary=global_summary, chapter_outline=chapter_outline, word_number=word_number, @@ -594,12 +588,13 @@ def finalize_chapter( ): """ 对当前章节进行定稿: - 1. 读取草稿文本 - 2. 若字数太短则再次扩写 - 3. 更新全局摘要、角色状态 - 4. 更新剧情要点 - 5. 更新向量库 + 1. 读取 chapter_{novel_number}.txt 的最终内容; + 2. 更新全局摘要、角色状态文件; + 3. 如果字数明显少于 word_number 的 80%,则自动调用 enrich_chapter_text 再次扩写; + 4. 更新向量库; + 5. 新增:更新剧情要点/未解决冲突 -> plot_arcs.txt """ + # 读取当前章节内容 chapters_dir = os.path.join(filepath, "chapters") chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") chapter_text = read_file(chapter_file).strip() @@ -615,9 +610,9 @@ def finalize_chapter( old_global_summary = read_file(global_summary_file) old_plot_arcs = read_file(plot_arcs_file) - # 若篇幅过短,二次扩写 + # 1) 若字数明显不足,做 enrich if len(chapter_text) < 0.8 * word_number: - logging.info("Chapter text is shorter than 80% of desired length. Enriching...") + logging.info("Chapter text seems shorter than 80% of desired length. Attempting to enrich content...") chapter_text = enrich_chapter_text( chapter_text=chapter_text, word_number=word_number, @@ -628,8 +623,9 @@ def finalize_chapter( ) clear_file_content(chapter_file) save_string_to_txt(chapter_text, chapter_file) + logging.info("Chapter text has been enriched and updated.") - # 更新全局摘要 + # 2) 更新全局摘要 model = ChatOpenAI( model=model_name, api_key=api_key, @@ -647,7 +643,7 @@ def finalize_chapter( new_global_summary = update_global_summary(chapter_text, old_global_summary) - # 更新角色状态 + # 3) 更新角色状态 def update_character_state(chapter_text: str, old_state: str) -> str: prompt = update_character_state_prompt.format( chapter_text=chapter_text, @@ -658,7 +654,7 @@ def finalize_chapter( new_char_state = update_character_state(chapter_text, old_char_state) - # 更新剧情要点 + # 4) 更新剧情要点 new_plot_arcs = update_plot_arcs( chapter_text=chapter_text, old_plot_arcs=old_plot_arcs, @@ -668,7 +664,7 @@ def finalize_chapter( temperature=temperature ) - # 写回文件 + # 5) 覆盖写入文件 clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) @@ -678,10 +674,10 @@ def finalize_chapter( clear_file_content(plot_arcs_file) save_string_to_txt(new_plot_arcs, plot_arcs_file) - # 更新向量库 + # 6) 更新向量库 update_vector_store( - api_key=api_key, - base_url=base_url, + api_key=api_key, + base_url=base_url, new_chapter=chapter_text, interface_format=interface_format, embedding_model_name=embedding_model_name @@ -699,6 +695,7 @@ def enrich_chapter_text( ) -> str: """ 当章节篇幅不足时,调用此函数对章节文本进行二次扩写。 + 可以让模型补充场景描写、角色心理等,保证与现有文本风格一致。 """ model = ChatOpenAI( model=model_name, @@ -716,21 +713,20 @@ def enrich_chapter_text( return chapter_text return response.content.strip() - # ============ 导入外部知识文本 ============ def import_knowledge_file( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - file_path: str, - embedding_base_url: str = "" -) -> None: + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + file_path: str, + embedding_base_url: str = "" + ) -> None: """ 将用户选定的文本文件导入到向量库,以便在写作时检索。 """ - logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}") + logging.info(f"开始导入知识库文件: {file_path},当前接口格式: {interface_format},当前模型: {embedding_model_name}") if not os.path.exists(file_path): logging.warning(f"知识库文件不存在: {file_path}") return @@ -764,12 +760,11 @@ def advanced_split_content(content: str, similarity_threshold: float = 0.7, max_length: int = 500) -> List[str]: """ - 将文本先按句子切分,然后根据语义相似度进行合并,最后按max_length二次切分。 + 将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 """ - # 纠正下载punkt包:'punkt' 而非 'punkt_tab' - nltk.download('punkt', quiet=True) - + nltk.download('punkt_tab', quiet=True) sentences = nltk.sent_tokenize(content) + if not sentences: return [] diff --git a/ui.py b/ui.py index dd787c5..6bfc0e8 100644 --- a/ui.py +++ b/ui.py @@ -1,12 +1,11 @@ # ui.py # -*- coding: utf-8 -*- -import logging import os import threading import customtkinter as ctk from tkinter import filedialog, messagebox -import traceback + from config_manager import load_config, save_config from utils import read_file, save_string_to_txt from novel_generator import ( @@ -20,12 +19,6 @@ from novel_generator import ( ) from consistency_checker import check_consistency -def log_error(message: str): - """ - 用于打印详细的错误信息和堆栈信息。 - """ - logging.error(f"{message}\n{traceback.format_exc()}") - # 设置全局主题和颜色 ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") @@ -45,6 +38,7 @@ 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")) @@ -52,7 +46,7 @@ class NovelGeneratorGUI: 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", "") self.genre_var = ctk.StringVar(value=self.loaded_config.get("genre", "玄幻")) @@ -134,6 +128,8 @@ class NovelGeneratorGUI: # 日志:只读 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): @@ -141,6 +137,8 @@ class NovelGeneratorGUI: 右侧用于显示配置与功能按钮。 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 + + 另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。 """ self.right_frame.grid_rowconfigure(0, weight=0) self.right_frame.grid_rowconfigure(1, weight=0) @@ -174,13 +172,14 @@ class NovelGeneratorGUI: - 模型名称 - Temperature """ + # 配置网格 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): - # 如果用户切换接口格式,可根据需要修改BaseURL为默认值 if new_value == "Ollama": self.base_url_var.set("http://localhost:11434/v1") elif new_value == "ML Studio": @@ -264,6 +263,7 @@ class NovelGeneratorGUI: def build_novel_params_area(self, start_row=2): """ 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 + 只使用两列:0列放Label,1列放输入框(可能加子Frame)。 """ # 主题(Topic) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) @@ -291,7 +291,7 @@ class NovelGeneratorGUI: 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") @@ -320,8 +320,11 @@ class NovelGeneratorGUI: """ 主要功能按钮 + 配置保存/加载 按钮 """ + # 调整下布局:0,1列 + # 这里按行摆放多个按钮 self.right_frame.grid_rowconfigure(start_row, 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) @@ -333,6 +336,7 @@ class NovelGeneratorGUI: 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, @@ -385,6 +389,9 @@ class NovelGeneratorGUI: # ------------------ Novel Settings Tab ------------------ def build_setting_tab(self): + """ + 可查看/编辑 Novel_setting.txt 并保存 + """ self.setting_tab.rowconfigure(0, weight=0) self.setting_tab.rowconfigure(1, weight=1) self.setting_tab.columnconfigure(0, weight=1) @@ -423,6 +430,9 @@ class NovelGeneratorGUI: # ------------------ Novel Directory Tab ------------------ def build_directory_tab(self): + """ + 可查看/编辑 Novel_directory.txt 并保存 + """ self.directory_tab.rowconfigure(0, weight=0) self.directory_tab.rowconfigure(1, weight=1) self.directory_tab.columnconfigure(0, weight=1) @@ -461,6 +471,9 @@ class NovelGeneratorGUI: # ------------------ Character State Tab ------------------ def build_character_tab(self): + """ + 查看/编辑 character_state.txt + """ self.character_tab.rowconfigure(0, weight=0) self.character_tab.rowconfigure(1, weight=1) self.character_tab.columnconfigure(0, weight=1) @@ -499,6 +512,9 @@ class NovelGeneratorGUI: # ------------------ Global Summary Tab ------------------ def build_summary_tab(self): + """ + 查看/编辑 global_summary.txt + """ self.summary_tab.rowconfigure(0, weight=0) self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.columnconfigure(0, weight=1) @@ -587,9 +603,11 @@ 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") # ------------------ 功能区 -------------------- @@ -632,7 +650,6 @@ class NovelGeneratorGUI: ) self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") except Exception as e: - logging.error(f"生成小说设定 & 目录时出错: {e}") self.log(f"❌ 生成小说设定 & 目录时出错: {e}") finally: self.enable_button(self.btn_generate_full) @@ -676,13 +693,10 @@ 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( - llm_model=model_name, - api_key=api_key, - base_url=base_url, - temperature=temperature, - chapters_text_list=recent_3_texts + model_name, # 此处仅示例 + recent_3_texts ) self.log(f"开始生成第{chap_num}章草稿...") @@ -699,10 +713,7 @@ class NovelGeneratorGUI: word_number=word_number, temperature=temperature, novel_novel_directory=novel_directory, - filepath=filepath, - interface_format=self.interface_format_var.get().strip(), - embedding_model_name=self.embedding_model_name_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip() + filepath=filepath ) if draft_text: self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") @@ -713,7 +724,6 @@ class NovelGeneratorGUI: self.log("⚠️ 本章草稿生成失败或无内容。") except Exception as e: - logging.error(f"生成章节草稿时出错: {e}") self.log(f"❌ 生成章节草稿时出错: {e}") finally: self.enable_button(self.btn_generate_chapter) @@ -731,8 +741,6 @@ class NovelGeneratorGUI: model_name = self.model_name_var.get().strip() temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() - interface_format = self.interface_format_var.get().strip() - embedding_model_name = self.embedding_model_name_var.get().strip() if not filepath: self.log("请先配置保存文件路径。") @@ -747,8 +755,6 @@ class NovelGeneratorGUI: word_number=word_number, api_key=api_key, base_url=base_url, - interface_format=interface_format, - embedding_model_name=embedding_model_name, model_name=model_name, temperature=temperature, filepath=filepath @@ -763,7 +769,6 @@ class NovelGeneratorGUI: self.chapter_result.see("end") except Exception as e: - logging.error(f"定稿章节时出错: {e}") self.log(f"❌ 定稿章节时出错: {e}") finally: self.enable_button(self.btn_finalize_chapter) @@ -796,6 +801,7 @@ class NovelGeneratorGUI: global_summary = read_file(global_summary_file) plot_arcs = read_file(plot_arcs_file) + # 获取当前章节文本 chap_num = self.chapter_num_var.get() chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") chapter_text = read_file(chap_file) @@ -820,7 +826,6 @@ class NovelGeneratorGUI: self.log(result) except Exception as e: - logging.error(f"审校时出错: {e}") self.log(f"❌ 审校时出错: {e}") finally: self.enable_button(self.btn_check_consistency) @@ -843,13 +848,12 @@ class NovelGeneratorGUI: api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), interface_format=self.interface_format_var.get().strip(), - embedding_model_name=self.embedding_model_name_var.get().strip(), - file_path=selected_file, - embedding_base_url=self.embedding_url_var.get().strip() + embedding_base_url=self.embedding_url_var.get().strip(), + embedding_base_url=self.embedding_url_var.get().strip(), + file_path=selected_file ) self.log("✅ 知识库文件导入完成。") except Exception as e: - logging.error(f"导入知识库时出错: {e}") self.log(f"❌ 导入知识库时出错: {e}") finally: self.enable_button(self.btn_import_knowledge) @@ -882,6 +886,7 @@ class NovelGeneratorGUI: if not arcs_text: arcs_text = "当前没有记录的剧情要点或冲突。" + # 弹窗显示 top = ctk.CTkToplevel(self.master) top.title("剧情要点/未解决冲突") top.geometry("600x400") From 231575c2da178de7de9ec222d27a33ddf6f3dc6e Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 18:25:35 +0800 Subject: [PATCH 07/11] 111 --- Src/outlines/outline_1.txt | 43 ++++++++ novel_generator.py | 215 +++++++++++++++++++------------------ ui.py | 71 ++++++------ 3 files changed, 186 insertions(+), 143 deletions(-) create mode 100644 Src/outlines/outline_1.txt diff --git a/Src/outlines/outline_1.txt b/Src/outlines/outline_1.txt new file mode 100644 index 0000000..1cd6cc6 --- /dev/null +++ b/Src/outlines/outline_1.txt @@ -0,0 +1,43 @@ + +好,我现在需要帮用户构思小说的第1章大纲,标题是“星河坠落提瓦特”,简述是“穿越与初遇派蒙,危机初现端倪”。首先,我得理解这个标题和简述的意思。看起来这是一部含有玄幻或奇幻元素的小说,可能涉及时间旅行、门派纷争或者某种隐藏的危机。 + +用户给出的内容里提到要围绕本章标题和简述设计详细大纲,并且用户已经提供了一个示例的结构。所以我需要按照那个格式来写,而不是用数字分点。也就是说,先描述每个部分,然后是主要冲突、出现的角色,他们的动机,暗线推进,以及结尾的悬念。 + +首先,本章标题“星河坠落提瓦特”,听起来像是主角从一个繁荣的地方坠落到另一个世界。简述说的是穿越和初遇派蒙,同时危机开始显现。这可能意味着主角从一个平行世界穿越到这个世界,并在门派之间初次相遇,引发了一些危机。 + +接下来,我需要构思主要冲突。可能是主角到达提瓦特城,发现这里有一个古老的力量或秘密,门派之间开始竞争,而主角作为穿越者,带来了改变,同时也面临着被追杀的风险。这样既呼应了标题,又体现了简述中的危机初现。 + +然后是角色部分。可能需要包括主角、对方的门派成员,比如派蒙,以及提瓦特城里的其他关键人物。每个角色在这章里有什么目标和动机,比如追杀主角的人,或者试图掌控力量的人。 + +接下来是推动暗线和悬念的部分。比如,提瓦特城的秘密可能涉及到星河的力量,而派蒙门派对这个力量的争夺可能引发更大的冲突。同时,主角可能带有未知的任务或使命,这会影响后续章节。 + +最后是结尾的悬念,可能是一个转折点,比如主角揭示了某个秘密,或者派蒙开始怀疑自己,而星河的力量在台面上或下面开始显现,为后续剧情埋下伏笔。 + +整体来看,我需要确保大纲紧扣标题和简述,同时为后续章节留有余地。可能还要注意角色的性格发展和潜在冲突,以保持故事的吸引力。 + + +### 第 1 章:星河坠落提瓦特 + +#### 主要冲突: +- **星河降临**:穿越者从一个繁荣世纪跨越到提瓦特大陆,带来了“星河”般的强大力量,引发了提瓦特城内外的关注与恐慌。 +- **门派初遇**:派蒙门派首领派长派出追杀队伍,试图抓住这股未知力量,以掌控“星河之力”。 +- **危机初现**:一场隐藏已久的秘密势力开始苏醒,在提瓦特城内外的阴谋逐渐浮现,为后续的灾难埋下伏笔。 + +#### 角色出现: +1. **主角(未命名)**:穿越者,带着星河般的强大力量,性格冷静果敢,却对自己的来历一无所知。 +2. **派长**:派蒙门派领袖,精通古老法术,眼光独到,对“星河之力”嗜求不已。 +3. **追杀队伍**:派长派出的十名绝技高手,打着门派的旗号,在提瓦特城内外肆虐。 +4. **提瓦特市民**:生活在平静世代的普通百姓,见证并参与这场突如其来的震撼。 +5. **神秘老者**:提瓦特城中的隐世高人,对“星河之力”知之甚详,暗中观察与等待。 + +#### 推动暗线: +- **星河力量的真相**:主角身上蕴含的力量竟是千年前就被预言的“星河之力”,而提瓦特城内外的秘密势力对此虎视眈眈。 +- **派蒙门派的野心**:派长不仅想要获取“星河之力”,还暗藏着更大的图谋,试图借此重塑整个世界。 +- **神秘老者的警告**:一位隐世高人试图接触主角,给出“星河之力”与世界危机的隐喻,为后续剧情埋下重大伏笔。 + +#### 结尾悬念: +- **力量暴露**:在一次追杀中,主角无意间释放出“星河之力”,震撼整个提瓦特城,引发更大的关注与恐慌。 +- **派长的挣扎**:派长对自己的能力和未来感到迷茫,开始怀疑自己是否已经被力量的猎头所误导。 +- **神秘老者的突然消失**:一位神秘老者在关键时刻悄然离去,为后续追杀埋下更多谜团。 + +通过这场开篇的震撼,未命名的主角将从一个普通穿越者逐渐成长为“星河”的继承者,而提瓦特城与整个世界也将迎来一场前所未有的巨变。 \ No newline at end of file diff --git a/novel_generator.py b/novel_generator.py index a9d53ca..71343a2 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -4,17 +4,12 @@ import os import logging import re from typing import Dict, List, Optional -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict - +from typing import TypedDict from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.docstore.document import Document - import nltk import math from sentence_transformers import SentenceTransformer @@ -34,15 +29,15 @@ 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") + 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,6 +55,8 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: return False +# ============ 创建 Embeddings 对象 ============ + def create_embeddings_object( api_key: str, base_url: str, @@ -72,27 +69,25 @@ def create_embeddings_object( - 当 interface_format = "Ollama" => OllamaEmbeddings(...) (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed) - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings - - 其它情况可自行扩展 + - 其它情况视需求可扩展 """ if is_using_ollama_api(interface_format, embed_url): - # 去除末尾斜杠 fixed_url = embed_url.rstrip("/") - # 如果包含 /v1 则替换为 /api fixed_url = fixed_url.replace("/v1", "/api") return OllamaEmbeddings( model_name=embedding_model_name, base_url=fixed_url ) elif is_using_ml_studio_api(interface_format, base_url): + # ML Studio / OpenAI 兼容 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") # ============ 向量库相关 ============ + VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): os.makedirs(VECTOR_STORE_DIR) @@ -102,8 +97,8 @@ def clear_vector_store(): 清空本地向量库(删除 vectorstore 文件夹内的内容)。 """ if os.path.exists(VECTOR_STORE_DIR): + import shutil try: - import shutil for filename in os.listdir(VECTOR_STORE_DIR): file_path = os.path.join(VECTOR_STORE_DIR, filename) if os.path.isfile(file_path) or os.path.islink(file_path): @@ -126,7 +121,6 @@ def init_vector_store( ) -> Chroma: """ 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 - embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。 """ embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -156,6 +150,7 @@ def load_vector_store( 读取已存在的向量库。若不存在则返回 None。 """ if not os.path.exists(VECTOR_STORE_DIR): + logging.info("Vector store not found. Initializing a new one...") return None embed_url = embedding_base_url if embedding_base_url else base_url embeddings = create_embeddings_object( @@ -185,8 +180,10 @@ def update_vector_store( 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...") + logging.info("Vector store does not exist. Initializing a new one for new chapter...") init_vector_store( api_key=api_key, base_url=base_url, @@ -200,6 +197,7 @@ def update_vector_store( new_doc = Document(page_content=new_chapter) store.add_documents([new_doc]) store.persist() + logging.info("Vector store updated with the new chapter.") def get_relevant_context_from_vector_store( api_key: str, @@ -212,7 +210,7 @@ def get_relevant_context_from_vector_store( ) -> str: """ 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 - 若向量库不存在则返回空字符串。 + 若向量库不存在或没有足够的内容,则返回空字符串。 """ store = load_vector_store( api_key=api_key, @@ -221,10 +219,19 @@ def get_relevant_context_from_vector_store( embedding_model_name=embedding_model_name, embedding_base_url=embedding_base_url ) + + # 如果向量库为空,直接返回空字符串 if not store: - logging.warning("Vector store not found. Returning empty context.") + logging.info("No vector store found. Returning empty context.") return "" + + # 向量库存在,但没有足够的内容时也避免索引错误 docs = store.similarity_search(query, k=k) + + if not docs: + logging.info(f"No relevant documents found for query '{query}'. Returning empty context.") + return "" + combined = "\n".join([d.page_content for d in docs]) return combined @@ -256,7 +263,6 @@ def Novel_novel_directory_generate( """ 使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。 """ - # 确保文件夹存在 os.makedirs(filepath, exist_ok=True) model = ChatOpenAI( @@ -327,7 +333,6 @@ def Novel_novel_directory_generate( debug_log(prompt, response.content) return {"novel_directory": response.content.strip()} - # 构建状态图 graph = StateGraph(OverallState) graph.add_node("generate_base_setting", generate_base_setting) graph.add_node("generate_character_setting", generate_character_setting) @@ -363,7 +368,6 @@ def Novel_novel_directory_generate( logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。") return - # 写入文件 filename_set = os.path.join(filepath, "Novel_setting.txt") filename_novel_directory = os.path.join(filepath, "Novel_directory.txt") @@ -375,7 +379,6 @@ def Novel_novel_directory_generate( append_text_to_file(final_novel_setting_cleaned, filename_set) append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) - logging.info("Novel settings and directory generated successfully.") @@ -394,19 +397,25 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int text = read_file(chap_file).strip() if text: texts.append(text) + if len(texts) < n: + texts = [''] * (n - len(texts)) + texts return texts + def summarize_recent_chapters( - llm_model: str, - api_key: str, - base_url: str, - temperature: float, - chapters_text_list: List[str] - ) -> str: + llm_model: str, + api_key: str, + base_url: str, + temperature: float, + chapters_text_list: List[str] +) -> str: """ - 将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。 - 如果没有可用的模型(model=None),则退化为简单截断示例。 + 将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。 """ + if not chapters_text_list: + return "" + if chapters_text_list==['', '', '']: + return "暂无摘要。" model = ChatOpenAI( model=llm_model, api_key=api_key, @@ -414,33 +423,19 @@ def summarize_recent_chapters( temperature=temperature ) - if not chapters_text_list: - return "" - combined_text = "\n".join(chapters_text_list) - # 如果未传入model,就做个简单的退化输出 - if not model: - return f"【摘要-演示】\n{combined_text[:800]}..." - - # 构造一个提示词(Prompt),指示模型生成精简摘要 - prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容: + prompt = f"""你是一名资深长篇小说写作辅助AI,下面是最近几章的合并文本: {combined_text} -请你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点。 -1.请用中文输出,不超过500字。 -2.仅回复摘要内容,不需要其他信息。 -""" - # 调用模型获取摘要 +请用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点:""" + response = model.invoke(prompt) if not response or not response.content.strip(): - # 若模型无响应或空,返回简单截断 - return f"【摘要-演示】\n{combined_text[:800]}..." - - # 返回模型生成的摘要文本 + return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text return response.content.strip() -# ============ 新增:更新剧情要点/未解决冲突 ============ +# ============ 新增:剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ 下面是新生成的章节内容: @@ -449,9 +444,9 @@ PLOT_ARCS_PROMPT = """\ 这里是已记录的剧情要点/未解决冲突(可能为空): {old_plot_arcs} -请基于新的章节内容,提炼出本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 +请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。 -最终输出一份更新后的剧情要点列表,以帮助后续保持故事的整体一致性和悬念延续。 +最终输出更新后的剧情要点列表,以帮助后续保持故事整体的一致性和悬念延续。 """ def update_plot_arcs( @@ -462,10 +457,6 @@ def update_plot_arcs( model_name: str, temperature: float ) -> str: - """ - 利用模型分析最新章节文本,提炼或更新“未解决冲突或剧情要点”。 - 并返回更新后的字符串。 - """ model = ChatOpenAI( model=model_name, api_key=api_key, @@ -480,7 +471,6 @@ def update_plot_arcs( if not response: logging.warning("update_plot_arcs: No response.") return old_plot_arcs - debug_log(prompt, response.content) return response.content.strip() @@ -499,28 +489,44 @@ def generate_chapter_draft( word_number: int, temperature: float, novel_novel_directory: str, - filepath: str + filepath: str, + interface_format: str, + embedding_model_name: str, + embedding_base_url: str ) -> str: """ - 仅生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 - 并将生成的内容写到 "chapter_{novel_number}.txt" 覆盖写入。 - 同时生成 "outline_{novel_number}.txt" 存储大纲内容。 + 生成当前章节的草稿,不更新全局摘要/角色状态/向量库。 """ - # 0) 根据 novel_number 从 novel_novel_directory 中获取本章标题及简述 + # 根据目录信息获取本章标题、简介 chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number) chapter_title = chapter_info["chapter_title"] chapter_brief = chapter_info["chapter_brief"] - # 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情") - relevant_context = get_relevant_context_from_vector_store( - api_key=api_key, - base_url=base_url, - query="回顾剧情", - interface_format="OpenAI", - embedding_model_name="", - embedding_base_url="", - k=2 - ) + # 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并) + queries = [] + if user_guidance.strip(): + queries.append(user_guidance) + if chapter_brief.strip(): + queries.append(chapter_brief) + # 也可加一句“回顾剧情”之类 + queries.append("回顾剧情") + + relevant_context = "" + for q in queries: + partial_context = get_relevant_context_from_vector_store( + api_key=api_key, + base_url=base_url, + query=q, + interface_format=interface_format, + embedding_model_name=embedding_model_name, + embedding_base_url=embedding_base_url, + k=2 + ) + if partial_context.strip(): + relevant_context += "\n" + partial_context + # 如果检索结果为空,使用默认值(如空字符串) + if not relevant_context: + relevant_context = "暂无相关内容。" model = ChatOpenAI( model=model_name, @@ -529,10 +535,10 @@ def generate_chapter_draft( temperature=temperature ) - # 2) 生成大纲 + # 1) 生成本章大纲 outline_prompt_text = chapter_outline_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, + character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, global_summary=global_summary, novel_number=novel_number, chapter_title=chapter_title, @@ -550,10 +556,10 @@ def generate_chapter_draft( clear_file_content(outline_file) save_string_to_txt(chapter_outline, outline_file) - # 3) 生成正文草稿 + # 2) 生成正文草稿 writing_prompt_text = chapter_write_prompt.format( novel_setting=novel_settings, - character_state=character_state + "\n\n【历史上下文】\n" + relevant_context, + character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context, global_summary=global_summary, chapter_outline=chapter_outline, word_number=word_number, @@ -588,13 +594,12 @@ def finalize_chapter( ): """ 对当前章节进行定稿: - 1. 读取 chapter_{novel_number}.txt 的最终内容; - 2. 更新全局摘要、角色状态文件; - 3. 如果字数明显少于 word_number 的 80%,则自动调用 enrich_chapter_text 再次扩写; - 4. 更新向量库; - 5. 新增:更新剧情要点/未解决冲突 -> plot_arcs.txt + 1. 读取草稿文本 + 2. 若字数太短则再次扩写 + 3. 更新全局摘要、角色状态 + 4. 更新剧情要点 + 5. 更新向量库 """ - # 读取当前章节内容 chapters_dir = os.path.join(filepath, "chapters") chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") chapter_text = read_file(chapter_file).strip() @@ -610,9 +615,9 @@ def finalize_chapter( old_global_summary = read_file(global_summary_file) old_plot_arcs = read_file(plot_arcs_file) - # 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...") + logging.info("Chapter text is shorter than 80% of desired length. Enriching...") chapter_text = enrich_chapter_text( chapter_text=chapter_text, word_number=word_number, @@ -623,9 +628,8 @@ def finalize_chapter( ) clear_file_content(chapter_file) save_string_to_txt(chapter_text, chapter_file) - logging.info("Chapter text has been enriched and updated.") - # 2) 更新全局摘要 + # 更新全局摘要 model = ChatOpenAI( model=model_name, api_key=api_key, @@ -643,7 +647,7 @@ def finalize_chapter( new_global_summary = update_global_summary(chapter_text, old_global_summary) - # 3) 更新角色状态 + # 更新角色状态 def update_character_state(chapter_text: str, old_state: str) -> str: prompt = update_character_state_prompt.format( chapter_text=chapter_text, @@ -654,7 +658,7 @@ def finalize_chapter( new_char_state = update_character_state(chapter_text, old_char_state) - # 4) 更新剧情要点 + # 更新剧情要点 new_plot_arcs = update_plot_arcs( chapter_text=chapter_text, old_plot_arcs=old_plot_arcs, @@ -664,7 +668,7 @@ def finalize_chapter( temperature=temperature ) - # 5) 覆盖写入文件 + # 写回文件 clear_file_content(character_state_file) save_string_to_txt(new_char_state, character_state_file) @@ -674,10 +678,10 @@ def finalize_chapter( clear_file_content(plot_arcs_file) save_string_to_txt(new_plot_arcs, plot_arcs_file) - # 6) 更新向量库 + # 更新向量库 update_vector_store( - api_key=api_key, - base_url=base_url, + api_key=api_key, + base_url=base_url, new_chapter=chapter_text, interface_format=interface_format, embedding_model_name=embedding_model_name @@ -695,7 +699,6 @@ def enrich_chapter_text( ) -> str: """ 当章节篇幅不足时,调用此函数对章节文本进行二次扩写。 - 可以让模型补充场景描写、角色心理等,保证与现有文本风格一致。 """ model = ChatOpenAI( model=model_name, @@ -713,20 +716,21 @@ def enrich_chapter_text( return chapter_text return response.content.strip() + # ============ 导入外部知识文本 ============ def import_knowledge_file( - api_key: str, - base_url: str, - interface_format: str, - embedding_model_name: str, - file_path: str, - embedding_base_url: str = "" - ) -> None: + api_key: str, + base_url: str, + interface_format: str, + embedding_model_name: str, + file_path: str, + embedding_base_url: str = "" +) -> None: """ 将用户选定的文本文件导入到向量库,以便在写作时检索。 """ - logging.info(f"开始导入知识库文件: {file_path},当前接口格式: {interface_format},当前模型: {embedding_model_name}") + logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}") if not os.path.exists(file_path): logging.warning(f"知识库文件不存在: {file_path}") return @@ -760,11 +764,12 @@ def advanced_split_content(content: str, similarity_threshold: float = 0.7, max_length: int = 500) -> List[str]: """ - 将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 + 将文本先按句子切分,然后根据语义相似度进行合并,最后按max_length二次切分。 """ - nltk.download('punkt_tab', quiet=True) - sentences = nltk.sent_tokenize(content) + # 纠正下载punkt包:'punkt' 而非 'punkt_tab' + nltk.download('punkt', quiet=True) + sentences = nltk.sent_tokenize(content) if not sentences: return [] diff --git a/ui.py b/ui.py index 6bfc0e8..dd787c5 100644 --- a/ui.py +++ b/ui.py @@ -1,11 +1,12 @@ # ui.py # -*- coding: utf-8 -*- +import logging import os import threading import customtkinter as ctk from tkinter import filedialog, messagebox - +import traceback from config_manager import load_config, save_config from utils import read_file, save_string_to_txt from novel_generator import ( @@ -19,6 +20,12 @@ from novel_generator import ( ) from consistency_checker import check_consistency +def log_error(message: str): + """ + 用于打印详细的错误信息和堆栈信息。 + """ + logging.error(f"{message}\n{traceback.format_exc()}") + # 设置全局主题和颜色 ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") @@ -38,7 +45,6 @@ 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")) @@ -46,7 +52,7 @@ class NovelGeneratorGUI: 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", "") self.genre_var = ctk.StringVar(value=self.loaded_config.get("genre", "玄幻")) @@ -128,8 +134,6 @@ class NovelGeneratorGUI: # 日志:只读 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): @@ -137,8 +141,6 @@ class NovelGeneratorGUI: 右侧用于显示配置与功能按钮。 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 - - 另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。 """ self.right_frame.grid_rowconfigure(0, weight=0) self.right_frame.grid_rowconfigure(1, weight=0) @@ -172,14 +174,13 @@ class NovelGeneratorGUI: - 模型名称 - Temperature """ - # 配置网格 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): + # 如果用户切换接口格式,可根据需要修改BaseURL为默认值 if new_value == "Ollama": self.base_url_var.set("http://localhost:11434/v1") elif new_value == "ML Studio": @@ -263,7 +264,6 @@ class NovelGeneratorGUI: def build_novel_params_area(self, start_row=2): """ 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 - 只使用两列:0列放Label,1列放输入框(可能加子Frame)。 """ # 主题(Topic) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) @@ -291,7 +291,7 @@ class NovelGeneratorGUI: 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") @@ -320,11 +320,8 @@ class NovelGeneratorGUI: """ 主要功能按钮 + 配置保存/加载 按钮 """ - # 调整下布局:0,1列 - # 这里按行摆放多个按钮 self.right_frame.grid_rowconfigure(start_row, 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) @@ -336,7 +333,6 @@ class NovelGeneratorGUI: 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, @@ -389,9 +385,6 @@ class NovelGeneratorGUI: # ------------------ Novel Settings Tab ------------------ def build_setting_tab(self): - """ - 可查看/编辑 Novel_setting.txt 并保存 - """ self.setting_tab.rowconfigure(0, weight=0) self.setting_tab.rowconfigure(1, weight=1) self.setting_tab.columnconfigure(0, weight=1) @@ -430,9 +423,6 @@ class NovelGeneratorGUI: # ------------------ Novel Directory Tab ------------------ def build_directory_tab(self): - """ - 可查看/编辑 Novel_directory.txt 并保存 - """ self.directory_tab.rowconfigure(0, weight=0) self.directory_tab.rowconfigure(1, weight=1) self.directory_tab.columnconfigure(0, weight=1) @@ -471,9 +461,6 @@ class NovelGeneratorGUI: # ------------------ Character State Tab ------------------ def build_character_tab(self): - """ - 查看/编辑 character_state.txt - """ self.character_tab.rowconfigure(0, weight=0) self.character_tab.rowconfigure(1, weight=1) self.character_tab.columnconfigure(0, weight=1) @@ -512,9 +499,6 @@ class NovelGeneratorGUI: # ------------------ Global Summary Tab ------------------ def build_summary_tab(self): - """ - 查看/编辑 global_summary.txt - """ self.summary_tab.rowconfigure(0, weight=0) self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.columnconfigure(0, weight=1) @@ -603,11 +587,9 @@ 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") # ------------------ 功能区 -------------------- @@ -650,6 +632,7 @@ class NovelGeneratorGUI: ) self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") except Exception as e: + logging.error(f"生成小说设定 & 目录时出错: {e}") self.log(f"❌ 生成小说设定 & 目录时出错: {e}") finally: self.enable_button(self.btn_generate_full) @@ -693,10 +676,13 @@ 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( - model_name, # 此处仅示例 - recent_3_texts + llm_model=model_name, + api_key=api_key, + base_url=base_url, + temperature=temperature, + chapters_text_list=recent_3_texts ) self.log(f"开始生成第{chap_num}章草稿...") @@ -713,7 +699,10 @@ class NovelGeneratorGUI: word_number=word_number, temperature=temperature, novel_novel_directory=novel_directory, - filepath=filepath + filepath=filepath, + interface_format=self.interface_format_var.get().strip(), + embedding_model_name=self.embedding_model_name_var.get().strip(), + embedding_base_url=self.embedding_url_var.get().strip() ) if draft_text: self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") @@ -724,6 +713,7 @@ class NovelGeneratorGUI: self.log("⚠️ 本章草稿生成失败或无内容。") except Exception as e: + logging.error(f"生成章节草稿时出错: {e}") self.log(f"❌ 生成章节草稿时出错: {e}") finally: self.enable_button(self.btn_generate_chapter) @@ -741,6 +731,8 @@ class NovelGeneratorGUI: model_name = self.model_name_var.get().strip() temperature = self.temperature_var.get() filepath = self.filepath_var.get().strip() + interface_format = self.interface_format_var.get().strip() + embedding_model_name = self.embedding_model_name_var.get().strip() if not filepath: self.log("请先配置保存文件路径。") @@ -755,6 +747,8 @@ class NovelGeneratorGUI: word_number=word_number, api_key=api_key, base_url=base_url, + interface_format=interface_format, + embedding_model_name=embedding_model_name, model_name=model_name, temperature=temperature, filepath=filepath @@ -769,6 +763,7 @@ class NovelGeneratorGUI: self.chapter_result.see("end") except Exception as e: + logging.error(f"定稿章节时出错: {e}") self.log(f"❌ 定稿章节时出错: {e}") finally: self.enable_button(self.btn_finalize_chapter) @@ -801,7 +796,6 @@ class NovelGeneratorGUI: global_summary = read_file(global_summary_file) plot_arcs = read_file(plot_arcs_file) - # 获取当前章节文本 chap_num = self.chapter_num_var.get() chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") chapter_text = read_file(chap_file) @@ -826,6 +820,7 @@ class NovelGeneratorGUI: self.log(result) except Exception as e: + logging.error(f"审校时出错: {e}") self.log(f"❌ 审校时出错: {e}") finally: self.enable_button(self.btn_check_consistency) @@ -848,12 +843,13 @@ class NovelGeneratorGUI: api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), interface_format=self.interface_format_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip(), - embedding_base_url=self.embedding_url_var.get().strip(), - file_path=selected_file + embedding_model_name=self.embedding_model_name_var.get().strip(), + file_path=selected_file, + embedding_base_url=self.embedding_url_var.get().strip() ) self.log("✅ 知识库文件导入完成。") except Exception as e: + logging.error(f"导入知识库时出错: {e}") self.log(f"❌ 导入知识库时出错: {e}") finally: self.enable_button(self.btn_import_knowledge) @@ -886,7 +882,6 @@ class NovelGeneratorGUI: if not arcs_text: arcs_text = "当前没有记录的剧情要点或冲突。" - # 弹窗显示 top = ctk.CTkToplevel(self.master) top.title("剧情要点/未解决冲突") top.geometry("600x400") From 6d8a67782cd9393a5518cd2267cf58220036d77d Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 18:35:10 +0800 Subject: [PATCH 08/11] 222 --- embedding_ollama.py | 18 ++-- novel_generator.py | 54 +++++------ ui.py | 218 ++++++++++++++++++++------------------------ 3 files changed, 133 insertions(+), 157 deletions(-) diff --git a/embedding_ollama.py b/embedding_ollama.py index b755ddd..8dd6dea 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -5,15 +5,18 @@ from typing import List class OllamaEmbeddings: """ Ollama 本地服务提供的 Embedding 接口, - 本需求里我们最终拼出形如: http://localhost:11434/api/embed + 最终拼出形如: http://localhost:11434/api/embed 即 base_url + "/embed" """ def __init__(self, model_name: str, base_url: str): self.model_name = model_name - self.base_url = base_url # 这里应形如 http://localhost:11434/api (不再含 /v1) + self.base_url = base_url # 这里一般形如 http://localhost:11434/api (而非 /v1) def embed(self, texts: List[str]) -> List[List[float]]: + """ + 批量将多段文本转换为embedding向量 + """ embeddings = [] for text in texts: embeddings.append(self.embed_single_document(text)) @@ -21,13 +24,9 @@ class OllamaEmbeddings: def embed_documents(self, texts: List[str]) -> List[List[float]]: """ - 将多段文本转换为向量列表 + 兼容langchain的接口写法 """ - embeddings = [] - for text in texts: - emb = self.embed_single_document(text) - embeddings.append(emb) - return embeddings + return self.embed(texts) def embed_query(self, query: str) -> List[float]: """ @@ -38,7 +37,6 @@ class OllamaEmbeddings: def embed_single_document(self, text: str) -> List[float]: """ 调用 Ollama 本地服务接口,获取文本的 embedding。 - 这里统一改为请求: [base_url]/embed """ url = f"{self.base_url}/embed" data = { @@ -49,6 +47,8 @@ class OllamaEmbeddings: response = requests.post(url, json=data) response.raise_for_status() result = response.json() + if "embedding" not in result: + raise ValueError("No 'embedding' field in Ollama response.") return result["embedding"] except requests.exceptions.RequestException as e: raise Exception(f"Ollama embeddings request error: {e}") diff --git a/novel_generator.py b/novel_generator.py index 71343a2..0b01ac6 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -29,12 +29,11 @@ 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): - logging.info(f"\n[Prompt >>>] {prompt}\n") - logging.info(f"[Response >>>] {response_content}\n") + logging.info(f"\n[Prompt >>>] {prompt}\n") + logging.info(f"[Response >>>] {response_content}\n") # ============ 判断接口格式相关 ============ @@ -54,7 +53,6 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool: return True return False - # ============ 创建 Embeddings 对象 ============ def create_embeddings_object( @@ -67,25 +65,23 @@ def create_embeddings_object( """ 根据用户在UI中配置的参数,返回对应的 embeddings 对象。 - 当 interface_format = "Ollama" => OllamaEmbeddings(...) - (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed) - - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings - - 其它情况视需求可扩展 + - 当 interface_format = "OpenAI"/"ML Studio" => OpenAIEmbeddings(...) + - 其它情况可扩展 """ if is_using_ollama_api(interface_format, embed_url): fixed_url = embed_url.rstrip("/") + # Ollama embedding接口通常是 /api/embed fixed_url = fixed_url.replace("/v1", "/api") return OllamaEmbeddings( model_name=embedding_model_name, base_url=fixed_url ) elif is_using_ml_studio_api(interface_format, base_url): - # ML Studio / OpenAI 兼容 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) - # ============ 向量库相关 ============ VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") @@ -225,9 +221,7 @@ def get_relevant_context_from_vector_store( logging.info("No vector store found. Returning empty context.") return "" - # 向量库存在,但没有足够的内容时也避免索引错误 docs = store.similarity_search(query, k=k) - if not docs: logging.info(f"No relevant documents found for query '{query}'. Returning empty context.") return "" @@ -235,7 +229,6 @@ def get_relevant_context_from_vector_store( combined = "\n".join([d.page_content for d in docs]) return combined - # ============ 多步生成:设置 & 目录 ============ class OverallState(TypedDict): @@ -308,7 +301,7 @@ def Novel_novel_directory_generate( debug_log(prompt, response.content) return {"dark_lines": response.content.strip()} - def finalize_novel_setting(state: OverallState) -> Dict[str, str]: + def finalize_novel_setting_func(state: OverallState) -> Dict[str, str]: prompt = finalize_setting_prompt.format( novel_setting_base=state["novel_setting_base"], character_setting=state["character_setting"], @@ -321,7 +314,7 @@ def Novel_novel_directory_generate( debug_log(prompt, response.content) return {"final_novel_setting": response.content.strip()} - def generate_novel_directory(state: OverallState) -> Dict[str, str]: + def generate_novel_directory_func(state: OverallState) -> Dict[str, str]: prompt = novel_directory_prompt.format( final_novel_setting=state["final_novel_setting"], number_of_chapters=state["number_of_chapters"] @@ -337,8 +330,8 @@ def Novel_novel_directory_generate( graph.add_node("generate_base_setting", generate_base_setting) graph.add_node("generate_character_setting", generate_character_setting) graph.add_node("generate_dark_lines", generate_dark_lines) - graph.add_node("finalize_novel_setting", finalize_novel_setting) - graph.add_node("generate_novel_directory", generate_novel_directory) + graph.add_node("finalize_novel_setting", finalize_novel_setting_func) + graph.add_node("generate_novel_directory", generate_novel_directory_func) graph.add_edge(START, "generate_base_setting") graph.add_edge("generate_base_setting", "generate_character_setting") @@ -377,10 +370,14 @@ def Novel_novel_directory_generate( final_novel_setting_cleaned = clean_text(final_novel_setting) final_novel_directory_cleaned = clean_text(final_novel_directory) - append_text_to_file(final_novel_setting_cleaned, filename_set) - append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) - logging.info("Novel settings and directory generated successfully.") + # 改进:写文件时先清空再写入 + clear_file_content(filename_set) + save_string_to_txt(final_novel_setting_cleaned, filename_set) + clear_file_content(filename_novel_directory) + save_string_to_txt(final_novel_directory_cleaned, filename_novel_directory) + + logging.info("Novel settings and directory generated successfully.") # ============ 获取最近N章内容,生成短期摘要 ============ @@ -401,7 +398,6 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int texts = [''] * (n - len(texts)) + texts return texts - def summarize_recent_chapters( llm_model: str, api_key: str, @@ -414,8 +410,10 @@ def summarize_recent_chapters( """ if not chapters_text_list: return "" - if chapters_text_list==['', '', '']: + # 如果列表里全是空,则无法生成摘要 + if all(not txt.strip() for txt in chapters_text_list): return "暂无摘要。" + model = ChatOpenAI( model=llm_model, api_key=api_key, @@ -431,10 +429,10 @@ def summarize_recent_chapters( response = model.invoke(prompt) if not response or not response.content.strip(): + # 若模型无响应,就截取一段作为“备选” return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text return response.content.strip() - # ============ 新增:剧情要点/未解决冲突 ============ PLOT_ARCS_PROMPT = """\ @@ -473,7 +471,6 @@ def update_plot_arcs( return old_plot_arcs return response.content.strip() - # ============ 生成章节草稿 & 定稿 ============ def generate_chapter_draft( @@ -502,13 +499,12 @@ def generate_chapter_draft( chapter_title = chapter_info["chapter_title"] chapter_brief = chapter_info["chapter_brief"] - # 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并) + # 从向量库检索上下文 queries = [] if user_guidance.strip(): queries.append(user_guidance) if chapter_brief.strip(): queries.append(chapter_brief) - # 也可加一句“回顾剧情”之类 queries.append("回顾剧情") relevant_context = "" @@ -524,7 +520,6 @@ def generate_chapter_draft( ) if partial_context.strip(): relevant_context += "\n" + partial_context - # 如果检索结果为空,使用默认值(如空字符串) if not relevant_context: relevant_context = "暂无相关内容。" @@ -716,7 +711,6 @@ def enrich_chapter_text( return chapter_text return response.content.strip() - # ============ 导入外部知识文本 ============ def import_knowledge_file( @@ -740,6 +734,8 @@ def import_knowledge_file( logging.warning("知识库文件内容为空。") return + nltk.download('punkt_tab', quiet=True) + paragraphs = advanced_split_content(content) store = load_vector_store(api_key, base_url, interface_format, embedding_model_name, embedding_base_url) @@ -765,10 +761,8 @@ def advanced_split_content(content: str, max_length: int = 500) -> List[str]: """ 将文本先按句子切分,然后根据语义相似度进行合并,最后按max_length二次切分。 + 可根据需要微调此逻辑。 """ - # 纠正下载punkt包:'punkt' 而非 'punkt_tab' - nltk.download('punkt', quiet=True) - sentences = nltk.sent_tokenize(content) if not sentences: return [] diff --git a/ui.py b/ui.py index dd787c5..43fe10c 100644 --- a/ui.py +++ b/ui.py @@ -8,7 +8,7 @@ import customtkinter as ctk from tkinter import filedialog, messagebox import traceback from config_manager import load_config, save_config -from utils import read_file, save_string_to_txt +from utils import read_file, save_string_to_txt, clear_file_content from novel_generator import ( Novel_novel_directory_generate, generate_chapter_draft, @@ -34,7 +34,14 @@ class NovelGeneratorGUI: def __init__(self, master): self.master = master self.master.title("Novel Generator GUI (CustomTkinter)") - self.master.iconbitmap("icon.ico") + + # 防止因 icon.ico 不存在导致程序崩溃 + try: + if os.path.exists("icon.ico"): + self.master.iconbitmap("icon.ico") + except Exception: + pass + # 窗口最大化 self.master.state("zoomed") # 配置窗口大小 @@ -90,9 +97,6 @@ class NovelGeneratorGUI: # ------------------ 主功能 Tab ------------------ def build_main_tab(self): - """ - 主Tab: 左侧显示章节草稿/日志, 右侧是功能区 & 配置区 - """ self.main_tab.rowconfigure(0, weight=1) self.main_tab.columnconfigure(0, weight=1) self.main_tab.columnconfigure(1, weight=0) @@ -100,6 +104,7 @@ class NovelGeneratorGUI: # 左侧Frame self.left_frame = ctk.CTkFrame(self.main_tab) self.left_frame.grid(row=0, column=0, sticky="nsew", padx=2, pady=2) + # 右侧Frame self.right_frame = ctk.CTkFrame(self.main_tab) self.right_frame.grid(row=0, column=1, sticky="nsew", padx=2, pady=2) @@ -110,11 +115,6 @@ class NovelGeneratorGUI: self.build_right_layout() def build_left_layout(self): - """ - 左侧包含两个区域: - 1. 本章草稿内容(可编辑) - 2. 输出日志(只读) - """ self.left_frame.grid_rowconfigure(0, weight=3) self.left_frame.grid_rowconfigure(1, weight=1) self.left_frame.grid_columnconfigure(0, weight=1) @@ -137,16 +137,11 @@ class NovelGeneratorGUI: self.log_text.configure(state="disabled") def build_right_layout(self): - """ - 右侧用于显示配置与功能按钮。 - 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 - 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 - """ 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.right_frame.columnconfigure(0, weight=0) + self.right_frame.columnconfigure(1, weight=1) # ========== 1. 配置项 TabView(AI接口配置 & Embedding配置) ========== self.config_tabview = ctk.CTkTabview(self.right_frame) @@ -166,19 +161,6 @@ class NovelGeneratorGUI: self.build_main_buttons_area(start_row=10) def build_ai_config_tab(self): - """ - 在 AI接口配置 子Tab 上放置: - - API Key - - Base URL - - 接口格式 - - 模型名称 - - Temperature - """ - 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) - def on_interface_format_changed(new_value): # 如果用户切换接口格式,可根据需要修改BaseURL为默认值 if new_value == "Ollama": @@ -188,6 +170,11 @@ class NovelGeneratorGUI: elif new_value == "OpenAI": self.base_url_var.set("https://api.agicto.cn/v1") + 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) + # 1. 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") @@ -239,11 +226,6 @@ class NovelGeneratorGUI: self.temp_value_label.grid(row=4, column=2, padx=1, pady=1, sticky="w") 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) @@ -262,9 +244,6 @@ class NovelGeneratorGUI: emb_model_name_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") def build_novel_params_area(self, start_row=2): - """ - 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 - """ # 主题(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") @@ -317,9 +296,6 @@ class NovelGeneratorGUI: 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.btn_frame_config = ctk.CTkFrame(self.right_frame) @@ -418,6 +394,7 @@ class NovelGeneratorGUI: content = self.setting_text.get("0.0", "end").strip() setting_file = os.path.join(filepath, "Novel_setting.txt") + clear_file_content(setting_file) save_string_to_txt(content, setting_file) self.log("已保存对 Novel_setting.txt 的修改。") @@ -456,6 +433,7 @@ class NovelGeneratorGUI: content = self.directory_text.get("0.0", "end").strip() directory_file = os.path.join(filepath, "Novel_directory.txt") + clear_file_content(directory_file) save_string_to_txt(content, directory_file) self.log("已保存对 Novel_directory.txt 的修改。") @@ -494,6 +472,7 @@ class NovelGeneratorGUI: content = self.character_text.get("0.0", "end").strip() char_file = os.path.join(filepath, "character_state.txt") + clear_file_content(char_file) save_string_to_txt(content, char_file) self.log("已保存对 character_state.txt 的修改。") @@ -532,6 +511,7 @@ class NovelGeneratorGUI: content = self.summary_text.get("0.0", "end").strip() summary_file = os.path.join(filepath, "global_summary.txt") + clear_file_content(summary_file) save_string_to_txt(content, summary_file) self.log("已保存对 global_summary.txt 的修改。") @@ -585,24 +565,35 @@ class NovelGeneratorGUI: if selected_dir: self.filepath_var.set(selected_dir) - # ------------------ 日志输出 ------------------ + # ------------------ 日志输出(主线程安全) ------------------ 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): - btn.configure(state="disabled") + def safe_log(self, message: str): + # 子线程中调用:把真正的日志操作切换回主线程执行 + self.master.after(0, lambda: self.log(message)) - def enable_button(self, btn): - btn.configure(state="normal") + # 由于按钮更新也属于UI操作,也需要安全方式 + def disable_button_safe(self, btn): + self.master.after(0, lambda: btn.configure(state="disabled")) + def enable_button_safe(self, btn): + self.master.after(0, lambda: btn.configure(state="normal")) + + # ------------------ 功能区(带线程处理) -------------------- def generate_full_novel(self): """生成小说设定 & 目录""" + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先选择保存文件路径") + return + def task(): - self.disable_button(self.btn_generate_full) + self.disable_button_safe(self.btn_generate_full) try: api_key = self.api_key_var.get().strip() base_url = self.base_url_var.get().strip() @@ -611,14 +602,9 @@ class NovelGeneratorGUI: genre = self.genre_var.get().strip() num_chapters = self.num_chapters_var.get() word_number = self.word_number_var.get() - filepath = self.filepath_var.get().strip() temperature = self.temperature_var.get() - if not filepath: - messagebox.showwarning("警告", "请先选择保存文件路径") - return - - self.log("开始生成小说设定和目录...") + self.safe_log("开始生成小说设定和目录...") Novel_novel_directory_generate( api_key=api_key, base_url=base_url, @@ -630,35 +616,33 @@ class NovelGeneratorGUI: filepath=filepath, temperature=temperature ) - self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") + self.safe_log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") except Exception as e: - logging.error(f"生成小说设定 & 目录时出错: {e}") - self.log(f"❌ 生成小说设定 & 目录时出错: {e}") + log_error(f"生成小说设定 & 目录时出错: {e}") + self.safe_log(f"❌ 生成小说设定 & 目录时出错: {e}") finally: - self.enable_button(self.btn_generate_full) + self.enable_button_safe(self.btn_generate_full) - thread = threading.Thread(target=task) - thread.start() + threading.Thread(target=task, daemon=True).start() def generate_chapter_draft_ui(self): """生成当前章节的草稿""" + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先配置保存文件路径。") + return + def task(): - self.disable_button(self.btn_generate_chapter) + self.disable_button_safe(self.btn_generate_chapter) try: 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() - filepath = self.filepath_var.get().strip() - - if not filepath: - self.log("请先配置保存文件路径。") - return - novel_settings_file = os.path.join(filepath, "Novel_setting.txt") novel_settings = read_file(novel_settings_file) if not novel_settings.strip(): - self.log("⚠️ 未找到 Novel_setting.txt,请先生成设定。") + self.safe_log("⚠️ 未找到 Novel_setting.txt,请先生成设定。") return character_state_file = os.path.join(filepath, "character_state.txt") @@ -685,7 +669,7 @@ class NovelGeneratorGUI: chapters_text_list=recent_3_texts ) - self.log(f"开始生成第{chap_num}章草稿...") + self.safe_log(f"开始生成第{chap_num}章草稿...") draft_text = generate_chapter_draft( novel_settings=novel_settings, global_summary=global_summary, @@ -705,43 +689,46 @@ class NovelGeneratorGUI: embedding_base_url=self.embedding_url_var.get().strip() ) if draft_text: - 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") + self.safe_log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") + # 更新UI中的章节草稿 + self.master.after(0, lambda: self.show_chapter_in_textbox(draft_text)) else: - self.log("⚠️ 本章草稿生成失败或无内容。") + self.safe_log("⚠️ 本章草稿生成失败或无内容。") except Exception as e: - logging.error(f"生成章节草稿时出错: {e}") - self.log(f"❌ 生成章节草稿时出错: {e}") + log_error(f"生成章节草稿时出错: {e}") + self.safe_log(f"❌ 生成章节草稿时出错: {e}") finally: - self.enable_button(self.btn_generate_chapter) + self.enable_button_safe(self.btn_generate_chapter) - thread = threading.Thread(target=task) - thread.start() + threading.Thread(target=task, daemon=True).start() + + def show_chapter_in_textbox(self, text: str): + self.chapter_result.delete("0.0", "end") + self.chapter_result.insert("0.0", text) + self.chapter_result.see("end") def finalize_chapter_ui(self): """定稿当前章节:更新全局摘要、角色状态、向量库等""" + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先配置保存文件路径。") + return + def task(): - self.disable_button(self.btn_finalize_chapter) + self.disable_button_safe(self.btn_finalize_chapter) try: 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() - filepath = self.filepath_var.get().strip() interface_format = self.interface_format_var.get().strip() embedding_model_name = self.embedding_model_name_var.get().strip() - if not filepath: - self.log("请先配置保存文件路径。") - return - chap_num = self.chapter_num_var.get() word_number = self.word_number_var.get() - self.log(f"开始定稿第{chap_num}章...") + self.safe_log(f"开始定稿第{chap_num}章...") finalize_chapter( novel_number=chap_num, word_number=word_number, @@ -753,38 +740,35 @@ class NovelGeneratorGUI: temperature=temperature, filepath=filepath ) - self.log(f"✅ 第{chap_num}章定稿完成(已更新全局摘要、角色状态、剧情要点、向量库)。") + self.safe_log(f"✅ 第{chap_num}章定稿完成(已更新全局摘要、角色状态、剧情要点、向量库)。") # 读取定稿后的文本显示 chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") final_text = read_file(chap_file) - self.chapter_result.delete("0.0", "end") - self.chapter_result.insert("0.0", final_text) - self.chapter_result.see("end") + self.master.after(0, lambda: self.show_chapter_in_textbox(final_text)) except Exception as e: - logging.error(f"定稿章节时出错: {e}") - self.log(f"❌ 定稿章节时出错: {e}") + log_error(f"定稿章节时出错: {e}") + self.safe_log(f"❌ 定稿章节时出错: {e}") finally: - self.enable_button(self.btn_finalize_chapter) + self.enable_button_safe(self.btn_finalize_chapter) - thread = threading.Thread(target=task) - thread.start() + threading.Thread(target=task, daemon=True).start() def do_consistency_check(self): """使用审校Agent对最新章节进行简单一致性或冲突检查""" + filepath = self.filepath_var.get().strip() + if not filepath: + messagebox.showwarning("警告", "请先配置保存文件路径。") + return + def task(): - self.disable_button(self.btn_check_consistency) + self.disable_button_safe(self.btn_check_consistency) try: 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() - filepath = self.filepath_var.get().strip() - - if not filepath: - self.log("请先配置保存文件路径。") - return novel_settings_file = os.path.join(filepath, "Novel_setting.txt") character_state_file = os.path.join(filepath, "character_state.txt") @@ -801,10 +785,10 @@ class NovelGeneratorGUI: chapter_text = read_file(chap_file) if not chapter_text.strip(): - self.log("⚠️ 当前章节文件为空或不存在,无法审校。") + self.safe_log("⚠️ 当前章节文件为空或不存在,无法审校。") return - self.log("开始一致性审校...") + self.safe_log("开始一致性审校...") result = check_consistency( novel_setting=novel_setting, character_state=character_state, @@ -816,17 +800,16 @@ class NovelGeneratorGUI: temperature=temperature, plot_arcs=plot_arcs ) - self.log("审校结果:") - self.log(result) + self.safe_log("审校结果:") + self.safe_log(result) except Exception as e: - logging.error(f"审校时出错: {e}") - self.log(f"❌ 审校时出错: {e}") + log_error(f"审校时出错: {e}") + self.safe_log(f"❌ 审校时出错: {e}") finally: - self.enable_button(self.btn_check_consistency) + self.enable_button_safe(self.btn_check_consistency) - thread = threading.Thread(target=task) - thread.start() + threading.Thread(target=task, daemon=True).start() def import_knowledge_handler(self): """处理导入知识库文件。""" @@ -836,9 +819,9 @@ class NovelGeneratorGUI: ) if selected_file: def task(): - self.disable_button(self.btn_import_knowledge) + self.disable_button_safe(self.btn_import_knowledge) try: - self.log(f"开始导入知识库文件: {selected_file}") + self.safe_log(f"开始导入知识库文件: {selected_file}") import_knowledge_file( api_key=self.api_key_var.get().strip(), base_url=self.base_url_var.get().strip(), @@ -847,15 +830,14 @@ class NovelGeneratorGUI: file_path=selected_file, embedding_base_url=self.embedding_url_var.get().strip() ) - self.log("✅ 知识库文件导入完成。") + self.safe_log("✅ 知识库文件导入完成。") except Exception as e: - logging.error(f"导入知识库时出错: {e}") - self.log(f"❌ 导入知识库时出错: {e}") + log_error(f"导入知识库时出错: {e}") + self.safe_log(f"❌ 导入知识库时出错: {e}") finally: - self.enable_button(self.btn_import_knowledge) + self.enable_button_safe(self.btn_import_knowledge) - thread = threading.Thread(target=task) - thread.start() + threading.Thread(target=task, daemon=True).start() def clear_vectorstore_handler(self): """清空向量库按钮:弹出二次确认。""" From 4b8ceffc01d3c4623424e1a54a765c01e429faf6 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 19:17:07 +0800 Subject: [PATCH 09/11] 333 --- Src/Novel_directory.txt | 182 ------------------------------------- Src/Novel_setting.txt | 98 -------------------- Src/outlines/outline_1.txt | 43 --------- embedding_ollama.py | 2 +- novel_generator.py | 100 ++++++++++---------- ui.py | 2 +- 6 files changed, 50 insertions(+), 377 deletions(-) delete mode 100644 Src/Novel_directory.txt delete mode 100644 Src/Novel_setting.txt delete mode 100644 Src/outlines/outline_1.txt diff --git a/Src/Novel_directory.txt b/Src/Novel_directory.txt deleted file mode 100644 index 3a9cc6f..0000000 --- a/Src/Novel_directory.txt +++ /dev/null @@ -1,182 +0,0 @@ - -第1章 :星河坠落提瓦特 - 穿越与初遇派蒙,危机初现端倪 -第2章 :蒙德的初印象 - 初探蒙德城,与丽莎的首次相遇 -第3章 :酒馆的闲谈 - 了解提瓦特局势,派蒙展现惊人一面 -第4章 :丽莎的试探 - 星河展现科技实力,引来丽莎的警惕 -第5章 :委托与冒险 - 接受委托,初次展现星穹铁道科技的运用 -第6章 :风魔龙的挑战 - 运用科技对抗风魔龙,展现策略与智慧 -第7章 :派蒙的异常 - 派蒙出现记忆碎片,暗示其不寻常身份 -第8章 :丽莎的秘密魔法 - 丽莎使用禁忌魔法,展现其神秘一面 -第9章 :旅行者的出现 - 与原神旅行者初次相遇,产生摩擦与合作 -第10章 :蒙德的阴谋 - 发现蒙德隐藏的危机,与丽莎深入合作 -第11章 :追捕令的阴影 - 星河收到星穹铁道追捕令,内心挣扎 -第12章 :深入调查 - 调查蒙德的危机,发现与丽莎的秘密组织有关 -第13章 :钟离的现身 - 钟离初次出现,对星河进行暗示与引导 -第14章 :失落科技的线索 - 发现关于提瓦特失落科技的线索 -第15章 :前往璃月 - 踏上前往璃月的旅程,面对新的挑战 -第16章 :璃月港的繁华 - 体验璃月港的文化,了解璃月局势 -第17章 :往生堂的委托 - 接受往生堂的委托,深入了解璃月历史 -第18章 :岩王帝君的考验 - 钟离对星河进行考验,揭示其真实目的 -第19章 :地脉异常 - 发现璃月地脉异常,与旅行者合作调查 -第20章 :黄金屋的秘密 - 探索黄金屋的秘密,发现与失落科技有关 -第21章 :刻晴的质疑 - 刻晴对星河的科技持有怀疑态度 -第22章 :对抗岩龙蜥 - 运用科技与策略对抗强大的岩龙蜥 -第23章 :派蒙的回忆 - 派蒙回忆片段增多,身份逐渐清晰 -第24章 :丽莎的引导 - 丽莎引导星河学习提瓦特魔法 -第25章 :星河的抉择 - 星河面临道德困境,做出艰难选择 -第26章 :前往稻妻 - 前往稻妻,面对新的挑战与危机 -第27章 :稻妻的雷电 - 体验稻妻的文化与习俗,对抗雷电将军的威压 -第28章 :八重神子的试探 - 八重神子对星河进行试探,考验其能力 -第29章 :影向地的秘密 - 探索影向地的秘密,发现与提瓦特历史有关 -第30章 :反抗军与合作 - 与稻妻反抗军合作,对抗幕后黑手 -第31章 :永恒的执政官 - 与雷电将军正面交锋,展现智慧与实力 -第32章 :派蒙的过去 - 派蒙的过去逐渐揭开,与提瓦特古老文明有关 -第33章 :丽莎的组织 - 丽莎所属组织的真相逐渐浮出水面 -第34章 :前往须弥 - 前往须弥,面对新的挑战与文化 -第35章 :须弥的智慧 - 体验须弥的文化,了解须弥的智慧与科技 -第36章 :草神的大智慧 - 与草神进行交流,探讨生命的意义 -第37章 :森林的危机 - 须弥森林面临危机,星河运用科技解决问题 -第38章 :沙漠的秘密 - 探索须弥沙漠的秘密,发现远古文明的遗迹 -第39章 :散兵的阴谋 - 散兵的阴谋逐渐浮出水面,星河需要应对 -第40章 :与散兵的对抗 - 与散兵展开对抗,展现星穹铁道科技的优势 -第41章 :前往枫丹 - 前往枫丹,面对新的挑战与科技 -第42章 :枫丹的审判 - 体验枫丹的法律与秩序,面对复杂的案件 -第43章 :水神的神秘 - 与水神进行互动,了解枫丹的秘密 -第44章 :机械岛的危机 - 枫丹机械岛面临危机,星河运用科技解决问题 -第45章 :潜入深海 - 潜入深海,探索深海的秘密与科技 -第46章 :派蒙的预言 - 派蒙预言即将到来的巨大危机 -第47章 :丽莎的抉择 - 丽莎面临艰难的抉择,需要做出牺牲 -第48章 :前往纳塔 - 前往纳塔,面对新的挑战与文化 -第49章 :纳塔的冰雪 - 体验纳塔的严寒环境,与冰雪元素对抗 -第50章 :雪山之巅的秘密 - 探索雪山之巅的秘密,发现远古文明的遗迹 -第51章 :冰神的考验 - 冰神对星河进行考验,考验其心性与毅力 -第52章 :与旅行者的合作 - 与旅行者深入合作,对抗共同的敌人 -第53章 :深渊的威胁 - 深渊的威胁日益逼近,星河需要寻找应对之策 -第54章 :钟离的计划 - 钟离的计划逐渐显露,其真实目的更加清晰 -第55章 :星河的过去 - 星河过去的阴影再次浮现,影响其与他人的关系 -第56章 :派蒙的觉醒 - 派蒙觉醒,展现其强大的力量 -第57章 :丽莎的牺牲 - 丽莎为了保护星河做出牺牲 -第58章 :前往至冬 - 前往至冬,面对新的挑战与阴谋 -第59章 :至冬的阴谋 - 揭露至冬的阴谋,对抗幕后黑手 -第60章 :女皇的算计 - 女皇对星河进行算计,考验其智慧与勇气 -第61章 :机械生命的危机 - 至冬的机械生命失控,引发巨大的危机 -第62章 :星河的策略 - 星河运用策略,化解机械生命的危机 -第63章 :派蒙的秘密力量 - 派蒙展现其强大的秘密力量 -第64章 :丽莎的遗产 - 丽莎留下宝贵的遗产,帮助星河对抗危机 -第65章 :对抗深渊使徒 - 对抗强大的深渊使徒,展现星穹铁道科技的威力 -第66章 :旅行者的选择 - 旅行者面临艰难的选择,需要做出决定 -第67章 :钟离的帮助 - 钟离给予星河关键的帮助,但其目的依旧不明 -第68章 :星河的成长 - 星河在经历种种磨难后得到成长 -第69章 :提瓦特的命运 - 提瓦特的命运掌握在星河手中 -第70章 :最终决战的序幕 - 最终决战的序幕拉开,危机全面爆发 -第71章 :与幕后黑手的对抗 - 与幕后黑手展开激烈的对抗 -第72章 :星河的牺牲 - 星河为了拯救提瓦特做出牺牲 -第73章 :派蒙的付出 - 派蒙为了帮助星河付出一切 -第74章 :丽莎的回归 - 丽莎以另一种方式回归,继续守护蒙德 -第75章 :旅行者的抉择 - 旅行者做出最终的抉择 -第76章 :钟离的真相 - 钟离的真实目的最终揭晓 -第77章 :星河的复活 - 星河以意想不到的方式复活 -第78章 :新的危机 - 新的危机出现,提瓦特面临新的挑战 -第79章 :星河的抉择 - 星河需要做出新的抉择,选择自己的未来 -第80章 :派蒙的未来 - 派蒙的未来充满希望与未知 -第81章 :丽莎的守护 - 丽莎继续守护蒙德,为提瓦特贡献力量 -第82章 :旅行者的旅程 - 旅行者继续自己的旅程,探索提瓦特的奥秘 -第83章 :钟离的隐退 - 钟离选择隐退,默默守护提瓦特 -第84章 :星河的归宿 - 星河思考自己的归宿,是留下还是离开 -第85章 :与伙伴的告别 - 与伙伴们告别,回忆共同经历的冒险 -第86章 :返回星穹铁道? - 星河考虑返回星穹铁道,但心中仍有牵挂 -第87章 :提瓦特的未来 - 提瓦特的未来充满希望与挑战 -第88章 :科技与魔法的融合 - 星穹铁道科技与提瓦特魔法开始融合 -第89章 :新的时代 - 提瓦特进入新的时代,科技与魔法共存 -第90章 :星河的遗产 - 星河留下宝贵的遗产,影响着提瓦特的未来 -第91章 :派蒙的传承 - 派蒙将自己的智慧与经验传承下去 -第92章 :丽莎的教导 - 丽莎继续教导年轻一代,传承魔法的艺术 -第93章 :旅行者的故事 - 旅行者的故事继续,探索更多未知的领域 -第94章 :钟离的箴言 - 钟离留下意味深长的箴言,引导提瓦特的未来 -第95章 :星河的回归 - 星河最终做出决定,选择自己的归宿 -第96章 :告别的时刻 - 与提瓦特的伙伴们告别,心中充满不舍 -第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 -第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 -第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 -第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 deleted file mode 100644 index 340c5cc..0000000 --- a/Src/Novel_setting.txt +++ /dev/null @@ -1,98 +0,0 @@ - -小说设定:《星河挽歌:提瓦特星轨》 - -小说类型:玄幻穿越,史诗级冒险,轻松幽默与细腻情感并存,整体偏正剧,但避免过于沉重。 - -字数:约400万字(100章 x 4000字/章) - -视角:第三人称,穿插主角第一人称内心独白。 - -世界观:基于原神世界线平行宇宙的提瓦特大陆,地理环境与原神基本一致,但部分区域势力分布及历史事件有所调整,以适应主角的到来和剧情发展。七国结构保留,但政治格局和社会风貌因主角介入而变。科技与魔法并存,魔法更为显著,主角的星穹铁道科技作为“外来力量”融入其中,引发冲突和变革。重要历史事件保留原神设定,并添加主角引发的新的历史事件。 - -核心内容梗概:主角星河在星穹铁道执行任务时意外穿越到提瓦特大陆,发现大陆面临史无前例的巨大危机。凭借星穹铁道科技力量及经验,星河成为对抗危机的关键人物。在拯救提瓦特过程中,他将与原神角色相遇,建立深厚情谊或产生冲突,最终完成使命,并决定自己的归宿。采用英雄之旅叙事结构,主线为拯救提瓦特,副线为主角与原神角色的情感纠葛。 - -主要角色: - -1. 星河:坚毅、责任感强、略带孤傲,内心温柔渴望归宿。拥有强大的星穹铁道科技力量,但更倾向于智慧和策略解决问题。拥有丰富战斗经验和冷静判断力,但也有过去任务的阴影和对未来命运的迷茫,偶尔冲动,也可能因自身能力产生优越感。他的目标是拯救提瓦特,找到归宿,弥补过去遗憾。他的隐藏秘密是:在星穹铁道的一次任务中,他导致一位重要伙伴牺牲,这个秘密影响着他与他人的关系,并在与原神角色建立深厚感情时成为关键转折点。 - -2. 派蒙:活泼、贪吃、略显成熟,更能理解星河内心世界,给予更深层次的支持。积极参与冒险,但鲁莽会带来麻烦,是重要的喜剧元素。对提瓦特知识渊博,但对星穹铁道科技一无所知。她的目标是帮助星河拯救提瓦特,探索自身存在秘密,与星河建立更深厚羁绊。她的隐藏秘密是:她并非简单的导航精灵,拥有远超常人的智慧和感知能力,与提瓦特大陆的过去有着神秘联系。这会在中段通过关键事件逐步揭示。 她的“记忆碎片”——偶尔失神,喃喃自语,对特定场景反应强烈,甚至出现幻觉,这些会逐渐清晰,暗示她与提瓦特古老文明甚至超越提瓦特的力量有关。 - -3. 丽莎:慵懒、智慧,强大的魔法能力,比原神设定中更成熟、更富有责任感。对星穹铁道科技好奇又警惕,将星河视为竞争对手和值得培养的对象。她会引导主角,但在关键时刻设置考验。她的目标是保护蒙德,培养星河,并寻求突破自身瓶颈。她的隐藏秘密是:她拥有不为人知的秘密身份,与某个神秘组织有关,该组织与即将到来的危机密切相关。她的“禁忌魔法”——偶尔使用威力远超平时能力的魔法,使用后疲惫甚至身体异样,她会刻意隐藏,这与她的神秘组织联系紧密。 - -4. 原神旅行者:与星河互动,产生冲突与合作。两人对提瓦特大陆的理解不同,可能会产生矛盾,但在对抗危机时携手合作。 - -5. 钟离:神秘幕后人物,引导星河完成使命,但目的和动机不明,会带来考验和帮助,也可能成为潜在敌人。他的“真实目的”——对星河的帮助看似无私,但始终保持距离,言行举止充满深意,他的行动与提瓦特危机有关,目的可能并非简单帮助主角,而是为了实现某种更宏大的计划。 - -角色关系与冲突:星河与派蒙(依赖与独立的矛盾),星河与丽莎(竞争与合作),星河与原神旅行者(身份认同冲突),星河与钟离(考验与引导)。 - -暗线与伏笔:派蒙的记忆碎片,丽莎的禁忌魔法,星河的星穹铁道追捕令,钟离的真实目的,提瓦特大陆的失落科技。这些暗线相互交织,推动剧情发展,并为角色成长和故事走向增添可能性和悬念。 - -结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 - - -世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 - 世界观设定:暗影之歌 - - 引言 -在一个名为提瓦特的大陆上,力量如同黑暗中的呼吸,无声无息地渗透着一切。这片大陆的历史被称为“暗影年代”,那时期充满了权力斗争和悲剧命运。如今,随着“暗影力量”的再次苏醒,新的阴谋与挑战正逐渐显露。 - - 主要角色 -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/Src/outlines/outline_1.txt b/Src/outlines/outline_1.txt deleted file mode 100644 index 1cd6cc6..0000000 --- a/Src/outlines/outline_1.txt +++ /dev/null @@ -1,43 +0,0 @@ - -好,我现在需要帮用户构思小说的第1章大纲,标题是“星河坠落提瓦特”,简述是“穿越与初遇派蒙,危机初现端倪”。首先,我得理解这个标题和简述的意思。看起来这是一部含有玄幻或奇幻元素的小说,可能涉及时间旅行、门派纷争或者某种隐藏的危机。 - -用户给出的内容里提到要围绕本章标题和简述设计详细大纲,并且用户已经提供了一个示例的结构。所以我需要按照那个格式来写,而不是用数字分点。也就是说,先描述每个部分,然后是主要冲突、出现的角色,他们的动机,暗线推进,以及结尾的悬念。 - -首先,本章标题“星河坠落提瓦特”,听起来像是主角从一个繁荣的地方坠落到另一个世界。简述说的是穿越和初遇派蒙,同时危机开始显现。这可能意味着主角从一个平行世界穿越到这个世界,并在门派之间初次相遇,引发了一些危机。 - -接下来,我需要构思主要冲突。可能是主角到达提瓦特城,发现这里有一个古老的力量或秘密,门派之间开始竞争,而主角作为穿越者,带来了改变,同时也面临着被追杀的风险。这样既呼应了标题,又体现了简述中的危机初现。 - -然后是角色部分。可能需要包括主角、对方的门派成员,比如派蒙,以及提瓦特城里的其他关键人物。每个角色在这章里有什么目标和动机,比如追杀主角的人,或者试图掌控力量的人。 - -接下来是推动暗线和悬念的部分。比如,提瓦特城的秘密可能涉及到星河的力量,而派蒙门派对这个力量的争夺可能引发更大的冲突。同时,主角可能带有未知的任务或使命,这会影响后续章节。 - -最后是结尾的悬念,可能是一个转折点,比如主角揭示了某个秘密,或者派蒙开始怀疑自己,而星河的力量在台面上或下面开始显现,为后续剧情埋下伏笔。 - -整体来看,我需要确保大纲紧扣标题和简述,同时为后续章节留有余地。可能还要注意角色的性格发展和潜在冲突,以保持故事的吸引力。 - - -### 第 1 章:星河坠落提瓦特 - -#### 主要冲突: -- **星河降临**:穿越者从一个繁荣世纪跨越到提瓦特大陆,带来了“星河”般的强大力量,引发了提瓦特城内外的关注与恐慌。 -- **门派初遇**:派蒙门派首领派长派出追杀队伍,试图抓住这股未知力量,以掌控“星河之力”。 -- **危机初现**:一场隐藏已久的秘密势力开始苏醒,在提瓦特城内外的阴谋逐渐浮现,为后续的灾难埋下伏笔。 - -#### 角色出现: -1. **主角(未命名)**:穿越者,带着星河般的强大力量,性格冷静果敢,却对自己的来历一无所知。 -2. **派长**:派蒙门派领袖,精通古老法术,眼光独到,对“星河之力”嗜求不已。 -3. **追杀队伍**:派长派出的十名绝技高手,打着门派的旗号,在提瓦特城内外肆虐。 -4. **提瓦特市民**:生活在平静世代的普通百姓,见证并参与这场突如其来的震撼。 -5. **神秘老者**:提瓦特城中的隐世高人,对“星河之力”知之甚详,暗中观察与等待。 - -#### 推动暗线: -- **星河力量的真相**:主角身上蕴含的力量竟是千年前就被预言的“星河之力”,而提瓦特城内外的秘密势力对此虎视眈眈。 -- **派蒙门派的野心**:派长不仅想要获取“星河之力”,还暗藏着更大的图谋,试图借此重塑整个世界。 -- **神秘老者的警告**:一位隐世高人试图接触主角,给出“星河之力”与世界危机的隐喻,为后续剧情埋下重大伏笔。 - -#### 结尾悬念: -- **力量暴露**:在一次追杀中,主角无意间释放出“星河之力”,震撼整个提瓦特城,引发更大的关注与恐慌。 -- **派长的挣扎**:派长对自己的能力和未来感到迷茫,开始怀疑自己是否已经被力量的猎头所误导。 -- **神秘老者的突然消失**:一位神秘老者在关键时刻悄然离去,为后续追杀埋下更多谜团。 - -通过这场开篇的震撼,未命名的主角将从一个普通穿越者逐渐成长为“星河”的继承者,而提瓦特城与整个世界也将迎来一场前所未有的巨变。 \ No newline at end of file diff --git a/embedding_ollama.py b/embedding_ollama.py index 8dd6dea..5538c87 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -11,7 +11,7 @@ class OllamaEmbeddings: def __init__(self, model_name: str, base_url: str): self.model_name = model_name - self.base_url = base_url # 这里一般形如 http://localhost:11434/api (而非 /v1) + self.base_url = base_url def embed(self, texts: List[str]) -> List[List[float]]: """ diff --git a/novel_generator.py b/novel_generator.py index 0b01ac6..407779a 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -5,11 +5,13 @@ import logging import re from typing import Dict, List, Optional from typing import TypedDict + from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.docstore.document import Document + import nltk import math from sentence_transformers import SentenceTransformer @@ -32,9 +34,30 @@ 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 remove_think_tags(text: str) -> str: + """ + 移除 ... 包裹的内容 + """ + return re.sub(r'.*?', '', text, flags=re.DOTALL) + +def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str: + """ + 通用封装:调用模型并移除 ... 文本,记录日志后返回 + """ + response = model.invoke(prompt) + if not response: + logging.warning("No response from model.") + return "" + cleaned_text = remove_think_tags(response.content) + debug_log(prompt, cleaned_text) + return cleaned_text.strip() + # ============ 判断接口格式相关 ============ def is_using_ollama_api(interface_format: str, base_url: str) -> bool: @@ -272,34 +295,22 @@ def Novel_novel_directory_generate( number_of_chapters=state["number_of_chapters"], word_number=state["word_number"] ) - response = model.invoke(prompt) - if not response: - logging.warning("generate_base_setting: No response.") - return {"novel_setting_base": ""} - debug_log(prompt, response.content) - return {"novel_setting_base": response.content.strip()} + result_text = invoke_with_cleaning(model, prompt) + return {"novel_setting_base": result_text} def generate_character_setting(state: OverallState) -> Dict[str, str]: prompt = character_prompt.format( novel_setting=state["novel_setting_base"] ) - response = model.invoke(prompt) - if not response: - logging.warning("generate_character_setting: No response.") - return {"character_setting": ""} - debug_log(prompt, response.content) - return {"character_setting": response.content.strip()} + result_text = invoke_with_cleaning(model, prompt) + return {"character_setting": result_text} def generate_dark_lines(state: OverallState) -> Dict[str, str]: prompt = dark_lines_prompt.format( character_info=state["character_setting"] ) - response = model.invoke(prompt) - if not response: - logging.warning("generate_dark_lines: No response.") - return {"dark_lines": ""} - debug_log(prompt, response.content) - return {"dark_lines": response.content.strip()} + result_text = invoke_with_cleaning(model, prompt) + return {"dark_lines": result_text} def finalize_novel_setting_func(state: OverallState) -> Dict[str, str]: prompt = finalize_setting_prompt.format( @@ -307,24 +318,16 @@ def Novel_novel_directory_generate( character_setting=state["character_setting"], dark_lines=state["dark_lines"] ) - response = model.invoke(prompt) - if not response: - logging.warning("finalize_novel_setting: No response.") - return {"final_novel_setting": ""} - debug_log(prompt, response.content) - return {"final_novel_setting": response.content.strip()} + result_text = invoke_with_cleaning(model, prompt) + return {"final_novel_setting": result_text} def generate_novel_directory_func(state: OverallState) -> Dict[str, str]: prompt = novel_directory_prompt.format( final_novel_setting=state["final_novel_setting"], number_of_chapters=state["number_of_chapters"] ) - response = model.invoke(prompt) - if not response: - logging.warning("generate_novel_directory: No response.") - return {"novel_directory": ""} - debug_log(prompt, response.content) - return {"novel_directory": response.content.strip()} + result_text = invoke_with_cleaning(model, prompt) + return {"novel_directory": result_text} graph = StateGraph(OverallState) graph.add_node("generate_base_setting", generate_base_setting) @@ -410,7 +413,6 @@ def summarize_recent_chapters( """ if not chapters_text_list: return "" - # 如果列表里全是空,则无法生成摘要 if all(not txt.strip() for txt in chapters_text_list): return "暂无摘要。" @@ -427,11 +429,11 @@ def summarize_recent_chapters( 请用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点:""" - response = model.invoke(prompt) - if not response or not response.content.strip(): + summary_text = invoke_with_cleaning(model, prompt) + if not summary_text: # 若模型无响应,就截取一段作为“备选” return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text - return response.content.strip() + return summary_text # ============ 新增:剧情要点/未解决冲突 ============ @@ -465,11 +467,11 @@ def update_plot_arcs( chapter_text=chapter_text, old_plot_arcs=old_plot_arcs ) - response = model.invoke(prompt) - if not response: - logging.warning("update_plot_arcs: No response.") + arcs_text = invoke_with_cleaning(model, prompt) + if not arcs_text: + logging.warning("update_plot_arcs: No response or empty result.") return old_plot_arcs - return response.content.strip() + return arcs_text # ============ 生成章节草稿 & 定稿 ============ @@ -542,8 +544,7 @@ def generate_chapter_draft( 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) - chapter_outline = response_outline.content.strip() if response_outline else "" + chapter_outline = invoke_with_cleaning(model, outline_prompt_text) outlines_dir = os.path.join(filepath, "outlines") os.makedirs(outlines_dir, exist_ok=True) @@ -564,8 +565,7 @@ def generate_chapter_draft( 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) - chapter_content = response_chapter.content.strip() if response_chapter else "" + chapter_content = invoke_with_cleaning(model, writing_prompt_text) chapters_dir = os.path.join(filepath, "chapters") os.makedirs(chapters_dir, exist_ok=True) @@ -637,8 +637,7 @@ def finalize_chapter( chapter_text=chapter_text, global_summary=old_summary ) - response = model.invoke(prompt) - return response.content.strip() if response else old_summary + return invoke_with_cleaning(model, prompt) or old_summary new_global_summary = update_global_summary(chapter_text, old_global_summary) @@ -648,8 +647,7 @@ def finalize_chapter( chapter_text=chapter_text, old_state=old_state ) - response = model.invoke(prompt) - return response.content.strip() if response else old_state + return invoke_with_cleaning(model, prompt) or old_state new_char_state = update_character_state(chapter_text, old_char_state) @@ -705,11 +703,8 @@ def enrich_chapter_text( 原章节内容: {chapter_text}""" - - response = model.invoke(prompt) - if not response: - return chapter_text - return response.content.strip() + enriched_text = invoke_with_cleaning(model, prompt) + return enriched_text if enriched_text else chapter_text # ============ 导入外部知识文本 ============ @@ -734,7 +729,8 @@ def import_knowledge_file( logging.warning("知识库文件内容为空。") return - nltk.download('punkt_tab', quiet=True) + # 改为下载 punkt_tab + nltk.download('punkt', quiet=True) paragraphs = advanced_split_content(content) diff --git a/ui.py b/ui.py index 43fe10c..bbf7767 100644 --- a/ui.py +++ b/ui.py @@ -33,7 +33,7 @@ ctk.set_default_color_theme("blue") class NovelGeneratorGUI: def __init__(self, master): self.master = master - self.master.title("Novel Generator GUI (CustomTkinter)") + self.master.title("Novel Generator GUI") # 防止因 icon.ico 不存在导致程序崩溃 try: From bc6e2ff869a79c8645519e6d6aa1445f6511c1c2 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 19:22:54 +0800 Subject: [PATCH 10/11] =?UTF-8?q?UI=E6=9B=B4=E6=96=B0=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=9C=AC=E5=9C=B0=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- embedding_ollama.py | 3 ++- novel_generator.py | 21 +++++---------------- ui.py | 31 ++++++++++++++++--------------- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/embedding_ollama.py b/embedding_ollama.py index 5538c87..c7979be 100644 --- a/embedding_ollama.py +++ b/embedding_ollama.py @@ -1,5 +1,6 @@ # embedding_ollama.py import requests +import traceback from typing import List class OllamaEmbeddings: @@ -51,4 +52,4 @@ class OllamaEmbeddings: raise ValueError("No 'embedding' field in Ollama response.") return result["embedding"] except requests.exceptions.RequestException as e: - raise Exception(f"Ollama embeddings request error: {e}") + raise Exception(f"Ollama embeddings request error: {e}\n{traceback.format_exc()}") diff --git a/novel_generator.py b/novel_generator.py index 407779a..44b96d3 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -3,6 +3,7 @@ import os import logging import re +import traceback from typing import Dict, List, Optional from typing import TypedDict @@ -59,25 +60,19 @@ def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str: return cleaned_text.strip() # ============ 判断接口格式相关 ============ - 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 + return interface_format.lower() == "ollama" 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 + return interface_format.lower() == "ml studio" # ============ 创建 Embeddings 对象 ============ - def create_embeddings_object( api_key: str, base_url: str, @@ -106,7 +101,6 @@ def create_embeddings_object( return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url) # ============ 向量库相关 ============ - VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore") if not os.path.exists(VECTOR_STORE_DIR): os.makedirs(VECTOR_STORE_DIR) @@ -125,8 +119,8 @@ def clear_vector_store(): elif os.path.isdir(file_path): shutil.rmtree(file_path) logging.info("Local vector store has been cleared.") - except Exception as e: - logging.warning(f"Failed to clear vector store: {e}") + except Exception: + logging.warning(f"Failed to clear vector store:\n{traceback.format_exc()}") else: logging.info("No vector store found to clear.") @@ -253,7 +247,6 @@ def get_relevant_context_from_vector_store( return combined # ============ 多步生成:设置 & 目录 ============ - class OverallState(TypedDict): topic: str genre: str @@ -383,7 +376,6 @@ 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]: """ 从指定文件夹中,读取最近 n 章的内容(如果存在),并按从旧到新的顺序返回文本列表。 @@ -436,7 +428,6 @@ def summarize_recent_chapters( return summary_text # ============ 新增:剧情要点/未解决冲突 ============ - PLOT_ARCS_PROMPT = """\ 下面是新生成的章节内容: {chapter_text} @@ -474,7 +465,6 @@ def update_plot_arcs( return arcs_text # ============ 生成章节草稿 & 定稿 ============ - def generate_chapter_draft( novel_settings: str, global_summary: str, @@ -707,7 +697,6 @@ def enrich_chapter_text( return enriched_text if enriched_text else chapter_text # ============ 导入外部知识文本 ============ - def import_knowledge_file( api_key: str, base_url: str, diff --git a/ui.py b/ui.py index bbf7767..cbd371f 100644 --- a/ui.py +++ b/ui.py @@ -95,6 +95,12 @@ class NovelGeneratorGUI: # 全局摘要 Tab self.build_summary_tab() + # ------------------ 统一异常处理方法 ------------------ + def handle_exception(self, context: str): + full_message = f"{context}\n{traceback.format_exc()}" + logging.error(full_message) + self.safe_log(full_message) + # ------------------ 主功能 Tab ------------------ def build_main_tab(self): self.main_tab.rowconfigure(0, weight=1) @@ -617,9 +623,8 @@ class NovelGeneratorGUI: temperature=temperature ) self.safe_log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") - except Exception as e: - log_error(f"生成小说设定 & 目录时出错: {e}") - self.safe_log(f"❌ 生成小说设定 & 目录时出错: {e}") + except Exception: + self.handle_exception("生成小说设定 & 目录时出错") finally: self.enable_button_safe(self.btn_generate_full) @@ -695,9 +700,8 @@ class NovelGeneratorGUI: else: self.safe_log("⚠️ 本章草稿生成失败或无内容。") - except Exception as e: - log_error(f"生成章节草稿时出错: {e}") - self.safe_log(f"❌ 生成章节草稿时出错: {e}") + except Exception: + self.handle_exception("生成章节草稿时出错") finally: self.enable_button_safe(self.btn_generate_chapter) @@ -747,9 +751,8 @@ class NovelGeneratorGUI: final_text = read_file(chap_file) self.master.after(0, lambda: self.show_chapter_in_textbox(final_text)) - except Exception as e: - log_error(f"定稿章节时出错: {e}") - self.safe_log(f"❌ 定稿章节时出错: {e}") + except Exception: + self.handle_exception("定稿章节时出错") finally: self.enable_button_safe(self.btn_finalize_chapter) @@ -803,9 +806,8 @@ class NovelGeneratorGUI: self.safe_log("审校结果:") self.safe_log(result) - except Exception as e: - log_error(f"审校时出错: {e}") - self.safe_log(f"❌ 审校时出错: {e}") + except Exception: + self.handle_exception("审校时出错") finally: self.enable_button_safe(self.btn_check_consistency) @@ -831,9 +833,8 @@ class NovelGeneratorGUI: embedding_base_url=self.embedding_url_var.get().strip() ) self.safe_log("✅ 知识库文件导入完成。") - except Exception as e: - log_error(f"导入知识库时出错: {e}") - self.safe_log(f"❌ 导入知识库时出错: {e}") + except Exception: + self.handle_exception("导入知识库时出错") finally: self.enable_button_safe(self.btn_import_knowledge) From fd3dc33b9d2b3ab7dce6965354f03402d86bdbb2 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Sun, 2 Feb 2025 19:28:21 +0800 Subject: [PATCH 11/11] 223 --- novel_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novel_generator.py b/novel_generator.py index 44b96d3..3c4722d 100644 --- a/novel_generator.py +++ b/novel_generator.py @@ -718,8 +718,8 @@ def import_knowledge_file( logging.warning("知识库文件内容为空。") return - # 改为下载 punkt_tab nltk.download('punkt', quiet=True) + nltk.download('punkt_tab', quiet=True) paragraphs = advanced_split_content(content)