feat: initial commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Agents Checkers
|
||||
|
||||
审查器模块,包括 tension_checker 等。
|
||||
"""
|
||||
|
||||
from .tension_checker import (
|
||||
TensionChecker,
|
||||
TensionChecker,
|
||||
CatharsisModel,
|
||||
TensionLevel,
|
||||
TensionReading,
|
||||
TensionIssue,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TensionChecker",
|
||||
"CatharsisModel",
|
||||
"TensionLevel",
|
||||
"TensionReading",
|
||||
"TensionIssue",
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: consistency-checker
|
||||
description: 设定一致性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# consistency-checker (设定一致性检查器)
|
||||
|
||||
> **职责**: 设定守卫者,执行第二防幻觉定律(设定即物理)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 设定违规、战力冲突、逻辑不一致的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载参考资料
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(主角当前状态)
|
||||
3. `设定集/`(世界观圣经)
|
||||
4. `大纲/`(对照上下文)
|
||||
|
||||
### 第二步: 三层一致性检查
|
||||
|
||||
#### 第一层: 战力一致性(战力检查)
|
||||
|
||||
**校验项**:
|
||||
- Protagonist's current realm/level matches state.json
|
||||
- Abilities used are within realm limitations
|
||||
- Power-ups follow established progression rules
|
||||
|
||||
**危险信号** (POWER_CONFLICT):
|
||||
```
|
||||
❌ 主角筑基3层使用金丹期才能掌握的"破空斩"
|
||||
→ Realm: 筑基3 | Ability: 破空斩 (requires 金丹期)
|
||||
→ VIOLATION: Premature ability access
|
||||
|
||||
❌ 上章境界淬体9层,本章突然变成凝气5层(无突破描写)
|
||||
→ Previous: 淬体9 | Current: 凝气5 | Missing: Breakthrough scene
|
||||
→ VIOLATION: Unexplained power jump
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.power.realm`, `protagonist_state.power.layer`
|
||||
- 设定集/修炼体系.md: Realm ability restrictions
|
||||
|
||||
#### 第二层: 地点/角色一致性(地点/角色检查)
|
||||
|
||||
**校验项**:
|
||||
- Current location matches state.json or has valid travel sequence
|
||||
- Characters appearing are established in 设定集/ or tagged with `<entity/>`
|
||||
- Character attributes (appearance, personality, affiliations) match records
|
||||
|
||||
**危险信号** (LOCATION_ERROR / CHARACTER_CONFLICT):
|
||||
```
|
||||
❌ 上章在"天云宗",本章突然出现在"千里外的血煞秘境"(无移动描写)
|
||||
→ Previous location: 天云宗 | Current: 血煞秘境 | Distance: 1000+ li
|
||||
→ VIOLATION: Teleportation without explanation
|
||||
|
||||
❌ 李雪上次是"筑基期修为",本章变成"练气期"(无解释)
|
||||
→ Character: 李雪 | Previous: 筑基期 | Current: 练气期
|
||||
→ VIOLATION: Power regression unexplained
|
||||
```
|
||||
|
||||
**校验依据**:
|
||||
- state.json: `protagonist_state.location.current`
|
||||
- 设定集/角色卡/: Character profiles
|
||||
|
||||
#### 第三层: 时间线一致性(时间线检查)
|
||||
|
||||
**校验项**:
|
||||
- Event sequence is chronologically logical
|
||||
- Time-sensitive elements (deadlines, age, seasonal events) align
|
||||
- Flashbacks are clearly marked
|
||||
- Chapter time anchors match volume timeline
|
||||
|
||||
**Severity Classification** (时间问题分级):
|
||||
| 问题类型 | Severity | 说明 |
|
||||
|---------|----------|------|
|
||||
| 倒计时算术错误 | **critical** | D-5 直接跳到 D-2,必须修复 |
|
||||
| 事件先后矛盾 | **high** | 先发生的事情后写,逻辑混乱 |
|
||||
| 年龄/修炼时长冲突 | **high** | 算术错误,如15岁修炼5年却10岁入门 |
|
||||
| 时间回跳无标注 | **high** | 非闪回章节却出现时间倒退 |
|
||||
| 大跨度无过渡 | **high** | 跨度>3天却无过渡说明 |
|
||||
| 时间锚点缺失 | **medium** | 无法确定章节时间,但不影响逻辑 |
|
||||
| 轻微时间模糊 | **low** | 时段不明确但不影响剧情 |
|
||||
|
||||
> 输出 JSON 时,`issues[].severity` 必须使用小写枚举:`critical|high|medium|low`。
|
||||
|
||||
**危险信号** (TIMELINE_ISSUE):
|
||||
```
|
||||
❌ [critical] 第10章物资耗尽倒计时 D-5,第11章直接变成 D-2(跳过3天)
|
||||
→ Setup: D-5 | Next chapter: D-2 | Missing: 3 days
|
||||
→ VIOLATION: Countdown arithmetic error (MUST FIX)
|
||||
|
||||
❌ [high] 第10章提到"三天后的宗门大比",第11章描述大比结束(中间无时间流逝)
|
||||
→ Setup: 3 days until event | Next chapter: Event concluded
|
||||
→ VIOLATION: Missing time passage
|
||||
|
||||
❌ [high] 主角15岁修炼5年,推算应该10岁开始,但设定集记录"12岁入门"
|
||||
→ Age: 15 | Cultivation years: 5 | Start age: 10 | Record: 12
|
||||
→ VIOLATION: Timeline arithmetic error
|
||||
|
||||
❌ [high] 第一章末世降临,第二章就建立帮派(无时间过渡)
|
||||
→ Chapter 1: 末世第1天 | Chapter 2: 建帮派火拼
|
||||
→ VIOLATION: Major event without reasonable time progression
|
||||
|
||||
❌ [high] 本章时间锚点"末世第3天",上章是"末世第5天"(时间回跳)
|
||||
→ Previous: 末世第5天 | Current: 末世第3天
|
||||
→ VIOLATION: Time regression without flashback marker
|
||||
```
|
||||
|
||||
### 第三步: 实体一致性检查
|
||||
|
||||
**对所有章节中检测到的新实体**:
|
||||
1. Check if they contradict existing settings
|
||||
2. Assess if their introduction is consistent with world-building
|
||||
3. Verify power levels are reasonable for the current arc
|
||||
|
||||
**报告不一致的新增实体**:
|
||||
```
|
||||
⚠️ 发现设定冲突:
|
||||
- 第46章出现"紫霄宗",与设定集中势力分布矛盾
|
||||
→ 建议: 确认是否为新势力或笔误
|
||||
```
|
||||
|
||||
### 第四步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 设定一致性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 战力一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {N} | ✓ 无违规 | - | - |
|
||||
| {M} | ✗ POWER_CONFLICT | high | 主角筑基3层使用金丹期技能"破空斩" |
|
||||
|
||||
**结论**: 发现 {X} 处违规
|
||||
|
||||
## 地点/角色一致性
|
||||
| 章节 | 类型 | 问题 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 地点 | ✗ LOCATION_ERROR | medium | 未描述移动过程,从天云宗跳跃到血煞秘境 |
|
||||
|
||||
**结论**: 发现 {Y} 处违规
|
||||
|
||||
## 时间线一致性
|
||||
| 章节 | 问题 | 严重度 | 详情 |
|
||||
|------|------|--------|------|
|
||||
| {M} | ✗ TIMELINE_ISSUE | critical | 倒计时从 D-5 跳到 D-2 |
|
||||
| {M} | ✗ TIMELINE_ISSUE | high | 大比倒计时逻辑不一致 |
|
||||
|
||||
**结论**: 发现 {Z} 处违规
|
||||
**严重时间线问题**: {count} 个(必须修复后才能继续)
|
||||
|
||||
## 新实体一致性检查
|
||||
- ✓ 与世界观一致的新实体: {count}
|
||||
- ⚠️ 不一致的实体: {count}(详见下方列表)
|
||||
- ❌ 矛盾实体: {count}
|
||||
|
||||
**不一致列表**:
|
||||
1. 第{M}章:"紫霄宗"(势力)- 与现有势力分布矛盾
|
||||
2. 第{M}章:"天雷果"(物品)- 效果与力量体系不符
|
||||
|
||||
## 修复建议
|
||||
- [战力冲突] 润色时修改第{M}章,将"破空斩"替换为筑基期可用技能
|
||||
- [地点错误] 润色时补充移动过程描述或调整地点设定
|
||||
- [时间线问题] 润色时统一时间线推算,修正矛盾
|
||||
- [实体冲突] 润色时确认是否为新设定或需要调整
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
**严重违规**: {count}(必须修复)
|
||||
**轻微问题**: {count}(建议修复)
|
||||
```
|
||||
|
||||
### 第五步: 标记无效事实(新增)
|
||||
|
||||
对于发现的严重级别(`critical`)问题,自动标记到 `invalid_facts`(状态为 `pending`):
|
||||
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "{PROJECT_ROOT}" index mark-invalid \
|
||||
--source-type entity \
|
||||
--source-id {entity_id} \
|
||||
--reason "{问题描述}" \
|
||||
--marked-by consistency-checker \
|
||||
--chapter {current_chapter}
|
||||
```
|
||||
|
||||
> 注意:自动标记仅为 `pending`,需用户确认后才生效。
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在 POWER_CONFLICT(战力崩坏)的章节
|
||||
❌ 忽略未标记的新实体
|
||||
❌ 接受无世界观解释的瞬移
|
||||
❌ **降低 TIMELINE_ISSUE 严重度**(时间问题不得降级)
|
||||
❌ **通过存在严重/高优先级时间线问题的章节**(必须修复)
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重违规(战力冲突、无解释的角色变化、**时间线算术错误**)
|
||||
- 0 个高优先级时间线问题(**倒计时错误、时间回跳、重大事件无时间推进**)
|
||||
- 所有新实体与现有世界观一致
|
||||
- 地点和时间线过渡合乎逻辑
|
||||
- 报告为润色步骤提供具体修复建议
|
||||
@@ -0,0 +1,455 @@
|
||||
"""
|
||||
Consistency Checker (设定一致性检查器)
|
||||
|
||||
检查设定一致性:
|
||||
- 战力等级一致性
|
||||
- 地点/角色一致性
|
||||
- 时间线一致性
|
||||
|
||||
输出结构化报告。
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IssueSeverity(Enum):
|
||||
"""问题严重度"""
|
||||
CRITICAL = "critical" # 必须修复
|
||||
HIGH = "high" # 强烈建议修复
|
||||
MEDIUM = "medium" # 建议修复
|
||||
LOW = "low" # 可忽略
|
||||
|
||||
|
||||
class IssueType(Enum):
|
||||
"""问题类型"""
|
||||
POWER_CONFLICT = "power_conflict" # 战力冲突
|
||||
LOCATION_ERROR = "location_error" # 地点错误
|
||||
CHARACTER_CONFLICT = "character_conflict" # 角色状态冲突
|
||||
TIMELINE_ISSUE = "timeline_issue" # 时间线问题
|
||||
NEW_ENTITY_CONFLICT = "new_entity_conflict" # 新实体与设定冲突
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyIssue:
|
||||
"""一致性问题"""
|
||||
issue_type: IssueType
|
||||
chapter: int
|
||||
description: str
|
||||
severity: IssueSeverity
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsistencyReport:
|
||||
"""一致性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ConsistencyIssue] = field(default_factory=list)
|
||||
power_issues_count: int = 0
|
||||
location_issues_count: int = 0
|
||||
timeline_issues_count: int = 0
|
||||
entity_issues_count: int = 0
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ConsistencyChecker:
|
||||
"""
|
||||
设定一致性检查器
|
||||
|
||||
检查范围:
|
||||
1. 战力一致性 - 境界/技能是否匹配
|
||||
2. 地点一致性 - 位置切换是否有过渡
|
||||
3. 时间线一致性 - 事件顺序/时间流逝
|
||||
4. 角色一致性 - 角色状态变化是否合理
|
||||
"""
|
||||
|
||||
# 境界关键词
|
||||
REALM_PATTERNS = [
|
||||
(r"凡人|淬体|炼气|筑基|金丹|元婴|化神|渡劫|大乘|真仙", "realm"),
|
||||
(r"一层|二层|三层|四层|五层|六层|七层|八层|九层|十层", "layer"),
|
||||
]
|
||||
|
||||
# 地点转移关键词
|
||||
LOCATION_TRANSITION_KEYWORDS = [
|
||||
"来到", "前往", "到达", "回到", "离开", "返回",
|
||||
"穿越", "瞬移", "飞行", "走了", "赶到"
|
||||
]
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = Path(project_root) if project_root else None
|
||||
self.state: Dict[str, Any] = {}
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""加载项目状态"""
|
||||
self.state = state
|
||||
|
||||
def load_settings(self, settings: Dict[str, Any]) -> None:
|
||||
"""加载设定集"""
|
||||
self.settings = settings
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_location: Optional[str] = None
|
||||
) -> ConsistencyReport:
|
||||
"""
|
||||
检查单章一致性
|
||||
|
||||
Args:
|
||||
chapter_num: 章节号
|
||||
chapter_text: 章节正文
|
||||
previous_location: 上一章位置
|
||||
|
||||
Returns:
|
||||
ConsistencyReport
|
||||
"""
|
||||
report = ConsistencyReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 战力检查
|
||||
power_issues = self._check_power_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(power_issues)
|
||||
report.power_issues_count = len(power_issues)
|
||||
|
||||
# 2. 地点检查
|
||||
location_issues = self._check_location_consistency(
|
||||
chapter_num, chapter_text, previous_location
|
||||
)
|
||||
report.issues.extend(location_issues)
|
||||
report.location_issues_count = len(location_issues)
|
||||
|
||||
# 3. 时间线检查
|
||||
timeline_issues = self._check_timeline_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(timeline_issues)
|
||||
report.timeline_issues_count = len(timeline_issues)
|
||||
|
||||
# 4. 新实体冲突检查
|
||||
entity_issues = self._check_new_entity_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(entity_issues)
|
||||
report.entity_issues_count = len(entity_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
|
||||
# 生成摘要
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def check_range(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
chapters: Dict[int, str],
|
||||
locations: Dict[int, str]
|
||||
) -> ConsistencyReport:
|
||||
"""检查章节区间"""
|
||||
report = ConsistencyReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
previous_location = None
|
||||
for ch in range(start_chapter, end_chapter + 1):
|
||||
if ch in chapters:
|
||||
chapter_text = chapters[ch]
|
||||
previous_loc = locations.get(ch - 1) if ch > start_chapter else None
|
||||
|
||||
ch_report = self.check_chapter(ch, chapter_text, previous_loc)
|
||||
report.issues.extend(ch_report.issues)
|
||||
report.power_issues_count += ch_report.power_issues_count
|
||||
report.location_issues_count += ch_report.location_issues_count
|
||||
report.timeline_issues_count += ch_report.timeline_issues_count
|
||||
report.entity_issues_count += ch_report.entity_issues_count
|
||||
|
||||
if ch in locations:
|
||||
previous_location = locations[ch]
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
report.passed = critical_count == 0
|
||||
report.summary = self._generate_summary(report)
|
||||
|
||||
return report
|
||||
|
||||
def _check_power_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查战力一致性"""
|
||||
issues = []
|
||||
|
||||
# 从 state 获取当前境界
|
||||
current_realm = self.state.get("protagonist_state", {}).get("power", {}).get("realm", "")
|
||||
current_layer = self.state.get("protagonist_state", {}).get("power", {}).get("level", 0)
|
||||
|
||||
# 检测本章提到的境界
|
||||
detected_realms = self._extract_realms(text)
|
||||
|
||||
# 检查是否使用了超出境界的技能
|
||||
for realm_match in detected_realms:
|
||||
realm = realm_match["realm"]
|
||||
layer = realm_match.get("layer", 0)
|
||||
|
||||
# 境界跳跃检测(无突破描写)
|
||||
if current_realm and realm != current_realm:
|
||||
# 检查是否有过渡描写
|
||||
transition_keywords = ["突破", "晋升", "进阶", "升级", "渡劫", "闭关"]
|
||||
has_transition = any(kw in text for kw in transition_keywords)
|
||||
|
||||
if not has_transition:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.POWER_CONFLICT,
|
||||
chapter=chapter,
|
||||
description=f"境界从 {current_realm} 变化到 {realm},但无突破描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_realm": current_realm,
|
||||
"current_realm": realm,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加突破场景或修正境界描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_realms(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""提取文本中的境界信息"""
|
||||
realms = []
|
||||
|
||||
realm_names = [
|
||||
"凡人", "淬体", "炼气", "筑基", "金丹", "元婴",
|
||||
"化神", "渡劫", "大乘", "真仙"
|
||||
]
|
||||
|
||||
for realm in realm_names:
|
||||
if realm in text:
|
||||
# 尝试提取层次
|
||||
layer_match = re.search(rf'{realm}(\d+)层', text)
|
||||
layer = int(layer_match.group(1)) if layer_match else 0
|
||||
|
||||
realms.append({
|
||||
"realm": realm,
|
||||
"layer": layer,
|
||||
"position": text.index(realm)
|
||||
})
|
||||
|
||||
return realms
|
||||
|
||||
def _check_location_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_location: Optional[str]
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查地点一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测本章地点
|
||||
current_location = self._extract_location(text)
|
||||
|
||||
if not previous_location:
|
||||
return issues
|
||||
|
||||
if not current_location:
|
||||
return issues
|
||||
|
||||
# 检查是否是同一地点
|
||||
if current_location != previous_location:
|
||||
# 检查是否有转移描写
|
||||
has_transition = any(kw in text for kw in self.LOCATION_TRANSITION_KEYWORDS)
|
||||
|
||||
# 检查是否在附近
|
||||
is_nearby = self._is_location_nearby(current_location, previous_location)
|
||||
|
||||
if not has_transition and not is_nearby:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.LOCATION_ERROR,
|
||||
chapter=chapter,
|
||||
description=f"从「{previous_location}」跳跃到「{current_location}」,无过渡描写",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_location": previous_location,
|
||||
"current_location": current_location,
|
||||
"has_transition": has_transition
|
||||
},
|
||||
suggested_fix="添加移动/传送过渡场景"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _extract_location(self, text: str) -> Optional[str]:
|
||||
"""提取当前地点"""
|
||||
# 常见地点指示词
|
||||
patterns = [
|
||||
r"在(.+?)的",
|
||||
r"来到(.+?)的",
|
||||
r"位于(.+?)的",
|
||||
r"到了(.+?),",
|
||||
r"【(.+?)】",
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text)
|
||||
if match:
|
||||
location = match.group(1).strip()
|
||||
if location and len(location) < 20:
|
||||
return location
|
||||
|
||||
# 检查分割线后的地点
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('---'):
|
||||
continue
|
||||
if '【' in line and '】' in line:
|
||||
start = line.index('【')
|
||||
end = line.index('】')
|
||||
return line[start+1:end]
|
||||
|
||||
return None
|
||||
|
||||
def _is_location_nearby(self, loc1: str, loc2: str) -> bool:
|
||||
"""判断两地点是否相近"""
|
||||
# 简化实现:检查是否有相同关键词
|
||||
keywords1 = set(loc1)
|
||||
keywords2 = set(loc2)
|
||||
overlap = keywords1 & keywords2
|
||||
return len(overlap) >= 2
|
||||
|
||||
def _check_timeline_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查时间线一致性"""
|
||||
issues = []
|
||||
|
||||
# 检测时间描述
|
||||
time_patterns = [
|
||||
(r'第(\d+)天', 'day'),
|
||||
(r'(\d+)日后', 'future_day'),
|
||||
(r'(\d+)天后', 'future_day'),
|
||||
(r'(\d+)年前', 'past'),
|
||||
(r'D-(\d+)', 'countdown'),
|
||||
(r'倒计时(\d+)天', 'countdown'),
|
||||
]
|
||||
|
||||
detected_times = []
|
||||
for pattern, time_type in time_patterns:
|
||||
matches = re.finditer(pattern, text)
|
||||
for match in matches:
|
||||
value = int(match.group(1))
|
||||
detected_times.append({
|
||||
"type": time_type,
|
||||
"value": value,
|
||||
"position": match.start()
|
||||
})
|
||||
|
||||
# 检查时间回跳
|
||||
if len(detected_times) >= 2:
|
||||
for i in range(len(detected_times) - 1):
|
||||
t1 = detected_times[i]
|
||||
t2 = detected_times[i + 1]
|
||||
|
||||
# 检查 day 类型的时间回跳
|
||||
if t1["type"] == "day" and t2["type"] == "day":
|
||||
if t2["value"] < t1["value"]:
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description=f"时间回跳:从第{t1['value']}天到第{t2['value']}天",
|
||||
severity=IssueSeverity.HIGH,
|
||||
details={
|
||||
"previous_time": t1["value"],
|
||||
"current_time": t2["value"]
|
||||
},
|
||||
suggested_fix="确认是否为闪回场景,或修正时间线"
|
||||
))
|
||||
|
||||
# 检查倒计时逻辑
|
||||
countdowns = [t for t in detected_times if t["type"] == "countdown"]
|
||||
if len(countdowns) >= 2:
|
||||
values = [t["value"] for t in countdowns]
|
||||
if values != sorted(values, reverse=True):
|
||||
issues.append(ConsistencyIssue(
|
||||
issue_type=IssueType.TIMELINE_ISSUE,
|
||||
chapter=chapter,
|
||||
description="倒计时数字递减错误",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
details={"countdowns": values},
|
||||
suggested_fix="修正倒计时数值"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_new_entity_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ConsistencyIssue]:
|
||||
"""检查新引入实体是否与设定冲突"""
|
||||
issues = []
|
||||
|
||||
# 从设定中获取已知势力
|
||||
known_factions = self.settings.get("factions", [])
|
||||
known_locations = self.settings.get("locations", [])
|
||||
|
||||
# 检测新出现的地点/势力
|
||||
new_mentions = re.findall(r'[""'']([^""'']{2,10})[""'']', text)
|
||||
|
||||
for mention in new_mentions:
|
||||
# 检查是否与已知势力冲突
|
||||
for faction in known_factions:
|
||||
if mention in faction.get("name", ""):
|
||||
# 检查是否有冲突描述
|
||||
conflict_keywords = ["对立", "敌对", "矛盾", "冲突"]
|
||||
if any(kw in text for kw in conflict_keywords):
|
||||
# 可能存在设定冲突
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_summary(self, report: ConsistencyReport) -> str:
|
||||
"""生成报告摘要"""
|
||||
total_issues = len(report.issues)
|
||||
critical = sum(1 for i in report.issues if i.severity == IssueSeverity.CRITICAL)
|
||||
high = sum(1 for i in report.issues if i.severity == IssueSeverity.HIGH)
|
||||
|
||||
if report.passed:
|
||||
return f"检查通过。发现 {total_issues} 个问题({critical} 严重,{high} 高优先级)"
|
||||
else:
|
||||
return f"检查未通过。发现 {total_issues} 个问题({critical} 严重必须修复)"
|
||||
|
||||
def export_report(self, report: ConsistencyReport) -> Dict[str, Any]:
|
||||
"""导出报告为字典"""
|
||||
return {
|
||||
"start_chapter": report.start_chapter,
|
||||
"end_chapter": report.end_chapter,
|
||||
"passed": report.passed,
|
||||
"summary": report.summary,
|
||||
"issues_count": {
|
||||
"power": report.power_issues_count,
|
||||
"location": report.location_issues_count,
|
||||
"timeline": report.timeline_issues_count,
|
||||
"entity": report.entity_issues_count,
|
||||
"total": len(report.issues)
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"type": i.issue_type.value,
|
||||
"chapter": i.chapter,
|
||||
"description": i.description,
|
||||
"severity": i.severity.value,
|
||||
"details": i.details,
|
||||
"suggested_fix": i.suggested_fix
|
||||
}
|
||||
for i in report.issues
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
---
|
||||
name: continuity-checker
|
||||
description: 连贯性检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# continuity-checker (连贯性检查器)
|
||||
|
||||
> **职责**: 叙事流守卫者,确保场景过渡顺畅、情节线连贯、逻辑一致。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 场景过渡、情节线、伏笔管理、逻辑流的连贯性分析。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. 前 2-3 章(过渡上下文)
|
||||
3. `大纲/`(对照大纲 - 大纲即法律)
|
||||
4. `{project_root}/..noma/novel_data/state.json`(情节线追踪器,若存在)
|
||||
|
||||
### 第二步: 四层连贯性检查
|
||||
|
||||
#### 第一层: 场景转换流畅度(场景转换)
|
||||
|
||||
**检查项**:
|
||||
```
|
||||
❌ Abrupt Transition:
|
||||
上一段:林天在天云宗大殿与长老对话
|
||||
下一段:林天已经在血煞秘境深处战斗
|
||||
问题:缺少移动过程/时间流逝描写
|
||||
|
||||
✓ Smooth Transition:
|
||||
上一段:林天告别长老,离开宗门
|
||||
过渡句:"三日后,林天抵达血煞秘境入口"
|
||||
下一段:林天在秘境中遭遇妖兽
|
||||
```
|
||||
|
||||
**过渡质量评级**:
|
||||
- **A**: 自然过渡 + 时间/空间标记清晰
|
||||
- **B**: 有过渡但略显生硬
|
||||
- **C**: 缺少过渡,靠读者推测
|
||||
- **F**: 完全断裂,逻辑跳跃
|
||||
|
||||
#### 第二层: 情节线连贯(情节线连贯)
|
||||
|
||||
**追踪活跃情节线**:
|
||||
- **Main Thread** (主线): 当前核心任务/目标
|
||||
- **Sub-threads** (支线): 次要任务、悬念、铺垫
|
||||
|
||||
**检查项**:
|
||||
- Threads introduced but never resolved (烂尾)
|
||||
- Threads resolved without proper setup (突兀)
|
||||
- Threads forgotten mid-story (遗忘)
|
||||
|
||||
**示例分析**:
|
||||
```
|
||||
第40章引入: "宗门大比将在10天后举行"(主线)
|
||||
第45章: 大比正在进行中 ✓
|
||||
第50章: 大比结束,主角获胜 ✓
|
||||
判定:✓ 线索完整,有始有终
|
||||
|
||||
vs.
|
||||
|
||||
第30章引入: "血煞门即将入侵"(支线伏笔)
|
||||
第31-50章: 完全未提及血煞门
|
||||
判定:⚠️ 线索悬空,可能遗忘或拖得太久
|
||||
```
|
||||
|
||||
#### 第三层: 伏笔管理(伏笔管理)
|
||||
|
||||
**伏笔分类**:
|
||||
| Type | Setup → Payoff Gap | Risk |
|
||||
|------|-------------------|------|
|
||||
| **Short-term** (短期) | 1-3 章 | Low |
|
||||
| **Mid-term** (中期) | 4-10 章 | Medium (容易被遗忘) |
|
||||
| **Long-term** (长期) | 10+ 章 | High (需明确标记) |
|
||||
|
||||
**危险信号**:
|
||||
第10章: "林天发现神秘玉佩,似乎隐藏秘密"
|
||||
第11-30章: 玉佩再未提及
|
||||
判定:⚠️ 伏笔遗忘风险,建议第31章回收或再次提及
|
||||
|
||||
✓ Proper Payoff:
|
||||
第10章: "李雪提到师父曾去过血煞秘境"
|
||||
第25章: "在秘境中发现李雪师父留下的线索"
|
||||
判定:✓ 伏笔回收合理,间隔15章属于中期伏笔
|
||||
```
|
||||
|
||||
**伏笔检查清单**:
|
||||
- [ ] 所有设置的伏笔是否在合理章节内回收?
|
||||
- [ ] 长期伏笔(10+章)是否定期提及以保持读者记忆?
|
||||
- [ ] 回收时是否自然,不生硬?
|
||||
|
||||
#### 第四层: 逻辑流畅性(逻辑流畅性)
|
||||
|
||||
**检查情节漏洞与逻辑不一致**:
|
||||
|
||||
```
|
||||
❌ Logic Hole:
|
||||
第45章: 主角说"我从未见过这种妖兽"
|
||||
第30章: 主角曾击败同种妖兽
|
||||
判定:❌ 前后矛盾,需修正
|
||||
|
||||
❌ Causality Break:
|
||||
第46章: 主角突然获得神秘力量
|
||||
问题: 无解释来源,违反"发明需申报"原则
|
||||
判定:❌ 缺少因果关系,需补充 `<entity/>` 或铺垫
|
||||
|
||||
✓ Logical:
|
||||
第44章: 主角服用聚气丹(铺垫)
|
||||
第45章: 主角突破境界(因果)
|
||||
判定:✓ 因果清晰
|
||||
```
|
||||
|
||||
### 第三步: 大纲一致性检查(大纲即法律)
|
||||
|
||||
**将章节与大纲对照**:
|
||||
|
||||
```
|
||||
大纲第45章: "主角参加宗门大比,对战王少,险胜"
|
||||
|
||||
实际第45章内容:
|
||||
- ✓ 主角参加大比
|
||||
- ✓ 对战王少
|
||||
- ✗ 结果是"轻松碾压"而非"险胜"
|
||||
|
||||
判定:⚠️ 偏离大纲(难度降低),需确认是否有意调整
|
||||
```
|
||||
|
||||
**偏差处理**:
|
||||
- **轻微**(细节优化): 可接受
|
||||
- **中等**(情节调整): 需标记并确认
|
||||
- **重大**(核心冲突变化): 必须标记 `<deviation reason="..."/>` 并说明
|
||||
|
||||
### 第四步: 拖沓检查(拖沓检查)
|
||||
|
||||
**识别拖沓段落**:
|
||||
```
|
||||
⚠️ Possible Drag:
|
||||
第45-46章: 两章都在描述"主角赶路"
|
||||
内容: 重复的风景描写,无关键事件
|
||||
判定:⚠️ 节奏拖沓,建议:
|
||||
- 压缩为1章
|
||||
- 或在赶路途中安排事件(遭遇/奇遇/思考)
|
||||
|
||||
✓ Efficient Pacing:
|
||||
第47章: "三日后,主角抵达秘境"(一句带过)
|
||||
判定:✓ 有效省略无关紧要的过程
|
||||
```
|
||||
|
||||
### 第五步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 连贯性检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 场景转换评分
|
||||
| 转换 | 从 → 到 | 评级 | 问题 |
|
||||
|------|---------|------|------|
|
||||
| 第{N}章→第{M}章 | 天云宗大殿 → 血煞秘境 | C | 缺少移动过程描写 |
|
||||
|
||||
**场景转换总评**: {平均评级}
|
||||
|
||||
## 情节线追踪
|
||||
| 情节线 | 引入 | 最近提及 | 状态 | 下一步 |
|
||||
|--------|------|---------|------|--------|
|
||||
| 宗门大比 | 第40章 | 第46章(结束)| ✓ 已解决 | - |
|
||||
| 血煞门入侵 | 第30章 | 第30章 | ⚠️ 休眠(16章未提及)| 建议第47章提及或回收 |
|
||||
| 神秘玉佩 | 第10章 | 第10章 | ⚠️ 遗忘(36章未提及)| 建议回收或删除伏笔 |
|
||||
|
||||
**活跃情节线**: {count}
|
||||
**休眠/遗忘**: {count}
|
||||
|
||||
## 伏笔管理
|
||||
| 设置 | 章节 | 类型 | 兑现 | 间隔 | 状态 |
|
||||
|------|------|------|------|------|------|
|
||||
| 李雪师父去过秘境 | 10 | 中期 | 第25章发现线索 | 15章 | ✓ 已回收 |
|
||||
| 神秘玉佩 | 10 | 长期 | 未回收 | 36章+ | ❌ 遗忘风险 |
|
||||
|
||||
**伏笔健康度**: {X} 已回收, {Y} 待处理, {Z} 有风险
|
||||
|
||||
## 逻辑一致性
|
||||
| 章节 | 问题 | 类型 | 严重度 |
|
||||
|------|------|------|--------|
|
||||
| {M} | 前后矛盾(主角称"从未见过"但第30章遇见过)| 前后矛盾 | high |
|
||||
| {M} | 突然获得力量无解释 | 因果缺失 | medium |
|
||||
|
||||
**发现逻辑漏洞**: {count}
|
||||
|
||||
## 大纲一致性
|
||||
| 章节 | 大纲 | 实际 | 偏差程度 |
|
||||
|------|------|------|---------|
|
||||
| {M} | 险胜王少 | 轻松碾压 | ⚠️ 中等(难度调整)|
|
||||
|
||||
**偏差数**: {count}({X} 轻微, {Y} 中等, {Z} 重大)
|
||||
|
||||
## 节奏拖沓检查
|
||||
- ⚠️ 第{N}-{M}章: 两章赶路场景重复,建议压缩或增加事件
|
||||
|
||||
## 修复建议
|
||||
1. **修复场景转换**: 第{M}章添加"三日后"等时间标记
|
||||
2. **回收遗忘伏笔**: 神秘玉佩已36章未提及,建议回收或回溯删除
|
||||
3. **解决逻辑矛盾**: 第{M}章修改"从未见过"为"很少见到"
|
||||
4. **提及休眠线索**: 血煞门入侵线索建议第47章再次提及
|
||||
5. **压缩拖沓段落**: 第{N}-{M}章赶路场景合并为1章
|
||||
|
||||
## 综合评分
|
||||
**连贯性总评**: {流畅/可接受/生硬/断裂}
|
||||
**严重问题**: {count}(必须修复)
|
||||
**改进建议**: {count}(建议改进)
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在重大大纲偏差且无 `<deviation/>` 标记的章节
|
||||
❌ 忽略遗忘伏笔(10+ 章休眠)
|
||||
❌ 接受突兀的场景转换(F 级)
|
||||
❌ 忽视情节漏洞和前后矛盾
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 所有场景转换评级 ≥ B
|
||||
- 无活跃情节线遗忘超过 15 章
|
||||
- 所有长期伏笔已追踪并有兑现计划
|
||||
- 0 个重大逻辑漏洞
|
||||
- 大纲偏差已正确标记
|
||||
- 报告指出需修复的具体章节
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Continuity Checker (连续性检查器)
|
||||
|
||||
检查场景与叙事连贯性:
|
||||
- 场景切换是否平滑
|
||||
- 视角是否一致
|
||||
- 叙事节奏是否连贯
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityIssue:
|
||||
"""连续性问题"""
|
||||
chapter: int
|
||||
issue_type: str # scene_transition / perspective_shift / info_gap
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuityReport:
|
||||
"""连续性检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
issues: List[ContinuityIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class ContinuityChecker:
|
||||
"""
|
||||
场景与叙事连续性检查器
|
||||
|
||||
检查:
|
||||
1. 场景切换是否平滑
|
||||
2. 视角是否一致
|
||||
3. 信息是否连贯
|
||||
"""
|
||||
|
||||
# 场景切换关键词
|
||||
SCENE_TRANSITION_KEYWORDS = [
|
||||
"与此同时", "镜头一转", "画面切换", "时间流逝",
|
||||
"与此同时", "另一边", "镜头回到", "视角转向"
|
||||
]
|
||||
|
||||
# 视角关键词
|
||||
PERSPECTIVE_PATTERNS = [
|
||||
(r"他(她)看", "third_person"),
|
||||
(r"我(你)", "first_person"),
|
||||
(r"叶尘(主角名)", "main_character"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.current_perspective = "third_person"
|
||||
self.previous_scene_ending = ""
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
previous_ending: Optional[str] = None
|
||||
) -> ContinuityReport:
|
||||
"""检查单章连续性"""
|
||||
report = ContinuityReport(start_chapter=chapter_num, end_chapter=chapter_num)
|
||||
|
||||
# 1. 检查场景切换平滑度
|
||||
transition_issues = self._check_scene_transitions(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(transition_issues)
|
||||
|
||||
# 2. 检查视角一致性
|
||||
perspective_issues = self._check_perspective_consistency(chapter_num, chapter_text)
|
||||
report.issues.extend(perspective_issues)
|
||||
|
||||
# 3. 检查信息缺口
|
||||
info_gap_issues = self._check_info_gaps(chapter_num, chapter_text, previous_ending)
|
||||
report.issues.extend(info_gap_issues)
|
||||
|
||||
# 综合判定
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"发现 {len(report.issues)} 个连续性问题"
|
||||
|
||||
return report
|
||||
|
||||
def _check_scene_transitions(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查场景切换"""
|
||||
issues = []
|
||||
|
||||
# 检查分割线后的场景切换
|
||||
scenes = text.split('---')
|
||||
if len(scenes) > 2: # 多场景
|
||||
# 检查开头是否有场景说明
|
||||
first_scene = scenes[1] if len(scenes) > 1 else ""
|
||||
if first_scene.strip() and not any(kw in first_scene[:50] for kw in ["此时", "这里", "地点"]):
|
||||
issues.append(ContinuityIssue(
|
||||
chapter=chapter,
|
||||
issue_type="scene_transition",
|
||||
description="场景切换无过渡说明",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_perspective_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查视角一致性"""
|
||||
issues = []
|
||||
|
||||
# 简化:只检测是否有明显的视角混乱
|
||||
# 实际实现需要更复杂的 NLP
|
||||
|
||||
return issues
|
||||
|
||||
def _check_info_gaps(
|
||||
self,
|
||||
chapter: int,
|
||||
text: str,
|
||||
previous_ending: Optional[str]
|
||||
) -> List[ContinuityIssue]:
|
||||
"""检查信息缺口"""
|
||||
issues = []
|
||||
|
||||
# 检查是否承接上文
|
||||
if previous_ending:
|
||||
# 简化:检查是否有承接关键词
|
||||
carry_over_keywords = ["继续", "接着", "与此同时", "然而", "但是"]
|
||||
has_carry_over = any(kw in text[:100] for kw in carry_over_keywords)
|
||||
|
||||
if not has_carry_over and len(text) > 500:
|
||||
# 可能存在信息缺口
|
||||
pass # 简化处理
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: high-point-checker
|
||||
description: 爽点密度检查,支持迪化误解/身份掉马模式,输出结构化报告
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# high-point-checker (爽点检查器)
|
||||
|
||||
> **职责**: 读者满足感机制的质量保障专家(爽点设计)。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 爽点密度、类型覆盖、执行质量的结构化报告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载目标章节
|
||||
|
||||
读取指定范围内 `正文/` 目录下的所有章节。
|
||||
|
||||
### 第二步: 识别爽点
|
||||
|
||||
扫描 **8 种标准执行模式**:
|
||||
|
||||
| 模式 | 特征关键词 | 最低要求 |
|
||||
|------|-----------------|---------------------|
|
||||
| **装逼打脸** | 嘲讽/废物/不屑 → 反转/震惊/目瞪口呆 | 铺垫 + 反转 + 反应 |
|
||||
| **扮猪吃虎** | 示弱/隐藏实力 → 碾压 | 隐藏 + 轻视 + 碾压 |
|
||||
| **越级反杀** | 实力差距 → 以弱胜强 → 震撼 | 展示差距 + 策略/爆发 + 反转 |
|
||||
| **打脸权威** | 权威/前辈/强者 → 主角挑战成功 | 建立权威 + 挑战 + 成功 |
|
||||
| **反派翻车** | 反派得意/阴谋 → 计划失败/被反杀 | 反派铺垫 + 主角反制 + 翻车 |
|
||||
| **甜蜜超预期** | 期待/心动 → 超预期表现 → 情感升华 | 期待 + 超越期待 + 情绪 |
|
||||
| **迪化误解** | 主角随意行为 → 配角脑补升华 → 读者优越感 | 随意行为 + 信息差 + 误解 + 读者优越 |
|
||||
| **身份掉马** | 身份伪装 → 关键时刻揭露 → 周围震惊 | 隐藏(长期)+ 触发事件 + 揭露 + 群体反应 |
|
||||
|
||||
### 第二步补充: 迪化误解模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 主角随意行为(无心插柳)
|
||||
2. 配角信息差(不知道主角真实情况)
|
||||
3. 配角脑补升华(合理化主角行为)
|
||||
4. 读者优越感(我知道真相)
|
||||
|
||||
**识别信号**:
|
||||
- "竟然"/"难道"/"莫非" + 配角内心戏
|
||||
- 主角行为与配角解读的反差
|
||||
- 读者视角知道真相
|
||||
|
||||
**质量评估**:
|
||||
- A级:脑补合理,读者优越感强
|
||||
- B级:脑补尚可,效果一般
|
||||
- C级:脑补太刻意,配角显得蠢
|
||||
|
||||
### 第二步补充: 身份掉马模式检测
|
||||
|
||||
**核心结构**:
|
||||
1. 身份伪装(需长期铺垫)
|
||||
2. 关键时刻(危机/高光)
|
||||
3. 身份揭露(意外或主动)
|
||||
4. 周围反应(震惊/后悔/敬畏)
|
||||
|
||||
**识别信号**:
|
||||
- 身份相关词汇(真实身份/原来是/竟然是)
|
||||
- 周围角色大规模反应
|
||||
- 前后反差描写
|
||||
|
||||
**质量评估**:
|
||||
- A级:有长期铺垫,反应层次丰富
|
||||
- B级:有铺垫,反应单一
|
||||
- C级:无铺垫,突兀
|
||||
- F级:硬编身份,逻辑矛盾
|
||||
|
||||
### 第三步: 密度检查
|
||||
|
||||
**推荐基线(滚动窗口)**:
|
||||
- **Per chapter**: 优先有爽点或同等兑现;允许过渡章低密度
|
||||
- **Every 5 chapters**: 建议 ≥ 1 组合爽点(2种模式叠加)
|
||||
- **Every 10-15 chapters**: 建议 ≥ 1 里程碑爽点(改变主角地位)
|
||||
|
||||
**输出**:
|
||||
```
|
||||
第 X 章: [✓ 2 个爽点] 或 [△ 0 个爽点 - 连续出现时需预警]
|
||||
```
|
||||
|
||||
### 第四步: 类型多样性检查
|
||||
|
||||
**反单调要求**: 审查范围内单一类型不得超过 80%。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
Chapters 1-2:
|
||||
- 装逼打脸: 3 (75%) ✓
|
||||
- 越级反杀: 1 (25%)
|
||||
Mode diversity: Acceptable
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 45-46:
|
||||
- 装逼打脸: 7 (87.5%) ✗ OVER-RELIANCE
|
||||
- 扮猪吃虎: 1 (12.5%)
|
||||
Mode diversity: Warning - Monotonous pacing
|
||||
```
|
||||
|
||||
### 第五步: 执行质量评估
|
||||
|
||||
对每个已识别的爽点,检查:
|
||||
|
||||
1. **铺垫充分性**: 是否有充分的前期铺垫(至少1-2章)?
|
||||
2. **反转冲击**: 转折是否出人意料又合乎逻辑?
|
||||
3. **情绪回报**: 是否实现了读者情绪释放?
|
||||
4. **30/40/30 参考结构**: 结构是否清晰(不要求严格比例)?
|
||||
- 30% 铺垫蓄势
|
||||
- 40% 兑现执行
|
||||
- 30% 微反转/余波
|
||||
5. **压扬比例**: 是否匹配题材?
|
||||
- 传统爽文: 压3扬7
|
||||
- 硬核正剧: 压5扬5
|
||||
- 虐恋文: 压7扬3
|
||||
|
||||
**质量评级**:
|
||||
- **A(优秀)**: 所有标准达标,执行有力,结构清晰
|
||||
- **B(良好)**: 多数标准达标,可能有轻微比例问题
|
||||
- **C(及格)**: 基本标准达标但结构偏弱
|
||||
- **F(失败)**: 爽点缺少铺垫突然出现,或逻辑不一致
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 爽点检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 密度检查
|
||||
- 第 {N} 章: ✓ 2 个爽点(装逼打脸 + 越级反杀)
|
||||
- 第 {M} 章: △ 0 个爽点 **[预警 - 连续出现时需补强]**
|
||||
|
||||
**结论**: {通过/预警/未通过}(基于滚动窗口)
|
||||
|
||||
## 类型分布
|
||||
- 装逼打脸: {count}({percent}%)
|
||||
- 扮猪吃虎: {count}({percent}%)
|
||||
- 越级反杀: {count}({percent}%)
|
||||
- 打脸权威: {count}({percent}%)
|
||||
- 反派翻车: {count}({percent}%)
|
||||
- 甜蜜超预期: {count}({percent}%)
|
||||
|
||||
**结论**: {通过/预警}(单一类型 > 80% 时有单调风险)
|
||||
|
||||
## 质量评级
|
||||
| 章节 | 爽点 | 模式 | 评级 | 30/40/30 | 压扬比 | 问题 |
|
||||
|------|------|------|------|---------|--------|------|
|
||||
| {N} | 主角被嘲讽后一招秒杀对手 | 装逼打脸 | A | ✓ | 压3扬7 | - |
|
||||
| {M} | 突然顿悟突破境界 | 越级反杀 | C | ✗ | 压1扬9 | 缺少铺垫,压扬比失衡 |
|
||||
|
||||
**结论**: 平均评级 = {X}
|
||||
|
||||
## 修复建议
|
||||
- [密度预警] 第 {M} 章低密度,建议补{mode}型爽点或同等兑现
|
||||
- [单调风险] 过度依赖{mode}型,建议增加{other_modes}
|
||||
- [质量问题] 第 {M} 章的爽点执行不足,需要补充{missing_element}
|
||||
- [结构偏弱] 爽点结构偏弱,建议补铺垫/兑现/余波中的缺项
|
||||
- [压扬比问题] 压扬比例不符合{genre}类型,建议调整为{ratio}
|
||||
|
||||
## 综合评分
|
||||
**结论**: {通过/未通过} - {简要说明}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 忽略连续低密度章节且不预警
|
||||
❌ 忽略缺乏铺垫的突发爽点
|
||||
❌ 通过连续 5+ 章同类型爽点
|
||||
❌ 迪化误解中配角智商明显下线
|
||||
❌ 身份掉马无任何前期暗示
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 滚动窗口密度保持健康(不连续低密度)
|
||||
- 类型分布显示多样性(单一类型不超过 80%)
|
||||
- 平均质量评级 ≥ B
|
||||
- 迪化误解的脑补需合理
|
||||
- 身份掉马需有铺垫
|
||||
- 报告包含可执行的修复建议
|
||||
|
||||
## 输出格式增强
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "high-point-checker",
|
||||
"chapter": 45,
|
||||
"overall_score": 86,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"metrics": {
|
||||
"cool_point_count": 2,
|
||||
"cool_point_types": ["迪化误解", "身份掉马"],
|
||||
"density_score": 8,
|
||||
"type_diversity": 0.9,
|
||||
"milestone_present": false,
|
||||
"monotony_risk": false
|
||||
},
|
||||
"summary": "爽点密度达标,类型分布健康,执行质量稳定。"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
High-Point Checker (爽点检查器)
|
||||
|
||||
检查爽点密度与质量:
|
||||
- 爽点出现频率
|
||||
- 爽点强度
|
||||
- 爽点类型分布
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointIssue:
|
||||
"""爽点问题"""
|
||||
chapter: int
|
||||
issue_type: str # low_density / weak_intensity / type_imbalance
|
||||
description: str
|
||||
severity: str
|
||||
suggested: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HighPointReport:
|
||||
"""爽点检查报告"""
|
||||
chapter: int
|
||||
hot_spots_count: int
|
||||
avg_intensity: float
|
||||
dominant_type: str
|
||||
issues: List[HighPointIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class HighPointChecker:
|
||||
"""
|
||||
爽点检查器
|
||||
|
||||
检查:
|
||||
1. 爽点密度(每千字爽点数)
|
||||
2. 爽点强度
|
||||
3. 爽点类型分布
|
||||
"""
|
||||
|
||||
# 爽点关键词
|
||||
HOT_SPOT_PATTERNS = {
|
||||
"overkill_reversal": [
|
||||
"碾压", "秒杀", "一击", "一招", "不堪一击",
|
||||
"跪下", "颤抖", "惊恐", "脸色大变", "难以置信"
|
||||
],
|
||||
"taboo_transgression": [
|
||||
"禁忌", "僭越", "危险", "邪魅", "诱惑",
|
||||
"堕落", "黑化", "疯狂", "失控", "暴走"
|
||||
],
|
||||
"cognitive_closure": [
|
||||
"原来", "竟然", "真相", "恍然大悟",
|
||||
"所有一切", "早该", "早就在", "伏笔", "埋下"
|
||||
],
|
||||
"payoff": [
|
||||
"收获", "突破", "晋升", "提升", "获得",
|
||||
"机缘", "惊喜", "传承", "觉醒", "解封"
|
||||
]
|
||||
}
|
||||
|
||||
# 每千字爽点期望值
|
||||
EXPECTED_PER_1000_WORDS = 2.0 # 每千字2个爽点
|
||||
|
||||
def __init__(self):
|
||||
self.history: List[HighPointReport] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[str] = None
|
||||
) -> HighPointReport:
|
||||
"""检查单章爽点"""
|
||||
word_count = len(chapter_text)
|
||||
|
||||
# 统计各类爽点
|
||||
type_counts = {t: 0 for t in self.HOT_SPOT_PATTERNS}
|
||||
type_positions = {t: [] for t in self.HOT_SPOT_PATTERNS}
|
||||
|
||||
for hot_type, keywords in self.HOT_SPOT_PATTERNS.items():
|
||||
for keyword in keywords:
|
||||
start = 0
|
||||
while True:
|
||||
pos = chapter_text.find(keyword, start)
|
||||
if pos == -1:
|
||||
break
|
||||
type_counts[hot_type] += 1
|
||||
type_positions[hot_type].append(pos)
|
||||
start = pos + 1
|
||||
|
||||
# 计算总爽点数
|
||||
total_hot_spots = sum(type_counts.values())
|
||||
|
||||
# 计算爽点密度
|
||||
density = (total_hot_spots / word_count) * 1000 if word_count > 0 else 0
|
||||
|
||||
# 确定主导类型
|
||||
dominant_type = max(type_counts, key=type_counts.get) if type_counts else "none"
|
||||
dominant_count = type_counts.get(dominant_type, 0)
|
||||
|
||||
# 估算平均强度
|
||||
avg_intensity = min(1.0, density / self.EXPECTED_PER_1000_WORDS)
|
||||
|
||||
# 生成报告
|
||||
report = HighPointReport(
|
||||
chapter=chapter_num,
|
||||
hot_spots_count=total_hot_spots,
|
||||
avg_intensity=avg_intensity,
|
||||
dominant_type=dominant_type
|
||||
)
|
||||
|
||||
# 检测问题
|
||||
if density < self.EXPECTED_PER_1000_WORDS * 0.5:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="low_density",
|
||||
description=f"爽点密度偏低({density:.1f}/千字),建议增加爽点",
|
||||
severity="high"
|
||||
))
|
||||
|
||||
if total_hot_spots == 0:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="no_hotspot",
|
||||
description="未检测到明显爽点",
|
||||
severity="critical"
|
||||
))
|
||||
|
||||
# 爽点类型过于单一
|
||||
if dominant_count > 0 and dominant_count == total_hot_spots:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="type_imbalance",
|
||||
description=f"爽点类型单一,全部为 {dominant_type}",
|
||||
severity="medium",
|
||||
suggested="建议混合多种爽点类型增加层次感"
|
||||
))
|
||||
|
||||
# 检查是否与 catharsis_model 匹配
|
||||
if catharsis_model and dominant_type != catharsis_model:
|
||||
report.issues.append(HighPointIssue(
|
||||
chapter=chapter_num,
|
||||
issue_type="model_mismatch",
|
||||
description=f"爽点类型与目标模型不匹配(期望 {catharsis_model},实际 {dominant_type})",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
report.passed = len([i for i in report.issues if i.severity == "critical"]) == 0
|
||||
report.summary = f"检测到 {total_hot_spots} 个爽点,密度 {density:.1f}/千字,主导类型: {dominant_type}"
|
||||
|
||||
self.history.append(report)
|
||||
return report
|
||||
|
||||
def get_arc_hotspot_analysis(self, start_chapter: int, end_chapter: int) -> Dict[str, Any]:
|
||||
"""分析情节弧的爽点分布"""
|
||||
relevant = [r for r in self.history if start_chapter <= r.chapter <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return {"status": "no_data"}
|
||||
|
||||
total_spots = sum(r.hot_spots_count for r in relevant)
|
||||
avg_intensity = sum(r.avg_intensity for r in relevant) / len(relevant)
|
||||
|
||||
type_distribution = {}
|
||||
for r in relevant:
|
||||
t = r.dominant_type
|
||||
type_distribution[t] = type_distribution.get(t, 0) + 1
|
||||
|
||||
return {
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"total_hot_spots": total_spots,
|
||||
"avg_intensity": avg_intensity,
|
||||
"type_distribution": type_distribution,
|
||||
"issues_count": sum(len(r.issues) for r in relevant)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: ooc-checker
|
||||
description: 人物OOC检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# ooc-checker (人物OOC检查器)
|
||||
|
||||
> **职责**: 角色完整性守卫者,防止 OOC(Out-Of-Character)违规。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 角色行为分析、OOC 违规、人设漂移警告。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载角色档案
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `设定集/角色卡/`(所有角色档案)
|
||||
3. 前序章节作为行为基线(若审查章节 > 10)
|
||||
|
||||
### 第二步: 提取角色档案
|
||||
|
||||
**对每个主要角色,提取**:
|
||||
- **Personality traits** (性格): e.g., "隐忍冷静/嚣张狂妄/温柔体贴"
|
||||
- **Speech patterns** (说话风格): e.g., "言简意赅/喜欢嘲讽/礼貌用词"
|
||||
- **Core values** (价值观): e.g., "重视承诺/追求力量/保护弱者"
|
||||
- **Behavioral tendencies** (行为倾向): e.g., "三思而后行/冲动鲁莽/谨慎多疑"
|
||||
|
||||
**角色档案示例**:
|
||||
```
|
||||
角色:林天(主角)
|
||||
性格:隐忍冷静、智谋深沉、不轻易暴露实力
|
||||
说话风格:言简意赅,很少废话,语气平淡
|
||||
价值观:重视家族荣誉,保护弱者
|
||||
行为倾向:三思而后行,善于隐藏真实意图
|
||||
```
|
||||
|
||||
### 第三步: 行为采样
|
||||
|
||||
**对每章,提取角色的动作和对话**:
|
||||
|
||||
```
|
||||
第45章 - 林天行为采样:
|
||||
[对话] "你找死!" 林天怒吼一声,失去理智冲向对手
|
||||
[行动] 不顾一切地正面硬刚
|
||||
[情绪] 暴怒失控
|
||||
```
|
||||
|
||||
### 第四步: OOC 检测(三级判定)
|
||||
|
||||
#### 一级: 轻微偏离
|
||||
**定义**: 角色行为略有不同,但有合理的世界观内解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
✓ Acceptable:
|
||||
角色:林天(平时冷静)
|
||||
场景:敌人威胁要杀他家人
|
||||
行为:罕见地暴怒
|
||||
判定:✓ 触及底线,情绪变化合理
|
||||
|
||||
✓ Acceptable:
|
||||
角色:李雪(平时温柔)
|
||||
场景:主角生死关头
|
||||
行为:展现强势果断的一面
|
||||
判定:✓ 危机激发隐藏面,有前置铺垫
|
||||
```
|
||||
|
||||
#### 二级: 中度失真
|
||||
**定义**: 角色行为不一致,缺乏充分的铺垫或解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
⚠️ Warning:
|
||||
角色:林天(三思而后行)
|
||||
场景:普通挑衅
|
||||
行为:突然冲动鲁莽
|
||||
判定:⚠️ 缺少动机,需补充原因(如压力积累/特殊影响)
|
||||
|
||||
⚠️ Warning:
|
||||
角色:慕容雪(高傲冷漠)
|
||||
场景:对路人甲
|
||||
行为:突然温柔体贴
|
||||
判定:⚠️ 性格转变过快,需铺垫(如特殊原因/渐进变化)
|
||||
```
|
||||
|
||||
#### 三级: 严重崩坏
|
||||
**定义**: 角色行为与既定特征完全相反,且无任何解释。
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
❌ Violation:
|
||||
角色:反派(嚣张狂妄、智商在线)
|
||||
场景:与主角对峙
|
||||
行为:突然智商下线,犯低级错误(故意让主角翻盘)
|
||||
判定:❌ 反派智商崩坏,纯粹为剧情服务
|
||||
|
||||
❌ Violation:
|
||||
角色:林天(隐忍冷静)
|
||||
场景:无特殊刺激
|
||||
行为:持续多章表现为冲动易怒
|
||||
判定:❌ 性格全面改变无解释,核心人设崩塌
|
||||
```
|
||||
|
||||
### 第五步: 对话风格检查
|
||||
|
||||
**校验对话一致性**:
|
||||
|
||||
| Character Type | Expected Style | OOC Examples |
|
||||
|---------------|----------------|--------------|
|
||||
| **主角(冷静型)** | 言简意赅、语气平淡 | ❌ "哈哈哈!老子今天就让你见识见识!" (过度张扬) |
|
||||
| **反派(嚣张型)** | 嘲讽、轻蔑、自信 | ❌ "对不起...我错了..." (突然怯懦) |
|
||||
| **修仙者** | "阁下/道友/在下" | ❌ "牛逼/666/OMG" (现代网络用语) |
|
||||
|
||||
### 第六步: 角色成长 vs. OOC
|
||||
|
||||
**区分合理成长与 OOC**:
|
||||
|
||||
```
|
||||
✓ Character Development:
|
||||
第1-10章:林天谨慎多疑(因为实力弱)
|
||||
第50章:林天开始自信果敢(实力提升+经历磨练)
|
||||
判定:✓ 合理成长,有渐进式铺垫
|
||||
|
||||
❌ OOC:
|
||||
第10章:林天隐忍冷静
|
||||
第11章:林天突然变成话痨
|
||||
判定:❌ 无解释的性格突变,非成长而是失真
|
||||
```
|
||||
|
||||
**成长检查清单**:
|
||||
- [ ] 性格转变有合理触发事件?
|
||||
- [ ] 转变过程有渐进式铺垫?
|
||||
- [ ] 转变后的行为与触发事件逻辑一致?
|
||||
|
||||
### 第七步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 人物OOC检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 主要角色行为采样
|
||||
|
||||
### 林天(主角)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {N} | "..." 冷静观察,未轻举妄动 | ✓ 符合"隐忍冷静" | 无 |
|
||||
| {M} | "你找死!"暴怒冲向对手 | ✗ 不符合"三思而后行" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 第{M}章林天失去冷静,**缺少触发原因**
|
||||
- 建议补充:对手触及底线(如威胁家人)来合理化情绪爆发
|
||||
|
||||
### 慕容雪(女配)
|
||||
| 章节 | 行为/对话 | 人设匹配 | OOC 级别 |
|
||||
|------|----------|---------|---------|
|
||||
| {M} | 突然对路人温柔体贴 | ✗ 不符合"高傲冷漠" | ⚠️ 中度 |
|
||||
|
||||
**OOC 分析**:
|
||||
- 性格转变缺少铺垫,建议:
|
||||
- 补充慕容雪性格变化的原因(如受主角影响)
|
||||
- 或将此场景改为"表面冷漠实则关心"来保持人设
|
||||
|
||||
## 对话风格检查
|
||||
| 角色 | 预期风格 | 发现违规 |
|
||||
|------|---------|---------|
|
||||
| 林天 | 言简意赅 | ✓ 无违规 |
|
||||
| 反派王少 | 嚣张嘲讽 | ✗ 第{M}章突然谦逊(智商下线)|
|
||||
|
||||
## 性格转变检查
|
||||
| 角色 | 原有特征 | 当前特征 | 合理性 | 判定 |
|
||||
|------|---------|---------|--------|------|
|
||||
| 林天 | 谨慎 | 自信 | ✓ 实力提升+经历铺垫 | ✓ 合理成长 |
|
||||
| 慕容雪 | 高傲 | 温柔 | ✗ 无铺垫 | ❌ OOC |
|
||||
|
||||
## 修复建议
|
||||
1. **修复第{M}章林天OOC**: 补充对手触及底线的情节
|
||||
2. **慕容雪性格转变**: 添加渐进式铺垫(3-5章)或调整此章表现
|
||||
3. **反派王少智商崩坏**: 修改对话,恢复嚣张狂妄但逻辑在线的人设
|
||||
|
||||
## 综合评分
|
||||
**OOC 违规**:
|
||||
- 严重: {count}
|
||||
- 中度: {count}
|
||||
- 轻微: {count}
|
||||
|
||||
**结论**: {通过/警告/未通过}
|
||||
**优先修复项**: {列出必须修复的严重OOC}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过存在严重 OOC 且未标记的章节(如反派智商下线)
|
||||
❌ 忽略角色对话风格违规
|
||||
❌ 混淆 OOC 与角色成长
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 0 个严重 OOC 违规
|
||||
- 中度 OOC 有合理的世界观内解释
|
||||
- 角色成长是渐进且有动机的
|
||||
- 对话风格与既定档案匹配
|
||||
- 报告能区分 OOC 和合理成长
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
OOC Checker (人物偏离检查器)
|
||||
|
||||
检查角色行为是否偏离人设:
|
||||
- 性格一致性
|
||||
- 行为合理性
|
||||
- 语言风格一致性
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCIssue:
|
||||
"""角色偏离问题"""
|
||||
chapter: int
|
||||
character: str
|
||||
issue_type: str # personality / behavior / dialogue
|
||||
description: str
|
||||
severity: str
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OOCReport:
|
||||
"""OOC 检查报告"""
|
||||
chapter: int
|
||||
character_count: int
|
||||
issues: List[OOCIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class OOChecker:
|
||||
"""
|
||||
角色 OOC 检查器
|
||||
|
||||
检查:
|
||||
1. 性格一致性
|
||||
2. 行为合理性
|
||||
3. 对话风格一致性
|
||||
"""
|
||||
|
||||
# 性格关键词
|
||||
PERSONALITY_KEYWORDS = {
|
||||
"冷酷": ["冷哼", "不屑", "冷笑", "漠然", "冰冷"],
|
||||
"热血": ["怒吼", "咆哮", "愤怒", "激动", "振奋"],
|
||||
"腹黑": ["微笑", "意味深长", "算计", "暗笑", "狡黠"],
|
||||
"阳光": ["微笑", "开朗", "乐观", "爽朗", "活力"],
|
||||
"沉稳": ["平静", "淡定", "从容", "冷静", "深思"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.character_profiles: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def load_character_profiles(self, profiles: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""加载角色设定"""
|
||||
self.character_profiles = profiles
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str,
|
||||
characters_in_chapter: Optional[List[str]] = None
|
||||
) -> OOCReport:
|
||||
"""检查单章 OOC"""
|
||||
report = OOCReport(chapter=chapter_num, character_count=0)
|
||||
|
||||
if not characters_in_chapter:
|
||||
characters_in_chapter = self._extract_characters(chapter_text)
|
||||
|
||||
report.character_count = len(characters_in_chapter)
|
||||
|
||||
for character in characters_in_chapter:
|
||||
if character not in self.character_profiles:
|
||||
continue
|
||||
|
||||
profile = self.character_profiles[character]
|
||||
personality = profile.get("personality", "")
|
||||
|
||||
# 检查性格关键词出现
|
||||
personality_keywords = self.PERSONALITY_KEYWORDS.get(personality, [])
|
||||
|
||||
# 检测可能的 OOC
|
||||
ooc_issues = self._detect_ooc(chapter_num, character, chapter_text, personality, personality_keywords)
|
||||
report.issues.extend(ooc_issues)
|
||||
|
||||
critical_count = sum(1 for i in report.issues if i.severity == "critical")
|
||||
report.passed = critical_count == 0
|
||||
report.summary = f"检查 {report.character_count} 个角色,发现 {len(report.issues)} 个 OOC 问题"
|
||||
|
||||
return report
|
||||
|
||||
def _extract_characters(self, text: str) -> List[str]:
|
||||
"""提取出现的角色"""
|
||||
characters = set()
|
||||
|
||||
# 简单实现:提取"说"前后的名字
|
||||
import re
|
||||
dialogue_pattern = re.compile(r'^"?([^"说]{2,5})"?[说问道喊叫笑骂冷哼]')
|
||||
for line in text.split('\n'):
|
||||
match = dialogue_pattern.match(line.strip())
|
||||
if match:
|
||||
name = match.group(1).strip()
|
||||
if name and len(name) <= 5:
|
||||
characters.add(name)
|
||||
|
||||
return list(characters)
|
||||
|
||||
def _detect_ooc(
|
||||
self,
|
||||
chapter: int,
|
||||
character: str,
|
||||
text: str,
|
||||
personality: str,
|
||||
expected_keywords: List[str]
|
||||
) -> List[OOCIssue]:
|
||||
"""检测 OOC"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有相反性格的关键词
|
||||
opposite_keywords = {
|
||||
"冷酷": ["微笑", "开心", "高兴", "热情"],
|
||||
"热血": ["冷漠", "冷淡", "冷静", "漠然"],
|
||||
"腹黑": ["坦诚", "直接", "单纯", "天真"],
|
||||
"阳光": ["阴沉", "冷漠", "阴暗", "消沉"],
|
||||
"沉稳": ["激动", "暴躁", "鲁莽", "轻浮"]
|
||||
}
|
||||
|
||||
opposite = opposite_keywords.get(personality, [])
|
||||
found_opposite = [kw for kw in opposite if kw in text]
|
||||
|
||||
if found_opposite:
|
||||
# 检查是否在对话中(可能需要根据上下文判断)
|
||||
# 简化:直接报告
|
||||
issues.append(OOCIssue(
|
||||
chapter=chapter,
|
||||
character=character,
|
||||
issue_type="personality",
|
||||
description=f"角色表现出与设定({personality})相反的性格特征: {', '.join(found_opposite)}",
|
||||
severity="medium",
|
||||
suggested_fix="确认是否为特定场景需要,或调整言行描述"
|
||||
))
|
||||
|
||||
return issues
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: pacing-checker
|
||||
description: Strand Weave 节奏检查,输出结构化报告供润色步骤参考
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# pacing-checker (节奏检查器)
|
||||
|
||||
> **职责**: 节奏分析师,执行 Strand Weave 平衡检查,防止读者疲劳。
|
||||
|
||||
> **输出格式**: 遵循 `${CLAUDE_PLUGIN_ROOT}/references/checker-output-schema.md` 统一 JSON Schema
|
||||
|
||||
## 检查范围
|
||||
|
||||
**输入**: 单章或章节区间(如 `45` / `"45-46"`)
|
||||
|
||||
**输出**: 情节线分布分析、平衡预警、节奏建议。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第一步: 加载上下文
|
||||
|
||||
**输入参数**:
|
||||
```json
|
||||
{
|
||||
"project_root": "{PROJECT_ROOT}",
|
||||
"storage_path": "..noma/novel_data/",
|
||||
"state_file": "..noma/novel_data/state.json",
|
||||
"chapter_file": "正文/第{NNNN}章-{title_safe}.md"
|
||||
}
|
||||
```
|
||||
|
||||
`chapter_file` 应传实际章节文件路径;若当前项目仍使用旧格式 `正文/第{NNNN}章.md`,同样允许。
|
||||
|
||||
**并行读取**:
|
||||
1. `正文/` 下的目标章节
|
||||
2. `{project_root}/..noma/novel_data/state.json`(strand_tracker 历史)
|
||||
3. `大纲/`(理解预期弧线结构)
|
||||
|
||||
**可选: 使用 status_reporter 进行自动化分析**:
|
||||
```bash
|
||||
python -X utf8 "${CLAUDE_PLUGIN_ROOT:?CLAUDE_PLUGIN_ROOT is required}/scripts/noma.py" --project-root "${PROJECT_ROOT}" status -- --focus strand
|
||||
```
|
||||
|
||||
### 第二步: 章节情节线分类
|
||||
|
||||
**对每章,识别主导情节线**:
|
||||
|
||||
| Strand | Indicators | Examples |
|
||||
|--------|-----------|----------|
|
||||
| **Quest** (主线) | 战斗/任务/探索/升级/打怪 | 参加宗门大比、探索秘境、击败反派 |
|
||||
| **Fire** (感情线) | 情感关系/暧昧/友情/羁绊 | 与李雪的感情发展、师徒情深、兄弟义气 |
|
||||
| **Constellation** (世界观线) | 势力关系/阵营/社交网络/揭示世界观 | 新势力登场、修仙界格局展示、宗门政治 |
|
||||
|
||||
**分类规则**:
|
||||
- 一章可以有多条情节线的**底色**,但只有**一条主导**
|
||||
- 主导 = 占据章节内容 ≥ 60%
|
||||
|
||||
**Example**:
|
||||
```
|
||||
第45章:主角参加大比(Quest 80%)+ 李雪担心主角(Fire 20%)
|
||||
→ Dominant: Quest
|
||||
|
||||
第46章:主角与李雪约会(Fire 70%)+ 揭示血煞门阴谋(Constellation 30%)
|
||||
→ Dominant: Fire
|
||||
```
|
||||
|
||||
### 第三步: 平衡检查(Strand Weave 违规)
|
||||
|
||||
**从 state.json 加载 strand_tracker**:
|
||||
```json
|
||||
{
|
||||
"strand_tracker": {
|
||||
"last_quest_chapter": 46,
|
||||
"last_fire_chapter": 42,
|
||||
"last_constellation_chapter": 38,
|
||||
"history": [
|
||||
{"chapter": 45, "dominant": "quest"},
|
||||
{"chapter": 46, "dominant": "quest"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**应用警告阈值**:
|
||||
|
||||
| 违规类型 | 触发条件 | 严重度 | 影响 |
|
||||
|-----------|-----------|----------|--------|
|
||||
| **Quest 过载** | 连续 5+ 章 Quest 主导 | High | 战斗疲劳,缺少情感深度 |
|
||||
| **Fire 干旱** | 距上次 Fire > 10 章 | Medium | 人物关系停滞 |
|
||||
| **Constellation 缺席** | 距上次 Constellation > 15 章 | Low | 世界观单薄 |
|
||||
|
||||
**违规示例**:
|
||||
```
|
||||
⚠️ Quest Overload (连续7章)
|
||||
Chapters 40-46 全部为 Quest 主导
|
||||
→ Impact: 读者疲劳,建议第47章安排感情戏或世界观扩展
|
||||
|
||||
⚠️ Fire Drought (已12章未出现)
|
||||
Last Fire chapter: 34 | Current: 46 | Gap: 12 chapters
|
||||
→ Impact: 李雪等角色存在感降低,建议补充互动场景
|
||||
|
||||
✓ Constellation Acceptable
|
||||
Last Constellation: 38 | Current: 46 | Gap: 8 chapters
|
||||
```
|
||||
|
||||
### 第四步: 节奏标准
|
||||
|
||||
**每10章理想分布与缺席阈值**:
|
||||
|
||||
| Strand | 理想占比 | 最大缺席 | 超限影响 |
|
||||
|--------|---------|---------|---------|
|
||||
| Quest (主线) | 55-65% | 5 章连续 | 战斗疲劳,缺少情感深度 |
|
||||
| Fire (感情线) | 20-30% | 10 章 | 人物关系停滞 |
|
||||
| Constellation (世界观线) | 10-20% | 15 章 | 世界观单薄 |
|
||||
|
||||
### 第五步: 历史趋势分析
|
||||
|
||||
**若 state.json 包含 20+ 章历史数据**:
|
||||
|
||||
生成情节线分布图:
|
||||
```
|
||||
Chapters 1-20 Strand Distribution:
|
||||
Quest: ████████████░░░░░░░░ 60% (12 chapters)
|
||||
Fire: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
Constellation: ████░░░░░░░░░░░░░░░░ 20% (4 chapters)
|
||||
|
||||
结论:✓ 节奏均衡(符合理想比例)
|
||||
```
|
||||
|
||||
vs.
|
||||
|
||||
```
|
||||
Chapters 21-40 Strand Distribution:
|
||||
Quest: ███████████████████░ 95% (19 chapters)
|
||||
Fire: █░░░░░░░░░░░░░░░░░░░ 5% (1 chapter)
|
||||
Constellation: ░░░░░░░░░░░░░░░░░░░░ 0% (0 chapters)
|
||||
|
||||
结论:✗ 严重失衡(Quest 过载,节奏单调)
|
||||
```
|
||||
|
||||
### 第六步: 生成报告
|
||||
|
||||
```markdown
|
||||
# 节奏检查报告
|
||||
|
||||
## 覆盖范围
|
||||
第 {N} 章 - 第 {M} 章
|
||||
|
||||
## 当前章节主导情节线
|
||||
| 章节 | 主导线 | 底色 | 强度 |
|
||||
|------|--------|------|------|
|
||||
| {N} | Quest | Fire(20%)| 高(战斗密集)|
|
||||
| {M} | Quest | - | 中等 |
|
||||
|
||||
## Strand 平衡检查
|
||||
### Quest 线(主线)
|
||||
- 最近出现: 第 {X} 章
|
||||
- 连续章数: {count}
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 过载}
|
||||
|
||||
### Fire 线(情感线)
|
||||
- 最近出现: 第 {Y} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警 / ✗ 干旱}
|
||||
|
||||
### Constellation 线(世界观线)
|
||||
- 最近出现: 第 {Z} 章
|
||||
- 距上次间隔: {count} 章
|
||||
- **状态**: {✓ 正常 / ⚠️ 预警}
|
||||
|
||||
## 历史趋势(需 ≥ 20 章数据)
|
||||
最近 20 章分布:
|
||||
- Quest: {X}%({count} 章)
|
||||
- Fire: {Y}%({count} 章)
|
||||
- Constellation: {Z}%({count} 章)
|
||||
|
||||
**趋势**: {均衡 / Quest偏重 / Fire不足 / ...}
|
||||
|
||||
## 修复建议
|
||||
- [Quest 过载] 连续{count}章Quest主导,建议在第{next}章安排:
|
||||
- 与{角色}的感情发展场景(Fire)
|
||||
- 或揭示{势力/世界观元素}(Constellation)
|
||||
|
||||
- [Fire 干旱] 距上次Fire已{count}章,建议补充:
|
||||
- 与李雪/师父/伙伴的互动
|
||||
- 不必是专门的感情章,可作为底色穿插
|
||||
|
||||
- [Constellation 间隔] 世界观扩展不足,建议:
|
||||
- 揭示新势力或修仙界格局
|
||||
- 展示新的修炼体系或设定
|
||||
|
||||
## 下一章节奏建议
|
||||
基于当前平衡状态,第 {next} 章应优先:
|
||||
**主导**: {线}(因为距上次{gap}章)
|
||||
**底色**: {线}
|
||||
|
||||
## 综合评分
|
||||
**节奏总评**: {健康/预警/危险}
|
||||
**读者疲劳风险**: {低/中/高}
|
||||
```
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 通过连续 5+ 章 Quest 主导且不预警
|
||||
❌ 忽略 Fire 干旱超过 10 章
|
||||
❌ 接受 20+ 章中完全相同的节奏模式
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 最近 10 章内单一情节线不超过 70%
|
||||
- 所有情节线在各自阈值内至少出现一次
|
||||
- 报告提供可执行的下一章建议
|
||||
- 趋势分析显示分布均衡(若有足够历史数据)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Pacing Checker (节奏检查器)
|
||||
|
||||
检查 Strand 比例与节奏:
|
||||
- Quest/Fire/Constellation 比例
|
||||
- 断档检查
|
||||
- 节奏红线
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingIssue:
|
||||
"""节奏问题"""
|
||||
chapter: int
|
||||
issue_type: str # strand_imbalance / gap_too_long / rush
|
||||
description: str
|
||||
severity: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacingReport:
|
||||
"""节奏检查报告"""
|
||||
start_chapter: int
|
||||
end_chapter: int
|
||||
strand_distribution: Dict[str, int] = field(default_factory=dict)
|
||||
issues: List[PacingIssue] = field(default_factory=list)
|
||||
passed: bool = True
|
||||
summary: str = ""
|
||||
|
||||
|
||||
class PacingChecker:
|
||||
"""
|
||||
节奏检查器
|
||||
|
||||
检查:
|
||||
1. Strand 比例(Quest/Fire/Constellation)
|
||||
2. 断档检查
|
||||
3. 节奏红线
|
||||
"""
|
||||
|
||||
# 理想比例
|
||||
IDEAL_RATIOS = {
|
||||
"quest": (55, 65), # 60% 主线
|
||||
"fire": (20, 30), # 25% 感情
|
||||
"constellation": (10, 20), # 15% 世界观
|
||||
}
|
||||
|
||||
# 断档红线
|
||||
MAX_GAPS = {
|
||||
"quest": 5, # Quest 连续不超过 5 章
|
||||
"fire": 10, # Fire 断档不超过 10 章
|
||||
"constellation": 15 # Constellation 断档不超过 15 章
|
||||
}
|
||||
|
||||
# 关键词
|
||||
STRAND_KEYWORDS = {
|
||||
"quest": ["战斗", "修炼", "突破", "机缘", "仇敌", "目标", "任务", "追杀"],
|
||||
"fire": ["感情", "爱慕", "暧昧", "心跳", "脸红", "温情", "甜蜜", "争吵"],
|
||||
"constellation": ["世界", "势力", "宗门", "规则", "历史", "背景", "设定"]
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.strand_history: List[Dict[str, Any]] = []
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter_num: int,
|
||||
chapter_text: str
|
||||
) -> PacingReport:
|
||||
"""检查单章节奏"""
|
||||
# 统计各 Strand 出现次数
|
||||
strand_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for strand, keywords in self.STRAND_KEYWORDS.items():
|
||||
for keyword in keywords:
|
||||
strand_counts[strand] += chapter_text.count(keyword)
|
||||
|
||||
# 确定主导 Strand
|
||||
dominant = max(strand_counts, key=strand_counts.get)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=chapter_num,
|
||||
end_chapter=chapter_num,
|
||||
strand_distribution=strand_counts
|
||||
)
|
||||
|
||||
# 记录历史
|
||||
self.strand_history.append({
|
||||
"chapter": chapter_num,
|
||||
"strand_counts": strand_counts,
|
||||
"dominant": dominant
|
||||
})
|
||||
|
||||
return report
|
||||
|
||||
def check_arc_pacing(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int
|
||||
) -> PacingReport:
|
||||
"""检查情节弧节奏"""
|
||||
relevant = [h for h in self.strand_history
|
||||
if start_chapter <= h["chapter"] <= end_chapter]
|
||||
|
||||
if not relevant:
|
||||
return PacingReport(start_chapter=start_chapter, end_chapter=end_chapter)
|
||||
|
||||
# 统计各 Strand 章节数
|
||||
strand_chapter_counts = {s: 0 for s in self.STRAND_KEYWORDS}
|
||||
for h in relevant:
|
||||
dominant = h["dominant"]
|
||||
if h["strand_counts"][dominant] > 0:
|
||||
strand_chapter_counts[dominant] += 1
|
||||
|
||||
total_chapters = len(relevant)
|
||||
|
||||
report = PacingReport(
|
||||
start_chapter=start_chapter,
|
||||
end_chapter=end_chapter,
|
||||
strand_distribution=strand_chapter_counts
|
||||
)
|
||||
|
||||
# 检查比例
|
||||
for strand, (min_ratio, max_ratio) in self.IDEAL_RATIOS.items():
|
||||
actual_ratio = (strand_chapter_counts[strand] / total_chapters * 100) if total_chapters > 0 else 0
|
||||
|
||||
if actual_ratio < min_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比不足({actual_ratio:.0f}% < {min_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
elif actual_ratio > max_ratio:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=0,
|
||||
issue_type="strand_imbalance",
|
||||
description=f"{strand} 占比过高({actual_ratio:.0f}% > {max_ratio}%)",
|
||||
severity="medium"
|
||||
))
|
||||
|
||||
# 检查断档
|
||||
self._check_gaps(report, relevant)
|
||||
|
||||
report.summary = f"检查 {total_chapters} 章,分布: {strand_chapter_counts}"
|
||||
return report
|
||||
|
||||
def _check_gaps(self, report: PacingReport, history: List[Dict[str, Any]]) -> None:
|
||||
"""检查断档"""
|
||||
if len(history) < 2:
|
||||
return
|
||||
|
||||
last_seen = {s: -100 for s in self.STRAND_KEYWORDS}
|
||||
|
||||
for i, h in enumerate(history):
|
||||
chapter = h["chapter"]
|
||||
dominant = h["dominant"]
|
||||
|
||||
for strand in self.STRAND_KEYWORDS:
|
||||
if strand == dominant:
|
||||
last_seen[strand] = chapter
|
||||
else:
|
||||
gap = chapter - last_seen[strand]
|
||||
if gap > self.MAX_GAPS[strand]:
|
||||
report.issues.append(PacingIssue(
|
||||
chapter=chapter,
|
||||
issue_type="gap_too_long",
|
||||
description=f"{strand} 断档过长({gap}章未出现)",
|
||||
severity="high"
|
||||
))
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: reader-pull-checker
|
||||
description: 追读力检查器,评估钩子/微兑现/约束分层,支持 Override Contract
|
||||
tools: Read, Grep, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
# reader-pull-checker (追读力检查器)
|
||||
|
||||
> **职责**: 审查"读者为什么会点下一章",执行 Hard/Soft 约束分层。
|
||||
|
||||
## 核心参考
|
||||
|
||||
- **分类法**: `${CLAUDE_PLUGIN_ROOT}/references/reading-power-taxonomy.md`
|
||||
- **题材画像**: `${CLAUDE_PLUGIN_ROOT}/references/genre-profiles.md`
|
||||
- **章节追读力数据**: `index.db → chapter_reading_power`
|
||||
- **上章钩子**: `state.json → chapter_meta` 或 `index.db`
|
||||
|
||||
## 输入
|
||||
- 章节正文(实际章节文件路径,优先 `正文/第{NNNN}章-{title_safe}.md`,旧格式 `正文/第{NNNN}章.md` 仍兼容)
|
||||
- 上章钩子与模式(从 `state.json → chapter_meta` 或 `index.db`)
|
||||
- 题材 Profile(从 `state.json → project.genre`)
|
||||
- 是否为过渡章标记
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"agent": "reader-pull-checker",
|
||||
"chapter": 100,
|
||||
"overall_score": 85,
|
||||
"pass": true,
|
||||
"issues": [],
|
||||
"hard_violations": [],
|
||||
"soft_suggestions": [
|
||||
{
|
||||
"id": "SOFT_HOOK_STRENGTH",
|
||||
"severity": "medium",
|
||||
"location": "章末",
|
||||
"description": "钩子强度为weak,建议提升至medium",
|
||||
"suggestion": "将'回去休息了'改为悬念/危机",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "CHARACTER_CREDIBILITY"]
|
||||
}
|
||||
],
|
||||
"metrics": {
|
||||
"hook_present": true,
|
||||
"hook_type": "渴望钩",
|
||||
"hook_strength": "medium",
|
||||
"prev_hook_fulfilled": true,
|
||||
"new_expectations": 2,
|
||||
"pattern_repeat_risk": false,
|
||||
"micropayoffs": ["能力兑现", "认可兑现"],
|
||||
"micropayoff_count": 2,
|
||||
"is_transition": false,
|
||||
"next_chapter_reason": "读者想知道云芝找萧炎什么事",
|
||||
"debt_balance": 0.0
|
||||
},
|
||||
"summary": "硬约束通过,钩子强度偏弱,建议增强章末期待。",
|
||||
"override_eligible": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、约束分层
|
||||
|
||||
### 1.1 硬约束
|
||||
|
||||
> **违反 = 必须修复,不可申诉跳过**
|
||||
|
||||
| ID | 约束名称 | 触发条件 | severity |
|
||||
|----|---------|---------|----------|
|
||||
| HARD-001 | 可读性底线 | 读者无法理解"发生了什么/谁/为什么" | critical |
|
||||
| HARD-002 | 承诺违背 | 上章明确承诺在本章完全无回应 | critical |
|
||||
| HARD-003 | 节奏灾难 | 连续N章无任何推进(N由profile决定) | critical |
|
||||
| HARD-004 | 冲突真空 | 整章无问题/目标/代价 | high |
|
||||
|
||||
**硬约束违规输出**:
|
||||
```json
|
||||
{
|
||||
"id": "HARD-002",
|
||||
"severity": "critical",
|
||||
"location": "全章",
|
||||
"description": "上章钩子'敌人即将到来'完全未在本章提及",
|
||||
"must_fix": true,
|
||||
"fix_suggestion": "在开头或中段回应敌人威胁"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 软建议
|
||||
|
||||
> **违反 = 可申诉,但需记录 `Override Contract` 并承担债务**
|
||||
|
||||
| ID | 约束名称 | 默认期望 | 可覆盖 |
|
||||
|----|---------|---------|-----------|
|
||||
| SOFT_NEXT_REASON | 下章动机 | 读者能明确"为何点下一章" | ✓ |
|
||||
| SOFT_HOOK_ANCHOR | 期待锚点有效性 | 有未闭合问题或明确期待(章末/后段均可) | ✓ |
|
||||
| SOFT_HOOK_STRENGTH | 钩子强度 | 题材profile baseline | ✓ |
|
||||
| SOFT_HOOK_TYPE | 钩子类型 | 匹配题材偏好 | ✓ |
|
||||
| SOFT_MICROPAYOFF | 微兑现数量 | ≥ profile.min_per_chapter | ✓ |
|
||||
| SOFT_PATTERN_REPEAT | 模式重复 | 避免连续3章同型 | ✓ |
|
||||
| SOFT_EXPECTATION_OVERLOAD | 期待过载 | 新增期待 ≤ 2 | ✓ |
|
||||
| SOFT_RHYTHM_NATURALNESS | 节奏自然性 | 避免固定字距机械打点 | ✓ |
|
||||
|
||||
**软建议输出**:
|
||||
```json
|
||||
{
|
||||
"id": "SOFT_MICROPAYOFF",
|
||||
"severity": "medium",
|
||||
"location": "全章",
|
||||
"description": "本章微兑现0个,题材要求≥1",
|
||||
"suggestion": "添加能力兑现或认可兑现",
|
||||
"can_override": true,
|
||||
"allowed_rationales": ["TRANSITIONAL_SETUP", "ARC_TIMING"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、钩子类型扩展
|
||||
|
||||
### 2.1 完整钩子类型
|
||||
|
||||
| 类型 | 标识 | 驱动力 |
|
||||
|------|------|--------|
|
||||
| 危机钩 | Crisis Hook | 危险逼近,读者担心 |
|
||||
| 悬念钩 | Mystery Hook | 信息缺口,读者好奇 |
|
||||
| 情绪钩 | Emotion Hook | 强情绪触发(愤怒/心疼/心动) |
|
||||
| 选择钩 | Choice Hook | 两难抉择,读者想知道选择 |
|
||||
| 渴望钩 | Desire Hook | 好事将至,读者期待 |
|
||||
|
||||
### 2.2 钩子强度
|
||||
|
||||
| 强度 | 适用场景 | 特征 |
|
||||
|------|---------|------|
|
||||
| **strong** | 卷末/关键转折/大冲突前 | 读者必须立刻知道 |
|
||||
| **medium** | 普通剧情章 | 读者想知道,但可等 |
|
||||
| **weak** | 过渡章/铺垫章 | 维持阅读惯性 |
|
||||
|
||||
---
|
||||
|
||||
## 三、微兑现检测
|
||||
|
||||
### 3.1 微兑现类型
|
||||
|
||||
| 类型 | 识别信号 |
|
||||
|------|---------|
|
||||
| 信息兑现 | 揭示新信息/线索/真相 |
|
||||
| 关系兑现 | 关系推进/确认/变化 |
|
||||
| 能力兑现 | 能力提升/新技能展示 |
|
||||
| 资源兑现 | 获得物品/资源/财富 |
|
||||
| 认可兑现 | 获得认可/面子/地位 |
|
||||
| 情绪兑现 | 情绪释放/共鸣 |
|
||||
| 线索兑现 | 伏笔回收/推进 |
|
||||
|
||||
### 3.2 检测规则
|
||||
|
||||
1. 扫描正文识别微兑现
|
||||
2. 按题材profile检查数量是否达标
|
||||
3. 过渡章可降级要求
|
||||
|
||||
---
|
||||
|
||||
## 四、模式重复检测
|
||||
|
||||
### 4.1 检测范围
|
||||
- 钩子类型:最近3章
|
||||
- 开头模式:最近3章
|
||||
- 爽点模式:最近5章
|
||||
|
||||
### 4.2 风险等级
|
||||
- **warning**: 连续2章同型
|
||||
- **risk**: 连续3章同型
|
||||
- **critical**: 连续4+章同型
|
||||
|
||||
---
|
||||
|
||||
## 五、`Override Contract` 机制
|
||||
|
||||
### 5.1 何时可覆盖
|
||||
|
||||
当 `soft_suggestions` 中的建议无法遵守时,可提交 `Override Contract`:
|
||||
|
||||
```json
|
||||
{
|
||||
"constraint_type": "SOFT_MICROPAYOFF",
|
||||
"constraint_id": "micropayoff_count",
|
||||
"rationale_type": "TRANSITIONAL_SETUP",
|
||||
"rationale_text": "本章为铺垫章,下章将有大爽点",
|
||||
"payback_plan": "下章补偿2个微兑现",
|
||||
"due_chapter": 101
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 rationale_type 枚举
|
||||
|
||||
| 类型 | 描述 | 债务影响 |
|
||||
|------|------|---------|
|
||||
| TRANSITIONAL_SETUP | 铺垫/过渡需要 | 标准 |
|
||||
| LOGIC_INTEGRITY | 剧情逻辑优先 | 减少 |
|
||||
| CHARACTER_CREDIBILITY | 人物可信度优先 | 减少 |
|
||||
| WORLD_RULE_CONSTRAINT | 设定约束 | 减少 |
|
||||
| ARC_TIMING | 长线节奏安排 | 标准 |
|
||||
| GENRE_CONVENTION | 题材惯例 | 标准 |
|
||||
| EDITORIAL_INTENT | 作者主观意图 | 增加 |
|
||||
|
||||
### 5.3 债务与利息
|
||||
|
||||
- 每个 `Override` 产生债务(量由题材 profile 的 `debt_multiplier` 决定)
|
||||
- 每章债务累积利息(默认10%/章)
|
||||
- 超过 `due_chapter` 未偿还,债务变为 `overdue`
|
||||
|
||||
---
|
||||
|
||||
## 六、执行步骤
|
||||
|
||||
### Step 1: 加载配置
|
||||
1. 读取题材Profile
|
||||
2. 读取上章钩子/模式记录
|
||||
3. 检查当前债务状态
|
||||
|
||||
### Step 2: 硬约束检查
|
||||
1. 检查可读性(关键信息完整性)
|
||||
2. 检查上章钩子兑现
|
||||
3. 检查节奏停滞
|
||||
4. 检查冲突存在
|
||||
|
||||
**任何硬约束违规 → 立即标记为必须修复**
|
||||
|
||||
### Step 3: 钩子分析
|
||||
1. 识别本章期待锚点(优先章末,允许后段)
|
||||
2. 评估钩子强度与有效性
|
||||
3. 对比题材偏好与章节类型
|
||||
|
||||
### Step 4: 微兑现扫描
|
||||
1. 识别章内微兑现
|
||||
2. 统计数量和类型
|
||||
3. 对比题材要求
|
||||
|
||||
### Step 5: 模式重复检测
|
||||
1. 获取最近N章模式
|
||||
2. 检测钩子类型重复
|
||||
3. 检测开头模式重复
|
||||
|
||||
### Step 6: 软建议评估
|
||||
1. 汇总所有软建议
|
||||
2. 标注可覆盖的建议
|
||||
3. 列出允许的 `rationale` 类型
|
||||
|
||||
### Step 7: 生成报告
|
||||
1. 计算总分
|
||||
2. 输出结构化JSON
|
||||
3. 提供修复建议
|
||||
|
||||
---
|
||||
|
||||
## 七、评分规则
|
||||
|
||||
### 7.1 硬约束违规
|
||||
- 任何硬约束违规 → 直接未通过
|
||||
- 必须修复后重新审核
|
||||
|
||||
### 7.2 软评分(无硬约束违规时)
|
||||
|
||||
| 得分 | 结果 |
|
||||
|------|------|
|
||||
| 85+ | 通过 |
|
||||
| 70-84 | 通过(有警告) |
|
||||
| 50-69 | 条件通过(可通过 `Override`)|
|
||||
| <50 | 未通过 |
|
||||
|
||||
### 7.3 软评分计算
|
||||
|
||||
| 检查项 | 权重 | 问题类型 |
|
||||
|--------|------|----------|
|
||||
| 下章动机清晰 | 20% | NEXT_REASON_WEAK |
|
||||
| 期待锚点有效(章末/后段) | 15% | WEAK_HOOK_ANCHOR |
|
||||
| 钩子强度适当 | 10% | WEAK_HOOK |
|
||||
| 微兑现达标 | 20% | LOW_MICROPAYOFF |
|
||||
| 模式不重复 | 15% | PATTERN_REPEAT |
|
||||
| 新增期待≤2个 | 10% | EXPECTATION_OVERLOAD |
|
||||
| 钩子类型匹配题材 | 5% | TYPE_MISMATCH |
|
||||
| 节奏自然性(非机械打点) | 5% | MECHANICAL_PACING |
|
||||
|
||||
---
|
||||
|
||||
## 八、与 Data Agent 的交互
|
||||
|
||||
审核完成后,由 Data Agent 执行:
|
||||
|
||||
1. **保存章节追读力元数据**
|
||||
```python
|
||||
index_manager.save_chapter_reading_power(ChapterReadingPowerMeta(...))
|
||||
```
|
||||
|
||||
2. **处理 `Override Contract`**(如有)
|
||||
```python
|
||||
index_manager.create_override_contract(OverrideContractMeta(...))
|
||||
index_manager.create_debt(ChaseDebtMeta(...))
|
||||
```
|
||||
|
||||
3. **计算利息**(每章)
|
||||
```python
|
||||
index_manager.accrue_interest(current_chapter)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、成功标准
|
||||
|
||||
- [ ] 无硬约束违规
|
||||
- [ ] 软评分 ≥ 70(或有有效 `Override`)
|
||||
- [ ] 存在可感知的未闭合问题/期待锚点(章末或后段)
|
||||
- [ ] 微兑现数量达标(或有 `Override`)
|
||||
- [ ] 无连续3章以上同型
|
||||
- [ ] 输出清晰的"下章动机"
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
Tension Checker (情绪压强校验器)
|
||||
|
||||
取代原爽点审查,校验情绪压强的积累与释放是否符合选定的 catharsis model。
|
||||
检测"降维打击"、"禁忌僭越"等爽感路由的执行效果。
|
||||
与 ledger.ts 中的 hooks_pool 压强联动。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class CatharsisModel(Enum):
|
||||
"""爽感路由模型"""
|
||||
TABOO_TRANSGRESSION = "taboo_transgression" # 禁忌僭越
|
||||
OVERKILL_REVERSAL = "overkill_reversal" # 降维打击
|
||||
COGNITIVE_CLOSURE = "cognitive_closure" # 认知闭环
|
||||
STRAND_WEAVE = "strand_weave" # 三轨编织
|
||||
|
||||
|
||||
class TensionLevel(Enum):
|
||||
"""张力等级"""
|
||||
LOW = "low" # < 30
|
||||
MEDIUM = "medium" # 30-60
|
||||
HIGH = "high" # 60-80
|
||||
DANGER = "danger" # > 80
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionReading:
|
||||
"""张力读数"""
|
||||
chapter: int
|
||||
start_tension: int # 起始压强 (0-100)
|
||||
end_tension: int # 结束压强 (0-100)
|
||||
peak_tension: int # 峰值压强
|
||||
catharsis_model: Optional[CatharsisModel]
|
||||
satisfaction_delivered: int # 爽感释放值 (0-100)
|
||||
issues: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TensionIssue:
|
||||
"""张力问题"""
|
||||
chapter: int
|
||||
issue_type: str # buildup_too_long / release_misaligned / pattern_broken
|
||||
description: str
|
||||
severity: str # critical/high/medium/low
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
class TensionChecker:
|
||||
"""
|
||||
情绪压强校验器
|
||||
|
||||
功能:
|
||||
1. 校验压强曲线是否符合 catharsis model
|
||||
2. 检测爽感释放时机
|
||||
3. 与 hooks_pool 联动
|
||||
4. 输出张力分析报告
|
||||
"""
|
||||
|
||||
# 各模型的标准张力曲线
|
||||
MODEL_CURVES = {
|
||||
CatharsisModel.TABOO_TRANSGRESSION: {
|
||||
"setup": (1, 30), # 阶段: (持续章节数, 目标压强)
|
||||
"approach": (3, 50),
|
||||
"crisis": (1, 85),
|
||||
"transgression": (1, 100),
|
||||
"fallout": (2, 40),
|
||||
},
|
||||
CatharsisModel.OVERKILL_REVERSAL: {
|
||||
"establish": (2, 40), # 建立敌人强大
|
||||
"suppress": (2, 30), # 主角被压制
|
||||
"trigger": (1, 50), # 金手指触发
|
||||
"reversal": (1, 100), # 降维打击
|
||||
"aftermath": (1, 60), # 余波
|
||||
},
|
||||
CatharsisModel.COGNITIVE_CLOSURE: {
|
||||
"setup": (3, 40), # 多线伏笔埋设
|
||||
"weave": (2, 55), # 线索交织
|
||||
"activate": (1, 75), # 伏笔激活
|
||||
"closure": (1, 100), # 认知闭合
|
||||
"new_hook": (1, 50), # 新悬念
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, ledger_path: Optional[str] = None):
|
||||
self.ledger_path = ledger_path
|
||||
self.hooks_pool: Dict[str, Any] = {}
|
||||
self.tension_history: List[TensionReading] = []
|
||||
|
||||
def load_ledger(self, ledger_data: Dict[str, Any]) -> None:
|
||||
"""加载账本数据"""
|
||||
self.hooks_pool = ledger_data.get("hooks", {})
|
||||
|
||||
def check_chapter(
|
||||
self,
|
||||
chapter: int,
|
||||
chapter_text: str,
|
||||
catharsis_model: Optional[CatharsisModel] = None,
|
||||
previous_tension: int = 30
|
||||
) -> TensionReading:
|
||||
"""
|
||||
检查单章张力
|
||||
|
||||
Args:
|
||||
chapter: 章节号
|
||||
chapter_text: 章节正文
|
||||
catharsis_model: 使用的爽感模型(可选)
|
||||
previous_tension: 上一章结束时的压强
|
||||
|
||||
Returns:
|
||||
TensionReading: 张力读数
|
||||
"""
|
||||
# 计算起始/结束/峰值压强
|
||||
start_tension = previous_tension
|
||||
|
||||
# 分析本章爽感事件
|
||||
climax_events = self._detect_climax_events(chapter_text)
|
||||
|
||||
# 计算结束压强
|
||||
if climax_events["major_climax"]:
|
||||
end_tension = max(20, previous_tension - 40) # 大高潮后压强下降
|
||||
peak_tension = 100
|
||||
elif climax_events["minor_climax"]:
|
||||
end_tension = max(30, previous_tension - 20) # 小高潮后压强下降
|
||||
peak_tension = max(70, previous_tension + 20)
|
||||
elif climax_events["tension_build"]:
|
||||
end_tension = min(90, previous_tension + 15) # 积累压强上升
|
||||
peak_tension = end_tension
|
||||
else:
|
||||
end_tension = previous_tension # 平稳过渡
|
||||
peak_tension = previous_tension
|
||||
|
||||
# 计算爽感释放值
|
||||
satisfaction = self._calculate_satisfaction(climax_events)
|
||||
|
||||
# 检测问题
|
||||
issues = self._detect_issues(chapter, previous_tension, end_tension, catharsis_model, climax_events)
|
||||
|
||||
reading = TensionReading(
|
||||
chapter=chapter,
|
||||
start_tension=start_tension,
|
||||
end_tension=end_tension,
|
||||
peak_tension=peak_tension,
|
||||
catharsis_model=catharsis_model,
|
||||
satisfaction_delivered=satisfaction,
|
||||
issues=issues
|
||||
)
|
||||
|
||||
self.tension_history.append(reading)
|
||||
return reading
|
||||
|
||||
def _detect_climax_events(self, text: str) -> Dict[str, Any]:
|
||||
"""检测章节中的高潮事件"""
|
||||
events = {
|
||||
"major_climax": False, # 大高潮(降维打击、禁忌突破等)
|
||||
"minor_climax": False, # 小高潮(打脸、收获等)
|
||||
"tension_build": False, # 压强积累
|
||||
}
|
||||
|
||||
# 关键词检测
|
||||
major_keywords = [
|
||||
"碾压", "秒杀", "一击", "降维打击", "禁忌突破",
|
||||
"震惊", "不可思议", "全场寂静", "彻底碾压"
|
||||
]
|
||||
minor_keywords = [
|
||||
"冷笑", "一掌", "打脸", "收获", "突破",
|
||||
"提升", "获得", "机缘", "惊喜"
|
||||
]
|
||||
buildup_keywords = [
|
||||
"危机", "困境", "压力", "紧张", "危机四伏",
|
||||
"蓄势", "积累", "暗中", "谋划"
|
||||
]
|
||||
|
||||
text_length = len(text)
|
||||
|
||||
for keyword in major_keywords:
|
||||
if keyword in text:
|
||||
events["major_climax"] = True
|
||||
break
|
||||
|
||||
if not events["major_climax"]:
|
||||
for keyword in minor_keywords:
|
||||
if keyword in text:
|
||||
events["minor_climax"] = True
|
||||
break
|
||||
|
||||
for keyword in buildup_keywords:
|
||||
if keyword in text:
|
||||
events["tension_build"] = True
|
||||
break
|
||||
|
||||
return events
|
||||
|
||||
def _calculate_satisfaction(self, events: Dict[str, Any]) -> int:
|
||||
"""计算爽感释放值"""
|
||||
if events["major_climax"]:
|
||||
return 90
|
||||
elif events["minor_climax"]:
|
||||
return 60
|
||||
elif events["tension_build"]:
|
||||
return 20
|
||||
else:
|
||||
return 10
|
||||
|
||||
def _detect_issues(
|
||||
self,
|
||||
chapter: int,
|
||||
previous_tension: int,
|
||||
current_tension: int,
|
||||
catharsis_model: Optional[CatharsisModel],
|
||||
events: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""检测张力问题"""
|
||||
issues = []
|
||||
|
||||
# 问题1: 压强过高持续太久
|
||||
if previous_tension > 85 and current_tension > 85:
|
||||
issues.append("⚠️ 压强持续过高 (>85),可能导致读者疲劳")
|
||||
|
||||
# 问题2: 压强过低但无爽感
|
||||
if current_tension < 30 and not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强过低且无爽感释放,节奏可能拖沓")
|
||||
|
||||
# 问题3: 压强突然下降
|
||||
if previous_tension > 50 and current_tension < previous_tension - 30:
|
||||
if not events["major_climax"] and not events["minor_climax"]:
|
||||
issues.append("⚠️ 压强突然下降但无对应爽感事件")
|
||||
|
||||
# 问题4: 压强过高但无释放
|
||||
if current_tension > 90 and not events["major_climax"]:
|
||||
issues.append("🔴 压强达到危险水平 (>90) 但无高潮释放")
|
||||
|
||||
return issues
|
||||
|
||||
def check_arc_tension(
|
||||
self,
|
||||
start_chapter: int,
|
||||
end_chapter: int,
|
||||
catharsis_model: CatharsisModel
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
检查情节弧的张力曲线
|
||||
|
||||
Args:
|
||||
start_chapter: 起始章节
|
||||
end_chapter: 结束章节
|
||||
catharsis_model: 使用的爽感模型
|
||||
|
||||
Returns:
|
||||
分析报告
|
||||
"""
|
||||
relevant_readings = [
|
||||
r for r in self.tension_history
|
||||
if start_chapter <= r.chapter <= end_chapter
|
||||
]
|
||||
|
||||
if not relevant_readings:
|
||||
return {"status": "no_data", "message": "无张力历史数据"}
|
||||
|
||||
model_curve = self.MODEL_CURVES.get(catharsis_model, {})
|
||||
if not model_curve:
|
||||
return {"status": "unknown_model", "message": f"未知的爽感模型: {catharsis_model}"}
|
||||
|
||||
# 分析曲线是否符合模型
|
||||
expected_phases = list(model_curve.keys())
|
||||
actual_phases = self._infer_phases(relevant_readings)
|
||||
|
||||
# 检查匹配度
|
||||
match_score = self._calculate_curve_match(model_curve, relevant_readings)
|
||||
|
||||
# 生成报告
|
||||
report = {
|
||||
"status": "analyzed",
|
||||
"chapters": f"{start_chapter}-{end_chapter}",
|
||||
"model": catharsis_model.value,
|
||||
"match_score": match_score,
|
||||
"phases": {
|
||||
"expected": expected_phases,
|
||||
"actual": actual_phases
|
||||
},
|
||||
"tension_summary": {
|
||||
"avg_start": sum(r.start_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"avg_end": sum(r.end_tension for r in relevant_readings) / len(relevant_readings),
|
||||
"max_peak": max(r.peak_tension for r in relevant_readings),
|
||||
"total_satisfaction": sum(r.satisfaction_delivered for r in relevant_readings)
|
||||
},
|
||||
"issues": self._collect_arc_issues(relevant_readings, model_curve),
|
||||
"recommendations": self._generate_recommendations(match_score, relevant_readings)
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
def _infer_phases(self, readings: List[TensionReading]) -> List[str]:
|
||||
"""从张力读数推断阶段"""
|
||||
phases = []
|
||||
for r in readings:
|
||||
if r.peak_tension >= 90:
|
||||
phases.append("peak")
|
||||
elif r.peak_tension >= 70:
|
||||
phases.append("elevated")
|
||||
elif r.end_tension > r.start_tension:
|
||||
phases.append("buildup")
|
||||
elif r.end_tension < r.start_tension:
|
||||
phases.append("release")
|
||||
else:
|
||||
phases.append("maintain")
|
||||
return phases
|
||||
|
||||
def _calculate_curve_match(
|
||||
self,
|
||||
expected_curve: Dict[str, tuple],
|
||||
readings: List[TensionReading]
|
||||
) -> float:
|
||||
"""计算曲线匹配度 (0-100)"""
|
||||
if not readings:
|
||||
return 0.0
|
||||
|
||||
# 简化实现:检查峰值是否出现
|
||||
has_peak = any(r.peak_tension >= 85 for r in readings)
|
||||
has_release = any(r.end_tension < r.start_tension for r in readings)
|
||||
|
||||
score = 50 # 基础分
|
||||
|
||||
if has_peak:
|
||||
score += 30
|
||||
if has_release:
|
||||
score += 20
|
||||
|
||||
return min(100, score)
|
||||
|
||||
def _collect_arc_issues(
|
||||
self,
|
||||
readings: List[TensionReading],
|
||||
expected_curve: Dict[str, tuple]
|
||||
) -> List[str]:
|
||||
"""收集情节弧问题"""
|
||||
issues = []
|
||||
|
||||
# 检查是否有峰值
|
||||
if not any(r.peak_tension >= 85 for r in readings):
|
||||
issues.append("缺少高潮点,张力未达到峰值")
|
||||
|
||||
# 检查是否有释放
|
||||
if not any(r.end_tension < r.start_tension for r in readings):
|
||||
issues.append("张力未释放,可能导致读者疲劳")
|
||||
|
||||
# 检查压强过高
|
||||
high_tension_count = sum(1 for r in readings if r.end_tension > 80)
|
||||
if high_tension_count > len(readings) * 0.5:
|
||||
issues.append("长时间高压状态,需要适当缓冲")
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_recommendations(
|
||||
self,
|
||||
match_score: float,
|
||||
readings: List[TensionReading]
|
||||
) -> List[str]:
|
||||
"""生成改进建议"""
|
||||
recommendations = []
|
||||
|
||||
if match_score < 50:
|
||||
recommendations.append("张力曲线偏离模型,建议重新规划节奏")
|
||||
elif match_score < 80:
|
||||
recommendations.append("张力曲线基本符合,可适当优化")
|
||||
|
||||
# 基于历史数据建议
|
||||
avg_tension = sum(r.end_tension for r in readings) / len(readings)
|
||||
if avg_tension > 70:
|
||||
recommendations.append("平均张力偏高,建议增加缓冲章节")
|
||||
|
||||
return recommendations
|
||||
|
||||
def get_tension_level(self, tension: int) -> TensionLevel:
|
||||
"""获取张力等级"""
|
||||
if tension < 30:
|
||||
return TensionLevel.LOW
|
||||
elif tension < 60:
|
||||
return TensionLevel.MEDIUM
|
||||
elif tension < 80:
|
||||
return TensionLevel.HIGH
|
||||
else:
|
||||
return TensionLevel.DANGER
|
||||
|
||||
def get_recommended_model(self, current_tension: int, target_tension: int) -> CatharsisModel:
|
||||
"""根据当前/目标压强推荐爽感模型"""
|
||||
tension_delta = target_tension - current_tension
|
||||
|
||||
if tension_delta > 40:
|
||||
# 需要大释放 -> 降维打击
|
||||
return CatharsisModel.OVERKILL_REVERSAL
|
||||
elif tension_delta > 20:
|
||||
# 中等释放 -> 禁忌僭越
|
||||
return CatharsisModel.TABOO_TRANSGRESSION
|
||||
else:
|
||||
# 认知闭合
|
||||
return CatharsisModel.COGNITIVE_CLOSURE
|
||||
|
||||
def export_history(self) -> str:
|
||||
"""导出张力历史为 JSON"""
|
||||
return json.dumps([
|
||||
{
|
||||
"chapter": r.chapter,
|
||||
"start_tension": r.start_tension,
|
||||
"end_tension": r.end_tension,
|
||||
"peak_tension": r.peak_tension,
|
||||
"catharsis_model": r.catharsis_model.value if r.catharsis_model else None,
|
||||
"satisfaction": r.satisfaction_delivered,
|
||||
"issues": r.issues
|
||||
}
|
||||
for r in self.tension_history
|
||||
], ensure_ascii=False, indent=2)
|
||||
Reference in New Issue
Block a user