From c99601fe1048807ba9cacfc32cd7909c10bbcb01 Mon Sep 17 00:00:00 2001 From: YILING0013 Date: Wed, 29 Jan 2025 20:33:20 +0800 Subject: [PATCH] Initial commit --- .gitattributes | 2 + AI_novel/Novel_directory.txt | 101 ++++++ AI_novel/Novel_setting.txt | 36 ++ AI_novel/chapter.txt | 72 ++++ AI_novel/character_state.txt | 0 AI_novel/global_summary.txt | 0 AI_novel/lastchapter.txt | 71 ++++ __pycache__/config_manager.cpython-310.pyc | Bin 0 -> 969 bytes .../consistency_checker.cpython-310.pyc | Bin 0 -> 1421 bytes __pycache__/novel_generator.cpython-310.pyc | Bin 0 -> 9107 bytes .../prompt_definitions.cpython-310.pyc | Bin 0 -> 5291 bytes __pycache__/ui.cpython-310.pyc | Bin 0 -> 10499 bytes __pycache__/utils.cpython-310.pyc | Bin 0 -> 2480 bytes config.json | 10 + config_manager.py | 21 ++ consistency_checker.py | 51 +++ main.py | 11 + novel_generator.py | 317 +++++++++++++++++ prompt_definitions.py | 129 +++++++ ui.py | 329 ++++++++++++++++++ utils.py | 51 +++ 21 files changed, 1201 insertions(+) create mode 100644 .gitattributes create mode 100644 AI_novel/Novel_directory.txt create mode 100644 AI_novel/Novel_setting.txt create mode 100644 AI_novel/chapter.txt create mode 100644 AI_novel/character_state.txt create mode 100644 AI_novel/global_summary.txt create mode 100644 AI_novel/lastchapter.txt create mode 100644 __pycache__/config_manager.cpython-310.pyc create mode 100644 __pycache__/consistency_checker.cpython-310.pyc create mode 100644 __pycache__/novel_generator.cpython-310.pyc create mode 100644 __pycache__/prompt_definitions.cpython-310.pyc create mode 100644 __pycache__/ui.cpython-310.pyc create mode 100644 __pycache__/utils.cpython-310.pyc create mode 100644 config.json create mode 100644 config_manager.py create mode 100644 consistency_checker.py create mode 100644 main.py create mode 100644 novel_generator.py create mode 100644 prompt_definitions.py create mode 100644 ui.py create mode 100644 utils.py 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 0000000000000000000000000000000000000000..c8224eae26bcaf179ade542453c1ea2acf21ca28 GIT binary patch literal 969 zcmY*Y&rcIU6rP!#E-MABXhPyad(cA8Wcv~FE?o0(ZaQWB$} z1gUZ`5y2mjsDT5>k)VXTf5Kkr(sJ_70i4;kh;Q@WzV~MG-rMhc>$bPI0x64K3;Ev& zfL#@w6kP^4rSe{B2q4e~srnnHAVCxnjcPeeFo{g!6ec>=8reurCoRN~S)ITEkT|t3 zD}baQ&ALv`%4c$xO?{nF;V~x*)mcO27{HKvTPlAjjQ|C>gi2r;vhMM?&`NR)A{&>r zw;dBu;H5|jF2f;!0Kphw@Ir%N2n>L0fL|Pi5UA&Y)FEKWY%^BA(nQ4TmrW`^-|Wr4 z3f32@3zP8^BgTW7rP}uP{#3cTz5eIdZ0+^8%4RuOn-4y&1k)dCJMV(U&(+nfVC{V{ zwYmH3xewVv#1pyh>#NW=sFQVx<>ZqI-A69>anTfyeVvY3T*$~RQpd*y?n-OBCP^Ts zKHrDARlrV5@1Q5!lfK6(J~#MABntRLdqW#1v*$4bc=$I>&t6m%;dD0ZQJzz=Gtbgwz@ky^Iysq~#wM#0c5A@IMIUjn|e}s$aME=6?ip-}k2Hf|)NNC!;JPOEb03eftWF z1WL70hkJ}>OrEtYU$-1W9g!SNXe_Gkwn^h-;tdyNMj;nc!#b3wsHvNnx)tT-2yFnZ z&O@LPiotWR^+-ptLmlZ*hpbDUC0fGDOhM{vTrkt?{;k6Sj!e(I~qj%UP To=tD4|Angx1~6bJI^X&i*J=vE literal 0 HcmV?d00001 diff --git a/__pycache__/consistency_checker.cpython-310.pyc b/__pycache__/consistency_checker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a88e555f7ab805fbbc93d311037f85ba990955e0 GIT binary patch literal 1421 zcmZux%}*Og6yF`MKUh9Os(z^_j`g9v^j1|#DXl6Fg{Y*5Ql!xVCBf@n%Vi(`HuhX=mrnd%xfNSfjO7 z7GUkZk&Ba{AUx)q#?=m+4?)b2Kxsn1potnviYe5OL_rJm2Q&#wS`Z`z5(bHYG=az< z%~-nFuC-{b<5GW6YwHuDr!sTkNM@DA+WJGfp6iTb-D1wFw_C36-K*usA31qiDzZY+ zKY8S=9#v*(d7tGSwzOTlTkx)rd50@rakf@+X!a)DsO~x5+)>_EFuNsjq$yq3HI!SKjeT2eg!{?#(rVV#{T^utD!F)BF}&o@2`g&rnP)Rrj*T z7_0j;bbXu?+8AT`Y4A^{ZqdnyN0prXPDi72{^e`kxQvsEi7hLk$46u^@ixElrXSR< z|HPcpqskQU@~-|uXD8^w-TGt}s8tV(Y_xn-nf4vT234ZQEKC&BQZ4Mepc7m4zQb-T zgSBrIPZ}3g2$M;rR5I+_H1eX?CTMB@xw|xXX5lLq*l`kT`D4ca=L#%=A7l%Xaa391 zxtaQwe>q&FD{D|?cB{~sC#W>9h1>c9YxmTDIJj*u;&ofV;9pd3SWPFCAw29xE~q9} zG9>BBDMN!a>S_wRA%d+8(PLp=@n?(oe+Z-t#5@Da5-y3hV2d`g1DcqXM#2`dL$+iG zHKYY#AF(5afE_N1BTaUb`IaX233k&ZsSqp*(BZ+%SKvhMfaPsptu1b`ts-5Up`*E% zbnNpM6ojs4qapW|uHK%$?!NP%^mKixocp}@ zvvcR&&_#o!REr?)D3s7I8$;L)#|+(q$#sLKMH1<#{4z)<07#R6IZcdI+9J(-6Cv14 z8@h>a3xp$jayItTzhTgsvFyM^FNS2E`2ez?N#Zl`2NBG-K}kZ3Bq4Mf;{J^g a`U?fnpGZPaQ4swBXr7=j`W;2ue)|W)Vpy;M literal 0 HcmV?d00001 diff --git a/__pycache__/novel_generator.cpython-310.pyc b/__pycache__/novel_generator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ad9ee7b5b20126c33296ccf8eaf312bd9c013b7 GIT binary patch literal 9107 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 Am;e9( literal 0 HcmV?d00001 diff --git a/__pycache__/ui.cpython-310.pyc b/__pycache__/ui.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c305f65fc4d6a1155a7f72d77661bd2f49e98e6 GIT binary patch literal 10499 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 JkTidv{J)>nvqt~` literal 0 HcmV?d00001 diff --git a/__pycache__/utils.cpython-310.pyc b/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b7873fe48beffed7229bad092971f2c07f4e358 GIT binary patch literal 2480 zcmai0?QaxC7@wKjdu@C41%e>rp+P*2Cin@__!6WBKO8aD5KGDByxEq;WA`#UOJ8zF z2rq3#DlsWV+Jdc#O;MnYCgg-F|Hpo%d%b?@cRqm6GkZO+FX&!wcJ`TlX6ByX%beBJ z6eDoWZ6EI$4HNPhAL_3VJbVOS+k}P_PIIKwztc|2na+4XXWbgtc<`du8Q>v^gjFQM zqY%;Xoo>csyaAqqyb;DRqVXmkhe(KT(ZiSl^F+EsyqUK^G|FiQNwzM-nk3~$x{aJ} zW=`vFNa)fLW|rbW_0r&uzu^#k?O|vJ5|R{PryS-S30#dAX@O4BX;Ngz2-h^CkqZGz z3h>lH_7i(YFQo)OU%}UpYDBXBH&&xO*a0~*(0_=MB4iH4rN%>rzy!I;Em zBi`K~y~)|~@R+x7-Yb>7;Tx4lqu#`=rP~j@yVsZQ%q*6Ee|+w|OU09*BfH-^aGSaj z-OO6tFndxiagwmyaiM2;8Y^}pQNMKg`hD-}4@;vzEsx%)%*`E4B!jMI<#p55FvD$W zhm@2h+bzfBhlQ|&8$H~Y)$`J@OgESphAESpV6cR%;q-1eYnf6vrAzhOM6|%bll|F) zU0>R|u)F#UJ!f=%qTAm{E8q3;k#x#Bqvt-=OjjK=O9D6F-a079v_sO)h(wRK`0`5j}L+N}*p3LF~SFbhH_#UDK zA{q*03dk`{?FLQ@WIHL6fjtr^f^^{;5N1n}71$IjkZA=j53W)M=mP))W%kxn#*Zq3 zwEqV|f;EDuUX>tSuqnh`v7YV=<Sj1BU^Z(5<}wr#Q`zGq`@ zoJA;t-bi127H7LC0R2q72m@<`9)Lhi=n@_VcC8V5tVrtxj(vsJwsmMh6wspm?*dv3 zPrOVQBEDTp>2A3wryulkQ3*Q0?0*#~|2b|`( zojIeYZ3$l1gYE$noJjcdRTa3wBsIVKJ8-r}Y^s#UlKmeJ8&`W3vT8W2Vf$_vaW$Ti zndbv>4&E8qy9S81B9rtKcvS&-%7Z*KqTzwW!*!k&Q7YKI4iJbc&-$|WKX)@+A%NB^P8vuH#LrNK#Q2MHZYh 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