Files
novelmaster/noma/agents/checkers/reader_pull_checker.py
T
2026-06-23 20:29:02 +08:00

208 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Reader-Pull Checker (追读力检查器)
检查钩子强度、期待管理、追读力:
- 章节开头的钩子
- 章节结尾的悬念
- 追读力债务
"""
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
@dataclass
class ReaderPullIssue:
"""追读力问题"""
chapter: int
issue_type: str # weak_hook / no_ending_hook / debt_high
description: str
severity: str
@dataclass
class ReaderPullReport:
"""追读力检查报告"""
chapter: int
hook_strength: float # 0-1
ending_hook: bool
debt_level: float
issues: List[ReaderPullIssue] = field(default_factory=list)
passed: bool = True
summary: str = ""
class ReaderPullChecker:
"""
追读力检查器
检查:
1. 章节开头的钩子强度
2. 章节结尾的悬念
3. 追读力债务水平
"""
# 强钩子关键词
STRONG_HOOK_KEYWORDS = [
"危机", "危险", "困境", "生死", "抉择", "真相",
"震惊", "意外", "转折", "揭秘", "冲突", "大战"
]
# 结尾悬念关键词
ENDING_HOOK_KEYWORDS = [
"然而", "但是", "就在这时", "突然", "意外",
"未完待续", "敬请期待", "欲知后事", "悬念",
"却不知道", "更大的"
]
# 悬念关键词
SUSPENSE_KEYWORDS = [
"悬念", "伏笔", "暗示", "预示", "危机", "威胁"
]
def __init__(self):
self.chapter_debts: List[float] = []
def check_chapter(
self,
chapter_num: int,
chapter_text: str,
previous_debt: float = 0.0
) -> ReaderPullReport:
"""检查单章追读力"""
# 分析开头钩子
opening = chapter_text[:500] if len(chapter_text) > 500 else chapter_text
hook_strength = self._analyze_hook_strength(opening)
# 分析结尾悬念
ending = chapter_text[-500:] if len(chapter_text) > 500 else chapter_text
ending_hook = self._has_ending_hook(ending)
# 计算本章追读力债务
debt_level = self._calculate_debt_level(chapter_text, hook_strength, ending_hook)
report = ReaderPullReport(
chapter=chapter_num,
hook_strength=hook_strength,
ending_hook=ending_hook,
debt_level=debt_level
)
# 检测问题
if hook_strength < 0.3:
report.issues.append(ReaderPullIssue(
chapter=chapter_num,
issue_type="weak_hook",
description="章节开头钩子较弱,可能无法吸引读者",
severity="medium"
))
if not ending_hook:
report.issues.append(ReaderPullIssue(
chapter=chapter_num,
issue_type="no_ending_hook",
description="章节结尾缺少悬念,读者缺乏继续阅读的动力",
severity="high"
))
# 检查追读力债务
total_debt = previous_debt + debt_level
if total_debt > 80:
report.issues.append(ReaderPullIssue(
chapter=chapter_num,
issue_type="debt_high",
description=f"追读力债务过高({total_debt:.0f}),建议释放",
severity="critical"
))
self.chapter_debts.append(debt_level)
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
if report.passed:
report.summary = f"钩子强度 {hook_strength:.0%},追读力债务 {total_debt:.0f}"
else:
report.summary = f"发现问题: {', '.join(i.issue_type for i in report.issues)}"
return report
def _analyze_hook_strength(self, opening: str) -> float:
"""分析钩子强度"""
if not opening:
return 0.0
strength = 0.3 # 基础分
# 检查强钩子关键词
strong_count = sum(1 for kw in self.STRONG_HOOK_KEYWORDS if kw in opening)
strength += min(0.4, strong_count * 0.1)
# 检查疑问句
question_count = opening.count('?') + opening.count('')
strength += min(0.2, question_count * 0.1)
# 检查省略号(制造悬念)
ellipsis_count = opening.count('...')
strength += min(0.1, ellipsis_count * 0.05)
return min(1.0, strength)
def _has_ending_hook(self, ending: str) -> bool:
"""检查结尾是否有悬念"""
if not ending:
return False
# 检查结尾悬念关键词
for kw in self.ENDING_HOOK_KEYWORDS:
if kw in ending:
return True
# 检查是否以冲突/悬念结尾
suspense_ending = ["", "", "……"]
if any(ending.strip().endswith(s) for s in suspense_ending):
return True
return False
def _calculate_debt_level(
self,
text: str,
hook_strength: float,
ending_hook: bool
) -> float:
"""计算追读力债务"""
debt = 0.0
# 无开头钩子
if hook_strength < 0.3:
debt += 20
# 无结尾悬念
if not ending_hook:
debt += 30
# 积累的悬念/伏笔
unresolved_count = 0
for kw in self.SUSPENSE_KEYWORDS:
unresolved_count += text.count(kw)
debt += min(30, unresolved_count * 5)
# 钩子强度可以抵消部分债务
debt -= hook_strength * 20
return max(0, min(100, debt))
def get_arc_reader_pull(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
"""分析情节弧的追读力"""
if not self.chapter_debts:
return {"status": "no_data"}
relevant = self.chapter_debts[start_chapter - 1:end_chapter]
return {
"chapters": f"{start_chapter}-{end_chapter}",
"avg_hook_strength": sum(h.hook_strength for h in self.chapter_debts[start_chapter-1:end_chapter]) / len(relevant) if relevant else 0,
"avg_debt": sum(relevant) / len(relevant) if relevant else 0,
"debt_trend": relevant[-1] - relevant[0] if len(relevant) > 1 else 0
}