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.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/bin/novel-maker.js +229 -0
  4. package/package.json +33 -0
  5. package/skill/CHANGELOG.md +82 -0
  6. package/skill/QUICK-REF.md +168 -0
  7. package/skill/README.md +75 -0
  8. package/skill/SKILL.md +715 -0
  9. package/skill/agents/README.md +59 -0
  10. package/skill/agents/auditor.md +234 -0
  11. package/skill/agents/coordinator.md +150 -0
  12. package/skill/agents/planner.md +220 -0
  13. package/skill/agents/reviewer.md +249 -0
  14. package/skill/agents/reviser.md +144 -0
  15. package/skill/agents/writer.md +213 -0
  16. package/skill/arc-templates/README.md +43 -0
  17. package/skill/arc-templates/apocalypse/ability.md +81 -0
  18. package/skill/arc-templates/apocalypse/faction.md +81 -0
  19. package/skill/arc-templates/apocalypse/humanity.md +81 -0
  20. package/skill/arc-templates/apocalypse/survival.md +81 -0
  21. package/skill/arc-templates/game/competition.md +81 -0
  22. package/skill/arc-templates/game/dungeon.md +81 -0
  23. package/skill/arc-templates/game/guild-war.md +81 -0
  24. package/skill/arc-templates/game/leveling.md +80 -0
  25. package/skill/arc-templates/general/challenge.md +44 -0
  26. package/skill/arc-templates/general/conflict.md +71 -0
  27. package/skill/arc-templates/general/explore.md +71 -0
  28. package/skill/arc-templates/general/growth.md +71 -0
  29. package/skill/arc-templates/general/mystery.md +71 -0
  30. package/skill/arc-templates/general/relation.md +71 -0
  31. package/skill/arc-templates/history/battle.md +71 -0
  32. package/skill/arc-templates/history/politics.md +71 -0
  33. package/skill/arc-templates/history/reform.md +71 -0
  34. package/skill/arc-templates/infinite-flow/boss.md +71 -0
  35. package/skill/arc-templates/infinite-flow/dungeon.md +71 -0
  36. package/skill/arc-templates/infinite-flow/enhance.md +71 -0
  37. package/skill/arc-templates/infinite-flow/team.md +71 -0
  38. package/skill/arc-templates/mystery/case.md +71 -0
  39. package/skill/arc-templates/mystery/deduction.md +71 -0
  40. package/skill/arc-templates/mystery/twist.md +71 -0
  41. package/skill/arc-templates/romance/angst.md +80 -0
  42. package/skill/arc-templates/romance/chase.md +71 -0
  43. package/skill/arc-templates/romance/slow-burn.md +71 -0
  44. package/skill/arc-templates/romance/sweet.md +80 -0
  45. package/skill/arc-templates/sci-fi/awakening.md +81 -0
  46. package/skill/arc-templates/sci-fi/breakthrough.md +81 -0
  47. package/skill/arc-templates/sci-fi/contact.md +81 -0
  48. package/skill/arc-templates/sci-fi/exploration.md +81 -0
  49. package/skill/arc-templates/urban/business.md +71 -0
  50. package/skill/arc-templates/urban/revenge.md +71 -0
  51. package/skill/arc-templates/urban/rise.md +71 -0
  52. package/skill/arc-templates/urban/romance.md +71 -0
  53. package/skill/arc-templates/western-fantasy/adventure.md +80 -0
  54. package/skill/arc-templates/western-fantasy/kingdom.md +71 -0
  55. package/skill/arc-templates/western-fantasy/magic-awakening.md +71 -0
  56. package/skill/arc-templates/western-fantasy/racial-conflict.md +71 -0
  57. package/skill/arc-templates/wuxia/breakthrough.md +71 -0
  58. package/skill/arc-templates/wuxia/grudge.md +71 -0
  59. package/skill/arc-templates/wuxia/hero-path.md +71 -0
  60. package/skill/arc-templates/wuxia/sect-war.md +71 -0
  61. package/skill/arc-templates/xianxia/breakthrough.md +71 -0
  62. package/skill/arc-templates/xianxia/dungeon.md +71 -0
  63. package/skill/arc-templates/xianxia/tournament.md +71 -0
  64. package/skill/arc-templates/xianxia/tribulation.md +71 -0
  65. package/skill/docs/examples.md +61 -0
  66. package/skill/docs/faq.md +81 -0
  67. package/skill/docs/installation.md +87 -0
  68. package/skill/docs/quickstart.md +83 -0
  69. package/skill/genre-packs/README.md +47 -0
  70. package/skill/genre-packs/_default/arc-types.md +153 -0
  71. package/skill/genre-packs/_default/rules.md +56 -0
  72. package/skill/genre-packs/_default/templates.md +135 -0
  73. package/skill/genre-packs/apocalypse/arc-types.md +109 -0
  74. package/skill/genre-packs/apocalypse/rules.md +113 -0
  75. package/skill/genre-packs/apocalypse/settings.md +106 -0
  76. package/skill/genre-packs/apocalypse/templates.md +192 -0
  77. package/skill/genre-packs/game/arc-types.md +109 -0
  78. package/skill/genre-packs/game/rules.md +113 -0
  79. package/skill/genre-packs/game/settings.md +103 -0
  80. package/skill/genre-packs/game/templates.md +173 -0
  81. package/skill/genre-packs/history/arc-types.md +109 -0
  82. package/skill/genre-packs/history/rules.md +107 -0
  83. package/skill/genre-packs/history/settings.md +126 -0
  84. package/skill/genre-packs/history/templates.md +179 -0
  85. package/skill/genre-packs/infinite-flow/arc-types.md +101 -0
  86. package/skill/genre-packs/infinite-flow/rules.md +75 -0
  87. package/skill/genre-packs/infinite-flow/settings.md +102 -0
  88. package/skill/genre-packs/infinite-flow/templates.md +226 -0
  89. package/skill/genre-packs/mystery/arc-types.md +109 -0
  90. package/skill/genre-packs/mystery/rules.md +107 -0
  91. package/skill/genre-packs/mystery/settings.md +103 -0
  92. package/skill/genre-packs/mystery/templates.md +178 -0
  93. package/skill/genre-packs/romance/arc-types.md +130 -0
  94. package/skill/genre-packs/romance/rules.md +88 -0
  95. package/skill/genre-packs/romance/settings.md +146 -0
  96. package/skill/genre-packs/romance/templates.md +245 -0
  97. package/skill/genre-packs/sci-fi/arc-types.md +109 -0
  98. package/skill/genre-packs/sci-fi/rules.md +113 -0
  99. package/skill/genre-packs/sci-fi/settings.md +99 -0
  100. package/skill/genre-packs/sci-fi/templates.md +170 -0
  101. package/skill/genre-packs/urban/arc-types.md +101 -0
  102. package/skill/genre-packs/urban/rules.md +75 -0
  103. package/skill/genre-packs/urban/settings.md +82 -0
  104. package/skill/genre-packs/urban/templates.md +212 -0
  105. package/skill/genre-packs/western-fantasy/arc-types.md +128 -0
  106. package/skill/genre-packs/western-fantasy/rules.md +88 -0
  107. package/skill/genre-packs/western-fantasy/settings.md +160 -0
  108. package/skill/genre-packs/western-fantasy/templates.md +225 -0
  109. package/skill/genre-packs/wuxia/arc-types.md +126 -0
  110. package/skill/genre-packs/wuxia/rules.md +86 -0
  111. package/skill/genre-packs/wuxia/settings.md +150 -0
  112. package/skill/genre-packs/wuxia/templates.md +195 -0
  113. package/skill/genre-packs/xianxia/arc-types.md +101 -0
  114. package/skill/genre-packs/xianxia/rules.md +74 -0
  115. package/skill/genre-packs/xianxia/settings.md +107 -0
  116. package/skill/genre-packs/xianxia/templates.md +202 -0
  117. package/skill/hooks/README.md +102 -0
  118. package/skill/hooks/chapter-complete.md +176 -0
  119. package/skill/hooks/context-injection.md +152 -0
  120. package/skill/hooks/intent-detection.md +183 -0
  121. package/skill/hooks/review-trigger.md +219 -0
  122. package/skill/hooks/summary-trigger.md +185 -0
  123. package/skill/references/act-guidance.md +228 -0
  124. package/skill/references/audit-core.md +130 -0
  125. package/skill/references/audit-dimensions.md +202 -0
  126. package/skill/references/character-voice-card.md +196 -0
  127. package/skill/references/consistency-checker.md +209 -0
  128. package/skill/references/content-expansion.md +68 -0
  129. package/skill/references/creative-constraints.md +200 -0
  130. package/skill/references/data-agent.md +286 -0
  131. package/skill/references/dialogue-writing.md +104 -0
  132. package/skill/references/editorial-perspective.md +166 -0
  133. package/skill/references/emotion-curve.md +127 -0
  134. package/skill/references/genre-rules.md +389 -0
  135. package/skill/references/golden-opening.md +81 -0
  136. package/skill/references/memory-system.md +288 -0
  137. package/skill/references/pacing-analysis.md +201 -0
  138. package/skill/references/platform-rules.md +244 -0
  139. package/skill/references/plot-structures.md +108 -0
  140. package/skill/references/reader-feedback.md +119 -0
  141. package/skill/references/rhythm-system.md +204 -0
  142. package/skill/references/style-imitation.md +193 -0
  143. package/skill/references/sweet-spot-tracking.md +182 -0
  144. package/skill/references/usage-guide.md +174 -0
  145. package/skill/references/writing-methods.md +169 -0
  146. package/skill/rules/anti-ai-expressions.md +206 -0
  147. package/skill/rules/character-voice.md +184 -0
  148. package/skill/rules/consistency-check.md +232 -0
  149. package/skill/rules/smart-query.md +263 -0
  150. package/skill/scripts/README.md +380 -0
  151. package/skill/scripts/auditor/chapter_transition.py +217 -0
  152. package/skill/scripts/auditor/consistency_scan.py +194 -0
  153. package/skill/scripts/auditor/dialogue_checker.py +194 -0
  154. package/skill/scripts/auditor/hook_report.py +115 -0
  155. package/skill/scripts/auditor/pacing_optimizer.py +303 -0
  156. package/skill/scripts/auditor/pacing_report.py +275 -0
  157. package/skill/scripts/auditor/pre_audit.py +203 -0
  158. package/skill/scripts/auditor/style_check.py +158 -0
  159. package/skill/scripts/auditor/worldbuilding_checker.py +637 -0
  160. package/skill/scripts/common/analyze.py +129 -0
  161. package/skill/scripts/common/init_guide.py +796 -0
  162. package/skill/scripts/common/install.py +169 -0
  163. package/skill/scripts/common/nm_utils.py +296 -0
  164. package/skill/scripts/common/validate.py +215 -0
  165. package/skill/scripts/coordinator/stats_report.py +165 -0
  166. package/skill/scripts/coordinator/volume_batch.py +121 -0
  167. package/skill/scripts/planner/outline_extractor.py +89 -0
  168. package/skill/scripts/planner/planner_context.py +220 -0
  169. package/skill/scripts/planner/query_engine.py +289 -0
  170. package/skill/scripts/reviewer/chapter_diff.py +143 -0
  171. package/skill/scripts/reviewer/character_arc_tracker.py +191 -0
  172. package/skill/scripts/reviewer/emotion_curve.py +340 -0
  173. package/skill/scripts/reviewer/foreshadowing_tracker.py +286 -0
  174. package/skill/scripts/reviewer/subplot_tracker.py +207 -0
  175. package/skill/scripts/reviewer/summary_generator.py +130 -0
  176. package/skill/scripts/reviewer/truth_diff.py +227 -0
  177. package/skill/scripts/reviewer/truth_manager.py +120 -0
  178. package/skill/scripts/test_scripts.py +366 -0
  179. package/skill/scripts/writer/build_write_context.py +255 -0
  180. package/skill/scripts/writer/chapter_info.py +67 -0
  181. package/skill/scripts/writer/check_wordcount.py +115 -0
  182. package/skill/scripts/writer/scene_builder.py +227 -0
  183. package/skill/scripts/writer/style_anchor.py +135 -0
  184. package/skill/styles/author-styles.md +53 -0
  185. 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
  186. 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
  187. 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
  188. 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
  189. 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
  190. 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
  191. 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
  192. 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
  193. 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
  194. 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. 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
  201. 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
  202. 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
  203. 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
  204. 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
  205. 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
  206. 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
  207. package/skill/templates/INDEX.md +48 -0
  208. package/skill/templates/act-plan.md +72 -0
  209. package/skill/templates/chapter.md +53 -0
  210. package/skill/templates/character-profile.md +107 -0
  211. package/skill/templates/character-voice.md +106 -0
  212. package/skill/templates/constitution.md +90 -0
  213. package/skill/templates/emotional-arcs.md +37 -0
  214. package/skill/templates/hook-template.md +68 -0
  215. package/skill/templates/outline.md +155 -0
  216. package/skill/templates/plot-card.md +432 -0
  217. package/skill/templates/power-system.md +124 -0
  218. package/skill/templates/presets.json +69 -0
  219. package/skill/templates/review-report.md +135 -0
  220. package/skill/templates/scene-plan.md +221 -0
  221. package/skill/templates/scene-template.md +78 -0
  222. package/skill/templates/subplot-board.md +48 -0
  223. package/skill/templates/summary-10chapters.md +79 -0
  224. package/skill/templates/summary-50chapters.md +131 -0
  225. package/skill/templates/summary-volume.md +148 -0
  226. package/skill/templates/timeline.md +37 -0
  227. package/skill/templates/volume-plan.md +44 -0
  228. package/skill/templates/world-setting.md +151 -0
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 章节衔接检查脚本
5
+ 检测章节之间的衔接问题:时间跳跃、地点突变、角色消失、情绪断裂等。
6
+ 供审计师在审查时使用,或写手在连续写作时自查。
7
+
8
+ 用法:
9
+ python scripts/auditor/chapter_transition.py 章节目录 --json
10
+ python scripts/auditor/chapter_transition.py 章节目录 --recent 5
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, generate_summary
24
+ )
25
+
26
+ # ─── 时间表达关键词 ───────────────────────────────
27
+ TIME_EXPRESSIONS = {
28
+ '瞬间': ['突然', '忽然', '刹那间', '瞬间', '立刻', '马上'],
29
+ '短期': ['第二天', '次日', '当晚', '傍晚', '清晨', '午后', '片刻', '一会儿'],
30
+ '中期': ['几天后', '一周后', '半月后', '一个月后', '数月后'],
31
+ '长期': ['一年后', '数年后', '多年后', '几年后', '十年后']
32
+ }
33
+
34
+ # ─── 地点转换关键词 ───────────────────────────────
35
+ LOCATION_TRANSITIONS = ['来到', '到达', '回到', '前往', '进入', '离开', '返回', '抵达']
36
+
37
+
38
+ def analyze_chapter_ending(filepath, last_n=500):
39
+ """Analyze the ending of a chapter."""
40
+ raw, title, clean, wc = read_chapter(filepath)
41
+ tail = clean[-last_n:] if len(clean) > last_n else clean
42
+
43
+ # Extract ending characters
44
+ ending_chars = extract_characters(tail)
45
+
46
+ # Extract ending locations
47
+ ending_locs = extract_locations(tail)
48
+
49
+ # Detect hook
50
+ hook = detect_hook(raw)
51
+
52
+ # Detect time expressions
53
+ time_expressions = []
54
+ for category, keywords in TIME_EXPRESSIONS.items():
55
+ for kw in keywords:
56
+ if kw in tail:
57
+ time_expressions.append({'category': category, 'keyword': kw})
58
+
59
+ return {
60
+ 'title': title,
61
+ 'word_count': wc,
62
+ 'ending_chars': ending_chars[:5],
63
+ 'ending_locations': ending_locs[:3],
64
+ 'hook': hook['type'],
65
+ 'time_expressions': time_expressions,
66
+ 'tail_preview': tail[-100:]
67
+ }
68
+
69
+
70
+ def analyze_chapter_beginning(filepath, first_n=500):
71
+ """Analyze the beginning of a chapter."""
72
+ raw, title, clean, wc = read_chapter(filepath)
73
+ head = clean[:first_n] if len(clean) > first_n else clean
74
+
75
+ # Extract beginning characters
76
+ beginning_chars = extract_characters(head)
77
+
78
+ # Extract beginning locations
79
+ beginning_locs = extract_locations(head)
80
+
81
+ # Detect time expressions
82
+ time_expressions = []
83
+ for category, keywords in TIME_EXPRESSIONS.items():
84
+ for kw in keywords:
85
+ if kw in head:
86
+ time_expressions.append({'category': category, 'keyword': kw})
87
+
88
+ # Detect location transitions
89
+ location_transitions = []
90
+ for transition in LOCATION_TRANSITIONS:
91
+ if transition in head:
92
+ location_transitions.append(transition)
93
+
94
+ return {
95
+ 'title': title,
96
+ 'word_count': wc,
97
+ 'beginning_chars': beginning_chars[:5],
98
+ 'beginning_locations': beginning_locs[:3],
99
+ 'time_expressions': time_expressions,
100
+ 'location_transitions': location_transitions,
101
+ 'head_preview': head[:100]
102
+ }
103
+
104
+
105
+ def check_transition(chapter_files):
106
+ """Check transitions between consecutive chapters."""
107
+ transitions = []
108
+
109
+ for i in range(len(chapter_files) - 1):
110
+ curr_file = chapter_files[i]
111
+ next_file = chapter_files[i + 1]
112
+
113
+ curr_ending = analyze_chapter_ending(curr_file)
114
+ next_beginning = analyze_chapter_beginning(next_file)
115
+
116
+ issues = []
117
+
118
+ # 1. Character continuity check
119
+ curr_chars = set(curr_ending['ending_chars'])
120
+ next_chars = set(next_beginning['beginning_chars'])
121
+ if curr_chars and not (curr_chars & next_chars):
122
+ issues.append({
123
+ 'type': '角色断裂',
124
+ 'message': f"上章结尾角色({', '.join(curr_ending['ending_chars'][:3])})未在下章开头出现",
125
+ 'severity': 'warning'
126
+ })
127
+
128
+ # 2. Location continuity check
129
+ curr_locs = set(curr_ending['ending_locations'])
130
+ next_locs = set(next_beginning['beginning_locations'])
131
+ if curr_locs and not (curr_locs & next_locs) and not next_beginning['location_transitions']:
132
+ issues.append({
133
+ 'type': '地点突变',
134
+ 'message': f"上章地点({', '.join(curr_ending['ending_locations'][:2])})到下章地点({', '.join(next_beginning['beginning_locations'][:2])})无过渡",
135
+ 'severity': 'info'
136
+ })
137
+
138
+ # 3. Time expression check
139
+ if next_beginning['time_expressions']:
140
+ time_cats = [t['category'] for t in next_beginning['time_expressions']]
141
+ if '长期' in time_cats:
142
+ issues.append({
143
+ 'type': '时间跳跃',
144
+ 'message': f"下章开头有长期时间跳跃({', '.join(t['keyword'] for t in next_beginning['time_expressions'] if t['category']=='长期')})",
145
+ 'severity': 'info'
146
+ })
147
+
148
+ # 4. Hook resolution check
149
+ if curr_ending['hook'] != '未检测':
150
+ issues.append({
151
+ 'type': '钩子待回收',
152
+ 'message': f"上章钩子类型: {curr_ending['hook']}",
153
+ 'severity': 'info'
154
+ })
155
+
156
+ transitions.append({
157
+ 'from_chapter': curr_ending['title'],
158
+ 'to_chapter': next_beginning['title'],
159
+ 'issues': issues,
160
+ 'issue_count': len(issues)
161
+ })
162
+
163
+ return transitions
164
+
165
+
166
+ def main():
167
+ parser = argparse.ArgumentParser(description='章节衔接检查脚本')
168
+ parser.add_argument('chapters_dir', help='章节目录路径')
169
+ parser.add_argument('--recent', type=int, help='仅检查最近N章的衔接')
170
+ parser.add_argument('--json', action='store_true', help='JSON输出')
171
+ args = parser.parse_args()
172
+
173
+ if not os.path.isdir(args.chapters_dir):
174
+ print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
175
+ sys.exit(1)
176
+
177
+ chapter_files = list_chapters(args.chapters_dir, args.recent)
178
+ if len(chapter_files) < 2:
179
+ print("错误: 至少需要2个章节才能检查衔接", file=sys.stderr)
180
+ sys.exit(1)
181
+
182
+ transitions = check_transition(chapter_files)
183
+
184
+ # Calculate summary
185
+ total_issues = sum(t['issue_count'] for t in transitions)
186
+ warning_count = sum(1 for t in transitions for i in t['issues'] if i['severity'] == 'warning')
187
+
188
+ result = {
189
+ 'total_chapters': len(chapter_files),
190
+ 'transitions_checked': len(transitions),
191
+ 'total_issues': total_issues,
192
+ 'warning_count': warning_count,
193
+ 'transitions': transitions
194
+ }
195
+
196
+ if args.json:
197
+ print(json.dumps(result, ensure_ascii=False, indent=2))
198
+ else:
199
+ print(f"\n=== 章节衔接检查 ===\n")
200
+ print(f"章节数: {result['total_chapters']}")
201
+ print(f"检查衔接数: {result['transitions_checked']}")
202
+ print(f"问题总数: {result['total_issues']}")
203
+ print(f"警告数: {result['warning_count']}")
204
+
205
+ print(f"\n衔接详情:")
206
+ for trans in transitions:
207
+ if trans['issues']:
208
+ print(f"\n {trans['from_chapter']} → {trans['to_chapter']}:")
209
+ for issue in trans['issues']:
210
+ icon = '⚠' if issue['severity'] == 'warning' else 'ℹ'
211
+ print(f" {icon} {issue['type']}: {issue['message']}")
212
+ else:
213
+ print(f"\n ✓ {trans['from_chapter']} → {trans['to_chapter']}: 无问题")
214
+
215
+
216
+ if __name__ == '__main__':
217
+ main()
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 一致性扫描
5
+ 提取章节中的角色、等级、地点,与真相文件比对,输出冲突报告。
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import re
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, extract_characters, extract_locations
17
+ )
18
+
19
+
20
+ def read_truth_file(filepath):
21
+ """Read a truth file and return its content."""
22
+ if not os.path.exists(filepath):
23
+ return None
24
+ try:
25
+ with open(filepath, 'r', encoding='utf-8') as f:
26
+ return f.read()
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ def extract_characters_from_truth(text):
32
+ """Extract character names from truth files."""
33
+ if not text:
34
+ return set()
35
+ names = set()
36
+ for m in re.finditer(r'[#|*\-]*\s*([\u4e00-\u9fff]{2,4})(?:[::]\s*[^\n]*|的角色)', text):
37
+ name = m.group(1)
38
+ if not name.startswith(('#', '!', ' ', '-', '|', '*')):
39
+ names.add(name)
40
+ return names
41
+
42
+
43
+ def extract_power_levels(text):
44
+ """Extract power level mentions from text."""
45
+ levels = {}
46
+ for m in re.finditer(r'([\u4e00-\u9fff]{2,4})[::]?\s*([\u4e00-\u9fff]+(?:[初期|中期|后期|巅峰|大圆满]*))', text):
47
+ name, level = m.group(1), m.group(2)
48
+ if len(name) >= 2 and len(name) <= 4:
49
+ levels[name] = level
50
+ return levels
51
+
52
+
53
+ def extract_power_mentions(text):
54
+ """Extract power level mentions from chapter."""
55
+ mentions = {}
56
+ for m in re.finditer(r'([\u4e00-\u9fff]{2,4})(?:已是|达到|突破到|升至|晋升为|修为|境界)\s*([\u4e00-\u9fff]{2,10})', text):
57
+ mentions[m.group(1)] = m.group(2)
58
+ return mentions
59
+
60
+
61
+ def scan_volume(chapters_dir, truth_dir, recent_n=None):
62
+ """Scan all chapters and compare with truth files."""
63
+ chapter_files = list_chapters(chapters_dir, recent_n)
64
+ if not chapter_files:
65
+ return {'error': f'No chapters found in {chapters_dir}'}
66
+
67
+ characters_md = read_truth_file(os.path.join(truth_dir, 'characters.md'))
68
+ current_state = read_truth_file(os.path.join(truth_dir, 'current-state.md'))
69
+
70
+ truth_text = ""
71
+ if characters_md:
72
+ truth_text += characters_md
73
+ if current_state:
74
+ truth_text += "\n" + current_state
75
+
76
+ truth_chars = extract_characters_from_truth(truth_text)
77
+ truth_levels = extract_power_levels(truth_text)
78
+
79
+ results = []
80
+ total_issues = []
81
+ new_chars_all = set()
82
+ level_conflicts_all = []
83
+
84
+ for idx, filepath in enumerate(chapter_files):
85
+ raw, title, clean, wc = read_chapter(filepath)
86
+ chapter_chars = extract_characters(clean)
87
+ power_mentions = extract_power_mentions(raw)
88
+
89
+ issues = []
90
+ new_chars = [c for c in chapter_chars if c not in truth_chars]
91
+ level_conflicts = []
92
+ for char, level in power_mentions.items():
93
+ if char in truth_levels and truth_levels[char] != level:
94
+ level_conflicts.append({
95
+ "character": char,
96
+ "truth_level": truth_levels[char],
97
+ "mentioned_level": level
98
+ })
99
+
100
+ if new_chars:
101
+ issues.append({
102
+ "type": "new_character",
103
+ "severity": "info",
104
+ "message": f"新角色出场: {', '.join(new_chars[:5])}"
105
+ })
106
+
107
+ for lc in level_conflicts:
108
+ issues.append({
109
+ "type": "level_conflict",
110
+ "severity": "warning",
111
+ "message": f"{lc['character']} 等级冲突: 真相文件={lc['truth_level']}, 正文={lc['mentioned_level']}"
112
+ })
113
+
114
+ results.append({
115
+ "chapter": idx + 1,
116
+ "title": title,
117
+ "word_count": wc,
118
+ "characters": chapter_chars[:10],
119
+ "issues": issues,
120
+ "new_characters": new_chars,
121
+ "level_conflicts": level_conflicts,
122
+ })
123
+ total_issues.extend(issues)
124
+ new_chars_all.update(new_chars)
125
+ level_conflicts_all.extend(level_conflicts)
126
+
127
+ return {
128
+ "total_chapters": len(results),
129
+ "truth_files_available": {
130
+ "characters.md": characters_md is not None,
131
+ "current-state.md": current_state is not None,
132
+ },
133
+ "truth_characters_count": len(truth_chars),
134
+ "truth_levels_count": len(truth_levels),
135
+ "chapters": [{"chapter": r["chapter"], "title": r["title"], "word_count": r["word_count"],
136
+ "characters": r["characters"], "issues": r["issues"]} for r in results],
137
+ "summary": {
138
+ "total_issues": len(total_issues),
139
+ "new_characters": sorted(new_chars_all),
140
+ "level_conflicts": level_conflicts_all,
141
+ "warnings": [i for i in total_issues if i["severity"] == "warning"],
142
+ "info": [i for i in total_issues if i["severity"] == "info"],
143
+ }
144
+ }
145
+
146
+
147
+ def main():
148
+ import argparse
149
+ parser = argparse.ArgumentParser(description="Consistency scan")
150
+ parser.add_argument("chapters_dir", help="Chapters directory")
151
+ parser.add_argument("truth_dir", nargs='?', default=None, help="Truth files directory")
152
+ parser.add_argument("--recent", type=int, help="Only scan last N chapters")
153
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
154
+ args = parser.parse_args()
155
+
156
+ if not os.path.isdir(args.chapters_dir):
157
+ print(f"Error: {args.chapters_dir} is not a directory", file=sys.stderr)
158
+ sys.exit(1)
159
+
160
+ truth_dir = args.truth_dir
161
+ if truth_dir is None:
162
+ truth_dir = os.path.join(os.path.dirname(args.chapters_dir.rstrip('/')), 'truth-files')
163
+
164
+ result = scan_volume(args.chapters_dir, truth_dir, recent_n=args.recent)
165
+
166
+ if args.json:
167
+ print(json.dumps(result, ensure_ascii=False, indent=2))
168
+ else:
169
+ print(f"=== 一致性扫描报告 (最近{result.get('total_chapters', 0)}章) ===\n")
170
+ if 'error' in result:
171
+ print(result['error'])
172
+ return
173
+ chars_ok = '✅' if result['truth_files_available']['characters.md'] else '❌'
174
+ state_ok = '✅' if result['truth_files_available']['current-state.md'] else '❌'
175
+ print(f"真相文件: characters.md={chars_ok} current-state.md={state_ok}")
176
+ print(f"已知角色: {result['truth_characters_count']}个 | 已知等级: {result['truth_levels_count']}条\n")
177
+ if result["summary"]["warnings"]:
178
+ print("⚠ 等级冲突:")
179
+ for w in result["summary"]["warnings"]:
180
+ print(f" {w['message']}")
181
+ if result["summary"]["new_characters"]:
182
+ print(f"\n📝 新角色 ({len(result['summary']['new_characters'])}个):")
183
+ for c in result["summary"]["new_characters"][:10]:
184
+ print(f" {c}")
185
+ if not result["summary"]["warnings"] and not result["summary"]["new_characters"]:
186
+ print("✅ 无一致性问题")
187
+ print()
188
+ for ch in result["chapters"]:
189
+ issues_str = f" [{len(ch['issues'])} issue(s)]" if ch["issues"] else ""
190
+ print(f" 第{ch['chapter']}章: {ch['title']} ({ch['word_count']}字, 角色: {', '.join(ch['characters'][:3])}){issues_str}")
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 对话质量检查脚本
5
+ 检测对话中的常见问题:重复用词、AI味表达、角色声音不一致等。
6
+ 供审计师在审查时使用,或写手自查。
7
+
8
+ 用法:
9
+ python scripts/auditor/dialogue_checker.py 章节文件 --json
10
+ python scripts/auditor/dialogue_checker.py 章节文件 --verbose
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ from collections import Counter
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
21
+ from nm_utils import read_chapter, extract_characters
22
+
23
+
24
+ # ─── 对话提取 ───────────────────────────────
25
+ def extract_dialogues(text):
26
+ """Extract all dialogues from text."""
27
+ dialogues = []
28
+ # 匹配 "xxx" 或 "xxx" 格式
29
+ for m in re.finditer(r'["\u201c]([^"\u201d]+)["\u201d]', text):
30
+ content = m.group(1).strip()
31
+ if content:
32
+ dialogues.append(content)
33
+ return dialogues
34
+
35
+
36
+ # ─── 重复用词检测 ──────────────────────────────
37
+ def check_repetitive_words(dialogues, threshold=3):
38
+ """Check for repetitive words in dialogues."""
39
+ word_counter = Counter()
40
+ for dialogue in dialogues:
41
+ # 分词(简单按2-3字切分)
42
+ words = re.findall(r'[\u4e00-\u9fff]{2,3}', dialogue)
43
+ word_counter.update(words)
44
+
45
+ repetitive = []
46
+ for word, count in word_counter.most_common():
47
+ if count >= threshold and word not in ['说道', '问道', '笑道']:
48
+ repetitive.append({'word': word, 'count': count})
49
+
50
+ return repetitive[:10] # 返回前10个
51
+
52
+
53
+ # ─── AI味表达检测 ───────────────────────────────
54
+ _AI_PATTERNS = [
55
+ (r'(?:心中|暗[自]?|心[中里])\s*(?:想|道|思忖|思虑)', '内心独白过多'),
56
+ (r'(?:嘴角|唇角)\s*(?:勾起|上扬|微[微扬])', '嘴角勾起套路'),
57
+ (r'(?:眼眸|眼睛)\s*(?:闪过|掠过)\s*(?:一丝|一抹)', '眼眸闪过套路'),
58
+ (r'(?:淡淡|冷冷|微微)\s*(?:一笑|说道|道)', '副词+动词套路'),
59
+ (r'(?:不禁|不由得)\s*(?:心中|感到)', '不禁/不由得套路'),
60
+ (r'(?:只见|但见)', '只见/但见套路'),
61
+ (r'(?:原来|竟然|居然)\s*(?:是|如此)', '原来/竟然套路'),
62
+ ]
63
+
64
+
65
+ def check_ai_patterns(dialogues):
66
+ """Check for AI-style patterns in dialogues."""
67
+ issues = []
68
+ for pattern, description in _AI_PATTERNS:
69
+ for i, dialogue in enumerate(dialogues):
70
+ if re.search(pattern, dialogue):
71
+ issues.append({
72
+ 'pattern': description,
73
+ 'example': dialogue[:50],
74
+ 'index': i
75
+ })
76
+ return issues[:10] # 返回前10个
77
+
78
+
79
+ # ─── 对话长度分析 ───────────────────────────────
80
+ def analyze_dialogue_length(dialogues):
81
+ """Analyze dialogue length distribution."""
82
+ if not dialogues:
83
+ return {'error': '未检测到对话'}
84
+
85
+ lengths = [len(d) for d in dialogues]
86
+ avg_length = sum(lengths) / len(lengths)
87
+
88
+ # 统计分布
89
+ short = sum(1 for l in lengths if l < 10)
90
+ medium = sum(1 for l in lengths if 10 <= l < 30)
91
+ long = sum(1 for l in lengths if 30 <= l < 60)
92
+ very_long = sum(1 for l in lengths if l >= 60)
93
+
94
+ return {
95
+ 'total_dialogues': len(dialogues),
96
+ 'avg_length': round(avg_length, 1),
97
+ 'distribution': {
98
+ 'short(<10字)': short,
99
+ 'medium(10-30字)': medium,
100
+ 'long(30-60字)': long,
101
+ 'very_long(>60字)': very_long
102
+ },
103
+ 'warnings': []
104
+ }
105
+
106
+
107
+ # ─── 主检查函数 ───────────────────────────────
108
+ def check_dialogue_quality(filepath, verbose=False):
109
+ """Check dialogue quality in a chapter file."""
110
+ if not os.path.exists(filepath):
111
+ return {'error': f'文件不存在: {filepath}'}
112
+
113
+ raw, title, clean, wc = read_chapter(filepath)
114
+ dialogues = extract_dialogues(clean)
115
+
116
+ result = {
117
+ 'file': os.path.basename(filepath),
118
+ 'title': title,
119
+ 'word_count': wc,
120
+ 'dialogue_count': len(dialogues),
121
+ 'dialogue_ratio': round(len(dialogues) / max(wc, 1) * 100, 1),
122
+ 'checks': {}
123
+ }
124
+
125
+ # 1. 重复用词检测
126
+ repetitive = check_repetitive_words(dialogues)
127
+ result['checks']['repetitive_words'] = {
128
+ 'count': len(repetitive),
129
+ 'items': repetitive,
130
+ 'severity': 'warning' if len(repetitive) > 3 else 'info'
131
+ }
132
+
133
+ # 2. AI味表达检测
134
+ ai_patterns = check_ai_patterns(dialogues)
135
+ result['checks']['ai_patterns'] = {
136
+ 'count': len(ai_patterns),
137
+ 'items': ai_patterns,
138
+ 'severity': 'warning' if len(ai_patterns) > 5 else 'info'
139
+ }
140
+
141
+ # 3. 对话长度分析
142
+ length_analysis = analyze_dialogue_length(dialogues)
143
+ result['checks']['length_analysis'] = length_analysis
144
+
145
+ # 4. 总体评估
146
+ total_issues = len(repetitive) + len(ai_patterns)
147
+ result['overall'] = {
148
+ 'score': max(0, 100 - total_issues * 5),
149
+ 'grade': '优秀' if total_issues < 5 else '良好' if total_issues < 10 else '需改进',
150
+ 'issue_count': total_issues
151
+ }
152
+
153
+ if verbose:
154
+ result['sample_dialogues'] = dialogues[:5]
155
+
156
+ return result
157
+
158
+
159
+ def main():
160
+ parser = argparse.ArgumentParser(description='对话质量检查脚本')
161
+ parser.add_argument('filepath', help='章节文件路径')
162
+ parser.add_argument('--json', action='store_true', help='JSON输出')
163
+ parser.add_argument('--verbose', '-v', action='store_true', help='显示详细信息')
164
+ args = parser.parse_args()
165
+
166
+ result = check_dialogue_quality(args.filepath, args.verbose)
167
+
168
+ if args.json:
169
+ print(json.dumps(result, ensure_ascii=False, indent=2))
170
+ else:
171
+ if 'error' in result:
172
+ print(result['error'])
173
+ return
174
+
175
+ print(f"\n=== 对话质量检查: {result['title']} ===\n")
176
+ print(f"字数: {result['word_count']}")
177
+ print(f"对话数: {result['dialogue_count']}")
178
+ print(f"对话占比: {result['dialogue_ratio']}%")
179
+
180
+ print(f"\n检查结果:")
181
+ for check_name, check_result in result['checks'].items():
182
+ count = check_result.get('count', 0)
183
+ severity = check_result.get('severity', 'info')
184
+ icon = '⚠' if severity == 'warning' else '✓'
185
+ print(f" {icon} {check_name}: {count}个问题")
186
+
187
+ print(f"\n总体评估:")
188
+ print(f" 得分: {result['overall']['score']}/100")
189
+ print(f" 等级: {result['overall']['grade']}")
190
+ print(f" 问题总数: {result['overall']['issue_count']}")
191
+
192
+
193
+ if __name__ == '__main__':
194
+ main()