Initial commit

This commit is contained in:
YILING0013
2025-01-29 20:33:20 +08:00
commit c99601fe10
21 changed files with 1201 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+101
View File
@@ -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章 :新世界的曙光
+36
View File
@@ -0,0 +1,36 @@
小说设定
小说名称: 星陨提瓦特
总字数走向: 约40万字,分为100章,每章约4000字。整体走向为主角经历成长、探索与冲突,逐渐揭开提瓦特大陆的秘密与危机,最终实现拯救与情感的圆满。
小说类型与基调: 本作为玄幻、穿越与冒险题材,主线围绕丰富的爱恨情仇展展开。基调相对轻松,同时穿插挑战与深思,旨在营造出一种既有趣又引人深思的氛围。
写作风格: 采用细腻、抒情的写作风格,重点描绘角色内心的探索与情感。叙事视角主要为第三人称全知视角,辅以第一人称的内心独白,以增强角色深度。
整体世界观: 故事设定在提瓦特大陆,这是一个拥有七个不同国度和独特元素力量的神秘幻想世界。历史与传说丰富,比如“天理之战”及各个神祇的传奇故事。科技与魔法并存,元素魔法高度发达,社会结构以王国与神明为核心,复杂的势力交错使人民生活受其影响。
核心内容梗概: 故事采用英雄之旅结构,主角星因意外穿越到提瓦特,面临神明失去信仰、元素失衡,国家动荡的空前危机。在寻找回家的过程中,星结识了来自不同国家的伙伴,体验各地人文风情,并逐渐揭示穿越的真相。最终,星与伙伴们合力对抗共同的敌人,重塑世界的平衡,也收获真挚的情感。
情节安排设想: 主线围绕星的成长与拯救提瓦特的冒险分为三个阶段:第一阶段是星的穿越与初识,以及角色冲突的引入;第二阶段是与伙伴的深入互动,逐步揭露敌人阴谋及情感发展;第三阶段是最终决战与角色命运交织。副线聚焦其他角色的故事,展示他们与星的命运交织,为故事营造丰富的世界感。
角色关系与主要角色设定:
1. 星(主角): 一名勇敢、好奇且善良的年轻人,满怀探索未知的欲望,面临对归属感的挣扎。在拯救提瓦特的过程中,她渴望成为真正的英雄,同时也处理与伙伴间复杂的情感关系。
2. 伊尔(冷静的魔法师): 理智、谨慎,拥有强大师法的他在经历失败后变得冷淡,内心深处隐藏痛苦。希望能通过知识与能力解决危机,努力解脱自己的内心痛楚。与星的理想与现实相悖,易引发冲突。
3. 莱娜(隐秘的元素使者): 神秘而强大的她深谙元素魔法,在寻找与星的关系中,探索自己的身份之谜。与伊尔之间存在信任危机,随着故事发展,二人将面临信任与情感的考验。
暗线与伏笔构思:
1. 星的过往: 星对旧生活的怀念与痛苦逐渐显现,推动她在救助他人和回归原生活之间的抉择,进而引发与伙伴之间的深入讨论。
2. 伊尔的秘密: 伊尔对过去的隐瞒以及与敌对势力的牵连,在关键时刻暴露,导致团队关系动摇,让星更深刻地理解他的痛苦。
3. 莱娜的使命: 莱娜的神秘身份及意图逐步显露,最终必须面对身份的真实,仅在关键时刻,有赖于星与伊尔的支持,坚定她内心的归属感。
4. 星与伊尔的理念冲突: 星与伊尔在道德取向上发生摩擦,在选择重大事件时,两人的冲突达到顶峰,最终通过事件的解决促进彼此之间的理解与成长。
在这些设定中,角色的动机与暗线及伏笔相互交织,为整个故事增添了复杂性与紧张感,确保每一位角色的成长与情感发展贯穿始终,丰富了提瓦特大陆的冒险传奇。
+72
View File
@@ -0,0 +1,72 @@
星缓缓睁开眼睛,意识到自己正躺在一片陌生的土地上。四周是葱茏的树木,阳光透过树叶洒下斑驳的光影,微风轻拂,带来一丝清新的气息。她试图站起身来,身体却感到无力,仿佛刚经历了一场翻天覆地的梦境。迷茫与恐惧瞬间涌上心头——她究竟身处何地?
“别 panicking,星。”她自言自语,努力让自己冷静下来。可无论她怎么深呼吸,心中的慌乱与不安依旧如影随形。刚才她还在自己的家中,思绪被一扇打开的窗户吸引,目光追随着飞过的鸟儿,突然遭遇了一道神秘的光芒,她的意识就在那瞬间被撕扯了开来。而现在,她却身处于这个陌生的世界,这一切都让她感到无比的恐慌。
尝试坐起身来,星环顾四周,发现这片森林似乎与她的记忆截然相反。树木高耸入云,树干粗壮而苍翠,叶子则在微风中轻轻摇晃,发出沙沙的声响。然而,这些美丽的自然景象并没有为她带来一丝安慰——相反,她觉得一种不安的气息弥漫在空气中,仿佛这片森林隐藏着某种不为人知的秘密。
星的心中掠过一丝奇异的感觉,似乎这片土地上有着某种她无法理解的力量在流动。她强迫自己站起身,决定探索这个世界。若想办法找到回家的路,先得了解这个地方。她开始慢慢前行,脚下的土地干燥而坚硬,偶尔踩到几根枯枝,发出清脆的声响。
随着深入森林,她注意到周围的植物似乎散发着微弱的光芒,仿佛在回应着她的存在。星试图触摸那些植物,感受那股能量,却发现它们在她的手指间划过时变成了莹莹的光点,瞬间就消散了。她心中一紧,这显然不是普通的植物,她的直觉告诉她,自己可能身处于一个充满魔法的地方。
就在这时,她的思绪被一阵低语声打断。星停下脚步,四处张望,试图找到声音的来源。声音似乎从四面八方传来,但却又似乎是从某个深渊中回荡而出,朦胧而模糊。
“是谁在说话?”星喃喃自语,然而没有人回应。她心中浮现出一丝不安,迫切想要找到声音的源头。正当她准备继续探索时,眼前突然闪过一道亮光。她的心一颤,抬头看去,透过树木的缝隙,她隐约看到一个身影,似乎在远处的空旷地带移动。
出于好奇,星忍不住向前走去,穿越过一片茂密的灌木丛。终于,她走出林间,来到了一个开阔的空地上。眼前的一幕让她惊讶不已——一座宏伟的法阵正镶嵌于地面,阵法复杂而美丽,闪烁着微光,似乎在向她召唤。
“这是什么?”星自言自语,心中充满了疑惑与好奇。她走到法阵边缘,感觉到一股温暖的气流从阵法中透出。星的手指微微颤动,不由自主地伸向了那个法阵,触碰到了那些复杂的符文。瞬间,她感到一阵剧烈的震动,法阵光芒大盛,星的身体被卷入其中,意识在瞬间模糊而又清晰。
“啊!”她惊呼,仿佛被一股无形的力量撕扯着。整个世界都在颠覆,光芒刺眼,四周的景象如同狂风中的飘絮般不断变换,星感到自己像是随风而舞的叶子,无法控制。就在意识即将消散的瞬间,她听到了那个低语声,竟然清晰而响亮:“命运的选择。”
然后,星失去了意识。
当她再次睁开眼睛时,发现自己坐在一块石头上,周围是一片开阔的草地,远处是连绵起伏的山脉,天空湛蓝,阳光温暖,仿佛一切都是那么美好。可在她心中,却依然残留着那股恐惧与不安。
“这是哪里?”她自言自语,四处打量。就在这时,一个阴影笼罩了她,星抬头一看,面前站着一个高大的男子,身穿长袍,手中握着法杖,面容冷峻,眼神中透着智慧与审视。
“你是谁?”星有些警惕地问道,虽然心中已经有了几分预感。
“我是伊尔,一个魔法师。”男子的声音低沉而有力,似乎带着某种压迫感,“你应该是从那个法阵中被传送过来的吧?”
星点点头,虽然内心依旧充满困惑,但她知道自己必须与眼前这个陌生人好好交流。“是的,我…我不知道自己为什么来到这里。”
伊尔微微皱眉,目光在她身上扫视,似乎在打量她的言语是否可信。“这片土地并不安全,你的到来可不是什么偶然。元素的平衡正在崩溃,神明的意志也逐渐淡去。”
“神明的意志?”星困惑地问道,心中浮现出关于提瓦特的传说与故事。她从小就听说过这个富饶而神秘的大陆,但从未想过自己会置身其中。
“没错,”伊尔坚定地点头,“提瓦特大陆上,七位神明各自守护着一个国度,而你在这里的出现,可能与某种更大的命运息息相关。”
“我只想回家。”星低声说道,心中依然渴望着熟悉的生活,尽管她也感受到这个新的世界是如此吸引人。
“回家并不是那么简单。”伊尔的声音中透着冷静,他的眼神中闪烁着某种复杂的情感,“在这片土地上,你必须要面对许多你无法想象的挑战。”
星感到了一丝绝望,她并不想成为一名英雄,自己不过是一名普通的年轻人。“我……我能做些什么?”
“或许我们可以一起探索这个世界。”伊尔的语气缓和了一些,似乎对她产生了一丝信任,“我对魔法的研究或许能帮助你找到线索。”
“谢谢你,伊尔。”星感激地点头,虽然她对眼前的陌生人依然有些顾虑,但她知道自己必须要有人陪伴,才能在这个陌生的世界中生存下去。
就在这时,草地的另一侧,一道神秘的光芒闪过,随即出现了另一个身影。那是一位少女,长发披肩,身穿轻盈的衣袍,眼神深邃而神秘,她似乎在默默地注视着星与伊尔。
“你是……”星愣了一下,有些惊讶。
“我叫莱娜。”少女轻声说道,她的声音如同空灵的风铃,给人一种奇特的亲切感,“我感觉到了你的到来,希望能和你们一起探索这个世界。”
“你也感知到了?”伊尔微微皱眉,似乎对莱娜的出现有些意外,却又没有质疑的意思。
“是的。”莱娜的眼神透着某种不易察觉的情感,“我对自然元素有着强烈的感应,能够感受到世界的变化。”
星心中一震,莱娜的出现让她感到了一丝安慰,也增添了对这个世界的探索欲望。“或许我们可以一起合作,找出这个世界的秘密。”
莱娜微微一笑,仿佛认可了星的提议。“我相信,我们的命运将会交织在一起。”
三人之间逐渐形成了一种微妙的默契。尽管星对这个新世界的未来仍感到模糊不清,内心的惶恐也未完全消散,但在伊尔与莱娜的陪伴下,她逐渐意识到自己不再孤单。接下来,等待她的将是未知的冒险,她必须做好面对的准备。
正当星准备问伊尔关于这个世界的更多事情时,突然,一股强烈的能量波动猛然袭来,她耳边仿佛听到了那道低语的声音再度浮现,清晰而坚定:“命运的选择,终于来临!”
星的心脏骤然加速,四周的空气似乎在那一瞬间凝固。她抬头,看到远处的天空中,一片阴云迅速聚集而来,预示着某种危险的逼近。一个模糊的影像在天际闪现,似乎正暗示着未来的冲突与挑战,而她所要面对的,绝不仅仅是对自我归属的探索。
“我们要走!”伊尔的声音打断了星的思绪,他的目光变得凝重,“不可忽视的危机即将降临,我们必须立刻找到安全的地方!”
星与莱娜对视一眼,心中都涌起一股紧迫感。对即将来临的挑战,三人并肩而立,准备迎接未知的未来。此刻,她们的命运已经交织在了一起,故事的篇章才刚刚开启,而未来的旅程充满了未知与悬念。
View File
View File
+71
View File
@@ -0,0 +1,71 @@
星缓缓睁开眼睛,意识到自己正躺在一片陌生的土地上。四周是葱茏的树木,阳光透过树叶洒下斑驳的光影,微风轻拂,带来一丝清新的气息。她试图站起身来,身体却感到无力,仿佛刚经历了一场翻天覆地的梦境。迷茫与恐惧瞬间涌上心头——她究竟身处何地?
“别 panicking,星。”她自言自语,努力让自己冷静下来。可无论她怎么深呼吸,心中的慌乱与不安依旧如影随形。刚才她还在自己的家中,思绪被一扇打开的窗户吸引,目光追随着飞过的鸟儿,突然遭遇了一道神秘的光芒,她的意识就在那瞬间被撕扯了开来。而现在,她却身处于这个陌生的世界,这一切都让她感到无比的恐慌。
尝试坐起身来,星环顾四周,发现这片森林似乎与她的记忆截然相反。树木高耸入云,树干粗壮而苍翠,叶子则在微风中轻轻摇晃,发出沙沙的声响。然而,这些美丽的自然景象并没有为她带来一丝安慰——相反,她觉得一种不安的气息弥漫在空气中,仿佛这片森林隐藏着某种不为人知的秘密。
星的心中掠过一丝奇异的感觉,似乎这片土地上有着某种她无法理解的力量在流动。她强迫自己站起身,决定探索这个世界。若想办法找到回家的路,先得了解这个地方。她开始慢慢前行,脚下的土地干燥而坚硬,偶尔踩到几根枯枝,发出清脆的声响。
随着深入森林,她注意到周围的植物似乎散发着微弱的光芒,仿佛在回应着她的存在。星试图触摸那些植物,感受那股能量,却发现它们在她的手指间划过时变成了莹莹的光点,瞬间就消散了。她心中一紧,这显然不是普通的植物,她的直觉告诉她,自己可能身处于一个充满魔法的地方。
就在这时,她的思绪被一阵低语声打断。星停下脚步,四处张望,试图找到声音的来源。声音似乎从四面八方传来,但却又似乎是从某个深渊中回荡而出,朦胧而模糊。
“是谁在说话?”星喃喃自语,然而没有人回应。她心中浮现出一丝不安,迫切想要找到声音的源头。正当她准备继续探索时,眼前突然闪过一道亮光。她的心一颤,抬头看去,透过树木的缝隙,她隐约看到一个身影,似乎在远处的空旷地带移动。
出于好奇,星忍不住向前走去,穿越过一片茂密的灌木丛。终于,她走出林间,来到了一个开阔的空地上。眼前的一幕让她惊讶不已——一座宏伟的法阵正镶嵌于地面,阵法复杂而美丽,闪烁着微光,似乎在向她召唤。
“这是什么?”星自言自语,心中充满了疑惑与好奇。她走到法阵边缘,感觉到一股温暖的气流从阵法中透出。星的手指微微颤动,不由自主地伸向了那个法阵,触碰到了那些复杂的符文。瞬间,她感到一阵剧烈的震动,法阵光芒大盛,星的身体被卷入其中,意识在瞬间模糊而又清晰。
“啊!”她惊呼,仿佛被一股无形的力量撕扯着。整个世界都在颠覆,光芒刺眼,四周的景象如同狂风中的飘絮般不断变换,星感到自己像是随风而舞的叶子,无法控制。就在意识即将消散的瞬间,她听到了那个低语声,竟然清晰而响亮:“命运的选择。”
然后,星失去了意识。
当她再次睁开眼睛时,发现自己坐在一块石头上,周围是一片开阔的草地,远处是连绵起伏的山脉,天空湛蓝,阳光温暖,仿佛一切都是那么美好。可在她心中,却依然残留着那股恐惧与不安。
“这是哪里?”她自言自语,四处打量。就在这时,一个阴影笼罩了她,星抬头一看,面前站着一个高大的男子,身穿长袍,手中握着法杖,面容冷峻,眼神中透着智慧与审视。
“你是谁?”星有些警惕地问道,虽然心中已经有了几分预感。
“我是伊尔,一个魔法师。”男子的声音低沉而有力,似乎带着某种压迫感,“你应该是从那个法阵中被传送过来的吧?”
星点点头,虽然内心依旧充满困惑,但她知道自己必须与眼前这个陌生人好好交流。“是的,我…我不知道自己为什么来到这里。”
伊尔微微皱眉,目光在她身上扫视,似乎在打量她的言语是否可信。“这片土地并不安全,你的到来可不是什么偶然。元素的平衡正在崩溃,神明的意志也逐渐淡去。”
“神明的意志?”星困惑地问道,心中浮现出关于提瓦特的传说与故事。她从小就听说过这个富饶而神秘的大陆,但从未想过自己会置身其中。
“没错,”伊尔坚定地点头,“提瓦特大陆上,七位神明各自守护着一个国度,而你在这里的出现,可能与某种更大的命运息息相关。”
“我只想回家。”星低声说道,心中依然渴望着熟悉的生活,尽管她也感受到这个新的世界是如此吸引人。
“回家并不是那么简单。”伊尔的声音中透着冷静,他的眼神中闪烁着某种复杂的情感,“在这片土地上,你必须要面对许多你无法想象的挑战。”
星感到了一丝绝望,她并不想成为一名英雄,自己不过是一名普通的年轻人。“我……我能做些什么?”
“或许我们可以一起探索这个世界。”伊尔的语气缓和了一些,似乎对她产生了一丝信任,“我对魔法的研究或许能帮助你找到线索。”
“谢谢你,伊尔。”星感激地点头,虽然她对眼前的陌生人依然有些顾虑,但她知道自己必须要有人陪伴,才能在这个陌生的世界中生存下去。
就在这时,草地的另一侧,一道神秘的光芒闪过,随即出现了另一个身影。那是一位少女,长发披肩,身穿轻盈的衣袍,眼神深邃而神秘,她似乎在默默地注视着星与伊尔。
“你是……”星愣了一下,有些惊讶。
“我叫莱娜。”少女轻声说道,她的声音如同空灵的风铃,给人一种奇特的亲切感,“我感觉到了你的到来,希望能和你们一起探索这个世界。”
“你也感知到了?”伊尔微微皱眉,似乎对莱娜的出现有些意外,却又没有质疑的意思。
“是的。”莱娜的眼神透着某种不易察觉的情感,“我对自然元素有着强烈的感应,能够感受到世界的变化。”
星心中一震,莱娜的出现让她感到了一丝安慰,也增添了对这个世界的探索欲望。“或许我们可以一起合作,找出这个世界的秘密。”
莱娜微微一笑,仿佛认可了星的提议。“我相信,我们的命运将会交织在一起。”
三人之间逐渐形成了一种微妙的默契。尽管星对这个新世界的未来仍感到模糊不清,内心的惶恐也未完全消散,但在伊尔与莱娜的陪伴下,她逐渐意识到自己不再孤单。接下来,等待她的将是未知的冒险,她必须做好面对的准备。
正当星准备问伊尔关于这个世界的更多事情时,突然,一股强烈的能量波动猛然袭来,她耳边仿佛听到了那道低语的声音再度浮现,清晰而坚定:“命运的选择,终于来临!”
星的心脏骤然加速,四周的空气似乎在那一瞬间凝固。她抬头,看到远处的天空中,一片阴云迅速聚集而来,预示着某种危险的逼近。一个模糊的影像在天际闪现,似乎正暗示着未来的冲突与挑战,而她所要面对的,绝不仅仅是对自我归属的探索。
“我们要走!”伊尔的声音打断了星的思绪,他的目光变得凝重,“不可忽视的危机即将降临,我们必须立刻找到安全的地方!”
星与莱娜对视一眼,心中都涌起一股紧迫感。对即将来临的挑战,三人并肩而立,准备迎接未知的未来。此刻,她们的命运已经交织在了一起,故事的篇章才刚刚开启,而未来的旅程充满了未知与悬念。
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -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"
}
+21
View File
@@ -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
+51
View File
@@ -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()
+11
View File
@@ -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()
+317
View File
@@ -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
+129
View File
@@ -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. 在结尾部分保留一定悬念或剧情转折,为下一章做铺垫。
"""
+329
View File
@@ -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()
+51
View File
@@ -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