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.
This commit is contained in:
353
.github/skills/human-ai/scripts/measure.py
vendored
Normal file
353
.github/skills/human-ai/scripts/measure.py
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user