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