Files
SlipItIn/.github/skills/human-ai/scripts/measure.py
Tim Krampitz 01046b01e4 Neue Skills, Referenzen & OpenWiki-Doku integriert
Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
2026-07-26 14:00:58 +02:00

354 lines
12 KiB
Python

#!/usr/bin/env python3
"""
measure.py — AI vs Human text metrics analyzer.
Usage:
echo "some text" | python3 measure.py
python3 measure.py --file path/to/text.txt
Calculates linguistic metrics (TTR, burstiness, entropy, sentence/paragraph
variation, passive voice, contractions, etc.) and compares against empirical
baselines to produce a verdict: likely_ai, mixed, or likely_human.
Requires Python 3.10+. No external dependencies.
"""
import argparse
import json
import math
import re
import statistics
import sys
from collections import Counter
# --- Empirical baselines ---
BASELINES = {
"ttr": {"ai_typical": 0.455, "human_typical": 0.553, "source": "SSRN"},
"burstiness": {"ai_typical": 0.00, "human_typical": 0.70, "source": "GPTZero"},
"sentence_length_cov": {"ai_typical": 0.30, "human_typical": 0.50},
"paragraph_length_cov": {"ai_typical": 0.30, "human_typical": 0.60},
"contraction_rate": {"ai_typical_range": [0.30, 0.50], "human_typical_range": [0.80, 0.95]},
"passive_voice_pct": {"ai_typical": 0.30, "human_typical_range": [0.10, 0.20]},
}
# Common contractions and their expanded forms
CONTRACTION_PAIRS = {
"i'm": "i am", "i've": "i have", "i'll": "i will", "i'd": "i would",
"you're": "you are", "you've": "you have", "you'll": "you will", "you'd": "you would",
"he's": "he is", "he'll": "he will", "he'd": "he would",
"she's": "she is", "she'll": "she will", "she'd": "she would",
"it's": "it is", "it'll": "it will", "it'd": "it would",
"we're": "we are", "we've": "we have", "we'll": "we will", "we'd": "we would",
"they're": "they are", "they've": "they have", "they'll": "they will", "they'd": "they would",
"that's": "that is", "there's": "there is", "here's": "here is",
"what's": "what is", "who's": "who is", "where's": "where is",
"won't": "will not", "can't": "cannot", "couldn't": "could not",
"wouldn't": "would not", "shouldn't": "should not", "doesn't": "does not",
"don't": "do not", "didn't": "did not", "isn't": "is not",
"aren't": "are not", "wasn't": "was not", "weren't": "were not",
"hasn't": "has not", "haven't": "have not", "hadn't": "had not",
"let's": "let us", "that'll": "that will", "who'll": "who will",
}
# Expanded forms to detect (when NOT contracted)
EXPANDED_FORMS = set(CONTRACTION_PAIRS.values())
# Common concrete nouns (physical, tangible objects)
CONCRETE_PATTERNS = re.compile(
r"\b(table|chair|car|house|tree|dog|cat|book|phone|door|window|wall|"
r"road|water|stone|hand|face|eye|foot|head|body|room|floor|glass|"
r"box|bag|cup|plate|knife|pen|paper|shirt|shoe|hat|bed|desk|"
r"computer|screen|keyboard|mouse|bottle|lamp|clock|mirror|bridge|"
r"river|mountain|ocean|sun|moon|star|cloud|rain|snow|fire|smoke|"
r"bread|meat|fruit|flower|grass|sand|iron|gold|silver|wood|rock|"
r"truck|bus|train|plane|boat|ship|bicycle|wheel|engine|hammer|"
r"needle|rope|chain|brick|coin|ring|bell|drum|guitar|piano)\b",
re.IGNORECASE,
)
# Abstract nouns (concepts, ideas, qualities)
ABSTRACT_PATTERNS = re.compile(
r"\b(freedom|justice|love|beauty|truth|wisdom|knowledge|power|"
r"happiness|sadness|anger|fear|hope|faith|courage|patience|"
r"democracy|philosophy|theory|concept|idea|thought|belief|"
r"understanding|experience|opportunity|challenge|strategy|"
r"approach|methodology|framework|perspective|consideration|"
r"implementation|optimization|functionality|capability|"
r"efficiency|effectiveness|sustainability|innovation|"
r"transformation|development|improvement|enhancement|"
r"complexity|simplicity|diversity|integrity|creativity)\b",
re.IGNORECASE,
)
def tokenize(text: str) -> list[str]:
"""Split text into lowercase word tokens."""
return re.findall(r"[a-z']+", text.lower())
def split_sentences(text: str) -> list[str]:
"""Split text into sentences using punctuation boundaries."""
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
return [s for s in sentences if s.strip()]
def split_paragraphs(text: str) -> list[str]:
"""Split text into paragraphs by blank lines."""
paragraphs = re.split(r'\n\s*\n', text.strip())
return [p for p in paragraphs if p.strip()]
def calc_ttr(tokens: list[str]) -> float:
"""Type-Token Ratio: unique tokens / total tokens."""
if not tokens:
return 0.0
return len(set(tokens)) / len(tokens)
def calc_burstiness(sentences: list[str]) -> float:
"""Standard deviation of sentence lengths (word count per sentence)."""
lengths = [len(s.split()) for s in sentences]
if len(lengths) < 2:
return 0.0
return statistics.stdev(lengths)
def calc_shannon_entropy(tokens: list[str]) -> float:
"""Shannon entropy: -sum p(x)*log2(p(x)) over vocabulary."""
if not tokens:
return 0.0
total = len(tokens)
counts = Counter(tokens)
entropy = 0.0
for count in counts.values():
p = count / total
if p > 0:
entropy -= p * math.log2(p)
return entropy
def calc_sentence_length_cov(sentences: list[str]) -> float:
"""Coefficient of variation of sentence lengths: std/mean."""
lengths = [len(s.split()) for s in sentences]
if len(lengths) < 2:
return 0.0
mean = statistics.mean(lengths)
if mean == 0:
return 0.0
return statistics.stdev(lengths) / mean
def calc_mean_sentence_length(sentences: list[str]) -> float:
"""Mean sentence length in words."""
lengths = [len(s.split()) for s in sentences]
if not lengths:
return 0.0
return statistics.mean(lengths)
def calc_paragraph_length_cov(paragraphs: list[str]) -> float:
"""Coefficient of variation of paragraph lengths (in sentences)."""
if len(paragraphs) < 2:
return 0.0
lengths = [len(split_sentences(p)) for p in paragraphs]
mean = statistics.mean(lengths)
if mean == 0:
return 0.0
return statistics.stdev(lengths) / mean
def calc_ly_adverbs_per_100(tokens: list[str]) -> float:
"""Count adverbs ending in -ly per 100 words."""
if not tokens:
return 0.0
# Exclude common non-adverb -ly words
exceptions = {
"only", "early", "likely", "family", "really", "actually",
"finally", "fly", "supply", "apply", "reply", "holy",
"ugly", "belly", "jelly", "bully", "ally", "rally",
}
ly_count = sum(
1 for t in tokens
if t.endswith("ly") and len(t) > 3 and t not in exceptions
)
return (ly_count / len(tokens)) * 100
def calc_passive_voice_pct(text: str) -> float:
"""Approximate passive voice: was/were/been/being + past participle pattern."""
sentences = split_sentences(text)
if not sentences:
return 0.0
passive_pattern = re.compile(
r'\b(was|were|been|being|is|are|am)\s+(\w+ed|(\w+en))\b',
re.IGNORECASE,
)
passive_count = sum(1 for s in sentences if passive_pattern.search(s))
return passive_count / len(sentences)
def calc_contraction_rate(text: str) -> float:
"""Percentage of contractable phrases that ARE contracted."""
text_lower = text.lower()
tokens_raw = re.findall(r"[a-z']+", text_lower)
text_joined = " ".join(tokens_raw)
contracted_count = 0
expanded_count = 0
# Count contractions present
for contraction in CONTRACTION_PAIRS:
contracted_count += text_joined.count(contraction)
# Count expanded forms present (not contracted)
for expanded in EXPANDED_FORMS:
expanded_count += text_joined.count(expanded)
total = contracted_count + expanded_count
if total == 0:
return 0.0
return contracted_count / total
def calc_concrete_noun_density(tokens: list[str]) -> float:
"""Ratio of concrete nouns to (concrete + abstract) nouns found."""
text = " ".join(tokens)
concrete_matches = len(CONCRETE_PATTERNS.findall(text))
abstract_matches = len(ABSTRACT_PATTERNS.findall(text))
total = concrete_matches + abstract_matches
if total == 0:
return 0.5 # neutral if can't determine
return concrete_matches / total
def score_metric(name: str, value: float) -> str:
"""Score a metric as 'ai', 'human', or 'neutral'."""
match name:
case "ttr":
midpoint = (BASELINES["ttr"]["ai_typical"] + BASELINES["ttr"]["human_typical"]) / 2
return "human" if value > midpoint else "ai"
case "burstiness":
midpoint = (BASELINES["burstiness"]["ai_typical"] + BASELINES["burstiness"]["human_typical"]) / 2
return "human" if value > midpoint else "ai"
case "sentence_length_cov":
midpoint = (BASELINES["sentence_length_cov"]["ai_typical"] + BASELINES["sentence_length_cov"]["human_typical"]) / 2
return "human" if value > midpoint else "ai"
case "paragraph_length_cov":
midpoint = (BASELINES["paragraph_length_cov"]["ai_typical"] + BASELINES["paragraph_length_cov"]["human_typical"]) / 2
return "human" if value > midpoint else "ai"
case "contraction_rate":
if value >= 0.65:
return "human"
elif value <= 0.40:
return "ai"
return "neutral"
case "passive_voice_pct":
if value > 0.25:
return "ai"
elif value <= 0.20:
return "human"
return "neutral"
case "ly_adverbs_per_100":
# AI tends to overuse adverbs
if value > 2.5:
return "ai"
elif value < 1.5:
return "human"
return "neutral"
case "concrete_noun_density":
# Humans use more concrete language
if value > 0.55:
return "human"
elif value < 0.40:
return "ai"
return "neutral"
case _:
return "neutral"
def determine_verdict(scores: dict[str, str]) -> str:
"""Determine overall verdict based on majority of metric signals."""
ai_count = sum(1 for v in scores.values() if v == "ai")
human_count = sum(1 for v in scores.values() if v == "human")
total_decisive = ai_count + human_count
if total_decisive == 0:
return "mixed"
ai_ratio = ai_count / total_decisive
if ai_ratio >= 0.6:
return "likely_ai"
elif ai_ratio <= 0.4:
return "likely_human"
return "mixed"
def analyze(text: str) -> dict:
"""Run full analysis on input text."""
tokens = tokenize(text)
sentences = split_sentences(text)
paragraphs = split_paragraphs(text)
metrics = {
"ttr": round(calc_ttr(tokens), 4),
"burstiness": round(calc_burstiness(sentences), 4),
"shannon_entropy": round(calc_shannon_entropy(tokens), 4),
"sentence_length_cov": round(calc_sentence_length_cov(sentences), 4),
"mean_sentence_length": round(calc_mean_sentence_length(sentences), 2),
"paragraph_length_cov": round(calc_paragraph_length_cov(paragraphs), 4),
"ly_adverbs_per_100": round(calc_ly_adverbs_per_100(tokens), 4),
"passive_voice_pct": round(calc_passive_voice_pct(text), 4),
"contraction_rate": round(calc_contraction_rate(text), 4),
"concrete_noun_density": round(calc_concrete_noun_density(tokens), 4),
}
scored_metrics = [
"ttr", "burstiness", "sentence_length_cov", "paragraph_length_cov",
"contraction_rate", "passive_voice_pct", "ly_adverbs_per_100",
"concrete_noun_density",
]
signals = {name: score_metric(name, metrics[name]) for name in scored_metrics}
verdict = determine_verdict(signals)
return {
"metrics": metrics,
"signals": signals,
"verdict": verdict,
"baselines": BASELINES,
"meta": {
"total_tokens": len(tokens),
"unique_tokens": len(set(tokens)),
"total_sentences": len(sentences),
"total_paragraphs": len(paragraphs),
},
}
def main() -> None:
parser = argparse.ArgumentParser(
description="Analyze text for AI vs human authorship signals.",
)
parser.add_argument(
"--file", "-f",
type=str,
help="Path to text file to analyze (reads stdin if omitted)",
)
args = parser.parse_args()
if args.file:
with open(args.file, encoding="utf-8") as f:
text = f.read()
else:
text = sys.stdin.read()
if not text.strip():
print(json.dumps({"error": "Empty input"}), file=sys.stdout)
sys.exit(1)
result = analyze(text)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()