Merge pull request #67 from YILING0013/dev-2

任务与提示词流程优化
This commit is contained in:
Xianyun
2025-02-06 23:04:51 +08:00
committed by GitHub
9 changed files with 2216 additions and 1647 deletions
+120 -35
View File
@@ -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": ""
}
+120
View File
@@ -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}")
-59
View File
@@ -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
View File
@@ -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}")
+4 -3
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+341 -93
View File
@@ -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
View File
@@ -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"+
"o1100,000\n"+
"o1-mini65,536\n"+
"gpt-4o16384\n"+
"gpt-4o-mini16384\n"+
"deepseek-reasoner8192\n"+
"deepseek-chat4096\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": "本章剧情中涉及的时间压力或时限设置。"
}
+943 -994
View File
File diff suppressed because it is too large Load Diff