优化提示词(可能优化了吧),改进UI以及支持对embedding模型的独立配置

This commit is contained in:
YILING0013
2025-02-04 00:10:19 +08:00
parent dd871741ed
commit f27c2c8087
4 changed files with 383 additions and 253 deletions
+1
View File
@@ -6,4 +6,5 @@
/__pycache__
/markdown
/vectorstore
/example
config.json
+96 -144
View File
@@ -43,21 +43,16 @@ from embedding_ollama import OllamaEmbeddings
from chapter_directory_parser import get_chapter_info_from_directory
# ============ 日志配置 ============
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
# ============ 通用调用函数 ============
# ============ 帮助函数 ============
def remove_think_tags(text: str) -> str:
"""
移除 <think>...</think> 包裹的内容
"""
"""移除 <think>...</think> 包裹的内容"""
return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str:
"""
通用封装:调用模型并移除 <think>...</think> 文本,记录日志后返回
"""
"""通用封装:调用模型并移除 <think>...</think> 文本,记录日志后返回"""
response = model.invoke(prompt)
if not response:
logging.warning("No response from model.")
@@ -67,70 +62,60 @@ def invoke_with_cleaning(model: ChatOpenAI, prompt: str) -> str:
return cleaned_text.strip()
def debug_log(prompt: str, response_content: str):
"""
打印prompt和response的辅助函数
"""
logging.info(f"\n[Prompt >>>] {prompt}\n")
logging.info(f"[Response >>>] {response_content}\n")
# ============ 判断接口格式相关 ============
def is_using_ollama_api(interface_format: str, base_url: str) -> bool:
"""
当 interface_format == "Ollama" 时返回 True
"""
return interface_format.lower() == "ollama"
def is_using_ml_studio_api(interface_format: str, base_url: str) -> bool:
"""
如果用户在下拉里选择了 ML Studio
"""
return interface_format.lower() == "ml studio"
# ============ 帮助函数:自动检查 & 补充 /v1 ============
import re
def ensure_openai_base_url_has_v1(url: str) -> str:
"""
如果用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'
如果已经包含 '/v1',则不再重复追加。
用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'
"""
import re
url = url.strip()
if not url:
return url
# 若末尾没有 /v\d+,但也没出现 /v1,才补上
if not re.search(r'/v\d+$', url):
if '/v1' not in url:
url = url.rstrip('/') + '/v1'
return url
def is_using_ollama_api(interface_format: str) -> bool:
return interface_format.lower() == "ollama"
def is_using_ml_studio_api(interface_format: str) -> bool:
return interface_format.lower() == "ml studio"
# ============ 获取 vectorstore 路径 ============
def get_vectorstore_dir(filepath: str) -> str:
"""
返回存储向量库的本地路径:
在用户指定的 `filepath` 下创建/使用 'vectorstore' 文件夹。
"""
return os.path.join(filepath, "vectorstore")
# ============ 创建 Embeddings 对象 ============
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"/"ML Studio" => OpenAIEmbeddings(...)
这里统一把 base_url/embed_url 处理为含 /v1。
根据 embedding_interface_format,选择 Ollama 或 OpenAIEmbeddings 等不同后端
base_url: 在 OpenAI 或 ML Studio 时,需要自动补'/v1'Ollama 则通常是 http://localhost:11434/v1
"""
if is_using_ollama_api(interface_format, embed_url):
fixed_url = embed_url.rstrip("/")
if is_using_ollama_api(interface_format):
fixed_url = base_url.rstrip("/")
return OllamaEmbeddings(
model_name=embedding_model_name,
base_url=fixed_url
)
else:
# OpenAI 或 ML Studio 统一用 OpenAIEmbeddings
# 并设置 model=embedding_model_name
# base_url/embed_url 若不含 /v1,需要自动补上
fixed_url = ensure_openai_base_url_has_v1(embed_url if embed_url else base_url)
# 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,
openai_api_base=fixed_url,
@@ -138,20 +123,17 @@ def create_embeddings_object(
)
# ============ 向量库相关 ============
VECTOR_STORE_DIR = os.path.join(os.getcwd(), "vectorstore")
if not os.path.exists(VECTOR_STORE_DIR):
os.makedirs(VECTOR_STORE_DIR)
def clear_vector_store():
# ============ 向量库相关操作 ============
def clear_vector_store(filepath: str):
"""
清空本地向量库(删除 vectorstore 文件夹内的所有内容)
清空本地向量库(删除 filepath/vectorstore 文件夹内的所有内容)
"""
if os.path.exists(VECTOR_STORE_DIR):
store_dir = get_vectorstore_dir(filepath)
if os.path.exists(store_dir):
import shutil
try:
for filename in os.listdir(VECTOR_STORE_DIR):
file_path = os.path.join(VECTOR_STORE_DIR, filename)
for filename in os.listdir(store_dir):
file_path = os.path.join(store_dir, filename)
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
@@ -169,25 +151,25 @@ def init_vector_store(
interface_format: str,
embedding_model_name: str,
texts: List[str],
embedding_base_url: str = ""
filepath: str
) -> Chroma:
"""
初始化并返回一个Chroma向量库,将传入的文本进行嵌入并保存到本地目录
在 filepath 下创建/加载一个 Chroma 向量库并插入 texts
"""
embed_url = embedding_base_url if embedding_base_url else base_url
store_dir = get_vectorstore_dir(filepath)
os.makedirs(store_dir, exist_ok=True)
embeddings = create_embeddings_object(
api_key=api_key,
base_url=base_url,
embed_url=embed_url,
interface_format=interface_format,
embedding_model_name=embedding_model_name
)
documents = [Document(page_content=str(t)) for t in texts] # 确保是字符串
documents = [Document(page_content=str(t)) for t in texts]
vectorstore = Chroma.from_documents(
documents,
embedding=embeddings,
persist_directory=VECTOR_STORE_DIR,
client_settings=Settings(anonymized_telemetry=False)
persist_directory=store_dir
)
vectorstore.persist()
return vectorstore
@@ -198,24 +180,26 @@ def load_vector_store(
base_url: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str = ""
filepath: str
) -> Optional[Chroma]:
"""
读取已存在的向量库。若不存在则返回 None。
读取已存在的 Chroma 向量库。若不存在则返回 None。
"""
if not os.path.exists(VECTOR_STORE_DIR):
store_dir = get_vectorstore_dir(filepath)
if not os.path.exists(store_dir):
logging.info("Vector store not found. Will return None.")
return None
embed_url = embedding_base_url if embedding_base_url else base_url
embeddings = create_embeddings_object(
api_key=api_key,
base_url=base_url,
embed_url=embed_url,
interface_format=interface_format,
embedding_model_name=embedding_model_name
)
return Chroma(persist_directory=VECTOR_STORE_DIR, embedding_function=embeddings,client_settings=Settings(anonymized_telemetry=False))
return Chroma(
persist_directory=store_dir,
embedding_function=embeddings
)
def update_vector_store(
@@ -224,19 +208,18 @@ def update_vector_store(
new_chapter: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str = ""
) -> None:
filepath: str
):
"""
将最新章节文本插入到向量库里,用于后续检索参考。若库不存在则初始化。
将最新章节文本插入到向量库。若库不存在则初始化。
"""
store = load_vector_store(
api_key=api_key,
base_url=base_url,
interface_format=interface_format,
embedding_model_name=embedding_model_name,
embedding_base_url=embedding_base_url
filepath=filepath
)
if not store:
logging.info("Vector store does not exist. Initializing a new one for new chapter...")
init_vector_store(
@@ -245,7 +228,7 @@ def update_vector_store(
interface_format=interface_format,
embedding_model_name=embedding_model_name,
texts=[new_chapter],
embedding_base_url=embedding_base_url
filepath=filepath
)
return
@@ -261,19 +244,18 @@ def get_relevant_context_from_vector_store(
query: str,
interface_format: str,
embedding_model_name: str,
embedding_base_url: str = "",
filepath: str,
k: int = 2
) -> str:
"""
从向量库中检索与 query 最相关的 k 条文本,拼接后返回。
若向量库不存在或没有足够内容,则返回空字符串。
"""
store = load_vector_store(
api_key=api_key,
base_url=base_url,
interface_format=interface_format,
embedding_model_name=embedding_model_name,
embedding_base_url=embedding_base_url
filepath=filepath
)
if not store:
logging.info("No vector store found. Returning empty context.")
@@ -288,7 +270,7 @@ def get_relevant_context_from_vector_store(
return combined
# ============ 1. 独立:生成小说“设定” (Novel_setting.txt) ============
# ============ 1. 生成小说“设定” (Novel_setting.txt) ============
def Novel_setting_generate(
api_key: str,
base_url: str,
@@ -300,16 +282,12 @@ def Novel_setting_generate(
filepath: str,
temperature: float = 0.7
) -> None:
"""
分步生成 Novel_setting.txt (含世界观、角色信息、暗线等)
不包括目录。
"""
os.makedirs(filepath, exist_ok=True)
model = ChatOpenAI(
model=llm_model,
api_key=api_key,
base_url=ensure_openai_base_url_has_v1(base_url), # 确保带 /v1
base_url=ensure_openai_base_url_has_v1(base_url),
temperature=temperature
)
@@ -334,7 +312,7 @@ def Novel_setting_generate(
)
dark_lines = invoke_with_cleaning(model, prompt_dark)
# Step4: 最终整合为“小说设定”
# Step4: 最终整合
prompt_final = finalize_setting_prompt.format(
novel_setting_base=base_setting,
character_setting=character_setting,
@@ -342,17 +320,15 @@ def Novel_setting_generate(
)
final_novel_setting = invoke_with_cleaning(model, prompt_final)
# 写入 Novel_setting.txt
filename_set = os.path.join(filepath, "Novel_setting.txt")
clear_file_content(filename_set)
final_novel_setting_cleaned = final_novel_setting.replace('#', '').replace('*', '')
save_string_to_txt(final_novel_setting_cleaned, filename_set)
logging.info("Novel_setting.txt has been generated successfully.")
# ============ 2. 独立:基于已有设定,生成小说目录 (Novel_directory.txt) ============
# ============ 2. 生成小说目录 (Novel_directory.txt) ============
def Novel_directory_generate(
api_key: str,
base_url: str,
@@ -361,10 +337,6 @@ def Novel_directory_generate(
filepath: str,
temperature: float = 0.7
) -> None:
"""
基于先前已经生成并保存的 Novel_setting.txt,来生成 Novel_directory.txt
"""
# 读取已有的小说设定
filename_set = os.path.join(filepath, "Novel_setting.txt")
final_novel_setting = read_file(filename_set).strip()
if not final_novel_setting:
@@ -378,7 +350,6 @@ def Novel_directory_generate(
temperature=temperature
)
# 生成目录
prompt_dir = novel_directory_prompt.format(
final_novel_setting=final_novel_setting,
number_of_chapters=number_of_chapters
@@ -388,7 +359,6 @@ def Novel_directory_generate(
logging.warning("Novel_directory生成结果为空。")
return
# 写入 Novel_directory.txt
filename_dir = os.path.join(filepath, "Novel_directory.txt")
clear_file_content(filename_dir)
@@ -400,10 +370,6 @@ def Novel_directory_generate(
# ============ 获取最近 N 章内容,生成短期摘要 ============
def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int = 3) -> List[str]:
"""
从指定文件夹中,读取最近 n 章的内容(如果存在),并按从旧到新的顺序返回文本列表。
不包含当前章,只拿之前的 n 章。
"""
texts = []
start_chap = max(1, current_chapter_num - n)
for c in range(start_chap, current_chapter_num):
@@ -413,7 +379,6 @@ def get_last_n_chapters_text(chapters_dir: str, current_chapter_num: int, n: int
if text:
texts.append(text)
if len(texts) < n:
# 如果前面章节不足 n 章,用空字符串填充
texts = [''] * (n - len(texts)) + texts
return texts
@@ -424,9 +389,6 @@ def summarize_recent_chapters(
temperature: float,
chapters_text_list: List[str]
) -> str:
"""
将最近几章文本拼接,通过模型生成相对简要的“短期内容摘要”。
"""
if not chapters_text_list:
return ""
if all(not txt.strip() for txt in chapters_text_list):
@@ -451,7 +413,7 @@ def summarize_recent_chapters(
return summary_text
# ============ 剧情要点/未解决冲突 ============
# ============ 剧情要点/冲突 ============
PLOT_ARCS_PROMPT = """\
下面是新生成的章节内容:
{chapter_text}
@@ -508,10 +470,7 @@ def generate_chapter_draft(
embedding_model_name: str,
embedding_base_url: str
) -> str:
"""
生成当前章节的草稿,不更新全局摘要/角色状态/向量库。
"""
# 1) 从目录中获取本章标题、简介
# 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"]
@@ -528,11 +487,11 @@ def generate_chapter_draft(
for q in queries:
partial_context = get_relevant_context_from_vector_store(
api_key=api_key,
base_url=base_url,
base_url=embedding_base_url if embedding_base_url else base_url,
query=q,
interface_format=interface_format,
embedding_model_name=embedding_model_name,
embedding_base_url=embedding_base_url,
filepath=filepath,
k=2
)
if partial_context.strip():
@@ -540,7 +499,7 @@ def generate_chapter_draft(
if not relevant_context:
relevant_context = "暂无相关内容。"
# 创建 ChatOpenAI,用于大纲和写作
# 3) 生成本章大纲
model = ChatOpenAI(
model=model_name,
api_key=api_key,
@@ -548,7 +507,6 @@ def generate_chapter_draft(
temperature=temperature
)
# 3) 生成本章大纲
outline_prompt_text = chapter_outline_prompt.format(
novel_setting=novel_settings,
character_state=character_state + "\n\n【检索到的上下文】\n" + relevant_context,
@@ -603,16 +561,10 @@ def finalize_chapter(
embedding_model_name: str,
model_name: str,
temperature: float,
filepath: str
filepath: str,
embedding_base_url: str,
embedding_api_key: str
):
"""
对当前章节进行定稿:
1. 读取草稿文本
2. 若字数太短则再次扩写
3. 更新全局摘要、角色状态
4. 更新剧情要点
5. 更新向量库
"""
chapters_dir = os.path.join(filepath, "chapters")
chapter_file = os.path.join(chapters_dir, f"chapter_{novel_number}.txt")
chapter_text = read_file(chapter_file).strip()
@@ -628,7 +580,7 @@ def finalize_chapter(
old_global_summary = read_file(global_summary_file)
old_plot_arcs = read_file(plot_arcs_file)
# 篇幅过短,二次扩写
# 篇幅不足,二次扩写
if len(chapter_text) < 0.8 * word_number:
logging.info("Chapter text is shorter than 80% of desired length. Enriching...")
chapter_text = enrich_chapter_text(
@@ -649,7 +601,6 @@ 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,
@@ -689,13 +640,14 @@ def finalize_chapter(
clear_file_content(plot_arcs_file)
save_string_to_txt(new_plot_arcs, plot_arcs_file)
# 更新向量库
# 更新向量库(此时用 embedding_api_key/embedding_base_url
update_vector_store(
api_key=api_key,
base_url=base_url,
api_key=embedding_api_key,
base_url=embedding_base_url if embedding_base_url else base_url,
new_chapter=chapter_text,
interface_format=interface_format,
embedding_model_name=embedding_model_name
embedding_model_name=embedding_model_name,
filepath=filepath
)
logging.info(f"Chapter {novel_number} has been finalized.")
@@ -709,9 +661,6 @@ def enrich_chapter_text(
model_name: str,
temperature: float
) -> str:
"""
当章节篇幅不足时,调用此函数对章节文本进行二次扩写。
"""
model = ChatOpenAI(
model=model_name,
api_key=api_key,
@@ -726,18 +675,16 @@ def enrich_chapter_text(
return enriched_text if enriched_text else 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 = ""
) -> None:
"""
将用户选定的文本文件导入到向量库,以便在写作时检索。
"""
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}")
@@ -752,22 +699,28 @@ def import_knowledge_file(
paragraphs = advanced_split_content(content)
store = load_vector_store(api_key, base_url, interface_format, embedding_model_name, embedding_base_url)
# 若向量库不存在则初始化,否则追加
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,
base_url,
interface_format,
embedding_model_name,
paragraphs,
embedding_base_url
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
)
return
docs = [Document(page_content=str(p)) for p in paragraphs]
store.add_documents(docs)
store.persist()
else:
docs = [Document(page_content=str(p)) for p in paragraphs]
store.add_documents(docs)
store.persist()
logging.info("知识库文件已成功导入至向量库。")
@@ -776,7 +729,6 @@ def advanced_split_content(content: str,
max_length: int = 500) -> List[str]:
"""
将文本先按句子切分,然后根据语义相似度进行合并,最后按 max_length 二次切分。
可根据需要微调此逻辑。
"""
sentences = nltk.sent_tokenize(content)
if not sentences:
+192 -82
View File
@@ -1,139 +1,249 @@
# prompt_definitions.py
# -*- coding: utf-8 -*-
"""
集中存放所有提示词(Prompt),便于统一管理和修改
小说创作辅助系统的提示词(Prompt)集合
这些提示词被设计用于引导AI生成连贯、丰富的小说内容。
"""
# =============== 提示词:设定 & 目录 ===================
# =============== 基础设定与规划提示词 ===================
set_prompt = """\
请根据主题:{topic}、类型:{genre}、章数:{number_of_chapters}、每章字数:{word_number}来完善小说整体设定
需要包含以下信息
1. 小说名称、总字数走向(大致范围即可)。
2. 小说类型与基调(如:都市、穿越、战争等类型,以及轻松、爆笑、暗黑等基调)。
3. 写作风格(正式 / 轻松;细腻 / 简洁;抒情 / 客观;叙事视角等)。
4. 整体世界观(时间背景、地理环境、社会结构、科技或魔法水平、重要历史事件等)。
5. 核心内容梗概(可以使用常见叙事结构,如三幕结构、英雄之旅等)。
6. 初步的情节安排设想(主线、副线、交织等关键思路)。
7. 初步的人物关系与主要角色设定(角色定位、主要冲突或关系)。
8. 结尾可能的方向(圆满、悲剧、开放式等)。
基于主题{topic}、类型{genre}」,将创作一部{number_of_chapters}、每章{word_number}字的小说
请详细规划以下要素
请按照上述要点详细输出,但不用标数字。要清晰、有逻辑、有条理。
【基本信息】
• 建议书名(可含副标题)
• 预估总字数区间
• 主要类型定位(如:奇幻/都市/科幻等)
• 基调与氛围(如:史诗/轻松/黑暗等)
【创作风格】
• 叙事视角选择及理由
• 语言风格特点
• 节奏把控思路
【世界观构筑】
• 时空背景设定
• 世界运行规则(社会/科技/魔法体系等)
• 重大历史事件或背景
【核心故事】
• 主线故事框架
• 重要支线规划
• 核心冲突设置
• 结局走向构思
请具体阐述以上各点,确保前后呼应、逻辑自洽。
"""
character_prompt = """\
基于已生成的小说整体设定:
基于已确立的小说设定:
{novel_setting}
请你完善以下内容,帮助我们更好地维持人物形象和成长轨迹:
1. 列出核心角色(至少3个),并对每个角色进行详细性格特征描述。
2. 强调每个角色的潜在内心冲突、目标与动机。
3. 为每个角色添加至少一个“暗线”或隐藏秘密,以及在故事进行中可能如何被揭示。
4. 指出主要角色之间的关键关系和冲突点,为后续情节埋下伏笔。
请完善以下角色体系:
【核心角色塑造】(至少3个)
• 角色基本信息(名字/年龄/身份等)
• 外在特征与性格特点
• 核心价值观与行为模式
• 个人成长轨迹设想
• 独特能力或专长
【人物关系网络】
• 角色间的重要关联
• 潜在矛盾点
• 关系发展预期
【隐藏维度】
• 每个角色的个人秘密
• 待揭示的过往经历
• 性格中的矛盾面
重点说明这些特质如何推动故事发展,为情节转折提供基础。
"""
dark_lines_prompt = """\
在当前设定中已出现以下角色与背景:
根据已设定的角色与背景:
{character_info}
请帮助我们构思若干暗线、伏笔或隐藏冲突,以便在后续章节中逐渐揭示并影响故事走向。要求:
1. 每个暗线至少说明其最初的表现、发展走向,以及揭示或爆发的条件。
2. 这些暗线可以与角色背景、世界观、关键事件等有关。
3. 需注意保留悬念,与已知设定不冲突。
4. 在后续创作中可多次提及这些暗线,并在中后期通过角色行为或剧情变化逐步揭示。
请构建以下隐藏线索体系:
【关键暗线设计】
• 暗线起源与表现形式
• 发展脉络规划
• 揭示时机与方式
• 对整体故事的影响
【伏笔布置】
• 早期暗示点设置
• 中期发展线索
• 后期爆发契机
【隐藏冲突】
• 角色间潜在矛盾
• 阵营对立根源
• 价值观冲突点
确保这些暗线自然融入故事,避免生硬设置。建议提供具体场景建议。
"""
finalize_setting_prompt = """\
基于以下信息,整合并输出最终的《小说设定》
1. 之前的“整体设定”:
整合以下创作准备内容
【现有设定】
{novel_setting_base}
2. 扩充的“角色设定”:
【角色系统】
{character_setting}
3. 暗线与伏笔构思:
【暗线规划】
{dark_lines}
要求
1. 结构清晰,将以上内容融合为一个完整的设定说明。
2. 着重强调角色与暗线的衔接、世界观与角色动机的结合,方便后续写作保持前后一致。
3. 语言通畅,不使用Markdown格式,直接输出文本内容。
将以上要素整合为完整的创作蓝图
1. 总体框架
• 核心故事脉络
• 世界观体系
• 主题表达方式
2. 人物系统
• 角色群像
• 关系网络
• 成长轨迹
3. 情节编排
• 主线发展
• 支线设计
• 暗线铺陈
4. 创作建议
• 重点场景构思
• 节奏控制要点
• 细节描写建议
请以流畅的叙述文本呈现,突出要素间的有机联系。
"""
novel_directory_prompt = """\
根据以下最终《小说设定
依据最终设定:
{final_novel_setting}
并按照下面的小说目录模板生成 {number_of_chapters} 章的目录,同时确保目录符合小说设定中的叙事结构、角色发展及暗线伏笔。
目录模板(示例):
第1章 < text >
第2章 < text >
...
{number_of_chapters}< text >
严格按照上述格式输出每一章的名称,最好在重要情节标题后增加提示性简述,
若要加更详细的简述,用“ - ”分隔,举例如“第n章 :< text > - 主要角色冲突爆发,角色A发生xx意外”。
请直接输出,不要使用Markdown语法。
规划{number_of_chapters}章的详细目录。每章格式:
第N章:章节名 - 核心内容提示
要求:
1. 章节名需简明扼要,富有吸引力
2. 核心内容提示需点明关键信息,为创作提供指引
3. 整体节奏要富有张力,符合三幕结构
4. 适当预留转折与高潮
示例:
第n章:黎明前的暗影 - 主角遭遇神秘袭击,接触核心谜题
...
"""
# =============== 提示词:章节+角色状态流程 ===================
# =============== 章节创作辅助提示词 ===================
summary_prompt = """\
这是新生成的章节文本:
新增章节内容:
{chapter_text}
这是当前全局摘要(可能为空):
当前全局摘要
{global_summary}
在不超过3000字的前提下,基于当前全局摘要和本章新增剧情,更新全局摘要。
保留原有重要信息,并融入本章的新内容。
不要透露结局,不要过度展开未来剧情。
更新全局摘要(控制在3000字以内):
【已发生】
• 关键事件梳理
• 人物关系变化
• 重要线索进展
【正在进行】
• 当前危机/冲突
• 角色动态
• 悬而未决的问题
确保摘要重点突出,为后续创作提供清晰参考。
不展开未来发展,保持故事悬念。
"""
update_character_state_prompt = """\
这是新生成的章节文本:
本章内容:
{chapter_text}
这是当前角色状态文档(可能为空):
现有角色状态:
{old_state}
请更新角色状态,包括
1. 角色持有的物品或能力变化。
2. 角色间关系、冲突或合作的新动向。
3. 正在发生的重要事件列表,有无进展或新事件产生。
4. 任意新增角色或出场人物等。
5. 请保证结构完整,能在后续章节继续引用。
请更新角色状态档案
使用简洁、易读的方式描述,可用条目或段落表示。保持与旧文档风格一致。
【角色发展】
• 能力/状态变化
• 重要物品获得/失去
• 性格/观念的微妙改变
【人际关系】
• 新建立的联系
• 关系的强化或弱化
• 潜在矛盾点
【事件参与】
• 正在进行的事件
• 个人目标进展
• 新接触的任务
请保持简洁明了,便于后续参考。
"""
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. 在结尾留下什么悬念或转折?(与本章标题或简述形成呼应或对比)
设计本章节详细大纲:
请直接用 1、2、3、4 分点说明大纲要点即可。
【核心设计】
• 本章主要冲突/事件
• 与章节主题的呼应方式
• 情节推进目标
【人物安排】
• 出场角色及其状态
• 个人目标与动机
• 互动关系设计
【暗线发展】
• 已有伏笔的推进
• 新增悬念的埋设
• 线索的暗示方式
【结构布局】
• 章节节奏规划
• 高潮设计
• 结尾悬念构思
"""
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}
{chapter_title}
核心{chapter_brief}
请写出本章节的完整正文
1. 确保本章字数不少于 {word_number} 字。
2. 内容需与标题“{chapter_title}”相呼应,并尽量呼应简述中的核心要点。
3. 不要使用分节标题,直接整体输出正文。
4. 可以着重描写人物心理、环境氛围,以保证足够长度。
5. 在结尾部分保留一定悬念或剧情转折,为下一章做铺垫。
创作要求
1. 字数不少于{word_number}
2. 紧扣章节主题
3. 注重细节描写
4. 深入角色内心
5. 为下章预留引子
建议:
• 通过环境描写渲染氛围
• 展现人物细微情感变化
• 适当运用对话推进情节
• 保持节奏张弛有度
"""
+93 -26
View File
@@ -21,6 +21,7 @@ from novel_generator import (
)
from consistency_checker import check_consistency
def log_error(message: str):
"""
用于打印详细的错误信息和堆栈信息。
@@ -31,6 +32,7 @@ def log_error(message: str):
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
class NovelGeneratorGUI:
def __init__(self, master):
self.master = master
@@ -51,15 +53,20 @@ 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"))
self.temperature_var = ctk.DoubleVar(value=self.loaded_config.get("temperature", 0.7))
# Embedding 接口相关
self.embedding_interface_format_var = ctk.StringVar(value=self.loaded_config.get("embedding_interface_format", "OpenAI"))
self.embedding_api_key_var = ctk.StringVar(value=self.loaded_config.get("embedding_api_key", ""))
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.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))
@@ -298,13 +305,10 @@ class NovelGeneratorGUI:
def on_interface_format_changed(new_value):
if new_value == "Ollama":
self.base_url_var.set("http://localhost:11434/v1")
self.embedding_url_var.set("http://localhost:11434/api")
elif new_value == "ML Studio":
self.base_url_var.set("http://localhost:1234/v1")
self.embedding_url_var.set("http://localhost:1234/api")
elif new_value == "OpenAI":
self.base_url_var.set("https://api.openai.com/v1")
self.embedding_url_var.set("https://api.openai.com/v1")
for i in range(5):
self.ai_config_tab.grid_rowconfigure(i, weight=0)
@@ -314,7 +318,7 @@ class NovelGeneratorGUI:
api_key_label = ctk.CTkLabel(
self.ai_config_tab,
text="API Key:",
text="LLM API Key:",
font=("Microsoft YaHei", 12)
)
api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
@@ -327,7 +331,7 @@ class NovelGeneratorGUI:
base_url_label = ctk.CTkLabel(
self.ai_config_tab,
text="Base URL:",
text="LLM Base URL:",
font=("Microsoft YaHei", 12)
)
base_url_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
@@ -340,7 +344,7 @@ class NovelGeneratorGUI:
interface_label = ctk.CTkLabel(
self.ai_config_tab,
text="接口格式:",
text="LLM 接口格式:",
font=("Microsoft YaHei", 12)
)
interface_label.grid(row=2, column=0, padx=5, pady=5, sticky="e")
@@ -394,36 +398,73 @@ class NovelGeneratorGUI:
self.temp_value_label.grid(row=4, column=2, padx=1, pady=1, sticky="w")
def build_embeddings_config_tab(self):
for i in range(2):
def on_embedding_interface_changed(new_value):
if new_value == "Ollama":
self.embedding_url_var.set("http://localhost:11434/v1")
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):
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)
embedding_url_label = ctk.CTkLabel(
emb_api_key_label = ctk.CTkLabel(
self.embeddings_config_tab,
text="Embedding URL:",
text="Embedding API Key:",
font=("Microsoft YaHei", 12)
)
embedding_url_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
embedding_url_entry = ctk.CTkEntry(
emb_api_key_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
emb_api_key_entry = ctk.CTkEntry(
self.embeddings_config_tab,
textvariable=self.embedding_api_key_var,
font=("Microsoft YaHei", 12)
)
emb_api_key_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
emb_interface_label = ctk.CTkLabel(
self.embeddings_config_tab,
text="Embedding 接口格式:",
font=("Microsoft YaHei", 12)
)
emb_interface_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
emb_interface_options = ["OpenAI", "Ollama", "ML Studio"]
emb_interface_dropdown = ctk.CTkOptionMenu(
self.embeddings_config_tab,
values=emb_interface_options,
variable=self.embedding_interface_format_var,
command=on_embedding_interface_changed,
font=("Microsoft YaHei", 12)
)
emb_interface_dropdown.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
emb_url_label = ctk.CTkLabel(
self.embeddings_config_tab,
text="Embedding Base URL:",
font=("Microsoft YaHei", 12)
)
emb_url_label.grid(row=2, column=0, padx=5, pady=5, sticky="e")
emb_url_entry = ctk.CTkEntry(
self.embeddings_config_tab,
textvariable=self.embedding_url_var,
font=("Microsoft YaHei", 12)
)
embedding_url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
emb_url_entry.grid(row=2, column=1, padx=5, pady=5, sticky="nsew")
emb_model_name_label = ctk.CTkLabel(
self.embeddings_config_tab,
text="Embedding Model Name:",
font=("Microsoft YaHei", 12)
)
emb_model_name_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
emb_model_name_label.grid(row=3, column=0, padx=5, pady=5, sticky="e")
emb_model_name_entry = ctk.CTkEntry(
self.embeddings_config_tab,
textvariable=self.embedding_model_name_var,
font=("Microsoft YaHei", 12)
)
emb_model_name_entry.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
emb_model_name_entry.grid(row=3, column=1, padx=5, pady=5, sticky="nsew")
# ========== 保存/加载 配置按钮区域 ==========
def build_main_buttons_area(self):
@@ -837,13 +878,20 @@ class NovelGeneratorGUI:
def load_config_btn(self):
cfg = load_config(self.config_file)
if cfg:
# LLM
self.api_key_var.set(cfg.get("api_key", ""))
self.base_url_var.set(cfg.get("base_url", ""))
self.interface_format_var.set(cfg.get("interface_format", "OpenAI"))
self.model_name_var.set(cfg.get("model_name", ""))
self.temperature_var.set(cfg.get("temperature", 0.7))
# Embedding
self.embedding_api_key_var.set(cfg.get("embedding_api_key", ""))
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", ""))
self.temperature_var.set(cfg.get("temperature", 0.7))
# 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))
@@ -859,13 +907,20 @@ class NovelGeneratorGUI:
def save_config_btn(self):
config_data = {
# LLM
"api_key": self.api_key_var.get(),
"base_url": self.base_url_var.get(),
"interface_format": self.interface_format_var.get(),
"model_name": self.model_name_var.get(),
"temperature": self.temperature_var.get(),
# Embedding
"embedding_api_key": self.embedding_api_key_var.get(),
"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(),
"temperature": self.temperature_var.get(),
# Novel
"topic": self.topic_text.get("0.0", "end").strip(),
"genre": self.genre_var.get(),
"num_chapters": self.num_chapters_var.get(),
@@ -1031,9 +1086,11 @@ class NovelGeneratorGUI:
temperature=temperature,
novel_novel_directory=novel_directory,
filepath=filepath,
interface_format=self.interface_format_var.get().strip(),
# 传入 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()
embedding_base_url=self.embedding_url_var.get().strip(),
)
if draft_text:
self.safe_log(f"✅ 第{chap_num}章草稿生成完成。请在左侧查看或编辑。")
@@ -1067,8 +1124,9 @@ 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.interface_format_var.get().strip()
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()
chap_num = self.chapter_num_var.get()
word_number = self.word_number_var.get()
@@ -1083,7 +1141,9 @@ class NovelGeneratorGUI:
embedding_model_name=embedding_model_name,
model_name=model_name,
temperature=temperature,
filepath=filepath
filepath=filepath,
embedding_base_url=embedding_base_url,
embedding_api_key=self.embedding_api_key_var.get().strip()
)
self.safe_log(f"✅ 第{chap_num}章定稿完成(已更新全局摘要、角色状态、剧情要点、向量库)。")
@@ -1167,12 +1227,13 @@ class NovelGeneratorGUI:
try:
self.safe_log(f"开始导入知识库文件: {selected_file}")
import_knowledge_file(
api_key=self.api_key_var.get().strip(),
base_url=self.base_url_var.get().strip(),
interface_format=self.interface_format_var.get().strip(),
api_key=self.embedding_api_key_var.get().strip(),
base_url=self.embedding_url_var.get().strip(),
interface_format=self.embedding_interface_format_var.get().strip(),
embedding_model_name=self.embedding_model_name_var.get().strip(),
file_path=selected_file,
embedding_base_url=self.embedding_url_var.get().strip()
embedding_base_url=self.embedding_url_var.get().strip(),
filepath=self.filepath_var.get().strip() # 新增,用于本地化 vectorstore
)
self.safe_log("✅ 知识库文件导入完成。")
except Exception:
@@ -1183,11 +1244,16 @@ class NovelGeneratorGUI:
threading.Thread(target=task, daemon=True).start()
def clear_vectorstore_handler(self):
filepath = self.filepath_var.get().strip()
if not filepath:
messagebox.showwarning("警告", "请先配置保存文件路径。")
return
first_confirm = messagebox.askyesno("警告", "确定要清空本地向量库吗?此操作不可恢复!")
if first_confirm:
second_confirm = messagebox.askyesno("二次确认", "你确定真的要删除所有向量数据吗?此操作不可恢复!")
if second_confirm:
clear_vector_store()
clear_vector_store(filepath)
self.log("已清空向量库。")
def show_plot_arcs_ui(self):
@@ -1304,6 +1370,7 @@ class NovelGeneratorGUI:
save_string_to_txt(content, summary_file)
self.log("已保存对 global_summary.txt 的修改。")
# 入口
if __name__ == "__main__":
app = ctk.CTk()