111
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
<think>
|
||||||
|
好,我现在需要帮用户构思小说的第1章大纲,标题是“星河坠落提瓦特”,简述是“穿越与初遇派蒙,危机初现端倪”。首先,我得理解这个标题和简述的意思。看起来这是一部含有玄幻或奇幻元素的小说,可能涉及时间旅行、门派纷争或者某种隐藏的危机。
|
||||||
|
|
||||||
|
用户给出的内容里提到要围绕本章标题和简述设计详细大纲,并且用户已经提供了一个示例的结构。所以我需要按照那个格式来写,而不是用数字分点。也就是说,先描述每个部分,然后是主要冲突、出现的角色,他们的动机,暗线推进,以及结尾的悬念。
|
||||||
|
|
||||||
|
首先,本章标题“星河坠落提瓦特”,听起来像是主角从一个繁荣的地方坠落到另一个世界。简述说的是穿越和初遇派蒙,同时危机开始显现。这可能意味着主角从一个平行世界穿越到这个世界,并在门派之间初次相遇,引发了一些危机。
|
||||||
|
|
||||||
|
接下来,我需要构思主要冲突。可能是主角到达提瓦特城,发现这里有一个古老的力量或秘密,门派之间开始竞争,而主角作为穿越者,带来了改变,同时也面临着被追杀的风险。这样既呼应了标题,又体现了简述中的危机初现。
|
||||||
|
|
||||||
|
然后是角色部分。可能需要包括主角、对方的门派成员,比如派蒙,以及提瓦特城里的其他关键人物。每个角色在这章里有什么目标和动机,比如追杀主角的人,或者试图掌控力量的人。
|
||||||
|
|
||||||
|
接下来是推动暗线和悬念的部分。比如,提瓦特城的秘密可能涉及到星河的力量,而派蒙门派对这个力量的争夺可能引发更大的冲突。同时,主角可能带有未知的任务或使命,这会影响后续章节。
|
||||||
|
|
||||||
|
最后是结尾的悬念,可能是一个转折点,比如主角揭示了某个秘密,或者派蒙开始怀疑自己,而星河的力量在台面上或下面开始显现,为后续剧情埋下伏笔。
|
||||||
|
|
||||||
|
整体来看,我需要确保大纲紧扣标题和简述,同时为后续章节留有余地。可能还要注意角色的性格发展和潜在冲突,以保持故事的吸引力。
|
||||||
|
</think>
|
||||||
|
|
||||||
|
### 第 1 章:星河坠落提瓦特
|
||||||
|
|
||||||
|
#### 主要冲突:
|
||||||
|
- **星河降临**:穿越者从一个繁荣世纪跨越到提瓦特大陆,带来了“星河”般的强大力量,引发了提瓦特城内外的关注与恐慌。
|
||||||
|
- **门派初遇**:派蒙门派首领派长派出追杀队伍,试图抓住这股未知力量,以掌控“星河之力”。
|
||||||
|
- **危机初现**:一场隐藏已久的秘密势力开始苏醒,在提瓦特城内外的阴谋逐渐浮现,为后续的灾难埋下伏笔。
|
||||||
|
|
||||||
|
#### 角色出现:
|
||||||
|
1. **主角(未命名)**:穿越者,带着星河般的强大力量,性格冷静果敢,却对自己的来历一无所知。
|
||||||
|
2. **派长**:派蒙门派领袖,精通古老法术,眼光独到,对“星河之力”嗜求不已。
|
||||||
|
3. **追杀队伍**:派长派出的十名绝技高手,打着门派的旗号,在提瓦特城内外肆虐。
|
||||||
|
4. **提瓦特市民**:生活在平静世代的普通百姓,见证并参与这场突如其来的震撼。
|
||||||
|
5. **神秘老者**:提瓦特城中的隐世高人,对“星河之力”知之甚详,暗中观察与等待。
|
||||||
|
|
||||||
|
#### 推动暗线:
|
||||||
|
- **星河力量的真相**:主角身上蕴含的力量竟是千年前就被预言的“星河之力”,而提瓦特城内外的秘密势力对此虎视眈眈。
|
||||||
|
- **派蒙门派的野心**:派长不仅想要获取“星河之力”,还暗藏着更大的图谋,试图借此重塑整个世界。
|
||||||
|
- **神秘老者的警告**:一位隐世高人试图接触主角,给出“星河之力”与世界危机的隐喻,为后续剧情埋下重大伏笔。
|
||||||
|
|
||||||
|
#### 结尾悬念:
|
||||||
|
- **力量暴露**:在一次追杀中,主角无意间释放出“星河之力”,震撼整个提瓦特城,引发更大的关注与恐慌。
|
||||||
|
- **派长的挣扎**:派长对自己的能力和未来感到迷茫,开始怀疑自己是否已经被力量的猎头所误导。
|
||||||
|
- **神秘老者的突然消失**:一位神秘老者在关键时刻悄然离去,为后续追杀埋下更多谜团。
|
||||||
|
|
||||||
|
通过这场开篇的震撼,未命名的主角将从一个普通穿越者逐渐成长为“星河”的继承者,而提瓦特城与整个世界也将迎来一场前所未有的巨变。
|
||||||
+91
-86
@@ -4,17 +4,12 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
try:
|
from typing import TypedDict
|
||||||
from typing import TypedDict
|
|
||||||
except ImportError:
|
|
||||||
from typing_extensions import TypedDict
|
|
||||||
|
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from langgraph.graph import StateGraph, START, END
|
from langgraph.graph import StateGraph, START, END
|
||||||
from langchain_openai import OpenAIEmbeddings
|
from langchain_openai import OpenAIEmbeddings
|
||||||
from langchain_community.vectorstores import Chroma
|
from langchain_community.vectorstores import Chroma
|
||||||
from langchain.docstore.document import Document
|
from langchain.docstore.document import Document
|
||||||
|
|
||||||
import nltk
|
import nltk
|
||||||
import math
|
import math
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
@@ -34,15 +29,15 @@ from embedding_ollama import OllamaEmbeddings
|
|||||||
from chapter_directory_parser import get_chapter_info_from_directory
|
from chapter_directory_parser import get_chapter_info_from_directory
|
||||||
|
|
||||||
# ============ 日志配置 ============
|
# ============ 日志配置 ============
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
def debug_log(prompt: str, response_content: str):
|
def debug_log(prompt: str, response_content: str):
|
||||||
"""打印Prompt与Response,可根据需要保留或去掉。"""
|
|
||||||
logging.info(f"\n[Prompt >>>] {prompt}\n")
|
logging.info(f"\n[Prompt >>>] {prompt}\n")
|
||||||
logging.info(f"[Response >>>] {response_content}\n")
|
logging.info(f"[Response >>>] {response_content}\n")
|
||||||
|
|
||||||
|
# ============ 判断接口格式相关 ============
|
||||||
|
|
||||||
# ============ 接口判断函数 ============
|
|
||||||
def is_using_ollama_api(interface_format: str, base_url: str) -> bool:
|
def is_using_ollama_api(interface_format: str, base_url: str) -> bool:
|
||||||
"""
|
"""
|
||||||
当 interface_format == "Ollama" 时返回 True
|
当 interface_format == "Ollama" 时返回 True
|
||||||
@@ -60,6 +55,8 @@ def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 创建 Embeddings 对象 ============
|
||||||
|
|
||||||
def create_embeddings_object(
|
def create_embeddings_object(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
@@ -72,27 +69,25 @@ def create_embeddings_object(
|
|||||||
- 当 interface_format = "Ollama" => OllamaEmbeddings(...)
|
- 当 interface_format = "Ollama" => OllamaEmbeddings(...)
|
||||||
(此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed)
|
(此时把 embed_url 中的 /v1 替换成 /api,以便最后调用 /api/embed)
|
||||||
- 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings
|
- 当 interface_format = "OpenAI" or "ML Studio" => OpenAIEmbeddings
|
||||||
- 其它情况可自行扩展
|
- 其它情况视需求可扩展
|
||||||
"""
|
"""
|
||||||
if is_using_ollama_api(interface_format, embed_url):
|
if is_using_ollama_api(interface_format, embed_url):
|
||||||
# 去除末尾斜杠
|
|
||||||
fixed_url = embed_url.rstrip("/")
|
fixed_url = embed_url.rstrip("/")
|
||||||
# 如果包含 /v1 则替换为 /api
|
|
||||||
fixed_url = fixed_url.replace("/v1", "/api")
|
fixed_url = fixed_url.replace("/v1", "/api")
|
||||||
return OllamaEmbeddings(
|
return OllamaEmbeddings(
|
||||||
model_name=embedding_model_name,
|
model_name=embedding_model_name,
|
||||||
base_url=fixed_url
|
base_url=fixed_url
|
||||||
)
|
)
|
||||||
elif is_using_ml_studio_api(interface_format, base_url):
|
elif is_using_ml_studio_api(interface_format, base_url):
|
||||||
|
# ML Studio / OpenAI 兼容
|
||||||
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
|
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
|
||||||
else:
|
else:
|
||||||
# 默认使用 OpenAIEmbeddings
|
# 默认使用 OpenAIEmbeddings
|
||||||
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
|
return OpenAIEmbeddings(openai_api_key=api_key, openai_api_base=base_url)
|
||||||
|
|
||||||
# ============ 日志配置 ============
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
||||||
|
|
||||||
# ============ 向量库相关 ============
|
# ============ 向量库相关 ============
|
||||||
|
|
||||||
VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore")
|
VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore")
|
||||||
if not os.path.exists(VECTOR_STORE_DIR):
|
if not os.path.exists(VECTOR_STORE_DIR):
|
||||||
os.makedirs(VECTOR_STORE_DIR)
|
os.makedirs(VECTOR_STORE_DIR)
|
||||||
@@ -102,8 +97,8 @@ def clear_vector_store():
|
|||||||
清空本地向量库(删除 vectorstore 文件夹内的内容)。
|
清空本地向量库(删除 vectorstore 文件夹内的内容)。
|
||||||
"""
|
"""
|
||||||
if os.path.exists(VECTOR_STORE_DIR):
|
if os.path.exists(VECTOR_STORE_DIR):
|
||||||
try:
|
|
||||||
import shutil
|
import shutil
|
||||||
|
try:
|
||||||
for filename in os.listdir(VECTOR_STORE_DIR):
|
for filename in os.listdir(VECTOR_STORE_DIR):
|
||||||
file_path = os.path.join(VECTOR_STORE_DIR, filename)
|
file_path = os.path.join(VECTOR_STORE_DIR, filename)
|
||||||
if os.path.isfile(file_path) or os.path.islink(file_path):
|
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||||
@@ -126,7 +121,6 @@ def init_vector_store(
|
|||||||
) -> Chroma:
|
) -> Chroma:
|
||||||
"""
|
"""
|
||||||
初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。
|
初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录。
|
||||||
embedding_base_url 若不为空,则用于 Ollama 模式下;否则默认使用 base_url。
|
|
||||||
"""
|
"""
|
||||||
embed_url = embedding_base_url if embedding_base_url else base_url
|
embed_url = embedding_base_url if embedding_base_url else base_url
|
||||||
embeddings = create_embeddings_object(
|
embeddings = create_embeddings_object(
|
||||||
@@ -156,6 +150,7 @@ def load_vector_store(
|
|||||||
读取已存在的向量库。若不存在则返回 None。
|
读取已存在的向量库。若不存在则返回 None。
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(VECTOR_STORE_DIR):
|
if not os.path.exists(VECTOR_STORE_DIR):
|
||||||
|
logging.info("Vector store not found. Initializing a new one...")
|
||||||
return None
|
return None
|
||||||
embed_url = embedding_base_url if embedding_base_url else base_url
|
embed_url = embedding_base_url if embedding_base_url else base_url
|
||||||
embeddings = create_embeddings_object(
|
embeddings = create_embeddings_object(
|
||||||
@@ -185,8 +180,10 @@ def update_vector_store(
|
|||||||
embedding_model_name=embedding_model_name,
|
embedding_model_name=embedding_model_name,
|
||||||
embedding_base_url=embedding_base_url
|
embedding_base_url=embedding_base_url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 如果向量库不存在,初始化它
|
||||||
if not store:
|
if not store:
|
||||||
logging.info("Vector store does not exist. Initializing a new one...")
|
logging.info("Vector store does not exist. Initializing a new one for new chapter...")
|
||||||
init_vector_store(
|
init_vector_store(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
@@ -200,6 +197,7 @@ def update_vector_store(
|
|||||||
new_doc = Document(page_content=new_chapter)
|
new_doc = Document(page_content=new_chapter)
|
||||||
store.add_documents([new_doc])
|
store.add_documents([new_doc])
|
||||||
store.persist()
|
store.persist()
|
||||||
|
logging.info("Vector store updated with the new chapter.")
|
||||||
|
|
||||||
def get_relevant_context_from_vector_store(
|
def get_relevant_context_from_vector_store(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
@@ -212,7 +210,7 @@ def get_relevant_context_from_vector_store(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
|
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
|
||||||
若向量库不存在则返回空字符串。
|
若向量库不存在或没有足够的内容,则返回空字符串。
|
||||||
"""
|
"""
|
||||||
store = load_vector_store(
|
store = load_vector_store(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
@@ -221,10 +219,19 @@ def get_relevant_context_from_vector_store(
|
|||||||
embedding_model_name=embedding_model_name,
|
embedding_model_name=embedding_model_name,
|
||||||
embedding_base_url=embedding_base_url
|
embedding_base_url=embedding_base_url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 如果向量库为空,直接返回空字符串
|
||||||
if not store:
|
if not store:
|
||||||
logging.warning("Vector store not found. Returning empty context.")
|
logging.info("No vector store found. Returning empty context.")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# 向量库存在,但没有足够的内容时也避免索引错误
|
||||||
docs = store.similarity_search(query, k=k)
|
docs = store.similarity_search(query, k=k)
|
||||||
|
|
||||||
|
if not docs:
|
||||||
|
logging.info(f"No relevant documents found for query '{query}'. Returning empty context.")
|
||||||
|
return ""
|
||||||
|
|
||||||
combined = "\n".join([d.page_content for d in docs])
|
combined = "\n".join([d.page_content for d in docs])
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
@@ -256,7 +263,6 @@ def Novel_novel_directory_generate(
|
|||||||
"""
|
"""
|
||||||
使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。
|
使用多步流程,生成 Novel_setting.txt 与 Novel_directory.txt 并保存到 filepath。
|
||||||
"""
|
"""
|
||||||
# 确保文件夹存在
|
|
||||||
os.makedirs(filepath, exist_ok=True)
|
os.makedirs(filepath, exist_ok=True)
|
||||||
|
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
@@ -327,7 +333,6 @@ def Novel_novel_directory_generate(
|
|||||||
debug_log(prompt, response.content)
|
debug_log(prompt, response.content)
|
||||||
return {"novel_directory": response.content.strip()}
|
return {"novel_directory": response.content.strip()}
|
||||||
|
|
||||||
# 构建状态图
|
|
||||||
graph = StateGraph(OverallState)
|
graph = StateGraph(OverallState)
|
||||||
graph.add_node("generate_base_setting", generate_base_setting)
|
graph.add_node("generate_base_setting", generate_base_setting)
|
||||||
graph.add_node("generate_character_setting", generate_character_setting)
|
graph.add_node("generate_character_setting", generate_character_setting)
|
||||||
@@ -363,7 +368,6 @@ def Novel_novel_directory_generate(
|
|||||||
logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。")
|
logging.warning("生成失败:缺少 final_novel_setting 或 novel_directory。")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 写入文件
|
|
||||||
filename_set = os.path.join(filepath, "Novel_setting.txt")
|
filename_set = os.path.join(filepath, "Novel_setting.txt")
|
||||||
filename_novel_directory = os.path.join(filepath, "Novel_directory.txt")
|
filename_novel_directory = os.path.join(filepath, "Novel_directory.txt")
|
||||||
|
|
||||||
@@ -375,7 +379,6 @@ def Novel_novel_directory_generate(
|
|||||||
|
|
||||||
append_text_to_file(final_novel_setting_cleaned, filename_set)
|
append_text_to_file(final_novel_setting_cleaned, filename_set)
|
||||||
append_text_to_file(final_novel_directory_cleaned, filename_novel_directory)
|
append_text_to_file(final_novel_directory_cleaned, filename_novel_directory)
|
||||||
|
|
||||||
logging.info("Novel settings and directory generated successfully.")
|
logging.info("Novel settings and directory generated successfully.")
|
||||||
|
|
||||||
|
|
||||||
@@ -394,19 +397,25 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int
|
|||||||
text = read_file(chap_file).strip()
|
text = read_file(chap_file).strip()
|
||||||
if text:
|
if text:
|
||||||
texts.append(text)
|
texts.append(text)
|
||||||
|
if len(texts) < n:
|
||||||
|
texts = [''] * (n - len(texts)) + texts
|
||||||
return texts
|
return texts
|
||||||
|
|
||||||
|
|
||||||
def summarize_recent_chapters(
|
def summarize_recent_chapters(
|
||||||
llm_model: str,
|
llm_model: str,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
chapters_text_list: List[str]
|
chapters_text_list: List[str]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
将最近几章的文本拼接后,通过模型生成一个相对详细的“短期内容摘要”。
|
将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。
|
||||||
如果没有可用的模型(model=None),则退化为简单截断示例。
|
|
||||||
"""
|
"""
|
||||||
|
if not chapters_text_list:
|
||||||
|
return ""
|
||||||
|
if chapters_text_list==['', '', '']:
|
||||||
|
return "暂无摘要。"
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
model=llm_model,
|
model=llm_model,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
@@ -414,33 +423,19 @@ def summarize_recent_chapters(
|
|||||||
temperature=temperature
|
temperature=temperature
|
||||||
)
|
)
|
||||||
|
|
||||||
if not chapters_text_list:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
combined_text = "\n".join(chapters_text_list)
|
combined_text = "\n".join(chapters_text_list)
|
||||||
# 如果未传入model,就做个简单的退化输出
|
prompt = f"""你是一名资深长篇小说写作辅助AI,下面是最近几章的合并文本:
|
||||||
if not model:
|
|
||||||
return f"【摘要-演示】\n{combined_text[:800]}..."
|
|
||||||
|
|
||||||
# 构造一个提示词(Prompt),指示模型生成精简摘要
|
|
||||||
prompt = f"""你是一名资深的长篇小说写作辅助AI。下面是最近几章的合并文本内容:
|
|
||||||
{combined_text}
|
{combined_text}
|
||||||
|
|
||||||
请你为此文本生成一段简洁扼要的摘要,突出主要剧情进展、角色变化、冲突焦点等要点。
|
请用中文输出不超过500字的摘要,只包含主要剧情进展、角色变化、冲突焦点等要点:"""
|
||||||
1.请用中文输出,不超过500字。
|
|
||||||
2.仅回复摘要内容,不需要其他信息。
|
|
||||||
"""
|
|
||||||
# 调用模型获取摘要
|
|
||||||
response = model.invoke(prompt)
|
response = model.invoke(prompt)
|
||||||
if not response or not response.content.strip():
|
if not response or not response.content.strip():
|
||||||
# 若模型无响应或空,返回简单截断
|
return combined_text[:800] + "..." if len(combined_text) > 800 else combined_text
|
||||||
return f"【摘要-演示】\n{combined_text[:800]}..."
|
|
||||||
|
|
||||||
# 返回模型生成的摘要文本
|
|
||||||
return response.content.strip()
|
return response.content.strip()
|
||||||
|
|
||||||
|
|
||||||
# ============ 新增:更新剧情要点/未解决冲突 ============
|
# ============ 新增:剧情要点/未解决冲突 ============
|
||||||
|
|
||||||
PLOT_ARCS_PROMPT = """\
|
PLOT_ARCS_PROMPT = """\
|
||||||
下面是新生成的章节内容:
|
下面是新生成的章节内容:
|
||||||
@@ -449,9 +444,9 @@ PLOT_ARCS_PROMPT = """\
|
|||||||
这里是已记录的剧情要点/未解决冲突(可能为空):
|
这里是已记录的剧情要点/未解决冲突(可能为空):
|
||||||
{old_plot_arcs}
|
{old_plot_arcs}
|
||||||
|
|
||||||
请基于新的章节内容,提炼出本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。
|
请基于新的章节内容,提炼本章引入或延续的悬念、冲突、角色暗线等,将其合并到旧的剧情要点中。
|
||||||
若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。
|
若有新的冲突则添加,若有已解决/不再重要的冲突可标注或移除。
|
||||||
最终输出一份更新后的剧情要点列表,以帮助后续保持故事的整体一致性和悬念延续。
|
最终输出更新后的剧情要点列表,以帮助后续保持故事整体的一致性和悬念延续。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def update_plot_arcs(
|
def update_plot_arcs(
|
||||||
@@ -462,10 +457,6 @@ def update_plot_arcs(
|
|||||||
model_name: str,
|
model_name: str,
|
||||||
temperature: float
|
temperature: float
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
|
||||||
利用模型分析最新章节文本,提炼或更新“未解决冲突或剧情要点”。
|
|
||||||
并返回更新后的字符串。
|
|
||||||
"""
|
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
@@ -480,7 +471,6 @@ def update_plot_arcs(
|
|||||||
if not response:
|
if not response:
|
||||||
logging.warning("update_plot_arcs: No response.")
|
logging.warning("update_plot_arcs: No response.")
|
||||||
return old_plot_arcs
|
return old_plot_arcs
|
||||||
debug_log(prompt, response.content)
|
|
||||||
return response.content.strip()
|
return response.content.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -499,28 +489,44 @@ def generate_chapter_draft(
|
|||||||
word_number: int,
|
word_number: int,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
novel_novel_directory: str,
|
novel_novel_directory: str,
|
||||||
filepath: str
|
filepath: str,
|
||||||
|
interface_format: str,
|
||||||
|
embedding_model_name: str,
|
||||||
|
embedding_base_url: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
仅生成当前章节的草稿,不更新全局摘要/角色状态/向量库。
|
生成当前章节的草稿,不更新全局摘要/角色状态/向量库。
|
||||||
并将生成的内容写到 "chapter_{novel_number}.txt" 覆盖写入。
|
|
||||||
同时生成 "outline_{novel_number}.txt" 存储大纲内容。
|
|
||||||
"""
|
"""
|
||||||
# 0) 根据 novel_number 从 novel_novel_directory 中获取本章标题及简述
|
# 根据目录信息获取本章标题、简介
|
||||||
chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number)
|
chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number)
|
||||||
chapter_title = chapter_info["chapter_title"]
|
chapter_title = chapter_info["chapter_title"]
|
||||||
chapter_brief = chapter_info["chapter_brief"]
|
chapter_brief = chapter_info["chapter_brief"]
|
||||||
|
|
||||||
# 1) 从向量库检索上下文 (此处仅演示 query="回顾剧情")
|
# 从向量库检索多次上下文(示例:对本章简介、用户指导分别做查询,再合并)
|
||||||
relevant_context = get_relevant_context_from_vector_store(
|
queries = []
|
||||||
|
if user_guidance.strip():
|
||||||
|
queries.append(user_guidance)
|
||||||
|
if chapter_brief.strip():
|
||||||
|
queries.append(chapter_brief)
|
||||||
|
# 也可加一句“回顾剧情”之类
|
||||||
|
queries.append("回顾剧情")
|
||||||
|
|
||||||
|
relevant_context = ""
|
||||||
|
for q in queries:
|
||||||
|
partial_context = get_relevant_context_from_vector_store(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
query="回顾剧情",
|
query=q,
|
||||||
interface_format="OpenAI",
|
interface_format=interface_format,
|
||||||
embedding_model_name="",
|
embedding_model_name=embedding_model_name,
|
||||||
embedding_base_url="",
|
embedding_base_url=embedding_base_url,
|
||||||
k=2
|
k=2
|
||||||
)
|
)
|
||||||
|
if partial_context.strip():
|
||||||
|
relevant_context += "\n" + partial_context
|
||||||
|
# 如果检索结果为空,使用默认值(如空字符串)
|
||||||
|
if not relevant_context:
|
||||||
|
relevant_context = "暂无相关内容。"
|
||||||
|
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
@@ -529,10 +535,10 @@ def generate_chapter_draft(
|
|||||||
temperature=temperature
|
temperature=temperature
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2) 生成大纲
|
# 1) 生成本章大纲
|
||||||
outline_prompt_text = chapter_outline_prompt.format(
|
outline_prompt_text = chapter_outline_prompt.format(
|
||||||
novel_setting=novel_settings,
|
novel_setting=novel_settings,
|
||||||
character_state=character_state + "\n\n【历史上下文】\n" + relevant_context,
|
character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context,
|
||||||
global_summary=global_summary,
|
global_summary=global_summary,
|
||||||
novel_number=novel_number,
|
novel_number=novel_number,
|
||||||
chapter_title=chapter_title,
|
chapter_title=chapter_title,
|
||||||
@@ -550,10 +556,10 @@ def generate_chapter_draft(
|
|||||||
clear_file_content(outline_file)
|
clear_file_content(outline_file)
|
||||||
save_string_to_txt(chapter_outline, outline_file)
|
save_string_to_txt(chapter_outline, outline_file)
|
||||||
|
|
||||||
# 3) 生成正文草稿
|
# 2) 生成正文草稿
|
||||||
writing_prompt_text = chapter_write_prompt.format(
|
writing_prompt_text = chapter_write_prompt.format(
|
||||||
novel_setting=novel_settings,
|
novel_setting=novel_settings,
|
||||||
character_state=character_state + "\n\n【历史上下文】\n" + relevant_context,
|
character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context,
|
||||||
global_summary=global_summary,
|
global_summary=global_summary,
|
||||||
chapter_outline=chapter_outline,
|
chapter_outline=chapter_outline,
|
||||||
word_number=word_number,
|
word_number=word_number,
|
||||||
@@ -588,13 +594,12 @@ def finalize_chapter(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
对当前章节进行定稿:
|
对当前章节进行定稿:
|
||||||
1. 读取 chapter_{novel_number}.txt 的最终内容;
|
1. 读取草稿文本
|
||||||
2. 更新全局摘要、角色状态文件;
|
2. 若字数太短则再次扩写
|
||||||
3. 如果字数明显少于 word_number 的 80%,则自动调用 enrich_chapter_text 再次扩写;
|
3. 更新全局摘要、角色状态
|
||||||
4. 更新向量库;
|
4. 更新剧情要点
|
||||||
5. 新增:更新剧情要点/未解决冲突 -> plot_arcs.txt
|
5. 更新向量库
|
||||||
"""
|
"""
|
||||||
# 读取当前章节内容
|
|
||||||
chapters_dir = os.path.join(filepath, "chapters")
|
chapters_dir = os.path.join(filepath, "chapters")
|
||||||
chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt")
|
chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt")
|
||||||
chapter_text = read_file(chapter_file).strip()
|
chapter_text = read_file(chapter_file).strip()
|
||||||
@@ -610,9 +615,9 @@ def finalize_chapter(
|
|||||||
old_global_summary = read_file(global_summary_file)
|
old_global_summary = read_file(global_summary_file)
|
||||||
old_plot_arcs = read_file(plot_arcs_file)
|
old_plot_arcs = read_file(plot_arcs_file)
|
||||||
|
|
||||||
# 1) 若字数明显不足,做 enrich
|
# 若篇幅过短,二次扩写
|
||||||
if len(chapter_text) < 0.8 * word_number:
|
if len(chapter_text) < 0.8 * word_number:
|
||||||
logging.info("Chapter text seems shorter than 80% of desired length. Attempting to enrich content...")
|
logging.info("Chapter text is shorter than 80% of desired length. Enriching...")
|
||||||
chapter_text = enrich_chapter_text(
|
chapter_text = enrich_chapter_text(
|
||||||
chapter_text=chapter_text,
|
chapter_text=chapter_text,
|
||||||
word_number=word_number,
|
word_number=word_number,
|
||||||
@@ -623,9 +628,8 @@ def finalize_chapter(
|
|||||||
)
|
)
|
||||||
clear_file_content(chapter_file)
|
clear_file_content(chapter_file)
|
||||||
save_string_to_txt(chapter_text, chapter_file)
|
save_string_to_txt(chapter_text, chapter_file)
|
||||||
logging.info("Chapter text has been enriched and updated.")
|
|
||||||
|
|
||||||
# 2) 更新全局摘要
|
# 更新全局摘要
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
@@ -643,7 +647,7 @@ def finalize_chapter(
|
|||||||
|
|
||||||
new_global_summary = update_global_summary(chapter_text, old_global_summary)
|
new_global_summary = update_global_summary(chapter_text, old_global_summary)
|
||||||
|
|
||||||
# 3) 更新角色状态
|
# 更新角色状态
|
||||||
def update_character_state(chapter_text: str, old_state: str) -> str:
|
def update_character_state(chapter_text: str, old_state: str) -> str:
|
||||||
prompt = update_character_state_prompt.format(
|
prompt = update_character_state_prompt.format(
|
||||||
chapter_text=chapter_text,
|
chapter_text=chapter_text,
|
||||||
@@ -654,7 +658,7 @@ def finalize_chapter(
|
|||||||
|
|
||||||
new_char_state = update_character_state(chapter_text, old_char_state)
|
new_char_state = update_character_state(chapter_text, old_char_state)
|
||||||
|
|
||||||
# 4) 更新剧情要点
|
# 更新剧情要点
|
||||||
new_plot_arcs = update_plot_arcs(
|
new_plot_arcs = update_plot_arcs(
|
||||||
chapter_text=chapter_text,
|
chapter_text=chapter_text,
|
||||||
old_plot_arcs=old_plot_arcs,
|
old_plot_arcs=old_plot_arcs,
|
||||||
@@ -664,7 +668,7 @@ def finalize_chapter(
|
|||||||
temperature=temperature
|
temperature=temperature
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5) 覆盖写入文件
|
# 写回文件
|
||||||
clear_file_content(character_state_file)
|
clear_file_content(character_state_file)
|
||||||
save_string_to_txt(new_char_state, character_state_file)
|
save_string_to_txt(new_char_state, character_state_file)
|
||||||
|
|
||||||
@@ -674,7 +678,7 @@ def finalize_chapter(
|
|||||||
clear_file_content(plot_arcs_file)
|
clear_file_content(plot_arcs_file)
|
||||||
save_string_to_txt(new_plot_arcs, plot_arcs_file)
|
save_string_to_txt(new_plot_arcs, plot_arcs_file)
|
||||||
|
|
||||||
# 6) 更新向量库
|
# 更新向量库
|
||||||
update_vector_store(
|
update_vector_store(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
@@ -695,7 +699,6 @@ def enrich_chapter_text(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
当章节篇幅不足时,调用此函数对章节文本进行二次扩写。
|
当章节篇幅不足时,调用此函数对章节文本进行二次扩写。
|
||||||
可以让模型补充场景描写、角色心理等,保证与现有文本风格一致。
|
|
||||||
"""
|
"""
|
||||||
model = ChatOpenAI(
|
model = ChatOpenAI(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
@@ -713,6 +716,7 @@ def enrich_chapter_text(
|
|||||||
return chapter_text
|
return chapter_text
|
||||||
return response.content.strip()
|
return response.content.strip()
|
||||||
|
|
||||||
|
|
||||||
# ============ 导入外部知识文本 ============
|
# ============ 导入外部知识文本 ============
|
||||||
|
|
||||||
def import_knowledge_file(
|
def import_knowledge_file(
|
||||||
@@ -722,11 +726,11 @@ def import_knowledge_file(
|
|||||||
embedding_model_name: str,
|
embedding_model_name: str,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
embedding_base_url: str = ""
|
embedding_base_url: str = ""
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
将用户选定的文本文件导入到向量库,以便在写作时检索。
|
将用户选定的文本文件导入到向量库,以便在写作时检索。
|
||||||
"""
|
"""
|
||||||
logging.info(f"开始导入知识库文件: {file_path},当前接口格式: {interface_format},当前模型: {embedding_model_name}")
|
logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}")
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
logging.warning(f"知识库文件不存在: {file_path}")
|
logging.warning(f"知识库文件不存在: {file_path}")
|
||||||
return
|
return
|
||||||
@@ -760,11 +764,12 @@ def advanced_split_content(content: str,
|
|||||||
similarity_threshold: float = 0.7,
|
similarity_threshold: float = 0.7,
|
||||||
max_length: int = 500) -> List[str]:
|
max_length: int = 500) -> List[str]:
|
||||||
"""
|
"""
|
||||||
将文本先按句子切分,然后根据语义相似度进行合并,最后根据max_length进行二次切分。
|
将文本先按句子切分,然后根据语义相似度进行合并,最后按max_length二次切分。
|
||||||
"""
|
"""
|
||||||
nltk.download('punkt_tab', quiet=True)
|
# 纠正下载punkt包:'punkt' 而非 'punkt_tab'
|
||||||
sentences = nltk.sent_tokenize(content)
|
nltk.download('punkt', quiet=True)
|
||||||
|
|
||||||
|
sentences = nltk.sent_tokenize(content)
|
||||||
if not sentences:
|
if not sentences:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# ui.py
|
# ui.py
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
import traceback
|
||||||
from config_manager import load_config, save_config
|
from config_manager import load_config, save_config
|
||||||
from utils import read_file, save_string_to_txt
|
from utils import read_file, save_string_to_txt
|
||||||
from novel_generator import (
|
from novel_generator import (
|
||||||
@@ -19,6 +20,12 @@ from novel_generator import (
|
|||||||
)
|
)
|
||||||
from consistency_checker import check_consistency
|
from consistency_checker import check_consistency
|
||||||
|
|
||||||
|
def log_error(message: str):
|
||||||
|
"""
|
||||||
|
用于打印详细的错误信息和堆栈信息。
|
||||||
|
"""
|
||||||
|
logging.error(f"{message}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
# 设置全局主题和颜色
|
# 设置全局主题和颜色
|
||||||
ctk.set_appearance_mode("System")
|
ctk.set_appearance_mode("System")
|
||||||
ctk.set_default_color_theme("blue")
|
ctk.set_default_color_theme("blue")
|
||||||
@@ -38,7 +45,6 @@ class NovelGeneratorGUI:
|
|||||||
self.loaded_config = load_config(self.config_file)
|
self.loaded_config = load_config(self.config_file)
|
||||||
|
|
||||||
# ========== 主要的属性变量 ==========
|
# ========== 主要的属性变量 ==========
|
||||||
# 注意:配置项较多,此处增加 embedding_model_name_var 以支持 Embedding 模型名称
|
|
||||||
self.api_key_var = ctk.StringVar(value=self.loaded_config.get("api_key", ""))
|
self.api_key_var = ctk.StringVar(value=self.loaded_config.get("api_key", ""))
|
||||||
self.base_url_var = ctk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1"))
|
self.base_url_var = ctk.StringVar(value=self.loaded_config.get("base_url", "https://api.agicto.cn/v1"))
|
||||||
self.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI"))
|
self.interface_format_var = ctk.StringVar(value=self.loaded_config.get("interface_format", "OpenAI"))
|
||||||
@@ -128,8 +134,6 @@ class NovelGeneratorGUI:
|
|||||||
# 日志:只读
|
# 日志:只读
|
||||||
self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12))
|
self.log_text = ctk.CTkTextbox(self.left_frame, wrap="word", font=("Microsoft YaHei", 12))
|
||||||
self.log_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
self.log_text.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
||||||
|
|
||||||
# 设置只读
|
|
||||||
self.log_text.configure(state="disabled")
|
self.log_text.configure(state="disabled")
|
||||||
|
|
||||||
def build_right_layout(self):
|
def build_right_layout(self):
|
||||||
@@ -137,8 +141,6 @@ class NovelGeneratorGUI:
|
|||||||
右侧用于显示配置与功能按钮。
|
右侧用于显示配置与功能按钮。
|
||||||
其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。
|
其中配置被拆分到一个子 TabView:AI接口配置 和 Embedding配置。
|
||||||
下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。
|
下面再放与小说相关的输入(主题/类型/章节数等)与功能按钮。
|
||||||
|
|
||||||
另:我们只使用 2 列布局,0 列放标签,1 列放输入框或子区域,保证对齐。
|
|
||||||
"""
|
"""
|
||||||
self.right_frame.grid_rowconfigure(0, weight=0)
|
self.right_frame.grid_rowconfigure(0, weight=0)
|
||||||
self.right_frame.grid_rowconfigure(1, weight=0)
|
self.right_frame.grid_rowconfigure(1, weight=0)
|
||||||
@@ -172,14 +174,13 @@ class NovelGeneratorGUI:
|
|||||||
- 模型名称
|
- 模型名称
|
||||||
- Temperature
|
- Temperature
|
||||||
"""
|
"""
|
||||||
# 配置网格
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
self.ai_config_tab.grid_rowconfigure(i, weight=0)
|
self.ai_config_tab.grid_rowconfigure(i, weight=0)
|
||||||
self.ai_config_tab.grid_columnconfigure(0, weight=0)
|
self.ai_config_tab.grid_columnconfigure(0, weight=0)
|
||||||
self.ai_config_tab.grid_columnconfigure(1, weight=1)
|
self.ai_config_tab.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# 回调:当接口格式下拉框发生变更时,如果 Base URL 为空,则根据接口类型自动填默认值
|
|
||||||
def on_interface_format_changed(new_value):
|
def on_interface_format_changed(new_value):
|
||||||
|
# 如果用户切换接口格式,可根据需要修改BaseURL为默认值
|
||||||
if new_value == "Ollama":
|
if new_value == "Ollama":
|
||||||
self.base_url_var.set("http://localhost:11434/v1")
|
self.base_url_var.set("http://localhost:11434/v1")
|
||||||
elif new_value == "ML Studio":
|
elif new_value == "ML Studio":
|
||||||
@@ -263,7 +264,6 @@ class NovelGeneratorGUI:
|
|||||||
def build_novel_params_area(self, start_row=2):
|
def build_novel_params_area(self, start_row=2):
|
||||||
"""
|
"""
|
||||||
放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等
|
放置与小说本身相关的一些配置:主题、类型、章节数、文件路径等
|
||||||
只使用两列:0列放Label,1列放输入框(可能加子Frame)。
|
|
||||||
"""
|
"""
|
||||||
# 主题(Topic)
|
# 主题(Topic)
|
||||||
topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12))
|
topic_label = ctk.CTkLabel(self.right_frame, text="主题(Topic):", font=("Microsoft YaHei", 12))
|
||||||
@@ -291,7 +291,7 @@ class NovelGeneratorGUI:
|
|||||||
word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80, font=("Microsoft YaHei", 12))
|
word_number_entry = ctk.CTkEntry(self.right_frame, textvariable=self.word_number_var, width=80, font=("Microsoft YaHei", 12))
|
||||||
word_number_entry.grid(row=start_row+3, column=1, padx=5, pady=5, sticky="w")
|
word_number_entry.grid(row=start_row+3, column=1, padx=5, pady=5, sticky="w")
|
||||||
|
|
||||||
# 保存路径 (使用子Frame放置 Entry + Button)
|
# 保存路径
|
||||||
filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12))
|
filepath_label = ctk.CTkLabel(self.right_frame, text="保存路径:", font=("Microsoft YaHei", 12))
|
||||||
filepath_label.grid(row=start_row+4, column=0, padx=5, pady=5, sticky="e")
|
filepath_label.grid(row=start_row+4, column=0, padx=5, pady=5, sticky="e")
|
||||||
|
|
||||||
@@ -320,11 +320,8 @@ class NovelGeneratorGUI:
|
|||||||
"""
|
"""
|
||||||
主要功能按钮 + 配置保存/加载 按钮
|
主要功能按钮 + 配置保存/加载 按钮
|
||||||
"""
|
"""
|
||||||
# 调整下布局:0,1列
|
|
||||||
# 这里按行摆放多个按钮
|
|
||||||
self.right_frame.grid_rowconfigure(start_row, weight=0)
|
self.right_frame.grid_rowconfigure(start_row, weight=0)
|
||||||
|
|
||||||
# 先放“保存配置”和“加载配置”按钮
|
|
||||||
self.btn_frame_config = ctk.CTkFrame(self.right_frame)
|
self.btn_frame_config = ctk.CTkFrame(self.right_frame)
|
||||||
self.btn_frame_config.grid(row=start_row, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
|
self.btn_frame_config.grid(row=start_row, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
|
||||||
self.btn_frame_config.columnconfigure(0, weight=1)
|
self.btn_frame_config.columnconfigure(0, weight=1)
|
||||||
@@ -336,7 +333,6 @@ class NovelGeneratorGUI:
|
|||||||
load_config_btn = ctk.CTkButton(self.btn_frame_config, text="加载配置", command=self.load_config_btn, font=("Microsoft YaHei", 12))
|
load_config_btn = ctk.CTkButton(self.btn_frame_config, text="加载配置", command=self.load_config_btn, font=("Microsoft YaHei", 12))
|
||||||
load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
load_config_btn.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
# 下面是主要功能按钮
|
|
||||||
self.btn_generate_full = ctk.CTkButton(
|
self.btn_generate_full = ctk.CTkButton(
|
||||||
self.right_frame, text="Step1. 生成设定 & 目录",
|
self.right_frame, text="Step1. 生成设定 & 目录",
|
||||||
command=self.generate_full_novel,
|
command=self.generate_full_novel,
|
||||||
@@ -389,9 +385,6 @@ class NovelGeneratorGUI:
|
|||||||
|
|
||||||
# ------------------ Novel Settings Tab ------------------
|
# ------------------ Novel Settings Tab ------------------
|
||||||
def build_setting_tab(self):
|
def build_setting_tab(self):
|
||||||
"""
|
|
||||||
可查看/编辑 Novel_setting.txt 并保存
|
|
||||||
"""
|
|
||||||
self.setting_tab.rowconfigure(0, weight=0)
|
self.setting_tab.rowconfigure(0, weight=0)
|
||||||
self.setting_tab.rowconfigure(1, weight=1)
|
self.setting_tab.rowconfigure(1, weight=1)
|
||||||
self.setting_tab.columnconfigure(0, weight=1)
|
self.setting_tab.columnconfigure(0, weight=1)
|
||||||
@@ -430,9 +423,6 @@ class NovelGeneratorGUI:
|
|||||||
|
|
||||||
# ------------------ Novel Directory Tab ------------------
|
# ------------------ Novel Directory Tab ------------------
|
||||||
def build_directory_tab(self):
|
def build_directory_tab(self):
|
||||||
"""
|
|
||||||
可查看/编辑 Novel_directory.txt 并保存
|
|
||||||
"""
|
|
||||||
self.directory_tab.rowconfigure(0, weight=0)
|
self.directory_tab.rowconfigure(0, weight=0)
|
||||||
self.directory_tab.rowconfigure(1, weight=1)
|
self.directory_tab.rowconfigure(1, weight=1)
|
||||||
self.directory_tab.columnconfigure(0, weight=1)
|
self.directory_tab.columnconfigure(0, weight=1)
|
||||||
@@ -471,9 +461,6 @@ class NovelGeneratorGUI:
|
|||||||
|
|
||||||
# ------------------ Character State Tab ------------------
|
# ------------------ Character State Tab ------------------
|
||||||
def build_character_tab(self):
|
def build_character_tab(self):
|
||||||
"""
|
|
||||||
查看/编辑 character_state.txt
|
|
||||||
"""
|
|
||||||
self.character_tab.rowconfigure(0, weight=0)
|
self.character_tab.rowconfigure(0, weight=0)
|
||||||
self.character_tab.rowconfigure(1, weight=1)
|
self.character_tab.rowconfigure(1, weight=1)
|
||||||
self.character_tab.columnconfigure(0, weight=1)
|
self.character_tab.columnconfigure(0, weight=1)
|
||||||
@@ -512,9 +499,6 @@ class NovelGeneratorGUI:
|
|||||||
|
|
||||||
# ------------------ Global Summary Tab ------------------
|
# ------------------ Global Summary Tab ------------------
|
||||||
def build_summary_tab(self):
|
def build_summary_tab(self):
|
||||||
"""
|
|
||||||
查看/编辑 global_summary.txt
|
|
||||||
"""
|
|
||||||
self.summary_tab.rowconfigure(0, weight=0)
|
self.summary_tab.rowconfigure(0, weight=0)
|
||||||
self.summary_tab.rowconfigure(1, weight=1)
|
self.summary_tab.rowconfigure(1, weight=1)
|
||||||
self.summary_tab.columnconfigure(0, weight=1)
|
self.summary_tab.columnconfigure(0, weight=1)
|
||||||
@@ -603,11 +587,9 @@ class NovelGeneratorGUI:
|
|||||||
|
|
||||||
# ------------------ 日志输出 ------------------
|
# ------------------ 日志输出 ------------------
|
||||||
def log(self, message: str):
|
def log(self, message: str):
|
||||||
# 临时切换为可写
|
|
||||||
self.log_text.configure(state="normal")
|
self.log_text.configure(state="normal")
|
||||||
self.log_text.insert("end", message + "\n")
|
self.log_text.insert("end", message + "\n")
|
||||||
self.log_text.see("end")
|
self.log_text.see("end")
|
||||||
# 再切回只读
|
|
||||||
self.log_text.configure(state="disabled")
|
self.log_text.configure(state="disabled")
|
||||||
|
|
||||||
# ------------------ 功能区 --------------------
|
# ------------------ 功能区 --------------------
|
||||||
@@ -650,6 +632,7 @@ class NovelGeneratorGUI:
|
|||||||
)
|
)
|
||||||
self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。")
|
self.log("✅ 小说设定和目录生成完成。查看 Novel_setting.txt 和 Novel_directory.txt。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f"生成小说设定 & 目录时出错: {e}")
|
||||||
self.log(f"❌ 生成小说设定 & 目录时出错: {e}")
|
self.log(f"❌ 生成小说设定 & 目录时出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.enable_button(self.btn_generate_full)
|
self.enable_button(self.btn_generate_full)
|
||||||
@@ -693,10 +676,13 @@ class NovelGeneratorGUI:
|
|||||||
chapters_dir = os.path.join(filepath, "chapters")
|
chapters_dir = os.path.join(filepath, "chapters")
|
||||||
recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3)
|
recent_3_texts = get_last_n_chapters_text(chapters_dir, chap_num, n=3)
|
||||||
|
|
||||||
# 简易生成最近章节摘要(示例)
|
# 生成最近章节摘要
|
||||||
recent_chapters_summary = summarize_recent_chapters(
|
recent_chapters_summary = summarize_recent_chapters(
|
||||||
model_name, # 此处仅示例
|
llm_model=model_name,
|
||||||
recent_3_texts
|
api_key=api_key,
|
||||||
|
base_url=base_url,
|
||||||
|
temperature=temperature,
|
||||||
|
chapters_text_list=recent_3_texts
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"开始生成第{chap_num}章草稿...")
|
self.log(f"开始生成第{chap_num}章草稿...")
|
||||||
@@ -713,7 +699,10 @@ class NovelGeneratorGUI:
|
|||||||
word_number=word_number,
|
word_number=word_number,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
novel_novel_directory=novel_directory,
|
novel_novel_directory=novel_directory,
|
||||||
filepath=filepath
|
filepath=filepath,
|
||||||
|
interface_format=self.interface_format_var.get().strip(),
|
||||||
|
embedding_model_name=self.embedding_model_name_var.get().strip(),
|
||||||
|
embedding_base_url=self.embedding_url_var.get().strip()
|
||||||
)
|
)
|
||||||
if draft_text:
|
if draft_text:
|
||||||
self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。")
|
self.log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。")
|
||||||
@@ -724,6 +713,7 @@ class NovelGeneratorGUI:
|
|||||||
self.log("⚠️ 本章草稿生成失败或无内容。")
|
self.log("⚠️ 本章草稿生成失败或无内容。")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f"生成章节草稿时出错: {e}")
|
||||||
self.log(f"❌ 生成章节草稿时出错: {e}")
|
self.log(f"❌ 生成章节草稿时出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.enable_button(self.btn_generate_chapter)
|
self.enable_button(self.btn_generate_chapter)
|
||||||
@@ -741,6 +731,8 @@ class NovelGeneratorGUI:
|
|||||||
model_name = self.model_name_var.get().strip()
|
model_name = self.model_name_var.get().strip()
|
||||||
temperature = self.temperature_var.get()
|
temperature = self.temperature_var.get()
|
||||||
filepath = self.filepath_var.get().strip()
|
filepath = self.filepath_var.get().strip()
|
||||||
|
interface_format = self.interface_format_var.get().strip()
|
||||||
|
embedding_model_name = self.embedding_model_name_var.get().strip()
|
||||||
|
|
||||||
if not filepath:
|
if not filepath:
|
||||||
self.log("请先配置保存文件路径。")
|
self.log("请先配置保存文件路径。")
|
||||||
@@ -755,6 +747,8 @@ class NovelGeneratorGUI:
|
|||||||
word_number=word_number,
|
word_number=word_number,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
|
interface_format=interface_format,
|
||||||
|
embedding_model_name=embedding_model_name,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
filepath=filepath
|
filepath=filepath
|
||||||
@@ -769,6 +763,7 @@ class NovelGeneratorGUI:
|
|||||||
self.chapter_result.see("end")
|
self.chapter_result.see("end")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f"定稿章节时出错: {e}")
|
||||||
self.log(f"❌ 定稿章节时出错: {e}")
|
self.log(f"❌ 定稿章节时出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.enable_button(self.btn_finalize_chapter)
|
self.enable_button(self.btn_finalize_chapter)
|
||||||
@@ -801,7 +796,6 @@ class NovelGeneratorGUI:
|
|||||||
global_summary = read_file(global_summary_file)
|
global_summary = read_file(global_summary_file)
|
||||||
plot_arcs = read_file(plot_arcs_file)
|
plot_arcs = read_file(plot_arcs_file)
|
||||||
|
|
||||||
# 获取当前章节文本
|
|
||||||
chap_num = self.chapter_num_var.get()
|
chap_num = self.chapter_num_var.get()
|
||||||
chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt")
|
chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt")
|
||||||
chapter_text = read_file(chap_file)
|
chapter_text = read_file(chap_file)
|
||||||
@@ -826,6 +820,7 @@ class NovelGeneratorGUI:
|
|||||||
self.log(result)
|
self.log(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f"审校时出错: {e}")
|
||||||
self.log(f"❌ 审校时出错: {e}")
|
self.log(f"❌ 审校时出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.enable_button(self.btn_check_consistency)
|
self.enable_button(self.btn_check_consistency)
|
||||||
@@ -848,12 +843,13 @@ class NovelGeneratorGUI:
|
|||||||
api_key=self.api_key_var.get().strip(),
|
api_key=self.api_key_var.get().strip(),
|
||||||
base_url=self.base_url_var.get().strip(),
|
base_url=self.base_url_var.get().strip(),
|
||||||
interface_format=self.interface_format_var.get().strip(),
|
interface_format=self.interface_format_var.get().strip(),
|
||||||
embedding_base_url=self.embedding_url_var.get().strip(),
|
embedding_model_name=self.embedding_model_name_var.get().strip(),
|
||||||
embedding_base_url=self.embedding_url_var.get().strip(),
|
file_path=selected_file,
|
||||||
file_path=selected_file
|
embedding_base_url=self.embedding_url_var.get().strip()
|
||||||
)
|
)
|
||||||
self.log("✅ 知识库文件导入完成。")
|
self.log("✅ 知识库文件导入完成。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logging.error(f"导入知识库时出错: {e}")
|
||||||
self.log(f"❌ 导入知识库时出错: {e}")
|
self.log(f"❌ 导入知识库时出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.enable_button(self.btn_import_knowledge)
|
self.enable_button(self.btn_import_knowledge)
|
||||||
@@ -886,7 +882,6 @@ class NovelGeneratorGUI:
|
|||||||
if not arcs_text:
|
if not arcs_text:
|
||||||
arcs_text = "当前没有记录的剧情要点或冲突。"
|
arcs_text = "当前没有记录的剧情要点或冲突。"
|
||||||
|
|
||||||
# 弹窗显示
|
|
||||||
top = ctk.CTkToplevel(self.master)
|
top = ctk.CTkToplevel(self.master)
|
||||||
top.title("剧情要点/未解决冲突")
|
top.title("剧情要点/未解决冲突")
|
||||||
top.geometry("600x400")
|
top.geometry("600x400")
|
||||||
|
|||||||
Reference in New Issue
Block a user