update: 优化剧情分析与章节规划算法,集成伏笔上下文追踪;完善章节删除时的级联清理逻辑
This commit is contained in:
@@ -12,6 +12,7 @@ from app.models.outline import Outline
|
|||||||
from app.models.character import Character
|
from app.models.character import Character
|
||||||
from app.models.career import Career, CharacterCareer
|
from app.models.career import Career, CharacterCareer
|
||||||
from app.models.memory import StoryMemory
|
from app.models.memory import StoryMemory
|
||||||
|
from app.models.foreshadow import Foreshadow
|
||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -25,12 +26,14 @@ class ChapterContext:
|
|||||||
采用RTCO框架的分层设计:
|
采用RTCO框架的分层设计:
|
||||||
- P0-核心(必须):大纲、衔接点、字数要求
|
- P0-核心(必须):大纲、衔接点、字数要求
|
||||||
- P1-重要(按需):角色、情感基调、风格
|
- P1-重要(按需):角色、情感基调、风格
|
||||||
- P2-参考(条件触发):记忆、故事骨架、MCP资料
|
- P2-参考(条件触发):记忆、故事骨架、MCP资料、伏笔提醒
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# === P0-核心信息(必须包含)===
|
# === P0-核心信息(必须包含)===
|
||||||
chapter_outline: str = "" # 本章大纲
|
chapter_outline: str = "" # 本章大纲
|
||||||
continuation_point: Optional[str] = None # 衔接锚点(上一章结尾)
|
continuation_point: Optional[str] = None # 衔接锚点(增强版:含上一章摘要和结尾)
|
||||||
|
previous_chapter_summary: Optional[str] = None # 🔧 新增:上一章剧情摘要
|
||||||
|
previous_chapter_events: Optional[List[str]] = None # 🔧 新增:上一章关键事件
|
||||||
target_word_count: int = 3000 # 目标字数
|
target_word_count: int = 3000 # 目标字数
|
||||||
min_word_count: int = 2500 # 最小字数
|
min_word_count: int = 2500 # 最小字数
|
||||||
max_word_count: int = 4000 # 最大字数
|
max_word_count: int = 4000 # 最大字数
|
||||||
@@ -54,6 +57,7 @@ class ChapterContext:
|
|||||||
relevant_memories: Optional[str] = None # 相关记忆(精简版)
|
relevant_memories: Optional[str] = None # 相关记忆(精简版)
|
||||||
story_skeleton: Optional[str] = None # 故事骨架(50章+启用)
|
story_skeleton: Optional[str] = None # 故事骨架(50章+启用)
|
||||||
mcp_references: Optional[str] = None # MCP参考资料
|
mcp_references: Optional[str] = None # MCP参考资料
|
||||||
|
foreshadow_reminders: Optional[str] = None # 伏笔提醒(新增)
|
||||||
|
|
||||||
# === 元信息 ===
|
# === 元信息 ===
|
||||||
context_stats: Dict[str, Any] = field(default_factory=dict) # 统计信息
|
context_stats: Dict[str, Any] = field(default_factory=dict) # 统计信息
|
||||||
@@ -62,7 +66,8 @@ class ChapterContext:
|
|||||||
"""计算总上下文长度"""
|
"""计算总上下文长度"""
|
||||||
total = 0
|
total = 0
|
||||||
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
for field_name in ['chapter_outline', 'continuation_point', 'chapter_characters',
|
||||||
'relevant_memories', 'story_skeleton', 'style_instruction']:
|
'relevant_memories', 'story_skeleton', 'style_instruction',
|
||||||
|
'foreshadow_reminders', 'previous_chapter_summary']:
|
||||||
value = getattr(self, field_name, None)
|
value = getattr(self, field_name, None)
|
||||||
if value:
|
if value:
|
||||||
total += len(value)
|
total += len(value)
|
||||||
@@ -91,14 +96,16 @@ class ChapterContextBuilder:
|
|||||||
STYLE_MAX_LENGTH = 200 # 风格描述最大长度
|
STYLE_MAX_LENGTH = 200 # 风格描述最大长度
|
||||||
MAX_CONTEXT_LENGTH = 3000 # 总上下文最大字符数
|
MAX_CONTEXT_LENGTH = 3000 # 总上下文最大字符数
|
||||||
|
|
||||||
def __init__(self, memory_service=None):
|
def __init__(self, memory_service=None, foreshadow_service=None):
|
||||||
"""
|
"""
|
||||||
初始化构建器
|
初始化构建器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_service: 记忆服务实例(可选,用于检索相关记忆)
|
memory_service: 记忆服务实例(可选,用于检索相关记忆)
|
||||||
|
foreshadow_service: 伏笔服务实例(可选,用于获取伏笔提醒)
|
||||||
"""
|
"""
|
||||||
self.memory_service = memory_service
|
self.memory_service = memory_service
|
||||||
|
self.foreshadow_service = foreshadow_service
|
||||||
|
|
||||||
async def build(
|
async def build(
|
||||||
self,
|
self,
|
||||||
@@ -155,20 +162,28 @@ class ChapterContextBuilder:
|
|||||||
chapter, outline, project.outline_mode
|
chapter, outline, project.outline_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
# === 衔接锚点(根据章节调整长度)===
|
# === 衔接锚点(根据章节调整长度,增强版含摘要和事件)===
|
||||||
if chapter_number == 1:
|
if chapter_number == 1:
|
||||||
context.continuation_point = None
|
context.continuation_point = None
|
||||||
|
context.previous_chapter_summary = None
|
||||||
|
context.previous_chapter_events = None
|
||||||
logger.info(" ✅ 第1章无需衔接锚点")
|
logger.info(" ✅ 第1章无需衔接锚点")
|
||||||
elif chapter_number <= 10:
|
elif chapter_number <= 10:
|
||||||
context.continuation_point = await self._get_last_ending(
|
ending_info = await self._get_last_ending_enhanced(
|
||||||
chapter, db, self.ENDING_LENGTH_SHORT
|
chapter, db, self.ENDING_LENGTH_SHORT
|
||||||
)
|
)
|
||||||
logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符")
|
context.continuation_point = ending_info.get('ending_text')
|
||||||
|
context.previous_chapter_summary = ending_info.get('summary')
|
||||||
|
context.previous_chapter_events = ending_info.get('key_events')
|
||||||
|
logger.info(f" ✅ 衔接锚点(短): {len(context.continuation_point or '')}字符, 摘要: {len(context.previous_chapter_summary or '')}字符")
|
||||||
else:
|
else:
|
||||||
context.continuation_point = await self._get_last_ending(
|
ending_info = await self._get_last_ending_enhanced(
|
||||||
chapter, db, self.ENDING_LENGTH_NORMAL
|
chapter, db, self.ENDING_LENGTH_NORMAL
|
||||||
)
|
)
|
||||||
logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符")
|
context.continuation_point = ending_info.get('ending_text')
|
||||||
|
context.previous_chapter_summary = ending_info.get('summary')
|
||||||
|
context.previous_chapter_events = ending_info.get('key_events')
|
||||||
|
logger.info(f" ✅ 衔接锚点(标准): {len(context.continuation_point or '')}字符, 摘要: {len(context.previous_chapter_summary or '')}字符")
|
||||||
|
|
||||||
# === P1-重要信息 ===
|
# === P1-重要信息 ===
|
||||||
context.chapter_characters = await self._build_chapter_characters(
|
context.chapter_characters = await self._build_chapter_characters(
|
||||||
@@ -200,6 +215,14 @@ class ChapterContextBuilder:
|
|||||||
)
|
)
|
||||||
logger.info(f" ✅ 故事骨架: {len(context.story_skeleton or '')}字符")
|
logger.info(f" ✅ 故事骨架: {len(context.story_skeleton or '')}字符")
|
||||||
|
|
||||||
|
# === P2-伏笔提醒(新增)===
|
||||||
|
if self.foreshadow_service:
|
||||||
|
context.foreshadow_reminders = await self._get_foreshadow_reminders(
|
||||||
|
project.id, chapter_number, db
|
||||||
|
)
|
||||||
|
if context.foreshadow_reminders:
|
||||||
|
logger.info(f" ✅ 伏笔提醒: {len(context.foreshadow_reminders)}字符")
|
||||||
|
|
||||||
# === 统计信息 ===
|
# === 统计信息 ===
|
||||||
context.context_stats = {
|
context.context_stats = {
|
||||||
"chapter_number": chapter_number,
|
"chapter_number": chapter_number,
|
||||||
@@ -208,6 +231,7 @@ class ChapterContextBuilder:
|
|||||||
"characters_length": len(context.chapter_characters),
|
"characters_length": len(context.chapter_characters),
|
||||||
"memories_length": len(context.relevant_memories or ""),
|
"memories_length": len(context.relevant_memories or ""),
|
||||||
"skeleton_length": len(context.story_skeleton or ""),
|
"skeleton_length": len(context.story_skeleton or ""),
|
||||||
|
"foreshadow_length": len(context.foreshadow_reminders or ""),
|
||||||
"total_length": context.get_total_context_length()
|
"total_length": context.get_total_context_length()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +287,7 @@ class ChapterContextBuilder:
|
|||||||
max_length: int
|
max_length: int
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
获取上一章结尾内容作为衔接锚点
|
获取上一章结尾内容作为衔接锚点(旧版本,保留兼容性)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
chapter: 当前章节
|
chapter: 当前章节
|
||||||
@@ -294,6 +318,105 @@ class ChapterContextBuilder:
|
|||||||
|
|
||||||
return content[-max_length:]
|
return content[-max_length:]
|
||||||
|
|
||||||
|
async def _get_last_ending_enhanced(
|
||||||
|
self,
|
||||||
|
chapter: Chapter,
|
||||||
|
db: AsyncSession,
|
||||||
|
max_length: int
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取增强版衔接锚点(含上一章摘要和关键事件)
|
||||||
|
|
||||||
|
🔧 新增功能:
|
||||||
|
1. 提取上一章结尾文本
|
||||||
|
2. 获取上一章剧情摘要(从记忆或expansion_plan)
|
||||||
|
3. 提取上一章关键事件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chapter: 当前章节
|
||||||
|
db: 数据库会话
|
||||||
|
max_length: 最大长度
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含 ending_text, summary, key_events 的字典
|
||||||
|
"""
|
||||||
|
result_info = {
|
||||||
|
'ending_text': None,
|
||||||
|
'summary': None,
|
||||||
|
'key_events': []
|
||||||
|
}
|
||||||
|
|
||||||
|
if chapter.chapter_number <= 1:
|
||||||
|
return result_info
|
||||||
|
|
||||||
|
# 查询上一章
|
||||||
|
result = await db.execute(
|
||||||
|
select(Chapter)
|
||||||
|
.where(Chapter.project_id == chapter.project_id)
|
||||||
|
.where(Chapter.chapter_number == chapter.chapter_number - 1)
|
||||||
|
)
|
||||||
|
prev_chapter = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not prev_chapter:
|
||||||
|
return result_info
|
||||||
|
|
||||||
|
# 1. 提取结尾内容
|
||||||
|
if prev_chapter.content:
|
||||||
|
content = prev_chapter.content.strip()
|
||||||
|
if len(content) <= max_length:
|
||||||
|
result_info['ending_text'] = content
|
||||||
|
else:
|
||||||
|
result_info['ending_text'] = content[-max_length:]
|
||||||
|
|
||||||
|
# 2. 获取上一章摘要
|
||||||
|
# 优先从记忆中获取 chapter_summary
|
||||||
|
summary_result = await db.execute(
|
||||||
|
select(StoryMemory.content)
|
||||||
|
.where(StoryMemory.project_id == chapter.project_id)
|
||||||
|
.where(StoryMemory.chapter_id == prev_chapter.id)
|
||||||
|
.where(StoryMemory.memory_type == 'chapter_summary')
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
summary_mem = summary_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if summary_mem:
|
||||||
|
result_info['summary'] = summary_mem[:300] # 限制长度
|
||||||
|
elif prev_chapter.summary:
|
||||||
|
# 回退到章节的summary字段
|
||||||
|
result_info['summary'] = prev_chapter.summary[:300]
|
||||||
|
elif prev_chapter.expansion_plan:
|
||||||
|
# 再回退到expansion_plan中的plot_summary
|
||||||
|
try:
|
||||||
|
plan = json.loads(prev_chapter.expansion_plan)
|
||||||
|
result_info['summary'] = plan.get('plot_summary', '')[:300]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 提取上一章关键事件
|
||||||
|
if prev_chapter.expansion_plan:
|
||||||
|
try:
|
||||||
|
plan = json.loads(prev_chapter.expansion_plan)
|
||||||
|
key_events = plan.get('key_events', [])
|
||||||
|
if key_events:
|
||||||
|
result_info['key_events'] = key_events[:5] # 最多5个事件
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果没有从expansion_plan获取到,尝试从记忆中获取
|
||||||
|
if not result_info['key_events']:
|
||||||
|
events_result = await db.execute(
|
||||||
|
select(StoryMemory.content)
|
||||||
|
.where(StoryMemory.project_id == chapter.project_id)
|
||||||
|
.where(StoryMemory.chapter_id == prev_chapter.id)
|
||||||
|
.where(StoryMemory.memory_type == 'plot_point')
|
||||||
|
.limit(5)
|
||||||
|
)
|
||||||
|
event_mems = events_result.scalars().all()
|
||||||
|
if event_mems:
|
||||||
|
result_info['key_events'] = [e[:100] for e in event_mems]
|
||||||
|
|
||||||
|
return result_info
|
||||||
|
|
||||||
async def _build_chapter_characters(
|
async def _build_chapter_characters(
|
||||||
self,
|
self,
|
||||||
chapter: Chapter,
|
chapter: Chapter,
|
||||||
@@ -564,6 +687,48 @@ class ChapterContextBuilder:
|
|||||||
|
|
||||||
return "\n".join(lines) if lines else None
|
return "\n".join(lines) if lines else None
|
||||||
|
|
||||||
|
async def _get_foreshadow_reminders(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
chapter_number: int,
|
||||||
|
db: AsyncSession
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
获取伏笔提醒信息用于章节生成
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 获取计划在本章或之前回收但未回收的伏笔(超期提醒)
|
||||||
|
2. 获取已埋入且接近需要回收的伏笔(提前提醒)
|
||||||
|
3. 获取本章计划埋入的伏笔(埋入提醒)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: 项目ID
|
||||||
|
chapter_number: 当前章节号
|
||||||
|
db: 数据库会话
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化的伏笔提醒文本
|
||||||
|
"""
|
||||||
|
if not self.foreshadow_service:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
context_result = await self.foreshadow_service.build_chapter_context(
|
||||||
|
db=db,
|
||||||
|
project_id=project_id,
|
||||||
|
chapter_number=chapter_number,
|
||||||
|
include_pending=True,
|
||||||
|
include_overdue=True,
|
||||||
|
lookahead=5
|
||||||
|
)
|
||||||
|
|
||||||
|
context_text = context_result.get("context_text", "")
|
||||||
|
return context_text if context_text else None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 获取伏笔提醒失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
async def _build_story_skeleton(
|
async def _build_story_skeleton(
|
||||||
self,
|
self,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class PlotExpansionService:
|
|||||||
batch_size: int,
|
batch_size: int,
|
||||||
progress_callback: Optional[callable]
|
progress_callback: Optional[callable]
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""分批生成章节规划"""
|
"""分批生成章节规划(增强差异化版本)"""
|
||||||
# 计算批次数
|
# 计算批次数
|
||||||
total_batches = (target_chapter_count + batch_size - 1) // batch_size
|
total_batches = (target_chapter_count + batch_size - 1) // batch_size
|
||||||
logger.info(f"分批生成计划: 总共{target_chapter_count}章,分{total_batches}批,每批{batch_size}章")
|
logger.info(f"分批生成计划: 总共{target_chapter_count}章,分{total_batches}批,每批{batch_size}章")
|
||||||
@@ -184,6 +184,9 @@ class PlotExpansionService:
|
|||||||
|
|
||||||
all_chapter_plans = []
|
all_chapter_plans = []
|
||||||
|
|
||||||
|
# 🔧 收集所有已使用的关键事件,用于防止重复
|
||||||
|
used_key_events = set()
|
||||||
|
|
||||||
for batch_num in range(total_batches):
|
for batch_num in range(total_batches):
|
||||||
# 计算当前批次的章节数
|
# 计算当前批次的章节数
|
||||||
remaining_chapters = target_chapter_count - len(all_chapter_plans)
|
remaining_chapters = target_chapter_count - len(all_chapter_plans)
|
||||||
@@ -196,19 +199,40 @@ class PlotExpansionService:
|
|||||||
if progress_callback:
|
if progress_callback:
|
||||||
await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size)
|
await progress_callback(batch_num + 1, total_batches, current_start_index, current_batch_size)
|
||||||
|
|
||||||
# 构建当前批次的提示词(包含已生成章节的上下文)
|
# 🔧 增强的上下文构建(包含完整的差异化信息)
|
||||||
previous_context = ""
|
previous_context = ""
|
||||||
if all_chapter_plans:
|
if all_chapter_plans:
|
||||||
|
# 构建完整的已生成章节摘要(包含关键事件)
|
||||||
previous_summaries = []
|
previous_summaries = []
|
||||||
for ch in all_chapter_plans[-3:]: # 只显示最近3章
|
for ch in all_chapter_plans: # 显示所有已生成章节
|
||||||
|
key_events_str = "、".join(ch.get('key_events', [])[:3]) if ch.get('key_events') else "无"
|
||||||
previous_summaries.append(
|
previous_summaries.append(
|
||||||
f"第{ch['sub_index']}节《{ch['title']}》: {ch['plot_summary'][:100]}..."
|
f"第{ch['sub_index']}节《{ch['title']}》:\n"
|
||||||
|
f" - 剧情:{ch.get('plot_summary', '')[:150]}\n"
|
||||||
|
f" - 关键事件:{key_events_str}\n"
|
||||||
|
f" - 结尾方式:{ch.get('ending_type', '未知')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 提取所有已使用的关键事件
|
||||||
|
all_used_events = []
|
||||||
|
for ch in all_chapter_plans:
|
||||||
|
all_used_events.extend(ch.get('key_events', []))
|
||||||
|
used_events_str = "、".join(all_used_events[-20:]) if all_used_events else "暂无"
|
||||||
|
|
||||||
previous_context = f"""
|
previous_context = f"""
|
||||||
【已生成章节概要】(接续生成,注意衔接)
|
【🔴 已生成章节完整信息(必须参考以确保差异化)】
|
||||||
{chr(10).join(previous_summaries)}
|
{chr(10).join(previous_summaries)}
|
||||||
|
|
||||||
⚠️ 当前是第{current_start_index}-{current_start_index + current_batch_size - 1}节(共{target_chapter_count}节中的一部分)
|
【🔴 已使用的关键事件(本批次不可重复使用)】
|
||||||
|
{used_events_str}
|
||||||
|
|
||||||
|
【🔴 差异化强制要求】
|
||||||
|
⚠️ 当前是第{current_start_index}-{current_start_index + current_batch_size - 1}节(共{target_chapter_count}节中的第{batch_num + 1}批)
|
||||||
|
⚠️ 每个新章节必须有完全不同的:
|
||||||
|
1. 开场场景(不同地点/时间/人物状态)
|
||||||
|
2. 核心事件(不与已生成章节的关键事件重复)
|
||||||
|
3. 结尾悬念(不同类型的钩子)
|
||||||
|
⚠️ 新章节的key_events不得与上面【已使用的关键事件】中的任何事件相同或相似
|
||||||
"""
|
"""
|
||||||
# 获取自定义提示词模板
|
# 获取自定义提示词模板
|
||||||
template = await PromptService.get_template("OUTLINE_EXPAND_MULTI", project.user_id, db)
|
template = await PromptService.get_template("OUTLINE_EXPAND_MULTI", project.user_id, db)
|
||||||
@@ -501,7 +525,7 @@ class PlotExpansionService:
|
|||||||
ai_response: str,
|
ai_response: str,
|
||||||
outline_id: str
|
outline_id: str
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""解析AI的展开响应(使用统一的JSON清洗方法)"""
|
"""解析AI的展开响应(使用统一的JSON清洗方法,增强差异化字段)"""
|
||||||
try:
|
try:
|
||||||
# 使用统一的JSON清洗方法
|
# 使用统一的JSON清洗方法
|
||||||
cleaned_text = self.ai_service._clean_json_response(ai_response)
|
cleaned_text = self.ai_service._clean_json_response(ai_response)
|
||||||
@@ -513,11 +537,30 @@ class PlotExpansionService:
|
|||||||
if not isinstance(chapter_plans, list):
|
if not isinstance(chapter_plans, list):
|
||||||
chapter_plans = [chapter_plans]
|
chapter_plans = [chapter_plans]
|
||||||
|
|
||||||
# 为每个章节规划添加outline_id
|
# 为每个章节规划添加outline_id和差异化标识
|
||||||
for plan in chapter_plans:
|
for idx, plan in enumerate(chapter_plans):
|
||||||
plan["outline_id"] = outline_id
|
plan["outline_id"] = outline_id
|
||||||
|
|
||||||
logger.info(f"✅ 成功解析 {len(chapter_plans)} 个章节规划")
|
# 🔧 确保有 ending_type 字段(用于差异化追踪)
|
||||||
|
if "ending_type" not in plan:
|
||||||
|
# 根据叙事目标推断结尾类型
|
||||||
|
narrative_goal = plan.get("narrative_goal", "")
|
||||||
|
if "悬念" in narrative_goal or "疑问" in narrative_goal:
|
||||||
|
plan["ending_type"] = "悬念"
|
||||||
|
elif "冲突" in narrative_goal or "对抗" in narrative_goal:
|
||||||
|
plan["ending_type"] = "冲突升级"
|
||||||
|
elif "转折" in narrative_goal:
|
||||||
|
plan["ending_type"] = "情节转折"
|
||||||
|
elif "情感" in narrative_goal or "情绪" in narrative_goal:
|
||||||
|
plan["ending_type"] = "情感收尾"
|
||||||
|
else:
|
||||||
|
plan["ending_type"] = f"自然过渡-{idx + 1}"
|
||||||
|
|
||||||
|
# 🔧 确保 key_events 是列表且非空
|
||||||
|
if not plan.get("key_events"):
|
||||||
|
plan["key_events"] = [f"章节{idx + 1}核心事件"]
|
||||||
|
|
||||||
|
logger.info(f"✅ 成功解析 {len(chapter_plans)} 个章节规划(含差异化标识)")
|
||||||
return chapter_plans
|
return chapter_plans
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
@@ -533,6 +576,7 @@ class PlotExpansionService:
|
|||||||
"emotional_tone": "未知",
|
"emotional_tone": "未知",
|
||||||
"narrative_goal": "需要重新生成",
|
"narrative_goal": "需要重新生成",
|
||||||
"conflict_type": "未知",
|
"conflict_type": "未知",
|
||||||
|
"ending_type": "未知",
|
||||||
"estimated_words": 3000
|
"estimated_words": 3000
|
||||||
}]
|
}]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -547,6 +591,7 @@ class PlotExpansionService:
|
|||||||
"emotional_tone": "未知",
|
"emotional_tone": "未知",
|
||||||
"narrative_goal": "需要重新生成",
|
"narrative_goal": "需要重新生成",
|
||||||
"conflict_type": "未知",
|
"conflict_type": "未知",
|
||||||
|
"ending_type": "未知",
|
||||||
"estimated_words": 3000
|
"estimated_words": 3000
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|||||||
@@ -561,7 +561,14 @@ class PromptService:
|
|||||||
上一章结尾:
|
上一章结尾:
|
||||||
「{continuation_point}」
|
「{continuation_point}」
|
||||||
|
|
||||||
⚠️ 要求:从此处自然续写,不得重复上述内容
|
【🔴 上一章已完成剧情(禁止重复!)】
|
||||||
|
{previous_chapter_summary}
|
||||||
|
|
||||||
|
⚠️ 严重警告:
|
||||||
|
1. 上述"已完成剧情"和"衔接锚点"是**已经写过的**内容
|
||||||
|
2. 本章必须推进到**新的情节点**,绝对不能重新叙述已经发生的事件
|
||||||
|
3. 如果锚点是对话结束,请描写对话后的动作或场景转换,不要重复对话
|
||||||
|
4. 如果锚点是场景描写,请直接开始人物行动,不要重复描写环境
|
||||||
</continuation>
|
</continuation>
|
||||||
|
|
||||||
<characters priority="P1">
|
<characters priority="P1">
|
||||||
@@ -569,6 +576,11 @@ class PromptService:
|
|||||||
{characters_info}
|
{characters_info}
|
||||||
</characters>
|
</characters>
|
||||||
|
|
||||||
|
<foreshadow_reminders priority="P1">
|
||||||
|
【🎯 伏笔提醒 - 需关注】
|
||||||
|
{foreshadow_reminders}
|
||||||
|
</foreshadow_reminders>
|
||||||
|
|
||||||
<memory priority="P2">
|
<memory priority="P2">
|
||||||
【相关记忆 - 参考】
|
【相关记忆 - 参考】
|
||||||
{relevant_memories}
|
{relevant_memories}
|
||||||
@@ -585,13 +597,20 @@ class PromptService:
|
|||||||
✅ 自然承接上一章结尾,不重复已发生事件
|
✅ 自然承接上一章结尾,不重复已发生事件
|
||||||
✅ 保持角色性格、说话方式一致
|
✅ 保持角色性格、说话方式一致
|
||||||
✅ 字数控制在目标范围内
|
✅ 字数控制在目标范围内
|
||||||
|
✅ 如有伏笔提醒,请在本章中适当埋入或回收相应伏笔
|
||||||
|
|
||||||
|
【🔴 反重复特别指令】
|
||||||
|
✅ 检查本章开篇是否与"衔接锚点"内容重复
|
||||||
|
✅ 检查本章情节是否与"上一章已完成剧情"重复
|
||||||
|
✅ 确保本章推进到了大纲中规划的新事件
|
||||||
|
|
||||||
【禁止事项】
|
【禁止事项】
|
||||||
❌ 输出章节标题、序号等元信息
|
❌ 输出章节标题、序号等元信息
|
||||||
❌ 使用"总之"、"综上所述"等AI常见总结语
|
❌ 使用"总之"、"综上所述"等AI常见总结语
|
||||||
❌ 在结尾处使用开放式反问
|
❌ 在结尾处使用开放式反问
|
||||||
❌ 添加作者注释或创作说明
|
❌ 添加作者注释或创作说明
|
||||||
❌ 重复叙述上一章已发生的事件
|
❌ 重复叙述上一章已发生的事件(包括环境描写、心理活动)
|
||||||
|
❌ 在开篇使用"接上回"、"书接上文"等套话
|
||||||
</constraints>
|
</constraints>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
@@ -783,7 +802,7 @@ class PromptService:
|
|||||||
❌ 空泛的描述
|
❌ 空泛的描述
|
||||||
</constraints>"""
|
</constraints>"""
|
||||||
|
|
||||||
# 情节分析提示词 V2(RTCO框架)
|
# 情节分析提示词 V2(RTCO框架 + 伏笔ID追踪)
|
||||||
PLOT_ANALYSIS = """<system>
|
PLOT_ANALYSIS = """<system>
|
||||||
你是专业的小说编辑和剧情分析师,擅长深度剖析章节内容。
|
你是专业的小说编辑和剧情分析师,擅长深度剖析章节内容。
|
||||||
</system>
|
</system>
|
||||||
@@ -791,6 +810,12 @@ class PromptService:
|
|||||||
<task>
|
<task>
|
||||||
【分析任务】
|
【分析任务】
|
||||||
全面分析第{chapter_number}章《{title}》的剧情要素、钩子、伏笔、冲突和角色发展。
|
全面分析第{chapter_number}章《{title}》的剧情要素、钩子、伏笔、冲突和角色发展。
|
||||||
|
|
||||||
|
【🔴 伏笔追踪任务(重要)】
|
||||||
|
系统已提供【已埋入伏笔列表】,当你识别到章节中有回收伏笔时:
|
||||||
|
1. 必须从列表中找出对应的伏笔ID
|
||||||
|
2. 在 foreshadows 数组中使用 reference_foreshadow_id 字段关联
|
||||||
|
3. 如果无法确定是哪个伏笔,reference_foreshadow_id 填 null
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<chapter priority="P0">
|
<chapter priority="P0">
|
||||||
@@ -803,6 +828,13 @@ class PromptService:
|
|||||||
{content}
|
{content}
|
||||||
</chapter>
|
</chapter>
|
||||||
|
|
||||||
|
<existing_foreshadows priority="P1">
|
||||||
|
【已埋入伏笔列表 - 用于回收匹配】
|
||||||
|
以下是本项目中已埋入但尚未回收的伏笔,分析时如发现章节内容回收了某个伏笔,请使用对应的ID:
|
||||||
|
|
||||||
|
{existing_foreshadows}
|
||||||
|
</existing_foreshadows>
|
||||||
|
|
||||||
<analysis_framework priority="P0">
|
<analysis_framework priority="P0">
|
||||||
【分析维度】
|
【分析维度】
|
||||||
|
|
||||||
@@ -820,12 +852,26 @@ class PromptService:
|
|||||||
- 出现位置(开头/中段/结尾)
|
- 出现位置(开头/中段/结尾)
|
||||||
- **关键词**:【必填】从原文逐字复制8-25字的文本片段,用于精确定位
|
- **关键词**:【必填】从原文逐字复制8-25字的文本片段,用于精确定位
|
||||||
|
|
||||||
**2. 伏笔分析 (Foreshadowing)**
|
**2. 伏笔分析 (Foreshadowing) - 🔴 支持ID追踪**
|
||||||
- 埋下的新伏笔:内容、预期作用、隐藏程度(1-10)
|
- 埋下的新伏笔:内容、预期作用、隐藏程度(1-10)
|
||||||
- 回收的旧伏笔:呼应哪一章、回收效果
|
- 回收的旧伏笔:【必须】从已埋入伏笔列表中匹配ID
|
||||||
- 伏笔质量:巧妙性和合理性
|
- 伏笔质量:巧妙性和合理性
|
||||||
- **关键词**:【必填】从原文逐字复制8-25字
|
- **关键词**:【必填】从原文逐字复制8-25字
|
||||||
|
|
||||||
|
每个伏笔需要:
|
||||||
|
- **title**:简洁标题(10-20字,概括伏笔核心)
|
||||||
|
- **content**:详细描述伏笔内容和预期作用
|
||||||
|
- **type**:planted(埋下)或 resolved(回收)
|
||||||
|
- **strength**:强度1-10(对读者的吸引力)
|
||||||
|
- **subtlety**:隐藏度1-10(越高越隐蔽)
|
||||||
|
- **reference_chapter**:回收时引用的原埋入章节号,埋下时为null
|
||||||
|
- **reference_foreshadow_id**:【回收时必填】被回收伏笔的ID(从已埋入伏笔列表中选择),埋下时为null
|
||||||
|
- **keyword**:【必填】从原文逐字复制8-25字的定位文本
|
||||||
|
- **category**:分类(identity=身世/mystery=悬念/item=物品/relationship=关系/event=事件/ability=能力/prophecy=预言)
|
||||||
|
- **is_long_term**:是否长线伏笔(跨10章以上回收为true)
|
||||||
|
- **related_characters**:涉及的角色名列表
|
||||||
|
- **estimated_resolve_chapter**:预估回收章节号(埋下时预估,回收时为当前章节)
|
||||||
|
|
||||||
**3. 冲突分析 (Conflict)**
|
**3. 冲突分析 (Conflict)**
|
||||||
- 冲突类型:人与人/人与己/人与环境/人与社会
|
- 冲突类型:人与人/人与己/人与环境/人与社会
|
||||||
- 冲突各方及立场
|
- 冲突各方及立场
|
||||||
@@ -921,12 +967,32 @@ class PromptService:
|
|||||||
],
|
],
|
||||||
"foreshadows": [
|
"foreshadows": [
|
||||||
{{
|
{{
|
||||||
"content": "伏笔内容",
|
"title": "伏笔简洁标题",
|
||||||
|
"content": "伏笔详细内容和预期作用",
|
||||||
"type": "planted",
|
"type": "planted",
|
||||||
"strength": 7,
|
"strength": 7,
|
||||||
"subtlety": 8,
|
"subtlety": 8,
|
||||||
"reference_chapter": null,
|
"reference_chapter": null,
|
||||||
"keyword": "从原文逐字复制的8-25字文本"
|
"reference_foreshadow_id": null,
|
||||||
|
"keyword": "从原文逐字复制的8-25字文本",
|
||||||
|
"category": "mystery",
|
||||||
|
"is_long_term": false,
|
||||||
|
"related_characters": ["角色A", "角色B"],
|
||||||
|
"estimated_resolve_chapter": 15
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
"title": "回收的伏笔标题",
|
||||||
|
"content": "伏笔如何被回收的描述",
|
||||||
|
"type": "resolved",
|
||||||
|
"strength": 8,
|
||||||
|
"subtlety": 6,
|
||||||
|
"reference_chapter": 5,
|
||||||
|
"reference_foreshadow_id": "abc123-已埋入伏笔的ID",
|
||||||
|
"keyword": "从原文逐字复制的8-25字文本",
|
||||||
|
"category": "mystery",
|
||||||
|
"is_long_term": false,
|
||||||
|
"related_characters": ["角色A"],
|
||||||
|
"estimated_resolve_chapter": 10
|
||||||
}}
|
}}
|
||||||
],
|
],
|
||||||
"conflict": {{
|
"conflict": {{
|
||||||
@@ -998,6 +1064,7 @@ class PromptService:
|
|||||||
✅ 逐字复制:keyword必须从原文复制,长度8-25字
|
✅ 逐字复制:keyword必须从原文复制,长度8-25字
|
||||||
✅ 精确定位:keyword能在原文中精确找到
|
✅ 精确定位:keyword能在原文中精确找到
|
||||||
✅ 职业变化可选:仅当章节明确描述时填写
|
✅ 职业变化可选:仅当章节明确描述时填写
|
||||||
|
✅ 【伏笔ID追踪】回收伏笔时,必须从【已埋入伏笔列表】中查找匹配的ID填入 reference_foreshadow_id
|
||||||
|
|
||||||
【评分约束 - 严格执行】
|
【评分约束 - 严格执行】
|
||||||
✅ 严格按评分标准打分,支持小数(如6.5、7.2、8.3)
|
✅ 严格按评分标准打分,支持小数(如6.5、7.2、8.3)
|
||||||
@@ -2336,7 +2403,7 @@ class PromptService:
|
|||||||
"description": "基于前置章节内容创作新章节(用于第2章及以后)",
|
"description": "基于前置章节内容创作新章节(用于第2章及以后)",
|
||||||
"parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline",
|
"parameters": ["project_title", "genre", "chapter_number", "chapter_title", "chapter_outline",
|
||||||
"target_word_count", "narrative_perspective", "characters_info", "continuation_point",
|
"target_word_count", "narrative_perspective", "characters_info", "continuation_point",
|
||||||
"relevant_memories", "story_skeleton"]
|
"foreshadow_reminders", "relevant_memories", "story_skeleton", "previous_chapter_summary"]
|
||||||
},
|
},
|
||||||
"CHAPTER_REGENERATION_SYSTEM": {
|
"CHAPTER_REGENERATION_SYSTEM": {
|
||||||
"name": "章节重写系统提示",
|
"name": "章节重写系统提示",
|
||||||
|
|||||||
Reference in New Issue
Block a user