Skip to content

Instantly share code, notes, and snippets.

@dashw00d
Created February 28, 2026 20:56
Show Gist options
  • Select an option

  • Save dashw00d/754a1425520490c61ea240d570ebad56 to your computer and use it in GitHub Desktop.

Select an option

Save dashw00d/754a1425520490c61ea240d570ebad56 to your computer and use it in GitHub Desktop.
Comprehensive AI content detection using phrase matching, structural analysis, and statistical linguistics. Zero external dependencies (stdlib only).
"""AI Slop Detector & Smart Humanizer v3.0
Comprehensive AI content detection using phrase matching, structural analysis,
and statistical linguistics. Zero external dependencies (stdlib only).
Python API:
cleaner = AISlopCleaner()
report = cleaner.detect(text) # Normalized scoring
report_raw = cleaner.detect(text, strict=True) # Raw scoring (no length normalization)
result = cleaner.humanize(text, 280) # Detect + clean
cleaner.configure_openrouter(api_key="...") # Optional OpenRouter backend
guarded = cleaner.rewrite_if_slop(text) # Rewrite if score exceeds threshold
CLI quick usage:
# Detect text
python3 humanize.py --mode detect --text "Here is some text"
echo "Here is some text" | python3 humanize.py --mode detect
# Humanize
python3 humanize.py --mode humanize --file input.txt --max-length 500
# Guardrail rewrite (rewrite only when score >= threshold)
python3 humanize.py --mode guardrail --file input.txt --threshold 8
# Generate via OpenRouter + auto-rewrite if slop
export OPENROUTER_API_KEY=...
python3 humanize.py --mode generate --prompt "Write release notes"
# Synthetic stress benchmark
python3 humanize.py --mode benchmark --benchmark-size small --threshold 8
Useful flags:
--score-mode normalized|raw
--json
--self-test
--openrouter-model MODEL_ID
"""
import json
import os
import argparse
import random
import re
import statistics
import sys
import time
import urllib.error
import urllib.request
from collections import Counter, defaultdict
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set, Tuple
# ═════════════════════════════════════════════════════════════════════════════
# DATA CLASSES
# ═════════════════════════════════════════════════════════════════════════════
@dataclass
class Finding:
category: str # "tier1", "tier2", "tier3", "structural", "statistical"
subcategory: str # specific signal name
score: int # points contributed
detail: str # human-readable explanation
severity: str # "info", "warning", "critical"
# ═════════════════════════════════════════════════════════════════════════════
# TIER 1 PATTERNS — Almost always AI, +3 per match
# ═════════════════════════════════════════════════════════════════════════════
TIER1_DB: Dict[str, List[str]] = {
"artificial_enthusiasm": [
r"delve(?:\s+into)?",
r"the answer surprised me",
r"here'?s what blew my mind",
r"that'?s when it clicked",
r"mind[\s.\-]?blowing",
],
"game_changer_variants": [
r"game[\s.\-]?chang(?:er|ing)",
r"\brevolutionary\b",
r"\bgroundbreaking\b",
r"cutting[\s\-]?edge",
r"paradigm shift",
],
"hedging_openers": [
r"it'?s worth noting(?:\s+that)?",
r"\bmoreover\b",
r"\bfurthermore\b",
r"in today'?s (?:digital )?(?:landscape|world|era)",
],
"corporate_jargon": [
r"leverage synergies",
r"unlock your potential",
r"cutting[\-\s]edge solutions",
r"the (?:real )?unlock\b",
r"the real moat",
r"the winning pattern",
],
"rhetorical_openers": [
r"(?:^|(?<=[.!?]\s))honestly\?",
r"(?:^|(?<=[.!?]\s))the truth\?",
r"(?:^|(?<=[.!?]\s))the reality\?",
r"the crazy part\?",
r"here'?s the thing:?(?!\w)",
r"here'?s why:?(?!\w)",
r"spoiler alert:?",
r"plot twist:?",
],
"claude_signatures": [
r"(?:lever|thing) most people miss",
r"(?:one key|a powerful|the most important|underappreciated|hidden) lever",
r"nuanced (?:understanding|perspective|approach|view)",
r"(?:rich|intricate) tapestry",
r"(?:complex|nuanced) interplay",
r"in the realm of",
r"sheds light on",
r"\billuminates\b",
r"underscores the importance",
r"\btestament to\b",
r"profound impact",
r"transformative potential",
r"multifaceted (?:nature|approach)",
r"at the intersection of",
r"holistic (?:approach|view)",
r"thoughtful consideration",
r"balanced perspective",
],
"claude_flattery": [
r"you'?re absolutely (?:right|correct)",
r"great (?:observation|question)",
r"excellent point",
r"fantastic question",
r"I love that(?:\!|\.)",
r"\bspot on\b",
],
"claude_apologies": [
r"I apologize for the confusion",
r"my apologies if",
],
"breakdown_phrases": [
r"let'?s break this down",
r"let'?s dive (?:in|deeper)",
r"let'?s delve into",
r"let'?s unpack this",
],
"throat_clearing": [
r"the uncomfortable truth is",
r"(?:^|(?<=[.!?]\s))it turns out(?:\s+that)?",
r"I'?ll say it again:?",
r"I'?m going to be honest:?",
r"can we talk about\b",
r"here'?s what I find interesting",
r"here'?s the problem though",
],
"emphasis_crutches": [
r"(?:full stop|period)\.",
r"this matters because",
r"make no mistake:?",
r"here'?s why that matters",
r"let that sink in",
r"say it louder",
r"this is huge",
],
"filler_adverbs": [
r"at its core,?",
r"(?:^|(?<=\s))interestingly,",
r"(?:^|(?<=\s))importantly,",
r"(?:^|(?<=\s))crucially,",
r"when it comes to\b",
r"in a world where\b",
r"(?:^|(?<=[.!?]\s))the reality is",
],
"meta_commentary": [
r"(?:^|(?<=\s))hint:",
r"you already know this,? but",
r"but that'?s another (?:post|story|topic)",
r"\w+ is a feature,? not a bug",
],
"banned_casual": [
r"\bcrazy take\b",
r"\bhot take\b",
r"\bunpopular opinion:?\b",
r"\bhear me out\b",
r"\bngl\b",
r"\blowkey\b",
r"the real question is",
r"the real problem is",
r"just my take",
r"but that'?s just me",
],
"hedging_fillers": [
r"I could be wrong,? but",
r"(?:^|(?<=\s))to be fair,?(?:\s|$)",
],
"navigation_filler": [
r"\bas per\b",
r"let me be clear",
r"navigat(?:e|ing) the\b",
],
"overstatement": [
r"\bfascinating\b",
r"\bremarkable\b",
],
}
# ═════════════════════════════════════════════════════════════════════════════
# TIER 2 PATTERNS — Suspicious when repeated (2+), +2 per occurrence
# ═════════════════════════════════════════════════════════════════════════════
TIER2_DB: Dict[str, List[str]] = {
"repetitive_transitions": [
r"here'?s the thing",
r"at the end of the day",
r"let'?s be clear",
r"the bottom line",
],
"paired_adjectives": [
r"comprehensive and thorough",
r"unique and intense",
r"simple and straightforward",
r"complex and nuanced",
r"powerful and versatile",
r"robust and scalable",
r"intuitive and user[\s\-]?friendly",
],
"template_phrases": [
r"in this (?:post|article),? we'?ll (?:cover|explore|look at|discuss)",
r"by the end of this (?:article|post|guide),? you'?ll",
r"without further ado",
],
}
# ═════════════════════════════════════════════════════════════════════════════
# TIER 3 PATTERNS — OK alone, flag clusters of 3+ in same paragraph
# ═════════════════════════════════════════════════════════════════════════════
TIER3_PATTERNS: List[str] = [
r"\bhowever\b",
r"\bfurthermore\b",
r"\bmoreover\b",
r"\badditionally\b",
r"\bconsequently\b",
r"\bnevertheless\b",
r"\bnonetheless\b",
r"\bfirstly\b",
r"\bsecondly\b",
r"\bthirdly\b",
r"\bin conclusion\b",
r"\bmoving forward\b",
r"\bstakeholder[s]?\b",
r"\bcircle back\b",
r"\btouch base\b",
r"\brobust\b",
r"\bseamless(?:ly)?\b",
r"\bscalable\b",
r"\bmeanwhile\b",
r"\bsubsequently\b",
r"\bsimilarly\b",
r"\baccordingly\b",
r"\blikewise\b",
]
# ═════════════════════════════════════════════════════════════════════════════
# REPLACEMENT RULES — (regex, replacement) for the humanize() cleaning pass
# ═════════════════════════════════════════════════════════════════════════════
REPLACE_RULES: List[Tuple[re.Pattern, str]] = [
# Original banned casual
(
re.compile(
r"\bcrazy take\b|\bhot take\b|\bunpopular opinion\b|\bhear me out\b|\bthis is huge\b|\blet that sink in\b|\bsay it louder\b",
re.I,
),
"",
),
(
re.compile(
r"\bgame.?changer\b|\bparadigm shift\b|\bthe winning pattern\b|\bthe real unlock\b|\bthe real moat\b",
re.I,
),
"real advantage",
),
(
re.compile(
r"\bngl\b|\blowkey\b|\bthe real question is\b|\bthe real problem is\b|\bjust my take\b|\bbut that'?s just me\b",
re.I,
),
"",
),
(
re.compile(
r"\bI could be wrong but\b|\bto be fair\b|\bpretty accurate\b|\bpretty close\b|\bI mean\b",
re.I,
),
"",
),
(
re.compile(
r"\bas per\b|\blet'?s dive in\b|\blet me be clear\b|\bnavigat(?:e|ing) the\b",
re.I,
),
"",
),
(
re.compile(
r"\bhere'?s the thing\b|\bhere'?s why\b|\bspoiler alert\b|\bplot twist\b|\bmind.?blowing\b",
re.I,
),
"",
),
(re.compile(r"\bfascinating\b|\bremarkable\b", re.I), "solid"),
# Tier 1 replacements
(re.compile(r"delve(?:\s+into)?", re.I), "look at"),
(
re.compile(
r"the answer surprised me|here's what blew my mind|that's when it clicked",
re.I,
),
"",
),
(re.compile(r"the crazy part\?|honestly\?|the truth\?|the reality\?", re.I), ""),
(re.compile(r"it's worth noting that|it's worth noting", re.I), ""),
(re.compile(r"moreover|furthermore", re.I), "also"),
(
re.compile(r"in today's (?:digital )?(?:landscape|world|era)", re.I),
"These days",
),
(
re.compile(r"game-?changing|revolutionary|cutting-?edge|groundbreaking", re.I),
"powerful",
),
(
re.compile(
r"leverage synergies|unlock your potential|cutting-edge solutions", re.I
),
"",
),
# Claude signatures
(
re.compile(
r"lever most people miss|one key lever|a powerful lever|the most important lever|underappreciated lever|hidden lever",
re.I,
),
"thing most people miss",
),
(
re.compile(
r"nuanced understanding|nuanced perspective|nuanced approach|nuanced view",
re.I,
),
"real take",
),
(
re.compile(
r"rich tapestry|intricate tapestry|complex interplay|nuanced interplay",
re.I,
),
"",
),
(
re.compile(
r"in the realm of|sheds light on|illuminates|underscores the importance|testament to",
re.I,
),
"",
),
(
re.compile(
r"profound impact|transformative potential|multifaceted nature|at the intersection of",
re.I,
),
"",
),
(
re.compile(
r"you're absolutely right|you're absolutely correct|great observation|excellent point|fantastic question|I love that|spot on",
re.I,
),
"",
),
(re.compile(r"I apologize for the confusion|my apologies if", re.I), ""),
(
re.compile(
r"let's break this down|let's dive deeper|let's delve into|let's unpack this",
re.I,
),
"Here's the breakdown",
),
(
re.compile(
r"holistic approach|holistic view|thoughtful consideration|balanced perspective",
re.I,
),
"",
),
# Clinical formality → plain language
(re.compile(r"\butilize[sd]?\b", re.I), "use"),
(re.compile(r"\bin order to\b", re.I), "to"),
(re.compile(r"\bfacilitat(?:e[sd]?|ing)\b", re.I), "help"),
(re.compile(r"\bprior to\b", re.I), "before"),
(re.compile(r"\bnumerous\b", re.I), "many"),
(re.compile(r"\bsubsequently\b", re.I), "then"),
(re.compile(r"\bindividuals\b", re.I), "people"),
# Throat-clearing + filler removal
(re.compile(r"the uncomfortable truth is,?\s*", re.I), ""),
(re.compile(r"it turns out(?:\s+that)?,?\s*", re.I), ""),
(re.compile(r"I'?m going to be honest:?\s*", re.I), ""),
(re.compile(r"this matters because\s*", re.I), ""),
(re.compile(r"make no mistake:?\s*", re.I), ""),
(re.compile(r"here'?s why that matters:?\s*", re.I), ""),
(re.compile(r"at its core,?\s*", re.I), ""),
(re.compile(r"when it comes to\s+", re.I), "for "),
(re.compile(r"in a world where\s+", re.I), "since "),
]
# ═════════════════════════════════════════════════════════════════════════════
# WORD SETS & STRUCTURAL PATTERNS
# ═════════════════════════════════════════════════════════════════════════════
CLINICAL_PAIRS: List[Tuple[str, str]] = [
(r"\bindividuals\b", "people"),
(r"\butilize[sd]?\b", "use"),
(r"\bin order to\b", "to"),
(r"\bfacilitat(?:e[sd]?|ing)\b", "help"),
(r"\bcommenc(?:e[sd]?|ing)\b", "start"),
(r"\bsubsequently\b", "then"),
(r"\bprior to\b", "before"),
(r"\bdemonstrat(?:e[sd]?|ing)\b", "show"),
(r"\bencompass(?:es|ing)?\b", "include"),
(r"\bnumerous\b", "many"),
(r"\bsufficient\b", "enough"),
(r"\bleverag(?:e|ing)\b", "use"),
(r"\bascertain(?:ed|ing)?\b", "find out"),
(r"\bimplement(?:ed|ing|s)?\b", "do"),
(r"\boptimal\b", "best"),
(r"\badditionally\b", "also"),
]
AI_INTENSIFIERS: Set[str] = {
"deeply",
"truly",
"fundamentally",
"inherently",
"simply",
"literally",
"inevitably",
"incredibly",
"remarkably",
"genuinely",
"profoundly",
"essentially",
"absolutely",
"undeniably",
"unquestionably",
}
HEDGE_WORDS: Set[str] = {
"might",
"could",
"perhaps",
"generally",
"typically",
"often",
"usually",
"sometimes",
"possibly",
"potentially",
"arguably",
"relatively",
"somewhat",
"largely",
"presumably",
"tends",
"likely",
"unlikely",
"suggests",
"appears",
"conceivably",
"ostensibly",
"approximately",
"virtually",
}
HEDGE_PHRASES: List[str] = [
"it seems",
"it appears",
"it is possible",
"it is likely",
"to some extent",
"in some cases",
"in many ways",
"it could be argued",
"one might argue",
"broadly speaking",
"for the most part",
"it should be noted",
]
TRANSITION_WORDS: Set[str] = {
"however",
"moreover",
"furthermore",
"additionally",
"consequently",
"therefore",
"nevertheless",
"nonetheless",
"meanwhile",
"subsequently",
"conversely",
"similarly",
"likewise",
"accordingly",
"alternatively",
"specifically",
"notably",
"importantly",
"significantly",
"ultimately",
"essentially",
"fundamentally",
"interestingly",
"surprisingly",
"fortunately",
"unfortunately",
"increasingly",
"overall",
}
TRANSITION_PHRASES: List[str] = [
"in addition",
"on the other hand",
"as a result",
"for example",
"for instance",
"in contrast",
"in fact",
"of course",
"at the same time",
"in other words",
"to be sure",
"in particular",
"by contrast",
"in summary",
"to conclude",
"first of all",
"last but not least",
"moving forward",
]
FUNCTION_WORDS: Set[str] = {
"the",
"a",
"an",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"do",
"does",
"did",
"will",
"would",
"shall",
"should",
"may",
"might",
"must",
"can",
"could",
"of",
"in",
"to",
"for",
"with",
"on",
"at",
"from",
"by",
"about",
"as",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"between",
"and",
"but",
"or",
"nor",
"not",
"so",
"yet",
"both",
"either",
"neither",
"each",
"every",
"all",
"any",
"few",
"more",
"most",
"other",
"some",
"such",
"no",
"only",
"own",
"same",
"than",
"too",
"very",
"just",
"that",
"this",
"these",
"those",
"it",
"its",
"i",
"me",
"my",
"we",
"us",
"our",
"you",
"your",
"he",
"him",
"his",
"she",
"her",
"they",
"them",
"their",
}
BINARY_CONTRAST_PATTERNS: List[str] = [
r"this isn'?t .{3,50}\.\s*it'?s ",
r"not because .{3,50}\.\s*because ",
r"\w+ isn'?t the (?:problem|issue|challenge)\.\s*\w+ is\b",
r"the (?:answer|solution|key|point) isn'?t .{3,40}\.\s*it'?s ",
r"it (?:feels|looks|seems) like .{3,40}\.\s*it'?s actually ",
r"the question isn'?t .{3,40}\.\s*it'?s ",
r"stops being .{3,30} and starts being ",
r"it'?s not (?:about|just) .{3,40}\.\s*it'?s (?:about )?",
]
SNARK_PATTERNS: List[str] = [
r"that got old fast",
r"less than (?:excellent|ideal)",
r"you guessed it",
r"surprise,? surprise",
r"ask me how I know",
r"don'?t ask me how I know",
]
WAR_STORY_PATTERNS: List[str] = [
r"here'?s what (?:bit|got|tripped|caught) me",
r"the \w+ gotcha",
r"learned this the hard way",
]
FLATTERY_PATTERNS: List[str] = [
r"while (?:traditional|conventional|older|existing) .{5,60},?\s*(?:modern|new|current|contemporary|today)",
r"though (?:conventional wisdom|traditional approaches|older methods)",
r"(?:traditional|conventional) (?:methods|approaches|tools|solutions) have (?:their )?merit",
r"(?:has|have) (?:its|their) (?:merits?|strengths?|advantages?).{0,20}(?:but|however|yet)",
]
CONTRACTIONS_RE = re.compile(
r"\b(?:i'm|i've|i'll|i'd|you're|you've|you'll|you'd|"
r"he's|she's|it's|we're|we've|we'll|we'd|they're|they've|they'll|they'd|"
r"isn't|aren't|wasn't|weren't|hasn't|haven't|hadn't|"
r"doesn't|don't|didn't|won't|wouldn't|can't|couldn't|"
r"shouldn't|mustn't|that's|there's|here's|"
r"what's|who's|let's)\b",
re.I,
)
EXPANDABLE_RE = re.compile(
r"\b(?:I am|I have|I will|I would|you are|you have|you will|"
r"he is|she is|it is|we are|we have|we will|they are|they have|they will|"
r"is not|are not|was not|were not|has not|have not|had not|"
r"does not|do not|did not|will not|would not|can not|cannot|could not|"
r"should not|that is|there is|here is|what is|who is|let us)\b",
re.I,
)
UNICODE_NORMALIZE_MAP = str.maketrans(
{
"\u2018": "'",
"\u2019": "'",
"\u201c": '"',
"\u201d": '"',
"\u2013": "-",
"\u2014": "-",
"\u2212": "-",
"\u2026": "...",
"\u00a0": " ",
"\u202f": " ",
}
)
DOMAIN_JARGON_TERMS: Set[str] = {
"api",
"apis",
"sdk",
"sql",
"postgres",
"mysql",
"redis",
"kubernetes",
"docker",
"pipeline",
"latency",
"throughput",
"schema",
"endpoint",
"endpoints",
"oauth",
"jwt",
"grpc",
"microservice",
"microservices",
"runtime",
"compiler",
"refactor",
"deployment",
"deploy",
}
DOMAIN_JARGON_PHRASES: List[str] = [
"ci/cd",
"unit test",
"integration test",
"load balancer",
"query planner",
]
TECH_ALLOWLIST_TIER3_PATTERNS: Set[str] = {
r"\brobust\b",
r"\bscalable\b",
r"\bseamless(?:ly)?\b",
}
# ═════════════════════════════════════════════════════════════════════════════
# DETECTOR CLASS
# ═════════════════════════════════════════════════════════════════════════════
class AISlopCleaner:
def __init__(self):
self._compiled_tier1: Dict[str, re.Pattern] = {}
self._compiled_tier2: Dict[str, re.Pattern] = {}
self._compiled_tier3: List[re.Pattern] = []
self._compiled_binary: List[re.Pattern] = []
self._compiled_clinical: List[Tuple[re.Pattern, str]] = []
self._compiled_snark: List[re.Pattern] = []
self._compiled_war: List[re.Pattern] = []
self._compiled_flattery: List[re.Pattern] = []
self.replace_rules = REPLACE_RULES
self._emoji_re = re.compile(
r"[\U0001F300-\U0001F9FF\U00002702-\U000027B0\U0001FA00-\U0001FAFF"
r"\U00002600-\U000026FF\U0000FE00-\U0000FE0F\U0001F000-\U0001F02F]"
)
self.openrouter_config: Dict[str, Any] = {
"enabled": False,
"api_key": None,
"model": "openai/gpt-4o-mini",
"url": "https://openrouter.ai/api/v1/chat/completions",
"timeout": 45,
"app_name": "AISlopCleaner",
"app_url": "https://localhost",
}
self._compile_all()
def _compile_all(self):
for cat, patterns in TIER1_DB.items():
combined = "|".join(f"(?:{p})" for p in patterns)
self._compiled_tier1[cat] = re.compile(combined, re.I)
for cat, patterns in TIER2_DB.items():
combined = "|".join(f"(?:{p})" for p in patterns)
self._compiled_tier2[cat] = re.compile(combined, re.I)
self._compiled_tier3 = [re.compile(p, re.I) for p in TIER3_PATTERNS]
self._compiled_binary = [re.compile(p, re.I) for p in BINARY_CONTRAST_PATTERNS]
self._compiled_clinical = [
(re.compile(p, re.I), alt) for p, alt in CLINICAL_PAIRS
]
self._compiled_snark = [re.compile(p, re.I) for p in SNARK_PATTERNS]
self._compiled_war = [re.compile(p, re.I) for p in WAR_STORY_PATTERNS]
self._compiled_flattery = [re.compile(p, re.I) for p in FLATTERY_PATTERNS]
# ─── Helpers ──────────────────────────────────────────────────────────
def _normalize_text(self, text: str) -> str:
normalized = text.translate(UNICODE_NORMALIZE_MAP)
normalized = normalized.replace("\r\n", "\n").replace("\r", "\n")
normalized = re.sub(r"[\u200B-\u200D\u2060\uFEFF]", "", normalized)
normalized = re.sub(r"[^\S\n]+", " ", normalized)
return normalized.strip()
def _is_technical_context(self, text: str, word_count: int = 0) -> bool:
lower = text.lower()
tokens = re.findall(r"\b[a-z0-9_./+-]+\b", lower)
total = word_count if word_count else len(tokens)
if total < 20:
return False
token_hits = sum(1 for t in tokens if t in DOMAIN_JARGON_TERMS)
phrase_hits = sum(lower.count(p) for p in DOMAIN_JARGON_PHRASES)
hits = token_hits + phrase_hits
density = hits / total
min_hits = 3 if total < 80 else 4
return hits >= min_hits and density >= 0.03
def _normalize_score_by_length(self, raw_score: int, word_count: int) -> int:
if raw_score <= 0:
return 0
if word_count <= 220:
return raw_score
factor = 1.0 + min(1.2, (word_count - 220) / 900)
return max(1, int(round(raw_score / factor)))
def configure_openrouter(
self,
api_key: Optional[str] = None,
model: str = "openai/gpt-4o-mini",
timeout: int = 45,
app_name: str = "AISlopCleaner",
app_url: str = "https://localhost",
url: str = "https://openrouter.ai/api/v1/chat/completions",
) -> bool:
"""Configure OpenRouter backend for optional rewrite/generation guardrails."""
key = api_key or os.getenv("OPENROUTER_API_KEY")
self.openrouter_config.update(
{
"enabled": bool(key),
"api_key": key,
"model": model,
"timeout": timeout,
"app_name": app_name,
"app_url": app_url,
"url": url,
}
)
return bool(key)
def _openrouter_chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.2,
max_tokens: int = 900,
) -> str:
cfg = self.openrouter_config
if not cfg.get("enabled") or not cfg.get("api_key"):
raise RuntimeError(
"OpenRouter is not configured. Call configure_openrouter() first."
)
payload = {
"model": cfg["model"],
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
req = urllib.request.Request(
cfg["url"],
data=json.dumps(payload).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {cfg['api_key']}",
"HTTP-Referer": cfg.get("app_url", "https://localhost"),
"X-Title": cfg.get("app_name", "AISlopCleaner"),
},
method="POST",
)
try:
with urllib.request.urlopen(
req, timeout=cfg.get("timeout", 45)
) as response:
raw = response.read().decode("utf-8")
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="ignore")
raise RuntimeError(f"OpenRouter HTTP {exc.code}: {detail[:300]}") from exc
except urllib.error.URLError as exc:
raise RuntimeError(f"OpenRouter connection error: {exc.reason}") from exc
data = json.loads(raw)
choices = data.get("choices") or []
if not choices:
raise RuntimeError("OpenRouter response missing choices")
content = choices[0].get("message", {}).get("content", "")
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
parts.append(item.get("text", ""))
content = "\n".join(p for p in parts if p)
return str(content).strip()
def _split_sentences(self, text: str) -> List[str]:
sentences = re.split(r"(?<=[.!?])\s+", text)
return [s.strip() for s in sentences if len(s.strip().split()) > 2]
def _sentence_lengths(self, text: str) -> List[int]:
sentences = re.split(r"[.!?]+", text)
return [len(s.strip().split()) for s in sentences if len(s.strip().split()) > 3]
# ─── Tier 1 Phrase Scanning ───────────────────────────────────────────
def _scan_tier1(self, text: str) -> List[Finding]:
findings = []
for cat, regex in self._compiled_tier1.items():
matches = regex.findall(text)
if matches:
count = len(matches)
findings.append(
Finding(
"tier1", cat, 3 * count, f"{cat}: {count} match(es)", "critical"
)
)
return findings
# ─── Tier 2 Scanning (flag on repetition) ────────────────────────────
def _scan_tier2(self, text: str) -> List[Finding]:
findings = []
for cat, regex in self._compiled_tier2.items():
matches = regex.findall(text)
if len(matches) >= 2:
findings.append(
Finding(
"tier2",
cat,
2 * len(matches),
f"{cat}: '{matches[0]}' repeated {len(matches)}x",
"warning",
)
)
return findings
# ─── Tier 3 Cluster Detection ─────────────────────────────────────────
def _scan_tier3_clusters(
self, text: str, technical_context: bool = False
) -> List[Finding]:
findings = []
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
if not paragraphs:
paragraphs = [text]
for i, para in enumerate(paragraphs):
present = []
for pattern in self._compiled_tier3:
if not pattern.search(para):
continue
if (
technical_context
and pattern.pattern in TECH_ALLOWLIST_TIER3_PATTERNS
):
continue
present.append(pattern)
if len(present) >= 3:
findings.append(
Finding(
"tier3",
"word_cluster",
2,
f"Paragraph {i + 1}: {len(present)} corporate/transition words clustered",
"warning",
)
)
# Also check: paragraphs starting with transition words
openers = []
for para in paragraphs:
if para.strip():
first = para.strip().split()[0].lower().rstrip(",")
openers.append(first)
t_count = sum(1 for w in openers if w in TRANSITION_WORDS)
if t_count >= 3 and len(openers) >= 4:
findings.append(
Finding(
"tier3",
"transition_openers",
3,
f"{t_count}/{len(openers)} paragraphs open with transition words",
"critical",
)
)
return findings
# ─── Structural: Staccato Fragments ───────────────────────────────────
def _detect_staccato(self, text: str) -> List[Finding]:
sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
if len(sentences) < 3:
return []
runs = 0
current_run = 0
for sentence in sentences:
words = sentence.split()
is_short_declarative = (
3 <= len(words) <= 6
and sentence[0:1].isupper()
and sentence[-1:] in ".!?"
)
if is_short_declarative:
current_run += 1
else:
if current_run >= 3:
runs += 1
current_run = 0
if current_run >= 3:
runs += 1
if not runs:
return []
return [
Finding(
"structural",
"staccato_fragments",
4 * runs,
f"Staccato fragment spam: {runs} instance(s) of 3+ very short declaratives",
"critical",
)
]
# ─── Structural: Binary Contrasts ─────────────────────────────────────
def _detect_binary_contrasts(self, text: str) -> List[Finding]:
count = 0
for regex in self._compiled_binary:
count += len(regex.findall(text))
findings = []
if count:
findings.append(
Finding(
"structural",
"binary_contrast",
2 * count,
f"Binary contrast structures x{count} ('This isn't X. It's Y.')",
"warning",
)
)
return findings
# ─── Structural: Manufactured Personality ─────────────────────────────
def _detect_manufactured_personality(self, text: str) -> List[Finding]:
lower = text.lower()
score = 0
signals = []
# Fake casual "So I..." transitions
so_i_count = len(re.findall(r"(?:^|\.\s+)so\s+i\s", lower))
if so_i_count >= 2:
score += 2
signals.append(f'"So I..." transitions x{so_i_count}')
# Manufactured snark
for regex in self._compiled_snark:
if regex.search(lower):
score += 1
signals.append(f"Manufactured snark: {regex.pattern[:30]}")
# Fake war stories
for regex in self._compiled_war:
if regex.search(lower):
score += 1
signals.append("Fake war story pattern")
# Anaphora: 3+ consecutive sentences starting with same word
sentences = self._split_sentences(text)
for i in range(len(sentences) - 2):
trio = sentences[i : i + 3]
firsts = [s.split()[0].lower() if s.split() else "" for s in trio]
if (
len(set(firsts)) == 1
and firsts[0]
and firsts[0] not in {"i", "we", "you"}
):
score += 2
signals.append(f'Anaphora: "{firsts[0]}..." x3')
break
findings = []
if score >= 3:
findings.append(
Finding(
"structural",
"manufactured_personality",
min(score, 6),
f"Manufactured personality ({score} signals): {'; '.join(signals[:4])}",
"critical",
)
)
elif score >= 1:
findings.append(
Finding(
"structural",
"manufactured_personality",
score,
f"Possible manufactured personality: {'; '.join(signals[:3])}",
"warning",
)
)
return findings
# ─── Structural: Flattery Sandwiches ──────────────────────────────────
def _detect_flattery_sandwiches(self, text: str) -> List[Finding]:
count = 0
for regex in self._compiled_flattery:
count += len(regex.findall(text))
findings = []
if count:
findings.append(
Finding(
"structural",
"flattery_sandwich",
2 * count,
f"Flattery sandwich pattern x{count}",
"warning",
)
)
return findings
# ─── Structural: List Addiction ────────────────────────────────────────
def _detect_list_addiction(self, text: str) -> List[Finding]:
bullets = len(re.findall(r"(?:^|\n)\s*[-*]\s", text))
numbered = len(re.findall(r"(?:^|\n)\s*\d+[.)]\s", text))
total = bullets + numbered
intros = len(re.findall(r"here are (?:the )?\d+\s+\w+", text, re.I))
findings = []
if total > 10 or (total > 5 and len(text.split("\n\n")) < 5):
findings.append(
Finding(
"structural",
"list_addiction",
2,
f"List addiction: {total} bullet/numbered items",
"warning",
)
)
if intros >= 2:
findings.append(
Finding(
"structural",
"list_addiction",
2,
f'"Here are the N..." pattern x{intros}',
"warning",
)
)
return findings
# ─── Structural: Emoji Headers ────────────────────────────────────────
def _detect_emoji_headers(self, text: str) -> List[Finding]:
count = 0
for line in text.split("\n"):
stripped = line.strip()
if stripped and self._emoji_re.match(stripped):
count += 1
if re.match(r"^#{1,4}\s*[\U0001F300-\U0001F9FF]", stripped):
count += 1
findings = []
if count >= 3:
findings.append(
Finding(
"structural",
"emoji_headers",
3,
f"Emoji headers: {count} instances",
"warning",
)
)
return findings
# ─── Structural: Over-Balanced Sections ───────────────────────────────
def _detect_section_balance(self, text: str) -> List[Finding]:
sections = re.split(r"\n#{1,3}\s|\n\n\n", text)
sections = [s.strip() for s in sections if len(s.strip()) > 20]
if len(sections) < 3:
return []
lengths = [len(s.split()) for s in sections]
mean_len = statistics.mean(lengths)
if mean_len < 5:
return []
sd = statistics.stdev(lengths) if len(lengths) > 1 else 0
cv = sd / mean_len if mean_len > 0 else 0
findings = []
if cv < 0.15:
findings.append(
Finding(
"structural",
"section_balance",
2,
f"Over-balanced sections (CV={cv:.2f}, {len(sections)} sections ~{mean_len:.0f} words each)",
"warning",
)
)
return findings
# ─── Statistical: Sentence Uniformity ─────────────────────────────────
def _analyze_sentence_uniformity(self, text: str) -> List[Finding]:
lengths = self._sentence_lengths(text)
if len(lengths) < 5:
return []
sd = statistics.stdev(lengths)
first_person_count = len(re.findall(r"\b(?:i|we)\b", text.lower()))
narrative_bias = first_person_count >= 3
findings = []
if sd < 3.0:
score = 3 if narrative_bias else 5
severity = "warning" if narrative_bias else "critical"
findings.append(
Finding(
"statistical",
"sentence_uniformity",
score,
f"Very uniform sentence lengths (SD={sd:.1f})",
severity,
)
)
elif sd < 5.0:
score = 2 if narrative_bias else 3
severity = "info" if narrative_bias else "warning"
findings.append(
Finding(
"statistical",
"sentence_uniformity",
score,
f"Low sentence length variance (SD={sd:.1f})",
severity,
)
)
# AI sweet spot: >82% of sentences in 8-16 word range
in_sweet = sum(1 for l in lengths if 8 <= l <= 16)
ratio = in_sweet / len(lengths)
if ratio > 0.82 and len(lengths) > 5 and not narrative_bias:
findings.append(
Finding(
"statistical",
"sentence_sweet_spot",
3,
f"{ratio:.0%} of sentences in 8-16 word AI range",
"warning",
)
)
return findings
# ─── Statistical: Burstiness ──────────────────────────────────────────
def _analyze_burstiness(self, text: str) -> List[Finding]:
lengths = self._sentence_lengths(text)
if len(lengths) < 8:
return []
deltas = [abs(lengths[i + 1] - lengths[i]) for i in range(len(lengths) - 1)]
if not deltas:
return []
mean_delta = statistics.mean(deltas)
findings = []
if mean_delta < 0.5:
findings.append(
Finding(
"statistical",
"burstiness",
3,
"Near-zero sentence variation",
"critical",
)
)
return findings
stdev_delta = statistics.stdev(deltas) if len(deltas) > 1 else 0
cv = stdev_delta / mean_delta if mean_delta > 0 else 0
if cv < 0.4:
findings.append(
Finding(
"statistical",
"burstiness",
3,
f"Very low burstiness (CV={cv:.2f}), uniform rhythm",
"critical",
)
)
elif cv < 0.6:
findings.append(
Finding(
"statistical",
"burstiness",
2,
f"Low burstiness (CV={cv:.2f})",
"warning",
)
)
return findings
# ─── Statistical: Lexical Diversity (MATTR) ──────────────────────────
def _analyze_lexical_diversity(self, text: str) -> List[Finding]:
words = re.findall(r"\b[a-z]+\b", text.lower())
total = len(words)
if total < 30:
return []
if total < 50:
# Short text: raw TTR
ttr = len(set(words)) / total
findings = []
if ttr < 0.40:
findings.append(
Finding(
"statistical",
"lexical_diversity",
3,
f"Low lexical diversity (TTR={ttr:.2f})",
"critical",
)
)
elif ttr < 0.50:
findings.append(
Finding(
"statistical",
"lexical_diversity",
1,
f"Below-average lexical diversity (TTR={ttr:.2f})",
"info",
)
)
return findings
# MATTR: Moving Average TTR with window of 50
window = 50
ttrs = []
for i in range(total - window + 1):
chunk = words[i : i + window]
ttrs.append(len(set(chunk)) / window)
mattr = statistics.mean(ttrs)
findings = []
if mattr < 0.35:
findings.append(
Finding(
"statistical",
"lexical_diversity",
3,
f"Very low lexical diversity (MATTR={mattr:.3f})",
"critical",
)
)
elif mattr < 0.45:
findings.append(
Finding(
"statistical",
"lexical_diversity",
2,
f"Low lexical diversity (MATTR={mattr:.3f})",
"warning",
)
)
elif mattr < 0.55:
findings.append(
Finding(
"statistical",
"lexical_diversity",
1,
f"Below-average lexical diversity (MATTR={mattr:.3f})",
"info",
)
)
return findings
# ─── Statistical: Hedging Frequency ───────────────────────────────────
def _analyze_hedging(self, text: str) -> List[Finding]:
words = re.findall(r"\b[a-z]+\b", text.lower())
total = len(words)
if total < 50:
return []
hedge_count = sum(1 for w in words if w in HEDGE_WORDS)
lower = text.lower()
for phrase in HEDGE_PHRASES:
hedge_count += lower.count(phrase)
ratio = hedge_count / total
findings = []
if ratio > 0.08:
findings.append(
Finding(
"statistical",
"hedging",
3,
f"Heavy hedging ({ratio:.1%}, {hedge_count} hedges in {total} words)",
"critical",
)
)
elif ratio > 0.05:
findings.append(
Finding(
"statistical",
"hedging",
2,
f"Excessive hedging ({ratio:.1%}, {hedge_count} hedges)",
"warning",
)
)
return findings
# ─── Statistical: Transition Word Overuse ─────────────────────────────
def _analyze_transitions(self, text: str) -> List[Finding]:
sentences = self._split_sentences(text)
if len(sentences) < 5:
return []
transition_starts = 0
types_used: Set[str] = set()
for sent in sentences:
lower_sent = sent.lower().strip()
first_word = lower_sent.split()[0].rstrip(",") if lower_sent.split() else ""
if first_word in TRANSITION_WORDS:
transition_starts += 1
types_used.add(first_word)
else:
for phrase in TRANSITION_PHRASES:
if lower_sent.startswith(phrase):
transition_starts += 1
types_used.add(phrase)
break
ratio = transition_starts / len(sentences)
findings = []
if ratio > 0.5:
findings.append(
Finding(
"statistical",
"transition_overuse",
3,
f"{transition_starts}/{len(sentences)} sentences start with transitions ({ratio:.0%})",
"critical",
)
)
elif ratio > 0.3:
findings.append(
Finding(
"statistical",
"transition_overuse",
2,
f"{transition_starts}/{len(sentences)} sentences start with transitions ({ratio:.0%})",
"warning",
)
)
if transition_starts > 3 and len(types_used) <= 2:
findings.append(
Finding(
"statistical",
"transition_diversity",
1,
f"Low transition diversity: only {len(types_used)} unique transitions used {transition_starts} times",
"warning",
)
)
return findings
# ─── Statistical: Contraction Analysis ────────────────────────────────
def _analyze_contractions(self, text: str) -> List[Finding]:
contraction_count = len(CONTRACTIONS_RE.findall(text))
expanded_count = len(EXPANDABLE_RE.findall(text))
total = contraction_count + expanded_count
if total < 3:
return []
ratio = contraction_count / total
findings = []
if ratio < 0.2:
findings.append(
Finding(
"statistical",
"contraction_avoidance",
2,
f"Low contraction ratio ({ratio:.0%}, {contraction_count}/{total} contracted)",
"warning",
)
)
elif ratio < 0.4:
findings.append(
Finding(
"statistical",
"contraction_avoidance",
1,
f"Below-average contraction ratio ({ratio:.0%})",
"info",
)
)
return findings
# ─── Statistical: Paragraph Structure Uniformity ──────────────────────
def _analyze_paragraph_structure(self, text: str) -> List[Finding]:
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
if len(paragraphs) < 3:
return []
# Sentences per paragraph
sent_counts = []
for p in paragraphs:
sents = [s for s in re.split(r"[.!?]+", p) if len(s.strip().split()) > 2]
sent_counts.append(len(sents))
word_counts = [len(p.split()) for p in paragraphs]
findings = []
if len(sent_counts) >= 3 and all(c > 0 for c in sent_counts):
var = statistics.pvariance(sent_counts)
if var < 1.0:
findings.append(
Finding(
"statistical",
"paragraph_uniformity",
2,
f"Uniform paragraph structure (sentence-count variance={var:.2f})",
"warning",
)
)
# >70% of paragraphs have 3-4 sentences
in_range = sum(1 for c in sent_counts if 3 <= c <= 4)
if in_range / len(sent_counts) > 0.7:
findings.append(
Finding(
"statistical",
"paragraph_uniformity",
1,
f"{in_range}/{len(sent_counts)} paragraphs have 3-4 sentences",
"info",
)
)
if len(word_counts) >= 3:
mean_w = statistics.mean(word_counts)
if mean_w > 0:
sd_w = statistics.pstdev(word_counts)
cv = sd_w / mean_w
if cv < 0.25:
findings.append(
Finding(
"statistical",
"paragraph_uniformity",
1,
f"Uniform paragraph lengths (CV={cv:.2f})",
"info",
)
)
return findings
# ─── Statistical: Repetition Patterns ─────────────────────────────────
def _analyze_repetition_patterns(self, text: str) -> List[Finding]:
sentences = [
s.strip() for s in re.split(r"[.!?]+", text) if len(s.strip().split()) > 3
]
if len(sentences) < 6:
return []
# Sentence opener repetition (first 2 words)
openers = []
for sent in sentences:
words = sent.lower().split()[:2]
openers.append(" ".join(words))
opener_counts = Counter(openers)
top_freq = opener_counts.most_common(1)[0][1] / len(sentences)
# Function-word skeleton comparison
skeletons = []
for sent in sentences:
words = sent.lower().split()[:8]
skel = tuple("F" if w in FUNCTION_WORDS else "C" for w in words)
skeletons.append(skel)
skel_counts = Counter(skeletons)
top_skel = skel_counts.most_common(1)[0][1] / len(sentences)
findings = []
if top_freq > 0.45:
findings.append(
Finding(
"statistical",
"opener_repetition",
1,
f"Repetitive sentence openers ({top_freq:.0%} share same opening)",
"warning",
)
)
if top_skel > 0.4:
findings.append(
Finding(
"statistical",
"structure_repetition",
1,
f"Repetitive sentence structures ({top_skel:.0%} share same skeleton)",
"warning",
)
)
return findings
# ─── Statistical: N-gram Repetition ───────────────────────────────────
def _analyze_ngram_repetition(self, text: str) -> List[Finding]:
words = re.findall(r"\b[a-z']+\b", text.lower())
if len(words) < 50:
return []
ngram_counts: Counter = Counter()
for i in range(len(words) - 3):
gram = tuple(words[i : i + 4])
content_words = sum(1 for w in gram if w not in FUNCTION_WORDS)
if content_words < 2:
continue
ngram_counts[gram] += 1
repeated = [(g, c) for g, c in ngram_counts.items() if c >= 3]
if not repeated:
return []
repeated.sort(key=lambda x: x[1], reverse=True)
top_gram, top_count = repeated[0]
score = 3 if top_count >= 4 else 2
return [
Finding(
"statistical",
"ngram_repetition",
score,
f"Repeated 4-word phrase '{' '.join(top_gram)}' x{top_count}",
"critical" if score >= 3 else "warning",
)
]
# ─── Statistical: Clinical Formality ──────────────────────────────────
def _analyze_clinical_formality(
self, text: str, technical_context: bool = False
) -> List[Finding]:
count = 0
instances = []
for regex, alt in self._compiled_clinical:
if technical_context and "implement" in regex.pattern:
continue
matches = regex.findall(text)
if matches:
count += len(matches)
instances.append(f"{matches[0]} → '{alt}'")
findings = []
if count >= 5:
findings.append(
Finding(
"statistical",
"clinical_formality",
4,
f"Clinical formality ({count} instances): {', '.join(instances[:5])}",
"critical",
)
)
elif count >= 3:
findings.append(
Finding(
"statistical",
"clinical_formality",
2,
f"Formal register ({count} instances): {', '.join(instances[:3])}",
"warning",
)
)
return findings
# ─── Statistical: Intensifier Overuse ─────────────────────────────────
def _analyze_intensifier_overuse(self, text: str) -> List[Finding]:
words = re.findall(r"\b\w+\b", text.lower())
total = len(words)
if total < 50:
return []
count = sum(1 for w in words if w in AI_INTENSIFIERS)
findings = []
if count >= 4 or (total > 0 and count / total > 0.02):
findings.append(
Finding(
"statistical",
"intensifier_overuse",
2,
f"AI-overused intensifiers ({count} in {total} words)",
"warning",
)
)
return findings
# ─── 30-Second Test ───────────────────────────────────────────────────
def _thirty_second_test(self, text: str, lengths: List[int]) -> List[Finding]:
lower = text.lower()
generic_openers = [
"in today's",
"in the modern",
"important to note",
"key takeaway",
"at the end of the day",
"in a world where",
"when it comes to",
"the bottom line is",
]
has_generic = any(g in lower for g in generic_openers)
first_person_count = len(re.findall(r"\b(?:i|we)\b", lower))
uniform_length = False
if len(lengths) > 5:
in_range = sum(1 for l in lengths if 8 < l < 16)
uniform_length = (in_range / len(lengths)) > 0.7
findings = []
if has_generic or (uniform_length and first_person_count < 3):
findings.append(
Finding(
"tier1",
"30sec_test",
5,
"Failed 30-second test (too generic / uniform)",
"critical",
)
)
return findings
# ─── Compute Metrics ──────────────────────────────────────────────────
def _compute_metrics(self, text: str, lengths: List[int]) -> Dict[str, Any]:
metrics: Dict[str, Any] = {}
# Lexical diversity
words = re.findall(r"\b[a-z]+\b", text.lower())
if len(words) >= 50:
window = 50
ttrs = [
len(set(words[i : i + window])) / window
for i in range(len(words) - window + 1)
]
metrics["lexical_diversity"] = round(statistics.mean(ttrs), 3)
elif words:
metrics["lexical_diversity"] = round(len(set(words)) / len(words), 3)
else:
metrics["lexical_diversity"] = 0
# Sentence length SD
if len(lengths) > 1:
metrics["sentence_length_sd"] = round(statistics.stdev(lengths), 1)
else:
metrics["sentence_length_sd"] = 0
# Burstiness
if len(lengths) >= 8:
deltas = [abs(lengths[i + 1] - lengths[i]) for i in range(len(lengths) - 1)]
mean_d = statistics.mean(deltas)
if mean_d > 0.5 and len(deltas) > 1:
metrics["burstiness_ratio"] = round(
statistics.stdev(deltas) / mean_d, 3
)
else:
metrics["burstiness_ratio"] = 0
else:
metrics["burstiness_ratio"] = None
# Hedging ratio
if len(words) >= 50:
h_count = sum(1 for w in words if w in HEDGE_WORDS)
for ph in HEDGE_PHRASES:
h_count += text.lower().count(ph)
metrics["hedging_ratio"] = round(h_count / len(words), 4)
else:
metrics["hedging_ratio"] = None
# Contraction ratio
c = len(CONTRACTIONS_RE.findall(text))
e = len(EXPANDABLE_RE.findall(text))
if c + e >= 3:
metrics["contraction_ratio"] = round(c / (c + e), 3)
else:
metrics["contraction_ratio"] = None
return metrics
# ═════════════════════════════════════════════════════════════════════
# MAIN API: detect()
# ═════════════════════════════════════════════════════════════════════
def detect(self, text: str, strict: bool = False) -> Dict[str, Any]:
"""Full slop detection with phrase, structural, and statistical analysis."""
text = self._normalize_text(text)
if not text or len(text) < 10:
return {
"slop_score": 0,
"raw_slop_score": 0,
"score_mode": "raw" if strict else "normalized",
"risk": "None",
"confidence": "none",
"signal_diversity": 0,
"findings": {},
"30sec_test": "N/A",
"metrics": {},
"sentence_variance": 0,
"score_per_100_words": 0.0,
"word_count": 0,
}
word_count = len(re.findall(r"\b\w+\b", text))
technical_context = self._is_technical_context(text, word_count)
all_findings: List[Finding] = []
# Phase 1: Phrase scanning
all_findings.extend(self._scan_tier1(text))
all_findings.extend(self._scan_tier2(text))
all_findings.extend(self._scan_tier3_clusters(text, technical_context))
# Phase 2: Structural analysis
all_findings.extend(self._detect_staccato(text))
all_findings.extend(self._detect_binary_contrasts(text))
all_findings.extend(self._detect_manufactured_personality(text))
all_findings.extend(self._detect_flattery_sandwiches(text))
all_findings.extend(self._detect_list_addiction(text))
all_findings.extend(self._detect_emoji_headers(text))
all_findings.extend(self._detect_section_balance(text))
# Phase 3: Statistical analysis
all_findings.extend(self._analyze_sentence_uniformity(text))
all_findings.extend(self._analyze_burstiness(text))
all_findings.extend(self._analyze_lexical_diversity(text))
all_findings.extend(self._analyze_hedging(text))
all_findings.extend(self._analyze_transitions(text))
all_findings.extend(self._analyze_contractions(text))
all_findings.extend(self._analyze_paragraph_structure(text))
all_findings.extend(self._analyze_repetition_patterns(text))
all_findings.extend(self._analyze_ngram_repetition(text))
all_findings.extend(self._analyze_clinical_formality(text, technical_context))
all_findings.extend(self._analyze_intensifier_overuse(text))
# Phase 4: 30-second test
lengths = self._sentence_lengths(text)
all_findings.extend(self._thirty_second_test(text, lengths))
# Phase 5: Scoring
raw_score = sum(f.score for f in all_findings)
score = (
raw_score
if strict
else self._normalize_score_by_length(raw_score, word_count)
)
categories = set(f.subcategory for f in all_findings if f.score > 0)
risk = "Low" if score <= 5 else "Medium" if score <= 12 else "High"
if len(categories) >= 4:
confidence = "high"
elif len(categories) >= 2:
confidence = "moderate"
elif len(categories) >= 1:
confidence = "low"
else:
confidence = "none"
# Group findings for output
grouped: Dict[str, List[str]] = defaultdict(list)
for f in all_findings:
grouped[f.category].append(f.detail)
return {
"slop_score": score,
"raw_slop_score": raw_score,
"score_mode": "raw" if strict else "normalized",
"risk": risk,
"confidence": confidence,
"signal_diversity": len(categories),
"findings": dict(grouped),
"30sec_test": "FAIL"
if any(f.subcategory == "30sec_test" for f in all_findings)
else "PASS",
"metrics": self._compute_metrics(text, lengths),
"sentence_variance": round(statistics.pvariance(lengths), 1)
if lengths
else 0,
"score_per_100_words": round((raw_score / word_count) * 100, 2)
if word_count
else 0.0,
"word_count": word_count,
}
# ═════════════════════════════════════════════════════════════════════
# HUMANIZE: Detect → Clean → Report
# ═════════════════════════════════════════════════════════════════════
def _merge_staccato(self, text: str) -> str:
"""Combine 3+ consecutive short sentences into flowing prose."""
parts = re.split(r"([.!?]\s*)", text)
result = []
buffer = []
for part in parts:
clean = part.strip()
if clean and len(clean.split()) <= 10 and clean[0:1].isupper():
buffer.append(clean)
else:
if len(buffer) >= 3:
merged = buffer[0] + " — " + ", ".join(buffer[1:]) + "."
result.append(merged)
elif buffer:
result.append(" ".join(buffer) + ".")
if clean:
result.append(part.strip())
buffer = []
if len(buffer) >= 3:
result.append(buffer[0] + " — " + ", ".join(buffer[1:]) + ".")
elif buffer:
result.append(" ".join(buffer) + ".")
return " ".join([p for p in result if p]).strip()
def humanize(
self, text: str, max_length: int = 280, strict: bool = False
) -> Dict[str, Any]:
"""Detect → clean → return report + cleaned text."""
text = self._normalize_text(text)
original_report = self.detect(text, strict=strict)
cleaned = text
# Apply smart replacements
for regex, repl in self.replace_rules:
cleaned = regex.sub(repl, cleaned)
# Fix staccato
cleaned = self._merge_staccato(cleaned)
# Cleanup passes — run multiple times to handle cascading removals
for _ in range(3):
cleaned = re.sub(r"[,;:]\s*[,;:]", ",", cleaned)
cleaned = re.sub(r"\b(?:to|and|or|but|for)\s*\.", ".", cleaned)
cleaned = re.sub(r"\s+\.", ".", cleaned)
cleaned = re.sub(r"\.{2,}", ".", cleaned)
cleaned = re.sub(r"\.\s*,", ".", cleaned)
cleaned = re.sub(r",\s*\.", ".", cleaned)
cleaned = re.sub(r"^[,;:\s\.]+", "", cleaned)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
# Capitalize sentence starts
cleaned = re.sub(
r"([.!?]\s+)([a-z])", lambda m: m.group(1) + m.group(2).upper(), cleaned
)
if cleaned:
cleaned = cleaned[0].upper() + cleaned[1:]
# Length enforcer
if len(cleaned) > max_length:
last_punct = max(
cleaned[:max_length].rfind("."), cleaned[:max_length].rfind("!"), 0
)
cleaned = (
cleaned[: last_punct + 1].strip()
if last_punct > 20
else cleaned[: max_length - 3] + "..."
)
after_report = self.detect(cleaned, strict=strict)
return {
"cleaned_text": cleaned if len(cleaned) >= 5 else None,
"original_slop_score": original_report["slop_score"],
"after_slop_score": after_report["slop_score"],
"original_raw_slop_score": original_report["raw_slop_score"],
"after_raw_slop_score": after_report["raw_slop_score"],
"score_mode": after_report["score_mode"],
"risk": after_report["risk"],
"improvement": original_report["slop_score"] - after_report["slop_score"],
"raw_improvement": original_report["raw_slop_score"]
- after_report["raw_slop_score"],
"findings": original_report["findings"],
"after_findings": after_report["findings"],
}
def _rewrite_with_openrouter(
self, text: str, max_length: Optional[int] = 280
) -> str:
if max_length and max_length > 0:
length_rule = f"Keep the rewritten output under {max_length} characters."
else:
length_rule = (
"Keep the rewritten output roughly the same length as the input."
)
messages = [
{
"role": "system",
"content": (
"You rewrite content to sound natural and human. Preserve facts, intent, and"
" technical details. Remove AI-style filler, cliches, transition spam, and"
" manufactured enthusiasm. Do not add new claims. Return only rewritten text."
),
},
{
"role": "user",
"content": f"{length_rule}\n\nRewrite this:\n{text}",
},
]
return self._openrouter_chat(messages, temperature=0.2, max_tokens=900)
def rewrite_if_slop(
self,
text: str,
max_length: int = 280,
score_threshold: int = 5,
strict: bool = False,
max_rewrites: int = 2,
prefer_openrouter: bool = True,
) -> Dict[str, Any]:
"""Rewrite text until slop_score is under threshold or attempts are exhausted."""
current_text = self._normalize_text(text)
if not current_text:
return {
"final_text": "",
"rewritten": False,
"passes": 0,
"method": "none",
"original_report": self.detect("", strict=strict),
"final_report": self.detect("", strict=strict),
"attempts": [],
}
original_report = self.detect(current_text, strict=strict)
current_report = original_report
attempts: List[Dict[str, Any]] = []
for i in range(max_rewrites):
if current_report["slop_score"] <= score_threshold:
break
method = "local_humanize"
candidate = None
if prefer_openrouter and self.openrouter_config.get("enabled"):
method = "openrouter"
try:
candidate = self._rewrite_with_openrouter(current_text, max_length)
except RuntimeError as exc:
method = "local_humanize_fallback"
candidate = self.humanize(
current_text, max_length=max_length, strict=strict
).get("cleaned_text")
attempts.append(
{
"pass": i + 1,
"method": "openrouter_error",
"error": str(exc),
}
)
else:
candidate = self.humanize(
current_text, max_length=max_length, strict=strict
).get("cleaned_text")
candidate = self._normalize_text(candidate or "")
if not candidate or candidate == current_text:
attempts.append(
{
"pass": i + 1,
"method": method,
"from_score": current_report["slop_score"],
"to_score": current_report["slop_score"],
"changed": False,
}
)
break
next_report = self.detect(candidate, strict=strict)
attempts.append(
{
"pass": i + 1,
"method": method,
"from_score": current_report["slop_score"],
"to_score": next_report["slop_score"],
"changed": True,
}
)
current_text = candidate
current_report = next_report
return {
"final_text": current_text,
"rewritten": current_text != text,
"passes": len([a for a in attempts if a.get("changed")]),
"method": attempts[-1]["method"] if attempts else "none",
"original_report": original_report,
"final_report": current_report,
"attempts": attempts,
}
def generate_with_guardrail(
self,
prompt: str,
max_length: int = 280,
score_threshold: int = 5,
strict: bool = False,
max_rewrites: int = 2,
) -> Dict[str, Any]:
"""Generate with OpenRouter, then auto-rewrite if output is detected as slop."""
draft = self._openrouter_chat(
[
{
"role": "system",
"content": "Answer clearly and directly. Return only the requested content.",
},
{"role": "user", "content": prompt},
],
temperature=0.4,
max_tokens=900,
)
guarded = self.rewrite_if_slop(
draft,
max_length=max_length,
score_threshold=score_threshold,
strict=strict,
max_rewrites=max_rewrites,
prefer_openrouter=True,
)
return {
"prompt": prompt,
"draft_text": draft,
"final_text": guarded["final_text"],
"rewritten": guarded["rewritten"],
"passes": guarded["passes"],
"method": guarded["method"],
"draft_report": guarded["original_report"],
"final_report": guarded["final_report"],
"attempts": guarded["attempts"],
}
# ═════════════════════════════════════════════════════════════════════════════
# CLI
# ═════════════════════════════════════════════════════════════════════════════
def _print_detection_report(report: Dict[str, Any]) -> None:
print(
f"Score: {report['slop_score']} ({report['score_mode']}) | "
f"Raw: {report['raw_slop_score']} | Risk: {report['risk']} | "
f"Confidence: {report['confidence']} | Signals: {report['signal_diversity']}"
)
print(
f"30-sec test: {report['30sec_test']} | "
f"Words: {report.get('word_count', 0)} | "
f"Density: {report.get('score_per_100_words', 0.0)}/100w"
)
metrics = report.get("metrics", {})
if metrics:
parts = []
if metrics.get("lexical_diversity") is not None:
parts.append(f"TTR={metrics['lexical_diversity']}")
if metrics.get("sentence_length_sd") is not None:
parts.append(f"SD={metrics['sentence_length_sd']}")
if metrics.get("burstiness_ratio") is not None:
parts.append(f"Burst={metrics['burstiness_ratio']}")
if metrics.get("hedging_ratio") is not None:
parts.append(f"Hedge={metrics['hedging_ratio']}")
if metrics.get("contraction_ratio") is not None:
parts.append(f"Contract={metrics['contraction_ratio']}")
if parts:
print(f"Metrics: {', '.join(parts)}")
for cat, items in report.get("findings", {}).items():
print(f"[{cat}]")
for item in items[:5]:
print(f" - {item}")
def _run_self_tests(cleaner: AISlopCleaner) -> None:
ai_heavy = (
"In today's digital landscape, it's worth noting that AI has become a game-changer "
"for content creation. Let's delve into how you can leverage these cutting-edge solutions "
"to unlock your potential. Moreover, by implementing these strategies, you'll discover that "
"the results are truly revolutionary. The answer surprised me. Here's what blew my mind. "
"This is a game-changing approach that fundamentally transforms how individuals utilize "
"technology in order to facilitate better outcomes."
)
ai_subtle = (
"The model is impressive. Complex code ships fast. Documentation writes itself. "
"Problems get solved quickly. Performance is remarkable. Integration works smoothly. "
"The results are consistent. The feedback is immediate. The workflow is efficient. "
"Testing is streamlined. Deployment is automated. Monitoring is comprehensive."
)
ai_persona = (
"I run five different media management services on my home server. Five different web "
"interfaces. Five different API endpoints. Five different browser tabs open whenever I "
"want to check what's downloading.\n\n"
"That got old fast.\n\n"
"So I built a tool that unifies all of them. It's not about simplicity. It's about "
"sanity. The loop is tight. The feedback is immediate. The reward is reliable."
)
human = (
"Three months ago, I switched from Notion to Obsidian. Not because Obsidian is "
"better-it isn't, really-but because I needed to own my files. After losing access "
"to a client's Notion workspace, I spent six hours recreating documentation I'd "
"written. Never again."
)
medium = (
"Here's the thing about productivity tools: they only work if you use them "
"consistently. The best system is one you'll actually stick with. I've tried "
"dozens of apps and the only one that stuck was a plain text file. It's worth "
"noting that most people give up after a week."
)
samples = [
("HEAVY AI SLOP", ai_heavy),
("SUBTLE AI (structural)", ai_subtle),
("MANUFACTURED PERSONA", ai_persona),
("HUMAN WRITING", human),
("MEDIUM SLOP", medium),
]
for label, text in samples:
result = cleaner.detect(text)
print(f"\n{'=' * 65}")
print(f"{label}")
print(f"{'=' * 65}")
_print_detection_report(result)
print(f"\n{'=' * 65}")
print("HUMANIZE TEST")
print(f"{'=' * 65}")
result = cleaner.humanize(ai_heavy, max_length=280)
print(f"Cleaned: {result['cleaned_text']}")
print(
f"Score: {result['original_slop_score']} -> {result['after_slop_score']} "
f"(improvement: {result['improvement']})"
)
def _read_input_text(text_arg: Optional[str], file_arg: Optional[str]) -> str:
if text_arg is not None:
return text_arg
if file_arg is not None:
with open(file_arg, "r", encoding="utf-8") as f:
return f.read()
if not sys.stdin.isatty():
return sys.stdin.read()
return ""
def _build_benchmark_samples(seed: int, size: str) -> List[Tuple[str, int, str]]:
rng = random.Random(seed)
per_class = {
"tiny": 40,
"small": 120,
"medium": 300,
"large": 600,
}.get(size, 120)
transitions = [
"However",
"Moreover",
"Furthermore",
"Additionally",
"Consequently",
"Meanwhile",
"Ultimately",
]
heavy_phrases = [
"In today's digital landscape",
"it's worth noting that",
"game-changing",
"cutting-edge",
"the real unlock",
"let's delve into",
"unlock your potential",
"This matters because",
"Here's the thing",
]
tech_terms = [
"API",
"OAuth",
"JWT",
"Redis",
"Postgres",
"pipeline",
"deployment",
"latency",
"throughput",
"gRPC",
"endpoint",
"schema",
"microservices",
"runtime",
]
nouns = [
"project",
"feature",
"bug",
"team",
"release",
"meeting",
"client",
"note",
"draft",
"ticket",
"workflow",
]
verbs = [
"fixed",
"reviewed",
"rewrote",
"debugged",
"planned",
"tested",
"shipped",
"documented",
"refined",
"deployed",
]
def ai_heavy() -> str:
picks = rng.sample(heavy_phrases, rng.randint(4, 7))
return " ".join(
f"{p} this approach helps teams move faster and see better outcomes."
for p in picks
)
def ai_structural() -> str:
base = [
"the model delivers stable performance for teams",
"the workflow remains efficient under pressure",
"the process produces consistent outcomes each sprint",
"the approach scales cleanly across environments",
"the system keeps feedback loops short and predictable",
"the rollout stays organized and measurable",
]
rng.shuffle(base)
return " ".join(
f"{transitions[i % len(transitions)]}, {sentence}."
for i, sentence in enumerate(base)
)
def ai_humanlike() -> str:
variants = [
"I tried three rollout plans this month and kept the one with fewer moving parts.",
"The first version looked fine, but the logs showed retries stacking up after midnight.",
"I swapped queue settings, reran load tests, and kept the slower but safer profile.",
"By Friday, the incident channel was quiet and nobody had to babysit deploys.",
"It wasn't dramatic, just a boring fix that made pager duty less painful.",
]
rng.shuffle(variants)
return " ".join(variants[:4])
def human_story() -> str:
endings = [
"and it finally made sense",
"after two failed tries",
"because the old setup kept breaking",
"so we could close it before lunch",
"even though I almost gave up",
]
lines = []
for _ in range(rng.randint(4, 7)):
lines.append(
f"I {rng.choice(verbs)} the {rng.choice(nouns)} {rng.choice(endings)}."
)
return " ".join(lines)
def human_technical() -> str:
picks = rng.sample(tech_terms, 7)
return (
f"We moved auth to {picks[0]} with {picks[1]} rotation and {picks[2]} checks. "
f"A {picks[3]} cache reduced p95 latency, and the {picks[4]} migration stabilized writes. "
f"Our {picks[5]} now handles retries cleanly across a stable {picks[6]}."
)
def human_repetitive() -> str:
thing = rng.choice(["pipeline", "service", "release", "worker"])
return (
f"Yesterday, I checked the {thing} and wrote notes for the team. "
f"After lunch, I checked the {thing} and wrote notes for the team. "
f"By evening, I checked the {thing} and wrote notes for the team. "
f"Later, I checked the {thing} and wrote notes for the team."
)
classes = [
("ai_heavy", 1, ai_heavy),
("ai_structural", 1, ai_structural),
("ai_humanlike", 1, ai_humanlike),
("human_story", 0, human_story),
("human_technical", 0, human_technical),
("human_repetitive", 0, human_repetitive),
]
samples: List[Tuple[str, int, str]] = []
for name, label, fn in classes:
for _ in range(per_class):
samples.append((name, label, fn()))
rng.shuffle(samples)
return samples
def _binary_metrics(
scores: List[int], labels: List[int], threshold: int
) -> Dict[str, Any]:
tp = fp = tn = fn = 0
for score, label in zip(scores, labels):
pred = 1 if score >= threshold else 0
if pred == 1 and label == 1:
tp += 1
elif pred == 1 and label == 0:
fp += 1
elif pred == 0 and label == 0:
tn += 1
else:
fn += 1
precision = tp / (tp + fp) if (tp + fp) else 0.0
recall = tp / (tp + fn) if (tp + fn) else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
accuracy = (tp + tn) / (tp + tn + fp + fn) if (tp + tn + fp + fn) else 0.0
return {
"tp": tp,
"fp": fp,
"tn": tn,
"fn": fn,
"accuracy": round(accuracy, 4),
"precision": round(precision, 4),
"recall": round(recall, 4),
"f1": round(f1, 4),
}
def _run_benchmark(
cleaner: AISlopCleaner,
strict: bool,
threshold: int,
size: str,
seed: int,
rewrite_samples: int,
) -> Dict[str, Any]:
samples = _build_benchmark_samples(seed=seed, size=size)
labels = [label for _, label, _ in samples]
start = time.perf_counter()
scores: List[int] = []
by_class_scores: Dict[str, List[int]] = defaultdict(list)
by_class_labels: Dict[str, int] = {}
for class_name, label, text in samples:
report = cleaner.detect(text, strict=strict)
score = report["slop_score"]
scores.append(score)
by_class_scores[class_name].append(score)
by_class_labels[class_name] = label
elapsed = time.perf_counter() - start
default_metrics = _binary_metrics(scores, labels, threshold)
best_threshold = threshold
best_metrics = default_metrics
for candidate in range(1, 21):
candidate_metrics = _binary_metrics(scores, labels, candidate)
rank = (
candidate_metrics["f1"],
candidate_metrics["accuracy"],
candidate_metrics["precision"],
-candidate,
)
best_rank = (
best_metrics["f1"],
best_metrics["accuracy"],
best_metrics["precision"],
-best_threshold,
)
if rank > best_rank:
best_threshold = candidate
best_metrics = candidate_metrics
per_class: Dict[str, Dict[str, Any]] = {}
for class_name, class_scores in by_class_scores.items():
class_label = by_class_labels[class_name]
class_ai_rate = sum(1 for s in class_scores if s >= threshold) / len(
class_scores
)
per_class[class_name] = {
"label": class_label,
"n": len(class_scores),
"mean_score": round(statistics.mean(class_scores), 2),
"ai_rate_at_threshold": round(class_ai_rate, 3),
}
rng = random.Random(seed + 1)
rewrite_n = min(rewrite_samples, len(samples))
rewrite_indices = rng.sample(range(len(samples)), rewrite_n)
rewrite_stats: Dict[str, Any] = {
"samples": rewrite_n,
"rewritten": 0,
"improved": 0,
"worse": 0,
"unchanged": 0,
"flagged_ai": 0,
"ai_fixed": 0,
"human_rewritten": 0,
}
for idx in rewrite_indices:
_, label, text = samples[idx]
before_score = scores[idx]
if label == 1 and before_score >= threshold:
rewrite_stats["flagged_ai"] += 1
result = cleaner.rewrite_if_slop(
text,
max_length=600,
score_threshold=threshold,
strict=strict,
max_rewrites=1,
prefer_openrouter=False,
)
after_score = result["final_report"]["slop_score"]
if result["rewritten"]:
rewrite_stats["rewritten"] += 1
if label == 0:
rewrite_stats["human_rewritten"] += 1
if after_score < before_score:
rewrite_stats["improved"] += 1
elif after_score > before_score:
rewrite_stats["worse"] += 1
else:
rewrite_stats["unchanged"] += 1
if label == 1 and before_score >= threshold and after_score < threshold:
rewrite_stats["ai_fixed"] += 1
if rewrite_stats["flagged_ai"] > 0:
rewrite_stats["ai_fix_rate"] = round(
rewrite_stats["ai_fixed"] / rewrite_stats["flagged_ai"], 3
)
else:
rewrite_stats["ai_fix_rate"] = None
return {
"config": {
"score_mode": "raw" if strict else "normalized",
"threshold": threshold,
"size": size,
"seed": seed,
"rewrite_samples": rewrite_n,
},
"runtime": {
"samples": len(samples),
"seconds": round(elapsed, 3),
"ms_per_sample": round((elapsed / len(samples)) * 1000, 3),
},
"default_threshold_metrics": default_metrics,
"best_threshold": best_threshold,
"best_threshold_metrics": best_metrics,
"per_class": per_class,
"rewrite": rewrite_stats,
}
def _print_benchmark_report(report: Dict[str, Any]) -> None:
cfg = report["config"]
rt = report["runtime"]
default = report["default_threshold_metrics"]
best = report["best_threshold_metrics"]
print(
f"Benchmark | mode={cfg['score_mode']} | size={cfg['size']} | "
f"threshold={cfg['threshold']} | samples={rt['samples']}"
)
print(
f"Speed: {rt['seconds']}s total | {rt['ms_per_sample']} ms/sample | seed={cfg['seed']}"
)
print(
"Default metrics: "
f"acc={default['accuracy']} prec={default['precision']} "
f"rec={default['recall']} f1={default['f1']} "
f"(tp={default['tp']} fp={default['fp']} tn={default['tn']} fn={default['fn']})"
)
print(
"Best threshold: "
f"{report['best_threshold']} | acc={best['accuracy']} prec={best['precision']} "
f"rec={best['recall']} f1={best['f1']}"
)
print("Per-class scores (mean score, ai-rate at default threshold):")
for class_name in sorted(report["per_class"].keys()):
info = report["per_class"][class_name]
label_name = "AI" if info["label"] == 1 else "Human"
print(
f"- {class_name}: label={label_name} n={info['n']} "
f"mean={info['mean_score']} ai_rate={info['ai_rate_at_threshold']}"
)
rw = report["rewrite"]
fix_rate = "n/a" if rw["ai_fix_rate"] is None else rw["ai_fix_rate"]
print(
"Rewrite probe: "
f"samples={rw['samples']} rewritten={rw['rewritten']} improved={rw['improved']} "
f"worse={rw['worse']} unchanged={rw['unchanged']} "
f"ai_fixed={rw['ai_fixed']}/{rw['flagged_ai']} ({fix_rate}) "
f"human_rewritten={rw['human_rewritten']}"
)
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="AI Slop Detector and Humanizer (single-file, stdlib only)."
)
parser.add_argument(
"--mode",
choices=["detect", "humanize", "guardrail", "generate", "benchmark"],
default="detect",
help="Operation mode.",
)
parser.add_argument("--text", help="Input text literal.")
parser.add_argument("--file", help="Read input text from file.")
parser.add_argument("--prompt", help="Prompt for generate mode.")
parser.add_argument("--max-length", type=int, default=280)
parser.add_argument("--threshold", type=int, default=5)
parser.add_argument("--max-rewrites", type=int, default=2)
parser.add_argument(
"--benchmark-size",
choices=["tiny", "small", "medium", "large"],
default="small",
help="Synthetic benchmark corpus size.",
)
parser.add_argument(
"--benchmark-seed",
type=int,
default=42,
help="Random seed for synthetic benchmark generation.",
)
parser.add_argument(
"--benchmark-rewrite-samples",
type=int,
default=120,
help="How many benchmark samples to probe with rewrite_if_slop.",
)
parser.add_argument(
"--score-mode",
choices=["normalized", "raw"],
default="normalized",
help="Scoring mode: normalized (default) or raw.",
)
parser.add_argument(
"--strict",
action="store_true",
help=argparse.SUPPRESS,
)
parser.add_argument("--json", action="store_true", help="Print JSON output.")
parser.add_argument(
"--self-test", action="store_true", help="Run built-in sanity tests."
)
parser.add_argument(
"--no-openrouter",
action="store_true",
help="Disable OpenRouter even if configured.",
)
parser.add_argument("--openrouter-api-key", help="OpenRouter API key.")
parser.add_argument(
"--openrouter-model",
default="openai/gpt-4o-mini",
help="OpenRouter model identifier.",
)
parser.add_argument("--openrouter-timeout", type=int, default=45)
parser.add_argument(
"--openrouter-url",
default="https://openrouter.ai/api/v1/chat/completions",
)
parser.add_argument("--openrouter-app-name", default="AISlopCleaner")
parser.add_argument("--openrouter-app-url", default="https://localhost")
return parser
if __name__ == "__main__":
parser = _build_parser()
args = parser.parse_args()
cleaner = AISlopCleaner()
openrouter_enabled = cleaner.configure_openrouter(
api_key=args.openrouter_api_key,
model=args.openrouter_model,
timeout=args.openrouter_timeout,
app_name=args.openrouter_app_name,
app_url=args.openrouter_app_url,
url=args.openrouter_url,
)
if args.self_test:
_run_self_tests(cleaner)
sys.exit(0)
use_raw_mode = args.strict or args.score_mode == "raw"
if args.mode == "benchmark":
result = _run_benchmark(
cleaner=cleaner,
strict=use_raw_mode,
threshold=args.threshold,
size=args.benchmark_size,
seed=args.benchmark_seed,
rewrite_samples=max(0, args.benchmark_rewrite_samples),
)
if args.json:
print(json.dumps(result, indent=2))
else:
_print_benchmark_report(result)
sys.exit(0)
if args.mode == "generate":
prompt = args.prompt or _read_input_text(args.text, args.file).strip()
if not prompt:
parser.error(
"generate mode requires --prompt, --text, --file, or piped stdin"
)
if not openrouter_enabled:
parser.error(
"generate mode requires OpenRouter. Set OPENROUTER_API_KEY or --openrouter-api-key"
)
result = cleaner.generate_with_guardrail(
prompt,
max_length=args.max_length,
score_threshold=args.threshold,
strict=use_raw_mode,
max_rewrites=args.max_rewrites,
)
if args.json:
print(json.dumps(result, indent=2))
else:
print(result["final_text"])
print(
f"\nDraft score: {result['draft_report']['slop_score']} | "
f"Final score: {result['final_report']['slop_score']} | "
f"Rewritten: {result['rewritten']}"
)
sys.exit(0)
input_text = _read_input_text(args.text, args.file)
if not input_text.strip():
parser.error("input text is required (use --text, --file, or piped stdin)")
if args.mode == "detect":
result = cleaner.detect(input_text, strict=use_raw_mode)
if args.json:
print(json.dumps(result, indent=2))
else:
_print_detection_report(result)
elif args.mode == "humanize":
result = cleaner.humanize(
input_text, max_length=args.max_length, strict=use_raw_mode
)
if args.json:
print(json.dumps(result, indent=2))
else:
print(result.get("cleaned_text") or "")
print(
f"\nScore: {result['original_slop_score']} -> {result['after_slop_score']} "
f"({result['score_mode']}, improvement {result['improvement']})"
)
elif args.mode == "guardrail":
result = cleaner.rewrite_if_slop(
input_text,
max_length=args.max_length,
score_threshold=args.threshold,
strict=use_raw_mode,
max_rewrites=args.max_rewrites,
prefer_openrouter=(not args.no_openrouter),
)
if args.json:
print(json.dumps(result, indent=2))
else:
print(result["final_text"])
print(
f"\nOriginal score: {result['original_report']['slop_score']} | "
f"Final score: {result['final_report']['slop_score']} | "
f"Rewritten: {result['rewritten']} | Passes: {result['passes']} | "
f"Method: {result['method']}"
)
else:
parser.error(f"unsupported mode: {args.mode}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment