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