不知道出了什么问题,先记录一下更新

This commit is contained in:
YILING0013
2025-02-02 17:41:16 +08:00
parent 7741345ee1
commit aca9e48683
5 changed files with 168 additions and 431 deletions
-182
View File
@@ -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章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局
<think>
嗯,我需要按照用户提供的《小说设定》来生成100章的目录。首先,我得仔细阅读并理解这个设定,确保每一章都紧扣主题和叙事结构。
世界观设定:暗影之歌
在提瓦特大陆上,暗影力量正在苏醒,带来了新的挑战和阴谋。主要角色包括星辰、艾莎、凯文,以及暗影力量本身。故事探讨了力量与责任、身份与命运、信任与背叛等主题。
目录模板
第1章:<text> - <简述>
...
第100章:<text> - <简述>
接下来,我需要规划每一章的内容,确保涵盖起始点、初识艾莎、凯文的动机、危机初现、真相浮现、高潮对决以及开放式结局。
起始点
星辰被选中成为暗影力量的继承者,但她对自己的能力感到不安,同时怀疑“暗影力量”是否真是保护大陆的存在。这一部分需要引入角色和设定,铺垫后续的冲突。
初识艾莎
艾莎以刺客的身份接近星辰,试图利用她的暗影能力。两人合作中发现彼此的共同点和隐藏的冲突,这可以增加角色的互动和紧张感。
凯文的动机
凯文表面上支持星辰和艾莎,但实际上有自己的秘密。他可能是“暗影力量”的后代,或者另有隐情。这部分需要揭示凯文的多面性和潜在威胁。
危机初现
随着暗影力量的觉醒,大陆出现诡异事件。暗影力量失控,普通民众受害。这可以是故事转折点,展示力量带来的危险。
真相浮现
通过对凯文、艾莎和星辰的深入探索,揭示他们的过去与命运联系。他们之间存在更大的联系,关系到暗影力量的未来。
高潮对决
关键时刻,星辰和艾莎必须共同面临强敌。这场对决是力量的较量,更是信任与背叛的考验,增加戏剧性和紧张感。
结局模糊
故事留下开放结局,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路,或者被力量吞噬。
在规划每一章时,我需要确保情节连贯,角色发展合理,同时埋下伏笔,为后续章节做好铺垫。例如,在初识艾莎的阶段,可以设置她的多面性和潜在的秘密,为后期冲突做准备。
此外,每章的标题应简洁明了,能够反映主要情节或角色的发展,并附上简短的提示性简述,帮助读者了解每一章的内容。
现在,我需要将这些思考整理成一个100章的目录,确保每一章都紧扣设定中的叙事结构和角色发展,同时埋下足够的暗线伏笔。
</think>
第1章 :星辰的选择 - 星辰被选中为暗影力量的继承者,但内心充满疑惑与不安。
第2章 :暗影之息 - 星辰在练习中意外触发暗影力量,引发混乱,开始探索自己的能力边界。
第3章 :艾莎的来临 - 艾莎以刺客身份接近星辰,试图获取更多关于暗影力量的信息,同时对星辰产生复杂情感。
第4章 :凯文的真实意图 - 凯文表面上支持星辰和艾莎,但逐渐显露出多面性和隐藏的秘密,引起星辰的警觉。
第5章:暗影失控 - 随着暗影力量的强化,大陆出现一系列诡异事件,普通民众受害,暗影力量开始失控。
第6章:艾莎的内心挣扎 - 艾莎在完成任务后,发现自己逐渐被暗影力量控制,对自己的选择感到恐慌和纠结。
第7章:凯文的秘密揭露 - 凯文被迫公开他的多面性和隐藏的秘密,星辰和艾莎联手调查他的真实意图。
第8章:命运的交错 - 通过对凯文、艾莎和星辰的深入了解,逐渐揭示他们之间存在更大的联系关系到暗影力量的未来走向。
第9章:危机加剧 - 凯文与暗影力量的势力开始对星辰和艾莎构成直接威胁,两人必须面临更为严峻的挑战。
第10章:信任的考验 - 在关键时刻,星辰和艾莎之间的信任面临突破,但也隐藏着潜在的背叛阴谋。
第11章:暗影力量的真相 - 星辰通过回忆和资料,逐渐揭开暗影力量的起源和其对世界的影响。
第12章:凯文的最终动向 - 凯文的双重身份和真实意图被彻底揭露,他的选择将对暗影力量的未来产生重大影响。
第13章:星辰的觉醒 - 星辰在与艾莎的合作中逐渐意识到自己肩负的责任,开始寻找解决问题的方法。
第14章:艾莎的挣扎 - 艾莎被迫面对自己的过去和现在的选择,她是否能站出来对抗暗影力量?
第15章:凯文的覆灭 - 凯文最终的命运如何?他的计划是否能够成功,或者他将成为阻碍?
第16章:暗影之歌 - 星辰和艾莎联手面临最后的危机,他们需要找到一个办法来控制暗影力量。
第17章:力量与责任的抉择 - 在关键时刻,星辰和艾莎必须做出艰难的决定,选择自己的道路还是被力量吞噬。
第18章:命运的终结 - 故事进入高潮,暗影力量的威胁更加严峻,星辰和艾莎必须团结起来对抗它。
第19章:最后的对决 - 星辰和艾莎面临最终的对决,他们的能力和信任将决定世界的命运。
第20章:暗影之光 - 故事情节以开放式结束,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路?
-98
View File
@@ -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. 结局的模糊性
- 故事情节留下了开放的结局,让读者思考暗影力量最终会带来什么样的未来。星辰和艾莎是否能找到自己的道路,或者他们将被暗影力量彻底吞噬。
主题
- 力量与责任:暗影力量赋予了一些人巨大的能力,但同时也带来了沉重的责任。
- 身份与命运:每个人的过去决定了他们的未来,他们是否能超越自己的宿命?
- 信任与背叛:在危难时刻,真实的朋友和潜在的敌人是如何区分的?
总结
“暗影之歌”是一个充满矛盾与复杂性的故事,它探讨了力量、身份、信任等多个深刻主题。通过星辰和艾莎两位主角的视角,读者可以感受到在黑暗与光明之间挣扎的人性,以及他们如何面对命运给予的挑战。
+25 -8
View File
@@ -1,14 +1,9 @@
# embedding_ollama.py import logging
import requests import requests
from typing import List from typing import List
import traceback
class OllamaEmbeddings: class OllamaEmbeddings:
"""
Ollama 本地服务提供的 Embedding 接口,
本需求里我们最终拼出形如: http://localhost:11434/api/embed
即 base_url + "/embed"
"""
def __init__(self, model_name: str, base_url: str): def __init__(self, model_name: str, base_url: str):
self.model_name = model_name 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)
@@ -49,6 +44,28 @@ class OllamaEmbeddings:
response = requests.post(url, json=data) response = requests.post(url, json=data)
response.raise_for_status() response.raise_for_status()
result = response.json() 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"] return result["embedding"]
except requests.exceptions.RequestException as e: 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 []
+91 -86
View File
@@ -4,17 +4,12 @@ import os
import logging import logging
import re import re
from typing import Dict, List, Optional from typing import Dict, List, Optional
try: from typing import TypedDict
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langchain_openai import OpenAIEmbeddings from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma from langchain_community.vectorstores import Chroma
from langchain.docstore.document import Document from langchain.docstore.document import Document
import nltk import nltk
import math import math
from sentence_transformers import SentenceTransformer from sentence_transformers import SentenceTransformer
@@ -34,15 +29,15 @@ from embedding_ollama import OllamaEmbeddings
from chapter_directory_parser import get_chapter_info_from_directory from chapter_directory_parser import get_chapter_info_from_directory
# ============ 日志配置 ============ # ============ 日志配置 ============
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
def debug_log(prompt: str, response_content: str): def debug_log(prompt: str, response_content: str):
"""打印Prompt与Response,可根据需要保留或去掉。"""
logging.info(f"\n[Prompt >>>] {prompt}\n") logging.info(f"\n[Prompt >>>] {prompt}\n")
logging.info(f"[Response >>>] {response_content}\n") logging.info(f"[Response >>>] {response_content}\n")
# ============ 判断接口格式相关 ============
# ============ 接口判断函数 ============
def is_using_ollama_api(interface_format: str, base_url: str) -> bool: def is_using_ollama_api(interface_format: str, base_url: str) -> bool:
""" """
当 interface_format == "Ollama" 时返回 True 当 interface_format == "Ollama" 时返回 True
@@ -60,6 +55,8 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool:
return False return False
# ============ 创建 Embeddings 对象 ============
def create_embeddings_object( def create_embeddings_object(
api_key: str, api_key: str,
base_url: str, base_url: str,
@@ -72,27 +69,25 @@ def create_embeddings_object(
- 当 interface_format = "Ollama" => OllamaEmbeddings(...) - 当 interface_format = "Ollama" => OllamaEmbeddings(...)
(此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed (此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed
- 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings - 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings
- 其它情况可自行扩展 - 其它情况视需求可扩展
""" """
if is_using_ollama_api(interface_format, embed_url): if is_using_ollama_api(interface_format, embed_url):
# 去除末尾斜杠
fixed_url = embed_url.rstrip("/") fixed_url = embed_url.rstrip("/")
# 如果包含 /v1 则替换为 /api
fixed_url = fixed_url.replace("/v1", "/api") fixed_url = fixed_url.replace("/v1", "/api")
return OllamaEmbeddings( return OllamaEmbeddings(
model_name=embedding_model_name, model_name=embedding_model_name,
base_url=fixed_url base_url=fixed_url
) )
elif is_using_ml_studio_api(interface_format, base_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) return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
else: else:
# 默认使用 OpenAIEmbeddings # 默认使用 OpenAIEmbeddings
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url) 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") VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore")
if not os.path.exists(VECTOR_STORE_DIR): if not os.path.exists(VECTOR_STORE_DIR):
os.makedirs(VECTOR_STORE_DIR) os.makedirs(VECTOR_STORE_DIR)
@@ -102,8 +97,8 @@ def clear_vector_store():
清空本地向量库(删除 vectorstore 文件夹内的内容)。 清空本地向量库(删除 vectorstore 文件夹内的内容)。
""" """
if os.path.exists(VECTOR_STORE_DIR): if os.path.exists(VECTOR_STORE_DIR):
try:
import shutil import shutil
try:
for filename in os.listdir(VECTOR_STORE_DIR): for filename in os.listdir(VECTOR_STORE_DIR):
file_path = os.path.join(VECTOR_STORE_DIR, filename) file_path = os.path.join(VECTOR_STORE_DIR, filename)
if os.path.isfile(file_path) or os.path.islink(file_path): if os.path.isfile(file_path) or os.path.islink(file_path):
@@ -126,7 +121,6 @@ def init_vector_store(
) -> Chroma: ) -> Chroma:
""" """
初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。
embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。
""" """
embed_url = embedding_base_url if embedding_base_url else base_url embed_url = embedding_base_url if embedding_base_url else base_url
embeddings = create_embeddings_object( embeddings = create_embeddings_object(
@@ -156,6 +150,7 @@ def load_vector_store(
读取已存在的向量库。若不存在则返回 None。 读取已存在的向量库。若不存在则返回 None。
""" """
if not os.path.exists(VECTOR_STORE_DIR): if not os.path.exists(VECTOR_STORE_DIR):
logging.info("Vector store not found. Initializing a new one...")
return None return None
embed_url = embedding_base_url if embedding_base_url else base_url embed_url = embedding_base_url if embedding_base_url else base_url
embeddings = create_embeddings_object( embeddings = create_embeddings_object(
@@ -185,8 +180,10 @@ def update_vector_store(
embedding_model_name=embedding_model_name, embedding_model_name=embedding_model_name,
embedding_base_url=embedding_base_url embedding_base_url=embedding_base_url
) )
# 如果向量库不存在,初始化它
if not store: 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( init_vector_store(
api_key=api_key, api_key=api_key,
base_url=base_url, base_url=base_url,
@@ -200,6 +197,7 @@ def update_vector_store(
new_doc = Document(page_content=new_chapter) new_doc = Document(page_content=new_chapter)
store.add_documents([new_doc]) store.add_documents([new_doc])
store.persist() store.persist()
logging.info("Vector store updated with the new chapter.")
def get_relevant_context_from_vector_store( def get_relevant_context_from_vector_store(
api_key: str, api_key: str,
@@ -212,7 +210,7 @@ def get_relevant_context_from_vector_store(
) -> str: ) -> str:
""" """
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
若向量库不存在则返回空字符串。 若向量库不存在或没有足够的内容,则返回空字符串。
""" """
store = load_vector_store( store = load_vector_store(
api_key=api_key, api_key=api_key,
@@ -221,10 +219,19 @@ def get_relevant_context_from_vector_store(
embedding_model_name=embedding_model_name, embedding_model_name=embedding_model_name,
embedding_base_url=embedding_base_url embedding_base_url=embedding_base_url
) )
# 如果向量库为空,直接返回空字符串
if not store: if not store:
logging.warning("Vector store not found. Returning empty context.") logging.info("No vector store found. Returning empty context.")
return "" return ""
# 向量库存在,但没有足够的内容时也避免索引错误
docs = store.similarity_search(query, k=k) 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]) combined = "\n".join([d.page_content for d in docs])
return combined return combined
@@ -256,7 +263,6 @@ def Novel_novel_directory_generate(
""" """
使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。 使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。
""" """
# 确保文件夹存在
os.makedirs(filepath, exist_ok=True) os.makedirs(filepath, exist_ok=True)
model = ChatOpenAI( model = ChatOpenAI(
@@ -327,7 +333,6 @@ def Novel_novel_directory_generate(
debug_log(prompt, response.content) debug_log(prompt, response.content)
return {"novel_directory": response.content.strip()} return {"novel_directory": response.content.strip()}
# 构建状态图
graph = StateGraph(OverallState) graph = StateGraph(OverallState)
graph.add_node("generate_base_setting", generate_base_setting) graph.add_node("generate_base_setting", generate_base_setting)
graph.add_node("generate_character_setting", generate_character_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。") logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。")
return return
# 写入文件
filename_set = os.path.join(filepath, "Novel_setting.txt") filename_set = os.path.join(filepath, "Novel_setting.txt")
filename_novel_directory = os.path.join(filepath, "Novel_directory.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_setting_cleaned, filename_set)
append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) append_text_to_file(final_novel_directory_cleaned, filename_novel_directory)
logging.info("Novel settings and directory generated successfully.") 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() text = read_file(chap_file).strip()
if text: if text:
texts.append(text) texts.append(text)
if len(texts) < n:
texts = [''] * (n - len(texts)) + texts
return texts return texts
def summarize_recent_chapters( def summarize_recent_chapters(
llm_model: str, llm_model: str,
api_key: str, api_key: str,
base_url: str, base_url: str,
temperature: float, temperature: float,
chapters_text_list: List[str] chapters_text_list: List[str]
) -> str: ) -> str:
""" """
将最近几章文本拼接,通过模型生成一个相对详细的“短期内容摘要”。 将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。
如果没有可用的模型(model=None),则退化为简单截断示例。
""" """
if not chapters_text_list:
return ""
if chapters_text_list==['', '', '']:
return "暂无摘要。"
model = ChatOpenAI( model = ChatOpenAI(
model=llm_model, model=llm_model,
api_key=api_key, api_key=api_key,
@@ -414,33 +423,19 @@ def summarize_recent_chapters(
temperature=temperature temperature=temperature
) )
if not chapters_text_list:
return ""
combined_text = "\n".join(chapters_text_list) combined_text = "\n".join(chapters_text_list)
# 如果未传入model,就做个简单的退化输出 prompt = f"""你是一名资深长篇小说写作辅助AI,下面是最近几章的合并文本:
if not model:
return f"【摘要-演示】\n{combined_text[:800]}..."
# 构造一个提示词(Prompt),指示模型生成精简摘要
prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容:
{combined_text} {combined_text}
你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点 用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点"""
1.请用中文输出,不超过500字。
2.仅回复摘要内容,不需要其他信息。
"""
# 调用模型获取摘要
response = model.invoke(prompt) response = model.invoke(prompt)
if not response or not response.content.strip(): 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() return response.content.strip()
# ============ 新增:更新剧情要点/未解决冲突 ============ # ============ 新增:剧情要点/未解决冲突 ============
PLOT_ARCS_PROMPT = """\ PLOT_ARCS_PROMPT = """\
下面是新生成的章节内容: 下面是新生成的章节内容:
@@ -449,9 +444,9 @@ PLOT_ARCS_PROMPT = """\
这里是已记录的剧情要点/未解决冲突(可能为空): 这里是已记录的剧情要点/未解决冲突(可能为空):
{old_plot_arcs} {old_plot_arcs}
请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。 请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。
若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。 若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。
最终输出一份更新后的剧情要点列表,以帮助后续保持故事整体一致性和悬念延续。 最终输出更新后的剧情要点列表,以帮助后续保持故事整体一致性和悬念延续。
""" """
def update_plot_arcs( def update_plot_arcs(
@@ -462,10 +457,6 @@ def update_plot_arcs(
model_name: str, model_name: str,
temperature: float temperature: float
) -> str: ) -> str:
"""
利用模型分析最新章节文本,提炼或更新“未解决冲突或剧情要点”。
并返回更新后的字符串。
"""
model = ChatOpenAI( model = ChatOpenAI(
model=model_name, model=model_name,
api_key=api_key, api_key=api_key,
@@ -480,7 +471,6 @@ def update_plot_arcs(
if not response: if not response:
logging.warning("update_plot_arcs: No response.") logging.warning("update_plot_arcs: No response.")
return old_plot_arcs return old_plot_arcs
debug_log(prompt, response.content)
return response.content.strip() return response.content.strip()
@@ -499,28 +489,44 @@ def generate_chapter_draft(
word_number: int, word_number: int,
temperature: float, temperature: float,
novel_novel_directory: str, novel_novel_directory: str,
filepath: str filepath: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str
) -> 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_info = get_chapter_info_from_directory(novel_novel_directory, novel_number)
chapter_title = chapter_info["chapter_title"] chapter_title = chapter_info["chapter_title"]
chapter_brief = chapter_info["chapter_brief"] chapter_brief = chapter_info["chapter_brief"]
# 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情") # 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并)
relevant_context = get_relevant_context_from_vector_store( 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, api_key=api_key,
base_url=base_url, base_url=base_url,
query="回顾剧情", query=q,
interface_format="OpenAI", interface_format=interface_format,
embedding_model_name="", embedding_model_name=embedding_model_name,
embedding_base_url="", embedding_base_url=embedding_base_url,
k=2 k=2
) )
if partial_context.strip():
relevant_context += "\n" + partial_context
# 如果检索结果为空,使用默认值(如空字符串)
if not relevant_context:
relevant_context = "暂无相关内容。"
model = ChatOpenAI( model = ChatOpenAI(
model=model_name, model=model_name,
@@ -529,10 +535,10 @@ def generate_chapter_draft(
temperature=temperature temperature=temperature
) )
# 2) 生成大纲 # 1) 生成本章大纲
outline_prompt_text = chapter_outline_prompt.format( outline_prompt_text = chapter_outline_prompt.format(
novel_setting=novel_settings, 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, global_summary=global_summary,
novel_number=novel_number, novel_number=novel_number,
chapter_title=chapter_title, chapter_title=chapter_title,
@@ -550,10 +556,10 @@ def generate_chapter_draft(
clear_file_content(outline_file) clear_file_content(outline_file)
save_string_to_txt(chapter_outline, outline_file) save_string_to_txt(chapter_outline, outline_file)
# 3) 生成正文草稿 # 2) 生成正文草稿
writing_prompt_text = chapter_write_prompt.format( writing_prompt_text = chapter_write_prompt.format(
novel_setting=novel_settings, 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, global_summary=global_summary,
chapter_outline=chapter_outline, chapter_outline=chapter_outline,
word_number=word_number, word_number=word_number,
@@ -588,13 +594,12 @@ def finalize_chapter(
): ):
""" """
对当前章节进行定稿: 对当前章节进行定稿:
1. 读取 chapter_{novel_number}.txt 的最终内容; 1. 读取草稿文本
2. 更新全局摘要、角色状态文件; 2. 若字数太短则再次扩写
3. 如果字数明显少于 word_number 的 80%,则自动调用 enrich_chapter_text 再次扩写; 3. 更新全局摘要、角色状态
4. 更新向量库; 4. 更新剧情要点
5. 新增:更新剧情要点/未解决冲突 -> plot_arcs.txt 5. 更新向量库
""" """
# 读取当前章节内容
chapters_dir = os.path.join(filepath, "chapters") chapters_dir = os.path.join(filepath, "chapters")
chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt") chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt")
chapter_text = read_file(chapter_file).strip() chapter_text = read_file(chapter_file).strip()
@@ -610,9 +615,9 @@ def finalize_chapter(
old_global_summary = read_file(global_summary_file) old_global_summary = read_file(global_summary_file)
old_plot_arcs = read_file(plot_arcs_file) old_plot_arcs = read_file(plot_arcs_file)
# 1) 若字数明显不足,做 enrich # 若篇幅过短,二次扩写
if len(chapter_text) < 0.8 * word_number: 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 = enrich_chapter_text(
chapter_text=chapter_text, chapter_text=chapter_text,
word_number=word_number, word_number=word_number,
@@ -623,9 +628,8 @@ def finalize_chapter(
) )
clear_file_content(chapter_file) clear_file_content(chapter_file)
save_string_to_txt(chapter_text, chapter_file) save_string_to_txt(chapter_text, chapter_file)
logging.info("Chapter text has been enriched and updated.")
# 2) 更新全局摘要 # 更新全局摘要
model = ChatOpenAI( model = ChatOpenAI(
model=model_name, model=model_name,
api_key=api_key, api_key=api_key,
@@ -643,7 +647,7 @@ def finalize_chapter(
new_global_summary = update_global_summary(chapter_text, old_global_summary) new_global_summary = update_global_summary(chapter_text, old_global_summary)
# 3) 更新角色状态 # 更新角色状态
def update_character_state(chapter_text: str, old_state: str) -> str: def update_character_state(chapter_text: str, old_state: str) -> str:
prompt = update_character_state_prompt.format( prompt = update_character_state_prompt.format(
chapter_text=chapter_text, chapter_text=chapter_text,
@@ -654,7 +658,7 @@ def finalize_chapter(
new_char_state = update_character_state(chapter_text, old_char_state) new_char_state = update_character_state(chapter_text, old_char_state)
# 4) 更新剧情要点 # 更新剧情要点
new_plot_arcs = update_plot_arcs( new_plot_arcs = update_plot_arcs(
chapter_text=chapter_text, chapter_text=chapter_text,
old_plot_arcs=old_plot_arcs, old_plot_arcs=old_plot_arcs,
@@ -664,7 +668,7 @@ def finalize_chapter(
temperature=temperature temperature=temperature
) )
# 5) 覆盖写入文件 # 写回文件
clear_file_content(character_state_file) clear_file_content(character_state_file)
save_string_to_txt(new_char_state, character_state_file) save_string_to_txt(new_char_state, character_state_file)
@@ -674,7 +678,7 @@ def finalize_chapter(
clear_file_content(plot_arcs_file) clear_file_content(plot_arcs_file)
save_string_to_txt(new_plot_arcs, plot_arcs_file) save_string_to_txt(new_plot_arcs, plot_arcs_file)
# 6) 更新向量库 # 更新向量库
update_vector_store( update_vector_store(
api_key=api_key, api_key=api_key,
base_url=base_url, base_url=base_url,
@@ -695,7 +699,6 @@ def enrich_chapter_text(
) -> str: ) -> str:
""" """
当章节篇幅不足时,调用此函数对章节文本进行二次扩写。 当章节篇幅不足时,调用此函数对章节文本进行二次扩写。
可以让模型补充场景描写、角色心理等,保证与现有文本风格一致。
""" """
model = ChatOpenAI( model = ChatOpenAI(
model=model_name, model=model_name,
@@ -713,6 +716,7 @@ def enrich_chapter_text(
return chapter_text return chapter_text
return response.content.strip() return response.content.strip()
# ============ 导入外部知识文本 ============ # ============ 导入外部知识文本 ============
def import_knowledge_file( def import_knowledge_file(
@@ -722,11 +726,11 @@ def import_knowledge_file(
embedding_model_name: str, embedding_model_name: str,
file_path: str, file_path: str,
embedding_base_url: str = "" embedding_base_url: str = ""
) -> None: ) -> 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): if not os.path.exists(file_path):
logging.warning(f"知识库文件不存在: {file_path}") logging.warning(f"知识库文件不存在: {file_path}")
return return
@@ -760,11 +764,12 @@ def advanced_split_content(content: str,
similarity_threshold: float = 0.7, similarity_threshold: float = 0.7,
max_length: int = 500) -> List[str]: max_length: int = 500) -> List[str]:
""" """
将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。 将文本先按句子切分,然后根据语义相似度进行合并,最后max_length二次切分。
""" """
nltk.download('punkt_tab', quiet=True) # 纠正下载punkt包:'punkt' 而非 'punkt_tab'
sentences = nltk.sent_tokenize(content) nltk.download('punkt', quiet=True)
sentences = nltk.sent_tokenize(content)
if not sentences: if not sentences:
return [] return []
+32 -37
View File
@@ -1,11 +1,12 @@
# ui.py # ui.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging
import os import os
import threading import threading
import customtkinter as ctk import customtkinter as ctk
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
import traceback
from config_manager import load_config, save_config 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
from novel_generator import ( from novel_generator import (
@@ -19,6 +20,12 @@ from novel_generator import (
) )
from consistency_checker import check_consistency 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_appearance_mode("System")
ctk.set_default_color_theme("blue") ctk.set_default_color_theme("blue")
@@ -38,7 +45,6 @@ class NovelGeneratorGUI:
self.loaded_config = load_config(self.config_file) 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.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.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.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI"))
@@ -128,8 +134,6 @@ class NovelGeneratorGUI:
# 日志:只读 # 日志:只读
self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12)) 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.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
# 设置只读
self.log_text.configure(state="disabled") self.log_text.configure(state="disabled")
def build_right_layout(self): def build_right_layout(self):
@@ -137,8 +141,6 @@ class NovelGeneratorGUI:
右侧用于显示配置与功能按钮。 右侧用于显示配置与功能按钮。
其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。 其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。
下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。 下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。
另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。
""" """
self.right_frame.grid_rowconfigure(0, weight=0) self.right_frame.grid_rowconfigure(0, weight=0)
self.right_frame.grid_rowconfigure(1, weight=0) self.right_frame.grid_rowconfigure(1, weight=0)
@@ -172,14 +174,13 @@ class NovelGeneratorGUI:
- 模型名称 - 模型名称
- Temperature - Temperature
""" """
# 配置网格
for i in range(5): for i in range(5):
self.ai_config_tab.grid_rowconfigure(i, weight=0) self.ai_config_tab.grid_rowconfigure(i, weight=0)
self.ai_config_tab.grid_columnconfigure(0, weight=0) self.ai_config_tab.grid_columnconfigure(0, weight=0)
self.ai_config_tab.grid_columnconfigure(1, weight=1) self.ai_config_tab.grid_columnconfigure(1, weight=1)
# 回调:当接口格式下拉框发生变更时,如果 Base URL 为空,则根据接口类型自动填默认值
def on_interface_format_changed(new_value): def on_interface_format_changed(new_value):
# 如果用户切换接口格式,可根据需要修改BaseURL为默认值
if new_value == "Ollama": if new_value == "Ollama":
self.base_url_var.set("http://localhost:11434/v1") self.base_url_var.set("http://localhost:11434/v1")
elif new_value == "ML Studio": elif new_value == "ML Studio":
@@ -263,7 +264,6 @@ class NovelGeneratorGUI:
def build_novel_params_area(self, start_row=2): def build_novel_params_area(self, start_row=2):
""" """
放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等 放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等
只使用两列:0列放Label,1列放输入框(可能加子Frame)。
""" """
# 主题(Topic) # 主题(Topic)
topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) 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 = 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") 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 = 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_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.right_frame.grid_rowconfigure(start_row, weight=0)
# 先放“保存配置”和“加载配置”按钮
self.btn_frame_config = ctk.CTkFrame(self.right_frame) 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.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(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 = 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") load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# 下面是主要功能按钮
self.btn_generate_full = ctk.CTkButton( self.btn_generate_full = ctk.CTkButton(
self.right_frame, text="Step1. 生成设定 & 目录", self.right_frame, text="Step1. 生成设定 & 目录",
command=self.generate_full_novel, command=self.generate_full_novel,
@@ -389,9 +385,6 @@ class NovelGeneratorGUI:
# ------------------ Novel Settings Tab ------------------ # ------------------ Novel Settings Tab ------------------
def build_setting_tab(self): def build_setting_tab(self):
"""
可查看/编辑 Novel_setting.txt 并保存
"""
self.setting_tab.rowconfigure(0, weight=0) self.setting_tab.rowconfigure(0, weight=0)
self.setting_tab.rowconfigure(1, weight=1) self.setting_tab.rowconfigure(1, weight=1)
self.setting_tab.columnconfigure(0, weight=1) self.setting_tab.columnconfigure(0, weight=1)
@@ -430,9 +423,6 @@ class NovelGeneratorGUI:
# ------------------ Novel Directory Tab ------------------ # ------------------ Novel Directory Tab ------------------
def build_directory_tab(self): def build_directory_tab(self):
"""
可查看/编辑 Novel_directory.txt 并保存
"""
self.directory_tab.rowconfigure(0, weight=0) self.directory_tab.rowconfigure(0, weight=0)
self.directory_tab.rowconfigure(1, weight=1) self.directory_tab.rowconfigure(1, weight=1)
self.directory_tab.columnconfigure(0, weight=1) self.directory_tab.columnconfigure(0, weight=1)
@@ -471,9 +461,6 @@ class NovelGeneratorGUI:
# ------------------ Character State Tab ------------------ # ------------------ Character State Tab ------------------
def build_character_tab(self): def build_character_tab(self):
"""
查看/编辑 character_state.txt
"""
self.character_tab.rowconfigure(0, weight=0) self.character_tab.rowconfigure(0, weight=0)
self.character_tab.rowconfigure(1, weight=1) self.character_tab.rowconfigure(1, weight=1)
self.character_tab.columnconfigure(0, weight=1) self.character_tab.columnconfigure(0, weight=1)
@@ -512,9 +499,6 @@ class NovelGeneratorGUI:
# ------------------ Global Summary Tab ------------------ # ------------------ Global Summary Tab ------------------
def build_summary_tab(self): def build_summary_tab(self):
"""
查看/编辑 global_summary.txt
"""
self.summary_tab.rowconfigure(0, weight=0) self.summary_tab.rowconfigure(0, weight=0)
self.summary_tab.rowconfigure(1, weight=1) self.summary_tab.rowconfigure(1, weight=1)
self.summary_tab.columnconfigure(0, weight=1) self.summary_tab.columnconfigure(0, weight=1)
@@ -603,11 +587,9 @@ class NovelGeneratorGUI:
# ------------------ 日志输出 ------------------ # ------------------ 日志输出 ------------------
def log(self, message: str): def log(self, message: str):
# 临时切换为可写
self.log_text.configure(state="normal") self.log_text.configure(state="normal")
self.log_text.insert("end", message + "\n") self.log_text.insert("end", message + "\n")
self.log_text.see("end") self.log_text.see("end")
# 再切回只读
self.log_text.configure(state="disabled") self.log_text.configure(state="disabled")
# ------------------ 功能区 -------------------- # ------------------ 功能区 --------------------
@@ -650,6 +632,7 @@ class NovelGeneratorGUI:
) )
self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。")
except Exception as e: except Exception as e:
logging.error(f"生成小说设定 & 目录时出错: {e}")
self.log(f"❌ 生成小说设定 & 目录时出错: {e}") self.log(f"❌ 生成小说设定 & 目录时出错: {e}")
finally: finally:
self.enable_button(self.btn_generate_full) self.enable_button(self.btn_generate_full)
@@ -693,10 +676,13 @@ class NovelGeneratorGUI:
chapters_dir = os.path.join(filepath, "chapters") chapters_dir = os.path.join(filepath, "chapters")
recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3) recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3)
# 简易生成最近章节摘要(示例) # 生成最近章节摘要
recent_chapters_summary = summarize_recent_chapters( recent_chapters_summary = summarize_recent_chapters(
model_name, # 此处仅示例 llm_model=model_name,
recent_3_texts api_key=api_key,
base_url=base_url,
temperature=temperature,
chapters_text_list=recent_3_texts
) )
self.log(f"开始生成第{chap_num}章草稿...") self.log(f"开始生成第{chap_num}章草稿...")
@@ -713,7 +699,10 @@ class NovelGeneratorGUI:
word_number=word_number, word_number=word_number,
temperature=temperature, temperature=temperature,
novel_novel_directory=novel_directory, 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: if draft_text:
self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。") self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。")
@@ -724,6 +713,7 @@ class NovelGeneratorGUI:
self.log("⚠️ 本章草稿生成失败或无内容。") self.log("⚠️ 本章草稿生成失败或无内容。")
except Exception as e: except Exception as e:
logging.error(f"生成章节草稿时出错: {e}")
self.log(f"❌ 生成章节草稿时出错: {e}") self.log(f"❌ 生成章节草稿时出错: {e}")
finally: finally:
self.enable_button(self.btn_generate_chapter) self.enable_button(self.btn_generate_chapter)
@@ -741,6 +731,8 @@ class NovelGeneratorGUI:
model_name = self.model_name_var.get().strip() model_name = self.model_name_var.get().strip()
temperature = self.temperature_var.get() temperature = self.temperature_var.get()
filepath = self.filepath_var.get().strip() 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: if not filepath:
self.log("请先配置保存文件路径。") self.log("请先配置保存文件路径。")
@@ -755,6 +747,8 @@ class NovelGeneratorGUI:
word_number=word_number, word_number=word_number,
api_key=api_key, api_key=api_key,
base_url=base_url, base_url=base_url,
interface_format=interface_format,
embedding_model_name=embedding_model_name,
model_name=model_name, model_name=model_name,
temperature=temperature, temperature=temperature,
filepath=filepath filepath=filepath
@@ -769,6 +763,7 @@ class NovelGeneratorGUI:
self.chapter_result.see("end") self.chapter_result.see("end")
except Exception as e: except Exception as e:
logging.error(f"定稿章节时出错: {e}")
self.log(f"❌ 定稿章节时出错: {e}") self.log(f"❌ 定稿章节时出错: {e}")
finally: finally:
self.enable_button(self.btn_finalize_chapter) self.enable_button(self.btn_finalize_chapter)
@@ -801,7 +796,6 @@ class NovelGeneratorGUI:
global_summary = read_file(global_summary_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() chap_num = self.chapter_num_var.get()
chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt") chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt")
chapter_text = read_file(chap_file) chapter_text = read_file(chap_file)
@@ -826,6 +820,7 @@ class NovelGeneratorGUI:
self.log(result) self.log(result)
except Exception as e: except Exception as e:
logging.error(f"审校时出错: {e}")
self.log(f"❌ 审校时出错: {e}") self.log(f"❌ 审校时出错: {e}")
finally: finally:
self.enable_button(self.btn_check_consistency) self.enable_button(self.btn_check_consistency)
@@ -848,12 +843,13 @@ class NovelGeneratorGUI:
api_key=self.api_key_var.get().strip(), api_key=self.api_key_var.get().strip(),
base_url=self.base_url_var.get().strip(), base_url=self.base_url_var.get().strip(),
interface_format=self.interface_format_var.get().strip(), interface_format=self.interface_format_var.get().strip(),
embedding_base_url=self.embedding_url_var.get().strip(), embedding_model_name=self.embedding_model_name_var.get().strip(),
embedding_base_url=self.embedding_url_var.get().strip(), file_path=selected_file,
file_path=selected_file embedding_base_url=self.embedding_url_var.get().strip()
) )
self.log("✅ 知识库文件导入完成。") self.log("✅ 知识库文件导入完成。")
except Exception as e: except Exception as e:
logging.error(f"导入知识库时出错: {e}")
self.log(f"❌ 导入知识库时出错: {e}") self.log(f"❌ 导入知识库时出错: {e}")
finally: finally:
self.enable_button(self.btn_import_knowledge) self.enable_button(self.btn_import_knowledge)
@@ -886,7 +882,6 @@ class NovelGeneratorGUI:
if not arcs_text: if not arcs_text:
arcs_text = "当前没有记录的剧情要点或冲突。" arcs_text = "当前没有记录的剧情要点或冲突。"
# 弹窗显示
top = ctk.CTkToplevel(self.master) top = ctk.CTkToplevel(self.master)
top.title("剧情要点/未解决冲突") top.title("剧情要点/未解决冲突")
top.geometry("600x400") top.geometry("600x400")