#!/usr/bin/env python3 """ as.if.records — Sprach-Linter. Prüft Text gegen /voice.json. Kein Setup, keine Abhängigkeiten. python3 lint.py text.md echo "Wir freuen uns, euch unser innovatives Produkt zu präsentieren!" | python3 lint.py python3 lint.py --lang en text.md Regeln kommen live von brand.as-if-records.com/voice.json, mit lokaler Datei als Fallback. Exit-Code 1, wenn ein Fehler gefunden wurde (für CI). """ import sys, re, json, os, urllib.request LIVE = "https://brand.as-if-records.com/voice.json" LOCAL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "voice.json") def load_rules(): try: with urllib.request.urlopen(LIVE, timeout=8) as r: return json.load(r) except Exception: with open(LOCAL, encoding="utf-8") as f: return json.load(f) def dotted(obj, path): cur = obj for p in path.split("."): cur = cur[p] return cur def sentences(text): return [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()] def run(text, rules, lang="de"): findings = [] def add(sev, cid, msg, sample=""): findings.append((sev, cid, msg, sample)) for c in rules["checks"]: cid, sev = c["id"], c["severity"] if c.get("lang") and c["lang"] != lang: continue # a) direkter Regex if "pattern" in c: flags = 0 if "i" in c.get("flags", ""): flags |= re.I if "m" in c.get("flags", ""): flags |= re.M try: hits = list(re.finditer(c["pattern"], text, flags)) except re.error: continue if len(hits) > c.get("maxMatches", 0): s = hits[c.get("maxMatches", 0)] add(sev, cid, c["message"], text[max(0, s.start()-35):s.start()+35].replace("\n", " ")) # b) Wortlisten elif "wordlist" in c: words = [] for wl in c["wordlist"]: words += dotted(rules, wl) for w in words: pat = c["patternTemplate"].replace("{word}", re.escape(w)) flags = re.I if "i" in c.get("flags", "") else 0 if "m" in c.get("flags", ""): flags |= re.M m = re.search(pat, text, flags) if m: add(sev, cid, f"{c['message']} → „{w}\"", text[max(0, m.start()-35):m.start()+35].replace("\n", " ")) # c) Mapping (Copula) elif "mapping" in c: mp = dotted(rules, c["mapping"]) table = mp.get(lang, {}) for k, v in table.items(): pat = c["patternTemplate"].replace("{key}", re.escape(k)) m = re.search(pat, text, re.I) if m: add(sev, cid, f"„{k}\" → „{v}\"", text[max(0, m.start()-35):m.start()+35].replace("\n", " ")) # d) Rhythmus elif c.get("algorithm") == "sentenceLengths": run_len = 0 for s in sentences(text): n = len(s.split()) run_len = run_len + 1 if 15 <= n <= 25 else 0 if run_len >= 3: add(sev, cid, c["message"], s[:70]) break return findings def main(): args = [a for a in sys.argv[1:]] lang = "de" if "--lang" in args: i = args.index("--lang"); lang = args[i+1]; del args[i:i+2] text = open(args[0], encoding="utf-8").read() if args else sys.stdin.read() rules = load_rules() findings = run(text, rules, lang) errors = [f for f in findings if f[0] == "error"] warns = [f for f in findings if f[0] == "warn"] if not findings: print("✓ sauber — keine Verstöße gegen voice.json") return 0 for sev, cid, msg, sample in findings: mark = "✗" if sev == "error" else "!" print(f"{mark} [{cid}] {msg}") if sample: print(f" …{sample}…") print(f"\n{len(errors)} Fehler, {len(warns)} Hinweise") print(rules["readAloudTest"]) return 1 if errors else 0 if __name__ == "__main__": sys.exit(main())