commit c99601fe1048807ba9cacfc32cd7909c10bbcb01 Author: YILING0013 Date: Wed Jan 29 20:33:20 2025 +0800 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/AI_novel/Novel_directory.txt b/AI_novel/Novel_directory.txt new file mode 100644 index 0000000..9d27345 --- /dev/null +++ b/AI_novel/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/AI_novel/Novel_setting.txt b/AI_novel/Novel_setting.txt new file mode 100644 index 0000000..84c7cd1 --- /dev/null +++ b/AI_novel/Novel_setting.txt @@ -0,0 +1,36 @@ + +小说设定 + +小说名称: 星陨提瓦特 + +总字数走向: 约40万字,分为100章,每章约4000字。整体走向为主角经历成长、探索与冲突,逐渐揭开提瓦特大陆的秘密与危机,最终实现拯救与情感的圆满。 + +小说类型与基调: 本作为玄幻、穿越与冒险题材,主线围绕丰富的爱恨情仇展展开。基调相对轻松,同时穿插挑战与深思,旨在营造出一种既有趣又引人深思的氛围。 + +写作风格: 采用细腻、抒情的写作风格,重点描绘角色内心的探索与情感。叙事视角主要为第三人称全知视角,辅以第一人称的内心独白,以增强角色深度。 + +整体世界观: 故事设定在提瓦特大陆,这是一个拥有七个不同国度和独特元素力量的神秘幻想世界。历史与传说丰富,比如“天理之战”及各个神祇的传奇故事。科技与魔法并存,元素魔法高度发达,社会结构以王国与神明为核心,复杂的势力交错使人民生活受其影响。 + +核心内容梗概: 故事采用英雄之旅结构,主角星因意外穿越到提瓦特,面临神明失去信仰、元素失衡,国家动荡的空前危机。在寻找回家的过程中,星结识了来自不同国家的伙伴,体验各地人文风情,并逐渐揭示穿越的真相。最终,星与伙伴们合力对抗共同的敌人,重塑世界的平衡,也收获真挚的情感。 + +情节安排设想: 主线围绕星的成长与拯救提瓦特的冒险分为三个阶段:第一阶段是星的穿越与初识,以及角色冲突的引入;第二阶段是与伙伴的深入互动,逐步揭露敌人阴谋及情感发展;第三阶段是最终决战与角色命运交织。副线聚焦其他角色的故事,展示他们与星的命运交织,为故事营造丰富的世界感。 + +角色关系与主要角色设定: + +1. 星(主角): 一名勇敢、好奇且善良的年轻人,满怀探索未知的欲望,面临对归属感的挣扎。在拯救提瓦特的过程中,她渴望成为真正的英雄,同时也处理与伙伴间复杂的情感关系。 + +2. 伊尔(冷静的魔法师): 理智、谨慎,拥有强大师法的他在经历失败后变得冷淡,内心深处隐藏痛苦。希望能通过知识与能力解决危机,努力解脱自己的内心痛楚。与星的理想与现实相悖,易引发冲突。 + +3. 莱娜(隐秘的元素使者): 神秘而强大的她深谙元素魔法,在寻找与星的关系中,探索自己的身份之谜。与伊尔之间存在信任危机,随着故事发展,二人将面临信任与情感的考验。 + +暗线与伏笔构思: + +1. 星的过往: 星对旧生活的怀念与痛苦逐渐显现,推动她在救助他人和回归原生活之间的抉择,进而引发与伙伴之间的深入讨论。 + +2. 伊尔的秘密: 伊尔对过去的隐瞒以及与敌对势力的牵连,在关键时刻暴露,导致团队关系动摇,让星更深刻地理解他的痛苦。 + +3. 莱娜的使命: 莱娜的神秘身份及意图逐步显露,最终必须面对身份的真实,仅在关键时刻,有赖于星与伊尔的支持,坚定她内心的归属感。 + +4. 星与伊尔的理念冲突: 星与伊尔在道德取向上发生摩擦,在选择重大事件时,两人的冲突达到顶峰,最终通过事件的解决促进彼此之间的理解与成长。 + +在这些设定中,角色的动机与暗线及伏笔相互交织,为整个故事增添了复杂性与紧张感,确保每一位角色的成长与情感发展贯穿始终,丰富了提瓦特大陆的冒险传奇。 \ No newline at end of file diff --git a/AI_novel/chapter.txt b/AI_novel/chapter.txt new file mode 100644 index 0000000..075f280 --- /dev/null +++ b/AI_novel/chapter.txt @@ -0,0 +1,72 @@ + +星缓缓睁开眼睛,意识到自己正躺在一片陌生的土地上。四周是葱茏的树木,阳光透过树叶洒下斑驳的光影,微风轻拂,带来一丝清新的气息。她试图站起身来,身体却感到无力,仿佛刚经历了一场翻天覆地的梦境。迷茫与恐惧瞬间涌上心头——她究竟身处何地? + +“别 panicking,星。”她自言自语,努力让自己冷静下来。可无论她怎么深呼吸,心中的慌乱与不安依旧如影随形。刚才她还在自己的家中,思绪被一扇打开的窗户吸引,目光追随着飞过的鸟儿,突然遭遇了一道神秘的光芒,她的意识就在那瞬间被撕扯了开来。而现在,她却身处于这个陌生的世界,这一切都让她感到无比的恐慌。 + +尝试坐起身来,星环顾四周,发现这片森林似乎与她的记忆截然相反。树木高耸入云,树干粗壮而苍翠,叶子则在微风中轻轻摇晃,发出沙沙的声响。然而,这些美丽的自然景象并没有为她带来一丝安慰——相反,她觉得一种不安的气息弥漫在空气中,仿佛这片森林隐藏着某种不为人知的秘密。 + +星的心中掠过一丝奇异的感觉,似乎这片土地上有着某种她无法理解的力量在流动。她强迫自己站起身,决定探索这个世界。若想办法找到回家的路,先得了解这个地方。她开始慢慢前行,脚下的土地干燥而坚硬,偶尔踩到几根枯枝,发出清脆的声响。 + +随着深入森林,她注意到周围的植物似乎散发着微弱的光芒,仿佛在回应着她的存在。星试图触摸那些植物,感受那股能量,却发现它们在她的手指间划过时变成了莹莹的光点,瞬间就消散了。她心中一紧,这显然不是普通的植物,她的直觉告诉她,自己可能身处于一个充满魔法的地方。 + +就在这时,她的思绪被一阵低语声打断。星停下脚步,四处张望,试图找到声音的来源。声音似乎从四面八方传来,但却又似乎是从某个深渊中回荡而出,朦胧而模糊。 + +“是谁在说话?”星喃喃自语,然而没有人回应。她心中浮现出一丝不安,迫切想要找到声音的源头。正当她准备继续探索时,眼前突然闪过一道亮光。她的心一颤,抬头看去,透过树木的缝隙,她隐约看到一个身影,似乎在远处的空旷地带移动。 + +出于好奇,星忍不住向前走去,穿越过一片茂密的灌木丛。终于,她走出林间,来到了一个开阔的空地上。眼前的一幕让她惊讶不已——一座宏伟的法阵正镶嵌于地面,阵法复杂而美丽,闪烁着微光,似乎在向她召唤。 + +“这是什么?”星自言自语,心中充满了疑惑与好奇。她走到法阵边缘,感觉到一股温暖的气流从阵法中透出。星的手指微微颤动,不由自主地伸向了那个法阵,触碰到了那些复杂的符文。瞬间,她感到一阵剧烈的震动,法阵光芒大盛,星的身体被卷入其中,意识在瞬间模糊而又清晰。 + +“啊!”她惊呼,仿佛被一股无形的力量撕扯着。整个世界都在颠覆,光芒刺眼,四周的景象如同狂风中的飘絮般不断变换,星感到自己像是随风而舞的叶子,无法控制。就在意识即将消散的瞬间,她听到了那个低语声,竟然清晰而响亮:“命运的选择。” + +然后,星失去了意识。 + +当她再次睁开眼睛时,发现自己坐在一块石头上,周围是一片开阔的草地,远处是连绵起伏的山脉,天空湛蓝,阳光温暖,仿佛一切都是那么美好。可在她心中,却依然残留着那股恐惧与不安。 + +“这是哪里?”她自言自语,四处打量。就在这时,一个阴影笼罩了她,星抬头一看,面前站着一个高大的男子,身穿长袍,手中握着法杖,面容冷峻,眼神中透着智慧与审视。 + +“你是谁?”星有些警惕地问道,虽然心中已经有了几分预感。 + +“我是伊尔,一个魔法师。”男子的声音低沉而有力,似乎带着某种压迫感,“你应该是从那个法阵中被传送过来的吧?” + +星点点头,虽然内心依旧充满困惑,但她知道自己必须与眼前这个陌生人好好交流。“是的,我…我不知道自己为什么来到这里。” + +伊尔微微皱眉,目光在她身上扫视,似乎在打量她的言语是否可信。“这片土地并不安全,你的到来可不是什么偶然。元素的平衡正在崩溃,神明的意志也逐渐淡去。” + +“神明的意志?”星困惑地问道,心中浮现出关于提瓦特的传说与故事。她从小就听说过这个富饶而神秘的大陆,但从未想过自己会置身其中。 + +“没错,”伊尔坚定地点头,“提瓦特大陆上,七位神明各自守护着一个国度,而你在这里的出现,可能与某种更大的命运息息相关。” + +“我只想回家。”星低声说道,心中依然渴望着熟悉的生活,尽管她也感受到这个新的世界是如此吸引人。 + +“回家并不是那么简单。”伊尔的声音中透着冷静,他的眼神中闪烁着某种复杂的情感,“在这片土地上,你必须要面对许多你无法想象的挑战。” + +星感到了一丝绝望,她并不想成为一名英雄,自己不过是一名普通的年轻人。“我……我能做些什么?” + +“或许我们可以一起探索这个世界。”伊尔的语气缓和了一些,似乎对她产生了一丝信任,“我对魔法的研究或许能帮助你找到线索。” + +“谢谢你,伊尔。”星感激地点头,虽然她对眼前的陌生人依然有些顾虑,但她知道自己必须要有人陪伴,才能在这个陌生的世界中生存下去。 + +就在这时,草地的另一侧,一道神秘的光芒闪过,随即出现了另一个身影。那是一位少女,长发披肩,身穿轻盈的衣袍,眼神深邃而神秘,她似乎在默默地注视着星与伊尔。 + +“你是……”星愣了一下,有些惊讶。 + +“我叫莱娜。”少女轻声说道,她的声音如同空灵的风铃,给人一种奇特的亲切感,“我感觉到了你的到来,希望能和你们一起探索这个世界。” + +“你也感知到了?”伊尔微微皱眉,似乎对莱娜的出现有些意外,却又没有质疑的意思。 + +“是的。”莱娜的眼神透着某种不易察觉的情感,“我对自然元素有着强烈的感应,能够感受到世界的变化。” + +星心中一震,莱娜的出现让她感到了一丝安慰,也增添了对这个世界的探索欲望。“或许我们可以一起合作,找出这个世界的秘密。” + +莱娜微微一笑,仿佛认可了星的提议。“我相信,我们的命运将会交织在一起。” + +三人之间逐渐形成了一种微妙的默契。尽管星对这个新世界的未来仍感到模糊不清,内心的惶恐也未完全消散,但在伊尔与莱娜的陪伴下,她逐渐意识到自己不再孤单。接下来,等待她的将是未知的冒险,她必须做好面对的准备。 + +正当星准备问伊尔关于这个世界的更多事情时,突然,一股强烈的能量波动猛然袭来,她耳边仿佛听到了那道低语的声音再度浮现,清晰而坚定:“命运的选择,终于来临!” + +星的心脏骤然加速,四周的空气似乎在那一瞬间凝固。她抬头,看到远处的天空中,一片阴云迅速聚集而来,预示着某种危险的逼近。一个模糊的影像在天际闪现,似乎正暗示着未来的冲突与挑战,而她所要面对的,绝不仅仅是对自我归属的探索。 + +“我们要走!”伊尔的声音打断了星的思绪,他的目光变得凝重,“不可忽视的危机即将降临,我们必须立刻找到安全的地方!” + +星与莱娜对视一眼,心中都涌起一股紧迫感。对即将来临的挑战,三人并肩而立,准备迎接未知的未来。此刻,她们的命运已经交织在了一起,故事的篇章才刚刚开启,而未来的旅程充满了未知与悬念。 \ No newline at end of file diff --git a/AI_novel/character_state.txt b/AI_novel/character_state.txt new file mode 100644 index 0000000..e69de29 diff --git a/AI_novel/global_summary.txt b/AI_novel/global_summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/AI_novel/lastchapter.txt b/AI_novel/lastchapter.txt new file mode 100644 index 0000000..8b609e7 --- /dev/null +++ b/AI_novel/lastchapter.txt @@ -0,0 +1,71 @@ +星缓缓睁开眼睛,意识到自己正躺在一片陌生的土地上。四周是葱茏的树木,阳光透过树叶洒下斑驳的光影,微风轻拂,带来一丝清新的气息。她试图站起身来,身体却感到无力,仿佛刚经历了一场翻天覆地的梦境。迷茫与恐惧瞬间涌上心头——她究竟身处何地? + +“别 panicking,星。”她自言自语,努力让自己冷静下来。可无论她怎么深呼吸,心中的慌乱与不安依旧如影随形。刚才她还在自己的家中,思绪被一扇打开的窗户吸引,目光追随着飞过的鸟儿,突然遭遇了一道神秘的光芒,她的意识就在那瞬间被撕扯了开来。而现在,她却身处于这个陌生的世界,这一切都让她感到无比的恐慌。 + +尝试坐起身来,星环顾四周,发现这片森林似乎与她的记忆截然相反。树木高耸入云,树干粗壮而苍翠,叶子则在微风中轻轻摇晃,发出沙沙的声响。然而,这些美丽的自然景象并没有为她带来一丝安慰——相反,她觉得一种不安的气息弥漫在空气中,仿佛这片森林隐藏着某种不为人知的秘密。 + +星的心中掠过一丝奇异的感觉,似乎这片土地上有着某种她无法理解的力量在流动。她强迫自己站起身,决定探索这个世界。若想办法找到回家的路,先得了解这个地方。她开始慢慢前行,脚下的土地干燥而坚硬,偶尔踩到几根枯枝,发出清脆的声响。 + +随着深入森林,她注意到周围的植物似乎散发着微弱的光芒,仿佛在回应着她的存在。星试图触摸那些植物,感受那股能量,却发现它们在她的手指间划过时变成了莹莹的光点,瞬间就消散了。她心中一紧,这显然不是普通的植物,她的直觉告诉她,自己可能身处于一个充满魔法的地方。 + +就在这时,她的思绪被一阵低语声打断。星停下脚步,四处张望,试图找到声音的来源。声音似乎从四面八方传来,但却又似乎是从某个深渊中回荡而出,朦胧而模糊。 + +“是谁在说话?”星喃喃自语,然而没有人回应。她心中浮现出一丝不安,迫切想要找到声音的源头。正当她准备继续探索时,眼前突然闪过一道亮光。她的心一颤,抬头看去,透过树木的缝隙,她隐约看到一个身影,似乎在远处的空旷地带移动。 + +出于好奇,星忍不住向前走去,穿越过一片茂密的灌木丛。终于,她走出林间,来到了一个开阔的空地上。眼前的一幕让她惊讶不已——一座宏伟的法阵正镶嵌于地面,阵法复杂而美丽,闪烁着微光,似乎在向她召唤。 + +“这是什么?”星自言自语,心中充满了疑惑与好奇。她走到法阵边缘,感觉到一股温暖的气流从阵法中透出。星的手指微微颤动,不由自主地伸向了那个法阵,触碰到了那些复杂的符文。瞬间,她感到一阵剧烈的震动,法阵光芒大盛,星的身体被卷入其中,意识在瞬间模糊而又清晰。 + +“啊!”她惊呼,仿佛被一股无形的力量撕扯着。整个世界都在颠覆,光芒刺眼,四周的景象如同狂风中的飘絮般不断变换,星感到自己像是随风而舞的叶子,无法控制。就在意识即将消散的瞬间,她听到了那个低语声,竟然清晰而响亮:“命运的选择。” + +然后,星失去了意识。 + +当她再次睁开眼睛时,发现自己坐在一块石头上,周围是一片开阔的草地,远处是连绵起伏的山脉,天空湛蓝,阳光温暖,仿佛一切都是那么美好。可在她心中,却依然残留着那股恐惧与不安。 + +“这是哪里?”她自言自语,四处打量。就在这时,一个阴影笼罩了她,星抬头一看,面前站着一个高大的男子,身穿长袍,手中握着法杖,面容冷峻,眼神中透着智慧与审视。 + +“你是谁?”星有些警惕地问道,虽然心中已经有了几分预感。 + +“我是伊尔,一个魔法师。”男子的声音低沉而有力,似乎带着某种压迫感,“你应该是从那个法阵中被传送过来的吧?” + +星点点头,虽然内心依旧充满困惑,但她知道自己必须与眼前这个陌生人好好交流。“是的,我…我不知道自己为什么来到这里。” + +伊尔微微皱眉,目光在她身上扫视,似乎在打量她的言语是否可信。“这片土地并不安全,你的到来可不是什么偶然。元素的平衡正在崩溃,神明的意志也逐渐淡去。” + +“神明的意志?”星困惑地问道,心中浮现出关于提瓦特的传说与故事。她从小就听说过这个富饶而神秘的大陆,但从未想过自己会置身其中。 + +“没错,”伊尔坚定地点头,“提瓦特大陆上,七位神明各自守护着一个国度,而你在这里的出现,可能与某种更大的命运息息相关。” + +“我只想回家。”星低声说道,心中依然渴望着熟悉的生活,尽管她也感受到这个新的世界是如此吸引人。 + +“回家并不是那么简单。”伊尔的声音中透着冷静,他的眼神中闪烁着某种复杂的情感,“在这片土地上,你必须要面对许多你无法想象的挑战。” + +星感到了一丝绝望,她并不想成为一名英雄,自己不过是一名普通的年轻人。“我……我能做些什么?” + +“或许我们可以一起探索这个世界。”伊尔的语气缓和了一些,似乎对她产生了一丝信任,“我对魔法的研究或许能帮助你找到线索。” + +“谢谢你,伊尔。”星感激地点头,虽然她对眼前的陌生人依然有些顾虑,但她知道自己必须要有人陪伴,才能在这个陌生的世界中生存下去。 + +就在这时,草地的另一侧,一道神秘的光芒闪过,随即出现了另一个身影。那是一位少女,长发披肩,身穿轻盈的衣袍,眼神深邃而神秘,她似乎在默默地注视着星与伊尔。 + +“你是……”星愣了一下,有些惊讶。 + +“我叫莱娜。”少女轻声说道,她的声音如同空灵的风铃,给人一种奇特的亲切感,“我感觉到了你的到来,希望能和你们一起探索这个世界。” + +“你也感知到了?”伊尔微微皱眉,似乎对莱娜的出现有些意外,却又没有质疑的意思。 + +“是的。”莱娜的眼神透着某种不易察觉的情感,“我对自然元素有着强烈的感应,能够感受到世界的变化。” + +星心中一震,莱娜的出现让她感到了一丝安慰,也增添了对这个世界的探索欲望。“或许我们可以一起合作,找出这个世界的秘密。” + +莱娜微微一笑,仿佛认可了星的提议。“我相信,我们的命运将会交织在一起。” + +三人之间逐渐形成了一种微妙的默契。尽管星对这个新世界的未来仍感到模糊不清,内心的惶恐也未完全消散,但在伊尔与莱娜的陪伴下,她逐渐意识到自己不再孤单。接下来,等待她的将是未知的冒险,她必须做好面对的准备。 + +正当星准备问伊尔关于这个世界的更多事情时,突然,一股强烈的能量波动猛然袭来,她耳边仿佛听到了那道低语的声音再度浮现,清晰而坚定:“命运的选择,终于来临!” + +星的心脏骤然加速,四周的空气似乎在那一瞬间凝固。她抬头,看到远处的天空中,一片阴云迅速聚集而来,预示着某种危险的逼近。一个模糊的影像在天际闪现,似乎正暗示着未来的冲突与挑战,而她所要面对的,绝不仅仅是对自我归属的探索。 + +“我们要走!”伊尔的声音打断了星的思绪,他的目光变得凝重,“不可忽视的危机即将降临,我们必须立刻找到安全的地方!” + +星与莱娜对视一眼,心中都涌起一股紧迫感。对即将来临的挑战,三人并肩而立,准备迎接未知的未来。此刻,她们的命运已经交织在了一起,故事的篇章才刚刚开启,而未来的旅程充满了未知与悬念。 \ No newline at end of file diff --git a/__pycache__/config_manager.cpython-310.pyc b/__pycache__/config_manager.cpython-310.pyc new file mode 100644 index 0000000..c8224ea Binary files /dev/null and b/__pycache__/config_manager.cpython-310.pyc differ diff --git a/__pycache__/consistency_checker.cpython-310.pyc b/__pycache__/consistency_checker.cpython-310.pyc new file mode 100644 index 0000000..a88e555 Binary files /dev/null and b/__pycache__/consistency_checker.cpython-310.pyc differ diff --git a/__pycache__/novel_generator.cpython-310.pyc b/__pycache__/novel_generator.cpython-310.pyc new file mode 100644 index 0000000..1ad9ee7 Binary files /dev/null and b/__pycache__/novel_generator.cpython-310.pyc differ diff --git a/__pycache__/prompt_definitions.cpython-310.pyc b/__pycache__/prompt_definitions.cpython-310.pyc new file mode 100644 index 0000000..d1eb36e Binary files /dev/null and b/__pycache__/prompt_definitions.cpython-310.pyc differ diff --git a/__pycache__/ui.cpython-310.pyc b/__pycache__/ui.cpython-310.pyc new file mode 100644 index 0000000..1c305f6 Binary files /dev/null and b/__pycache__/ui.cpython-310.pyc differ diff --git a/__pycache__/utils.cpython-310.pyc b/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..0b7873f Binary files /dev/null and b/__pycache__/utils.cpython-310.pyc differ diff --git a/config.json b/config.json new file mode 100644 index 0000000..13cf58e --- /dev/null +++ b/config.json @@ -0,0 +1,10 @@ +{ + "api_key": "sk-TxjhfBwxVJ6jA7Px3reCDR2x3Ua6vVghoraX8LtwUBFntD4I", + "base_url": "https://api.agicto.cn/v1", + "model_name": "gpt-4o-mini", + "topic": "星穹铁道主角星穿越到原神提瓦特大陆,拯救提瓦特大陆,并与其中的角色展开爱恨情仇的小说设定", + "genre": "玄幻", + "num_chapters": 100, + "word_number": 4000, + "filepath": "C:/Users/xieli/Desktop/AI_NovelGenerator/AI_novel" +} \ No newline at end of file diff --git a/config_manager.py b/config_manager.py new file mode 100644 index 0000000..892fa90 --- /dev/null +++ b/config_manager.py @@ -0,0 +1,21 @@ +import json +import os + +def load_config(config_file: str) -> dict: + """从指定的 config_file 加载配置,若不存在则返回空字典。""" + if os.path.exists(config_file): + try: + with open(config_file, 'r', encoding='utf-8') as f: + return json.load(f) + except: + pass + return {} + +def save_config(config_data: dict, config_file: str) -> bool: + """将 config_data 保存到 config_file 中,返回 True/False 表示是否成功。""" + try: + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config_data, f, ensure_ascii=False, indent=4) + return True + except: + return False diff --git a/consistency_checker.py b/consistency_checker.py new file mode 100644 index 0000000..2cf6afe --- /dev/null +++ b/consistency_checker.py @@ -0,0 +1,51 @@ +""" +演示多Agent思路中的“审校Agent”,对最新章节进行简单的一致性或逻辑冲突检查。 +可根据需要进行扩展。 +""" +from langchain_openai import ChatOpenAI + +CONSISTENCY_PROMPT = """\ +请检查下面的小说设定与最新章节是否存在明显冲突或不一致之处,如有请列出: +- 小说设定: +{novel_setting} + +- 角色状态(可能包含重要信息): +{character_state} + +- 全局摘要: +{global_summary} + +- 最新章节内容: +{chapter_text} + +如果存在冲突或不一致,请说明;否则请返回“无明显冲突”。 +""" + +def check_consistency( + novel_setting: str, + character_state: str, + global_summary: str, + chapter_text: str, + api_key: str, + base_url: str, + model_name: str +) -> str: + """ + 调用模型做简单的一致性检查。可扩展更多提示或校验规则。 + """ + prompt = CONSISTENCY_PROMPT.format( + novel_setting=novel_setting, + character_state=character_state, + global_summary=global_summary, + chapter_text=chapter_text + ) + model = ChatOpenAI( + model=model_name, + api_key=api_key, + base_url=base_url, + temperature=0.3 + ) + response = model.invoke(prompt) + if not response: + return "审校Agent无回复" + return response.content.strip() diff --git a/main.py b/main.py new file mode 100644 index 0000000..2639413 --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +import tkinter as tk +from ui import NovelGeneratorGUI + +def main(): + root = tk.Tk() + root.title("Novel Generator - Innovative Flow") + app = NovelGeneratorGUI(root) + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/novel_generator.py b/novel_generator.py new file mode 100644 index 0000000..744ac66 --- /dev/null +++ b/novel_generator.py @@ -0,0 +1,317 @@ +import os +from typing_extensions import TypedDict +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, START, END +from typing import Dict + +from utils import ( + read_file, append_text_to_file, clear_file_content, save_string_to_txt +) +from prompt_definitions import ( + set_prompt, character_prompt, dark_lines_prompt, + finalize_setting_prompt, novel_directory_prompt, + summary_prompt, update_character_state_prompt, + chapter_outline_prompt, chapter_write_prompt +) + +# 向量检索相关 (以Chroma为例),需要安装 langchain, chromadb 等 +from langchain.embeddings.openai import OpenAIEmbeddings +from langchain.vectorstores import Chroma +from langchain.docstore.document import Document + +# 默认用此目录存放向量库 +VECTOR_STORE_DIR = "vectorstore" + +# =============== 多步生成:设置 & 目录 =============== +class OverallState(TypedDict): + topic: str + genre: str + number_of_chapters: int + word_number: int + novel_setting_base: str + character_setting: str + dark_lines: str + final_novel_setting: str + novel_directory: str + +def Novel_novel_directory_generate( + api_key: str, + base_url: str, + llm_model: str, + topic: str, + genre: str, + number_of_chapters: int, + word_number: int, + filepath: str +): + """ + 使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt + """ + model = ChatOpenAI( + model=llm_model, + api_key=api_key, + base_url=base_url + ) + + def generate_base_setting(state: OverallState): + prompt = set_prompt.format( + topic=state["topic"], + genre=state["genre"], + number_of_chapters=state["number_of_chapters"], + word_number=state["word_number"], + ) + response = model.invoke(prompt) + if not response: + return {"novel_setting_base": ""} + return {"novel_setting_base": response.content.strip()} + + def generate_character_setting(state: OverallState): + prompt = character_prompt.format(novel_setting=state["novel_setting_base"]) + response = model.invoke(prompt) + if not response: + return {"character_setting": ""} + return {"character_setting": response.content.strip()} + + def generate_dark_lines(state: OverallState): + prompt = dark_lines_prompt.format(character_info=state["character_setting"]) + response = model.invoke(prompt) + if not response: + return {"dark_lines": ""} + return {"dark_lines": response.content.strip()} + + def finalize_novel_setting(state: OverallState): + prompt = finalize_setting_prompt.format( + novel_setting_base=state["novel_setting_base"], + character_setting=state["character_setting"], + dark_lines=state["dark_lines"] + ) + response = model.invoke(prompt) + if not response: + return {"final_novel_setting": ""} + return {"final_novel_setting": response.content.strip()} + + def generate_novel_directory(state: OverallState): + 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: + return {"novel_directory": ""} + 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) + 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_edge(START, "generate_base_setting") + graph.add_edge("generate_base_setting", "generate_character_setting") + graph.add_edge("generate_character_setting", "generate_dark_lines") + graph.add_edge("generate_dark_lines", "finalize_novel_setting") + graph.add_edge("finalize_novel_setting", "generate_novel_directory") + graph.add_edge("generate_novel_directory", END) + + app = graph.compile() + + input_params = { + "topic": topic, + "genre": genre, + "number_of_chapters": number_of_chapters, + "word_number": word_number + } + result = app.invoke(input_params) + + if not result: + print("⚠️ invoke() 结果为空,生成失败。") + return + + final_novel_setting = result.get("final_novel_setting", "") + final_novel_directory = result.get("novel_directory", "") + if not final_novel_setting or not final_novel_directory: + print("⚠️ 生成失败:缺少 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") + + final_novel_setting_cleaned = final_novel_setting.replace('#', '').replace('*', '') + final_novel_directory_cleaned = final_novel_directory.replace('#', '').replace('*', '') + + append_text_to_file(final_novel_setting_cleaned, filename_set) + append_text_to_file(final_novel_directory_cleaned, filename_novel_directory) + + +# =============== 生成章节(含角色状态 & 全局摘要 & 向量检索) =============== + +def init_vector_store(api_key: str, texts: list[str]) -> Chroma: + """ + 初始化并返回一个Chroma向量库,将传入的文本进行嵌入。 + 若需要可对 texts 做分句或分块处理;这里只演示简单用法。 + """ + embeddings = OpenAIEmbeddings(openai_api_key=api_key) + documents = [Document(page_content=t) for t in texts] + vectorstore = Chroma.from_documents(documents, embedding=embeddings, persist_directory=VECTOR_STORE_DIR) + vectorstore.persist() + return vectorstore + +def load_vector_store(api_key: str) -> Chroma: + """ + 读取已存在的向量库。若不存在则返回None或新建一个空的。 + """ + if not os.path.exists(VECTOR_STORE_DIR): + return None + embeddings = OpenAIEmbeddings(openai_api_key=api_key) + return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings) + + +def update_vector_store(api_key: str, new_chapter: str): + """ + 将最新章节文本插入到向量库里,用于后续检索参考。 + 可根据实际需求做分块处理。此处仅作简单示范。 + """ + store = load_vector_store(api_key) + if not store: + # 如果vector store不存在,先初始化 + store = init_vector_store(api_key, [new_chapter]) + return + + embeddings = OpenAIEmbeddings(openai_api_key=api_key) + new_doc = Document(page_content=new_chapter) + store.add_documents([new_doc]) + store.persist() + + +def get_relevant_context_from_vector_store(api_key: str, query: str, k: int=2) -> str: + """ + 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 + 用于在生成大纲或写正文时,为大模型提供更多上下文。 + """ + store = load_vector_store(api_key) + if not store: + return "" + docs = store.similarity_search(query, k=k) + # 简单拼接 + combined = "\n".join([d.page_content for d in docs]) + return combined + +def generate_chapter_with_state( + novel_settings: str, + novel_novel_directory: str, + api_key: str, + base_url: str, + model_name: str, + novel_number: int, + filepath: str, + word_number: int, + lastchapter: str +) -> str: + """ + 多步流程: + 1) 更新/创建全局摘要 + 2) 更新/生成角色状态文档 + 3) 根据向量检索获取往期章节相关内容 + 4) 大纲 -> 正文 + 5) 更新向量库 + 最终写入 chapter.txt、lastchapter.txt、character_state.txt、global_summary.txt + """ + model = ChatOpenAI( + model=model_name, + api_key=api_key, + base_url=base_url, + temperature=0.9 + ) + + # --- 文件名定义 --- + character_state_file = os.path.join(filepath, "character_state.txt") + global_summary_file = os.path.join(filepath, "global_summary.txt") + chapter_file = os.path.join(filepath, "chapter.txt") + lastchapter_file = os.path.join(filepath, "lastchapter.txt") + + # --- 读取现有文档(可能为空) --- + old_char_state = read_file(character_state_file) + old_global_summary = read_file(global_summary_file) + + # --- 1) 更新全局摘要 (若上一章文本不为空) --- + def update_global_summary(chapter_text: str, old_summary: str) -> str: + prompt = summary_prompt.format(chapter_text=chapter_text, global_summary=old_summary) + response = model.invoke(prompt) + if not response: + return old_summary + return response.content.strip() + + if lastchapter.strip(): + # 用上一章内容更新全局摘要 + new_global_summary = update_global_summary(lastchapter, old_global_summary) + else: + new_global_summary = old_global_summary + + # --- 2) 更新角色状态文档 --- + def update_character_state(chapter_text: str, old_state: str) -> str: + prompt = update_character_state_prompt.format(chapter_text=chapter_text, old_state=old_state) + response = model.invoke(prompt) + if not response: + return old_state + return response.content.strip() + + if lastchapter.strip(): + new_char_state = update_character_state(lastchapter, old_char_state) + else: + new_char_state = old_char_state + + # --- 3) 从向量库检索相关上下文,用来帮助生成新的大纲 --- + # 例如,可以根据“角色状态”或“本章关键词”来查询。 + # 简单示范:以 "回顾剧情" 作为检索Query + relevant_context = get_relevant_context_from_vector_store(api_key, "回顾剧情", k=2) + + # --- 4) 大纲 -> 正文 --- + def outline_chapter(novel_setting: str, char_state: str, global_summary: str, chap_num: int, extra_context: str) -> str: + prompt = chapter_outline_prompt.format( + novel_setting=novel_setting, + character_state=char_state + "\n\n【历史上下文】\n" + extra_context, + global_summary=global_summary, + novel_number=chap_num + ) + response = model.invoke(prompt) + if not response: + return "" + return response.content.strip() + + chap_outline = outline_chapter(novel_settings, new_char_state, new_global_summary, novel_number, relevant_context) + + def write_chapter(novel_setting: str, char_state: str, global_summary: str, outline: str, wnum: int, extra_context: str) -> str: + prompt = chapter_write_prompt.format( + novel_setting=novel_setting, + character_state=char_state + "\n\n【历史上下文】\n" + extra_context, + global_summary=global_summary, + chapter_outline=outline, + word_number=wnum + ) + response = model.invoke(prompt) + if not response: + return "" + return response.content.strip() + + chapter_content = write_chapter(novel_settings, new_char_state, new_global_summary, chap_outline, word_number, relevant_context) + + if chapter_content: + # --- 写入 chapter.txt 与 lastchapter.txt --- + append_text_to_file(chapter_content, chapter_file) + + clear_file_content(lastchapter_file) + save_string_to_txt(chapter_content, lastchapter_file) + + # --- 更新全局摘要、角色状态到文件 --- + clear_file_content(character_state_file) + save_string_to_txt(new_char_state, character_state_file) + + clear_file_content(global_summary_file) + save_string_to_txt(new_global_summary, global_summary_file) + + # --- 5) 更新向量检索库 --- + update_vector_store(api_key, chapter_content) + + return chapter_content diff --git a/prompt_definitions.py b/prompt_definitions.py new file mode 100644 index 0000000..b1d9af3 --- /dev/null +++ b/prompt_definitions.py @@ -0,0 +1,129 @@ +""" +集中存放所有提示词(Prompt),便于统一管理和修改。 +""" + +# =============== 提示词:设定 & 目录 =================== + +set_prompt = """\ +请根据主题:{topic}、类型:{genre}、章数:{number_of_chapters}、每章字数:{word_number}来完善小说整体设定。 +需要包含以下信息: +1. 小说名称、总字数走向(大致范围即可)。 +2. 小说类型与基调(如:都市、穿越、战争等类型,以及轻松、爆笑、暗黑等基调)。 +3. 写作风格(正式 / 轻松;细腻 / 简洁;抒情 / 客观;叙事视角等)。 +4. 整体世界观(时间背景、地理环境、社会结构、科技或魔法水平、重要历史传说或事件等)。 +5. 核心内容梗概(可以使用常见叙事结构,如三幕结构、英雄之旅等)。 +6. 初步的情节安排设想(主线、副线、交织等关键思路)。 +7. 初步的人物关系与主要角色设定(角色定位、主要冲突或关系)。 +8. 结尾可能的方向(圆满、悲剧、开放式等)。 + +请按照上述要点详细输出,但不用标数字。要清晰、有逻辑、有条理。 +""" + +character_prompt = """\ +基于已生成的小说整体设定: +{novel_setting} +请你完善以下内容,帮助我们更好地维持人物形象和成长轨迹: +1. 列出核心角色(至少3个),并对每个角色进行详细性格特征描述。 +2. 强调每个角色的潜在内心冲突、目标与动机。 +3. 为每个角色添加至少一个“暗线”或隐藏秘密,以及在故事进行中如何可能被揭示的思路。 +4. 指出主要角色之间的关键关系和冲突点,为后续情节埋下伏笔。 +""" + +dark_lines_prompt = """\ +在当前设定中已出现以下角色与背景: +{character_info} +请帮助我们构思若干暗线、伏笔或隐藏冲突,以便在后续章节中逐渐揭示并影响故事走向。要求: +1. 每个暗线至少说明其最初的表现、发展走向,以及揭示或爆发的条件。 +2. 这些暗线可以与角色背景、世界观、关键事件等有关。 +3. 需注意保留悬念,与已知设定不冲突。 +4. 在后续创作中可多次提及这些暗线,并在中后期通过角色行为或剧情变化逐步揭示。 +""" + +finalize_setting_prompt = """\ +请基于以下信息,整合并输出最终的《小说设定》: +1. 之前的“整体设定”: +{novel_setting_base} +2. 扩充的“角色设定”: +{character_setting} +3. 暗线与伏笔构思: +{dark_lines} + +要求: +1. 结构清晰,将以上内容融合为一个完整的设定说明。 +2. 着重强调角色与暗线的衔接、世界观与角色动机的结合,方便后续写作保持前后一致。 +3. 语言通畅,不使用Markdown格式,直接输出文本内容。 +""" + +novel_directory_prompt = """\ +根据以下最终《小说设定》: +{final_novel_setting} +并按照下面的小说目录模板生成 {number_of_chapters} 章的目录,同时确保目录符合小说设定中的叙事结构、角色发展及暗线伏笔。 +目录模板: +第1章 :< text > +第2章 :< text > +... +第{number_of_chapters}章 :< text > + +请严格按照上述格式输出每一章的名称,且勿使用Markdown语法。 +""" + +# =============== 提示词:章节+角色状态流程 =================== + +summary_prompt = """\ +这是新生成的章节文本: +{chapter_text} + +这是当前的全局摘要(可能为空): +{global_summary} + +请在不超过1000字的前提下,基于当前全局摘要和本章新增剧情,更新全局摘要。 +保留原有重要信息,并融入本章的新内容。 +不要透露结局,不要过度展开未来剧情。 +""" + +update_character_state_prompt = """\ +这是新生成的章节文本: +{chapter_text} + +这是当前角色状态文档(可能为空): +{old_state} + +请更新角色状态,包括: +1. 角色持有的物品或能力变化。 +2. 角色间关系、冲突或合作的新动向。 +3. 正在发生的重要事件列表,有无进展或新事件产生。 +4. 任意新增角色或出场人物等。 +5. 请保证结构完整,能在后续章节继续引用。 + +使用简洁、易读的方式描述,可用条目或段落表示。保持与旧文档风格一致。 +""" + +chapter_outline_prompt = """\ +以下是当前小说设定与角色状态信息: +- 小说设定:{novel_setting} +- 角色状态:{character_state} +- 全局摘要:{global_summary} +- 本章节编号:第 {novel_number} 章 + +请为即将写作的 第{novel_number}章 设计一个简要大纲: +1. 本章的主要冲突或事件? +2. 哪些角色会出现?情感与目标变化? +3. 如何进一步暗示或推动暗线和角色冲突? +4. 如何结尾留下悬念? + +直接用1、2、3、4分点说明即可。 +""" + +chapter_write_prompt = """\ +下面是该章写作所需信息: +1. 小说设定:{novel_setting} +2. 角色状态:{character_state} +3. 全局摘要:{global_summary} +4. 本章大纲:{chapter_outline} + +请写出本章节的完整正文: +1. 确保本章字数不少于 {word_number} 字。 +2. 不要使用分节标题,直接整体输出正文。 +3. 可以着重描写人物心理、环境氛围等,以保证足够长度。 +4. 在结尾部分保留一定悬念或剧情转折,为下一章做铺垫。 +""" diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..6bbcaa6 --- /dev/null +++ b/ui.py @@ -0,0 +1,329 @@ +import os +import tkinter as tk +from tkinter import ttk, filedialog, scrolledtext, messagebox +import threading + +from config_manager import load_config, save_config +from utils import read_file +from novel_generator import ( + Novel_novel_directory_generate, + generate_chapter_with_state +) +from consistency_checker import check_consistency + +class NovelGeneratorGUI: + def __init__(self, master): + self.master = master + self.master.title("Novel Generator GUI - Advanced") + + # 配置持久化 + 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.left_frame = ttk.Frame(main_pane) + main_pane.add(self.left_frame, weight=3) + + # 右侧:参数输入区 + self.right_frame = ttk.Frame(main_pane, padding="10 10 10 10") + main_pane.add(self.right_frame, weight=1) + + # 左侧布局:日志区 + 章节内容 + 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) + + # 日志区 + 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) + + self.log_text = scrolledtext.ScrolledText(log_frame, width=80, height=10) + self.log_text.grid(row=0, column=0, sticky="nsew") + + # 章节内容区 + 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) + + self.chapter_result = scrolledtext.ScrolledText(chapter_frame, width=80, height=10, foreground="blue") + self.chapter_result.grid(row=0, column=0, sticky="nsew") + + def build_right_layout(self, ): + # 行列配置 + for i in range(12): + self.right_frame.rowconfigure(i, weight=0) + self.right_frame.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") + + # 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") + + # 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") + + # 4. 主题(Topic) 多行输入 + ttk.Label(self.right_frame, text="主题(Topic):").grid(row=3, 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=3, 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) + + # 5. 类型(Genre) + ttk.Label(self.right_frame, text="类型(Genre):").grid(row=4, 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=4, column=1, padx=5, pady=5, sticky="w") + + # 6. 章节数 + ttk.Label(self.right_frame, text="章节数:").grid(row=5, 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=5, column=1, padx=5, pady=5, sticky="w") + + # 7. 每章字数 + ttk.Label(self.right_frame, text="每章字数:").grid(row=6, 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=6, 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.filepath_var = tk.StringVar(value=self.loaded_config.get("filepath", "")) + ttk.Entry(self.right_frame, textvariable=self.filepath_var, width=32).grid(row=7, column=1, padx=5, pady=5, sticky="w") + ttk.Button(self.right_frame, text="浏览...", command=self.browse_folder).grid(row=7, column=2, padx=5, pady=5, sticky="w") + + # 保存/加载配置按钮 + config_frame = ttk.Frame(self.right_frame) + config_frame.grid(row=8, 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) + + # 按钮区域 + row_base = 9 + ttk.Label(self.right_frame, text="章节号:").grid(row=row_base, 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=row_base, column=1, padx=5, pady=5, sticky="w") + + self.btn_generate_full = ttk.Button(self.right_frame, text="1. 生成设定 & 目录", command=self.generate_full_novel) + self.btn_generate_full.grid(row=row_base+1, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + self.btn_generate_chapter = ttk.Button(self.right_frame, text="2. 生成单章(含角色状态)", command=self.generate_chapter_text) + self.btn_generate_chapter.grid(row=row_base+2, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + # 可选:添加一个“一致性审校”按钮 + self.btn_check_consistency = ttk.Button(self.right_frame, text="3. 一致性审校", command=self.do_consistency_check) + self.btn_check_consistency.grid(row=row_base+3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + + # -------------- 配置管理 -------------- + 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.model_name_var.set(cfg.get("model_name", "")) + 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.log("已加载配置。") + else: + messagebox.showwarning("提示", "未找到或无法读取配置文件。") + + def save_config_btn(self): + config_data = { + "api_key": self.api_key_var.get(), + "base_url": self.base_url_var.get(), + "model_name": self.model_name_var.get(), + "topic": self.topic_text.get("1.0", tk.END).strip(), + "genre": self.genre_var.get(), + "num_chapters": self.num_chapters_var.get(), + "word_number": self.word_number_var.get(), + "filepath": self.filepath_var.get() + } + if save_config(config_data, self.config_file): + messagebox.showinfo("提示", "配置已保存至 config.json") + self.log("配置已保存。") + else: + messagebox.showerror("错误", "保存配置失败。") + + def browse_folder(self): + selected_dir = filedialog.askdirectory() + 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) + + # -------------- 核心功能按钮 -------------- + def disable_button(self, btn): + btn.config(state=tk.DISABLED) + + def enable_button(self, btn): + btn.config(state=tk.NORMAL) + + def generate_full_novel(self): + """生成小说设定 & 目录""" + def task(): + self.disable_button(self.btn_generate_full) + 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() + topic = self.topic_text.get("1.0", tk.END).strip() + 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() + + if not filepath: + messagebox.showwarning("警告", "请先选择保存文件路径") + return + + self.log("开始生成小说设定和目录...") + Novel_novel_directory_generate( + api_key=api_key, + base_url=base_url, + llm_model=model_name, + topic=topic, + genre=genre, + number_of_chapters=num_chapters, + word_number=word_number, + filepath=filepath + ) + self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。") + except Exception as e: + self.log(f"❌ 生成小说设定 & 目录时出错: {e}") + finally: + self.enable_button(self.btn_generate_full) + + thread = threading.Thread(target=task) + thread.start() + + def generate_chapter_text(self): + """多步生成章节:维护全局摘要+角色状态文档,向量检索辅助""" + def task(): + self.disable_button(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() + novel_number = self.chapter_num_var.get() + filepath = self.filepath_var.get().strip() + word_number = self.word_number_var.get() + + # 读取设定 & 目录 + novel_settings_file = os.path.join(filepath, "Novel_setting.txt") + novel_novel_directory_file = os.path.join(filepath, "Novel_directory.txt") + last_chapter_file = os.path.join(filepath, "lastchapter.txt") + + novel_settings = read_file(novel_settings_file) + novel_novel_directory = read_file(novel_novel_directory_file) + lastchapter = read_file(last_chapter_file) + + if not novel_settings.strip(): + self.log("⚠️ 未找到 Novel_setting.txt,请先生成设定。") + return + if not novel_novel_directory.strip(): + self.log("⚠️ 未找到 Novel_directory.txt,请先生成目录。") + return + + self.log(f"开始生成第{novel_number}章内容(含角色状态文档更新)...") + chapter_text = generate_chapter_with_state( + novel_settings=novel_settings, + novel_novel_directory=novel_novel_directory, + api_key=api_key, + base_url=base_url, + model_name=model_name, + novel_number=novel_number, + filepath=filepath, + word_number=word_number, + lastchapter=lastchapter + ) + + if chapter_text: + self.log(f"✅ 第{novel_number}章内容生成完成。chapter.txt 已更新。") + self.chapter_result.delete("1.0", tk.END) + self.chapter_result.insert(tk.END, chapter_text) + self.chapter_result.see(tk.END) + else: + self.log("⚠️ 本章生成失败或无内容。") + + except Exception as e: + self.log(f"❌ 生成章节内容时出错: {e}") + finally: + self.enable_button(self.btn_generate_chapter) + + thread = threading.Thread(target=task) + thread.start() + + def do_consistency_check(self): + """使用审校Agent对最新章节进行简单一致性或冲突检查""" + def task(): + self.disable_button(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() + filepath = self.filepath_var.get().strip() + + # 读取关键文件 + 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") + last_chapter_file = os.path.join(filepath, "lastchapter.txt") + + novel_setting = read_file(novel_settings_file) + character_state = read_file(character_state_file) + global_summary = read_file(global_summary_file) + last_chapter_text = read_file(last_chapter_file) + + if not last_chapter_text.strip(): + self.log("⚠️ lastchapter.txt 为空,暂无可检查的章节文本。") + return + + self.log("开始一致性审校...") + result = check_consistency( + novel_setting=novel_setting, + character_state=character_state, + global_summary=global_summary, + chapter_text=last_chapter_text, + api_key=api_key, + base_url=base_url, + model_name=model_name + ) + self.log("审校结果:") + self.log(result) + + except Exception as e: + self.log(f"❌ 审校时出错: {e}") + finally: + self.enable_button(self.btn_check_consistency) + + thread = threading.Thread(target=task) + thread.start() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..2c21cfe --- /dev/null +++ b/utils.py @@ -0,0 +1,51 @@ +import os +import json + +def read_file(filename: str) -> str: + """读取文件的全部内容,若文件不存在或异常则返回空字符串。""" + try: + with open(filename, 'r', encoding='utf-8') as file: + content = file.read() + return content + except FileNotFoundError: + return "" + except Exception as e: + print(f"[read_file] 读取文件时发生错误: {e}") + return "" + +def append_text_to_file(text_to_append: str, file_path: str): + """在文件末尾追加文本(带换行)。若文本非空且无换行,则自动加换行。""" + if text_to_append and not text_to_append.startswith('\n'): + text_to_append = '\n' + text_to_append + + try: + with open(file_path, 'a', encoding='utf-8') as file: + file.write(text_to_append) + except IOError as e: + print(f"[append_text_to_file] 发生错误:{e}") + +def clear_file_content(filename: str): + """清空指定文件内容。""" + try: + with open(filename, 'w', encoding='utf-8') as file: + pass + except IOError as e: + print(f"[clear_file_content] 无法清空文件 '{filename}' 的内容:{e}") + +def save_string_to_txt(content: str, filename: str): + """将字符串保存为 txt 文件(覆盖写)。""" + try: + with open(filename, 'w', encoding='utf-8') as file: + file.write(content) + except Exception as e: + print(f"[save_string_to_txt] 保存文件时发生错误: {e}") + +def save_data_to_json(data: dict, file_path: str) -> bool: + """将数据保存到 JSON 文件。""" + try: + with open(file_path, 'w', encoding='utf-8') as json_file: + json.dump(data, json_file, ensure_ascii=False, indent=4) + return True + except Exception as e: + print(f"[save_data_to_json] 保存数据到JSON文件时出错: {e}") + return False