novel-maker 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/novel-maker.js +229 -0
- package/package.json +33 -0
- package/skill/CHANGELOG.md +82 -0
- package/skill/QUICK-REF.md +168 -0
- package/skill/README.md +75 -0
- package/skill/SKILL.md +715 -0
- package/skill/agents/README.md +59 -0
- package/skill/agents/auditor.md +234 -0
- package/skill/agents/coordinator.md +150 -0
- package/skill/agents/planner.md +220 -0
- package/skill/agents/reviewer.md +249 -0
- package/skill/agents/reviser.md +144 -0
- package/skill/agents/writer.md +213 -0
- package/skill/arc-templates/README.md +43 -0
- package/skill/arc-templates/apocalypse/ability.md +81 -0
- package/skill/arc-templates/apocalypse/faction.md +81 -0
- package/skill/arc-templates/apocalypse/humanity.md +81 -0
- package/skill/arc-templates/apocalypse/survival.md +81 -0
- package/skill/arc-templates/game/competition.md +81 -0
- package/skill/arc-templates/game/dungeon.md +81 -0
- package/skill/arc-templates/game/guild-war.md +81 -0
- package/skill/arc-templates/game/leveling.md +80 -0
- package/skill/arc-templates/general/challenge.md +44 -0
- package/skill/arc-templates/general/conflict.md +71 -0
- package/skill/arc-templates/general/explore.md +71 -0
- package/skill/arc-templates/general/growth.md +71 -0
- package/skill/arc-templates/general/mystery.md +71 -0
- package/skill/arc-templates/general/relation.md +71 -0
- package/skill/arc-templates/history/battle.md +71 -0
- package/skill/arc-templates/history/politics.md +71 -0
- package/skill/arc-templates/history/reform.md +71 -0
- package/skill/arc-templates/infinite-flow/boss.md +71 -0
- package/skill/arc-templates/infinite-flow/dungeon.md +71 -0
- package/skill/arc-templates/infinite-flow/enhance.md +71 -0
- package/skill/arc-templates/infinite-flow/team.md +71 -0
- package/skill/arc-templates/mystery/case.md +71 -0
- package/skill/arc-templates/mystery/deduction.md +71 -0
- package/skill/arc-templates/mystery/twist.md +71 -0
- package/skill/arc-templates/romance/angst.md +80 -0
- package/skill/arc-templates/romance/chase.md +71 -0
- package/skill/arc-templates/romance/slow-burn.md +71 -0
- package/skill/arc-templates/romance/sweet.md +80 -0
- package/skill/arc-templates/sci-fi/awakening.md +81 -0
- package/skill/arc-templates/sci-fi/breakthrough.md +81 -0
- package/skill/arc-templates/sci-fi/contact.md +81 -0
- package/skill/arc-templates/sci-fi/exploration.md +81 -0
- package/skill/arc-templates/urban/business.md +71 -0
- package/skill/arc-templates/urban/revenge.md +71 -0
- package/skill/arc-templates/urban/rise.md +71 -0
- package/skill/arc-templates/urban/romance.md +71 -0
- package/skill/arc-templates/western-fantasy/adventure.md +80 -0
- package/skill/arc-templates/western-fantasy/kingdom.md +71 -0
- package/skill/arc-templates/western-fantasy/magic-awakening.md +71 -0
- package/skill/arc-templates/western-fantasy/racial-conflict.md +71 -0
- package/skill/arc-templates/wuxia/breakthrough.md +71 -0
- package/skill/arc-templates/wuxia/grudge.md +71 -0
- package/skill/arc-templates/wuxia/hero-path.md +71 -0
- package/skill/arc-templates/wuxia/sect-war.md +71 -0
- package/skill/arc-templates/xianxia/breakthrough.md +71 -0
- package/skill/arc-templates/xianxia/dungeon.md +71 -0
- package/skill/arc-templates/xianxia/tournament.md +71 -0
- package/skill/arc-templates/xianxia/tribulation.md +71 -0
- package/skill/docs/examples.md +61 -0
- package/skill/docs/faq.md +81 -0
- package/skill/docs/installation.md +87 -0
- package/skill/docs/quickstart.md +83 -0
- package/skill/genre-packs/README.md +47 -0
- package/skill/genre-packs/_default/arc-types.md +153 -0
- package/skill/genre-packs/_default/rules.md +56 -0
- package/skill/genre-packs/_default/templates.md +135 -0
- package/skill/genre-packs/apocalypse/arc-types.md +109 -0
- package/skill/genre-packs/apocalypse/rules.md +113 -0
- package/skill/genre-packs/apocalypse/settings.md +106 -0
- package/skill/genre-packs/apocalypse/templates.md +192 -0
- package/skill/genre-packs/game/arc-types.md +109 -0
- package/skill/genre-packs/game/rules.md +113 -0
- package/skill/genre-packs/game/settings.md +103 -0
- package/skill/genre-packs/game/templates.md +173 -0
- package/skill/genre-packs/history/arc-types.md +109 -0
- package/skill/genre-packs/history/rules.md +107 -0
- package/skill/genre-packs/history/settings.md +126 -0
- package/skill/genre-packs/history/templates.md +179 -0
- package/skill/genre-packs/infinite-flow/arc-types.md +101 -0
- package/skill/genre-packs/infinite-flow/rules.md +75 -0
- package/skill/genre-packs/infinite-flow/settings.md +102 -0
- package/skill/genre-packs/infinite-flow/templates.md +226 -0
- package/skill/genre-packs/mystery/arc-types.md +109 -0
- package/skill/genre-packs/mystery/rules.md +107 -0
- package/skill/genre-packs/mystery/settings.md +103 -0
- package/skill/genre-packs/mystery/templates.md +178 -0
- package/skill/genre-packs/romance/arc-types.md +130 -0
- package/skill/genre-packs/romance/rules.md +88 -0
- package/skill/genre-packs/romance/settings.md +146 -0
- package/skill/genre-packs/romance/templates.md +245 -0
- package/skill/genre-packs/sci-fi/arc-types.md +109 -0
- package/skill/genre-packs/sci-fi/rules.md +113 -0
- package/skill/genre-packs/sci-fi/settings.md +99 -0
- package/skill/genre-packs/sci-fi/templates.md +170 -0
- package/skill/genre-packs/urban/arc-types.md +101 -0
- package/skill/genre-packs/urban/rules.md +75 -0
- package/skill/genre-packs/urban/settings.md +82 -0
- package/skill/genre-packs/urban/templates.md +212 -0
- package/skill/genre-packs/western-fantasy/arc-types.md +128 -0
- package/skill/genre-packs/western-fantasy/rules.md +88 -0
- package/skill/genre-packs/western-fantasy/settings.md +160 -0
- package/skill/genre-packs/western-fantasy/templates.md +225 -0
- package/skill/genre-packs/wuxia/arc-types.md +126 -0
- package/skill/genre-packs/wuxia/rules.md +86 -0
- package/skill/genre-packs/wuxia/settings.md +150 -0
- package/skill/genre-packs/wuxia/templates.md +195 -0
- package/skill/genre-packs/xianxia/arc-types.md +101 -0
- package/skill/genre-packs/xianxia/rules.md +74 -0
- package/skill/genre-packs/xianxia/settings.md +107 -0
- package/skill/genre-packs/xianxia/templates.md +202 -0
- package/skill/hooks/README.md +102 -0
- package/skill/hooks/chapter-complete.md +176 -0
- package/skill/hooks/context-injection.md +152 -0
- package/skill/hooks/intent-detection.md +183 -0
- package/skill/hooks/review-trigger.md +219 -0
- package/skill/hooks/summary-trigger.md +185 -0
- package/skill/references/act-guidance.md +228 -0
- package/skill/references/audit-core.md +130 -0
- package/skill/references/audit-dimensions.md +202 -0
- package/skill/references/character-voice-card.md +196 -0
- package/skill/references/consistency-checker.md +209 -0
- package/skill/references/content-expansion.md +68 -0
- package/skill/references/creative-constraints.md +200 -0
- package/skill/references/data-agent.md +286 -0
- package/skill/references/dialogue-writing.md +104 -0
- package/skill/references/editorial-perspective.md +166 -0
- package/skill/references/emotion-curve.md +127 -0
- package/skill/references/genre-rules.md +389 -0
- package/skill/references/golden-opening.md +81 -0
- package/skill/references/memory-system.md +288 -0
- package/skill/references/pacing-analysis.md +201 -0
- package/skill/references/platform-rules.md +244 -0
- package/skill/references/plot-structures.md +108 -0
- package/skill/references/reader-feedback.md +119 -0
- package/skill/references/rhythm-system.md +204 -0
- package/skill/references/style-imitation.md +193 -0
- package/skill/references/sweet-spot-tracking.md +182 -0
- package/skill/references/usage-guide.md +174 -0
- package/skill/references/writing-methods.md +169 -0
- package/skill/rules/anti-ai-expressions.md +206 -0
- package/skill/rules/character-voice.md +184 -0
- package/skill/rules/consistency-check.md +232 -0
- package/skill/rules/smart-query.md +263 -0
- package/skill/scripts/README.md +380 -0
- package/skill/scripts/auditor/chapter_transition.py +217 -0
- package/skill/scripts/auditor/consistency_scan.py +194 -0
- package/skill/scripts/auditor/dialogue_checker.py +194 -0
- package/skill/scripts/auditor/hook_report.py +115 -0
- package/skill/scripts/auditor/pacing_optimizer.py +303 -0
- package/skill/scripts/auditor/pacing_report.py +275 -0
- package/skill/scripts/auditor/pre_audit.py +203 -0
- package/skill/scripts/auditor/style_check.py +158 -0
- package/skill/scripts/auditor/worldbuilding_checker.py +637 -0
- package/skill/scripts/common/analyze.py +129 -0
- package/skill/scripts/common/init_guide.py +796 -0
- package/skill/scripts/common/install.py +169 -0
- package/skill/scripts/common/nm_utils.py +296 -0
- package/skill/scripts/common/validate.py +215 -0
- package/skill/scripts/coordinator/stats_report.py +165 -0
- package/skill/scripts/coordinator/volume_batch.py +121 -0
- package/skill/scripts/planner/outline_extractor.py +89 -0
- package/skill/scripts/planner/planner_context.py +220 -0
- package/skill/scripts/planner/query_engine.py +289 -0
- package/skill/scripts/reviewer/chapter_diff.py +143 -0
- package/skill/scripts/reviewer/character_arc_tracker.py +191 -0
- package/skill/scripts/reviewer/emotion_curve.py +340 -0
- package/skill/scripts/reviewer/foreshadowing_tracker.py +286 -0
- package/skill/scripts/reviewer/subplot_tracker.py +207 -0
- package/skill/scripts/reviewer/summary_generator.py +130 -0
- package/skill/scripts/reviewer/truth_diff.py +227 -0
- package/skill/scripts/reviewer/truth_manager.py +120 -0
- package/skill/scripts/test_scripts.py +366 -0
- package/skill/scripts/writer/build_write_context.py +255 -0
- package/skill/scripts/writer/chapter_info.py +67 -0
- package/skill/scripts/writer/check_wordcount.py +115 -0
- package/skill/scripts/writer/scene_builder.py +227 -0
- package/skill/scripts/writer/style_anchor.py +135 -0
- package/skill/styles/author-styles.md +53 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//345/277/230/350/257/255.md +110 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//347/210/261/346/275/234/346/260/264/347/232/204/344/271/214/350/264/274.md +110 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//350/250/200/345/275/222/346/255/243/344/274/240.md +110 -0
- package/skill/styles/authors//345/244/232/347/245/236/350/257/235/347/203/255/350/241/200/346/265/201//344/270/211/344/271/235/351/237/263/345/237/237.md +108 -0
- package/skill/styles/authors//346/202/254/347/226/221/346/216/250/347/220/206/346/265/201//346/235/200/350/231/253/351/230/237/351/230/237/345/221/230.md +108 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/270/211/345/244/251/344/270/244/350/247/211.md +110 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/274/232/350/257/264/350/257/235/347/232/204/350/202/230/345/255/220.md +110 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/215/226/346/212/245/345/260/217/351/203/216/345/220/233.md +108 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/274/210/351/235/222/345/263/260.md +123 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/224/220/345/256/266/344/270/211/345/260/221.md +109 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/244/251/350/232/225/345/234/237/350/261/206.md +110 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/210/221/345/220/203/350/245/277/347/272/242/346/237/277.md +108 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/273/232/345/274/200.md +109 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//350/276/260/344/270/234.md +109 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//345/244/251/344/270/213/351/234/270/345/224/261.md +110 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//346/234/210/345/205/263.md +108 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/220/247/351/274/216.md +109 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/235/264/350/235/266/350/223/235.md +112 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//346/204/244/346/200/222/347/232/204/351/246/231/350/225/211.md +109 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/203/275/347/201/253/346/210/217/350/257/270/344/276/257.md +109 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/214/253/350/205/273.md +110 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//350/200/263/346/240/271.md +109 -0
- package/skill/templates/INDEX.md +48 -0
- package/skill/templates/act-plan.md +72 -0
- package/skill/templates/chapter.md +53 -0
- package/skill/templates/character-profile.md +107 -0
- package/skill/templates/character-voice.md +106 -0
- package/skill/templates/constitution.md +90 -0
- package/skill/templates/emotional-arcs.md +37 -0
- package/skill/templates/hook-template.md +68 -0
- package/skill/templates/outline.md +155 -0
- package/skill/templates/plot-card.md +432 -0
- package/skill/templates/power-system.md +124 -0
- package/skill/templates/presets.json +69 -0
- package/skill/templates/review-report.md +135 -0
- package/skill/templates/scene-plan.md +221 -0
- package/skill/templates/scene-template.md +78 -0
- package/skill/templates/subplot-board.md +48 -0
- package/skill/templates/summary-10chapters.md +79 -0
- package/skill/templates/summary-50chapters.md +131 -0
- package/skill/templates/summary-volume.md +148 -0
- package/skill/templates/timeline.md +37 -0
- package/skill/templates/volume-plan.md +44 -0
- package/skill/templates/world-setting.md +151 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
支线追踪器
|
|
5
|
+
追踪小说中的支线剧情发展状态,检测未回收的伏笔和断裂的支线。
|
|
6
|
+
供复盘师在卷末复盘时使用,或规划师在规划新支线时参考。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python scripts/reviewer/subplot_tracker.py 章节目录 --json
|
|
10
|
+
python scripts/reviewer/subplot_tracker.py 章节目录 --verbose
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
21
|
+
from nm_utils import (
|
|
22
|
+
list_chapters, read_chapter, extract_characters, extract_locations,
|
|
23
|
+
detect_hook, count_chinese
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ─── 支线类型定义 ───────────────────────────────
|
|
27
|
+
SUBPLOT_PATTERNS = {
|
|
28
|
+
'感情线': {
|
|
29
|
+
'keywords': ['喜欢', '爱', '情', '心动', '暗恋', '表白', '分手', '复合', '婚姻'],
|
|
30
|
+
'indicators': ['两人独处', '眼神交流', '心跳加速', '脸红', '牵手']
|
|
31
|
+
},
|
|
32
|
+
'复仇线': {
|
|
33
|
+
'keywords': ['仇', '恨', '报复', '凶手', '仇人', '血债', '讨回'],
|
|
34
|
+
'indicators': ['调查真相', '寻找证据', '准备复仇', ' confrontation']
|
|
35
|
+
},
|
|
36
|
+
'成长线': {
|
|
37
|
+
'keywords': ['修炼', '突破', '进阶', '领悟', '提升', '实力'],
|
|
38
|
+
'indicators': ['闭关', '顿悟', '突破瓶颈', '新技能']
|
|
39
|
+
},
|
|
40
|
+
'阴谋线': {
|
|
41
|
+
'keywords': ['阴谋', '计划', '暗中', '秘密', '隐藏', '真相'],
|
|
42
|
+
'indicators': ['密谋', '布局', '暗中观察', '设局']
|
|
43
|
+
},
|
|
44
|
+
'冒险线': {
|
|
45
|
+
'keywords': ['探险', '寻宝', '秘境', '遗迹', '未知', '危险'],
|
|
46
|
+
'indicators': ['出发', '探索', '发现', '遭遇危险']
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def detect_subplot_mentions(text):
|
|
52
|
+
"""Detect subplot mentions in text."""
|
|
53
|
+
mentions = defaultdict(list)
|
|
54
|
+
|
|
55
|
+
for subplot_type, config in SUBPLOT_PATTERNS.items():
|
|
56
|
+
for keyword in config['keywords']:
|
|
57
|
+
if keyword in text:
|
|
58
|
+
mentions[subplot_type].append(keyword)
|
|
59
|
+
for indicator in config['indicators']:
|
|
60
|
+
if indicator in text:
|
|
61
|
+
mentions[subplot_type].append(f"[{indicator}]")
|
|
62
|
+
|
|
63
|
+
return dict(mentions)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def track_subplot_across_chapters(chapters_dir, recent_n=None):
|
|
67
|
+
"""Track subplot development across chapters."""
|
|
68
|
+
chapter_files = list_chapters(chapters_dir, recent_n)
|
|
69
|
+
if not chapter_files:
|
|
70
|
+
return {'error': '未找到章节文件'}
|
|
71
|
+
|
|
72
|
+
subplot_timeline = defaultdict(list)
|
|
73
|
+
subplot_status = {}
|
|
74
|
+
|
|
75
|
+
for idx, cf in enumerate(chapter_files):
|
|
76
|
+
raw, title, clean, wc = read_chapter(cf)
|
|
77
|
+
ch_num = idx + 1
|
|
78
|
+
|
|
79
|
+
# Detect subplot mentions
|
|
80
|
+
mentions = detect_subplot_mentions(clean)
|
|
81
|
+
|
|
82
|
+
for subplot_type, keywords in mentions.items():
|
|
83
|
+
subplot_timeline[subplot_type].append({
|
|
84
|
+
'chapter_num': ch_num,
|
|
85
|
+
'chapter_title': title,
|
|
86
|
+
'keywords': list(set(keywords))[:5], # Limit to 5 keywords
|
|
87
|
+
'word_count': wc
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
# Analyze subplot status
|
|
91
|
+
for subplot_type, timeline in subplot_timeline.items():
|
|
92
|
+
total_chapters = len(timeline)
|
|
93
|
+
first_chapter = timeline[0]['chapter_num'] if timeline else 0
|
|
94
|
+
last_chapter = timeline[-1]['chapter_num'] if timeline else 0
|
|
95
|
+
|
|
96
|
+
# Determine status
|
|
97
|
+
if total_chapters == 0:
|
|
98
|
+
status = '未出现'
|
|
99
|
+
elif total_chapters == 1:
|
|
100
|
+
status = '刚引入'
|
|
101
|
+
elif last_chapter - first_chapter < 3:
|
|
102
|
+
status = '发展中'
|
|
103
|
+
elif total_chapters > len(chapter_files) * 0.3:
|
|
104
|
+
status = '主要支线'
|
|
105
|
+
else:
|
|
106
|
+
status = '间歇性出现'
|
|
107
|
+
|
|
108
|
+
subplot_status[subplot_type] = {
|
|
109
|
+
'status': status,
|
|
110
|
+
'total_chapters': total_chapters,
|
|
111
|
+
'first_appearance': first_chapter,
|
|
112
|
+
'last_appearance': last_chapter,
|
|
113
|
+
'chapters_involved': [t['chapter_num'] for t in timeline],
|
|
114
|
+
'timeline': timeline[-5:] # Last 5 appearances
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
'total_chapters_analyzed': len(chapter_files),
|
|
119
|
+
'subplots_detected': len(subplot_timeline),
|
|
120
|
+
'subplot_status': subplot_status,
|
|
121
|
+
'subplot_timeline': dict(subplot_timeline)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_subplot_consistency(result):
|
|
126
|
+
"""Check for subplot consistency issues."""
|
|
127
|
+
issues = []
|
|
128
|
+
|
|
129
|
+
for subplot_type, status in result.get('subplot_status', {}).items():
|
|
130
|
+
# Check for abandoned subplots
|
|
131
|
+
if status['status'] == '刚引入' and result['total_chapters_analyzed'] > 10:
|
|
132
|
+
issues.append({
|
|
133
|
+
'type': '可能遗弃',
|
|
134
|
+
'subplot': subplot_type,
|
|
135
|
+
'message': f"{subplot_type}在第{status['first_appearance']}章引入后未再出现",
|
|
136
|
+
'severity': 'warning'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Check for inconsistent pacing
|
|
140
|
+
if status['total_chapters'] > 0:
|
|
141
|
+
chapters = status['chapters_involved']
|
|
142
|
+
gaps = [chapters[i+1] - chapters[i] for i in range(len(chapters)-1)]
|
|
143
|
+
if gaps and max(gaps) > 5:
|
|
144
|
+
issues.append({
|
|
145
|
+
'type': '节奏不均',
|
|
146
|
+
'subplot': subplot_type,
|
|
147
|
+
'message': f"{subplot_type}章节间隔过大(最大{max(gaps)}章)",
|
|
148
|
+
'severity': 'info'
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return issues
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main():
|
|
155
|
+
parser = argparse.ArgumentParser(description='支线追踪器')
|
|
156
|
+
parser.add_argument('chapters_dir', help='章节目录路径')
|
|
157
|
+
parser.add_argument('--recent', type=int, help='仅分析最近N章')
|
|
158
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
159
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='显示详细信息')
|
|
160
|
+
args = parser.parse_args()
|
|
161
|
+
|
|
162
|
+
if not os.path.isdir(args.chapters_dir):
|
|
163
|
+
print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
|
|
166
|
+
result = track_subplot_across_chapters(args.chapters_dir, args.recent)
|
|
167
|
+
|
|
168
|
+
# Add consistency check
|
|
169
|
+
if not args.json:
|
|
170
|
+
issues = check_subplot_consistency(result)
|
|
171
|
+
result['consistency_issues'] = issues
|
|
172
|
+
|
|
173
|
+
if args.json:
|
|
174
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
175
|
+
else:
|
|
176
|
+
if 'error' in result:
|
|
177
|
+
print(result['error'])
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
print(f"\n=== 支线追踪报告 ===\n")
|
|
181
|
+
print(f"分析章节数: {result['total_chapters_analyzed']}")
|
|
182
|
+
print(f"检测支线数: {result['subplots_detected']}")
|
|
183
|
+
|
|
184
|
+
print(f"\n支线状态:")
|
|
185
|
+
for subplot_type, status in result['subplot_status'].items():
|
|
186
|
+
icon = '●' if status['status'] == '主要支线' else '○'
|
|
187
|
+
print(f" {icon} {subplot_type}: {status['status']}")
|
|
188
|
+
print(f" 出场章数: {status['total_chapters']}, 首次: 第{status['first_appearance']}章, 最近: 第{status['last_appearance']}章")
|
|
189
|
+
|
|
190
|
+
if result.get('consistency_issues'):
|
|
191
|
+
print(f"\n一致性问题:")
|
|
192
|
+
for issue in result['consistency_issues']:
|
|
193
|
+
icon = '' if issue['severity'] == 'warning' else 'ℹ'
|
|
194
|
+
print(f" {icon} {issue['subplot']}: {issue['message']}")
|
|
195
|
+
|
|
196
|
+
if args.verbose and result.get('subplot_timeline'):
|
|
197
|
+
print(f"\n详细时间线:")
|
|
198
|
+
for subplot_type, timeline in result['subplot_timeline'].items():
|
|
199
|
+
print(f"\n {subplot_type}:")
|
|
200
|
+
for entry in timeline[-5:]: # Last 5 entries
|
|
201
|
+
print(f" 第{entry['chapter_num']}章: {entry['chapter_title']}")
|
|
202
|
+
if entry['keywords']:
|
|
203
|
+
print(f" 关键词: {', '.join(entry['keywords'])}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == '__main__':
|
|
207
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
阶段总结辅助脚本
|
|
5
|
+
从各章结构化数据中提取摘要,拼接剧情时间线。
|
|
6
|
+
供 `/novel-maker summary` 指令使用,AI 读取此输出代替读全文。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
14
|
+
from nm_utils import (
|
|
15
|
+
list_chapters, read_chapter, extract_characters, extract_locations,
|
|
16
|
+
detect_hook, count_chinese, clean_markdown
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_chapter_timeline(chapter_path):
|
|
21
|
+
"""Extract a structured timeline entry from a single chapter."""
|
|
22
|
+
raw, title, clean, wc = read_chapter(chapter_path)
|
|
23
|
+
chars = extract_characters(clean)
|
|
24
|
+
locs = extract_locations(clean)
|
|
25
|
+
hook = detect_hook(raw)
|
|
26
|
+
|
|
27
|
+
# Extract first and last paragraph summaries
|
|
28
|
+
paragraphs = [p.strip() for p in clean.split('\n') if p.strip()]
|
|
29
|
+
first_para = paragraphs[0][:100] if paragraphs else ''
|
|
30
|
+
last_para = paragraphs[-1][-150:] if paragraphs else ''
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
'title': title,
|
|
34
|
+
'words': wc,
|
|
35
|
+
'characters': chars[:6],
|
|
36
|
+
'locations': locs[:3],
|
|
37
|
+
'hook_type': hook['type'],
|
|
38
|
+
'first_para_summary': first_para,
|
|
39
|
+
'last_para_summary': last_para,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_summary(chapters_dir, start_n=1, end_n=None):
|
|
44
|
+
"""Generate a structured summary for chapters start_n through end_n."""
|
|
45
|
+
all_chapters = list_chapters(chapters_dir)
|
|
46
|
+
if not all_chapters:
|
|
47
|
+
return {'error': 'No chapters found'}
|
|
48
|
+
|
|
49
|
+
if end_n is None:
|
|
50
|
+
end_n = len(all_chapters)
|
|
51
|
+
start_idx = max(0, start_n - 1)
|
|
52
|
+
end_idx = min(len(all_chapters), end_n)
|
|
53
|
+
target_chapters = all_chapters[start_idx:end_idx]
|
|
54
|
+
|
|
55
|
+
timeline = []
|
|
56
|
+
all_chars = set()
|
|
57
|
+
all_locs = set()
|
|
58
|
+
|
|
59
|
+
for cf in target_chapters:
|
|
60
|
+
entry = build_chapter_timeline(cf)
|
|
61
|
+
timeline.append(entry)
|
|
62
|
+
all_chars.update(entry['characters'])
|
|
63
|
+
all_locs.update(entry['locations'])
|
|
64
|
+
|
|
65
|
+
total_words = sum(t['words'] for t in timeline)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
'chapter_range': f"第{start_n}-{start_n + len(timeline) - 1}章",
|
|
69
|
+
'total_chapters': len(timeline),
|
|
70
|
+
'total_words': total_words,
|
|
71
|
+
'avg_words': round(total_words / max(len(timeline), 1), 0),
|
|
72
|
+
'timeline': timeline,
|
|
73
|
+
'characters_involved': sorted(all_chars),
|
|
74
|
+
'locations_visited': sorted(all_locs),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
import argparse
|
|
80
|
+
parser = argparse.ArgumentParser(description="Summary generator")
|
|
81
|
+
parser.add_argument("chapters_dir", help="Chapters directory")
|
|
82
|
+
parser.add_argument("--range", type=str, help="Chapter range, e.g. '1-10' or '11-20'", default=None)
|
|
83
|
+
parser.add_argument("--last", type=int, help="Last N chapters", default=None)
|
|
84
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
|
|
87
|
+
if not os.path.isdir(args.chapters_dir):
|
|
88
|
+
print(f"Error: {args.chapters_dir} is not a directory", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
start_n = 1
|
|
92
|
+
end_n = None
|
|
93
|
+
|
|
94
|
+
if args.range:
|
|
95
|
+
parts = args.range.split('-')
|
|
96
|
+
if len(parts) == 2:
|
|
97
|
+
start_n = int(parts[0])
|
|
98
|
+
end_n = int(parts[1])
|
|
99
|
+
elif args.last:
|
|
100
|
+
all_chapters = list_chapters(args.chapters_dir)
|
|
101
|
+
start_n = max(1, len(all_chapters) - args.last + 1)
|
|
102
|
+
|
|
103
|
+
result = generate_summary(args.chapters_dir, start_n, end_n)
|
|
104
|
+
|
|
105
|
+
if args.json:
|
|
106
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
107
|
+
else:
|
|
108
|
+
if 'error' in result:
|
|
109
|
+
print(result['error'])
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
print(f"\n{'=' * 60}")
|
|
113
|
+
print(f"📋 阶段总结 ({result['chapter_range']})")
|
|
114
|
+
print(f"{'=' * 60}")
|
|
115
|
+
print(f"章节数: {result['total_chapters']}")
|
|
116
|
+
print(f"总字数: {result['total_words']:,}")
|
|
117
|
+
print(f"平均字数: {result['avg_words']}/章")
|
|
118
|
+
print(f"涉及角色: {', '.join(result['characters_involved'][:8])}")
|
|
119
|
+
print(f"涉及场景: {', '.join(result['locations_visited'][:5])}")
|
|
120
|
+
print(f"\n剧情时间线:")
|
|
121
|
+
for t in result['timeline']:
|
|
122
|
+
print(f"\n {t['title']} ({t['words']}字)")
|
|
123
|
+
print(f" 角色: {', '.join(t['characters'][:4])}")
|
|
124
|
+
print(f" 开头: {t['first_para_summary'][:60]}...")
|
|
125
|
+
print(f" 结尾: ...{t['last_para_summary'][-60:]}")
|
|
126
|
+
print(f" 钩子: {t['hook_type']}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
main()
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
真相文件变更检测 — 自动检测章节变更并生成更新 diff
|
|
5
|
+
|
|
6
|
+
对比定稿章节与真相文件,检测需要更新的内容(新角色、新地点、
|
|
7
|
+
伏笔变化、情感变化等),输出结构化 diff 供复盘师审核。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python scripts/reviewer/truth_diff.py ch15.md --truth-dir .novel-maker/truth-files/
|
|
11
|
+
python scripts/reviewer/truth_diff.py ch15.md --prev ch14.md --truth-dir .novel-maker/truth-files/ --json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
21
|
+
from nm_utils import (
|
|
22
|
+
read_chapter, extract_characters, extract_locations,
|
|
23
|
+
detect_hook, clean_markdown, read_truth_section
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_known_names(truth_dir, filename='characters.md'):
|
|
28
|
+
"""从真相文件中提取已知角色名"""
|
|
29
|
+
fpath = os.path.join(truth_dir, filename)
|
|
30
|
+
sections = read_truth_section(fpath)
|
|
31
|
+
if not sections:
|
|
32
|
+
return set()
|
|
33
|
+
names = set()
|
|
34
|
+
for title, content in sections.items():
|
|
35
|
+
for m in re.finditer(r'\*\*([^*]{1,10})\*\*', content):
|
|
36
|
+
name = m.group(1).strip()
|
|
37
|
+
if len(name) >= 2 and not any(c in name for c in '()()'):
|
|
38
|
+
names.add(name)
|
|
39
|
+
for m in re.finditer(r'[::]\s*([^\s,,。]{2,6})', content):
|
|
40
|
+
name = m.group(1).strip()
|
|
41
|
+
if len(name) >= 2:
|
|
42
|
+
names.add(name)
|
|
43
|
+
return names
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_known_hooks(truth_dir, filename='pending-hooks.md'):
|
|
47
|
+
"""从伏笔表中提取已知伏笔"""
|
|
48
|
+
fpath = os.path.join(truth_dir, filename)
|
|
49
|
+
sections = read_truth_section(fpath)
|
|
50
|
+
if not sections:
|
|
51
|
+
return {'pending': [], 'recovered': []}
|
|
52
|
+
hooks = {'pending': [], 'recovered': []}
|
|
53
|
+
for title, content in sections.items():
|
|
54
|
+
if '已回收' in title or '已解决' in title:
|
|
55
|
+
for m in re.finditer(r'H\[(\d+)\]', content):
|
|
56
|
+
hooks['recovered'].append(m.group(1))
|
|
57
|
+
elif 'header' not in title:
|
|
58
|
+
for m in re.finditer(r'H\[(\d+)\]', content):
|
|
59
|
+
hooks['pending'].append(m.group(1))
|
|
60
|
+
return hooks
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _load_known_locations(truth_dir, filename='world-setting.md'):
|
|
64
|
+
"""从世界观设定中提取已知地点"""
|
|
65
|
+
fpath = os.path.join(truth_dir, filename)
|
|
66
|
+
sections = read_truth_section(fpath)
|
|
67
|
+
if not sections:
|
|
68
|
+
return set()
|
|
69
|
+
locs = set()
|
|
70
|
+
for title, content in sections.items():
|
|
71
|
+
if '地' in title or '区域' in title or '地点' in title:
|
|
72
|
+
for m in re.finditer(r'\*\*([^*]{2,8})\*\*', content):
|
|
73
|
+
locs.add(m.group(1))
|
|
74
|
+
return locs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _detect_hook_keywords(text):
|
|
78
|
+
"""检测伏笔相关关键词"""
|
|
79
|
+
hook_keywords = {
|
|
80
|
+
'reveal': ['原来', '竟然', '居然', '真相', '秘密', '身份'],
|
|
81
|
+
'setup': ['记住', '将来', '总有一天', '等着', '约定'],
|
|
82
|
+
'foreshadow': ['似乎', '隐约', '感觉', '仿佛', '好像'],
|
|
83
|
+
}
|
|
84
|
+
found = {}
|
|
85
|
+
for htype, keywords in hook_keywords.items():
|
|
86
|
+
matches = [kw for kw in keywords if kw in text]
|
|
87
|
+
if matches:
|
|
88
|
+
found[htype] = matches
|
|
89
|
+
return found
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _detect_emotion_keywords(text):
|
|
93
|
+
"""检测情感关键词"""
|
|
94
|
+
emotion_map = {
|
|
95
|
+
'愤怒': ['怒', '火', '恨', '咬牙', '攥拳', '瞪'],
|
|
96
|
+
'悲伤': ['泪', '哭', '泣', '哽咽', '心痛', '悲伤'],
|
|
97
|
+
'喜悦': ['笑', '喜', '乐', '高兴', '兴奋', '激动'],
|
|
98
|
+
'恐惧': ['怕', '恐', '惊', '颤', '抖', '畏惧'],
|
|
99
|
+
'惊讶': ['惊', '没想到', '居然', '竟然', '不敢相信'],
|
|
100
|
+
'平静': ['平静', '淡然', '从容', '沉稳', '泰然'],
|
|
101
|
+
}
|
|
102
|
+
found = {}
|
|
103
|
+
for emotion, keywords in emotion_map.items():
|
|
104
|
+
count = sum(1 for kw in keywords if kw in text)
|
|
105
|
+
if count >= 2:
|
|
106
|
+
found[emotion] = count
|
|
107
|
+
return found
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def detect_changes(chapter_path, truth_dir, prev_chapter_path=None):
|
|
111
|
+
"""检测章节变更"""
|
|
112
|
+
if not os.path.exists(chapter_path):
|
|
113
|
+
return {'error': f'章节文件不存在: {chapter_path}'}
|
|
114
|
+
if not os.path.isdir(truth_dir):
|
|
115
|
+
return {'error': f'真相文件目录不存在: {truth_dir}'}
|
|
116
|
+
|
|
117
|
+
raw, title, clean, wc = read_chapter(chapter_path)
|
|
118
|
+
changes = {
|
|
119
|
+
'chapter': os.path.basename(chapter_path),
|
|
120
|
+
'title': title,
|
|
121
|
+
'word_count': wc,
|
|
122
|
+
'detected': {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# 1. 新角色检测
|
|
126
|
+
known_chars = _load_known_names(truth_dir)
|
|
127
|
+
chapter_chars = set(extract_characters(clean))
|
|
128
|
+
new_chars = chapter_chars - known_chars
|
|
129
|
+
if new_chars:
|
|
130
|
+
changes['detected']['new_characters'] = sorted(new_chars)
|
|
131
|
+
|
|
132
|
+
# 2. 新地点检测
|
|
133
|
+
known_locs = _load_known_locations(truth_dir)
|
|
134
|
+
chapter_locs = set(extract_locations(clean))
|
|
135
|
+
new_locs = chapter_locs - known_locs
|
|
136
|
+
if new_locs:
|
|
137
|
+
changes['detected']['new_locations'] = sorted(new_locs)
|
|
138
|
+
|
|
139
|
+
# 3. 伏笔检测
|
|
140
|
+
hook_keywords = _detect_hook_keywords(clean)
|
|
141
|
+
if hook_keywords:
|
|
142
|
+
changes['detected']['hook_keywords'] = hook_keywords
|
|
143
|
+
|
|
144
|
+
# 4. 章末钩子
|
|
145
|
+
hook = detect_hook(raw)
|
|
146
|
+
if hook['type'] not in ('未检测', '未知'):
|
|
147
|
+
changes['detected']['chapter_hook'] = {
|
|
148
|
+
'type': hook['type'],
|
|
149
|
+
'tail_preview': hook.get('tail_preview', '')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# 5. 情感变化
|
|
153
|
+
emotions = _detect_emotion_keywords(clean)
|
|
154
|
+
if emotions:
|
|
155
|
+
changes['detected']['emotions'] = emotions
|
|
156
|
+
|
|
157
|
+
# 6. 与前章对比
|
|
158
|
+
if prev_chapter_path and os.path.exists(prev_chapter_path):
|
|
159
|
+
prev_raw, prev_title, prev_clean, prev_wc = read_chapter(prev_chapter_path)
|
|
160
|
+
prev_chars = set(extract_characters(prev_clean))
|
|
161
|
+
prev_locs = set(extract_locations(prev_clean))
|
|
162
|
+
continuing_chars = chapter_chars & prev_chars
|
|
163
|
+
new_in_chapter = chapter_chars - prev_chars
|
|
164
|
+
changes['detected']['continuing_characters'] = sorted(continuing_chars)
|
|
165
|
+
if new_in_chapter:
|
|
166
|
+
changes['detected']['new_vs_prev'] = sorted(new_in_chapter)
|
|
167
|
+
|
|
168
|
+
# 7. 伏笔表状态
|
|
169
|
+
known_hooks = _load_known_hooks(truth_dir)
|
|
170
|
+
changes['detected']['hook_status'] = {
|
|
171
|
+
'pending_count': len(known_hooks['pending']),
|
|
172
|
+
'recovered_count': len(known_hooks['recovered'])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# 汇总
|
|
176
|
+
change_count = len([v for v in changes['detected'].values() if v])
|
|
177
|
+
changes['summary'] = {
|
|
178
|
+
'total_changes': change_count,
|
|
179
|
+
'has_new_characters': 'new_characters' in changes['detected'],
|
|
180
|
+
'has_new_locations': 'new_locations' in changes['detected'],
|
|
181
|
+
'has_hook_keywords': 'hook_keywords' in changes['detected'],
|
|
182
|
+
'has_emotions': 'emotions' in changes['detected'],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return changes
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
parser = argparse.ArgumentParser(description='真相文件变更检测')
|
|
190
|
+
parser.add_argument('chapter', help='定稿章节文件路径')
|
|
191
|
+
parser.add_argument('--truth-dir', '-t', required=True, help='真相文件目录')
|
|
192
|
+
parser.add_argument('--prev', '-p', help='前一章文件路径')
|
|
193
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
|
|
196
|
+
changes = detect_changes(args.chapter, args.truth_dir, args.prev)
|
|
197
|
+
|
|
198
|
+
if args.json:
|
|
199
|
+
print(json.dumps(changes, ensure_ascii=False, indent=2))
|
|
200
|
+
else:
|
|
201
|
+
if 'error' in changes:
|
|
202
|
+
print(f"错误: {changes['error']}")
|
|
203
|
+
return
|
|
204
|
+
print(f"=== 变更检测: {changes['chapter']} ({changes['title']}) ===\n")
|
|
205
|
+
|
|
206
|
+
detected = changes['detected']
|
|
207
|
+
if 'new_characters' in detected:
|
|
208
|
+
print(f"新角色: {', '.join(detected['new_characters'])}")
|
|
209
|
+
if 'new_locations' in detected:
|
|
210
|
+
print(f"新地点: {', '.join(detected['new_locations'])}")
|
|
211
|
+
if 'hook_keywords' in detected:
|
|
212
|
+
for htype, keywords in detected['hook_keywords'].items():
|
|
213
|
+
print(f"伏笔({htype}): {', '.join(keywords)}")
|
|
214
|
+
if 'chapter_hook' in detected:
|
|
215
|
+
print(f"章末钩子: {detected['chapter_hook']['type']}")
|
|
216
|
+
if 'emotions' in detected:
|
|
217
|
+
for emotion, count in detected['emotions'].items():
|
|
218
|
+
print(f"情感({emotion}): {count}次")
|
|
219
|
+
if 'continuing_characters' in detected:
|
|
220
|
+
print(f"延续角色: {', '.join(detected['continuing_characters'])}")
|
|
221
|
+
|
|
222
|
+
if changes['summary']['total_changes'] == 0:
|
|
223
|
+
print("未检测到需要更新的内容")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == '__main__':
|
|
227
|
+
main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
真相文件管理器
|
|
5
|
+
解析 truth-files 目录,快速查看角色、伏笔、世界观、力量体系等实体信息。
|
|
6
|
+
供 `/novel-maker memory entity` 指令使用。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
15
|
+
from nm_utils import read_truth_section, count_chinese
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TRUTH_FILES = {
|
|
19
|
+
'characters': '角色档案',
|
|
20
|
+
'world-setting': '世界观',
|
|
21
|
+
'power-system': '力量体系',
|
|
22
|
+
'current-state': '当前状态',
|
|
23
|
+
'pending-hooks': '伏笔表',
|
|
24
|
+
'constitution': '创作宪法',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scan_truth_files(truth_dir):
|
|
29
|
+
"""Scan all truth files and return structured data."""
|
|
30
|
+
if not os.path.isdir(truth_dir):
|
|
31
|
+
return {'error': f'目录不存在: {truth_dir}'}
|
|
32
|
+
|
|
33
|
+
result = {}
|
|
34
|
+
for key, label in TRUTH_FILES.items():
|
|
35
|
+
filepath = os.path.join(truth_dir, f'{key}.md')
|
|
36
|
+
if os.path.exists(filepath):
|
|
37
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
38
|
+
text = f.read()
|
|
39
|
+
wc = count_chinese(text)
|
|
40
|
+
sections = read_truth_section(filepath)
|
|
41
|
+
result[key] = {
|
|
42
|
+
'label': label,
|
|
43
|
+
'words': wc,
|
|
44
|
+
'sections': list(sections.keys()) if sections else [],
|
|
45
|
+
'sections_content': sections,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Extract character names from characters.md
|
|
49
|
+
if 'characters' in result:
|
|
50
|
+
content = result['characters'].get('sections_content', {})
|
|
51
|
+
char_names = []
|
|
52
|
+
for section_text in content.values():
|
|
53
|
+
for m in re.finditer(r'[*\-]?\s*\*\*?([\u4e00-\u9fff]{2,4})\*\*?', section_text):
|
|
54
|
+
name = m.group(1)
|
|
55
|
+
if name not in ('角色', '人物', '主角', '配角', '反派', '姓名'):
|
|
56
|
+
char_names.append(name)
|
|
57
|
+
result['characters']['extracted_names'] = list(set(char_names))[:20]
|
|
58
|
+
|
|
59
|
+
# Extract hook info from pending-hooks.md
|
|
60
|
+
if 'pending-hooks' in result:
|
|
61
|
+
content = result['pending-hooks'].get('sections_content', {})
|
|
62
|
+
hooks = []
|
|
63
|
+
for section_text in content.values():
|
|
64
|
+
for m in re.finditer(r'\[([^\]]+)\]', section_text):
|
|
65
|
+
hooks.append(m.group(1))
|
|
66
|
+
result['pending-hooks']['extracted_hooks'] = hooks[:10]
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
import argparse
|
|
73
|
+
parser = argparse.ArgumentParser(description="Truth files manager")
|
|
74
|
+
parser.add_argument("truth_dir", nargs='?', default=None, help="Truth files directory")
|
|
75
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
76
|
+
parser.add_argument("--entity", type=str, help="Show specific entity (characters/world/power/hooks/state)")
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
truth_dir = args.truth_dir
|
|
80
|
+
if truth_dir is None:
|
|
81
|
+
# Try common locations
|
|
82
|
+
for candidate in ['.novel-maker/truth-files', 'novels/truth-files', 'truth-files']:
|
|
83
|
+
if os.path.isdir(candidate):
|
|
84
|
+
truth_dir = candidate
|
|
85
|
+
break
|
|
86
|
+
if truth_dir is None:
|
|
87
|
+
print("错误: 未找到 truth-files 目录,请指定路径", file=sys.stderr)
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
result = scan_truth_files(truth_dir)
|
|
91
|
+
|
|
92
|
+
if args.json:
|
|
93
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if 'error' in result:
|
|
97
|
+
print(result['error'])
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
print(f"\n{'=' * 60}")
|
|
101
|
+
print(f"📁 真相文件管理 ({truth_dir})")
|
|
102
|
+
print(f"{'=' * 60}\n")
|
|
103
|
+
|
|
104
|
+
for key, info in result.items():
|
|
105
|
+
if args.entity and key != args.entity:
|
|
106
|
+
continue
|
|
107
|
+
print(f"📄 {info['label']} ({key}.md, {info['words']}字)")
|
|
108
|
+
if info['sections']:
|
|
109
|
+
print(f" 章节: {', '.join(info['sections'])}")
|
|
110
|
+
if key == 'characters' and info.get('extracted_names'):
|
|
111
|
+
print(f" 角色: {', '.join(info['extracted_names'][:10])}")
|
|
112
|
+
if key == 'pending-hooks' and info.get('extracted_hooks'):
|
|
113
|
+
print(f" 伏笔 ({len(info['extracted_hooks'])}条):")
|
|
114
|
+
for h in info['extracted_hooks'][:5]:
|
|
115
|
+
print(f" • {h}")
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|