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,366 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 单元测试脚本
5
+ 测试所有辅助脚本的基本功能。
6
+
7
+ 用法:
8
+ python scripts/test_scripts.py
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import json
14
+ import tempfile
15
+ import shutil
16
+ from pathlib import Path
17
+
18
+ # Add common to path
19
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'common'))
20
+
21
+ # Test results
22
+ test_results = {
23
+ 'passed': 0,
24
+ 'failed': 0,
25
+ 'errors': []
26
+ }
27
+
28
+
29
+ def test_passed(test_name):
30
+ test_results['passed'] += 1
31
+ print(f" ✓ {test_name}")
32
+
33
+
34
+ def test_failed(test_name, error):
35
+ test_results['failed'] += 1
36
+ test_results['errors'].append({'test': test_name, 'error': str(error)})
37
+ print(f" ✗ {test_name}: {error}")
38
+
39
+
40
+ def create_test_chapter(filepath, title="测试章节", content=None):
41
+ """Create a test chapter file."""
42
+ if content is None:
43
+ content = f"""# {title}
44
+
45
+ 林风站在山巅,望着远方的城市。
46
+
47
+ "苏婉,你来了。"林风转身说道。
48
+
49
+ 苏婉微微一笑:"我当然会来。"
50
+
51
+ 两人相视一笑,心中都明白彼此的心意。
52
+
53
+ 突然,远处传来一声巨响,天空中出现了一道裂缝。
54
+
55
+ "不好!"林风脸色大变,"危机降临了!"
56
+
57
+ 苏婉握紧手中的宝剑:"我们一起面对!"
58
+
59
+ 林风从怀中掏出一本古老的秘籍,这是师父留给他的传承。
60
+
61
+ "据说这本秘籍具有强大的力量,但需要足够的修为才能使用。"林风说道。
62
+
63
+ 苏婉点头:"我相信你一定能突破瓶颈。"
64
+
65
+ 林风闭上眼睛,开始运功修炼。他的修为从筑基期开始提升...
66
+
67
+ 一刻钟后,林风睁开眼睛,眼中闪过一丝精光。
68
+
69
+ "我突破了!现在是金丹期!"林风兴奋地说道。
70
+
71
+ 苏婉为他高兴:"太好了!"
72
+
73
+ 但就在这时,一个神秘人出现在他们面前。
74
+
75
+ "你们以为这样就结束了吗?"神秘人冷笑道,"真正的危险才刚刚开始。"
76
+
77
+ 林风和苏婉对视一眼,都看到了对方眼中的决心。
78
+
79
+ 无论前方有什么危险,他们都会一起面对。
80
+
81
+ ---
82
+
83
+ **下章预告**:神秘人的真实身份究竟是什么?林风能否保护苏婉?"""
84
+
85
+ with open(filepath, 'w', encoding='utf-8') as f:
86
+ f.write(content)
87
+
88
+
89
+ def create_test_truth_file(filepath, content):
90
+ """Create a test truth file."""
91
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
92
+ with open(filepath, 'w', encoding='utf-8') as f:
93
+ f.write(content)
94
+
95
+
96
+ def test_scene_builder():
97
+ """Test scene_builder.py"""
98
+ print("\n=== 测试 scene_builder.py ===")
99
+ try:
100
+ from writer.scene_builder import build_scene, list_scene_types
101
+
102
+ # Test list_scene_types
103
+ result = list_scene_types()
104
+ assert 'available_types' in result
105
+ assert len(result['available_types']) > 0
106
+ test_passed('list_scene_types')
107
+
108
+ # Test build_scene
109
+ result = build_scene('冲突', ['林风', '苏婉'], '紧张')
110
+ assert 'scene_type' in result
111
+ assert result['scene_type'] == '冲突'
112
+ assert 'structure' in result
113
+ assert len(result['structure']) > 0
114
+ test_passed('build_scene with type, chars, emotion')
115
+
116
+ # Test invalid type
117
+ result = build_scene('invalid_type')
118
+ assert 'error' in result
119
+ test_passed('build_scene with invalid type')
120
+
121
+ except Exception as e:
122
+ test_failed('scene_builder', e)
123
+
124
+
125
+ def test_dialogue_checker():
126
+ """Test dialogue_checker.py"""
127
+ print("\n=== 测试 dialogue_checker.py ===")
128
+ try:
129
+ from auditor.dialogue_checker import check_dialogue_quality, extract_dialogues
130
+
131
+ # Test extract_dialogues (using ASCII quotes which the regex also matches)
132
+ text = '林风说道:"你好。"苏婉回答:"我很好。"'
133
+ dialogues = extract_dialogues(text)
134
+ assert len(dialogues) == 2
135
+ test_passed('extract_dialogues')
136
+
137
+ # Test check_dialogue_quality
138
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
139
+ f.write('# 测试章节\n\n林风说道:"你好。"苏婉回答:"我很好。"')
140
+ temp_file = f.name
141
+
142
+ try:
143
+ result = check_dialogue_quality(temp_file)
144
+ assert 'dialogue_count' in result
145
+ assert 'checks' in result
146
+ test_passed('check_dialogue_quality')
147
+ finally:
148
+ os.unlink(temp_file)
149
+
150
+ except Exception as e:
151
+ test_failed('dialogue_checker', e)
152
+
153
+
154
+ def test_character_arc_tracker():
155
+ """Test character_arc_tracker.py"""
156
+ print("\n=== 测试 character_arc_tracker.py ===")
157
+ try:
158
+ from reviewer.character_arc_tracker import build_character_arc
159
+
160
+ # Create temp directory with test chapters
161
+ temp_dir = tempfile.mkdtemp()
162
+ try:
163
+ for i in range(3):
164
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
165
+
166
+ result = build_character_arc(temp_dir, ['林风'])
167
+ assert 'total_chapters_analyzed' in result
168
+ assert result['total_chapters_analyzed'] == 3
169
+ assert 'characters_tracked' in result
170
+ test_passed('build_character_arc')
171
+ finally:
172
+ shutil.rmtree(temp_dir)
173
+
174
+ except Exception as e:
175
+ test_failed('character_arc_tracker', e)
176
+
177
+
178
+ def test_subplot_tracker():
179
+ """Test subplot_tracker.py"""
180
+ print("\n=== 测试 subplot_tracker.py ===")
181
+ try:
182
+ from reviewer.subplot_tracker import track_subplot_across_chapters
183
+
184
+ # Create temp directory with test chapters
185
+ temp_dir = tempfile.mkdtemp()
186
+ try:
187
+ for i in range(3):
188
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
189
+
190
+ result = track_subplot_across_chapters(temp_dir)
191
+ assert 'total_chapters_analyzed' in result
192
+ assert 'subplots_detected' in result
193
+ test_passed('track_subplot_across_chapters')
194
+ finally:
195
+ shutil.rmtree(temp_dir)
196
+
197
+ except Exception as e:
198
+ test_failed('subplot_tracker', e)
199
+
200
+
201
+ def test_chapter_transition():
202
+ """Test chapter_transition.py"""
203
+ print("\n=== 测试 chapter_transition.py ===")
204
+ try:
205
+ from auditor.chapter_transition import check_transition, list_chapters
206
+
207
+ # Create temp directory with test chapters
208
+ temp_dir = tempfile.mkdtemp()
209
+ try:
210
+ for i in range(3):
211
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
212
+
213
+ chapter_files = list_chapters(temp_dir)
214
+ assert len(chapter_files) == 3
215
+
216
+ transitions = check_transition(chapter_files)
217
+ assert len(transitions) == 2 # 3 chapters = 2 transitions
218
+ test_passed('check_transition')
219
+ finally:
220
+ shutil.rmtree(temp_dir)
221
+
222
+ except Exception as e:
223
+ test_failed('chapter_transition', e)
224
+
225
+
226
+ def test_emotion_curve():
227
+ """Test emotion_curve.py"""
228
+ print("\n=== 测试 emotion_curve.py ===")
229
+ try:
230
+ from reviewer.emotion_curve import analyze_emotion_in_text, build_emotion_curve
231
+
232
+ # Test analyze_emotion_in_text
233
+ text = '林风很开心,苏婉很悲伤,两人心情复杂。'
234
+ result = analyze_emotion_in_text(text)
235
+ assert 'dominant' in result
236
+ assert 'scores' in result
237
+ test_passed('analyze_emotion_in_text')
238
+
239
+ # Test build_emotion_curve
240
+ temp_dir = tempfile.mkdtemp()
241
+ try:
242
+ for i in range(3):
243
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
244
+
245
+ result = build_emotion_curve(temp_dir)
246
+ assert 'total_chapters' in result
247
+ assert 'curve' in result
248
+ test_passed('build_emotion_curve')
249
+ finally:
250
+ shutil.rmtree(temp_dir)
251
+
252
+ except Exception as e:
253
+ test_failed('emotion_curve', e)
254
+
255
+
256
+ def test_foreshadowing_tracker():
257
+ """Test foreshadowing_tracker.py"""
258
+ print("\n=== 测试 foreshadowing_tracker.py ===")
259
+ try:
260
+ from reviewer.foreshadowing_tracker import track_foreshadowing_across_chapters
261
+
262
+ # Create temp directory with test chapters
263
+ temp_dir = tempfile.mkdtemp()
264
+ try:
265
+ for i in range(3):
266
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
267
+
268
+ result = track_foreshadowing_across_chapters(temp_dir)
269
+ assert 'total_chapters' in result
270
+ assert 'total_setups' in result
271
+ assert 'total_resolutions' in result
272
+ test_passed('track_foreshadowing_across_chapters')
273
+ finally:
274
+ shutil.rmtree(temp_dir)
275
+
276
+ except Exception as e:
277
+ test_failed('foreshadowing_tracker', e)
278
+
279
+
280
+ def test_worldbuilding_checker():
281
+ """Test worldbuilding_checker.py"""
282
+ print("\n=== 测试 worldbuilding_checker.py ===")
283
+ try:
284
+ from auditor.worldbuilding_checker import check_worldbuilding_consistency
285
+
286
+ # Create temp directory with test chapters
287
+ temp_dir = tempfile.mkdtemp()
288
+ truth_dir = tempfile.mkdtemp()
289
+ try:
290
+ for i in range(3):
291
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
292
+
293
+ # Create truth file
294
+ create_test_truth_file(
295
+ os.path.join(truth_dir, 'power_system.md'),
296
+ '# 力量体系\n\n- 筑基期\n- 金丹期\n- 元婴期'
297
+ )
298
+
299
+ result = check_worldbuilding_consistency(temp_dir, truth_dir)
300
+ assert 'total_chapters' in result
301
+ assert 'dimensions_checked' in result
302
+ assert 'issues' in result
303
+ test_passed('check_worldbuilding_consistency')
304
+ finally:
305
+ shutil.rmtree(temp_dir)
306
+ shutil.rmtree(truth_dir)
307
+
308
+ except Exception as e:
309
+ test_failed('worldbuilding_checker', e)
310
+
311
+
312
+ def test_pacing_optimizer():
313
+ """Test pacing_optimizer.py"""
314
+ print("\n=== 测试 pacing_optimizer.py ===")
315
+ try:
316
+ from auditor.pacing_optimizer import analyze_pacing_with_suggestions
317
+
318
+ # Create temp directory with test chapters
319
+ temp_dir = tempfile.mkdtemp()
320
+ try:
321
+ for i in range(5):
322
+ create_test_chapter(os.path.join(temp_dir, f'第{i+1:02d}章.md'), f'第{i+1}章')
323
+
324
+ result = analyze_pacing_with_suggestions(temp_dir)
325
+ assert 'total_chapters' in result
326
+ assert 'pacing_distribution' in result
327
+ assert 'suggestions' in result
328
+ test_passed('analyze_pacing_with_suggestions')
329
+ finally:
330
+ shutil.rmtree(temp_dir)
331
+
332
+ except Exception as e:
333
+ test_failed('pacing_optimizer', e)
334
+
335
+
336
+ def main():
337
+ print("=" * 60)
338
+ print("NovelMaker 辅助脚本单元测试")
339
+ print("=" * 60)
340
+
341
+ # Run all tests
342
+ test_scene_builder()
343
+ test_dialogue_checker()
344
+ test_character_arc_tracker()
345
+ test_subplot_tracker()
346
+ test_chapter_transition()
347
+ test_emotion_curve()
348
+ test_foreshadowing_tracker()
349
+ test_worldbuilding_checker()
350
+ test_pacing_optimizer()
351
+
352
+ # Print summary
353
+ print("\n" + "=" * 60)
354
+ print(f"测试完成: {test_results['passed']}通过, {test_results['failed']}失败")
355
+ print("=" * 60)
356
+
357
+ if test_results['errors']:
358
+ print("\n失败详情:")
359
+ for error in test_results['errors']:
360
+ print(f" - {error['test']}: {error['error']}")
361
+
362
+ return 0 if test_results['failed'] == 0 else 1
363
+
364
+
365
+ if __name__ == '__main__':
366
+ sys.exit(main())
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 写手上下文构建器 — 一键生成写作所需的精简上下文
5
+
6
+ 将写手需要读取的 15+ 个文件压缩为一个结构化 JSON(~3000 token),
7
+ 每章节省约 40,000-60,000 token 的文件读取消耗。
8
+
9
+ 用法:
10
+ python scripts/writer/build_write_context.py novels/volume-01/chapters/ch15.md
11
+ python scripts/writer/build_write_context.py --chapter 15 --volume 01
12
+ python scripts/writer/build_write_context.py --chapter 15 --json
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
21
+ from nm_utils import (
22
+ read_truth_section, list_chapters, read_chapter,
23
+ extract_characters, generate_summary, detect_hook,
24
+ parse_outline_headings
25
+ )
26
+
27
+ NOVEL_MAKER_DIR = '.novel-maker'
28
+ TRUTH_DIR = os.path.join(NOVEL_MAKER_DIR, 'truth-files')
29
+
30
+
31
+ def _find_project_root(start_path):
32
+ """向上查找包含 .novel-maker 的项目根目录"""
33
+ path = os.path.abspath(start_path)
34
+ for _ in range(10):
35
+ if os.path.isdir(os.path.join(path, NOVEL_MAKER_DIR)):
36
+ return path
37
+ parent = os.path.dirname(path)
38
+ if parent == path:
39
+ break
40
+ path = parent
41
+ return None
42
+
43
+
44
+ def _read_truth_compact(filepath, max_chars=800):
45
+ """读取真相文件并压缩为精简摘要"""
46
+ sections = read_truth_section(filepath)
47
+ if not sections:
48
+ return None
49
+ result = {}
50
+ for title, content in sections.items():
51
+ if title == 'header':
52
+ continue
53
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
54
+ compact = '\n'.join(lines[:15]) # 每个section最多15行
55
+ if len(compact) > max_chars:
56
+ compact = compact[:max_chars] + '...'
57
+ if compact:
58
+ result[title] = compact
59
+ return result
60
+
61
+
62
+ def _get_prev_chapters_info(chapters_dir, current_chapter_num, count=2):
63
+ """获取前N章的结构化摘要"""
64
+ all_chapters = list_chapters(chapters_dir)
65
+ prev = []
66
+ for ch_path in all_chapters:
67
+ raw, title, clean, wc = read_chapter(ch_path)
68
+ # 判断是否在当前章之前
69
+ from nm_utils import chapter_sort_key
70
+ ch_num = chapter_sort_key(ch_path)
71
+ if ch_num >= current_chapter_num:
72
+ break
73
+ prev.append((ch_num, ch_path, raw, title, clean, wc))
74
+
75
+ # 只取最近N章
76
+ prev = prev[-count:]
77
+ results = []
78
+ for ch_num, ch_path, raw, title, clean, wc in prev:
79
+ chars = extract_characters(clean)[:8]
80
+ summary = generate_summary(raw, first_n=100, last_n=100)
81
+ hook = detect_hook(raw)
82
+ results.append({
83
+ 'chapter': os.path.basename(ch_path),
84
+ 'number': ch_num,
85
+ 'title': title,
86
+ 'word_count': wc,
87
+ 'characters': chars,
88
+ 'hook': hook['type'],
89
+ 'summary': summary
90
+ })
91
+ return results
92
+
93
+
94
+ def _get_outline_context(outline_path, chapter_num):
95
+ """从大纲中提取当前章节的目标"""
96
+ if not os.path.exists(outline_path):
97
+ return None
98
+ with open(outline_path, encoding='utf-8') as f:
99
+ text = f.read()
100
+ headings = parse_outline_headings(text)
101
+ # 找到当前章节附近的大纲节点
102
+ result = []
103
+ found_current = False
104
+ for h in headings:
105
+ if f'第{chapter_num}章' in h['title'] or f'第{_to_cn(chapter_num)}章' in h['title']:
106
+ found_current = True
107
+ if found_current:
108
+ result.append(h)
109
+ if len(result) >= 5:
110
+ break
111
+ # 如果没找到精确匹配,返回最后几个节点
112
+ if not result:
113
+ result = headings[-5:] if len(headings) > 5 else headings
114
+ return [{'level': h['level'], 'title': h['title']} for h in result]
115
+
116
+
117
+ def _to_cn(n):
118
+ """数字转中文"""
119
+ cn = '零一二三四五六七八九十'
120
+ if n <= 10:
121
+ return cn[n]
122
+ if n < 20:
123
+ return '十' + (cn[n - 10] if n > 10 else '')
124
+ return str(n)
125
+
126
+
127
+ def _get_constitution_summary(constitution_path):
128
+ """提取创作宪法要点"""
129
+ if not os.path.exists(constitution_path):
130
+ return None
131
+ sections = read_truth_section(constitution_path)
132
+ if not sections:
133
+ return None
134
+ key_sections = ['字数要求', '文风配置', '核心创作原则', '禁忌清单']
135
+ result = {}
136
+ for title, content in sections.items():
137
+ if any(k in title for k in key_sections):
138
+ lines = [l.strip() for l in content.split('\n') if l.strip() and not l.startswith('#')]
139
+ result[title] = '\n'.join(lines[:8])
140
+ return result
141
+
142
+
143
+ def build_context(chapter_path=None, chapter_num=None, volume=None,
144
+ novel_root=None, truth_dir=None, recent_count=2):
145
+ """构建写手上下文"""
146
+
147
+ # 确定项目根目录
148
+ if novel_root:
149
+ root = novel_root
150
+ elif chapter_path:
151
+ root = _find_project_root(os.path.dirname(chapter_path))
152
+ else:
153
+ root = _find_project_root('.')
154
+
155
+ if not root:
156
+ return {'error': '未找到 .novel-maker 目录,请先运行 /novel-maker init'}
157
+
158
+ truth_path = truth_dir or os.path.join(root, TRUTH_DIR)
159
+ context = {'meta': {}, 'truth_files': {}, 'prev_chapters': [], 'outline': None, 'constitution': None}
160
+
161
+ # 确定章节号和卷号
162
+ if chapter_path and os.path.exists(chapter_path):
163
+ from nm_utils import chapter_sort_key
164
+ chapter_num = chapter_num or chapter_sort_key(chapter_path)
165
+ chapters_dir = os.path.dirname(chapter_path)
166
+ volume = volume or os.path.basename(os.path.dirname(chapters_dir))
167
+ elif chapter_num and volume:
168
+ chapters_dir = os.path.join(root, 'novels', f'volume-{volume.zfill(2)}', 'chapters')
169
+ chapter_path = None
170
+ else:
171
+ return {'error': '请指定章节路径或章节号+卷号'}
172
+
173
+ context['meta'] = {
174
+ 'chapter_num': chapter_num,
175
+ 'volume': volume,
176
+ 'chapters_dir': chapters_dir
177
+ }
178
+
179
+ # 1. 读取精简版真相文件
180
+ truth_files = {
181
+ 'characters': 'characters.md',
182
+ 'current_state': 'current-state.md',
183
+ 'world_setting': 'world-setting.md',
184
+ 'power_system': 'power-system.md',
185
+ 'pending_hooks': 'pending-hooks.md',
186
+ 'emotional_arcs': 'emotional-arcs.md',
187
+ }
188
+ for key, filename in truth_files.items():
189
+ fpath = os.path.join(truth_path, filename)
190
+ compact = _read_truth_compact(fpath)
191
+ if compact:
192
+ context['truth_files'][key] = compact
193
+
194
+ # 2. 前N章摘要
195
+ if os.path.isdir(chapters_dir):
196
+ context['prev_chapters'] = _get_prev_chapters_info(chapters_dir, chapter_num, recent_count)
197
+
198
+ # 3. 大纲目标
199
+ outline_path = os.path.join(root, 'novels', f'volume-{volume.zfill(2) if volume else "01"}', 'outline.md')
200
+ if not os.path.exists(outline_path):
201
+ outline_path = os.path.join(root, 'novels', 'outline.md')
202
+ context['outline'] = _get_outline_context(outline_path, chapter_num)
203
+
204
+ # 4. 创作宪法要点
205
+ constitution_path = os.path.join(root, NOVEL_MAKER_DIR, 'constitution.md')
206
+ if not os.path.exists(constitution_path):
207
+ constitution_path = os.path.join(root, 'constitution.md')
208
+ context['constitution'] = _get_constitution_summary(constitution_path)
209
+
210
+ return context
211
+
212
+
213
+ def main():
214
+ parser = argparse.ArgumentParser(description='写手上下文构建器')
215
+ parser.add_argument('chapter', nargs='?', help='章节文件路径')
216
+ parser.add_argument('--chapter-num', '-n', type=int, help='章节号')
217
+ parser.add_argument('--volume', '-v', help='卷号')
218
+ parser.add_argument('--root', help='项目根目录')
219
+ parser.add_argument('--truth-dir', help='真相文件目录')
220
+ parser.add_argument('--recent', type=int, default=2, help='前N章摘要(默认2)')
221
+ parser.add_argument('--json', action='store_true', help='JSON输出')
222
+ args = parser.parse_args()
223
+
224
+ ctx = build_context(
225
+ chapter_path=args.chapter,
226
+ chapter_num=args.chapter_num,
227
+ volume=args.volume,
228
+ novel_root=args.root,
229
+ truth_dir=args.truth_dir,
230
+ recent_count=args.recent
231
+ )
232
+
233
+ if args.json:
234
+ print(json.dumps(ctx, ensure_ascii=False, indent=2))
235
+ else:
236
+ if 'error' in ctx:
237
+ print(f"错误: {ctx['error']}")
238
+ return
239
+ print(f"=== 写手上下文: 第{ctx['meta']['chapter_num']}章 ===\n")
240
+ if ctx['truth_files']:
241
+ print("【真相文件摘要】")
242
+ for name, data in ctx['truth_files'].items():
243
+ print(f" {name}: {len(data)}个section")
244
+ if ctx['prev_chapters']:
245
+ print(f"\n【前{len(ctx['prev_chapters'])}章摘要】")
246
+ for ch in ctx['prev_chapters']:
247
+ print(f" {ch['chapter']}: {ch['title']} ({ch['word_count']}字) 钩子:{ch['hook']}")
248
+ if ctx['outline']:
249
+ print(f"\n【大纲目标】")
250
+ for h in ctx['outline']:
251
+ print(f" {' ' * (h['level'] - 1)}{h['title']}")
252
+
253
+
254
+ if __name__ == '__main__':
255
+ main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 章节结构化信息提取
5
+ 将章节全文压缩为结构化数据,AI 可直接读取此输出代替全文,大幅减少 token 消耗。
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import os
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
13
+ from nm_utils import (
14
+ clean_markdown, count_chinese, extract_title, read_chapter,
15
+ extract_characters, extract_locations, detect_structure,
16
+ detect_hook, generate_summary
17
+ )
18
+
19
+
20
+ def analyze_chapter(filepath):
21
+ raw, title, clean, wc = read_chapter(filepath)
22
+ return {
23
+ 'file': os.path.basename(filepath),
24
+ 'title': title,
25
+ 'word_count': wc,
26
+ 'characters': extract_characters(clean),
27
+ 'locations': extract_locations(clean),
28
+ 'structure': detect_structure(raw),
29
+ 'hook': detect_hook(raw),
30
+ 'summary': generate_summary(raw)
31
+ }
32
+
33
+
34
+ def main():
35
+ if len(sys.argv) < 2:
36
+ print('用法: python chapter_info.py <章节文件路径> [--json]')
37
+ print(' python chapter_info.py <文件> --json # 纯JSON输出给AI用')
38
+ return
39
+ filepath = sys.argv[1]
40
+ json_only = '--json' in sys.argv
41
+
42
+ if not os.path.exists(filepath):
43
+ result = {'error': f'文件不存在: {filepath}'}
44
+ else:
45
+ result = analyze_chapter(filepath)
46
+
47
+ if json_only:
48
+ print(json.dumps(result, ensure_ascii=False, indent=2))
49
+ else:
50
+ info = result
51
+ if 'error' in info:
52
+ print(info['error'])
53
+ return
54
+ print(f"章节: {info['title'] or info['file']}")
55
+ print(f"字数: {info['word_count']}")
56
+ print(f"角色: {', '.join(info['characters'][:8]) if info['characters'] else '未检测到'}")
57
+ print(f"地点: {', '.join(info['locations'][:5]) if info['locations'] else '未检测到'}")
58
+ s = info['structure']
59
+ print(f"结构: 对话{s['dialogue_pct']}% / 动作{s['action_pct']}% / 描写{s['description_pct']}% ({s['total_paragraphs']}段)")
60
+ h = info['hook']
61
+ print(f"钩子: {h['type']} (结尾预览: {h['tail_preview'][:40]}...)")
62
+ print(f"摘要: 开头={info['summary']['开头'][:50]}...")
63
+ print(f" 结尾={info['summary']['结尾'][-50:]}...")
64
+
65
+
66
+ if __name__ == '__main__':
67
+ main()