+120
-35
@@ -1,47 +1,132 @@
|
||||
# chapter_directory_parser.py
|
||||
# chapter_blueprint_parser.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
def get_chapter_info_from_directory(novel_directory_content: str, chapter_number: int):
|
||||
def parse_chapter_blueprint(blueprint_text: str):
|
||||
"""
|
||||
从给定的 novel_directory_content 文本中,解析 “第X章” 行,并提取本章的标题和可能的简述。
|
||||
返回一个 dict: {
|
||||
"chapter_title": <字符串>,
|
||||
"chapter_brief": <字符串> (若没有则为空)
|
||||
解析整份章节蓝图文本,返回一个列表,每个元素是一个 dict:
|
||||
{
|
||||
"chapter_number": int,
|
||||
"chapter_title": str,
|
||||
"chapter_role": str, # 本章定位
|
||||
"chapter_purpose": str, # 核心作用
|
||||
"suspense_level": str, # 悬念密度
|
||||
"foreshadowing": str, # 伏笔操作
|
||||
"plot_twist_level": str, # 认知颠覆
|
||||
"chapter_summary": str # 本章简述
|
||||
}
|
||||
注意:目录文本示例格式:
|
||||
第1章 :潮起
|
||||
第2章 :阴影浮现 - 主要角色冲突爆发
|
||||
...
|
||||
也可能没有简述,只有一个简单标题。
|
||||
"""
|
||||
|
||||
# 将文本逐行拆分
|
||||
lines = novel_directory_content.splitlines()
|
||||
# 先按空行进行分块,以免多章之间混淆
|
||||
chunks = re.split(r'\n\s*\n', blueprint_text.strip())
|
||||
results = []
|
||||
|
||||
# 章节匹配:形如 “第5章 :xxx” or “第5章: xxx” or “第5章 xxx”
|
||||
pattern = re.compile(r'^第\s*(\d+)\s*章\s*[::]?\s*(.*)$')
|
||||
# 兼容是否使用方括号包裹章节标题
|
||||
# 例如:
|
||||
# 第1章 - 紫极光下的预兆
|
||||
# 或
|
||||
# 第1章 - [紫极光下的预兆]
|
||||
chapter_number_pattern = re.compile(r'^第\s*(\d+)\s*章\s*-\s*\[?(.*?)\]?$')
|
||||
|
||||
for line in lines:
|
||||
match = pattern.match(line.strip())
|
||||
if match:
|
||||
chap_num = int(match.group(1))
|
||||
if chap_num == chapter_number:
|
||||
full_title = match.group(2).strip()
|
||||
if ' - ' in full_title:
|
||||
parts = full_title.split(' - ', 1)
|
||||
return {
|
||||
"chapter_title": parts[0].strip(),
|
||||
"chapter_brief": parts[1].strip()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"chapter_title": full_title,
|
||||
"chapter_brief": ""
|
||||
}
|
||||
role_pattern = re.compile(r'^本章定位:\s*\[?(.*)\]?$')
|
||||
purpose_pattern = re.compile(r'^核心作用:\s*\[?(.*)\]?$')
|
||||
suspense_pattern = re.compile(r'^悬念密度:\s*\[?(.*)\]?$')
|
||||
foreshadow_pattern = re.compile(r'^伏笔操作:\s*\[?(.*)\]?$')
|
||||
twist_pattern = re.compile(r'^认知颠覆:\s*\[?(.*)\]?$')
|
||||
summary_pattern = re.compile(r'^本章简述:\s*\[?(.*)\]?$')
|
||||
|
||||
# 如果没有匹配到,返回默认
|
||||
for chunk in chunks:
|
||||
lines = chunk.strip().splitlines()
|
||||
if not lines:
|
||||
continue
|
||||
|
||||
chapter_number = None
|
||||
chapter_title = ""
|
||||
chapter_role = ""
|
||||
chapter_purpose = ""
|
||||
suspense_level = ""
|
||||
foreshadowing = ""
|
||||
plot_twist_level = ""
|
||||
chapter_summary = ""
|
||||
|
||||
# 先匹配第一行(或前几行),找到章号和标题
|
||||
header_match = chapter_number_pattern.match(lines[0].strip())
|
||||
if not header_match:
|
||||
# 不符合“第X章 - 标题”的格式,跳过
|
||||
continue
|
||||
|
||||
chapter_number = int(header_match.group(1))
|
||||
chapter_title = header_match.group(2).strip()
|
||||
|
||||
# 从后面的行匹配其他字段
|
||||
for line in lines[1:]:
|
||||
line_stripped = line.strip()
|
||||
if not line_stripped:
|
||||
continue
|
||||
|
||||
m_role = role_pattern.match(line_stripped)
|
||||
if m_role:
|
||||
chapter_role = m_role.group(1).strip()
|
||||
continue
|
||||
|
||||
m_purpose = purpose_pattern.match(line_stripped)
|
||||
if m_purpose:
|
||||
chapter_purpose = m_purpose.group(1).strip()
|
||||
continue
|
||||
|
||||
m_suspense = suspense_pattern.match(line_stripped)
|
||||
if m_suspense:
|
||||
suspense_level = m_suspense.group(1).strip()
|
||||
continue
|
||||
|
||||
m_foreshadow = foreshadow_pattern.match(line_stripped)
|
||||
if m_foreshadow:
|
||||
foreshadowing = m_foreshadow.group(1).strip()
|
||||
continue
|
||||
|
||||
m_twist = twist_pattern.match(line_stripped)
|
||||
if m_twist:
|
||||
plot_twist_level = m_twist.group(1).strip()
|
||||
continue
|
||||
|
||||
m_summary = summary_pattern.match(line_stripped)
|
||||
if m_summary:
|
||||
chapter_summary = m_summary.group(1).strip()
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"chapter_number": chapter_number,
|
||||
"chapter_title": chapter_title,
|
||||
"chapter_role": chapter_role,
|
||||
"chapter_purpose": chapter_purpose,
|
||||
"suspense_level": suspense_level,
|
||||
"foreshadowing": foreshadowing,
|
||||
"plot_twist_level": plot_twist_level,
|
||||
"chapter_summary": chapter_summary
|
||||
})
|
||||
|
||||
# 按照 chapter_number 排序后返回
|
||||
results.sort(key=lambda x: x["chapter_number"])
|
||||
return results
|
||||
|
||||
|
||||
def get_chapter_info_from_blueprint(blueprint_text: str, target_chapter_number: int):
|
||||
"""
|
||||
在已经加载好的章节蓝图文本中,找到对应章号的结构化信息,返回一个 dict。
|
||||
若找不到则返回一个默认的结构。
|
||||
"""
|
||||
all_chapters = parse_chapter_blueprint(blueprint_text)
|
||||
for ch in all_chapters:
|
||||
if ch["chapter_number"] == target_chapter_number:
|
||||
return ch
|
||||
# 默认返回
|
||||
return {
|
||||
"chapter_title": f"第{chapter_number}章",
|
||||
"chapter_brief": ""
|
||||
"chapter_number": target_chapter_number,
|
||||
"chapter_title": f"第{target_chapter_number}章",
|
||||
"chapter_role": "",
|
||||
"chapter_purpose": "",
|
||||
"suspense_level": "",
|
||||
"foreshadowing": "",
|
||||
"plot_twist_level": "",
|
||||
"chapter_summary": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# embedding_adapters.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import requests
|
||||
import traceback
|
||||
from typing import List
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
|
||||
def ensure_openai_base_url_has_v1(url: str) -> str:
|
||||
"""
|
||||
若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'。
|
||||
"""
|
||||
import re
|
||||
url = url.strip()
|
||||
if not url:
|
||||
return url
|
||||
if not re.search(r'/v\d+$', url):
|
||||
if '/v1' not in url:
|
||||
url = url.rstrip('/') + '/v1'
|
||||
return url
|
||||
|
||||
class BaseEmbeddingAdapter:
|
||||
"""
|
||||
Embedding 接口统一基类
|
||||
"""
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
raise NotImplementedError
|
||||
|
||||
class OpenAIEmbeddingAdapter(BaseEmbeddingAdapter):
|
||||
"""
|
||||
基于 OpenAIEmbeddings(或兼容接口)的适配器
|
||||
"""
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str):
|
||||
self._embedding = OpenAIEmbeddings(
|
||||
openai_api_key=api_key,
|
||||
openai_api_base=ensure_openai_base_url_has_v1(base_url),
|
||||
model=model_name
|
||||
)
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
return self._embedding.embed_documents(texts)
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
return self._embedding.embed_query(query)
|
||||
|
||||
class OllamaEmbeddingAdapter(BaseEmbeddingAdapter):
|
||||
"""
|
||||
其接口路径为 /api/embeddings
|
||||
"""
|
||||
def __init__(self, model_name: str, base_url: str):
|
||||
self.model_name = model_name
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
embeddings = []
|
||||
for text in texts:
|
||||
vec = self._embed_single(text)
|
||||
embeddings.append(vec)
|
||||
return embeddings
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
return self._embed_single(query)
|
||||
|
||||
def _embed_single(self, text: str) -> List[float]:
|
||||
"""
|
||||
调用 Ollama 本地服务 /api/embeddings 接口,获取文本 embedding
|
||||
"""
|
||||
url = self.base_url
|
||||
if "api/embeddings" not in url:
|
||||
url = f"{url}/api/embeddings"
|
||||
|
||||
data = {
|
||||
"model": self.model_name,
|
||||
"prompt": text
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
if "embedding" not in result:
|
||||
raise ValueError("No 'embedding' field in Ollama response.")
|
||||
return result["embedding"]
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"Ollama embeddings request error: {e}\n{traceback.format_exc()}")
|
||||
return []
|
||||
|
||||
class MLStudioEmbeddingAdapter(BaseEmbeddingAdapter):
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str):
|
||||
self._embedding = OpenAIEmbeddings(
|
||||
openai_api_key=api_key,
|
||||
openai_api_base=ensure_openai_base_url_has_v1(base_url),
|
||||
model=model_name
|
||||
)
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
return self._embedding.embed_documents(texts)
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
return self._embedding.embed_query(query)
|
||||
|
||||
def create_embedding_adapter(
|
||||
interface_format: str,
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
model_name: str
|
||||
) -> BaseEmbeddingAdapter:
|
||||
"""
|
||||
工厂函数:根据 interface_format 返回不同的 embedding 适配器实例
|
||||
"""
|
||||
if interface_format.lower() == "openai":
|
||||
return OpenAIEmbeddingAdapter(api_key, base_url, model_name)
|
||||
elif interface_format.lower() == "ollama":
|
||||
return OllamaEmbeddingAdapter(model_name, base_url)
|
||||
elif interface_format.lower() == "ml studio":
|
||||
return MLStudioEmbeddingAdapter(api_key, base_url, model_name)
|
||||
else:
|
||||
raise ValueError(f"Unknown embedding interface_format: {interface_format}")
|
||||
@@ -1,59 +0,0 @@
|
||||
# embedding_ollama.py
|
||||
import requests
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
class OllamaEmbeddings:
|
||||
def __init__(self, model_name: str, base_url: str):
|
||||
self.model_name = model_name
|
||||
self.base_url = base_url
|
||||
|
||||
def embed(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
批量将多段文本转换为embedding向量
|
||||
"""
|
||||
embeddings = []
|
||||
for text in texts:
|
||||
embeddings.append(self.embed_single_document(text))
|
||||
return embeddings
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
兼容langchain的接口写法
|
||||
"""
|
||||
return self.embed(texts)
|
||||
|
||||
def embed_query(self, query: str) -> List[float]:
|
||||
"""
|
||||
将单条 query 转换为 embedding 向量
|
||||
"""
|
||||
return self.embed_single_document(query)
|
||||
|
||||
def embed_single_document(self, text: str) -> List[float]:
|
||||
"""
|
||||
调用 Ollama 本地服务接口,获取文本的 embedding。
|
||||
"""
|
||||
if self.base_url.endswith("/"):
|
||||
self.base_url = self.base_url.rstrip("/")
|
||||
if "api/embeddings" in self.base_url:
|
||||
# 如果 base_url 已经包含 'api/embeddings',则保持不变
|
||||
url = f"{self.base_url.rstrip('/')}/api/embeddings"
|
||||
else:
|
||||
if "/v1" in self.base_url:
|
||||
self.base_url = self.base_url.split("/v1")[0]
|
||||
if "/api" in self.base_url:
|
||||
self.base_url = self.base_url.split("/api")[0]
|
||||
url = f"{self.base_url}/api/embeddings"
|
||||
data = {
|
||||
"model": self.model_name,
|
||||
"prompt": text
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
if "embedding" not in result:
|
||||
raise ValueError("No 'embedding' field in Ollama response.")
|
||||
return result["embedding"]
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"Ollama embeddings request error: {e}\n{traceback.format_exc()}")
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# llm_adapters.py
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from typing import Optional
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
def ensure_openai_base_url_has_v1(url: str) -> str:
|
||||
"""
|
||||
若用户输入的 url 不包含 '/v1',则在末尾追加 '/v1'。
|
||||
"""
|
||||
import re
|
||||
url = url.strip()
|
||||
if not url:
|
||||
return url
|
||||
if not re.search(r'/v\d+$', url):
|
||||
if '/v1' not in url:
|
||||
url = url.rstrip('/') + '/v1'
|
||||
return url
|
||||
|
||||
class BaseLLMAdapter:
|
||||
"""
|
||||
统一的 LLM 接口基类,为不同后端(OpenAI、Ollama、ML Studio 等)提供一致的方法签名。
|
||||
"""
|
||||
def invoke(self, prompt: str) -> str:
|
||||
raise NotImplementedError("Subclasses must implement .invoke(prompt) method.")
|
||||
|
||||
class DeepSeekAdapter(BaseLLMAdapter):
|
||||
"""
|
||||
适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI)
|
||||
"""
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7):
|
||||
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
|
||||
self._client = ChatOpenAI(
|
||||
model=self.model_name,
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature
|
||||
)
|
||||
|
||||
def invoke(self, prompt: str) -> str:
|
||||
response = self._client.invoke(prompt)
|
||||
if not response:
|
||||
logging.warning("No response from DeepSeekAdapter.")
|
||||
return ""
|
||||
return response.content
|
||||
|
||||
class OpenAIAdapter(BaseLLMAdapter):
|
||||
"""
|
||||
适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI)
|
||||
"""
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7):
|
||||
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
|
||||
self._client = ChatOpenAI(
|
||||
model=self.model_name,
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature
|
||||
)
|
||||
|
||||
def invoke(self, prompt: str) -> str:
|
||||
response = self._client.invoke(prompt)
|
||||
if not response:
|
||||
logging.warning("No response from OpenAIAdapter.")
|
||||
return ""
|
||||
return response.content
|
||||
|
||||
class OllamaAdapter(BaseLLMAdapter):
|
||||
"""
|
||||
Ollama 同样有一个 OpenAI-like /v1/chat 接口,可直接使用 ChatOpenAI。
|
||||
但是通常 Ollama 默认本地服务在 http://localhost:11434,如果符合OpenAI风格即可直接传参。
|
||||
"""
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7):
|
||||
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
|
||||
self._client = ChatOpenAI(
|
||||
model=self.model_name,
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature
|
||||
)
|
||||
|
||||
def invoke(self, prompt: str) -> str:
|
||||
response = self._client.invoke(prompt)
|
||||
if not response:
|
||||
logging.warning("No response from OllamaAdapter.")
|
||||
return ""
|
||||
return response.content
|
||||
|
||||
class MLStudioAdapter(BaseLLMAdapter):
|
||||
def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7):
|
||||
self.base_url = ensure_openai_base_url_has_v1(base_url)
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
|
||||
self._client = ChatOpenAI(
|
||||
model=self.model_name,
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature
|
||||
)
|
||||
|
||||
def invoke(self, prompt: str) -> str:
|
||||
response = self._client.invoke(prompt)
|
||||
if not response:
|
||||
logging.warning("No response from MLStudioAdapter.")
|
||||
return ""
|
||||
return response.content
|
||||
|
||||
def create_llm_adapter(
|
||||
interface_format: str,
|
||||
base_url: str,
|
||||
model_name: str,
|
||||
api_key: str,
|
||||
temperature: float,
|
||||
max_tokens: int
|
||||
) -> BaseLLMAdapter:
|
||||
"""
|
||||
工厂函数:根据 interface_format 返回不同的适配器实例。
|
||||
"""
|
||||
if interface_format.lower() == "deepseek":
|
||||
return DeepSeekAdapter(api_key, base_url, model_name, max_tokens, temperature)
|
||||
elif interface_format.lower() == "openai":
|
||||
return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature)
|
||||
elif interface_format.lower() == "ollama":
|
||||
return OllamaAdapter(api_key, base_url, model_name, max_tokens, temperature)
|
||||
elif interface_format.lower() == "ml studio":
|
||||
return MLStudioAdapter(api_key, base_url, model_name, max_tokens, temperature)
|
||||
else:
|
||||
raise ValueError(f"Unknown interface_format: {interface_format}")
|
||||
@@ -14,7 +14,8 @@ hiddenimports = ['typing_extensions',
|
||||
'pydantic',
|
||||
'pydantic.deprecated.decorator',
|
||||
'tiktoken_ext.openai_public',
|
||||
'tiktoken_ext'
|
||||
'tiktoken_ext',
|
||||
'chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2'
|
||||
]
|
||||
|
||||
tmp_ret = collect_all('chromadb')
|
||||
@@ -44,7 +45,7 @@ exe = EXE(
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='AI_NovelGenerator_V1.3.2',
|
||||
name='AI_NovelGenerator_V1.3.3',
|
||||
debug=True,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
@@ -65,5 +66,5 @@ coll = COLLECT(
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='AI_NovelGenerator_V1.3.2'
|
||||
name='AI_NovelGenerator_V1.3.3'
|
||||
)
|
||||
|
||||
+502
-463
File diff suppressed because it is too large
Load Diff
+340
-92
@@ -1,86 +1,215 @@
|
||||
# prompt_definitions.py
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
集中存放所有提示词(Prompt),新版本更精确、更具创新性,结合部分雪花写作法等理论。
|
||||
集中存放所有提示词 (Prompt),整合雪花写作法、角色弧光理论、悬念三要素模型等
|
||||
并包含新增加的短期摘要/下一章关键字提炼提示词,以及章节正文写作提示词。
|
||||
"""
|
||||
|
||||
# =============== 1. 整体设定 ===================
|
||||
set_prompt = """\
|
||||
请根据主题: {topic}、类型: {genre}、章数: {number_of_chapters}、每章字数: {word_number} 来设计小说的总体结构和世界观。
|
||||
写作时请参考雪花写作法等创作理论,结合以下要点:
|
||||
# =============== 摘要与下一章关键字提炼 ===============
|
||||
summarize_recent_chapters_prompt = """\
|
||||
你是一名资深长篇小说编辑,请分析以下合并文本(可能包含最近几章内容):
|
||||
{combined_text}
|
||||
|
||||
• 小说标题与大致篇幅(总字数范围)。
|
||||
• 类型与基调(如:都市、魔幻、战争、轻松、暗黑等)。
|
||||
• 主要写作风格(视角、叙述方式、语言风格等)。
|
||||
• 世界观设定(背景时间、地理环境、社会结构、科技或魔法水平、重要历史等)。
|
||||
• 整体故事框架(可提及常见叙事结构:三幕、英雄之旅等)。
|
||||
• 主线与副线的初步构想,以及它们如何交织。
|
||||
• 关键角色群像定位与主要冲突关系。
|
||||
• 结局的可能方向(圆满、悲剧、开放式等)。
|
||||
现在请你基于目前故事的进展,完成以下两件事:
|
||||
1) 用最多200字,写一个简洁明了的「当前情节短期摘要」。
|
||||
2) 提炼「下一章」的关键字(例如关键物品、重要人物、地点、事件、情节等),可以用逗号分隔或条目列出。
|
||||
|
||||
请以简洁、逻辑清晰的方式输出,保留足够细节以支撑后续创作。
|
||||
请按如下格式输出(不需要额外解释):
|
||||
短期摘要: <这里写短期摘要>
|
||||
下一章关键字: <这里写下一章关键字>
|
||||
"""
|
||||
|
||||
# =============== 2. 角色设定 ===================
|
||||
character_prompt = """\
|
||||
基于已生成的小说整体设定:
|
||||
{novel_setting}
|
||||
# =============== 1. 核心种子设定(雪花第1层)===================
|
||||
core_seed_prompt = """\
|
||||
作为专业作家,请用"雪花写作法"第一步构建故事核心:
|
||||
主题:{topic}
|
||||
类型:{genre}
|
||||
篇幅:约{number_of_chapters}章(每章{word_number}字)
|
||||
|
||||
请进一步扩展角色设置,至少包含三名核心角色。对每位角色说明:
|
||||
• 角色背景、外貌与主要性格特征。
|
||||
• 内在冲突、目标与动机。
|
||||
• 暗藏的秘密或潜在弱点(可与世界观或其他角色有关)。
|
||||
• 与其他角色的关系或对立点,如何推动或阻碍情节发展。
|
||||
请用单句公式概括故事本质,例如:
|
||||
"当[主角]遭遇[核心事件],必须[关键行动],否则[灾难后果];与此同时,[隐藏的更大危机]正在发酵。"
|
||||
|
||||
请重点突出角色的多重面向,为后续情节埋下伏笔。
|
||||
要求:
|
||||
1. 必须包含显性冲突与潜在危机
|
||||
2. 体现人物核心驱动力
|
||||
3. 暗示世界观关键矛盾
|
||||
4. 使用25-100字精准表达
|
||||
|
||||
仅返回故事核心文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 3. 暗线与伏笔 ===================
|
||||
dark_lines_prompt = """\
|
||||
在当前世界观与角色关系中:
|
||||
{character_info}
|
||||
# =============== 2. 角色动力学设定(角色弧光模型)===================
|
||||
character_dynamics_prompt = """\
|
||||
基于核心种子:
|
||||
{core_seed}
|
||||
|
||||
请构思若干暗线、伏笔或隐藏冲突,结合雪花写作法“细节逐步扩展”的思路。要求:
|
||||
• 每条暗线给出初始迹象、后续发展与可能的爆发条件。
|
||||
• 与角色背景、世界观或关键事件相呼应。
|
||||
• 保持合理的悬念设置,与现有设定不冲突。
|
||||
• 为后续剧情保留足够展开空间,强调持续影响故事进程。
|
||||
请设计3-6个具有动态变化潜力的核心角色,每个角色需包含:
|
||||
特征:
|
||||
- 背景、外貌、性别、年龄、职业等
|
||||
- 暗藏的秘密或潜在弱点(可与世界观或其他角色有关)
|
||||
|
||||
请避免一次性透漏全部细节,以逐步揭示的方式为后文做铺垫。
|
||||
核心驱动力三角:
|
||||
- 表面追求(物质目标)
|
||||
- 深层渴望(情感需求)
|
||||
- 灵魂需求(哲学层面)
|
||||
|
||||
角色弧线设计:
|
||||
初始状态 → 触发事件 → 认知失调 → 蜕变节点 → 最终状态
|
||||
|
||||
关系冲突网:
|
||||
- 与其他角色的关系或对立点
|
||||
- 与至少两个其他角色的价值观冲突
|
||||
- 一个合作纽带
|
||||
- 一个隐藏的背叛可能性
|
||||
|
||||
要求:
|
||||
仅给出最终文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 4. 最终设定整合 ===================
|
||||
finalize_setting_prompt = """\
|
||||
基于以下内容,请整合并输出终稿《小说设定》:
|
||||
1. 整体设定:
|
||||
{novel_setting_base}
|
||||
2. 角色设定:
|
||||
{character_setting}
|
||||
3. 暗线与伏笔:
|
||||
{dark_lines}
|
||||
# =============== 3. 世界构建矩阵(三维度交织法)===================
|
||||
world_building_prompt = """\
|
||||
为服务核心冲突"{core_seed}",请构建三维交织的世界观:
|
||||
|
||||
整合要求:
|
||||
• 以整体视角整理世界观、角色与暗线,形成一个有机结合的故事框架。
|
||||
• 注意角色动机与暗线如何与世界观互相呼应,并兼顾主线与副线节奏。
|
||||
• 语言通顺、层次分明。直接输出文本,不使用Markdown格式。
|
||||
1. 物理维度:
|
||||
- 空间结构(地理×社会阶层分布图)
|
||||
- 时间轴(关键历史事件年表)
|
||||
- 法则体系(物理/魔法/社会规则的漏洞点)
|
||||
|
||||
2. 社会维度:
|
||||
- 权力结构断层线(可引发冲突的阶层/种族/组织矛盾)
|
||||
- 文化禁忌(可被打破的禁忌及其后果)
|
||||
- 经济命脉(资源争夺焦点)
|
||||
|
||||
3. 隐喻维度:
|
||||
- 贯穿全书的视觉符号系统(如反复出现的意象)
|
||||
- 氣候/环境变化映射的心理状态
|
||||
- 建筑风格暗示的文明困境
|
||||
|
||||
要求:
|
||||
每个维度至少包含3个可与角色决策产生互动的动态元素。
|
||||
仅给出最终文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 5. 小说目录 ===================
|
||||
novel_directory_prompt = """\
|
||||
根据以下最终《小说设定》:
|
||||
{final_novel_setting}
|
||||
# =============== 4. 情节架构(三幕式悬念)===================
|
||||
plot_architecture_prompt = """\
|
||||
基于以下元素构建三幕式悬念架构:
|
||||
核心种子:{core_seed}
|
||||
角色体系:{character_dynamics}
|
||||
世界观:{world_building}
|
||||
|
||||
请列出共 {number_of_chapters} 章的目录,并保证每章的标题或简述能呼应主要冲突、角色成长及暗线埋设。
|
||||
目录格式示例:
|
||||
第1章 :<标题> - <简要说明>
|
||||
第2章 :<标题> - <简要说明>
|
||||
...
|
||||
第{number_of_chapters}章 :<标题> - <简要说明>
|
||||
要求按以下结构设计:
|
||||
第一幕(触发)
|
||||
- 日常状态中的异常征兆(3处铺垫)
|
||||
- 引出故事:展示主线、暗线、副线的开端
|
||||
- 关键事件:打破平衡的催化剂(需改变至少3个角色的关系)
|
||||
- 错误抉择:主角的认知局限导致的错误反应
|
||||
|
||||
每章可酌情加入一句简述,引导读者了解本章看点。直接输出文本,不使用Markdown。
|
||||
第二幕(对抗)
|
||||
- 剧情升级:主线+副线的交叉点
|
||||
- 双重压力:外部障碍升级+内部挫折
|
||||
- 虚假胜利:看似解决实则深化危机的转折点
|
||||
- 灵魂黑夜:世界观认知颠覆时刻
|
||||
|
||||
第三幕(解决)
|
||||
- 代价显现:解决危机必须牺牲的核心价值
|
||||
- 嵌套转折:至少包含三层认知颠覆(表面解→新危机→终极抉择)
|
||||
- 余波:留下2个开放式悬念因子
|
||||
|
||||
每个阶段需包含3个关键转折点及其对应的伏笔回收方案。
|
||||
仅给出最终文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 6. 全局摘要更新 ===================
|
||||
# =============== 5. 章节目录生成(悬念节奏曲线)===================
|
||||
chapter_blueprint_prompt = """\
|
||||
根据小说架构:\n
|
||||
{novel_architecture}
|
||||
|
||||
设计{number_of_chapters}章的节奏分布:
|
||||
1. 章节集群划分:
|
||||
- 每3-5章构成一个悬念单元,包含完整的小高潮
|
||||
- 单元之间设置"认知过山车"(连续2章紧张→1章缓冲)
|
||||
- 关键转折章需预留多视角铺垫
|
||||
|
||||
2. 每章需明确:
|
||||
- 章节定位(角色/事件/主题等)
|
||||
- 核心悬念类型(信息差/道德困境/时间压力等)
|
||||
- 情感基调迁移(如从怀疑→恐惧→决绝)
|
||||
- 伏笔操作(埋设/强化/回收)
|
||||
- 认知颠覆强度(1-5级)
|
||||
|
||||
输出格式示例:
|
||||
第n章 - [标题]
|
||||
本章定位:[角色/事件/主题/...]
|
||||
核心作用:[推进/转折/揭示/...]
|
||||
悬念密度:[紧凑/渐进/爆发/...]
|
||||
伏笔操作:埋设(A线索)→强化(B矛盾)...
|
||||
认知颠覆:★☆☆☆☆
|
||||
本章简述:[一句话概括]
|
||||
|
||||
第n+1章 - [标题]
|
||||
本章定位:[角色/事件/主题/...]
|
||||
核心作用:[推进/转折/揭示/...]
|
||||
悬念密度:[紧凑/渐进/爆发/...]
|
||||
伏笔操作:埋设(A线索)→强化(B矛盾)...
|
||||
认知颠覆:★☆☆☆☆
|
||||
本章简述:[一句话概括]
|
||||
|
||||
要求:
|
||||
- 使用精炼语言描述,每章字数控制在100字以内。
|
||||
- 合理安排节奏,确保整体悬念曲线的连贯性。
|
||||
- 在生成{number_of_chapters}章前不要出现结局章节。
|
||||
|
||||
仅给出最终文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
chunked_chapter_blueprint_prompt = """\
|
||||
根据小说架构:\n
|
||||
{novel_architecture}
|
||||
|
||||
需要生成总共{number_of_chapters}章的节奏分布,
|
||||
|
||||
当前已有章节目录(若未空则说明是初始生成):\n
|
||||
{chapter_list}
|
||||
|
||||
现在请设计第{n}章到第{m}的节奏分布:
|
||||
1. 章节集群划分:
|
||||
- 每3-5章构成一个悬念单元,包含完整的小高潮
|
||||
- 单元之间设置"认知过山车"(连续2章紧张→1章缓冲)
|
||||
- 关键转折章需预留多视角铺垫
|
||||
|
||||
2. 每章需明确:
|
||||
- 章节定位(角色/事件/主题等)
|
||||
- 核心悬念类型(信息差/道德困境/时间压力等)
|
||||
- 情感基调迁移(如从怀疑→恐惧→决绝)
|
||||
- 伏笔操作(埋设/强化/回收)
|
||||
- 认知颠覆强度(1-5级)
|
||||
|
||||
输出格式示例:
|
||||
第n章 - [标题]
|
||||
本章定位:[角色/事件/主题/...]
|
||||
核心作用:[推进/转折/揭示/...]
|
||||
悬念密度:[紧凑/渐进/爆发/...]
|
||||
伏笔操作:埋设(A线索)→强化(B矛盾)...
|
||||
认知颠覆:★☆☆☆☆
|
||||
本章简述:[一句话概括]
|
||||
|
||||
第n+1章 - [标题]
|
||||
本章定位:[角色/事件/主题/...]
|
||||
核心作用:[推进/转折/揭示/...]
|
||||
悬念密度:[紧凑/渐进/爆发/...]
|
||||
伏笔操作:埋设(A线索)→强化(B矛盾)...
|
||||
认知颠覆:★☆☆☆☆
|
||||
本章简述:[一句话概括]
|
||||
|
||||
要求:
|
||||
- 使用精炼语言描述,每章字数控制在100字以内。
|
||||
- 合理安排节奏,确保整体悬念曲线的连贯性。
|
||||
- 在生成{number_of_chapters}章前不要出现结局章节。
|
||||
|
||||
仅给出最终文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 6. 全局摘要更新 ===================
|
||||
summary_prompt = """\
|
||||
以下是新完成的章节文本:
|
||||
{chapter_text}
|
||||
@@ -88,10 +217,17 @@ summary_prompt = """\
|
||||
这是当前的全局摘要(可为空):
|
||||
{global_summary}
|
||||
|
||||
请根据本章新增内容,更新全局摘要。保留既有重要信息,同时融入新剧情要点,勿剧透未来。控制在不超过3000字的范围内,语言简练流畅。
|
||||
请根据本章新增内容,更新全局摘要。
|
||||
要求:
|
||||
- 保留既有重要信息,同时融入新剧情要点
|
||||
- 以简洁、连贯的语言描述全书进展
|
||||
- 客观描绘,不展开联想或解释
|
||||
- 字数控制在2000字以内
|
||||
|
||||
仅返回全局摘要文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 7. 角色状态更新 ===================
|
||||
# =============== 7. 角色状态更新 ===================
|
||||
update_character_state_prompt = """\
|
||||
以下是新完成的章节文本:
|
||||
{chapter_text}
|
||||
@@ -99,45 +235,157 @@ update_character_state_prompt = """\
|
||||
这是当前的角色状态文档(可为空):
|
||||
{old_state}
|
||||
|
||||
请更新角色状态,内容包括:
|
||||
• 角色的物品、能力或心理状态变化。
|
||||
• 角色间关系的最新进展或冲突。
|
||||
• 是否触发或加深了某些暗线或关键事件。
|
||||
• 任何新增角色或临时出场人物的基本信息。
|
||||
请更新角色状态,内容格式:
|
||||
角色A属性:
|
||||
├──物品:
|
||||
├──某物(道具):描述
|
||||
├──XX长剑(武器):描述
|
||||
...
|
||||
├──能力
|
||||
├──技能1:描述
|
||||
├──技能2:描述
|
||||
...
|
||||
├──状态
|
||||
├──身体状态:
|
||||
├──Buff/Debuff
|
||||
├──心理状态:描述
|
||||
|
||||
请直接在已有文档基础上进行增删,语言尽量简洁、有条理。
|
||||
├──主要角色间关系网
|
||||
├──角色B:描述
|
||||
├──角色C:描述
|
||||
...
|
||||
├──触发或加深的事件
|
||||
├──事件1:描述
|
||||
├──事件2:描述
|
||||
...
|
||||
|
||||
角色B属性:
|
||||
├──物品
|
||||
├──...
|
||||
├──能力
|
||||
├──...
|
||||
├──状态
|
||||
├──...
|
||||
├──主要角色间关系网
|
||||
├──...
|
||||
├──触发或加深的事件
|
||||
├──...
|
||||
|
||||
角色C属性:
|
||||
......
|
||||
|
||||
新出场角色:
|
||||
- 任何新增角色或临时出场人物的基本信息
|
||||
|
||||
要求:
|
||||
- 请直接在已有文档基础上进行增删
|
||||
- 不改变原有结构,语言尽量简洁、有条理
|
||||
|
||||
仅返回更新后的角色状态文本,不要解释任何内容。
|
||||
"""
|
||||
|
||||
# =============== 8. 章节大纲设计 ===================
|
||||
chapter_outline_prompt = """\
|
||||
这是当前小说的重要信息:
|
||||
- 小说设定:{novel_setting}
|
||||
- 角色状态:{character_state}
|
||||
- 全局摘要:{global_summary}
|
||||
# =============== 8. 章节正文写作 ===================
|
||||
|
||||
我们即将写第 {novel_number} 章,标题:{chapter_title},简述(若有):{chapter_brief}
|
||||
# 8.1 第一章草稿提示
|
||||
first_chapter_draft_prompt = """\
|
||||
即将创作:第 {novel_number} 章《{chapter_title}》
|
||||
本章定位:{chapter_role}
|
||||
核心作用:{chapter_purpose}
|
||||
悬念密度:{suspense_level}
|
||||
伏笔操作:{foreshadowing}
|
||||
认知颠覆:{plot_twist_level}
|
||||
本章简述:{chapter_summary}
|
||||
|
||||
请按照以下思路设计本章大纲:
|
||||
1. 本章的主要冲突或情节目标,如何与标题呼应。
|
||||
2. 出场角色及其动机,对剧情走向的推动或阻碍。
|
||||
3. 暗线或伏笔如何有所进展或被揭示。
|
||||
4. 本章结尾的悬念或转折,如何为下一章做铺垫。
|
||||
可用元素:
|
||||
- 核心人物(可能未指定):{characters_involved}
|
||||
- 关键道具(可能未指定):{key_items}
|
||||
- 空间坐标(可能未指定):{scene_location}
|
||||
- 时间压力(可能未指定):{time_constraint}
|
||||
|
||||
请以简要分点方式输出,不要使用Markdown格式。
|
||||
参考文档:
|
||||
- 小说设定:
|
||||
{novel_setting}
|
||||
|
||||
请完成第 {novel_number} 章的正文,至少设计下方2个具有动态张力的场景:
|
||||
1. 对话场景:
|
||||
- 潜台词冲突(表面谈论A,实际博弈B)
|
||||
- 权力关系变化(通过非对称对话长度体现)
|
||||
- 至少1处双关语暗示未来危机
|
||||
|
||||
2. 动作场景:
|
||||
- 环境交互细节(至少3个感官描写)
|
||||
- 节奏控制(短句加速+比喻减速)
|
||||
- 动作揭示人物隐藏特质
|
||||
|
||||
3. 心理场景:
|
||||
- 认知失调的具体表现(行为矛盾)
|
||||
- 隐喻系统的运用(连接世界观符号)
|
||||
- 决策前的价值天平描写
|
||||
|
||||
文末设置一个"钩链转折":结尾时回收旧悬念/创造新悬念/抛出新危机/颠覆某个认知/神转折等。
|
||||
|
||||
格式要求:
|
||||
- 仅返回章节正文文本;
|
||||
- 不使用分章节小标题;
|
||||
- 不要使用markdown格式。
|
||||
|
||||
额外指导(可能未指定):{user_guidance}
|
||||
"""
|
||||
|
||||
# =============== 9. 章节正文写作 ===================
|
||||
chapter_write_prompt = """\
|
||||
以下信息供你参考:
|
||||
1. 小说设定:{novel_setting}
|
||||
2. 角色状态:{character_state}
|
||||
3. 全局摘要:{global_summary}
|
||||
4. 本章大纲:{chapter_outline}
|
||||
# 8.2 后续章节草稿提示
|
||||
next_chapter_draft_prompt = """\
|
||||
参考文档:
|
||||
- 小说设定:
|
||||
{novel_setting}
|
||||
|
||||
请写出第 {novel_number} 章的正文,标题为“{chapter_title}”。需要:
|
||||
• 字数不少于 {word_number} 字,与标题和简述相呼应。
|
||||
• 保持连贯叙述,可增加环境、心理、对话等细节描写。
|
||||
• 适度呼应前文暗线或角色矛盾,为后续发展留出悬念。
|
||||
- 全局摘要:
|
||||
{global_summary}
|
||||
|
||||
直接输出正文内容,不使用分章节小标题,章节末模仿正常小说中断或转场。
|
||||
- 角色状态:
|
||||
{character_state}
|
||||
|
||||
本地知识库检索到的片段:
|
||||
{context_excerpt}
|
||||
|
||||
即将创作:第 {novel_number} 章《{chapter_title}》
|
||||
本章定位:{chapter_role}
|
||||
核心作用:{chapter_purpose}
|
||||
悬念密度:{suspense_level}
|
||||
伏笔操作:{foreshadowing}
|
||||
认知颠覆:{plot_twist_level}
|
||||
本章简述:{chapter_summary}
|
||||
|
||||
可用元素:
|
||||
- 核心人物(可能未指定):{characters_involved}
|
||||
- 关键道具(可能未指定):{key_items}
|
||||
- 空间坐标(可能未指定):{scene_location}
|
||||
- 时间压力(可能未指定):{time_constraint}
|
||||
|
||||
前章结尾段:
|
||||
{previous_chapter_excerpt}
|
||||
|
||||
请依据前章结尾片段,继续完成第 {novel_number} 章的正文,至少设计下方2个具有动态张力的场景:
|
||||
1. 对话场景:
|
||||
- 潜台词冲突(表面谈论A,实际博弈B)
|
||||
- 权力关系变化(通过非对称对话长度体现)
|
||||
- 至少1处双关语暗示未来危机
|
||||
|
||||
2. 动作场景:
|
||||
- 环境交互细节(至少3个感官描写)
|
||||
- 节奏控制(短句加速+比喻减速)
|
||||
- 动作揭示人物隐藏特质
|
||||
|
||||
3. 心理场景:
|
||||
- 认知失调的具体表现(行为矛盾)
|
||||
- 隐喻系统的运用(连接世界观符号)
|
||||
- 决策前的价值天平描写
|
||||
|
||||
文末设置一个"钩链转折":结尾时回收旧悬念/创造新悬念/抛出新危机/颠覆某个认知/神转折等。
|
||||
|
||||
格式要求:
|
||||
- 仅返回章节正文文本;
|
||||
- 不使用分章节小标题;
|
||||
- 不要使用markdown格式。
|
||||
|
||||
额外指导(可能未指定):{user_guidance}
|
||||
"""
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# tooltips.py
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
tooltips = {
|
||||
"api_key": "在这里填写你的API Key。如果使用OpenAI官方接口,请在 https://platform.openai.com/account/api-keys 获取。",
|
||||
"base_url": "模型的接口地址。若使用OpenAI官方:https://api.openai.com/v1。若使用Ollama本地部署,则类似 http://localhost:11434/v1。",
|
||||
"interface_format": "指定LLM接口兼容格式,可选DeepSeek\OpenAI\Ollama\ML Studio等。\n\n注意:"+
|
||||
"OpenAI 兼容是指的可以通过该标准请求的任何接口,不是只允许使用api.openai.com接口\n"+
|
||||
"例如Ollama接口格式也兼容OpenAI,可以无需修改直接使用\n"+
|
||||
"ML Studio接口格式与OpenAI接口格式也一致。",
|
||||
"model_name": "要使用的模型名称,例如deepseek-reasoner、gpt-4o等。如果是Ollama等,请填写你下载好的本地模型名。",
|
||||
"temperature": "生成文本的随机度。数值越大越具有发散性,越小越严谨。",
|
||||
"max_tokens": "限制单次生成的最大Token数。范围1~100000,请根据模型上下文及需求填写合适值。\n"+
|
||||
"以下是一些常见模型的最大值:\n"+
|
||||
"o1:100,000\n"+
|
||||
"o1-mini:65,536\n"+
|
||||
"gpt-4o:16384\n"+
|
||||
"gpt-4o-mini:16384\n"+
|
||||
"deepseek-reasoner:8192\n"+
|
||||
"deepseek-chat:4096\n",
|
||||
"embedding_api_key": "调用Embedding模型时所需的API Key。",
|
||||
"embedding_interface_format": "Embedding模型接口风格,比如OpenAI或Ollama。",
|
||||
"embedding_url": "Embedding模型接口地址。",
|
||||
"embedding_model_name": "Embedding模型名称,如text-embedding-ada-002。",
|
||||
"embedding_retrieval_k": "向量检索时返回的Top-K结果数量。",
|
||||
"topic": "小说的大致主题或主要故事背景描述。",
|
||||
"genre": "小说的题材类型,如玄幻、都市、科幻等。",
|
||||
"num_chapters": "小说期望的章节总数。",
|
||||
"word_number": "每章的目标字数。",
|
||||
"filepath": "生成文件存储的根目录路径。所有txt文件、向量库等放在该目录下。",
|
||||
"chapter_num": "当前正在处理的章节号,用于生成草稿或定稿操作。",
|
||||
"user_guidance": "为本章提供的一些额外指令或写作引导。",
|
||||
"characters_involved": "本章需要重点描写或影响剧情的角色名单。",
|
||||
"key_items": "在本章中出现的重要道具、线索或物品。",
|
||||
"scene_location": "本章主要发生的地点或场景描述。",
|
||||
"time_constraint": "本章剧情中涉及的时间压力或时限设置。"
|
||||
}
|
||||
Reference in New Issue
Block a user