跳到主要内容

宣传文案审校与润色

收藏0

逐轮检查宣传文案的清晰度、语气、利益点与证据,给出保留原意的修改稿和修改理由。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 2 次2 个版本

scripts/readability_scorer.py

10.6 KB
1#!/usr/bin/env python3
2"""
3readability_scorer.py — Readability metrics for marketing copy
4Usage:
5 python3 readability_scorer.py --file copy.txt
6 echo "Your text here" | python3 readability_scorer.py
7 python3 readability_scorer.py # demo mode
8 python3 readability_scorer.py --json
9"""
10
11import argparse
12import json
13import math
14import re
15import sys
16
17
18# ---------------------------------------------------------------------------
19# Word lists
20# ---------------------------------------------------------------------------
21
22FILLER_WORDS = [
23 "very", "really", "just", "actually", "basically", "literally",
24 "honestly", "totally", "absolutely", "definitely", "certainly",
25 "obviously", "clearly", "quite", "rather", "somewhat", "fairly",
26 "pretty", "simply", "truly", "genuinely", "essentially",
27]
28
29# Simple passive voice detection: "was/were/is/are/been/being + past participle"
30PASSIVE_PATTERN = re.compile(
31 r"\b(was|were|is|are|been|being|be|am)\s+(\w+ed|known|written|built|made|done|seen|given|taken|brought|thought|found|put|set|cut|read|let|hit|hurt|cost|led|felt|kept|left|meant|sent|spent|stood|told|wore|won|beat|lost|broke|chose|drove|flew|froze|grew|hid|rang|rode|rose|ran|sank|sang|spoke|swore|swam|threw|woke|wrote)\b",
32 re.IGNORECASE,
33)
34
35ADVERB_PATTERN = re.compile(r"\b\w+ly\b", re.IGNORECASE)
36
37# Syllable estimation: count vowel groups
38def count_syllables(word: str) -> int:
39 word = word.lower().strip(".,!?;:\"'")
40 if not word:
41 return 0
42 # Silent e
43 if word.endswith("e") and len(word) > 2:
44 word = word[:-1]
45 count = len(re.findall(r"[aeiou]+", word))
46 return max(1, count)
47
48
49def split_sentences(text: str) -> list:
50 # Split on sentence-ending punctuation
51 parts = re.split(r"(?<=[.!?])\s+", text.strip())
52 return [p.strip() for p in parts if p.strip()]
53
54
55def split_words(text: str) -> list:
56 return re.findall(r"\b[a-zA-Z]+\b", text)
57
58
59# ---------------------------------------------------------------------------
60# Metrics
61# ---------------------------------------------------------------------------
62
63def flesch_reading_ease(avg_sentence_len: float, avg_syllables: float) -> float:
64 """Flesch Reading Ease formula."""
65 score = 206.835 - (1.015 * avg_sentence_len) - (84.6 * avg_syllables)
66 return round(max(0.0, min(100.0, score)), 1)
67
68
69def flesch_kincaid_grade(avg_sentence_len: float, avg_syllables: float) -> float:
70 """Flesch-Kincaid Grade Level formula."""
71 grade = (0.39 * avg_sentence_len) + (11.8 * avg_syllables) - 15.59
72 return round(max(0.0, grade), 1)
73
74
75def ease_label(score: float) -> str:
76 if score >= 90: return "Very Easy (5th grade)"
77 if score >= 80: return "Easy (6th grade)"
78 if score >= 70: return "Fairly Easy (7th grade)"
79 if score >= 60: return "Standard (8-9th grade)"
80 if score >= 50: return "Fairly Difficult (10-12th grade)"
81 if score >= 30: return "Difficult (College)"
82 return "Very Confusing (Professional)"
83
84
85def analyze_text(text: str) -> dict:
86 sentences = split_sentences(text)
87 words = split_words(text)
88
89 if not words:
90 return {"error": "No readable text found."}
91
92 num_sentences = max(1, len(sentences))
93 num_words = len(words)
94
95 # Syllables
96 syllable_counts = [count_syllables(w) for w in words]
97 total_syllables = sum(syllable_counts)
98
99 avg_sentence_len = num_words / num_sentences
100 avg_word_len = sum(len(w) for w in words) / num_words
101 avg_syllables_per_word = total_syllables / num_words
102
103 fre = flesch_reading_ease(avg_sentence_len, avg_syllables_per_word)
104 fk_grade = flesch_kincaid_grade(avg_sentence_len, avg_syllables_per_word)
105
106 # Passive voice
107 passive_matches = PASSIVE_PATTERN.findall(text)
108 passive_count = len(passive_matches)
109 passive_pct = round(passive_count / num_sentences * 100, 1)
110
111 # Adverbs
112 adverb_matches = ADVERB_PATTERN.findall(text)
113 # Filter obvious non-adverbs
114 non_adverb = {"family", "early", "only", "likely", "nearly", "really",
115 "daily", "weekly", "monthly", "yearly", "friendly", "lovely",
116 "lonely", "lively", "elderly", "costly"}
117 adverbs = [a for a in adverb_matches if a.lower() not in non_adverb]
118 adverb_density = round(len(adverbs) / num_words * 100, 1)
119
120 # Filler words
121 text_lower = text.lower()
122 word_tokens_lower = [w.lower() for w in words]
123 filler_found = {fw: word_tokens_lower.count(fw) for fw in FILLER_WORDS if fw in word_tokens_lower}
124 filler_total = sum(filler_found.values())
125
126 # Scoring:
127 # FRE already 0-100 (higher = easier = better for marketing copy)
128 # Target for marketing: 60-80 range
129 fre_score = fre # use as-is
130
131 return {
132 "stats": {
133 "word_count": num_words,
134 "sentence_count": num_sentences,
135 "avg_sentence_length": round(avg_sentence_len, 1),
136 "avg_word_length": round(avg_word_len, 1),
137 "avg_syllables_per_word": round(avg_syllables_per_word, 2),
138 },
139 "flesch_reading_ease": {
140 "score": fre,
141 "label": ease_label(fre),
142 "target": "60-80 for most marketing copy",
143 },
144 "flesch_kincaid_grade": {
145 "grade_level": fk_grade,
146 "note": f"Equivalent to grade {fk_grade} reading level",
147 },
148 "passive_voice": {
149 "count": passive_count,
150 "percentage": passive_pct,
151 "target": "<10%",
152 "pass": passive_pct < 10,
153 },
154 "adverb_density": {
155 "count": len(adverbs),
156 "percentage": adverb_density,
157 "examples": list(set(adverbs))[:8],
158 "target": "<5%",
159 "pass": adverb_density < 5,
160 },
161 "filler_words": {
162 "total_count": filler_total,
163 "breakdown": filler_found,
164 "target": "0-3 per 100 words",
165 "per_100_words": round(filler_total / num_words * 100, 1),
166 },
167 "overall_score": round(fre),
168 }
169
170
171# ---------------------------------------------------------------------------
172# Demo text
173# ---------------------------------------------------------------------------
174
175DEMO_TEXT = """
176Marketing copy needs to be clear, direct, and persuasive. When you write for your audience,
177you should always think about what they actually want to hear. Really good copy is basically
178about solving problems. It is very important to avoid using overly complicated language that
179might confuse the reader.
180
181The best headlines are written by experts who truly understand their customers. A strong
182call-to-action is absolutely essential for any landing page. You need to make sure that
183every single word is earning its place on the page.
184
185Studies show that shorter sentences improve comprehension. The average reader processes
186information faster when sentences contain fewer than 20 words. This is genuinely proven
187by research. Passive voice constructions are often used by writers who want to sound
188authoritative, but they can actually make copy feel distant and unclear.
189
190Focus on benefits, not features. Tell the reader what they will gain. Use numbers when
191you can — "save 3 hours per week" beats "save time" every single time. Specificity
192builds trust. Vague promises are ignored.
193"""
194
195
196# ---------------------------------------------------------------------------
197# Main
198# ---------------------------------------------------------------------------
199
200def main():
201 parser = argparse.ArgumentParser(
202 description="Readability scorer for marketing copy — Flesch, passive voice, filler words."
203 )
204 parser.add_argument("--file", help="Path to text file")
205 parser.add_argument("--json", action="store_true", help="Output as JSON")
206 args = parser.parse_args()
207
208 if args.file:
209 with open(args.file, "r", encoding="utf-8", errors="replace") as f:
210 text = f.read()
211 elif not sys.stdin.isatty():
212 text = sys.stdin.read()
213 if not text.strip():
214 text = DEMO_TEXT
215 if not args.json:
216 print("No input provided — running in demo mode.\n")
217 else:
218 text = DEMO_TEXT
219 if not args.json:
220 print("No input provided — running in demo mode.\n")
221
222 result = analyze_text(text)
223
224 if "error" in result:
225 print(f"Error: {result['error']}", file=sys.stderr)
226 sys.exit(1)
227
228 if args.json:
229 print(json.dumps(result, indent=2))
230 return
231
232 fre = result["flesch_reading_ease"]
233 fk = result["flesch_kincaid_grade"]
234 stats = result["stats"]
235 passive = result["passive_voice"]
236 adverbs = result["adverb_density"]
237 fillers = result["filler_words"]
238 score = result["overall_score"]
239
240 PASS = "✅"
241 FAIL = "❌"
242
243 print("=" * 62)
244 print(f" READABILITY REPORT Flesch Score: {fre['score']}/100")
245 print("=" * 62)
246 print(f" {fre['label']}")
247 print(f" Target: {fre['target']}")
248 print()
249 print(f" 📊 Stats")
250 print(f" Words: {stats['word_count']}")
251 print(f" Sentences: {stats['sentence_count']}")
252 print(f" Avg sentence length:{stats['avg_sentence_length']} words")
253 print(f" Avg word length: {stats['avg_word_length']} chars")
254 print(f" Syllables/word: {stats['avg_syllables_per_word']}")
255 print()
256 print(f" 📐 Flesch-Kincaid Grade Level: {fk['grade_level']}")
257 print(f" {fk['note']}")
258 print()
259
260 pv_icon = PASS if passive["pass"] else FAIL
261 print(f" {pv_icon} Passive Voice: {passive['count']} instances ({passive['percentage']}%)")
262 print(f" Target: {passive['target']}")
263
264 av_icon = PASS if adverbs["pass"] else FAIL
265 print(f" {av_icon} Adverb Density: {adverbs['count']} adverbs ({adverbs['percentage']}%)")
266 if adverbs["examples"]:
267 print(f" Examples: {', '.join(adverbs['examples'][:5])}")
268
269 filler_ok = fillers["per_100_words"] <= 3
270 fw_icon = PASS if filler_ok else FAIL
271 print(f" {fw_icon} Filler Words: {fillers['total_count']} total ({fillers['per_100_words']} per 100 words)")
272 if fillers["breakdown"]:
273 top = sorted(fillers["breakdown"].items(), key=lambda x: -x[1])[:5]
274 print(f" Top: {', '.join(f'{w}({c})' for w,c in top)}")
275
276 print()
277 print("=" * 62)
278 score_bar_len = round(score / 10)
279 bar = "█" * score_bar_len + "░" * (10 - score_bar_len)
280 print(f" Readability Score: [{bar}] {score}/100")
281 print("=" * 62)
282
283
284if __name__ == "__main__":
285 main()

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。