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,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
项目统计报告
|
|
5
|
+
快速扫描 novels/ 和 .novel-maker/ 目录,输出完整的写作进度统计。
|
|
6
|
+
供 `/novel-maker stats` 指令使用。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
from collections import Counter
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
15
|
+
from nm_utils import (
|
|
16
|
+
list_chapters, read_chapter, count_chinese,
|
|
17
|
+
extract_characters, extract_locations, estimate_pacing
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scan_volume(volume_path):
|
|
22
|
+
"""Scan a single volume directory."""
|
|
23
|
+
vol_name = os.path.basename(volume_path)
|
|
24
|
+
chapters_dir = os.path.join(volume_path, 'chapters')
|
|
25
|
+
if not os.path.isdir(chapters_dir):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
chapter_files = list_chapters(chapters_dir)
|
|
29
|
+
if not chapter_files:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
chapters = []
|
|
33
|
+
all_chars = Counter()
|
|
34
|
+
all_locs = Counter()
|
|
35
|
+
pacing_dist = Counter()
|
|
36
|
+
total_words = 0
|
|
37
|
+
|
|
38
|
+
for idx, filepath in enumerate(chapter_files):
|
|
39
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
40
|
+
pacing_label, _ = estimate_pacing(raw)
|
|
41
|
+
chars = extract_characters(clean)
|
|
42
|
+
locs = extract_locations(clean)
|
|
43
|
+
|
|
44
|
+
chapters.append({
|
|
45
|
+
'num': idx + 1,
|
|
46
|
+
'title': title,
|
|
47
|
+
'words': wc,
|
|
48
|
+
'pacing': pacing_label,
|
|
49
|
+
'characters': chars[:4],
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
total_words += wc
|
|
53
|
+
for c in chars:
|
|
54
|
+
all_chars[c] += 1
|
|
55
|
+
for loc in locs:
|
|
56
|
+
all_locs[loc] += 1
|
|
57
|
+
pacing_dist[pacing_label] += 1
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
'name': vol_name,
|
|
61
|
+
'total_chapters': len(chapters),
|
|
62
|
+
'total_words': total_words,
|
|
63
|
+
'avg_words': round(total_words / max(len(chapters), 1), 0),
|
|
64
|
+
'chapters': chapters,
|
|
65
|
+
'main_characters': [name for name, _ in all_chars.most_common(8)],
|
|
66
|
+
'character_appearances': dict(all_chars.most_common(10)),
|
|
67
|
+
'main_locations': [loc for loc, _ in all_locs.most_common(5)],
|
|
68
|
+
'pacing_distribution': dict(pacing_dist),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scan_project(novels_dir):
|
|
73
|
+
"""Scan the entire novels directory."""
|
|
74
|
+
if not os.path.isdir(novels_dir):
|
|
75
|
+
return {'error': f'目录不存在: {novels_dir}'}
|
|
76
|
+
|
|
77
|
+
# Check for outline
|
|
78
|
+
outline_path = os.path.join(novels_dir, 'outline.md')
|
|
79
|
+
has_outline = os.path.exists(outline_path)
|
|
80
|
+
outline_word_count = 0
|
|
81
|
+
if has_outline:
|
|
82
|
+
with open(outline_path, 'r', encoding='utf-8') as f:
|
|
83
|
+
outline_word_count = count_chinese(f.read())
|
|
84
|
+
|
|
85
|
+
# Scan volumes
|
|
86
|
+
volumes = []
|
|
87
|
+
for entry in sorted(os.listdir(novels_dir)):
|
|
88
|
+
vol_path = os.path.join(novels_dir, entry)
|
|
89
|
+
if os.path.isdir(vol_path) and entry.startswith('volume'):
|
|
90
|
+
result = scan_volume(vol_path)
|
|
91
|
+
if result:
|
|
92
|
+
volumes.append(result)
|
|
93
|
+
|
|
94
|
+
# Aggregate stats
|
|
95
|
+
total_chapters = sum(v['total_chapters'] for v in volumes)
|
|
96
|
+
total_words = sum(v['total_words'] for v in volumes)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
'has_outline': has_outline,
|
|
100
|
+
'outline_words': outline_word_count,
|
|
101
|
+
'volumes': len(volumes),
|
|
102
|
+
'total_chapters': total_chapters,
|
|
103
|
+
'total_words': total_words,
|
|
104
|
+
'volume_details': volumes,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
import argparse
|
|
110
|
+
parser = argparse.ArgumentParser(description="Project stats report")
|
|
111
|
+
parser.add_argument("novels_dir", nargs='?', default='novels', help="Novels directory")
|
|
112
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
113
|
+
parser.add_argument("--volume", type=str, help="Show only specific volume")
|
|
114
|
+
args = parser.parse_args()
|
|
115
|
+
|
|
116
|
+
novels_dir = args.novels_dir
|
|
117
|
+
if not os.path.isdir(novels_dir):
|
|
118
|
+
# Try to find novels dir
|
|
119
|
+
base = os.path.dirname(os.path.abspath(novels_dir))
|
|
120
|
+
alt = os.path.join(base, 'novels')
|
|
121
|
+
if os.path.isdir(alt):
|
|
122
|
+
novels_dir = alt
|
|
123
|
+
else:
|
|
124
|
+
print(f"错误: 目录不存在 - {novels_dir}", file=sys.stderr)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
result = scan_project(novels_dir)
|
|
128
|
+
|
|
129
|
+
if args.json:
|
|
130
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if 'error' in result:
|
|
134
|
+
print(result['error'])
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
print(f"\n{'=' * 60}")
|
|
138
|
+
print(f"📚 NovelMaker 项目统计")
|
|
139
|
+
print(f"{'=' * 60}")
|
|
140
|
+
print(f"大纲: {'✅ 已生成' if result['has_outline'] else '❌ 未生成'} ({result['outline_words']}字)")
|
|
141
|
+
print(f"卷数: {result['volumes']}")
|
|
142
|
+
print(f"总章节: {result['total_chapters']}")
|
|
143
|
+
print(f"总字数: {result['total_words']:,}")
|
|
144
|
+
print()
|
|
145
|
+
|
|
146
|
+
for vol in result['volume_details']:
|
|
147
|
+
if args.volume and vol['name'] != args.volume:
|
|
148
|
+
continue
|
|
149
|
+
print(f" 📖 {vol['name']}")
|
|
150
|
+
print(f" 章节: {vol['total_chapters']} 章 | 字数: {vol['total_words']:,} | 平均: {vol['avg_words']}字/章")
|
|
151
|
+
print(f" 主要角色: {', '.join(vol['main_characters'][:5])}")
|
|
152
|
+
print(f" 主要场景: {', '.join(vol['main_locations'][:3])}")
|
|
153
|
+
pacing = vol['pacing_distribution']
|
|
154
|
+
pacing_str = ' | '.join(f"{k}:{v}章" for k, v in sorted(pacing.items()))
|
|
155
|
+
print(f" 节奏分布: {pacing_str}")
|
|
156
|
+
|
|
157
|
+
if args.volume == vol['name']:
|
|
158
|
+
print(f"\n 章节明细:")
|
|
159
|
+
for ch in vol['chapters']:
|
|
160
|
+
print(f" 第{ch['num']}章: {ch['title']} ({ch['words']}字, {ch['pacing']})")
|
|
161
|
+
print()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
卷级批量章节分析
|
|
5
|
+
批处理卷内所有(或最近N章),输出卷级汇总 JSON。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
from collections import Counter
|
|
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,
|
|
16
|
+
extract_locations, detect_hook, detect_hook_type_from_patterns,
|
|
17
|
+
count_chinese
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FORESHADOWING_KEYWORDS = {
|
|
21
|
+
'血脉': ['血脉', '祖先', '传承', '遗传', '血统'],
|
|
22
|
+
'身世': ['身世', '出身', '来历', '母亲', '父亲'],
|
|
23
|
+
'预言': ['预言', '命中注定', '天意', '卦象', '命格'],
|
|
24
|
+
'阴谋': ['阴谋', '暗算', '布局', '棋局', '幕后'],
|
|
25
|
+
'身份': ['身份', '真实身份', '秘密', '隐藏'],
|
|
26
|
+
'仇敌': ['仇人', '仇敌', '恩怨', '血海深仇', '宿敌'],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def analyze_volume(chapters_dir, recent_n=None):
|
|
31
|
+
chapter_files = list_chapters(chapters_dir, recent_n)
|
|
32
|
+
if not chapter_files:
|
|
33
|
+
return {'error': f'No chapters found in {chapters_dir}'}
|
|
34
|
+
|
|
35
|
+
chapter_summaries = []
|
|
36
|
+
all_characters = Counter()
|
|
37
|
+
foreshadowing_counter = Counter()
|
|
38
|
+
hook_type_counter = Counter()
|
|
39
|
+
character_matrix = {}
|
|
40
|
+
|
|
41
|
+
for idx, filepath in enumerate(chapter_files):
|
|
42
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
43
|
+
chars = extract_characters(clean)
|
|
44
|
+
hook = detect_hook_type_from_patterns(raw)
|
|
45
|
+
|
|
46
|
+
chapter_num = idx + 1
|
|
47
|
+
chapter_summaries.append({
|
|
48
|
+
'chapter': chapter_num,
|
|
49
|
+
'title': title,
|
|
50
|
+
'word_count': wc,
|
|
51
|
+
'within_range': 2200 <= wc <= 2800,
|
|
52
|
+
'characters': chars[:6],
|
|
53
|
+
'hook_type': hook,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
for c in chars:
|
|
57
|
+
all_characters[c] += 1
|
|
58
|
+
hook_type_counter[hook] += 1
|
|
59
|
+
character_matrix[f'ch{chapter_num}'] = chars
|
|
60
|
+
|
|
61
|
+
for keyword in clean:
|
|
62
|
+
for ftype, keywords in FORESHADOWING_KEYWORDS.items():
|
|
63
|
+
if any(kw in keyword for kw in keywords):
|
|
64
|
+
foreshadowing_counter[ftype] += 1
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
main_characters = [name for name, _ in all_characters.most_common(8)]
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
'total_chapters': len(chapter_files),
|
|
71
|
+
'total_words': sum(s['word_count'] for s in chapter_summaries),
|
|
72
|
+
'avg_words': round(sum(s['word_count'] for s in chapter_summaries) / max(len(chapter_summaries), 1), 0),
|
|
73
|
+
'chapters': chapter_summaries,
|
|
74
|
+
'main_characters': main_characters,
|
|
75
|
+
'character_matrix': character_matrix,
|
|
76
|
+
'foreshadowing_summary': dict(foreshadowing_counter),
|
|
77
|
+
'hook_distribution': dict(hook_type_counter),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
if len(sys.argv) < 2:
|
|
83
|
+
print('用法: python volume_batch.py <卷章节目录路径> [--recent N] [--json]')
|
|
84
|
+
return
|
|
85
|
+
chapters_dir = sys.argv[1]
|
|
86
|
+
if not os.path.isdir(chapters_dir):
|
|
87
|
+
print(f'错误: 目录不存在 - {chapters_dir}')
|
|
88
|
+
return
|
|
89
|
+
recent_n = None
|
|
90
|
+
json_only = False
|
|
91
|
+
if '--recent' in sys.argv:
|
|
92
|
+
idx = sys.argv.index('--recent')
|
|
93
|
+
if idx + 1 < len(sys.argv):
|
|
94
|
+
recent_n = int(sys.argv[idx + 1])
|
|
95
|
+
json_only = '--json' in sys.argv
|
|
96
|
+
|
|
97
|
+
result = analyze_volume(chapters_dir, recent_n)
|
|
98
|
+
|
|
99
|
+
if json_only:
|
|
100
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
101
|
+
else:
|
|
102
|
+
print(f"=== 卷级批量分析 ===\n")
|
|
103
|
+
if 'error' in result:
|
|
104
|
+
print(result['error'])
|
|
105
|
+
return
|
|
106
|
+
print(f"总章节: {result['total_chapters']} 章")
|
|
107
|
+
print(f"总字数: {result['total_words']} 字")
|
|
108
|
+
print(f"平均每章: {result['avg_words']} 字\n")
|
|
109
|
+
print(f"主要角色: {', '.join(result['main_characters'][:8])}\n")
|
|
110
|
+
print(f"钩子类型分布:")
|
|
111
|
+
for ht, count in sorted(result.get('hook_distribution', {}).items(), key=lambda x: x[1], reverse=True):
|
|
112
|
+
bar = "█" * count
|
|
113
|
+
print(f" {ht}: {bar} ({count})")
|
|
114
|
+
print(f"\n章节概览:")
|
|
115
|
+
for ch in result['chapters']:
|
|
116
|
+
status = "✓" if ch['within_range'] else "⚠"
|
|
117
|
+
print(f" {status} 第{ch['chapter']}章: {ch['title']} ({ch['word_count']}字, 角色: {', '.join(ch['characters'][:3])})")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == '__main__':
|
|
121
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
大纲快速提取
|
|
5
|
+
解析 outline.md 和 vol*/plan.md 的标题层级,输出结构化大纲树。
|
|
6
|
+
供 `/novel-maker memory outline` 指令使用。
|
|
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 parse_outline_headings, count_chinese
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_outline(filepath):
|
|
18
|
+
"""Extract structured outline from a markdown file."""
|
|
19
|
+
if not os.path.exists(filepath):
|
|
20
|
+
return None
|
|
21
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
22
|
+
text = f.read()
|
|
23
|
+
wc = count_chinese(text)
|
|
24
|
+
tree = parse_outline_headings(text)
|
|
25
|
+
return {'file': os.path.basename(filepath), 'words': wc, 'headings': tree}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_all_outlines(base_dir):
|
|
29
|
+
"""Extract all outline-related files."""
|
|
30
|
+
results = {}
|
|
31
|
+
|
|
32
|
+
# Main outline
|
|
33
|
+
outline_path = os.path.join(base_dir, 'outline.md')
|
|
34
|
+
if os.path.exists(outline_path):
|
|
35
|
+
results['outline'] = extract_outline(outline_path)
|
|
36
|
+
|
|
37
|
+
# Volume plans
|
|
38
|
+
for entry in sorted(os.listdir(base_dir)):
|
|
39
|
+
vol_path = os.path.join(base_dir, entry)
|
|
40
|
+
if os.path.isdir(vol_path) and entry.startswith('volume'):
|
|
41
|
+
plan_path = os.path.join(vol_path, 'plan.md')
|
|
42
|
+
if os.path.exists(plan_path):
|
|
43
|
+
results[entry] = extract_outline(plan_path)
|
|
44
|
+
|
|
45
|
+
# Act plans
|
|
46
|
+
act_path = os.path.join(vol_path, 'act-plan.md')
|
|
47
|
+
if os.path.exists(act_path):
|
|
48
|
+
results[f"{entry}/act-plan"] = extract_outline(act_path)
|
|
49
|
+
|
|
50
|
+
return results
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
import argparse
|
|
55
|
+
parser = argparse.ArgumentParser(description="Outline extractor")
|
|
56
|
+
parser.add_argument("base_dir", nargs='?', default='novels', help="Novels base directory")
|
|
57
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
58
|
+
parser.add_argument("--file", type=str, help="Specific file to extract")
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
|
|
61
|
+
if args.file:
|
|
62
|
+
if not os.path.isfile(args.file):
|
|
63
|
+
print(f"Error: {args.file} not found", file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
result = {args.file: extract_outline(args.file)}
|
|
66
|
+
else:
|
|
67
|
+
if not os.path.isdir(args.base_dir):
|
|
68
|
+
print(f"Error: {args.base_dir} is not a directory", file=sys.stderr)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
result = extract_all_outlines(args.base_dir)
|
|
71
|
+
|
|
72
|
+
if args.json:
|
|
73
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
74
|
+
else:
|
|
75
|
+
for key, val in result.items():
|
|
76
|
+
if val is None:
|
|
77
|
+
print(f"\n❌ {key}: 文件不存在")
|
|
78
|
+
continue
|
|
79
|
+
print(f"\n{'=' * 50}")
|
|
80
|
+
print(f"📄 {key} ({val['words']}字, {len(val['headings'])}个标题)")
|
|
81
|
+
print(f"{'=' * 50}")
|
|
82
|
+
for h in val['headings']:
|
|
83
|
+
indent = " " * (h['level'] - 1)
|
|
84
|
+
marker = {1: '📖', 2: '📑', 3: '📝', 4: '•'}.get(h['level'], '•')
|
|
85
|
+
print(f"{indent}{marker} {h['title']}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
规划师上下文包 — 一键生成规划所需的精简上下文
|
|
5
|
+
|
|
6
|
+
将规划师需要读取的 10+ 个文件压缩为一个结构化 JSON(~5000 token),
|
|
7
|
+
替代手动读取全部真相文件+大纲(~30,000 token)。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python scripts/planner/planner_context.py --volume 01 --json
|
|
11
|
+
python scripts/planner/planner_context.py --volume 01 --act 2 --json
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
20
|
+
from nm_utils import (
|
|
21
|
+
read_truth_section, list_chapters, read_chapter,
|
|
22
|
+
extract_characters, generate_summary, parse_outline_headings,
|
|
23
|
+
chapter_sort_key
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
NOVEL_MAKER_DIR = '.novel-maker'
|
|
27
|
+
TRUTH_DIR = os.path.join(NOVEL_MAKER_DIR, 'truth-files')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _find_project_root(start_path):
|
|
31
|
+
path = os.path.abspath(start_path)
|
|
32
|
+
for _ in range(10):
|
|
33
|
+
if os.path.isdir(os.path.join(path, NOVEL_MAKER_DIR)):
|
|
34
|
+
return path
|
|
35
|
+
parent = os.path.dirname(path)
|
|
36
|
+
if parent == path:
|
|
37
|
+
break
|
|
38
|
+
path = parent
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_truth_compact(filepath, max_chars=600):
|
|
43
|
+
sections = read_truth_section(filepath)
|
|
44
|
+
if not sections:
|
|
45
|
+
return None
|
|
46
|
+
result = {}
|
|
47
|
+
for title, content in sections.items():
|
|
48
|
+
if title == 'header':
|
|
49
|
+
continue
|
|
50
|
+
lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
|
|
51
|
+
compact = '\n'.join(lines[:10])
|
|
52
|
+
if len(compact) > max_chars:
|
|
53
|
+
compact = compact[:max_chars] + '...'
|
|
54
|
+
if compact:
|
|
55
|
+
result[title] = compact
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_volume_progress(volume_dir):
|
|
60
|
+
chapters_dir = os.path.join(volume_dir, 'chapters')
|
|
61
|
+
if not os.path.isdir(chapters_dir):
|
|
62
|
+
return {'total': 0, 'chapters': []}
|
|
63
|
+
all_chapters = list_chapters(chapters_dir)
|
|
64
|
+
chapters = []
|
|
65
|
+
total_wc = 0
|
|
66
|
+
for ch_path in all_chapters:
|
|
67
|
+
raw, title, clean, wc = read_chapter(ch_path)
|
|
68
|
+
ch_num = chapter_sort_key(ch_path)
|
|
69
|
+
chars = extract_characters(clean)[:5]
|
|
70
|
+
summary = generate_summary(raw, first_n=50, last_n=50)
|
|
71
|
+
chapters.append({
|
|
72
|
+
'number': ch_num,
|
|
73
|
+
'title': title,
|
|
74
|
+
'word_count': wc,
|
|
75
|
+
'characters': chars,
|
|
76
|
+
'summary': summary
|
|
77
|
+
})
|
|
78
|
+
total_wc += wc
|
|
79
|
+
return {
|
|
80
|
+
'total': len(chapters),
|
|
81
|
+
'total_word_count': total_wc,
|
|
82
|
+
'chapters': chapters[-5:] # 只返回最近5章
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_outline_context(outline_path, current_chapter=None):
|
|
87
|
+
if not os.path.exists(outline_path):
|
|
88
|
+
return None
|
|
89
|
+
with open(outline_path, encoding='utf-8') as f:
|
|
90
|
+
text = f.read()
|
|
91
|
+
headings = parse_outline_headings(text)
|
|
92
|
+
if current_chapter:
|
|
93
|
+
result = []
|
|
94
|
+
found = False
|
|
95
|
+
for h in headings:
|
|
96
|
+
if f'第{current_chapter}章' in h['title']:
|
|
97
|
+
found = True
|
|
98
|
+
if found:
|
|
99
|
+
result.append(h)
|
|
100
|
+
if len(result) >= 10:
|
|
101
|
+
break
|
|
102
|
+
if not result:
|
|
103
|
+
result = headings[-10:] if len(headings) > 10 else headings
|
|
104
|
+
else:
|
|
105
|
+
result = headings[-10:] if len(headings) > 10 else headings
|
|
106
|
+
return [{'level': h['level'], 'title': h['title']} for h in result]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_active_hooks(truth_dir):
|
|
110
|
+
fpath = os.path.join(truth_dir, 'pending-hooks.md')
|
|
111
|
+
sections = read_truth_section(fpath)
|
|
112
|
+
if not sections:
|
|
113
|
+
return []
|
|
114
|
+
hooks = []
|
|
115
|
+
for title, content in sections.items():
|
|
116
|
+
if 'header' in title or '已回收' in title or '已解决' in title:
|
|
117
|
+
continue
|
|
118
|
+
lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
|
|
119
|
+
for line in lines:
|
|
120
|
+
if 'H[' in line or '伏笔' in line or '钩子' in line:
|
|
121
|
+
hooks.append(line[:100])
|
|
122
|
+
return hooks[:10]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_active_subplots(truth_dir):
|
|
126
|
+
fpath = os.path.join(truth_dir, 'subplot-board.md')
|
|
127
|
+
sections = read_truth_section(fpath)
|
|
128
|
+
if not sections:
|
|
129
|
+
return []
|
|
130
|
+
subplots = []
|
|
131
|
+
for title, content in sections.items():
|
|
132
|
+
if '活跃' in title:
|
|
133
|
+
lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
|
|
134
|
+
for line in lines:
|
|
135
|
+
if line.startswith('-') or line.startswith('*'):
|
|
136
|
+
subplots.append(line[:100])
|
|
137
|
+
return subplots[:5]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_planner_context(volume='01', act=None, root=None, truth_dir=None):
|
|
141
|
+
if root:
|
|
142
|
+
pass
|
|
143
|
+
else:
|
|
144
|
+
root = _find_project_root('.')
|
|
145
|
+
if not root:
|
|
146
|
+
return {'error': '未找到 .novel-maker 目录'}
|
|
147
|
+
|
|
148
|
+
truth_path = truth_dir or os.path.join(root, TRUTH_DIR)
|
|
149
|
+
volume_dir = os.path.join(root, 'novels', f'volume-{volume.zfill(2)}')
|
|
150
|
+
|
|
151
|
+
context = {
|
|
152
|
+
'meta': {'volume': volume, 'act': act},
|
|
153
|
+
'volume_progress': {},
|
|
154
|
+
'truth_files': {},
|
|
155
|
+
'outline': None,
|
|
156
|
+
'active_hooks': [],
|
|
157
|
+
'active_subplots': []
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# 卷进度
|
|
161
|
+
context['volume_progress'] = _get_volume_progress(volume_dir)
|
|
162
|
+
|
|
163
|
+
# 真相文件摘要
|
|
164
|
+
truth_files = {
|
|
165
|
+
'characters': 'characters.md',
|
|
166
|
+
'current_state': 'current-state.md',
|
|
167
|
+
'world_setting': 'world-setting.md',
|
|
168
|
+
'power_system': 'power-system.md',
|
|
169
|
+
'emotional_arcs': 'emotional-arcs.md',
|
|
170
|
+
}
|
|
171
|
+
for key, filename in truth_files.items():
|
|
172
|
+
fpath = os.path.join(truth_path, filename)
|
|
173
|
+
compact = _read_truth_compact(fpath)
|
|
174
|
+
if compact:
|
|
175
|
+
context['truth_files'][key] = compact
|
|
176
|
+
|
|
177
|
+
# 大纲
|
|
178
|
+
outline_path = os.path.join(volume_dir, 'outline.md')
|
|
179
|
+
current_ch = context['volume_progress']['total'] + 1 if context['volume_progress']['total'] > 0 else None
|
|
180
|
+
context['outline'] = _get_outline_context(outline_path, current_ch)
|
|
181
|
+
|
|
182
|
+
# 活跃伏笔
|
|
183
|
+
context['active_hooks'] = _get_active_hooks(truth_path)
|
|
184
|
+
|
|
185
|
+
# 活跃支线
|
|
186
|
+
context['active_subplots'] = _get_active_subplots(truth_path)
|
|
187
|
+
|
|
188
|
+
return context
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main():
|
|
192
|
+
parser = argparse.ArgumentParser(description='规划师上下文包')
|
|
193
|
+
parser.add_argument('--volume', '-v', default='01', help='卷号(默认01)')
|
|
194
|
+
parser.add_argument('--act', '-a', type=int, help='当前幕号')
|
|
195
|
+
parser.add_argument('--root', help='项目根目录')
|
|
196
|
+
parser.add_argument('--truth-dir', help='真相文件目录')
|
|
197
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
198
|
+
args = parser.parse_args()
|
|
199
|
+
|
|
200
|
+
ctx = build_planner_context(args.volume, args.act, args.root, args.truth_dir)
|
|
201
|
+
|
|
202
|
+
if args.json:
|
|
203
|
+
print(json.dumps(ctx, ensure_ascii=False, indent=2))
|
|
204
|
+
else:
|
|
205
|
+
if 'error' in ctx:
|
|
206
|
+
print(f"错误: {ctx['error']}")
|
|
207
|
+
return
|
|
208
|
+
print(f"=== 规划师上下文: 卷{ctx['meta']['volume']} ===\n")
|
|
209
|
+
prog = ctx['volume_progress']
|
|
210
|
+
print(f"进度: {prog['total']}章, {prog.get('total_word_count', 0)}字")
|
|
211
|
+
if ctx['truth_files']:
|
|
212
|
+
print(f"真相文件: {len(ctx['truth_files'])}个")
|
|
213
|
+
if ctx['active_hooks']:
|
|
214
|
+
print(f"活跃伏笔: {len(ctx['active_hooks'])}个")
|
|
215
|
+
if ctx['active_subplots']:
|
|
216
|
+
print(f"活跃支线: {len(ctx['active_subplots'])}条")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == '__main__':
|
|
220
|
+
main()
|