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,217 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
章节衔接检查脚本
|
|
5
|
+
检测章节之间的衔接问题:时间跳跃、地点突变、角色消失、情绪断裂等。
|
|
6
|
+
供审计师在审查时使用,或写手在连续写作时自查。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python scripts/auditor/chapter_transition.py 章节目录 --json
|
|
10
|
+
python scripts/auditor/chapter_transition.py 章节目录 --recent 5
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
21
|
+
from nm_utils import (
|
|
22
|
+
list_chapters, read_chapter, extract_characters, extract_locations,
|
|
23
|
+
detect_hook, count_chinese, generate_summary
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ─── 时间表达关键词 ───────────────────────────────
|
|
27
|
+
TIME_EXPRESSIONS = {
|
|
28
|
+
'瞬间': ['突然', '忽然', '刹那间', '瞬间', '立刻', '马上'],
|
|
29
|
+
'短期': ['第二天', '次日', '当晚', '傍晚', '清晨', '午后', '片刻', '一会儿'],
|
|
30
|
+
'中期': ['几天后', '一周后', '半月后', '一个月后', '数月后'],
|
|
31
|
+
'长期': ['一年后', '数年后', '多年后', '几年后', '十年后']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ─── 地点转换关键词 ───────────────────────────────
|
|
35
|
+
LOCATION_TRANSITIONS = ['来到', '到达', '回到', '前往', '进入', '离开', '返回', '抵达']
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def analyze_chapter_ending(filepath, last_n=500):
|
|
39
|
+
"""Analyze the ending of a chapter."""
|
|
40
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
41
|
+
tail = clean[-last_n:] if len(clean) > last_n else clean
|
|
42
|
+
|
|
43
|
+
# Extract ending characters
|
|
44
|
+
ending_chars = extract_characters(tail)
|
|
45
|
+
|
|
46
|
+
# Extract ending locations
|
|
47
|
+
ending_locs = extract_locations(tail)
|
|
48
|
+
|
|
49
|
+
# Detect hook
|
|
50
|
+
hook = detect_hook(raw)
|
|
51
|
+
|
|
52
|
+
# Detect time expressions
|
|
53
|
+
time_expressions = []
|
|
54
|
+
for category, keywords in TIME_EXPRESSIONS.items():
|
|
55
|
+
for kw in keywords:
|
|
56
|
+
if kw in tail:
|
|
57
|
+
time_expressions.append({'category': category, 'keyword': kw})
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
'title': title,
|
|
61
|
+
'word_count': wc,
|
|
62
|
+
'ending_chars': ending_chars[:5],
|
|
63
|
+
'ending_locations': ending_locs[:3],
|
|
64
|
+
'hook': hook['type'],
|
|
65
|
+
'time_expressions': time_expressions,
|
|
66
|
+
'tail_preview': tail[-100:]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def analyze_chapter_beginning(filepath, first_n=500):
|
|
71
|
+
"""Analyze the beginning of a chapter."""
|
|
72
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
73
|
+
head = clean[:first_n] if len(clean) > first_n else clean
|
|
74
|
+
|
|
75
|
+
# Extract beginning characters
|
|
76
|
+
beginning_chars = extract_characters(head)
|
|
77
|
+
|
|
78
|
+
# Extract beginning locations
|
|
79
|
+
beginning_locs = extract_locations(head)
|
|
80
|
+
|
|
81
|
+
# Detect time expressions
|
|
82
|
+
time_expressions = []
|
|
83
|
+
for category, keywords in TIME_EXPRESSIONS.items():
|
|
84
|
+
for kw in keywords:
|
|
85
|
+
if kw in head:
|
|
86
|
+
time_expressions.append({'category': category, 'keyword': kw})
|
|
87
|
+
|
|
88
|
+
# Detect location transitions
|
|
89
|
+
location_transitions = []
|
|
90
|
+
for transition in LOCATION_TRANSITIONS:
|
|
91
|
+
if transition in head:
|
|
92
|
+
location_transitions.append(transition)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
'title': title,
|
|
96
|
+
'word_count': wc,
|
|
97
|
+
'beginning_chars': beginning_chars[:5],
|
|
98
|
+
'beginning_locations': beginning_locs[:3],
|
|
99
|
+
'time_expressions': time_expressions,
|
|
100
|
+
'location_transitions': location_transitions,
|
|
101
|
+
'head_preview': head[:100]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_transition(chapter_files):
|
|
106
|
+
"""Check transitions between consecutive chapters."""
|
|
107
|
+
transitions = []
|
|
108
|
+
|
|
109
|
+
for i in range(len(chapter_files) - 1):
|
|
110
|
+
curr_file = chapter_files[i]
|
|
111
|
+
next_file = chapter_files[i + 1]
|
|
112
|
+
|
|
113
|
+
curr_ending = analyze_chapter_ending(curr_file)
|
|
114
|
+
next_beginning = analyze_chapter_beginning(next_file)
|
|
115
|
+
|
|
116
|
+
issues = []
|
|
117
|
+
|
|
118
|
+
# 1. Character continuity check
|
|
119
|
+
curr_chars = set(curr_ending['ending_chars'])
|
|
120
|
+
next_chars = set(next_beginning['beginning_chars'])
|
|
121
|
+
if curr_chars and not (curr_chars & next_chars):
|
|
122
|
+
issues.append({
|
|
123
|
+
'type': '角色断裂',
|
|
124
|
+
'message': f"上章结尾角色({', '.join(curr_ending['ending_chars'][:3])})未在下章开头出现",
|
|
125
|
+
'severity': 'warning'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
# 2. Location continuity check
|
|
129
|
+
curr_locs = set(curr_ending['ending_locations'])
|
|
130
|
+
next_locs = set(next_beginning['beginning_locations'])
|
|
131
|
+
if curr_locs and not (curr_locs & next_locs) and not next_beginning['location_transitions']:
|
|
132
|
+
issues.append({
|
|
133
|
+
'type': '地点突变',
|
|
134
|
+
'message': f"上章地点({', '.join(curr_ending['ending_locations'][:2])})到下章地点({', '.join(next_beginning['beginning_locations'][:2])})无过渡",
|
|
135
|
+
'severity': 'info'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
# 3. Time expression check
|
|
139
|
+
if next_beginning['time_expressions']:
|
|
140
|
+
time_cats = [t['category'] for t in next_beginning['time_expressions']]
|
|
141
|
+
if '长期' in time_cats:
|
|
142
|
+
issues.append({
|
|
143
|
+
'type': '时间跳跃',
|
|
144
|
+
'message': f"下章开头有长期时间跳跃({', '.join(t['keyword'] for t in next_beginning['time_expressions'] if t['category']=='长期')})",
|
|
145
|
+
'severity': 'info'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
# 4. Hook resolution check
|
|
149
|
+
if curr_ending['hook'] != '未检测':
|
|
150
|
+
issues.append({
|
|
151
|
+
'type': '钩子待回收',
|
|
152
|
+
'message': f"上章钩子类型: {curr_ending['hook']}",
|
|
153
|
+
'severity': 'info'
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
transitions.append({
|
|
157
|
+
'from_chapter': curr_ending['title'],
|
|
158
|
+
'to_chapter': next_beginning['title'],
|
|
159
|
+
'issues': issues,
|
|
160
|
+
'issue_count': len(issues)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return transitions
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main():
|
|
167
|
+
parser = argparse.ArgumentParser(description='章节衔接检查脚本')
|
|
168
|
+
parser.add_argument('chapters_dir', help='章节目录路径')
|
|
169
|
+
parser.add_argument('--recent', type=int, help='仅检查最近N章的衔接')
|
|
170
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
|
|
173
|
+
if not os.path.isdir(args.chapters_dir):
|
|
174
|
+
print(f"错误: 目录不存在: {args.chapters_dir}", file=sys.stderr)
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
chapter_files = list_chapters(args.chapters_dir, args.recent)
|
|
178
|
+
if len(chapter_files) < 2:
|
|
179
|
+
print("错误: 至少需要2个章节才能检查衔接", file=sys.stderr)
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
transitions = check_transition(chapter_files)
|
|
183
|
+
|
|
184
|
+
# Calculate summary
|
|
185
|
+
total_issues = sum(t['issue_count'] for t in transitions)
|
|
186
|
+
warning_count = sum(1 for t in transitions for i in t['issues'] if i['severity'] == 'warning')
|
|
187
|
+
|
|
188
|
+
result = {
|
|
189
|
+
'total_chapters': len(chapter_files),
|
|
190
|
+
'transitions_checked': len(transitions),
|
|
191
|
+
'total_issues': total_issues,
|
|
192
|
+
'warning_count': warning_count,
|
|
193
|
+
'transitions': transitions
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if args.json:
|
|
197
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
198
|
+
else:
|
|
199
|
+
print(f"\n=== 章节衔接检查 ===\n")
|
|
200
|
+
print(f"章节数: {result['total_chapters']}")
|
|
201
|
+
print(f"检查衔接数: {result['transitions_checked']}")
|
|
202
|
+
print(f"问题总数: {result['total_issues']}")
|
|
203
|
+
print(f"警告数: {result['warning_count']}")
|
|
204
|
+
|
|
205
|
+
print(f"\n衔接详情:")
|
|
206
|
+
for trans in transitions:
|
|
207
|
+
if trans['issues']:
|
|
208
|
+
print(f"\n {trans['from_chapter']} → {trans['to_chapter']}:")
|
|
209
|
+
for issue in trans['issues']:
|
|
210
|
+
icon = '⚠' if issue['severity'] == 'warning' else 'ℹ'
|
|
211
|
+
print(f" {icon} {issue['type']}: {issue['message']}")
|
|
212
|
+
else:
|
|
213
|
+
print(f"\n ✓ {trans['from_chapter']} → {trans['to_chapter']}: 无问题")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == '__main__':
|
|
217
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
一致性扫描
|
|
5
|
+
提取章节中的角色、等级、地点,与真相文件比对,输出冲突报告。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from collections import Counter
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
15
|
+
from nm_utils import (
|
|
16
|
+
list_chapters, read_chapter, extract_characters, extract_locations
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_truth_file(filepath):
|
|
21
|
+
"""Read a truth file and return its content."""
|
|
22
|
+
if not os.path.exists(filepath):
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
26
|
+
return f.read()
|
|
27
|
+
except Exception:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_characters_from_truth(text):
|
|
32
|
+
"""Extract character names from truth files."""
|
|
33
|
+
if not text:
|
|
34
|
+
return set()
|
|
35
|
+
names = set()
|
|
36
|
+
for m in re.finditer(r'[#|*\-]*\s*([\u4e00-\u9fff]{2,4})(?:[::]\s*[^\n]*|的角色)', text):
|
|
37
|
+
name = m.group(1)
|
|
38
|
+
if not name.startswith(('#', '!', ' ', '-', '|', '*')):
|
|
39
|
+
names.add(name)
|
|
40
|
+
return names
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_power_levels(text):
|
|
44
|
+
"""Extract power level mentions from text."""
|
|
45
|
+
levels = {}
|
|
46
|
+
for m in re.finditer(r'([\u4e00-\u9fff]{2,4})[::]?\s*([\u4e00-\u9fff]+(?:[初期|中期|后期|巅峰|大圆满]*))', text):
|
|
47
|
+
name, level = m.group(1), m.group(2)
|
|
48
|
+
if len(name) >= 2 and len(name) <= 4:
|
|
49
|
+
levels[name] = level
|
|
50
|
+
return levels
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_power_mentions(text):
|
|
54
|
+
"""Extract power level mentions from chapter."""
|
|
55
|
+
mentions = {}
|
|
56
|
+
for m in re.finditer(r'([\u4e00-\u9fff]{2,4})(?:已是|达到|突破到|升至|晋升为|修为|境界)\s*([\u4e00-\u9fff]{2,10})', text):
|
|
57
|
+
mentions[m.group(1)] = m.group(2)
|
|
58
|
+
return mentions
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def scan_volume(chapters_dir, truth_dir, recent_n=None):
|
|
62
|
+
"""Scan all chapters and compare with truth files."""
|
|
63
|
+
chapter_files = list_chapters(chapters_dir, recent_n)
|
|
64
|
+
if not chapter_files:
|
|
65
|
+
return {'error': f'No chapters found in {chapters_dir}'}
|
|
66
|
+
|
|
67
|
+
characters_md = read_truth_file(os.path.join(truth_dir, 'characters.md'))
|
|
68
|
+
current_state = read_truth_file(os.path.join(truth_dir, 'current-state.md'))
|
|
69
|
+
|
|
70
|
+
truth_text = ""
|
|
71
|
+
if characters_md:
|
|
72
|
+
truth_text += characters_md
|
|
73
|
+
if current_state:
|
|
74
|
+
truth_text += "\n" + current_state
|
|
75
|
+
|
|
76
|
+
truth_chars = extract_characters_from_truth(truth_text)
|
|
77
|
+
truth_levels = extract_power_levels(truth_text)
|
|
78
|
+
|
|
79
|
+
results = []
|
|
80
|
+
total_issues = []
|
|
81
|
+
new_chars_all = set()
|
|
82
|
+
level_conflicts_all = []
|
|
83
|
+
|
|
84
|
+
for idx, filepath in enumerate(chapter_files):
|
|
85
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
86
|
+
chapter_chars = extract_characters(clean)
|
|
87
|
+
power_mentions = extract_power_mentions(raw)
|
|
88
|
+
|
|
89
|
+
issues = []
|
|
90
|
+
new_chars = [c for c in chapter_chars if c not in truth_chars]
|
|
91
|
+
level_conflicts = []
|
|
92
|
+
for char, level in power_mentions.items():
|
|
93
|
+
if char in truth_levels and truth_levels[char] != level:
|
|
94
|
+
level_conflicts.append({
|
|
95
|
+
"character": char,
|
|
96
|
+
"truth_level": truth_levels[char],
|
|
97
|
+
"mentioned_level": level
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if new_chars:
|
|
101
|
+
issues.append({
|
|
102
|
+
"type": "new_character",
|
|
103
|
+
"severity": "info",
|
|
104
|
+
"message": f"新角色出场: {', '.join(new_chars[:5])}"
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
for lc in level_conflicts:
|
|
108
|
+
issues.append({
|
|
109
|
+
"type": "level_conflict",
|
|
110
|
+
"severity": "warning",
|
|
111
|
+
"message": f"{lc['character']} 等级冲突: 真相文件={lc['truth_level']}, 正文={lc['mentioned_level']}"
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
results.append({
|
|
115
|
+
"chapter": idx + 1,
|
|
116
|
+
"title": title,
|
|
117
|
+
"word_count": wc,
|
|
118
|
+
"characters": chapter_chars[:10],
|
|
119
|
+
"issues": issues,
|
|
120
|
+
"new_characters": new_chars,
|
|
121
|
+
"level_conflicts": level_conflicts,
|
|
122
|
+
})
|
|
123
|
+
total_issues.extend(issues)
|
|
124
|
+
new_chars_all.update(new_chars)
|
|
125
|
+
level_conflicts_all.extend(level_conflicts)
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"total_chapters": len(results),
|
|
129
|
+
"truth_files_available": {
|
|
130
|
+
"characters.md": characters_md is not None,
|
|
131
|
+
"current-state.md": current_state is not None,
|
|
132
|
+
},
|
|
133
|
+
"truth_characters_count": len(truth_chars),
|
|
134
|
+
"truth_levels_count": len(truth_levels),
|
|
135
|
+
"chapters": [{"chapter": r["chapter"], "title": r["title"], "word_count": r["word_count"],
|
|
136
|
+
"characters": r["characters"], "issues": r["issues"]} for r in results],
|
|
137
|
+
"summary": {
|
|
138
|
+
"total_issues": len(total_issues),
|
|
139
|
+
"new_characters": sorted(new_chars_all),
|
|
140
|
+
"level_conflicts": level_conflicts_all,
|
|
141
|
+
"warnings": [i for i in total_issues if i["severity"] == "warning"],
|
|
142
|
+
"info": [i for i in total_issues if i["severity"] == "info"],
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main():
|
|
148
|
+
import argparse
|
|
149
|
+
parser = argparse.ArgumentParser(description="Consistency scan")
|
|
150
|
+
parser.add_argument("chapters_dir", help="Chapters directory")
|
|
151
|
+
parser.add_argument("truth_dir", nargs='?', default=None, help="Truth files directory")
|
|
152
|
+
parser.add_argument("--recent", type=int, help="Only scan last N chapters")
|
|
153
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
154
|
+
args = parser.parse_args()
|
|
155
|
+
|
|
156
|
+
if not os.path.isdir(args.chapters_dir):
|
|
157
|
+
print(f"Error: {args.chapters_dir} is not a directory", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
truth_dir = args.truth_dir
|
|
161
|
+
if truth_dir is None:
|
|
162
|
+
truth_dir = os.path.join(os.path.dirname(args.chapters_dir.rstrip('/')), 'truth-files')
|
|
163
|
+
|
|
164
|
+
result = scan_volume(args.chapters_dir, truth_dir, recent_n=args.recent)
|
|
165
|
+
|
|
166
|
+
if args.json:
|
|
167
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
168
|
+
else:
|
|
169
|
+
print(f"=== 一致性扫描报告 (最近{result.get('total_chapters', 0)}章) ===\n")
|
|
170
|
+
if 'error' in result:
|
|
171
|
+
print(result['error'])
|
|
172
|
+
return
|
|
173
|
+
chars_ok = '✅' if result['truth_files_available']['characters.md'] else '❌'
|
|
174
|
+
state_ok = '✅' if result['truth_files_available']['current-state.md'] else '❌'
|
|
175
|
+
print(f"真相文件: characters.md={chars_ok} current-state.md={state_ok}")
|
|
176
|
+
print(f"已知角色: {result['truth_characters_count']}个 | 已知等级: {result['truth_levels_count']}条\n")
|
|
177
|
+
if result["summary"]["warnings"]:
|
|
178
|
+
print("⚠ 等级冲突:")
|
|
179
|
+
for w in result["summary"]["warnings"]:
|
|
180
|
+
print(f" {w['message']}")
|
|
181
|
+
if result["summary"]["new_characters"]:
|
|
182
|
+
print(f"\n📝 新角色 ({len(result['summary']['new_characters'])}个):")
|
|
183
|
+
for c in result["summary"]["new_characters"][:10]:
|
|
184
|
+
print(f" {c}")
|
|
185
|
+
if not result["summary"]["warnings"] and not result["summary"]["new_characters"]:
|
|
186
|
+
print("✅ 无一致性问题")
|
|
187
|
+
print()
|
|
188
|
+
for ch in result["chapters"]:
|
|
189
|
+
issues_str = f" [{len(ch['issues'])} issue(s)]" if ch["issues"] else ""
|
|
190
|
+
print(f" 第{ch['chapter']}章: {ch['title']} ({ch['word_count']}字, 角色: {', '.join(ch['characters'][:3])}){issues_str}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
对话质量检查脚本
|
|
5
|
+
检测对话中的常见问题:重复用词、AI味表达、角色声音不一致等。
|
|
6
|
+
供审计师在审查时使用,或写手自查。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python scripts/auditor/dialogue_checker.py 章节文件 --json
|
|
10
|
+
python scripts/auditor/dialogue_checker.py 章节文件 --verbose
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from collections import Counter
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
21
|
+
from nm_utils import read_chapter, extract_characters
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ─── 对话提取 ───────────────────────────────
|
|
25
|
+
def extract_dialogues(text):
|
|
26
|
+
"""Extract all dialogues from text."""
|
|
27
|
+
dialogues = []
|
|
28
|
+
# 匹配 "xxx" 或 "xxx" 格式
|
|
29
|
+
for m in re.finditer(r'["\u201c]([^"\u201d]+)["\u201d]', text):
|
|
30
|
+
content = m.group(1).strip()
|
|
31
|
+
if content:
|
|
32
|
+
dialogues.append(content)
|
|
33
|
+
return dialogues
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ─── 重复用词检测 ──────────────────────────────
|
|
37
|
+
def check_repetitive_words(dialogues, threshold=3):
|
|
38
|
+
"""Check for repetitive words in dialogues."""
|
|
39
|
+
word_counter = Counter()
|
|
40
|
+
for dialogue in dialogues:
|
|
41
|
+
# 分词(简单按2-3字切分)
|
|
42
|
+
words = re.findall(r'[\u4e00-\u9fff]{2,3}', dialogue)
|
|
43
|
+
word_counter.update(words)
|
|
44
|
+
|
|
45
|
+
repetitive = []
|
|
46
|
+
for word, count in word_counter.most_common():
|
|
47
|
+
if count >= threshold and word not in ['说道', '问道', '笑道']:
|
|
48
|
+
repetitive.append({'word': word, 'count': count})
|
|
49
|
+
|
|
50
|
+
return repetitive[:10] # 返回前10个
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ─── AI味表达检测 ───────────────────────────────
|
|
54
|
+
_AI_PATTERNS = [
|
|
55
|
+
(r'(?:心中|暗[自]?|心[中里])\s*(?:想|道|思忖|思虑)', '内心独白过多'),
|
|
56
|
+
(r'(?:嘴角|唇角)\s*(?:勾起|上扬|微[微扬])', '嘴角勾起套路'),
|
|
57
|
+
(r'(?:眼眸|眼睛)\s*(?:闪过|掠过)\s*(?:一丝|一抹)', '眼眸闪过套路'),
|
|
58
|
+
(r'(?:淡淡|冷冷|微微)\s*(?:一笑|说道|道)', '副词+动词套路'),
|
|
59
|
+
(r'(?:不禁|不由得)\s*(?:心中|感到)', '不禁/不由得套路'),
|
|
60
|
+
(r'(?:只见|但见)', '只见/但见套路'),
|
|
61
|
+
(r'(?:原来|竟然|居然)\s*(?:是|如此)', '原来/竟然套路'),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check_ai_patterns(dialogues):
|
|
66
|
+
"""Check for AI-style patterns in dialogues."""
|
|
67
|
+
issues = []
|
|
68
|
+
for pattern, description in _AI_PATTERNS:
|
|
69
|
+
for i, dialogue in enumerate(dialogues):
|
|
70
|
+
if re.search(pattern, dialogue):
|
|
71
|
+
issues.append({
|
|
72
|
+
'pattern': description,
|
|
73
|
+
'example': dialogue[:50],
|
|
74
|
+
'index': i
|
|
75
|
+
})
|
|
76
|
+
return issues[:10] # 返回前10个
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ─── 对话长度分析 ───────────────────────────────
|
|
80
|
+
def analyze_dialogue_length(dialogues):
|
|
81
|
+
"""Analyze dialogue length distribution."""
|
|
82
|
+
if not dialogues:
|
|
83
|
+
return {'error': '未检测到对话'}
|
|
84
|
+
|
|
85
|
+
lengths = [len(d) for d in dialogues]
|
|
86
|
+
avg_length = sum(lengths) / len(lengths)
|
|
87
|
+
|
|
88
|
+
# 统计分布
|
|
89
|
+
short = sum(1 for l in lengths if l < 10)
|
|
90
|
+
medium = sum(1 for l in lengths if 10 <= l < 30)
|
|
91
|
+
long = sum(1 for l in lengths if 30 <= l < 60)
|
|
92
|
+
very_long = sum(1 for l in lengths if l >= 60)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
'total_dialogues': len(dialogues),
|
|
96
|
+
'avg_length': round(avg_length, 1),
|
|
97
|
+
'distribution': {
|
|
98
|
+
'short(<10字)': short,
|
|
99
|
+
'medium(10-30字)': medium,
|
|
100
|
+
'long(30-60字)': long,
|
|
101
|
+
'very_long(>60字)': very_long
|
|
102
|
+
},
|
|
103
|
+
'warnings': []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ─── 主检查函数 ───────────────────────────────
|
|
108
|
+
def check_dialogue_quality(filepath, verbose=False):
|
|
109
|
+
"""Check dialogue quality in a chapter file."""
|
|
110
|
+
if not os.path.exists(filepath):
|
|
111
|
+
return {'error': f'文件不存在: {filepath}'}
|
|
112
|
+
|
|
113
|
+
raw, title, clean, wc = read_chapter(filepath)
|
|
114
|
+
dialogues = extract_dialogues(clean)
|
|
115
|
+
|
|
116
|
+
result = {
|
|
117
|
+
'file': os.path.basename(filepath),
|
|
118
|
+
'title': title,
|
|
119
|
+
'word_count': wc,
|
|
120
|
+
'dialogue_count': len(dialogues),
|
|
121
|
+
'dialogue_ratio': round(len(dialogues) / max(wc, 1) * 100, 1),
|
|
122
|
+
'checks': {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# 1. 重复用词检测
|
|
126
|
+
repetitive = check_repetitive_words(dialogues)
|
|
127
|
+
result['checks']['repetitive_words'] = {
|
|
128
|
+
'count': len(repetitive),
|
|
129
|
+
'items': repetitive,
|
|
130
|
+
'severity': 'warning' if len(repetitive) > 3 else 'info'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# 2. AI味表达检测
|
|
134
|
+
ai_patterns = check_ai_patterns(dialogues)
|
|
135
|
+
result['checks']['ai_patterns'] = {
|
|
136
|
+
'count': len(ai_patterns),
|
|
137
|
+
'items': ai_patterns,
|
|
138
|
+
'severity': 'warning' if len(ai_patterns) > 5 else 'info'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# 3. 对话长度分析
|
|
142
|
+
length_analysis = analyze_dialogue_length(dialogues)
|
|
143
|
+
result['checks']['length_analysis'] = length_analysis
|
|
144
|
+
|
|
145
|
+
# 4. 总体评估
|
|
146
|
+
total_issues = len(repetitive) + len(ai_patterns)
|
|
147
|
+
result['overall'] = {
|
|
148
|
+
'score': max(0, 100 - total_issues * 5),
|
|
149
|
+
'grade': '优秀' if total_issues < 5 else '良好' if total_issues < 10 else '需改进',
|
|
150
|
+
'issue_count': total_issues
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if verbose:
|
|
154
|
+
result['sample_dialogues'] = dialogues[:5]
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
parser = argparse.ArgumentParser(description='对话质量检查脚本')
|
|
161
|
+
parser.add_argument('filepath', help='章节文件路径')
|
|
162
|
+
parser.add_argument('--json', action='store_true', help='JSON输出')
|
|
163
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='显示详细信息')
|
|
164
|
+
args = parser.parse_args()
|
|
165
|
+
|
|
166
|
+
result = check_dialogue_quality(args.filepath, args.verbose)
|
|
167
|
+
|
|
168
|
+
if args.json:
|
|
169
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
170
|
+
else:
|
|
171
|
+
if 'error' in result:
|
|
172
|
+
print(result['error'])
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
print(f"\n=== 对话质量检查: {result['title']} ===\n")
|
|
176
|
+
print(f"字数: {result['word_count']}")
|
|
177
|
+
print(f"对话数: {result['dialogue_count']}")
|
|
178
|
+
print(f"对话占比: {result['dialogue_ratio']}%")
|
|
179
|
+
|
|
180
|
+
print(f"\n检查结果:")
|
|
181
|
+
for check_name, check_result in result['checks'].items():
|
|
182
|
+
count = check_result.get('count', 0)
|
|
183
|
+
severity = check_result.get('severity', 'info')
|
|
184
|
+
icon = '⚠' if severity == 'warning' else '✓'
|
|
185
|
+
print(f" {icon} {check_name}: {count}个问题")
|
|
186
|
+
|
|
187
|
+
print(f"\n总体评估:")
|
|
188
|
+
print(f" 得分: {result['overall']['score']}/100")
|
|
189
|
+
print(f" 等级: {result['overall']['grade']}")
|
|
190
|
+
print(f" 问题总数: {result['overall']['issue_count']}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == '__main__':
|
|
194
|
+
main()
|