+120
-35
@@ -1,47 +1,132 @@
|
|||||||
# chapter_directory_parser.py
|
# chapter_blueprint_parser.py
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
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:
|
||||||
返回一个 dict: {
|
{
|
||||||
"chapter_title": <字符串>,
|
"chapter_number": int,
|
||||||
"chapter_brief": <字符串> (若没有则为空)
|
"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:
|
role_pattern = re.compile(r'^本章定位:\s*\[?(.*)\]?$')
|
||||||
match = pattern.match(line.strip())
|
purpose_pattern = re.compile(r'^核心作用:\s*\[?(.*)\]?$')
|
||||||
if match:
|
suspense_pattern = re.compile(r'^悬念密度:\s*\[?(.*)\]?$')
|
||||||
chap_num = int(match.group(1))
|
foreshadow_pattern = re.compile(r'^伏笔操作:\s*\[?(.*)\]?$')
|
||||||
if chap_num == chapter_number:
|
twist_pattern = re.compile(r'^认知颠覆:\s*\[?(.*)\]?$')
|
||||||
full_title = match.group(2).strip()
|
summary_pattern = re.compile(r'^本章简述:\s*\[?(.*)\]?$')
|
||||||
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": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# 如果没有匹配到,返回默认
|
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 {
|
return {
|
||||||
"chapter_title": f"第{chapter_number}章",
|
"chapter_number": target_chapter_number,
|
||||||
"chapter_brief": ""
|
"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',
|
||||||
'pydantic.deprecated.decorator',
|
'pydantic.deprecated.decorator',
|
||||||
'tiktoken_ext.openai_public',
|
'tiktoken_ext.openai_public',
|
||||||
'tiktoken_ext'
|
'tiktoken_ext',
|
||||||
|
'chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2'
|
||||||
]
|
]
|
||||||
|
|
||||||
tmp_ret = collect_all('chromadb')
|
tmp_ret = collect_all('chromadb')
|
||||||
@@ -44,7 +45,7 @@ exe = EXE(
|
|||||||
a.scripts,
|
a.scripts,
|
||||||
[],
|
[],
|
||||||
exclude_binaries=True,
|
exclude_binaries=True,
|
||||||
name='AI_NovelGenerator_V1.3.2',
|
name='AI_NovelGenerator_V1.3.3',
|
||||||
debug=True,
|
debug=True,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
@@ -65,5 +66,5 @@ coll = COLLECT(
|
|||||||
strip=False,
|
strip=False,
|
||||||
upx=True,
|
upx=True,
|
||||||
upx_exclude=[],
|
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
+341
-93
@@ -1,86 +1,215 @@
|
|||||||
# prompt_definitions.py
|
# prompt_definitions.py
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
集中存放所有提示词(Prompt),新版本更精确、更具创新性,结合部分雪花写作法等理论。
|
集中存放所有提示词 (Prompt),整合雪花写作法、角色弧光理论、悬念三要素模型等
|
||||||
|
并包含新增加的短期摘要/下一章关键字提炼提示词,以及章节正文写作提示词。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 1. 整体设定 ===================
|
# =============== 摘要与下一章关键字提炼 ===============
|
||||||
set_prompt = """\
|
summarize_recent_chapters_prompt = """\
|
||||||
请根据主题: {topic}、类型: {genre}、章数: {number_of_chapters}、每章字数: {word_number} 来设计小说的总体结构和世界观。
|
你是一名资深长篇小说编辑,请分析以下合并文本(可能包含最近几章内容):
|
||||||
写作时请参考雪花写作法等创作理论,结合以下要点:
|
{combined_text}
|
||||||
|
|
||||||
• 小说标题与大致篇幅(总字数范围)。
|
现在请你基于目前故事的进展,完成以下两件事:
|
||||||
• 类型与基调(如:都市、魔幻、战争、轻松、暗黑等)。
|
1) 用最多200字,写一个简洁明了的「当前情节短期摘要」。
|
||||||
• 主要写作风格(视角、叙述方式、语言风格等)。
|
2) 提炼「下一章」的关键字(例如关键物品、重要人物、地点、事件、情节等),可以用逗号分隔或条目列出。
|
||||||
• 世界观设定(背景时间、地理环境、社会结构、科技或魔法水平、重要历史等)。
|
|
||||||
• 整体故事框架(可提及常见叙事结构:三幕、英雄之旅等)。
|
|
||||||
• 主线与副线的初步构想,以及它们如何交织。
|
|
||||||
• 关键角色群像定位与主要冲突关系。
|
|
||||||
• 结局的可能方向(圆满、悲剧、开放式等)。
|
|
||||||
|
|
||||||
请以简洁、逻辑清晰的方式输出,保留足够细节以支撑后续创作。
|
请按如下格式输出(不需要额外解释):
|
||||||
|
短期摘要: <这里写短期摘要>
|
||||||
|
下一章关键字: <这里写下一章关键字>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 2. 角色设定 ===================
|
# =============== 1. 核心种子设定(雪花第1层)===================
|
||||||
character_prompt = """\
|
core_seed_prompt = """\
|
||||||
基于已生成的小说整体设定:
|
作为专业作家,请用"雪花写作法"第一步构建故事核心:
|
||||||
{novel_setting}
|
主题:{topic}
|
||||||
|
类型:{genre}
|
||||||
|
篇幅:约{number_of_chapters}章(每章{word_number}字)
|
||||||
|
|
||||||
请进一步扩展角色设置,至少包含三名核心角色。对每位角色说明:
|
请用单句公式概括故事本质,例如:
|
||||||
• 角色背景、外貌与主要性格特征。
|
"当[主角]遭遇[核心事件],必须[关键行动],否则[灾难后果];与此同时,[隐藏的更大危机]正在发酵。"
|
||||||
• 内在冲突、目标与动机。
|
|
||||||
• 暗藏的秘密或潜在弱点(可与世界观或其他角色有关)。
|
|
||||||
• 与其他角色的关系或对立点,如何推动或阻碍情节发展。
|
|
||||||
|
|
||||||
请重点突出角色的多重面向,为后续情节埋下伏笔。
|
要求:
|
||||||
|
1. 必须包含显性冲突与潜在危机
|
||||||
|
2. 体现人物核心驱动力
|
||||||
|
3. 暗示世界观关键矛盾
|
||||||
|
4. 使用25-100字精准表达
|
||||||
|
|
||||||
|
仅返回故事核心文本,不要解释任何内容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 3. 暗线与伏笔 ===================
|
# =============== 2. 角色动力学设定(角色弧光模型)===================
|
||||||
dark_lines_prompt = """\
|
character_dynamics_prompt = """\
|
||||||
在当前世界观与角色关系中:
|
基于核心种子:
|
||||||
{character_info}
|
{core_seed}
|
||||||
|
|
||||||
请构思若干暗线、伏笔或隐藏冲突,结合雪花写作法“细节逐步扩展”的思路。要求:
|
请设计3-6个具有动态变化潜力的核心角色,每个角色需包含:
|
||||||
• 每条暗线给出初始迹象、后续发展与可能的爆发条件。
|
特征:
|
||||||
• 与角色背景、世界观或关键事件相呼应。
|
- 背景、外貌、性别、年龄、职业等
|
||||||
• 保持合理的悬念设置,与现有设定不冲突。
|
- 暗藏的秘密或潜在弱点(可与世界观或其他角色有关)
|
||||||
• 为后续剧情保留足够展开空间,强调持续影响故事进程。
|
|
||||||
|
|
||||||
请避免一次性透漏全部细节,以逐步揭示的方式为后文做铺垫。
|
核心驱动力三角:
|
||||||
|
- 表面追求(物质目标)
|
||||||
|
- 深层渴望(情感需求)
|
||||||
|
- 灵魂需求(哲学层面)
|
||||||
|
|
||||||
|
角色弧线设计:
|
||||||
|
初始状态 → 触发事件 → 认知失调 → 蜕变节点 → 最终状态
|
||||||
|
|
||||||
|
关系冲突网:
|
||||||
|
- 与其他角色的关系或对立点
|
||||||
|
- 与至少两个其他角色的价值观冲突
|
||||||
|
- 一个合作纽带
|
||||||
|
- 一个隐藏的背叛可能性
|
||||||
|
|
||||||
|
要求:
|
||||||
|
仅给出最终文本,不要解释任何内容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 4. 最终设定整合 ===================
|
# =============== 3. 世界构建矩阵(三维度交织法)===================
|
||||||
finalize_setting_prompt = """\
|
world_building_prompt = """\
|
||||||
基于以下内容,请整合并输出终稿《小说设定》:
|
为服务核心冲突"{core_seed}",请构建三维交织的世界观:
|
||||||
1. 整体设定:
|
|
||||||
{novel_setting_base}
|
|
||||||
2. 角色设定:
|
|
||||||
{character_setting}
|
|
||||||
3. 暗线与伏笔:
|
|
||||||
{dark_lines}
|
|
||||||
|
|
||||||
整合要求:
|
1. 物理维度:
|
||||||
• 以整体视角整理世界观、角色与暗线,形成一个有机结合的故事框架。
|
- 空间结构(地理×社会阶层分布图)
|
||||||
• 注意角色动机与暗线如何与世界观互相呼应,并兼顾主线与副线节奏。
|
- 时间轴(关键历史事件年表)
|
||||||
• 语言通顺、层次分明。直接输出文本,不使用Markdown格式。
|
- 法则体系(物理/魔法/社会规则的漏洞点)
|
||||||
|
|
||||||
|
2. 社会维度:
|
||||||
|
- 权力结构断层线(可引发冲突的阶层/种族/组织矛盾)
|
||||||
|
- 文化禁忌(可被打破的禁忌及其后果)
|
||||||
|
- 经济命脉(资源争夺焦点)
|
||||||
|
|
||||||
|
3. 隐喻维度:
|
||||||
|
- 贯穿全书的视觉符号系统(如反复出现的意象)
|
||||||
|
- 氣候/环境变化映射的心理状态
|
||||||
|
- 建筑风格暗示的文明困境
|
||||||
|
|
||||||
|
要求:
|
||||||
|
每个维度至少包含3个可与角色决策产生互动的动态元素。
|
||||||
|
仅给出最终文本,不要解释任何内容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 5. 小说目录 ===================
|
# =============== 4. 情节架构(三幕式悬念)===================
|
||||||
novel_directory_prompt = """\
|
plot_architecture_prompt = """\
|
||||||
根据以下最终《小说设定》:
|
基于以下元素构建三幕式悬念架构:
|
||||||
{final_novel_setting}
|
核心种子:{core_seed}
|
||||||
|
角色体系:{character_dynamics}
|
||||||
|
世界观:{world_building}
|
||||||
|
|
||||||
请列出共 {number_of_chapters} 章的目录,并保证每章的标题或简述能呼应主要冲突、角色成长及暗线埋设。
|
要求按以下结构设计:
|
||||||
目录格式示例:
|
第一幕(触发)
|
||||||
第1章 :<标题> - <简要说明>
|
- 日常状态中的异常征兆(3处铺垫)
|
||||||
第2章 :<标题> - <简要说明>
|
- 引出故事:展示主线、暗线、副线的开端
|
||||||
...
|
- 关键事件:打破平衡的催化剂(需改变至少3个角色的关系)
|
||||||
第{number_of_chapters}章 :<标题> - <简要说明>
|
- 错误抉择:主角的认知局限导致的错误反应
|
||||||
|
|
||||||
每章可酌情加入一句简述,引导读者了解本章看点。直接输出文本,不使用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 = """\
|
summary_prompt = """\
|
||||||
以下是新完成的章节文本:
|
以下是新完成的章节文本:
|
||||||
{chapter_text}
|
{chapter_text}
|
||||||
@@ -88,10 +217,17 @@ summary_prompt = """\
|
|||||||
这是当前的全局摘要(可为空):
|
这是当前的全局摘要(可为空):
|
||||||
{global_summary}
|
{global_summary}
|
||||||
|
|
||||||
请根据本章新增内容,更新全局摘要。保留既有重要信息,同时融入新剧情要点,勿剧透未来。控制在不超过3000字的范围内,语言简练流畅。
|
请根据本章新增内容,更新全局摘要。
|
||||||
|
要求:
|
||||||
|
- 保留既有重要信息,同时融入新剧情要点
|
||||||
|
- 以简洁、连贯的语言描述全书进展
|
||||||
|
- 客观描绘,不展开联想或解释
|
||||||
|
- 字数控制在2000字以内
|
||||||
|
|
||||||
|
仅返回全局摘要文本,不要解释任何内容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 7. 角色状态更新 ===================
|
# =============== 7. 角色状态更新 ===================
|
||||||
update_character_state_prompt = """\
|
update_character_state_prompt = """\
|
||||||
以下是新完成的章节文本:
|
以下是新完成的章节文本:
|
||||||
{chapter_text}
|
{chapter_text}
|
||||||
@@ -99,45 +235,157 @@ update_character_state_prompt = """\
|
|||||||
这是当前的角色状态文档(可为空):
|
这是当前的角色状态文档(可为空):
|
||||||
{old_state}
|
{old_state}
|
||||||
|
|
||||||
请更新角色状态,内容包括:
|
请更新角色状态,内容格式:
|
||||||
• 角色的物品、能力或心理状态变化。
|
角色A属性:
|
||||||
• 角色间关系的最新进展或冲突。
|
├──物品:
|
||||||
• 是否触发或加深了某些暗线或关键事件。
|
├──某物(道具):描述
|
||||||
• 任何新增角色或临时出场人物的基本信息。
|
├──XX长剑(武器):描述
|
||||||
|
...
|
||||||
|
├──能力
|
||||||
|
├──技能1:描述
|
||||||
|
├──技能2:描述
|
||||||
|
...
|
||||||
|
├──状态
|
||||||
|
├──身体状态:
|
||||||
|
├──Buff/Debuff
|
||||||
|
├──心理状态:描述
|
||||||
|
|
||||||
|
├──主要角色间关系网
|
||||||
|
├──角色B:描述
|
||||||
|
├──角色C:描述
|
||||||
|
...
|
||||||
|
├──触发或加深的事件
|
||||||
|
├──事件1:描述
|
||||||
|
├──事件2:描述
|
||||||
|
...
|
||||||
|
|
||||||
请直接在已有文档基础上进行增删,语言尽量简洁、有条理。
|
角色B属性:
|
||||||
|
├──物品
|
||||||
|
├──...
|
||||||
|
├──能力
|
||||||
|
├──...
|
||||||
|
├──状态
|
||||||
|
├──...
|
||||||
|
├──主要角色间关系网
|
||||||
|
├──...
|
||||||
|
├──触发或加深的事件
|
||||||
|
├──...
|
||||||
|
|
||||||
|
角色C属性:
|
||||||
|
......
|
||||||
|
|
||||||
|
新出场角色:
|
||||||
|
- 任何新增角色或临时出场人物的基本信息
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- 请直接在已有文档基础上进行增删
|
||||||
|
- 不改变原有结构,语言尽量简洁、有条理
|
||||||
|
|
||||||
|
仅返回更新后的角色状态文本,不要解释任何内容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 8. 章节大纲设计 ===================
|
# =============== 8. 章节正文写作 ===================
|
||||||
chapter_outline_prompt = """\
|
|
||||||
这是当前小说的重要信息:
|
|
||||||
- 小说设定:{novel_setting}
|
|
||||||
- 角色状态:{character_state}
|
|
||||||
- 全局摘要:{global_summary}
|
|
||||||
|
|
||||||
我们即将写第 {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. 本章的主要冲突或情节目标,如何与标题呼应。
|
- 核心人物(可能未指定):{characters_involved}
|
||||||
2. 出场角色及其动机,对剧情走向的推动或阻碍。
|
- 关键道具(可能未指定):{key_items}
|
||||||
3. 暗线或伏笔如何有所进展或被揭示。
|
- 空间坐标(可能未指定):{scene_location}
|
||||||
4. 本章结尾的悬念或转折,如何为下一章做铺垫。
|
- 时间压力(可能未指定):{time_constraint}
|
||||||
|
|
||||||
请以简要分点方式输出,不要使用Markdown格式。
|
参考文档:
|
||||||
|
- 小说设定:
|
||||||
|
{novel_setting}
|
||||||
|
|
||||||
|
请完成第 {novel_number} 章的正文,至少设计下方2个具有动态张力的场景:
|
||||||
|
1. 对话场景:
|
||||||
|
- 潜台词冲突(表面谈论A,实际博弈B)
|
||||||
|
- 权力关系变化(通过非对称对话长度体现)
|
||||||
|
- 至少1处双关语暗示未来危机
|
||||||
|
|
||||||
|
2. 动作场景:
|
||||||
|
- 环境交互细节(至少3个感官描写)
|
||||||
|
- 节奏控制(短句加速+比喻减速)
|
||||||
|
- 动作揭示人物隐藏特质
|
||||||
|
|
||||||
|
3. 心理场景:
|
||||||
|
- 认知失调的具体表现(行为矛盾)
|
||||||
|
- 隐喻系统的运用(连接世界观符号)
|
||||||
|
- 决策前的价值天平描写
|
||||||
|
|
||||||
|
文末设置一个"钩链转折":结尾时回收旧悬念/创造新悬念/抛出新危机/颠覆某个认知/神转折等。
|
||||||
|
|
||||||
|
格式要求:
|
||||||
|
- 仅返回章节正文文本;
|
||||||
|
- 不使用分章节小标题;
|
||||||
|
- 不要使用markdown格式。
|
||||||
|
|
||||||
|
额外指导(可能未指定):{user_guidance}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# =============== 9. 章节正文写作 ===================
|
# 8.2 后续章节草稿提示
|
||||||
chapter_write_prompt = """\
|
next_chapter_draft_prompt = """\
|
||||||
以下信息供你参考:
|
参考文档:
|
||||||
1. 小说设定:{novel_setting}
|
- 小说设定:
|
||||||
2. 角色状态:{character_state}
|
{novel_setting}
|
||||||
3. 全局摘要:{global_summary}
|
|
||||||
4. 本章大纲:{chapter_outline}
|
|
||||||
|
|
||||||
请写出第 {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