novel-maker 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/novel-maker.js +229 -0
- package/package.json +33 -0
- package/skill/CHANGELOG.md +82 -0
- package/skill/QUICK-REF.md +168 -0
- package/skill/README.md +75 -0
- package/skill/SKILL.md +715 -0
- package/skill/agents/README.md +59 -0
- package/skill/agents/auditor.md +234 -0
- package/skill/agents/coordinator.md +150 -0
- package/skill/agents/planner.md +220 -0
- package/skill/agents/reviewer.md +249 -0
- package/skill/agents/reviser.md +144 -0
- package/skill/agents/writer.md +213 -0
- package/skill/arc-templates/README.md +43 -0
- package/skill/arc-templates/apocalypse/ability.md +81 -0
- package/skill/arc-templates/apocalypse/faction.md +81 -0
- package/skill/arc-templates/apocalypse/humanity.md +81 -0
- package/skill/arc-templates/apocalypse/survival.md +81 -0
- package/skill/arc-templates/game/competition.md +81 -0
- package/skill/arc-templates/game/dungeon.md +81 -0
- package/skill/arc-templates/game/guild-war.md +81 -0
- package/skill/arc-templates/game/leveling.md +80 -0
- package/skill/arc-templates/general/challenge.md +44 -0
- package/skill/arc-templates/general/conflict.md +71 -0
- package/skill/arc-templates/general/explore.md +71 -0
- package/skill/arc-templates/general/growth.md +71 -0
- package/skill/arc-templates/general/mystery.md +71 -0
- package/skill/arc-templates/general/relation.md +71 -0
- package/skill/arc-templates/history/battle.md +71 -0
- package/skill/arc-templates/history/politics.md +71 -0
- package/skill/arc-templates/history/reform.md +71 -0
- package/skill/arc-templates/infinite-flow/boss.md +71 -0
- package/skill/arc-templates/infinite-flow/dungeon.md +71 -0
- package/skill/arc-templates/infinite-flow/enhance.md +71 -0
- package/skill/arc-templates/infinite-flow/team.md +71 -0
- package/skill/arc-templates/mystery/case.md +71 -0
- package/skill/arc-templates/mystery/deduction.md +71 -0
- package/skill/arc-templates/mystery/twist.md +71 -0
- package/skill/arc-templates/romance/angst.md +80 -0
- package/skill/arc-templates/romance/chase.md +71 -0
- package/skill/arc-templates/romance/slow-burn.md +71 -0
- package/skill/arc-templates/romance/sweet.md +80 -0
- package/skill/arc-templates/sci-fi/awakening.md +81 -0
- package/skill/arc-templates/sci-fi/breakthrough.md +81 -0
- package/skill/arc-templates/sci-fi/contact.md +81 -0
- package/skill/arc-templates/sci-fi/exploration.md +81 -0
- package/skill/arc-templates/urban/business.md +71 -0
- package/skill/arc-templates/urban/revenge.md +71 -0
- package/skill/arc-templates/urban/rise.md +71 -0
- package/skill/arc-templates/urban/romance.md +71 -0
- package/skill/arc-templates/western-fantasy/adventure.md +80 -0
- package/skill/arc-templates/western-fantasy/kingdom.md +71 -0
- package/skill/arc-templates/western-fantasy/magic-awakening.md +71 -0
- package/skill/arc-templates/western-fantasy/racial-conflict.md +71 -0
- package/skill/arc-templates/wuxia/breakthrough.md +71 -0
- package/skill/arc-templates/wuxia/grudge.md +71 -0
- package/skill/arc-templates/wuxia/hero-path.md +71 -0
- package/skill/arc-templates/wuxia/sect-war.md +71 -0
- package/skill/arc-templates/xianxia/breakthrough.md +71 -0
- package/skill/arc-templates/xianxia/dungeon.md +71 -0
- package/skill/arc-templates/xianxia/tournament.md +71 -0
- package/skill/arc-templates/xianxia/tribulation.md +71 -0
- package/skill/docs/examples.md +61 -0
- package/skill/docs/faq.md +81 -0
- package/skill/docs/installation.md +87 -0
- package/skill/docs/quickstart.md +83 -0
- package/skill/genre-packs/README.md +47 -0
- package/skill/genre-packs/_default/arc-types.md +153 -0
- package/skill/genre-packs/_default/rules.md +56 -0
- package/skill/genre-packs/_default/templates.md +135 -0
- package/skill/genre-packs/apocalypse/arc-types.md +109 -0
- package/skill/genre-packs/apocalypse/rules.md +113 -0
- package/skill/genre-packs/apocalypse/settings.md +106 -0
- package/skill/genre-packs/apocalypse/templates.md +192 -0
- package/skill/genre-packs/game/arc-types.md +109 -0
- package/skill/genre-packs/game/rules.md +113 -0
- package/skill/genre-packs/game/settings.md +103 -0
- package/skill/genre-packs/game/templates.md +173 -0
- package/skill/genre-packs/history/arc-types.md +109 -0
- package/skill/genre-packs/history/rules.md +107 -0
- package/skill/genre-packs/history/settings.md +126 -0
- package/skill/genre-packs/history/templates.md +179 -0
- package/skill/genre-packs/infinite-flow/arc-types.md +101 -0
- package/skill/genre-packs/infinite-flow/rules.md +75 -0
- package/skill/genre-packs/infinite-flow/settings.md +102 -0
- package/skill/genre-packs/infinite-flow/templates.md +226 -0
- package/skill/genre-packs/mystery/arc-types.md +109 -0
- package/skill/genre-packs/mystery/rules.md +107 -0
- package/skill/genre-packs/mystery/settings.md +103 -0
- package/skill/genre-packs/mystery/templates.md +178 -0
- package/skill/genre-packs/romance/arc-types.md +130 -0
- package/skill/genre-packs/romance/rules.md +88 -0
- package/skill/genre-packs/romance/settings.md +146 -0
- package/skill/genre-packs/romance/templates.md +245 -0
- package/skill/genre-packs/sci-fi/arc-types.md +109 -0
- package/skill/genre-packs/sci-fi/rules.md +113 -0
- package/skill/genre-packs/sci-fi/settings.md +99 -0
- package/skill/genre-packs/sci-fi/templates.md +170 -0
- package/skill/genre-packs/urban/arc-types.md +101 -0
- package/skill/genre-packs/urban/rules.md +75 -0
- package/skill/genre-packs/urban/settings.md +82 -0
- package/skill/genre-packs/urban/templates.md +212 -0
- package/skill/genre-packs/western-fantasy/arc-types.md +128 -0
- package/skill/genre-packs/western-fantasy/rules.md +88 -0
- package/skill/genre-packs/western-fantasy/settings.md +160 -0
- package/skill/genre-packs/western-fantasy/templates.md +225 -0
- package/skill/genre-packs/wuxia/arc-types.md +126 -0
- package/skill/genre-packs/wuxia/rules.md +86 -0
- package/skill/genre-packs/wuxia/settings.md +150 -0
- package/skill/genre-packs/wuxia/templates.md +195 -0
- package/skill/genre-packs/xianxia/arc-types.md +101 -0
- package/skill/genre-packs/xianxia/rules.md +74 -0
- package/skill/genre-packs/xianxia/settings.md +107 -0
- package/skill/genre-packs/xianxia/templates.md +202 -0
- package/skill/hooks/README.md +102 -0
- package/skill/hooks/chapter-complete.md +176 -0
- package/skill/hooks/context-injection.md +152 -0
- package/skill/hooks/intent-detection.md +183 -0
- package/skill/hooks/review-trigger.md +219 -0
- package/skill/hooks/summary-trigger.md +185 -0
- package/skill/references/act-guidance.md +228 -0
- package/skill/references/audit-core.md +130 -0
- package/skill/references/audit-dimensions.md +202 -0
- package/skill/references/character-voice-card.md +196 -0
- package/skill/references/consistency-checker.md +209 -0
- package/skill/references/content-expansion.md +68 -0
- package/skill/references/creative-constraints.md +200 -0
- package/skill/references/data-agent.md +286 -0
- package/skill/references/dialogue-writing.md +104 -0
- package/skill/references/editorial-perspective.md +166 -0
- package/skill/references/emotion-curve.md +127 -0
- package/skill/references/genre-rules.md +389 -0
- package/skill/references/golden-opening.md +81 -0
- package/skill/references/memory-system.md +288 -0
- package/skill/references/pacing-analysis.md +201 -0
- package/skill/references/platform-rules.md +244 -0
- package/skill/references/plot-structures.md +108 -0
- package/skill/references/reader-feedback.md +119 -0
- package/skill/references/rhythm-system.md +204 -0
- package/skill/references/style-imitation.md +193 -0
- package/skill/references/sweet-spot-tracking.md +182 -0
- package/skill/references/usage-guide.md +174 -0
- package/skill/references/writing-methods.md +169 -0
- package/skill/rules/anti-ai-expressions.md +206 -0
- package/skill/rules/character-voice.md +184 -0
- package/skill/rules/consistency-check.md +232 -0
- package/skill/rules/smart-query.md +263 -0
- package/skill/scripts/README.md +380 -0
- package/skill/scripts/auditor/chapter_transition.py +217 -0
- package/skill/scripts/auditor/consistency_scan.py +194 -0
- package/skill/scripts/auditor/dialogue_checker.py +194 -0
- package/skill/scripts/auditor/hook_report.py +115 -0
- package/skill/scripts/auditor/pacing_optimizer.py +303 -0
- package/skill/scripts/auditor/pacing_report.py +275 -0
- package/skill/scripts/auditor/pre_audit.py +203 -0
- package/skill/scripts/auditor/style_check.py +158 -0
- package/skill/scripts/auditor/worldbuilding_checker.py +637 -0
- package/skill/scripts/common/analyze.py +129 -0
- package/skill/scripts/common/init_guide.py +796 -0
- package/skill/scripts/common/install.py +169 -0
- package/skill/scripts/common/nm_utils.py +296 -0
- package/skill/scripts/common/validate.py +215 -0
- package/skill/scripts/coordinator/stats_report.py +165 -0
- package/skill/scripts/coordinator/volume_batch.py +121 -0
- package/skill/scripts/planner/outline_extractor.py +89 -0
- package/skill/scripts/planner/planner_context.py +220 -0
- package/skill/scripts/planner/query_engine.py +289 -0
- package/skill/scripts/reviewer/chapter_diff.py +143 -0
- package/skill/scripts/reviewer/character_arc_tracker.py +191 -0
- package/skill/scripts/reviewer/emotion_curve.py +340 -0
- package/skill/scripts/reviewer/foreshadowing_tracker.py +286 -0
- package/skill/scripts/reviewer/subplot_tracker.py +207 -0
- package/skill/scripts/reviewer/summary_generator.py +130 -0
- package/skill/scripts/reviewer/truth_diff.py +227 -0
- package/skill/scripts/reviewer/truth_manager.py +120 -0
- package/skill/scripts/test_scripts.py +366 -0
- package/skill/scripts/writer/build_write_context.py +255 -0
- package/skill/scripts/writer/chapter_info.py +67 -0
- package/skill/scripts/writer/check_wordcount.py +115 -0
- package/skill/scripts/writer/scene_builder.py +227 -0
- package/skill/scripts/writer/style_anchor.py +135 -0
- package/skill/styles/author-styles.md +53 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//345/277/230/350/257/255.md +110 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//347/210/261/346/275/234/346/260/264/347/232/204/344/271/214/350/264/274.md +110 -0
- package/skill/styles/authors//344/270/245/350/260/250/350/256/276/345/256/232/346/265/201//350/250/200/345/275/222/346/255/243/344/274/240.md +110 -0
- package/skill/styles/authors//345/244/232/347/245/236/350/257/235/347/203/255/350/241/200/346/265/201//344/270/211/344/271/235/351/237/263/345/237/237.md +108 -0
- package/skill/styles/authors//346/202/254/347/226/221/346/216/250/347/220/206/346/265/201//346/235/200/350/231/253/351/230/237/351/230/237/345/221/230.md +108 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/270/211/345/244/251/344/270/244/350/247/211.md +110 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//344/274/232/350/257/264/350/257/235/347/232/204/350/202/230/345/255/220.md +110 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/215/226/346/212/245/345/260/217/351/203/216/345/220/233.md +108 -0
- package/skill/styles/authors//346/220/236/347/254/221/345/271/275/351/273/230/346/265/201//345/274/210/351/235/222/345/263/260.md +123 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/224/220/345/256/266/344/270/211/345/260/221.md +109 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//345/244/251/350/232/225/345/234/237/350/261/206.md +110 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/210/221/345/220/203/350/245/277/347/272/242/346/237/277.md +108 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//346/273/232/345/274/200.md +109 -0
- package/skill/styles/authors//347/203/255/350/241/200/345/215/207/347/272/247/346/265/201//350/276/260/344/270/234.md +109 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//345/244/251/344/270/213/351/234/270/345/224/261.md +110 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//346/234/210/345/205/263.md +108 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/220/247/351/274/216.md +109 -0
- package/skill/styles/authors//347/211/271/350/211/262/351/242/206/345/237/237/346/265/201//350/235/264/350/235/266/350/223/235.md +112 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//346/204/244/346/200/222/347/232/204/351/246/231/350/225/211.md +109 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/203/275/347/201/253/346/210/217/350/257/270/344/276/257.md +109 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//347/214/253/350/205/273.md +110 -0
- package/skill/styles/authors//347/273/206/350/205/273/346/226/207/351/235/222/346/265/201//350/200/263/346/240/271.md +109 -0
- package/skill/templates/INDEX.md +48 -0
- package/skill/templates/act-plan.md +72 -0
- package/skill/templates/chapter.md +53 -0
- package/skill/templates/character-profile.md +107 -0
- package/skill/templates/character-voice.md +106 -0
- package/skill/templates/constitution.md +90 -0
- package/skill/templates/emotional-arcs.md +37 -0
- package/skill/templates/hook-template.md +68 -0
- package/skill/templates/outline.md +155 -0
- package/skill/templates/plot-card.md +432 -0
- package/skill/templates/power-system.md +124 -0
- package/skill/templates/presets.json +69 -0
- package/skill/templates/review-report.md +135 -0
- package/skill/templates/scene-plan.md +221 -0
- package/skill/templates/scene-template.md +78 -0
- package/skill/templates/subplot-board.md +48 -0
- package/skill/templates/summary-10chapters.md +79 -0
- package/skill/templates/summary-50chapters.md +131 -0
- package/skill/templates/summary-volume.md +148 -0
- package/skill/templates/timeline.md +37 -0
- package/skill/templates/volume-plan.md +44 -0
- package/skill/templates/world-setting.md +151 -0
|
@@ -0,0 +1,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()
|