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,203 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 预审计管线 — 一键运行所有可自动化的审计维度
5
+
6
+ 将审计师需要手动执行的 5 个检查脚本整合为一个命令,
7
+ 输出结构化 JSON,审计师只需读取结果做 AI 判断,无需读取全文。
8
+
9
+ 用法:
10
+ python scripts/auditor/pre_audit.py novels/volume-01/chapters/ch15.md
11
+ python scripts/auditor/pre_audit.py novels/volume-01/chapters/ch15.md --json
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import sys
18
+ import subprocess
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
21
+ from nm_utils import (
22
+ read_chapter, extract_characters, detect_hook,
23
+ detect_structure, estimate_pacing,
24
+ clean_markdown, generate_summary
25
+ )
26
+
27
+
28
+ def _run_script(script_name, args_list):
29
+ """运行子脚本并返回结果"""
30
+ script_path = os.path.join(os.path.dirname(__file__), script_name)
31
+ if not os.path.exists(script_path):
32
+ return {'error': f'{script_name} 不存在'}
33
+ try:
34
+ result = subprocess.run(
35
+ [sys.executable, script_path] + args_list,
36
+ capture_output=True, text=True, encoding='utf-8', timeout=30
37
+ )
38
+ if result.returncode == 0:
39
+ try:
40
+ return json.loads(result.stdout)
41
+ except json.JSONDecodeError:
42
+ return {'output': result.stdout.strip()}
43
+ else:
44
+ return {'error': result.stderr.strip() or f'{script_name} 返回码 {result.returncode}'}
45
+ except subprocess.TimeoutExpired:
46
+ return {'error': f'{script_name} 执行超时'}
47
+ except Exception as e:
48
+ return {'error': str(e)}
49
+
50
+
51
+ def _check_wordcount(chapter_path, min_words=2000, max_words=4000):
52
+ """字数检查"""
53
+ raw, title, clean, wc = read_chapter(chapter_path)
54
+ status = 'OK'
55
+ if wc < min_words:
56
+ status = f'不足({wc}<{min_words})'
57
+ elif wc > max_words:
58
+ status = f'超标({wc}>{max_words})'
59
+ return {'word_count': wc, 'min': min_words, 'max': max_words, 'status': status}
60
+
61
+
62
+ def _check_hook(chapter_path):
63
+ """章末钩子检查"""
64
+ with open(chapter_path, encoding='utf-8') as f:
65
+ text = f.read()
66
+ hook = detect_hook(text)
67
+ has_hook = hook['type'] not in ('未检测', '未知')
68
+ return {
69
+ 'has_hook': has_hook,
70
+ 'hook_type': hook['type'],
71
+ 'cues': hook.get('cues', {}),
72
+ 'tail_preview': hook.get('tail_preview', '')
73
+ }
74
+
75
+
76
+ def _check_pacing(chapter_path):
77
+ """节奏评估"""
78
+ with open(chapter_path, encoding='utf-8') as f:
79
+ text = f.read()
80
+ pacing_label, pacing_score = estimate_pacing(text)
81
+ structure = detect_structure(text)
82
+ return {
83
+ 'pacing': pacing_label,
84
+ 'pacing_score': pacing_score,
85
+ 'structure': structure
86
+ }
87
+
88
+
89
+ def _check_ai_style(chapter_path):
90
+ """AI味检测(调用 style_check.py)"""
91
+ return _run_script('style_check.py', [chapter_path, '--json'])
92
+
93
+
94
+ def _check_consistency(chapter_path, truth_dir=None):
95
+ """一致性扫描(调用 consistency_scan.py)"""
96
+ args = [chapter_path]
97
+ if truth_dir:
98
+ args.extend(['--truth-dir', truth_dir])
99
+ args.append('--json')
100
+ return _run_script('consistency_scan.py', args)
101
+
102
+
103
+ def _check_characters(chapter_path):
104
+ """角色提取"""
105
+ with open(chapter_path, encoding='utf-8') as f:
106
+ text = f.read()
107
+ clean = clean_markdown(text)
108
+ chars = extract_characters(clean)
109
+ return {'characters': chars, 'count': len(chars)}
110
+
111
+
112
+ def _check_summary(chapter_path):
113
+ """章节摘要"""
114
+ with open(chapter_path, encoding='utf-8') as f:
115
+ text = f.read()
116
+ summary = generate_summary(text, first_n=150, last_n=150)
117
+ return summary
118
+
119
+
120
+ def run_pre_audit(chapter_path, truth_dir=None, min_words=2000, max_words=4000):
121
+ """运行完整预审计管线"""
122
+ if not os.path.exists(chapter_path):
123
+ return {'error': f'章节文件不存在: {chapter_path}'}
124
+
125
+ results = {
126
+ 'chapter': os.path.basename(chapter_path),
127
+ 'checks': {}
128
+ }
129
+
130
+ # 1. 字数检查
131
+ results['checks']['word_count'] = _check_wordcount(chapter_path, min_words, max_words)
132
+
133
+ # 2. 角色提取
134
+ results['checks']['characters'] = _check_characters(chapter_path)
135
+
136
+ # 3. 章末钩子
137
+ results['checks']['hook'] = _check_hook(chapter_path)
138
+
139
+ # 4. 节奏评估
140
+ results['checks']['pacing'] = _check_pacing(chapter_path)
141
+
142
+ # 5. 章节摘要
143
+ results['checks']['summary'] = _check_summary(chapter_path)
144
+
145
+ # 6. AI味检测
146
+ results['checks']['ai_style'] = _check_ai_style(chapter_path)
147
+
148
+ # 7. 一致性扫描(可选)
149
+ if truth_dir:
150
+ results['checks']['consistency'] = _check_consistency(chapter_path, truth_dir)
151
+
152
+ # 汇总
153
+ issues = []
154
+ wc = results['checks']['word_count']
155
+ if wc['status'] != 'OK':
156
+ issues.append(f"字数: {wc['status']}")
157
+ if not results['checks']['hook']['has_hook']:
158
+ issues.append("章末无钩子")
159
+ if results['checks']['pacing']['pacing_score'] >= 4:
160
+ issues.append(f"节奏偏高: {results['checks']['pacing']['pacing']}")
161
+
162
+ results['summary'] = {
163
+ 'total_checks': len(results['checks']),
164
+ 'issues': issues,
165
+ 'issue_count': len(issues)
166
+ }
167
+
168
+ return results
169
+
170
+
171
+ def main():
172
+ parser = argparse.ArgumentParser(description='预审计管线')
173
+ parser.add_argument('chapter', help='章节文件路径')
174
+ parser.add_argument('--truth-dir', help='真相文件目录')
175
+ parser.add_argument('--min-words', type=int, default=2000, help='最少字数(默认2000)')
176
+ parser.add_argument('--max-words', type=int, default=4000, help='最多字数(默认4000)')
177
+ parser.add_argument('--json', action='store_true', help='JSON输出')
178
+ args = parser.parse_args()
179
+
180
+ results = run_pre_audit(args.chapter, args.truth_dir, args.min_words, args.max_words)
181
+
182
+ if args.json:
183
+ print(json.dumps(results, ensure_ascii=False, indent=2))
184
+ else:
185
+ if 'error' in results:
186
+ print(f"错误: {results['error']}")
187
+ return
188
+ print(f"=== 预审计报告: {results['chapter']} ===\n")
189
+ for name, check in results['checks'].items():
190
+ if isinstance(check, dict) and 'error' not in check:
191
+ print(f"✓ {name}: OK")
192
+ elif isinstance(check, dict) and 'error' in check:
193
+ print(f"✗ {name}: {check['error']}")
194
+ if results['summary']['issues']:
195
+ print(f"\n发现 {results['summary']['issue_count']} 个问题:")
196
+ for issue in results['summary']['issues']:
197
+ print(f" - {issue}")
198
+ else:
199
+ print("\n全部通过!")
200
+
201
+
202
+ if __name__ == '__main__':
203
+ main()
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ AI写作风格检测
5
+ 检测 AI 高频词密度、连续"了/的"、句式重复、对话比例。
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 list_chapters, extract_title, count_chinese
16
+
17
+
18
+ AI_FREQ_WORDS = [
19
+ '苦涩', '复杂', '微妙', '难以言喻', '心中涌起', '内心深处',
20
+ '五味杂陈', '百感交集', '缓缓', '默默', '轻轻',
21
+ '深吸一口气', '苦笑一声', '皱了皱眉', '点了点头',
22
+ '犹如', '宛若', '好似', '映入眼帘', '传入耳中',
23
+ '不知何时', '不知不觉', '不禁', '不由得',
24
+ '一丝', '一抹', '仿佛', '似乎',
25
+ ]
26
+
27
+ AI_PATTERNS = {
28
+ "连续'了'字": re.compile(r'了.{0,3}了.{0,3}了'),
29
+ "连续'的'字": re.compile(r'的.{0,3}的.{0,3}的'),
30
+ "不禁": re.compile(r'不禁[感到觉得想起]'),
31
+ "不由得": re.compile(r'不由得[想起觉得]'),
32
+ "仿佛一般": re.compile(r'仿佛.{1,10}一般'),
33
+ "似乎样子": re.compile(r'似乎.{1,10}的样子'),
34
+ }
35
+
36
+
37
+ def analyze_chapter_style(text, filepath=''):
38
+ """Analyze a single chapter for AI-like patterns."""
39
+ word_count = count_chinese(text)
40
+ if word_count == 0:
41
+ return None
42
+
43
+ title = extract_title(text) or os.path.basename(filepath)
44
+ issues = []
45
+
46
+ for pattern_name, pattern in AI_PATTERNS.items():
47
+ matches = pattern.findall(text)
48
+ if matches:
49
+ issues.append({
50
+ "type": "ai_pattern",
51
+ "pattern": pattern_name,
52
+ "count": len(matches),
53
+ "examples": [m[:30] for m in matches[:2]]
54
+ })
55
+
56
+ word_freq = Counter(re.findall(r'[\u4e00-\u9fff]{2}', text))
57
+ top_words = word_freq.most_common(15)
58
+ repetitive = [(w, c) for w, c in top_words if c > 5 and w not in ('我们', '你们', '他们', '自己', '什么')]
59
+ for w, c in repetitive:
60
+ issues.append({
61
+ "type": "repetitive_word",
62
+ "word": w,
63
+ "count": c
64
+ })
65
+
66
+ ai_word_count = 0
67
+ ai_found_words = []
68
+ for word in AI_FREQ_WORDS:
69
+ count = len(re.findall(re.escape(word), text))
70
+ if count > 0:
71
+ ai_word_count += count
72
+ ai_found_words.append({"word": word, "count": count})
73
+
74
+ ai_density = ai_word_count / max(word_count / 1000, 1)
75
+ if ai_density > 3:
76
+ issues.append({
77
+ "type": "ai_density",
78
+ "ai_words_per_1000": round(ai_density, 1),
79
+ "found_words": ai_found_words
80
+ })
81
+
82
+ paragraphs = [p.strip() for p in text.split('\n') if p.strip() and not p.strip().startswith('#')]
83
+ dialogue_lines = sum(1 for p in paragraphs if re.match(r'^["\u201c]', p))
84
+ dialogue_ratio = dialogue_lines / max(len(paragraphs), 1) * 100
85
+
86
+ sentences = re.split(r'[。!?]', text)
87
+ avg_sentence_len = sum(len(s) for s in sentences) / max(len(sentences), 1)
88
+
89
+ return {
90
+ "file": os.path.basename(filepath),
91
+ "title": title,
92
+ "word_count": word_count,
93
+ "paragraphs": len(paragraphs),
94
+ "dialogue_ratio_pct": round(dialogue_ratio, 1),
95
+ "avg_sentence_length": round(avg_sentence_len, 1),
96
+ "ai_density_per_1000": round(ai_density, 1),
97
+ "ai_words_found": ai_found_words,
98
+ "repetitive_words": [{"word": w, "count": c} for w, c in repetitive],
99
+ "issues": issues,
100
+ "top_words": [{"word": w, "count": c} for w, c in top_words[:8]],
101
+ }
102
+
103
+
104
+ def main():
105
+ import argparse
106
+ parser = argparse.ArgumentParser(description="AI style check")
107
+ parser.add_argument("path", help="Chapter file or chapters directory")
108
+ parser.add_argument("--all", action="store_true", help="Analyze all chapters in directory")
109
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
110
+ parser.add_argument("--recent", type=int, help="Only analyze last N chapters")
111
+ args = parser.parse_args()
112
+
113
+ path = args.path
114
+ if not os.path.exists(path):
115
+ print(f"Error: {path} does not exist", file=sys.stderr)
116
+ sys.exit(1)
117
+
118
+ if os.path.isdir(path) or args.all:
119
+ if os.path.isfile(path):
120
+ chapters_dir = os.path.dirname(path)
121
+ else:
122
+ chapters_dir = path
123
+ chapter_files = list_chapters(chapters_dir, recent_n=args.recent)
124
+ else:
125
+ chapter_files = [path]
126
+
127
+ results = []
128
+ for cf in chapter_files:
129
+ try:
130
+ with open(cf, 'r', encoding='utf-8') as f:
131
+ text = f.read()
132
+ analysis = analyze_chapter_style(text, cf)
133
+ if analysis:
134
+ results.append(analysis)
135
+ except Exception as e:
136
+ if args.json:
137
+ results.append({"file": os.path.basename(cf), "error": str(e)})
138
+
139
+ if args.json:
140
+ print(json.dumps(results, ensure_ascii=False, indent=2))
141
+ else:
142
+ for r in results:
143
+ print(f"\n=== {r['title']} ({r['word_count']}字) ===")
144
+ print(f" 对话占比: {r['dialogue_ratio_pct']}% | 平均句长: {r['avg_sentence_length']}字 | AI词密度: {r['ai_density_per_1000']}/千字")
145
+ if r['issues']:
146
+ for issue in r['issues']:
147
+ if issue['type'] == 'ai_pattern':
148
+ print(f" ⚠ {issue['pattern']}: 出现{issue['count']}次")
149
+ elif issue['type'] == 'repetitive_word':
150
+ print(f" ⚠ 重复词汇: 「{issue['word']}」出现{issue['count']}次")
151
+ elif issue['type'] == 'ai_density':
152
+ print(f" ⚠ AI词密度过高: {issue['ai_words_per_1000']}/千字")
153
+ else:
154
+ print(" ✅ 无明显AI味")
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()