记录二次更改

This commit is contained in:
YILING0013
2025-02-02 12:59:13 +08:00
parent bcd13b8ca3
commit f2dc98744d
5 changed files with 401 additions and 107 deletions
+82 -1
View File
@@ -98,4 +98,85 @@
第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战 第97章 :新的旅程 - 星河踏上新的旅程,迎接新的挑战
第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千 第98章 :回首往昔 - 回首在提瓦特的冒险,感慨万千
第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存 第99章 :未来的希望 - 提瓦特的未来充满希望,光明与挑战并存
第100章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局 第100章 :星河挽歌 - 故事的结尾,留下无限的遐想,开放式结局
<think>
嗯,我需要按照用户提供的《小说设定》来生成100章的目录。首先,我得仔细阅读并理解这个设定,确保每一章都紧扣主题和叙事结构。
世界观设定:暗影之歌
在提瓦特大陆上,暗影力量正在苏醒,带来了新的挑战和阴谋。主要角色包括星辰、艾莎、凯文,以及暗影力量本身。故事探讨了力量与责任、身份与命运、信任与背叛等主题。
目录模板
第1章:<text> - <简述>
...
第100章:<text> - <简述>
接下来,我需要规划每一章的内容,确保涵盖起始点、初识艾莎、凯文的动机、危机初现、真相浮现、高潮对决以及开放式结局。
起始点
星辰被选中成为暗影力量的继承者,但她对自己的能力感到不安,同时怀疑“暗影力量”是否真是保护大陆的存在。这一部分需要引入角色和设定,铺垫后续的冲突。
初识艾莎
艾莎以刺客的身份接近星辰,试图利用她的暗影能力。两人合作中发现彼此的共同点和隐藏的冲突,这可以增加角色的互动和紧张感。
凯文的动机
凯文表面上支持星辰和艾莎,但实际上有自己的秘密。他可能是“暗影力量”的后代,或者另有隐情。这部分需要揭示凯文的多面性和潜在威胁。
危机初现
随着暗影力量的觉醒,大陆出现诡异事件。暗影力量失控,普通民众受害。这可以是故事转折点,展示力量带来的危险。
真相浮现
通过对凯文、艾莎和星辰的深入探索,揭示他们的过去与命运联系。他们之间存在更大的联系,关系到暗影力量的未来。
高潮对决
关键时刻,星辰和艾莎必须共同面临强敌。这场对决是力量的较量,更是信任与背叛的考验,增加戏剧性和紧张感。
结局模糊
故事留下开放结局,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路,或者被力量吞噬。
在规划每一章时,我需要确保情节连贯,角色发展合理,同时埋下伏笔,为后续章节做好铺垫。例如,在初识艾莎的阶段,可以设置她的多面性和潜在的秘密,为后期冲突做准备。
此外,每章的标题应简洁明了,能够反映主要情节或角色的发展,并附上简短的提示性简述,帮助读者了解每一章的内容。
现在,我需要将这些思考整理成一个100章的目录,确保每一章都紧扣设定中的叙事结构和角色发展,同时埋下足够的暗线伏笔。
</think>
第1章 :星辰的选择 - 星辰被选中为暗影力量的继承者,但内心充满疑惑与不安。
第2章 :暗影之息 - 星辰在练习中意外触发暗影力量,引发混乱,开始探索自己的能力边界。
第3章 :艾莎的来临 - 艾莎以刺客身份接近星辰,试图获取更多关于暗影力量的信息,同时对星辰产生复杂情感。
第4章 :凯文的真实意图 - 凯文表面上支持星辰和艾莎,但逐渐显露出多面性和隐藏的秘密,引起星辰的警觉。
第5章:暗影失控 - 随着暗影力量的强化,大陆出现一系列诡异事件,普通民众受害,暗影力量开始失控。
第6章:艾莎的内心挣扎 - 艾莎在完成任务后,发现自己逐渐被暗影力量控制,对自己的选择感到恐慌和纠结。
第7章:凯文的秘密揭露 - 凯文被迫公开他的多面性和隐藏的秘密,星辰和艾莎联手调查他的真实意图。
第8章:命运的交错 - 通过对凯文、艾莎和星辰的深入了解,逐渐揭示他们之间存在更大的联系关系到暗影力量的未来走向。
第9章:危机加剧 - 凯文与暗影力量的势力开始对星辰和艾莎构成直接威胁,两人必须面临更为严峻的挑战。
第10章:信任的考验 - 在关键时刻,星辰和艾莎之间的信任面临突破,但也隐藏着潜在的背叛阴谋。
第11章:暗影力量的真相 - 星辰通过回忆和资料,逐渐揭开暗影力量的起源和其对世界的影响。
第12章:凯文的最终动向 - 凯文的双重身份和真实意图被彻底揭露,他的选择将对暗影力量的未来产生重大影响。
第13章:星辰的觉醒 - 星辰在与艾莎的合作中逐渐意识到自己肩负的责任,开始寻找解决问题的方法。
第14章:艾莎的挣扎 - 艾莎被迫面对自己的过去和现在的选择,她是否能站出来对抗暗影力量?
第15章:凯文的覆灭 - 凯文最终的命运如何?他的计划是否能够成功,或者他将成为阻碍?
第16章:暗影之歌 - 星辰和艾莎联手面临最后的危机,他们需要找到一个办法来控制暗影力量。
第17章:力量与责任的抉择 - 在关键时刻,星辰和艾莎必须做出艰难的决定,选择自己的道路还是被力量吞噬。
第18章:命运的终结 - 故事进入高潮,暗影力量的威胁更加严峻,星辰和艾莎必须团结起来对抗它。
第19章:最后的对决 - 星辰和艾莎面临最终的对决,他们的能力和信任将决定世界的命运。
第20章:暗影之光 - 故事情节以开放式结束,暗示暗影力量的未来影响,星辰和艾莎是否能找到自己的道路?
+66 -1
View File
@@ -30,4 +30,69 @@
结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。 结局:开放式结局,主角战胜危机,但提瓦特仍面临挑战,主角可以选择留在提瓦特或回到星穹铁道;也可能根据剧情走向相对圆满结局。
世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。 世界观与角色动机结合:提瓦特大陆面临的危机是推动星河行动的主要动力,而他过去的经历(星穹铁道任务的阴影和伙伴的牺牲)以及对归宿的渴望,则构成了他内心的冲突和驱动力。派蒙的秘密与提瓦特古老文明的关联,丽莎的禁忌魔法与神秘组织的联系,这些都与即将到来的危机息息相关,也与主角的命运紧密相连。 钟离的真实目的则成为贯穿始终的神秘力量,推动剧情发展,并最终影响故事的结局。 星河的星穹铁道科技,则成为改变提瓦特魔法世界格局的关键因素,引发一系列冲突和变革。
世界观设定:暗影之歌
引言
在一个名为提瓦特的大陆上,力量如同黑暗中的呼吸,无声无息地渗透着一切。这片大陆的历史被称为“暗影年代”,那时期充满了权力斗争和悲剧命运。如今,随着“暗影力量”的再次苏醒,新的阴谋与挑战正逐渐显露。
主要角色
1. 星辰(亦名:斯琴)
- 背景:一位被称为“暗影之心”的天才少女,拥有操纵暗影的特殊能力。她的力量是暗影之力最强者之一,但同时也让她背负着沉重的责任。
2. 艾莎(亦名:艾丽莎)
- 背景:一位冷酷无情的刺客,曾为“暗影力量”效力。她的身世与“暗影力量”密不可分,性格多变,隐藏着巨大的秘密。
3. 凯文(亦名:克莱文)
- 背景:自封为提瓦特大陆的守护者,实则心中藏着大不明小谋。他表面上支持星辰和艾莎,但背后暗藏自己的秘密计划。
4. 暗影力量
- 背景:一股强大的力量源于远古,能够操控暗影与黑暗。它的真实目的尚未完全被揭示,既有守护大陆的传说,也有毁灭一切的预言。
世界观亮点
1. 力量的双面性
- 暗影力量是世界的核心,但同时也是最大的危险。掌握它的人往往会被力量所控制,经历深刻的心理变化。
2. 身份的迷失
- 星辰和艾莎都背负着来自“暗影力量”的阴影,他们的身份与命运与力量紧密相连,这种联系既是他们的优势,也是他们的诅咒。
3. 权力真空
- 提瓦特大陆在“暗影年代”后期逐渐走向新的权力真空,各种势力争夺资源和权利,形成了复杂多变的政治格局。
4. 宿命的重遇
- 星辰和艾莎被暗影力量联系起来,这种宿命使他们在面对危机时不得不合作,但同时也让彼此之间充满了矛盾与误解。
5. 未知的威胁
- “暗影力量”的真实目标尚未完全明确,可能是为了大陆的繁荣,也可能是为了彻底摧毁一切。这种不确定性为故事增添了紧张感和悬念。
故事情节展开
1. 起始点
- 星辰被选中成为暗影力量的继承者,但她对自己的能力感到不安,同时也怀疑“暗影力量”是否真的是保护大陆的存在。
2. 初识艾莎
- 艾莎以刺客的身份接近星辰,试图利用她的暗影能力。但两人在合作中逐渐发现彼此之间的共同点,以及隐藏的冲突。
3. 凯文的动机
- 凯文表面上是支持星辰和艾莎,但实际上他有自己的秘密。他可能是“暗影力量”的后代,或者背后另有hidden agenda。
4. 危机初现
- 随着暗影力量的觉醒,大陆出现了一系列诡异的事件。暗影力量开始失控,普通大民被笼罩在阴影中,生存成为一件艰难的事。
5. 真相浮现
- 通过对凯文、艾莎和星辰的深入探索,逐渐揭示他们各自的过去与命运联系。最终,他们会发现自己之间存在着更大的联系,而这可能关系到整个暗影力量的未来。
6. 高潮对决
- 在一次关键时刻,星辰和艾莎必须共同面临一个强大的敌人。这场对决不仅是力量的较量,更是信任与背叛的考验。
7. 结局的模糊性
- 故事情节留下了开放的结局,让读者思考暗影力量最终会带来什么样的未来。星辰和艾莎是否能找到自己的道路,或者他们将被暗影力量彻底吞噬。
主题
- 力量与责任:暗影力量赋予了一些人巨大的能力,但同时也带来了沉重的责任。
- 身份与命运:每个人的过去决定了他们的未来,他们是否能超越自己的宿命?
- 信任与背叛:在危难时刻,真实的朋友和潜在的敌人是如何区分的?
总结
“暗影之歌”是一个充满矛盾与复杂性的故事,它探讨了力量、身份、信任等多个深刻主题。通过星辰和艾莎两位主角的视角,读者可以感受到在黑暗与光明之间挣扎的人性,以及他们如何面对命运给予的挑战。
+7 -1
View File
@@ -10,7 +10,13 @@ class OllamaEmbeddings:
def __init__(self, model_name: str, base_url: str): def __init__(self, model_name: str, base_url: str):
self.model_name = model_name self.model_name = model_name
self.base_url = base_url self.base_url = base_url
def embed(self, texts: List[str]) -> List[List[float]]:
embeddings = []
for text in texts:
embeddings.append(self.embed_single_document(text))
return embeddings
def embed_documents(self, texts: List[str]) -> List[List[float]]: def embed_documents(self, texts: List[str]) -> List[List[float]]:
""" """
将多段文本转换为向量列表 将多段文本转换为向量列表
+77 -17
View File
@@ -30,10 +30,48 @@ from prompt_definitions import (
summary_prompt, update_character_state_prompt, summary_prompt, update_character_state_prompt,
chapter_outline_prompt, chapter_write_prompt chapter_outline_prompt, chapter_write_prompt
) )
from embedding_ollama import OllamaEmbeddings
# ============ 新增:导入 chapter_directory_parser ============
from chapter_directory_parser import get_chapter_info_from_directory from chapter_directory_parser import get_chapter_info_from_directory
# ============ 适配接口的判断函数 ============
def is_using_ollama_api(interface_format: str, base_url: str) -> bool:
"""
当 interface_format == "Ollama" 时返回 True
"""
if interface_format.lower() == "ollama":
return True
return False
def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool:
"""
如果用户在下拉里选择了 ML Studio
"""
if interface_format.lower() == "ml studio":
return True
return False
def create_embeddings_object(
api_key: str,
base_url: str,
embed_url: str,
interface_format: str,
embedding_model_name: str
):
"""
根据用户在UI中配置的参数,返回对应的 embeddings 对象。
- 当 interface_format = "Ollama" => OllamaEmbeddings(...)
- 当 interface_format = "OpenAI" => OpenAIEmbeddings
- 当 interface_format = "ML Studio" => OpenAIEmbeddings
"""
if is_using_ollama_api(interface_format, embed_url):
# 使用 Ollama Embeddings
return OllamaEmbeddings(model_name=embedding_model_name, base_url=embed_url)
elif is_using_ml_studio_api(interface_format, base_url):
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
else:
# 默认使用 OpenAIEmbeddings
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
# ============ 日志配置 ============ # ============ 日志配置 ============
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
@@ -67,17 +105,21 @@ def clear_vector_store():
else: else:
logging.info("No vector store found to clear.") logging.info("No vector store found to clear.")
def init_vector_store(api_key: str, base_url: str, texts: List[str], embedding_base_url: str = "") -> Chroma: def init_vector_store(
api_key: str,
base_url: str,
interface_format: str,
embedding_model_name: str,
texts: List[str],
embedding_base_url: str = ""
) -> Chroma:
""" """
初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。 初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。
如果不存在该目录,会自动创建。 如果不存在该目录,会自动创建。
如果 embedding_base_url 不为空,则使用它做为embedding的base,否则默认base_url。 如果 embedding_base_url 不为空,则使用它做为embedding的base,否则默认base_url。
""" """
embed_url = embedding_base_url if embedding_base_url else base_url embed_url = embedding_base_url if embedding_base_url else base_url
embeddings = OpenAIEmbeddings( embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, embedding_model_name)
openai_api_key=api_key,
openai_api_base=embed_url
)
documents = [Document(page_content=t) for t in texts] documents = [Document(page_content=t) for t in texts]
vectorstore = Chroma.from_documents( vectorstore = Chroma.from_documents(
documents, documents,
@@ -87,7 +129,13 @@ def init_vector_store(api_key: str, base_url: str, texts: List[str], embedding_b
vectorstore.persist() vectorstore.persist()
return vectorstore return vectorstore
def load_vector_store(api_key: str, base_url: str, embedding_base_url: str = "") -> Optional[Chroma]: def load_vector_store(
api_key: str,
base_url: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str = ""
) -> Optional[Chroma]:
""" """
读取已存在的向量库。若不存在则返回 None。 读取已存在的向量库。若不存在则返回 None。
同样支持可选的 embedding_base_url。 同样支持可选的 embedding_base_url。
@@ -95,30 +143,42 @@ def load_vector_store(api_key: str, base_url: str, embedding_base_url: str = "")
if not os.path.exists(VECTOR_STORE_DIR): if not os.path.exists(VECTOR_STORE_DIR):
return None return None
embed_url = embedding_base_url if embedding_base_url else base_url embed_url = embedding_base_url if embedding_base_url else base_url
embeddings = OpenAIEmbeddings( embeddings = create_embeddings_object(api_key, base_url, embed_url, interface_format, embedding_model_name)
openai_api_key=api_key,
openai_api_base=embed_url
)
return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings) return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings)
def update_vector_store(api_key: str, base_url: str, new_chapter: str, embedding_base_url: str = "") -> None: def update_vector_store(
api_key: str,
base_url: str,
new_chapter: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str = ""
) -> None:
"""将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。""" """将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。"""
store = load_vector_store(api_key, base_url, embedding_base_url) store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url)
if not store: if not store:
logging.info("Vector store does not exist. Initializing a new one...") logging.info("Vector store does not exist. Initializing a new one...")
init_vector_store(api_key, base_url, [new_chapter], embedding_base_url) init_vector_store(api_key, base_url,interface_format, embedding_model_name, [new_chapter], embedding_base_url)
return return
new_doc = Document(page_content=new_chapter) new_doc = Document(page_content=new_chapter)
store.add_documents([new_doc]) store.add_documents([new_doc])
store.persist() store.persist()
def get_relevant_context_from_vector_store(api_key: str, base_url: str, query: str, k: int = 2, embedding_base_url: str = "") -> str: def get_relevant_context_from_vector_store(
api_key: str,
base_url: str,
interface_format: str,
embedding_model_name: str,
query: str,
k: int = 2,
embedding_base_url: str = ""
) -> str:
""" """
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。 从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
若向量库不存在则返回空字符串。 若向量库不存在则返回空字符串。
""" """
store = load_vector_store(api_key, base_url, embedding_base_url) store = load_vector_store(api_key, base_url,interface_format, embedding_model_name, embedding_base_url)
if not store: if not store:
logging.warning("Vector store not found. Returning empty context.") logging.warning("Vector store not found. Returning empty context.")
return "" return ""
+169 -87
View File
@@ -38,12 +38,14 @@ class NovelGeneratorGUI:
self.loaded_config = load_config(self.config_file) self.loaded_config = load_config(self.config_file)
# ========== 主要的属性变量 ========== # ========== 主要的属性变量 ==========
# 右侧参数区 - 各种输入 # 注意:配置项较多,此处增加 embedding_model_name_var 以支持 Embedding 模型名称
self.api_key_var = ctk.StringVar(value=self.loaded_config.get("api_key", "")) self.api_key_var = ctk.StringVar(value=self.loaded_config.get("api_key", ""))
self.base_url_var = ctk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1")) self.base_url_var = ctk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1"))
self.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI")) self.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI"))
self.model_name_var = ctk.StringVar(value=self.loaded_config.get("model_name", "gpt-4o-mini")) self.model_name_var = ctk.StringVar(value=self.loaded_config.get("model_name", "gpt-4o-mini"))
self.embedding_url_var = ctk.StringVar(value=self.loaded_config.get("embedding_url", "")) # 新增:可选Embedding模型URL
self.embedding_url_var = ctk.StringVar(value=self.loaded_config.get("embedding_url", ""))
self.embedding_model_name_var = ctk.StringVar(value=self.loaded_config.get("embedding_model_name", ""))
self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7)) self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7))
self.topic_default = self.loaded_config.get("topic", "") self.topic_default = self.loaded_config.get("topic", "")
@@ -83,7 +85,7 @@ class NovelGeneratorGUI:
# ------------------ 主功能 Tab ------------------ # ------------------ 主功能 Tab ------------------
def build_main_tab(self): def build_main_tab(self):
""" """
主Tab: 左侧显示日志 / 本章内容, 右侧显示主要功能操作 主Tab: 左侧显示章节草稿/日志, 右侧是功能区 & 配置
""" """
self.main_tab.rowconfigure(0, weight=1) self.main_tab.rowconfigure(0, weight=1)
self.main_tab.columnconfigure(0, weight=1) self.main_tab.columnconfigure(0, weight=1)
@@ -104,8 +106,8 @@ class NovelGeneratorGUI:
def build_left_layout(self): def build_left_layout(self):
""" """
左侧包含两个区域: 左侧包含两个区域:
1. 输出日志(下半部分 1. 本章草稿内容(可编辑
2. 本章内容(上半部分 2. 输出日志(只读
""" """
self.left_frame.grid_rowconfigure(0, weight=3) self.left_frame.grid_rowconfigure(0, weight=3)
self.left_frame.grid_rowconfigure(1, weight=1) self.left_frame.grid_rowconfigure(1, weight=1)
@@ -115,177 +117,250 @@ class NovelGeneratorGUI:
chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容", font=("Microsoft YaHei", 14)) chapter_label = ctk.CTkLabel(self.left_frame, text="本章内容", font=("Microsoft YaHei", 14))
chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w") chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
# 章节草稿:可编辑
self.chapter_result = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 14)) self.chapter_result = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 14))
self.chapter_result.grid(row=0, column=0, sticky="nsew", padx=5, pady=(0, 5)) self.chapter_result.grid(row=0, column=0, sticky="nsew", padx=5, pady=(0, 5))
# 输出日志 # 输出日志
log_label = ctk.CTkLabel(self.left_frame, text="输出日志", font=("Microsoft YaHei", 14)) log_label = ctk.CTkLabel(self.left_frame, text="输出日志 (只读)", font=("Microsoft YaHei", 14))
log_label.grid(row=1, column=0, padx=5, pady=(5, 0), sticky="w") log_label.grid(row=1, column=0, padx=5, pady=(5, 0), sticky="w")
# 日志:只读
self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12)) self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12))
self.log_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) self.log_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
# 设置只读
self.log_text.configure(state="disabled")
def build_right_layout(self): def build_right_layout(self):
""" """
右侧用于显示一系列参数输入和功能按钮。 右侧用于显示配置与功能按钮。
其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。
下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。
"""
# 第一行创建一个子 TabView 放置 AI 配置 & Embedding 配置
self.config_tabview = ctk.CTkTabview(self.right_frame, width=350, height=300)
self.config_tabview.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=5, pady=5)
self.ai_config_tab = self.config_tabview.add("AI接口配置")
self.embeddings_config_tab = self.config_tabview.add("Embedding配置")
# 分别构建这两个 Tab 的布局
self.build_ai_config_tab()
self.build_embeddings_config_tab()
# 下面再放其他通用参数(主题、类型等) & 功能按钮
row_base = 1
# row_base + 1 处构建剩余输入,如topic, genre, etc.
self.build_novel_params_area(start_row=row_base+1)
# 最后放一些主功能按钮
self.build_main_buttons_area(start_row=row_base+10)
def build_ai_config_tab(self):
"""
在 AI接口配置 子Tab 上放置:
- API Key
- Base URL
- 接口格式
- 模型名称
- Temperature
""" """
# 配置网格 # 配置网格
for i in range(25): for i in range(6):
self.right_frame.grid_rowconfigure(i, weight=0) self.ai_config_tab.grid_rowconfigure(i, weight=0)
self.right_frame.grid_columnconfigure(0, weight=0) self.ai_config_tab.grid_columnconfigure(0, weight=0)
self.right_frame.grid_columnconfigure(1, weight=1) self.ai_config_tab.grid_columnconfigure(1, weight=1)
# 1. API Key # 1. API Key
api_key_label = ctk.CTkLabel(self.right_frame, text="API Key:") api_key_label = ctk.CTkLabel(self.ai_config_tab, text="API Key:")
api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e") api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
api_key_entry = ctk.CTkEntry(self.right_frame, textvariable=self.api_key_var) api_key_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.api_key_var)
api_key_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") api_key_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
# 2. Base URL # 2. Base URL
base_url_label = ctk.CTkLabel(self.right_frame, text="Base URL:") base_url_label = ctk.CTkLabel(self.ai_config_tab, text="Base URL:")
base_url_label.grid(row=1, column=0, padx=5, pady=5, sticky="e") base_url_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
base_url_entry = ctk.CTkEntry(self.right_frame, textvariable=self.base_url_var) base_url_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.base_url_var)
base_url_entry.grid(row=1, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") base_url_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
# 2.1 接口格式 下拉菜单 # 3. 接口格式
interface_label = ctk.CTkLabel(self.right_frame, text="接口格式:") interface_label = ctk.CTkLabel(self.ai_config_tab, text="接口格式:")
interface_label.grid(row=2, column=0, padx=5, pady=5, sticky="e") interface_label.grid(row=2, column=0, padx=5, pady=5, sticky="e")
interface_options = ["OpenAI", "Ollama", "ML Studio", "Local"] interface_options = ["OpenAI", "Ollama", "ML Studio", "Local"]
interface_dropdown = ctk.CTkOptionMenu(self.right_frame, values=interface_options, variable=self.interface_format_var) interface_dropdown = ctk.CTkOptionMenu(
interface_dropdown.grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") self.ai_config_tab,
values=interface_options,
variable=self.interface_format_var
)
interface_dropdown.grid(row=2, column=1, padx=5, pady=5, sticky="nsew")
# 3. Model Name # 4. 模型名称
model_name_label = ctk.CTkLabel(self.right_frame, text="Model Name:") model_name_label = ctk.CTkLabel(self.ai_config_tab, text="Model Name:")
model_name_label.grid(row=3, column=0, padx=5, pady=5, sticky="e") model_name_label.grid(row=3, column=0, padx=5, pady=5, sticky="e")
model_name_entry = ctk.CTkEntry(self.right_frame, textvariable=self.model_name_var) model_name_entry = ctk.CTkEntry(self.ai_config_tab, textvariable=self.model_name_var)
model_name_entry.grid(row=3, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") model_name_entry.grid(row=3, column=1, padx=5, pady=5, sticky="nsew")
# 3.1 Embedding Model URL (可选) # 5. Temperature
embedding_url_label = ctk.CTkLabel(self.right_frame, text="Embedding URL:") temp_label = ctk.CTkLabel(self.ai_config_tab, text="Temperature:")
embedding_url_label.grid(row=4, column=0, padx=5, pady=5, sticky="e") temp_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
embedding_url_entry = ctk.CTkEntry(self.right_frame, textvariable=self.embedding_url_var)
embedding_url_entry.grid(row=4, column=1, columnspan=2, padx=5, pady=5, sticky="nsew")
# 4. Temperature
temp_label = ctk.CTkLabel(self.right_frame, text="Temperature:")
temp_label.grid(row=5, column=0, padx=5, pady=5, sticky="e")
def update_temp_label(value): def update_temp_label(value):
self.temp_value_label.configure(text=f"{float(value):.2f}") self.temp_value_label.configure(text=f"{float(value):.2f}")
temp_scale = ctk.CTkSlider(self.right_frame, from_=0.0, to=1.0, number_of_steps=100,
command=update_temp_label, variable=self.temperature_var)
temp_scale.grid(row=5, column=1, padx=5, pady=5, sticky="we")
self.temp_value_label = ctk.CTkLabel(self.right_frame, text=f"{self.temperature_var.get():.2f}")
self.temp_value_label.grid(row=5, column=2, padx=1, pady=1, sticky="w")
# 5. 主题(Topic) 多行输入 temp_scale = ctk.CTkSlider(
self.ai_config_tab,
from_=0.0, to=1.0,
number_of_steps=100,
command=update_temp_label,
variable=self.temperature_var
)
temp_scale.grid(row=4, column=1, padx=5, pady=5, sticky="we")
self.temp_value_label = ctk.CTkLabel(self.ai_config_tab, text=f"{self.temperature_var.get():.2f}")
self.temp_value_label.grid(row=4, column=2, padx=1, pady=1, sticky="w")
# 保存/加载配置按钮(与AI配置归一处)
config_frame = ctk.CTkFrame(self.ai_config_tab)
config_frame.grid(row=5, column=0, columnspan=3, sticky="nsew")
save_config_btn = ctk.CTkButton(config_frame, text="保存配置", command=self.save_config_btn)
save_config_btn.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
load_config_btn = ctk.CTkButton(config_frame, text="加载配置", command=self.load_config_btn)
load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
def build_embeddings_config_tab(self):
"""
在 Embedding配置 子Tab 上放置:
- Embedding URL
- Embedding 模型名称
"""
for i in range(2):
self.embeddings_config_tab.grid_rowconfigure(i, weight=0)
self.embeddings_config_tab.grid_columnconfigure(0, weight=0)
self.embeddings_config_tab.grid_columnconfigure(1, weight=1)
# 1. Embedding URL
embedding_url_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding URL:")
embedding_url_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
embedding_url_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_url_var)
embedding_url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
# 2. Embedding 模型名称
emb_model_name_label = ctk.CTkLabel(self.embeddings_config_tab, text="Embedding Model Name:")
emb_model_name_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
emb_model_name_entry = ctk.CTkEntry(self.embeddings_config_tab, textvariable=self.embedding_model_name_var)
emb_model_name_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
def build_novel_params_area(self, start_row=2):
"""
放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等
"""
# row = start_row
# 主题(Topic)
topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12)) topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12))
topic_label.grid(row=6, column=0, padx=5, pady=5, sticky="e") topic_label.grid(row=start_row, column=0, padx=5, pady=5, sticky="e")
self.topic_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12)) self.topic_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12))
self.topic_text.grid(row=6, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") self.topic_text.grid(row=start_row, column=1, padx=5, pady=5, sticky="nsew")
if self.topic_default: if self.topic_default:
self.topic_text.insert("0.0", self.topic_default) self.topic_text.insert("0.0", self.topic_default)
# 6. 类型(Genre) # 类型(Genre)
genre_label = ctk.CTkLabel(self.right_frame, text="类型(Genre):", font=("Microsoft YaHei", 12)) genre_label = ctk.CTkLabel(self.right_frame, text="类型(Genre):", font=("Microsoft YaHei", 12))
genre_label.grid(row=7, column=0, padx=5, pady=5, sticky="e") genre_label.grid(row=start_row+1, column=0, padx=5, pady=5, sticky="e")
genre_entry = ctk.CTkEntry(self.right_frame, textvariable=self.genre_var, font=("Microsoft YaHei", 12)) genre_entry = ctk.CTkEntry(self.right_frame, textvariable=self.genre_var, font=("Microsoft YaHei", 12))
genre_entry.grid(row=7, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") genre_entry.grid(row=start_row+1, column=1, padx=5, pady=5, sticky="nsew")
# 7. 章节数 # 章节数
num_chapters_label = ctk.CTkLabel(self.right_frame, text="章节数:", font=("Microsoft YaHei", 12)) num_chapters_label = ctk.CTkLabel(self.right_frame, text="章节数:", font=("Microsoft YaHei", 12))
num_chapters_label.grid(row=8, column=0, padx=5, pady=5, sticky="e") num_chapters_label.grid(row=start_row+2, column=0, padx=5, pady=5, sticky="e")
num_chapters_entry = ctk.CTkEntry(self.right_frame, textvariable=self.num_chapters_var, width=80) num_chapters_entry = ctk.CTkEntry(self.right_frame, textvariable=self.num_chapters_var, width=80)
num_chapters_entry.grid(row=8, column=1, padx=5, pady=5, sticky="w") num_chapters_entry.grid(row=start_row+2, column=1, padx=5, pady=5, sticky="w")
# 8. 每章字数 # 每章字数
word_number_label = ctk.CTkLabel(self.right_frame, text="每章字数:", font=("Microsoft YaHei", 12)) word_number_label = ctk.CTkLabel(self.right_frame, text="每章字数:", font=("Microsoft YaHei", 12))
word_number_label.grid(row=9, column=0, padx=5, pady=5, sticky="e") word_number_label.grid(row=start_row+3, column=0, padx=5, pady=5, sticky="e")
word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80) word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80)
word_number_entry.grid(row=9, column=1, padx=5, pady=5, sticky="w") word_number_entry.grid(row=start_row+3, column=1, padx=5, pady=5, sticky="w")
# 9. 文件保存路径 # 保存路径
filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12)) filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12))
filepath_label.grid(row=10, column=0, padx=5, pady=5, sticky="e") filepath_label.grid(row=start_row+4, column=0, padx=5, pady=5, sticky="e")
filepath_entry = ctk.CTkEntry(self.right_frame, textvariable=self.filepath_var) filepath_entry = ctk.CTkEntry(self.right_frame, textvariable=self.filepath_var)
filepath_entry.grid(row=10, column=1, padx=5, pady=5, sticky="nsew") filepath_entry.grid(row=start_row+4, column=1, padx=5, pady=5, sticky="nsew")
browse_btn = ctk.CTkButton(self.right_frame, text="浏览...", command=self.browse_folder, width=60, font=("Microsoft YaHei", 12)) browse_btn = ctk.CTkButton(self.right_frame, text="浏览...", command=self.browse_folder, width=60)
browse_btn.grid(row=10, column=2, padx=1, pady=1, sticky="w") browse_btn.grid(row=start_row+4, column=2, padx=1, pady=1, sticky="w")
# 保存/加载配置按钮 # 章节号
config_frame = ctk.CTkFrame(self.right_frame)
config_frame.grid(row=11, column=1, columnspan=2, sticky="nsew")
save_config_btn = ctk.CTkButton(config_frame, text="保存配置", command=self.save_config_btn, font=("Microsoft YaHei", 12))
save_config_btn.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
load_config_btn = ctk.CTkButton(config_frame, text="加载配置", command=self.load_config_btn, font=("Microsoft YaHei", 12))
load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
# 10. 章节号
chapter_num_label = ctk.CTkLabel(self.right_frame, text="章节号:", font=("Microsoft YaHei", 12)) chapter_num_label = ctk.CTkLabel(self.right_frame, text="章节号:", font=("Microsoft YaHei", 12))
chapter_num_label.grid(row=12, column=0, padx=5, pady=5, sticky="e") chapter_num_label.grid(row=start_row+5, column=0, padx=5, pady=5, sticky="e")
chapter_num_entry = ctk.CTkEntry(self.right_frame, textvariable=self.chapter_num_var, width=80) chapter_num_entry = ctk.CTkEntry(self.right_frame, textvariable=self.chapter_num_var, width=80)
chapter_num_entry.grid(row=12, column=1, padx=5, pady=5, sticky="w") chapter_num_entry.grid(row=start_row+5, column=1, padx=5, pady=5, sticky="w")
# 11. “用户指导” 多行输入 # 用户指导
guide_label = ctk.CTkLabel(self.right_frame, text="本章指导:", font=("Microsoft YaHei", 12)) guide_label = ctk.CTkLabel(self.right_frame, text="本章指导:", font=("Microsoft YaHei", 12))
guide_label.grid(row=13, column=0, padx=5, pady=5, sticky="ne") guide_label.grid(row=start_row+6, column=0, padx=5, pady=5, sticky="ne")
self.user_guide_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12)) self.user_guide_text = ctk.CTkTextbox(self.right_frame, width=200, height=80, wrap="word", font=("Microsoft YaHei", 12))
self.user_guide_text.grid(row=13, column=1, columnspan=2, padx=5, pady=5, sticky="nsew") self.user_guide_text.grid(row=start_row+6, column=1, padx=5, pady=5, sticky="nsew")
def build_main_buttons_area(self, start_row=10):
"""
主要功能按钮
"""
self.right_frame.grid_rowconfigure(start_row, weight=0)
self.right_frame.grid_columnconfigure(0, weight=0)
self.right_frame.grid_columnconfigure(1, weight=0)
# 功能按钮区域
row_base = 14
self.btn_generate_full = ctk.CTkButton( self.btn_generate_full = ctk.CTkButton(
self.right_frame, text="Step1. 生成设定 & 目录", self.right_frame, text="Step1. 生成设定 & 目录",
command=self.generate_full_novel, command=self.generate_full_novel,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_generate_full.grid(row=row_base, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_generate_full.grid(row=start_row, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
self.btn_generate_chapter = ctk.CTkButton( self.btn_generate_chapter = ctk.CTkButton(
self.right_frame, text="Step2. 生成章节草稿", self.right_frame, text="Step2. 生成章节草稿",
command=self.generate_chapter_draft_ui, command=self.generate_chapter_draft_ui,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_generate_chapter.grid(row=row_base+1, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_generate_chapter.grid(row=start_row+1, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
self.btn_finalize_chapter = ctk.CTkButton( self.btn_finalize_chapter = ctk.CTkButton(
self.right_frame, text="Step3. 定稿当前章节", self.right_frame, text="Step3. 定稿当前章节",
command=self.finalize_chapter_ui, command=self.finalize_chapter_ui,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_finalize_chapter.grid(row=row_base+2, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_finalize_chapter.grid(row=start_row+2, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
self.btn_check_consistency = ctk.CTkButton( self.btn_check_consistency = ctk.CTkButton(
self.right_frame, text="[可选]一致性审校", self.right_frame, text="[可选]一致性审校",
command=self.do_consistency_check, command=self.do_consistency_check,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_check_consistency.grid(row=row_base+3, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_check_consistency.grid(row=start_row+3, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
self.btn_import_knowledge = ctk.CTkButton( self.btn_import_knowledge = ctk.CTkButton(
self.right_frame, text="[可选]导入知识库", self.right_frame, text="[可选]导入知识库",
command=self.import_knowledge_handler, command=self.import_knowledge_handler,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_import_knowledge.grid(row=row_base+4, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_import_knowledge.grid(row=start_row+4, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
self.btn_clear_vectorstore = ctk.CTkButton( self.btn_clear_vectorstore = ctk.CTkButton(
self.right_frame, text="清空向量库", self.right_frame, text="清空向量库",
fg_color="red", # 让按钮显眼一些 fg_color="red",
command=self.clear_vectorstore_handler, command=self.clear_vectorstore_handler,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
self.btn_clear_vectorstore.grid(row=row_base+5, column=0, columnspan=3, padx=5, pady=5, sticky="ew") self.btn_clear_vectorstore.grid(row=start_row+5, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
plot_arcs_btn = ctk.CTkButton( plot_arcs_btn = ctk.CTkButton(
self.right_frame, text="[查看] 剧情要点", self.right_frame, text="[查看] 剧情要点",
command=self.show_plot_arcs_ui, command=self.show_plot_arcs_ui,
font=("Microsoft YaHei", 12) font=("Microsoft YaHei", 12)
) )
plot_arcs_btn.grid(row=row_base+6, column=0, columnspan=3, padx=5, pady=5, sticky="ew") plot_arcs_btn.grid(row=start_row+6, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
# ------------------ Novel Settings Tab ------------------ # ------------------ Novel Settings Tab ------------------
def build_setting_tab(self): def build_setting_tab(self):
@@ -460,6 +535,7 @@ class NovelGeneratorGUI:
self.interface_format_var.set(cfg.get("interface_format", "OpenAI")) self.interface_format_var.set(cfg.get("interface_format", "OpenAI"))
self.model_name_var.set(cfg.get("model_name", "")) self.model_name_var.set(cfg.get("model_name", ""))
self.embedding_url_var.set(cfg.get("embedding_url", "")) self.embedding_url_var.set(cfg.get("embedding_url", ""))
self.embedding_model_name_var.set(cfg.get("embedding_model_name", ""))
self.temperature_var.set(cfg.get("temperature", 0.7)) self.temperature_var.set(cfg.get("temperature", 0.7))
self.genre_var.set(cfg.get("genre", "")) self.genre_var.set(cfg.get("genre", ""))
self.num_chapters_var.set(cfg.get("num_chapters", 10)) self.num_chapters_var.set(cfg.get("num_chapters", 10))
@@ -481,6 +557,7 @@ class NovelGeneratorGUI:
"interface_format": self.interface_format_var.get(), "interface_format": self.interface_format_var.get(),
"model_name": self.model_name_var.get(), "model_name": self.model_name_var.get(),
"embedding_url": self.embedding_url_var.get(), "embedding_url": self.embedding_url_var.get(),
"embedding_model_name": self.embedding_model_name_var.get(),
"temperature": self.temperature_var.get(), "temperature": self.temperature_var.get(),
"topic": self.topic_text.get("0.0", "end").strip(), "topic": self.topic_text.get("0.0", "end").strip(),
"genre": self.genre_var.get(), "genre": self.genre_var.get(),
@@ -501,8 +578,12 @@ class NovelGeneratorGUI:
# ------------------ 日志输出 ------------------ # ------------------ 日志输出 ------------------
def log(self, message: str): def log(self, message: str):
# 临时切换为可写
self.log_text.configure(state="normal")
self.log_text.insert("end", message + "\n") self.log_text.insert("end", message + "\n")
self.log_text.see("end") self.log_text.see("end")
# 再切回只读
self.log_text.configure(state="disabled")
# ------------------ 功能区 -------------------- # ------------------ 功能区 --------------------
def disable_button(self, btn): def disable_button(self, btn):
@@ -587,9 +668,9 @@ class NovelGeneratorGUI:
chapters_dir = os.path.join(filepath, "chapters") chapters_dir = os.path.join(filepath, "chapters")
recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3) recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3)
# 用当前模型生成一个较为详细的最近剧情摘要 # 简易生成最近章节摘要(示例)
recent_chapters_summary = summarize_recent_chapters( recent_chapters_summary = summarize_recent_chapters(
None, # 这里只是示例,实际可根据你的需求传入对应的模型 None, # 此处仅示例
recent_3_texts recent_3_texts
) )
@@ -610,7 +691,7 @@ class NovelGeneratorGUI:
filepath=filepath filepath=filepath
) )
if draft_text: if draft_text:
self.log(f"✅ 第{chap_num}章草稿生成完成。请在下方查看") self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑")
self.chapter_result.delete("0.0", "end") self.chapter_result.delete("0.0", "end")
self.chapter_result.insert("0.0", draft_text) self.chapter_result.insert("0.0", draft_text)
self.chapter_result.see("end") self.chapter_result.see("end")
@@ -741,7 +822,8 @@ class NovelGeneratorGUI:
import_knowledge_file( import_knowledge_file(
api_key=self.api_key_var.get().strip(), api_key=self.api_key_var.get().strip(),
base_url=self.base_url_var.get().strip(), base_url=self.base_url_var.get().strip(),
embedding_base_url=self.embedding_url_var.get().strip(), # 新增:传入embedding url # 传入 embedding_url + embedding_model_name
embedding_base_url=self.embedding_url_var.get().strip(),
file_path=selected_file file_path=selected_file
) )
self.log("✅ 知识库文件导入完成。") self.log("✅ 知识库文件导入完成。")