@@ -27,11 +27,8 @@ def get_chapter_info_from_directory(novel_directory_content: str, chapter_number
|
||||
if match:
|
||||
chap_num = int(match.group(1))
|
||||
if chap_num == chapter_number:
|
||||
# group(2) 可能是标题及简述的混合
|
||||
full_title = match.group(2).strip()
|
||||
# 这里假设用 '-' 进一步区分“标题 - 简述”,也可能用户没写“ - ”
|
||||
if ' - ' in full_title:
|
||||
# 根据你的目录格式自由处理
|
||||
parts = full_title.split(' - ', 1)
|
||||
return {
|
||||
"chapter_title": parts[0].strip(),
|
||||
|
||||
@@ -44,7 +44,7 @@ exe = EXE(
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='AI_NovelGenerator_V1.3.0',
|
||||
name='AI_NovelGenerator_V1.3.2',
|
||||
debug=True,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
@@ -65,5 +65,5 @@ coll = COLLECT(
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='AI_NovelGenerator_V1.3.0'
|
||||
name='AI_NovelGenerator_V1.3.2'
|
||||
)
|
||||
|
||||
+145
-104
@@ -8,7 +8,7 @@ import traceback
|
||||
from typing import List, Optional
|
||||
|
||||
# langchain 相关
|
||||
from langchain_openai import ChatOpenAI,OpenAIEmbeddings
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from langchain_chroma import Chroma
|
||||
from chromadb.config import Settings
|
||||
from langchain.docstore.document import Document
|
||||
@@ -51,6 +51,10 @@ def remove_think_tags(text: str) -> str:
|
||||
"""移除 <think>...</think> 包裹的内容"""
|
||||
return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
|
||||
|
||||
def debug_log(prompt: str, response_content: str):
|
||||
logging.info(f"\n[######################################### Prompt #########################################]\n {prompt}\n")
|
||||
logging.info(f"\n[######################################### Response #########################################]\n {response_content}\n")
|
||||
|
||||
def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str:
|
||||
"""通用封装:调用模型并移除 <think>...</think> 文本,记录日志后返回"""
|
||||
response = model.invoke(prompt)
|
||||
@@ -61,11 +65,6 @@ def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str:
|
||||
debug_log(prompt, cleaned_text)
|
||||
return cleaned_text.strip()
|
||||
|
||||
def debug_log(prompt: str, response_content: str):
|
||||
logging.info(f"\n[Prompt >>>] {prompt}\n")
|
||||
logging.info(f"[Response >>>] {response_content}\n")
|
||||
|
||||
|
||||
def ensure_openai_base_url_has_v1(url: str) -> str:
|
||||
"""
|
||||
若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'。
|
||||
@@ -79,7 +78,6 @@ def ensure_openai_base_url_has_v1(url: str) -> str:
|
||||
url = url.rstrip('/') + '/v1'
|
||||
return url
|
||||
|
||||
|
||||
def is_using_ollama_api(interface_format: str) -> bool:
|
||||
return interface_format.lower() == "ollama"
|
||||
|
||||
@@ -113,6 +111,7 @@ def create_embeddings_object(
|
||||
base_url=fixed_url
|
||||
)
|
||||
else:
|
||||
# OpenAI 或 ML Studio 均使用 OpenAIEmbeddings,注意 base_url 可能需要 ensure /v1
|
||||
fixed_url = ensure_openai_base_url_has_v1(base_url)
|
||||
return OpenAIEmbeddings(
|
||||
openai_api_key=api_key,
|
||||
@@ -122,38 +121,26 @@ def create_embeddings_object(
|
||||
|
||||
|
||||
# ============ 向量库相关操作 ============
|
||||
def clear_vector_store(filepath: str):
|
||||
def clear_vector_store(filepath: str) -> bool:
|
||||
"""
|
||||
不删除文件,仅通过 Chroma API 移除集合数据(保留空目录)
|
||||
返回值表示是否成功清空向量库。
|
||||
"""
|
||||
from chromadb import Client
|
||||
import shutil
|
||||
|
||||
store_dir = get_vectorstore_dir(filepath)
|
||||
if not os.path.exists(store_dir):
|
||||
logging.info("No vector store found to clear.")
|
||||
return
|
||||
return False
|
||||
|
||||
try:
|
||||
client = Client(settings=Settings(
|
||||
persist_directory=store_dir,
|
||||
allow_reset=True # 允许重置操作
|
||||
))
|
||||
print(client.list_collections())
|
||||
|
||||
if client.list_collections():
|
||||
client.delete_collection(name="novel_collection")
|
||||
logging.info("Collection 'novel_collection' deleted via API.")
|
||||
|
||||
client.reset()
|
||||
|
||||
if os.path.exists(store_dir):
|
||||
shutil.rmtree(store_dir)
|
||||
logging.info(f"Vector store directory '{store_dir}' removed.")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"API-based clear failed: {str(e)}")
|
||||
logging.error(f"程序正在运行,无法删除,请在程序关闭后手动前往 {store_dir} 删除目录。\n {str(e)}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# 显式关闭客户端释放资源
|
||||
if 'client' in locals():
|
||||
del client
|
||||
|
||||
return False
|
||||
|
||||
def init_vector_store(
|
||||
api_key: str,
|
||||
@@ -215,6 +202,65 @@ def load_vector_store(
|
||||
)
|
||||
|
||||
|
||||
def split_by_length(text: str, max_length: int = 500) -> List[str]:
|
||||
segments = []
|
||||
start_idx = 0
|
||||
while start_idx < len(text):
|
||||
end_idx = min(start_idx + max_length, len(text))
|
||||
segment = text[start_idx:end_idx]
|
||||
segments.append(segment.strip())
|
||||
start_idx = end_idx
|
||||
return segments
|
||||
|
||||
|
||||
def split_text_for_vectorstore(chapter_text: str,
|
||||
max_length: int = 500,
|
||||
similarity_threshold: float = 0.7) -> List[str]:
|
||||
"""
|
||||
对新的章节文本进行分段后,再用于存入向量库。
|
||||
"""
|
||||
if not chapter_text.strip():
|
||||
return []
|
||||
|
||||
nltk.download('punkt', quiet=True)
|
||||
nltk.download('punkt_tab', quiet=True)
|
||||
sentences = nltk.sent_tokenize(chapter_text)
|
||||
if not sentences:
|
||||
return []
|
||||
|
||||
# 先对相近句子进行合并
|
||||
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
|
||||
embeddings = model.encode(sentences)
|
||||
|
||||
merged_paragraphs = []
|
||||
current_sentences = [sentences[0]]
|
||||
current_embedding = embeddings[0]
|
||||
|
||||
for i in range(1, len(sentences)):
|
||||
sim = cosine_similarity([current_embedding], [embeddings[i]])[0][0]
|
||||
if sim >= similarity_threshold:
|
||||
current_sentences.append(sentences[i])
|
||||
current_embedding = (current_embedding + embeddings[i]) / 2.0
|
||||
else:
|
||||
merged_paragraphs.append(" ".join(current_sentences))
|
||||
current_sentences = [sentences[i]]
|
||||
current_embedding = embeddings[i]
|
||||
|
||||
if current_sentences:
|
||||
merged_paragraphs.append(" ".join(current_sentences))
|
||||
|
||||
# 再对合并好的段落做 max_length 切分
|
||||
final_segments = []
|
||||
for para in merged_paragraphs:
|
||||
if len(para) > max_length:
|
||||
sub_segments = split_by_length(para, max_length=max_length)
|
||||
final_segments.extend(sub_segments)
|
||||
else:
|
||||
final_segments.append(para)
|
||||
|
||||
return final_segments
|
||||
|
||||
|
||||
def update_vector_store(
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
@@ -226,6 +272,11 @@ def update_vector_store(
|
||||
"""
|
||||
将最新章节文本插入到向量库中。若库不存在则初始化。
|
||||
"""
|
||||
splitted_texts = split_text_for_vectorstore(new_chapter)
|
||||
if not splitted_texts:
|
||||
logging.warning("No valid text to insert into vector store. Skipping.")
|
||||
return
|
||||
|
||||
store = load_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
@@ -240,14 +291,14 @@ def update_vector_store(
|
||||
base_url=base_url,
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
texts=[new_chapter],
|
||||
texts=splitted_texts,
|
||||
filepath=filepath
|
||||
)
|
||||
return
|
||||
|
||||
new_doc = Document(page_content=str(new_chapter))
|
||||
store.add_documents([new_doc])
|
||||
logging.info("Vector store updated with the new chapter.")
|
||||
docs = [Document(page_content=str(t)) for t in splitted_texts]
|
||||
store.add_documents(docs)
|
||||
logging.info("Vector store updated with the new chapter splitted segments.")
|
||||
|
||||
|
||||
def get_relevant_context_from_vector_store(
|
||||
@@ -480,37 +531,38 @@ def generate_chapter_draft(
|
||||
filepath: str,
|
||||
interface_format: str,
|
||||
embedding_model_name: str,
|
||||
embedding_base_url: str
|
||||
embedding_base_url: str,
|
||||
embedding_retrieval_k: int = 4
|
||||
) -> str:
|
||||
# 1) 根据目录解析标题、简介
|
||||
chapter_info = get_chapter_info_from_directory(novel_novel_directory, novel_number)
|
||||
chapter_title = chapter_info["chapter_title"]
|
||||
chapter_brief = chapter_info["chapter_brief"]
|
||||
|
||||
# 2) 从向量库检索上下文
|
||||
queries = []
|
||||
# 合并要检索的文本(用户指导 + 章节简介 + 最近摘要)
|
||||
combined_query_parts = []
|
||||
if user_guidance.strip():
|
||||
queries.append(user_guidance)
|
||||
combined_query_parts.append(user_guidance)
|
||||
if chapter_brief.strip():
|
||||
queries.append(chapter_brief)
|
||||
combined_query_parts.append(chapter_brief)
|
||||
if recent_chapters_summary.strip():
|
||||
queries.append(recent_chapters_summary)
|
||||
queries.append("回顾剧情")
|
||||
combined_query_parts.append(recent_chapters_summary)
|
||||
# 额外加一个关键字
|
||||
combined_query_parts.append("回顾剧情")
|
||||
|
||||
relevant_context = ""
|
||||
for q in queries:
|
||||
partial_context = get_relevant_context_from_vector_store(
|
||||
merged_query_str = "\n".join(combined_query_parts)
|
||||
|
||||
# 2) 从向量库检索上下文
|
||||
relevant_context = get_relevant_context_from_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=embedding_base_url if embedding_base_url else base_url,
|
||||
query=q,
|
||||
query=merged_query_str,
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
filepath=filepath,
|
||||
k=2
|
||||
k=embedding_retrieval_k
|
||||
)
|
||||
if partial_context.strip():
|
||||
relevant_context += "\n" + partial_context
|
||||
if not relevant_context:
|
||||
if not relevant_context.strip():
|
||||
relevant_context = "暂无相关内容。"
|
||||
|
||||
# 3) 生成本章大纲
|
||||
@@ -547,6 +599,7 @@ def generate_chapter_draft(
|
||||
global_summary=global_summary,
|
||||
chapter_outline=chapter_outline,
|
||||
word_number=word_number,
|
||||
novel_number=novel_number,
|
||||
chapter_title=chapter_title,
|
||||
chapter_brief=chapter_brief
|
||||
)
|
||||
@@ -615,6 +668,7 @@ def finalize_chapter(
|
||||
base_url=ensure_openai_base_url_has_v1(base_url),
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
def update_global_summary(chapter_text: str, old_summary: str) -> str:
|
||||
prompt = summary_prompt.format(
|
||||
chapter_text=chapter_text,
|
||||
@@ -690,59 +744,13 @@ def enrich_chapter_text(
|
||||
|
||||
|
||||
# ============ 导入外部知识文本到向量库 ============
|
||||
def import_knowledge_file(
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
interface_format: str,
|
||||
embedding_model_name: str,
|
||||
file_path: str,
|
||||
embedding_base_url: str,
|
||||
filepath: str
|
||||
):
|
||||
logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}")
|
||||
if not os.path.exists(file_path):
|
||||
logging.warning(f"知识库文件不存在: {file_path}")
|
||||
return
|
||||
|
||||
content = read_file(file_path)
|
||||
if not content.strip():
|
||||
logging.warning("知识库文件内容为空。")
|
||||
return
|
||||
|
||||
nltk.download('punkt', quiet=True)
|
||||
|
||||
paragraphs = advanced_split_content(content)
|
||||
|
||||
# 若向量库不存在则初始化,否则追加
|
||||
store = load_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=base_url if base_url else "http://localhost:11434/v1",
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
filepath=filepath
|
||||
)
|
||||
if not store:
|
||||
logging.info("Vector store does not exist. Initializing a new one for knowledge import...")
|
||||
init_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=base_url if base_url else "http://localhost:11434/v1",
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
texts=paragraphs,
|
||||
filepath=filepath
|
||||
)
|
||||
else:
|
||||
docs = [Document(page_content=str(p)) for p in paragraphs]
|
||||
store.add_documents(docs)
|
||||
logging.info("知识库文件已成功导入至向量库。")
|
||||
|
||||
|
||||
def advanced_split_content(content: str,
|
||||
similarity_threshold: float = 0.7,
|
||||
max_length: int = 500) -> List[str]:
|
||||
"""
|
||||
将文本先按句子切分,然后根据语义相似度进行合并,最后按 max_length 二次切分。
|
||||
"""
|
||||
nltk.download('punkt', quiet=True)
|
||||
sentences = nltk.sent_tokenize(content)
|
||||
if not sentences:
|
||||
return []
|
||||
@@ -777,13 +785,46 @@ def advanced_split_content(content: str,
|
||||
|
||||
return final_segments
|
||||
|
||||
def import_knowledge_file(
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
interface_format: str,
|
||||
embedding_model_name: str,
|
||||
file_path: str,
|
||||
embedding_base_url: str,
|
||||
filepath: str
|
||||
):
|
||||
logging.info(f"开始导入知识库文件: {file_path}, 接口格式: {interface_format}, 模型: {embedding_model_name}")
|
||||
if not os.path.exists(file_path):
|
||||
logging.warning(f"知识库文件不存在: {file_path}")
|
||||
return
|
||||
|
||||
def split_by_length(text: str, max_length: int = 500) -> List[str]:
|
||||
segments = []
|
||||
start_idx = 0
|
||||
while start_idx < len(text):
|
||||
end_idx = min(start_idx + max_length, len(text))
|
||||
segment = text[start_idx:end_idx]
|
||||
segments.append(segment.strip())
|
||||
start_idx = end_idx
|
||||
return segments
|
||||
content = read_file(file_path)
|
||||
if not content.strip():
|
||||
logging.warning("知识库文件内容为空。")
|
||||
return
|
||||
|
||||
paragraphs = advanced_split_content(content)
|
||||
|
||||
# 若向量库不存在则初始化,否则追加
|
||||
store = load_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=base_url if base_url else "http://localhost:11434/v1",
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
filepath=filepath
|
||||
)
|
||||
if not store:
|
||||
logging.info("Vector store does not exist. Initializing a new one for knowledge import...")
|
||||
init_vector_store(
|
||||
api_key=api_key,
|
||||
base_url=base_url if base_url else "http://localhost:11434/v1",
|
||||
interface_format=interface_format,
|
||||
embedding_model_name=embedding_model_name,
|
||||
texts=paragraphs,
|
||||
filepath=filepath
|
||||
)
|
||||
else:
|
||||
docs = [Document(page_content=str(p)) for p in paragraphs]
|
||||
store.add_documents(docs)
|
||||
logging.info("知识库文件已成功导入至向量库。")
|
||||
|
||||
+80
-186
@@ -1,249 +1,143 @@
|
||||
# prompt_definitions.py
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
小说创作辅助系统的提示词(Prompt)集合。
|
||||
这些提示词被设计用于引导AI生成连贯、丰富的小说内容。
|
||||
集中存放所有提示词(Prompt),新版本更精确、更具创新性,结合部分雪花写作法等理论。
|
||||
"""
|
||||
|
||||
# =============== 基础设定与规划提示词 ===================
|
||||
# =============== 1. 整体设定 ===================
|
||||
set_prompt = """\
|
||||
基于主题「{topic}」、类型「{genre}」,将创作一部{number_of_chapters}章、每章约{word_number}字的小说。
|
||||
请详细规划以下要素:
|
||||
请根据主题: {topic}、类型: {genre}、章数: {number_of_chapters}、每章字数: {word_number} 来设计小说的总体结构和世界观。
|
||||
写作时请参考雪花写作法等创作理论,结合以下要点:
|
||||
|
||||
【基本信息】
|
||||
• 建议书名(可含副标题)
|
||||
• 预估总字数区间
|
||||
• 主要类型定位(如:奇幻/都市/科幻等)
|
||||
• 基调与氛围(如:史诗/轻松/黑暗等)
|
||||
• 小说标题与大致篇幅(总字数范围)。
|
||||
• 类型与基调(如:都市、魔幻、战争、轻松、暗黑等)。
|
||||
• 主要写作风格(视角、叙述方式、语言风格等)。
|
||||
• 世界观设定(背景时间、地理环境、社会结构、科技或魔法水平、重要历史等)。
|
||||
• 整体故事框架(可提及常见叙事结构:三幕、英雄之旅等)。
|
||||
• 主线与副线的初步构想,以及它们如何交织。
|
||||
• 关键角色群像定位与主要冲突关系。
|
||||
• 结局的可能方向(圆满、悲剧、开放式等)。
|
||||
|
||||
【创作风格】
|
||||
• 叙事视角选择及理由
|
||||
• 语言风格特点
|
||||
• 节奏把控思路
|
||||
|
||||
【世界观构筑】
|
||||
• 时空背景设定
|
||||
• 世界运行规则(社会/科技/魔法体系等)
|
||||
• 重大历史事件或背景
|
||||
|
||||
【核心故事】
|
||||
• 主线故事框架
|
||||
• 重要支线规划
|
||||
• 核心冲突设置
|
||||
• 结局走向构思
|
||||
|
||||
请具体阐述以上各点,确保前后呼应、逻辑自洽。
|
||||
请以简洁、逻辑清晰的方式输出,保留足够细节以支撑后续创作。
|
||||
"""
|
||||
|
||||
# =============== 2. 角色设定 ===================
|
||||
character_prompt = """\
|
||||
基于已确立的小说设定:
|
||||
基于已生成的小说整体设定:
|
||||
{novel_setting}
|
||||
|
||||
请完善以下角色体系:
|
||||
请进一步扩展角色设置,至少包含三名核心角色。对每位角色说明:
|
||||
• 角色背景、外貌与主要性格特征。
|
||||
• 内在冲突、目标与动机。
|
||||
• 暗藏的秘密或潜在弱点(可与世界观或其他角色有关)。
|
||||
• 与其他角色的关系或对立点,如何推动或阻碍情节发展。
|
||||
|
||||
【核心角色塑造】(至少3个)
|
||||
• 角色基本信息(名字/年龄/身份等)
|
||||
• 外在特征与性格特点
|
||||
• 核心价值观与行为模式
|
||||
• 个人成长轨迹设想
|
||||
• 独特能力或专长
|
||||
|
||||
【人物关系网络】
|
||||
• 角色间的重要关联
|
||||
• 潜在矛盾点
|
||||
• 关系发展预期
|
||||
|
||||
【隐藏维度】
|
||||
• 每个角色的个人秘密
|
||||
• 待揭示的过往经历
|
||||
• 性格中的矛盾面
|
||||
|
||||
重点说明这些特质如何推动故事发展,为情节转折提供基础。
|
||||
请重点突出角色的多重面向,为后续情节埋下伏笔。
|
||||
"""
|
||||
|
||||
# =============== 3. 暗线与伏笔 ===================
|
||||
dark_lines_prompt = """\
|
||||
根据已设定的角色与背景:
|
||||
在当前世界观与角色关系中:
|
||||
{character_info}
|
||||
|
||||
请构建以下隐藏线索体系:
|
||||
请构思若干暗线、伏笔或隐藏冲突,结合雪花写作法“细节逐步扩展”的思路。要求:
|
||||
• 每条暗线给出初始迹象、后续发展与可能的爆发条件。
|
||||
• 与角色背景、世界观或关键事件相呼应。
|
||||
• 保持合理的悬念设置,与现有设定不冲突。
|
||||
• 为后续剧情保留足够展开空间,强调持续影响故事进程。
|
||||
|
||||
【关键暗线设计】
|
||||
• 暗线起源与表现形式
|
||||
• 发展脉络规划
|
||||
• 揭示时机与方式
|
||||
• 对整体故事的影响
|
||||
|
||||
【伏笔布置】
|
||||
• 早期暗示点设置
|
||||
• 中期发展线索
|
||||
• 后期爆发契机
|
||||
|
||||
【隐藏冲突】
|
||||
• 角色间潜在矛盾
|
||||
• 阵营对立根源
|
||||
• 价值观冲突点
|
||||
|
||||
确保这些暗线自然融入故事,避免生硬设置。建议提供具体场景建议。
|
||||
请避免一次性透漏全部细节,以逐步揭示的方式为后文做铺垫。
|
||||
"""
|
||||
|
||||
# =============== 4. 最终设定整合 ===================
|
||||
finalize_setting_prompt = """\
|
||||
请整合以下创作准备内容:
|
||||
|
||||
【现有设定】
|
||||
基于以下内容,请整合并输出终稿《小说设定》:
|
||||
1. 整体设定:
|
||||
{novel_setting_base}
|
||||
|
||||
【角色系统】
|
||||
2. 角色设定:
|
||||
{character_setting}
|
||||
|
||||
【暗线规划】
|
||||
3. 暗线与伏笔:
|
||||
{dark_lines}
|
||||
|
||||
将以上要素整合为完整的创作蓝图:
|
||||
|
||||
1. 总体框架
|
||||
• 核心故事脉络
|
||||
• 世界观体系
|
||||
• 主题表达方式
|
||||
|
||||
2. 人物系统
|
||||
• 角色群像
|
||||
• 关系网络
|
||||
• 成长轨迹
|
||||
|
||||
3. 情节编排
|
||||
• 主线发展
|
||||
• 支线设计
|
||||
• 暗线铺陈
|
||||
|
||||
4. 创作建议
|
||||
• 重点场景构思
|
||||
• 节奏控制要点
|
||||
• 细节描写建议
|
||||
|
||||
请以流畅的叙述文本呈现,突出要素间的有机联系。
|
||||
整合要求:
|
||||
• 以整体视角整理世界观、角色与暗线,形成一个有机结合的故事框架。
|
||||
• 注意角色动机与暗线如何与世界观互相呼应,并兼顾主线与副线节奏。
|
||||
• 语言通顺、层次分明。直接输出文本,不使用Markdown格式。
|
||||
"""
|
||||
|
||||
# =============== 5. 小说目录 ===================
|
||||
novel_directory_prompt = """\
|
||||
依据最终设定:
|
||||
根据以下最终《小说设定》:
|
||||
{final_novel_setting}
|
||||
|
||||
请规划{number_of_chapters}章的详细目录。每章格式:
|
||||
第N章:章节名 - 核心内容提示
|
||||
|
||||
要求:
|
||||
1. 章节名需简明扼要,富有吸引力
|
||||
2. 核心内容提示需点明关键信息,为创作提供指引
|
||||
3. 整体节奏要富有张力,符合三幕结构
|
||||
4. 适当预留转折与高潮
|
||||
|
||||
示例:
|
||||
第n章:黎明前的暗影 - 主角遭遇神秘袭击,接触核心谜题
|
||||
请列出共 {number_of_chapters} 章的目录,并保证每章的标题或简述能呼应主要冲突、角色成长及暗线埋设。
|
||||
目录格式示例:
|
||||
第1章 :<标题> - <简要说明>
|
||||
第2章 :<标题> - <简要说明>
|
||||
...
|
||||
第{number_of_chapters}章 :<标题> - <简要说明>
|
||||
|
||||
每章可酌情加入一句简述,引导读者了解本章看点。直接输出文本,不使用Markdown。
|
||||
"""
|
||||
|
||||
# =============== 章节创作辅助提示词 ===================
|
||||
# =============== 6. 全局摘要更新 ===================
|
||||
summary_prompt = """\
|
||||
新增章节内容:
|
||||
以下是新完成的章节文本:
|
||||
{chapter_text}
|
||||
|
||||
当前全局摘要:
|
||||
这是当前的全局摘要(可为空):
|
||||
{global_summary}
|
||||
|
||||
请更新全局摘要(控制在3000字以内):
|
||||
|
||||
【已发生】
|
||||
• 关键事件梳理
|
||||
• 人物关系变化
|
||||
• 重要线索进展
|
||||
|
||||
【正在进行】
|
||||
• 当前危机/冲突
|
||||
• 角色动态
|
||||
• 悬而未决的问题
|
||||
|
||||
确保摘要重点突出,为后续创作提供清晰参考。
|
||||
不展开未来发展,保持故事悬念。
|
||||
请根据本章新增内容,更新全局摘要。保留既有重要信息,同时融入新剧情要点,勿剧透未来。控制在不超过3000字的范围内,语言简练流畅。
|
||||
"""
|
||||
|
||||
# =============== 7. 角色状态更新 ===================
|
||||
update_character_state_prompt = """\
|
||||
本章内容:
|
||||
以下是新完成的章节文本:
|
||||
{chapter_text}
|
||||
|
||||
现有角色状态:
|
||||
这是当前的角色状态文档(可为空):
|
||||
{old_state}
|
||||
|
||||
请更新角色状态档案:
|
||||
请更新角色状态,内容包括:
|
||||
• 角色的物品、能力或心理状态变化。
|
||||
• 角色间关系的最新进展或冲突。
|
||||
• 是否触发或加深了某些暗线或关键事件。
|
||||
• 任何新增角色或临时出场人物的基本信息。
|
||||
|
||||
【角色发展】
|
||||
• 能力/状态变化
|
||||
• 重要物品获得/失去
|
||||
• 性格/观念的微妙改变
|
||||
|
||||
【人际关系】
|
||||
• 新建立的联系
|
||||
• 关系的强化或弱化
|
||||
• 潜在矛盾点
|
||||
|
||||
【事件参与】
|
||||
• 正在进行的事件
|
||||
• 个人目标进展
|
||||
• 新接触的任务
|
||||
|
||||
请保持简洁明了,便于后续参考。
|
||||
请直接在已有文档基础上进行增删,语言尽量简洁、有条理。
|
||||
"""
|
||||
|
||||
# =============== 8. 章节大纲设计 ===================
|
||||
chapter_outline_prompt = """\
|
||||
创作参考资料:
|
||||
- 设定:{novel_setting}
|
||||
这是当前小说的重要信息:
|
||||
- 小说设定:{novel_setting}
|
||||
- 角色状态:{character_state}
|
||||
- 全局摘要:{global_summary}
|
||||
|
||||
第{novel_number}章:{chapter_title}
|
||||
章节说明:{chapter_brief}
|
||||
我们即将写第 {novel_number} 章,标题:{chapter_title},简述(若有):{chapter_brief}
|
||||
|
||||
请设计本章节详细大纲:
|
||||
请按照以下思路设计本章大纲:
|
||||
1. 本章的主要冲突或情节目标,如何与标题呼应。
|
||||
2. 出场角色及其动机,对剧情走向的推动或阻碍。
|
||||
3. 暗线或伏笔如何有所进展或被揭示。
|
||||
4. 本章结尾的悬念或转折,如何为下一章做铺垫。
|
||||
|
||||
【核心设计】
|
||||
• 本章主要冲突/事件
|
||||
• 与章节主题的呼应方式
|
||||
• 情节推进目标
|
||||
|
||||
【人物安排】
|
||||
• 出场角色及其状态
|
||||
• 个人目标与动机
|
||||
• 互动关系设计
|
||||
|
||||
【暗线发展】
|
||||
• 已有伏笔的推进
|
||||
• 新增悬念的埋设
|
||||
• 线索的暗示方式
|
||||
|
||||
【结构布局】
|
||||
• 章节节奏规划
|
||||
• 高潮设计
|
||||
• 结尾悬念构思
|
||||
请以简要分点方式输出,不要使用Markdown格式。
|
||||
"""
|
||||
|
||||
# =============== 9. 章节正文写作 ===================
|
||||
chapter_write_prompt = """\
|
||||
创作参考信息:
|
||||
1. 设定:{novel_setting}
|
||||
以下信息供你参考:
|
||||
1. 小说设定:{novel_setting}
|
||||
2. 角色状态:{character_state}
|
||||
3. 摘要:{global_summary}
|
||||
4. 大纲:{chapter_outline}
|
||||
3. 全局摘要:{global_summary}
|
||||
4. 本章大纲:{chapter_outline}
|
||||
|
||||
第{chapter_title}章
|
||||
核心:{chapter_brief}
|
||||
请写出第 {novel_number} 章的正文,标题为“{chapter_title}”。需要:
|
||||
• 字数不少于 {word_number} 字,与标题和简述相呼应。
|
||||
• 保持连贯叙述,可增加环境、心理、对话等细节描写。
|
||||
• 适度呼应前文暗线或角色矛盾,为后续发展留出悬念。
|
||||
|
||||
创作要求:
|
||||
1. 字数不少于{word_number}字
|
||||
2. 紧扣章节主题
|
||||
3. 注重细节描写
|
||||
4. 深入角色内心
|
||||
5. 为下章预留引子
|
||||
|
||||
建议:
|
||||
• 通过环境描写渲染氛围
|
||||
• 展现人物细微情感变化
|
||||
• 适当运用对话推进情节
|
||||
• 保持节奏张弛有度
|
||||
直接输出正文内容,不使用分章节小标题,章节末模仿正常小说中断或转场。
|
||||
"""
|
||||
@@ -28,6 +28,7 @@ def log_error(message: str):
|
||||
"""
|
||||
logging.error(f"{message}\n{traceback.format_exc()}")
|
||||
|
||||
|
||||
# 设置全局主题和颜色
|
||||
ctk.set_appearance_mode("System")
|
||||
ctk.set_default_color_theme("blue")
|
||||
@@ -53,11 +54,14 @@ class NovelGeneratorGUI:
|
||||
self.loaded_config = load_config(self.config_file)
|
||||
|
||||
# ========== 主要的属性变量 ==========
|
||||
|
||||
# LLM 接口相关
|
||||
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.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"))
|
||||
|
||||
# 仍然用 DoubleVar,但因为是 Slider,不会让用户手动清空文本,一般不会出现空字符串问题
|
||||
self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7))
|
||||
|
||||
# Embedding 接口相关
|
||||
@@ -66,14 +70,21 @@ class NovelGeneratorGUI:
|
||||
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", ""))
|
||||
|
||||
# ### CHANGED:将 IntVar 改为 StringVar,避免用户清空输入时抛错
|
||||
self.embedding_retrieval_k_var = ctk.StringVar(value=str(self.loaded_config.get("embedding_retrieval_k", 4)))
|
||||
|
||||
# 小说通用参数
|
||||
self.topic_default = self.loaded_config.get("topic", "")
|
||||
self.genre_var = ctk.StringVar(value=self.loaded_config.get("genre", "玄幻"))
|
||||
self.num_chapters_var = ctk.IntVar(value=self.loaded_config.get("num_chapters", 10))
|
||||
self.word_number_var = ctk.IntVar(value=self.loaded_config.get("word_number", 3000))
|
||||
|
||||
# ### CHANGED:将章节数、每章字数改为 StringVar
|
||||
self.num_chapters_var = ctk.StringVar(value=str(self.loaded_config.get("num_chapters", 10)))
|
||||
self.word_number_var = ctk.StringVar(value=str(self.loaded_config.get("word_number", 3000)))
|
||||
|
||||
self.filepath_var = ctk.StringVar(value=self.loaded_config.get("filepath", ""))
|
||||
|
||||
self.chapter_num_var = ctk.IntVar(value=1)
|
||||
# ### CHANGED:章节号也改为 StringVar
|
||||
self.chapter_num_var = ctk.StringVar(value="1")
|
||||
|
||||
# ========== 主容器使用 TabView ==========
|
||||
self.tabview = ctk.CTkTabview(self.master, width=1200, height=800)
|
||||
@@ -95,11 +106,18 @@ class NovelGeneratorGUI:
|
||||
self.build_summary_tab()
|
||||
self.build_chapters_tab() # 新增
|
||||
|
||||
# ------------------ 统一异常处理方法 ------------------
|
||||
def handle_exception(self, context: str):
|
||||
full_message = f"{context}\n{traceback.format_exc()}"
|
||||
logging.error(full_message)
|
||||
self.safe_log(full_message)
|
||||
# ------------------ 工具方法:安全获取 IntVar (现已兼容 StringVar) ------------------
|
||||
def safe_get_int(self, var, default=1):
|
||||
"""
|
||||
尝试把 StringVar 或 IntVar 中的值转换为 int;
|
||||
若失败则将其重置为 default 并返回 default。
|
||||
"""
|
||||
try:
|
||||
val_str = str(var.get()).strip()
|
||||
return int(val_str)
|
||||
except:
|
||||
var.set(str(default))
|
||||
return default
|
||||
|
||||
# ------------------ 主功能 Tab ------------------
|
||||
def build_main_tab(self):
|
||||
@@ -144,7 +162,7 @@ class NovelGeneratorGUI:
|
||||
chapter_label = ctk.CTkLabel(
|
||||
self.left_frame,
|
||||
text="本章内容 (可编辑)",
|
||||
font=("Microsoft YaHei", 14)
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
chapter_label.grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||
|
||||
@@ -163,7 +181,7 @@ class NovelGeneratorGUI:
|
||||
log_label = ctk.CTkLabel(
|
||||
self.left_frame,
|
||||
text="输出日志 (只读)",
|
||||
font=("Microsoft YaHei", 14)
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
log_label.grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||
|
||||
@@ -400,13 +418,13 @@ class NovelGeneratorGUI:
|
||||
def build_embeddings_config_tab(self):
|
||||
def on_embedding_interface_changed(new_value):
|
||||
if new_value == "Ollama":
|
||||
self.embedding_url_var.set("http://localhost:11434/v1")
|
||||
self.embedding_url_var.set("http://localhost:11434/api")
|
||||
elif new_value == "ML Studio":
|
||||
self.embedding_url_var.set("http://localhost:1234/v1")
|
||||
elif new_value == "OpenAI":
|
||||
self.embedding_url_var.set("https://api.openai.com/v1")
|
||||
|
||||
for i in range(3):
|
||||
for i in range(5):
|
||||
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)
|
||||
@@ -466,6 +484,19 @@ class NovelGeneratorGUI:
|
||||
)
|
||||
emb_model_name_entry.grid(row=3, column=1, padx=5, pady=5, sticky="nsew")
|
||||
|
||||
emb_retrieval_k_label = ctk.CTkLabel(
|
||||
self.embeddings_config_tab,
|
||||
text="Retrieval Top-K:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
emb_retrieval_k_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
|
||||
emb_retrieval_k_entry = ctk.CTkEntry(
|
||||
self.embeddings_config_tab,
|
||||
textvariable=self.embedding_retrieval_k_var,
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
emb_retrieval_k_entry.grid(row=4, column=1, padx=5, pady=5, sticky="nsew")
|
||||
|
||||
# ========== 保存/加载 配置按钮区域 ==========
|
||||
def build_main_buttons_area(self):
|
||||
"""
|
||||
@@ -499,7 +530,7 @@ class NovelGeneratorGUI:
|
||||
"""
|
||||
self.params_frame = ctk.CTkScrollableFrame(
|
||||
self.right_frame,
|
||||
orientation="vertical" # 默认垂直滚动
|
||||
orientation="vertical"
|
||||
)
|
||||
self.params_frame.grid(row=start_row, column=0, sticky="nsew", padx=5, pady=5)
|
||||
self.params_frame.columnconfigure(1, weight=1)
|
||||
@@ -536,35 +567,44 @@ class NovelGeneratorGUI:
|
||||
)
|
||||
genre_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
||||
|
||||
# 章节数
|
||||
# 章节数、每章字数 放在同一行
|
||||
row_for_chapter_and_word = 2
|
||||
num_chapters_label = ctk.CTkLabel(
|
||||
self.params_frame,
|
||||
text="章节数:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
num_chapters_label.grid(row=2, column=0, padx=5, pady=5, sticky="e")
|
||||
num_chapters_label.grid(row=row_for_chapter_and_word, column=0, padx=5, pady=5, sticky="e")
|
||||
|
||||
ch_word_frame = ctk.CTkFrame(self.params_frame)
|
||||
ch_word_frame.grid(row=row_for_chapter_and_word, column=1, padx=5, pady=5, sticky="ew")
|
||||
ch_word_frame.columnconfigure(0, weight=0)
|
||||
ch_word_frame.columnconfigure(1, weight=0)
|
||||
ch_word_frame.columnconfigure(2, weight=0)
|
||||
ch_word_frame.columnconfigure(3, weight=1)
|
||||
|
||||
num_chapters_entry = ctk.CTkEntry(
|
||||
self.params_frame,
|
||||
ch_word_frame,
|
||||
textvariable=self.num_chapters_var,
|
||||
width=80,
|
||||
width=60,
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
num_chapters_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w")
|
||||
num_chapters_entry.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
|
||||
# 每章字数
|
||||
word_number_label = ctk.CTkLabel(
|
||||
self.params_frame,
|
||||
ch_word_frame,
|
||||
text="每章字数:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
word_number_label.grid(row=3, column=0, padx=5, pady=5, sticky="e")
|
||||
word_number_label.grid(row=0, column=1, padx=(15, 5), pady=5, sticky="e")
|
||||
|
||||
word_number_entry = ctk.CTkEntry(
|
||||
self.params_frame,
|
||||
ch_word_frame,
|
||||
textvariable=self.word_number_var,
|
||||
width=80,
|
||||
width=60,
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
word_number_entry.grid(row=3, column=1, padx=5, pady=5, sticky="w")
|
||||
word_number_entry.grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
||||
|
||||
# 保存路径
|
||||
filepath_label = ctk.CTkLabel(
|
||||
@@ -572,10 +612,10 @@ class NovelGeneratorGUI:
|
||||
text="保存路径:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
filepath_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
|
||||
filepath_label.grid(row=3, column=0, padx=5, pady=5, sticky="e")
|
||||
|
||||
self.filepath_frame = ctk.CTkFrame(self.params_frame)
|
||||
self.filepath_frame.grid(row=4, column=1, padx=5, pady=5, sticky="nsew")
|
||||
self.filepath_frame.grid(row=3, column=1, padx=5, pady=5, sticky="nsew")
|
||||
self.filepath_frame.columnconfigure(0, weight=1)
|
||||
|
||||
filepath_entry = ctk.CTkEntry(
|
||||
@@ -599,14 +639,14 @@ class NovelGeneratorGUI:
|
||||
text="章节号:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
chapter_num_label.grid(row=5, column=0, padx=5, pady=5, sticky="e")
|
||||
chapter_num_label.grid(row=4, column=0, padx=5, pady=5, sticky="e")
|
||||
chapter_num_entry = ctk.CTkEntry(
|
||||
self.params_frame,
|
||||
textvariable=self.chapter_num_var,
|
||||
width=80,
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
chapter_num_entry.grid(row=5, column=1, padx=5, pady=5, sticky="w")
|
||||
chapter_num_entry.grid(row=4, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
# 用户指导
|
||||
guide_label = ctk.CTkLabel(
|
||||
@@ -614,7 +654,7 @@ class NovelGeneratorGUI:
|
||||
text="本章指导:",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
guide_label.grid(row=6, column=0, padx=5, pady=5, sticky="ne")
|
||||
guide_label.grid(row=5, column=0, padx=5, pady=5, sticky="ne")
|
||||
self.user_guide_text = ctk.CTkTextbox(
|
||||
self.params_frame,
|
||||
width=200,
|
||||
@@ -622,7 +662,7 @@ class NovelGeneratorGUI:
|
||||
wrap="word",
|
||||
font=("Microsoft YaHei", 12)
|
||||
)
|
||||
self.user_guide_text.grid(row=6, column=1, padx=5, pady=5, sticky="nsew")
|
||||
self.user_guide_text.grid(row=5, column=1, padx=5, pady=5, sticky="nsew")
|
||||
|
||||
# ------------------ 其他Tab的构建 ------------------
|
||||
def build_setting_tab(self):
|
||||
@@ -890,16 +930,20 @@ class NovelGeneratorGUI:
|
||||
self.embedding_interface_format_var.set(cfg.get("embedding_interface_format", "OpenAI"))
|
||||
self.embedding_url_var.set(cfg.get("embedding_url", ""))
|
||||
self.embedding_model_name_var.set(cfg.get("embedding_model_name", ""))
|
||||
# ### CHANGED:用字符串形式设值
|
||||
self.embedding_retrieval_k_var.set(str(cfg.get("embedding_retrieval_k", 4)))
|
||||
|
||||
# Novel
|
||||
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))
|
||||
|
||||
# ### CHANGED:用字符串形式设值
|
||||
self.num_chapters_var.set(str(cfg.get("num_chapters", 10)))
|
||||
self.word_number_var.set(str(cfg.get("word_number", 3000)))
|
||||
self.filepath_var.set(cfg.get("filepath", ""))
|
||||
|
||||
# 主题
|
||||
topic_value = cfg.get("topic", "")
|
||||
self.topic_text.delete("0.0", "end")
|
||||
self.topic_text.insert("0.0", cfg.get("topic", ""))
|
||||
self.topic_text.insert("0.0", topic_value)
|
||||
|
||||
self.log("已加载配置。")
|
||||
else:
|
||||
@@ -919,12 +963,13 @@ class NovelGeneratorGUI:
|
||||
"embedding_interface_format": self.embedding_interface_format_var.get(),
|
||||
"embedding_url": self.embedding_url_var.get(),
|
||||
"embedding_model_name": self.embedding_model_name_var.get(),
|
||||
"embedding_retrieval_k": self.safe_get_int(self.embedding_retrieval_k_var, 4),
|
||||
|
||||
# Novel
|
||||
"topic": self.topic_text.get("0.0", "end").strip(),
|
||||
"genre": self.genre_var.get(),
|
||||
"num_chapters": self.num_chapters_var.get(),
|
||||
"word_number": self.word_number_var.get(),
|
||||
"num_chapters": self.safe_get_int(self.num_chapters_var, 10),
|
||||
"word_number": self.safe_get_int(self.word_number_var, 3000),
|
||||
"filepath": self.filepath_var.get()
|
||||
}
|
||||
if save_config(config_data, self.config_file):
|
||||
@@ -954,6 +999,11 @@ class NovelGeneratorGUI:
|
||||
def enable_button_safe(self, btn):
|
||||
self.master.after(0, lambda: btn.configure(state="normal"))
|
||||
|
||||
def handle_exception(self, context: str):
|
||||
full_message = f"{context}\n{traceback.format_exc()}"
|
||||
logging.error(full_message)
|
||||
self.safe_log(full_message)
|
||||
|
||||
# ------------------ 分步操作:生成设定、目录、章节草稿、定稿 ------------------
|
||||
def generate_novel_setting_ui(self):
|
||||
"""Step1. 生成小说设定(Novel_setting.txt)"""
|
||||
@@ -968,10 +1018,11 @@ class NovelGeneratorGUI:
|
||||
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("0.0", "end").strip()
|
||||
genre = self.genre_var.get().strip()
|
||||
num_chapters = self.num_chapters_var.get()
|
||||
word_number = self.word_number_var.get()
|
||||
num_chapters = self.safe_get_int(self.num_chapters_var, 10)
|
||||
word_number = self.safe_get_int(self.word_number_var, 3000)
|
||||
temperature = self.temperature_var.get()
|
||||
|
||||
self.safe_log("开始生成小说设定...")
|
||||
@@ -1007,7 +1058,7 @@ class NovelGeneratorGUI:
|
||||
api_key = self.api_key_var.get().strip()
|
||||
base_url = self.base_url_var.get().strip()
|
||||
model_name = self.model_name_var.get().strip()
|
||||
num_chapters = self.num_chapters_var.get()
|
||||
num_chapters = self.safe_get_int(self.num_chapters_var, 10)
|
||||
temperature = self.temperature_var.get()
|
||||
|
||||
self.safe_log("开始生成小说目录...")
|
||||
@@ -1041,6 +1092,7 @@ class NovelGeneratorGUI:
|
||||
base_url = self.base_url_var.get().strip()
|
||||
model_name = self.model_name_var.get().strip()
|
||||
temperature = self.temperature_var.get()
|
||||
|
||||
novel_settings_file = os.path.join(filepath, "Novel_setting.txt")
|
||||
novel_settings = read_file(novel_settings_file)
|
||||
if not novel_settings.strip():
|
||||
@@ -1054,8 +1106,8 @@ class NovelGeneratorGUI:
|
||||
novel_directory_file = os.path.join(filepath, "Novel_directory.txt")
|
||||
novel_directory = read_file(novel_directory_file)
|
||||
|
||||
chap_num = self.chapter_num_var.get()
|
||||
word_number = self.word_number_var.get()
|
||||
chap_num = self.safe_get_int(self.chapter_num_var, 1)
|
||||
word_number = self.safe_get_int(self.word_number_var, 3000)
|
||||
user_guidance = self.user_guide_text.get("0.0", "end").strip()
|
||||
|
||||
# 获取最近3章文本
|
||||
@@ -1087,10 +1139,12 @@ class NovelGeneratorGUI:
|
||||
novel_novel_directory=novel_directory,
|
||||
filepath=filepath,
|
||||
|
||||
# 传入 Embedding 的专用配置
|
||||
# Embedding 配置
|
||||
interface_format=self.embedding_interface_format_var.get().strip(),
|
||||
embedding_model_name=self.embedding_model_name_var.get().strip(),
|
||||
embedding_base_url=self.embedding_url_var.get().strip(),
|
||||
# 新增:检索 K 值
|
||||
embedding_retrieval_k=self.safe_get_int(self.embedding_retrieval_k_var, 4)
|
||||
)
|
||||
if draft_text:
|
||||
self.safe_log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。")
|
||||
@@ -1124,12 +1178,14 @@ class NovelGeneratorGUI:
|
||||
base_url = self.base_url_var.get().strip()
|
||||
model_name = self.model_name_var.get().strip()
|
||||
temperature = self.temperature_var.get()
|
||||
|
||||
interface_format = self.embedding_interface_format_var.get().strip()
|
||||
embedding_model_name = self.embedding_model_name_var.get().strip()
|
||||
embedding_base_url = self.embedding_url_var.get().strip()
|
||||
embedding_api_key = self.embedding_api_key_var.get().strip()
|
||||
|
||||
chap_num = self.chapter_num_var.get()
|
||||
word_number = self.word_number_var.get()
|
||||
chap_num = self.safe_get_int(self.chapter_num_var, 1)
|
||||
word_number = self.safe_get_int(self.word_number_var, 3000)
|
||||
|
||||
self.safe_log(f"开始定稿第{chap_num}章...")
|
||||
finalize_chapter(
|
||||
@@ -1143,7 +1199,7 @@ class NovelGeneratorGUI:
|
||||
temperature=temperature,
|
||||
filepath=filepath,
|
||||
embedding_base_url=embedding_base_url,
|
||||
embedding_api_key=self.embedding_api_key_var.get().strip()
|
||||
embedding_api_key=embedding_api_key
|
||||
)
|
||||
self.safe_log(f"✅ 第{chap_num}章定稿完成(已更新全局摘要、角色状态、剧情要点、向量库)。")
|
||||
|
||||
@@ -1185,7 +1241,7 @@ class NovelGeneratorGUI:
|
||||
global_summary = read_file(global_summary_file)
|
||||
plot_arcs = read_file(plot_arcs_file)
|
||||
|
||||
chap_num = self.chapter_num_var.get()
|
||||
chap_num = self.safe_get_int(self.chapter_num_var, 1)
|
||||
chap_file = os.path.join(filepath, "chapters", f"chapter_{chap_num}.txt")
|
||||
chapter_text = read_file(chap_file)
|
||||
|
||||
@@ -1233,7 +1289,7 @@ class NovelGeneratorGUI:
|
||||
embedding_model_name=self.embedding_model_name_var.get().strip(),
|
||||
file_path=selected_file,
|
||||
embedding_base_url=self.embedding_url_var.get().strip(),
|
||||
filepath=self.filepath_var.get().strip() # 用于本地化 vectorstore
|
||||
filepath=self.filepath_var.get().strip()
|
||||
)
|
||||
self.safe_log("✅ 知识库文件导入完成。")
|
||||
except Exception:
|
||||
@@ -1253,8 +1309,10 @@ class NovelGeneratorGUI:
|
||||
if first_confirm:
|
||||
second_confirm = messagebox.askyesno("二次确认", "你确定真的要删除所有向量数据吗?此操作不可恢复!")
|
||||
if second_confirm:
|
||||
clear_vector_store(filepath)
|
||||
if clear_vector_store(filepath):
|
||||
self.log("已清空向量库。")
|
||||
else:
|
||||
self.log(f"未能清空向量库。请关闭程序后前往{filepath}文件夹手动删除vectorstore目录。")
|
||||
|
||||
def show_plot_arcs_ui(self):
|
||||
filepath = self.filepath_var.get().strip()
|
||||
|
||||
Reference in New Issue
Block a user