153 lines
6.2 KiB
Python
153 lines
6.2 KiB
Python
"""Check public texts against the brand voice in branding/README.md: plain, exact, every claim next to its limitation.
|
|
|
|
Code checks the mechanical rules (em and en dashes in prose, banned marketing words). For every paragraph of at least
|
|
15 words, Jev answers three questions in one request: does the paragraph claim something about SimurgArray without
|
|
any limitation or condition (Noul), does it use promotional exaggeration (Noul), and how plain is the tone (Score,
|
|
four concrete levels). The tool marks paragraphs to look at; it never rewrites them.
|
|
|
|
python scripts/jev/voice_check.py README.md blog/*.md my_post.txt # writes voice_report.md
|
|
python scripts/jev/voice_check.py --rules-only blog/*.md # mechanical rules only, no API calls
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from client import ROOT, noul, score, system_one
|
|
|
|
BANNED = ["revolutionary", "cutting edge", "cutting-edge", "ai powered", "ai-powered", "next generation",
|
|
"next-generation", "seamless", "seamlessly", "unlock", "game changer", "game-changing", "best in class",
|
|
"world class", "blazing"] # fmt: skip
|
|
LIMIT_T, HYPE_T, TONE_T = 0.7, 0.6, 1.5
|
|
TONE_LEVELS = [
|
|
"Plain and exact: short sentences, concrete facts or numbers, no salesmanship",
|
|
"Mostly plain, with an occasional vague or enthusiastic phrase",
|
|
"Noticeably promotional or vague: several adjectives or benefits stated without specifics",
|
|
"Marketing copy: superlatives, buzzwords and promises dominate",
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class Para:
|
|
path: str
|
|
line: int
|
|
heading: str
|
|
text: str
|
|
issues: list[str] = field(default_factory=list)
|
|
|
|
|
|
def paragraphs(path: Path, text: str) -> list[Para]:
|
|
"""Prose paragraphs and bullets; code blocks, tables, headings and front matter lines are skipped."""
|
|
out: list[Para] = []
|
|
heading, buf, start, in_code = "", [], 0, False
|
|
|
|
def flush() -> None:
|
|
if buf:
|
|
out.append(Para(str(path), start, heading, " ".join(buf)))
|
|
buf.clear()
|
|
|
|
for i, raw in enumerate([*text.splitlines(), ""], 1):
|
|
ln = raw.rstrip()
|
|
if ln.lstrip().startswith("```"):
|
|
flush()
|
|
in_code = not in_code
|
|
continue
|
|
if in_code:
|
|
continue
|
|
if not ln.strip() or ln.lstrip().startswith("|") or ln.startswith("#"):
|
|
flush()
|
|
if ln.startswith("#"):
|
|
heading = ln.lstrip("#").strip()
|
|
continue
|
|
m = re.match(r"\s*([*>-]|\d+\.)\s+", ln)
|
|
if m:
|
|
flush()
|
|
ln = ln[m.end() :]
|
|
if ln.lstrip().startswith("<"): # raw HTML blocks (banners, badges) are layout, not prose
|
|
continue
|
|
if not buf:
|
|
start = i
|
|
buf.append(ln.strip())
|
|
return out
|
|
|
|
|
|
def mechanical(p: Para) -> list[str]:
|
|
prose = re.sub(r"`[^`]*`|https?://\S+|\([^)]*\.(md|png|jpg|svg)\)", "", p.text)
|
|
prose = re.sub(r"\"[^\"]*\"|\u201c[^\u201d]*\u201d", "", prose) # quoted words are examples, not usage
|
|
if re.match(r"\s*(Never|No|Do not)\b", prose):
|
|
prose = ""
|
|
issues = []
|
|
if re.search("[—–]", prose):
|
|
issues.append("em or en dash in prose")
|
|
low = prose.lower()
|
|
found = [w for w in BANNED if re.search(rf"\b{re.escape(w)}\b", low)]
|
|
if found:
|
|
issues.append("banned words: " + ", ".join(found))
|
|
return issues
|
|
|
|
|
|
QUESTIONS = {
|
|
"no_limit": noul(
|
|
"Does `paragraph` claim that SimurgArray achieves, guarantees or provides something (accuracy, speed, "
|
|
"compatibility, safety, a benefit) without stating any limitation, condition, number or caveat in the same "
|
|
"paragraph?",
|
|
true="A capability or benefit is asserted with nothing that bounds it",
|
|
false="There is no such claim, or the claim comes with a number, condition or limitation",
|
|
),
|
|
"hype": noul(
|
|
"Does `paragraph` use promotional exaggeration: superlatives, buzzwords or unqualified promises instead of "
|
|
"plain description?",
|
|
true="Contains marketing language or overstatement",
|
|
false="Plain description, even if enthusiastic in a modest way",
|
|
),
|
|
"tone": score("How plain and exact is the tone of `paragraph`?", TONE_LEVELS),
|
|
}
|
|
|
|
|
|
def judge(p: Para, doc: str) -> dict[str, float]:
|
|
ans = system_one({"document": doc, "section": p.heading, "paragraph": p.text}, QUESTIONS)
|
|
return {"no_limit": float(ans["no_limit"]["noul"]), "hype": float(ans["hype"]["noul"]),
|
|
"tone": float(ans["tone"]["score"])} # fmt: skip
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
ap.add_argument("files", nargs="+", type=Path)
|
|
ap.add_argument("--out", type=Path, default=ROOT / "voice_report.md")
|
|
ap.add_argument("--rules-only", action="store_true", help="mechanical rules only, no API calls")
|
|
a = ap.parse_args(argv)
|
|
flagged: list[tuple[Para, dict[str, float]]] = []
|
|
n = 0
|
|
for f in a.files:
|
|
doc = f.name
|
|
for p in paragraphs(f, f.read_text(encoding="utf-8")):
|
|
p.issues = mechanical(p)
|
|
vals: dict[str, float] = {}
|
|
if not a.rules_only and len(p.text.split()) >= 15:
|
|
n += 1
|
|
vals = judge(p, doc)
|
|
if vals["no_limit"] >= LIMIT_T:
|
|
p.issues.append(f"claim without a limitation (p {vals['no_limit']:.2f})")
|
|
if vals["hype"] >= HYPE_T:
|
|
p.issues.append(f"promotional language (p {vals['hype']:.2f})")
|
|
if vals["tone"] >= TONE_T:
|
|
p.issues.append(f"tone {vals['tone']:.1f} of 3")
|
|
if p.issues:
|
|
flagged.append((p, vals))
|
|
lines = ["# Voice check", "", f"{n} paragraphs judged, {len(flagged)} flagged.", ""]
|
|
for p, _ in flagged:
|
|
lines.append(f"* `{p.path}:{p.line}` ({p.heading}) **{'; '.join(p.issues)}**")
|
|
lines.append(f" > {p.text[:400]}")
|
|
a.out.write_text("\n".join(lines) + "\n")
|
|
print(f"{n} paragraphs judged, {len(flagged)} flagged → {a.out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|