Spaces:
Running
Running
AJAY KASU commited on
Commit ·
d1abcca
1
Parent(s): 1bb9a73
feat: complete 3-agent AI humanizer pipeline
Browse files- Agent 1: SemanticAnalyzer (Mistral-7B for context extraction)
- Agent 2: DraftGenerator (Mistral-7B for natural rewrite)
- Agent 3: Humanizer (Zephyr-7B with high temperature for human patterns)
- Verifier: roberta-base-openai-detector with 3x retry loop
- CLI orchestrator (main.py) and Gradio web UI (app.py)
- Post-processing: contractions, fillers, hedging, punctuation tricks
- .gitignore +20 -0
- README.md +31 -3
- agents/__init__.py +14 -0
- agents/draft_generator.py +117 -0
- agents/humanizer.py +241 -0
- agents/semantic_analyzer.py +166 -0
- agents/verifier.py +141 -0
- app.py +144 -0
- main.py +171 -0
- requirements.txt +9 -0
.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.egg-info/
|
| 5 |
+
dist/
|
| 6 |
+
build/
|
| 7 |
+
.eggs/
|
| 8 |
+
*.egg
|
| 9 |
+
.env
|
| 10 |
+
.venv
|
| 11 |
+
env/
|
| 12 |
+
venv/
|
| 13 |
+
*.so
|
| 14 |
+
.cache/
|
| 15 |
+
models_cache/
|
| 16 |
+
*.pt
|
| 17 |
+
*.bin
|
| 18 |
+
*.safetensors
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.log
|
README.md
CHANGED
|
@@ -1,12 +1,40 @@
|
|
| 1 |
---
|
| 2 |
title: AI Humanizer
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: AI Humanizer
|
| 3 |
+
emoji: "\U0001F9E0"
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.12.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# AI Text Humanizer
|
| 13 |
+
|
| 14 |
+
**3-Agent Sequential Pipeline** for transforming AI-generated text into
|
| 15 |
+
natural, human-sounding writing.
|
| 16 |
+
|
| 17 |
+
## Architecture
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
Input Text -> [Semantic Analyzer] -> [Draft Generator] -> [Humanizer] -> [Verifier] -> Output
|
| 21 |
+
^ |
|
| 22 |
+
| (loop if AI) |
|
| 23 |
+
+----------------+
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Models Used (all free HF Inference)
|
| 27 |
+
|
| 28 |
+
| Agent | Model |
|
| 29 |
+
|-------|-------|
|
| 30 |
+
| Semantic Analyzer | mistralai/Mistral-7B-Instruct-v0.3 |
|
| 31 |
+
| Draft Generator | mistralai/Mistral-7B-Instruct-v0.3 |
|
| 32 |
+
| Humanizer | HuggingFaceH4/zephyr-7b-beta |
|
| 33 |
+
| Verifier | roberta-base-openai-detector |
|
| 34 |
+
|
| 35 |
+
## How It Works
|
| 36 |
+
|
| 37 |
+
1. **Agent 1** extracts topic, tone, audience, and arguments (read-only analysis)
|
| 38 |
+
2. **Agent 2** rewrites text naturally while preserving 100% of facts
|
| 39 |
+
3. **Agent 3** injects human imperfections: contractions, fillers, hedging, varied sentence lengths
|
| 40 |
+
4. **Verifier** checks with AI detector -- loops back to Agent 3 if flagged (max 3x)
|
agents/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agents package
|
| 2 |
+
# sequential pipeline: analyze -> draft -> humanize -> verify
|
| 3 |
+
|
| 4 |
+
from .semantic_analyzer import SemanticAnalyzer
|
| 5 |
+
from .draft_generator import DraftGenerator
|
| 6 |
+
from .humanizer import Humanizer
|
| 7 |
+
from .verifier import Verifier
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"SemanticAnalyzer",
|
| 11 |
+
"DraftGenerator",
|
| 12 |
+
"Humanizer",
|
| 13 |
+
"Verifier",
|
| 14 |
+
]
|
agents/draft_generator.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent 2: Draft Generator
|
| 3 |
+
------------------------
|
| 4 |
+
Takes the structured context from Agent 1 and rewrites the
|
| 5 |
+
original text in natural language while keeping 100% of the
|
| 6 |
+
factual content intact.
|
| 7 |
+
|
| 8 |
+
Uses Mistral-7B-Instruct via HF Inference API.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from huggingface_hub import InferenceClient
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DraftGenerator:
|
| 20 |
+
"""Second link — produce a coherent, natural-sounding draft."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, hf_token=None):
|
| 23 |
+
self.token = hf_token or os.getenv("HF_TOKEN", "")
|
| 24 |
+
self.client = InferenceClient(token=self.token)
|
| 25 |
+
self.model = "mistralai/Mistral-7B-Instruct-v0.3"
|
| 26 |
+
|
| 27 |
+
# ------------------------------------------------------------------
|
| 28 |
+
# public api
|
| 29 |
+
# ------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
def generate(self, context: dict) -> str:
|
| 32 |
+
"""
|
| 33 |
+
Parameters
|
| 34 |
+
----------
|
| 35 |
+
context : dict
|
| 36 |
+
The output of SemanticAnalyzer.analyze() — must contain
|
| 37 |
+
'original_text' and 'analysis' keys.
|
| 38 |
+
|
| 39 |
+
Returns
|
| 40 |
+
-------
|
| 41 |
+
str — The rewritten draft text.
|
| 42 |
+
"""
|
| 43 |
+
original = context.get("original_text", "")
|
| 44 |
+
analysis = context.get("analysis", {})
|
| 45 |
+
tone = analysis.get("tone", "neutral")
|
| 46 |
+
audience = analysis.get("target_audience", "general audience")
|
| 47 |
+
topic = analysis.get("core_topic", "the given topic")
|
| 48 |
+
|
| 49 |
+
logger.info("draft generator: rewriting %d chars (tone=%s)", len(original), tone)
|
| 50 |
+
|
| 51 |
+
prompt = self._build_prompt(original, tone, audience, topic)
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
draft = self.client.text_generation(
|
| 55 |
+
prompt,
|
| 56 |
+
model=self.model,
|
| 57 |
+
max_new_tokens=1024,
|
| 58 |
+
temperature=0.6, # moderate creativity
|
| 59 |
+
top_p=0.9,
|
| 60 |
+
)
|
| 61 |
+
draft = self._cleanup(draft)
|
| 62 |
+
except Exception as exc:
|
| 63 |
+
logger.error("draft generation failed: %s — returning original", exc)
|
| 64 |
+
draft = original # safe fallback
|
| 65 |
+
|
| 66 |
+
return draft
|
| 67 |
+
|
| 68 |
+
# ------------------------------------------------------------------
|
| 69 |
+
# internals
|
| 70 |
+
# ------------------------------------------------------------------
|
| 71 |
+
|
| 72 |
+
def _build_prompt(self, text, tone, audience, topic):
|
| 73 |
+
return (
|
| 74 |
+
"[INST] You are a skilled writer. Rewrite the text below in clear, "
|
| 75 |
+
"natural language. Follow these rules strictly:\n\n"
|
| 76 |
+
"1. Preserve ALL factual content — do not add or remove information.\n"
|
| 77 |
+
"2. Keep the same overall structure and flow.\n"
|
| 78 |
+
f"3. Match the tone: {tone}\n"
|
| 79 |
+
f"4. Write for this audience: {audience}\n"
|
| 80 |
+
f"5. The core topic is: {topic}\n"
|
| 81 |
+
"6. Use natural phrasing but you can still sound polished at this stage.\n"
|
| 82 |
+
"7. Return ONLY the rewritten text, nothing else.\n\n"
|
| 83 |
+
f"Original text:\n\"{text}\"\n\n"
|
| 84 |
+
"Rewritten version: [/INST]"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _cleanup(raw: str) -> str:
|
| 89 |
+
"""Strip stray quotes, whitespace, markdown fences."""
|
| 90 |
+
text = raw.strip()
|
| 91 |
+
# remove markdown code fences if the model wrapped it
|
| 92 |
+
if text.startswith("```"):
|
| 93 |
+
lines = text.split("\n")
|
| 94 |
+
lines = [l for l in lines if not l.strip().startswith("```")]
|
| 95 |
+
text = "\n".join(lines).strip()
|
| 96 |
+
# strip surrounding quotes
|
| 97 |
+
if text.startswith('"') and text.endswith('"'):
|
| 98 |
+
text = text[1:-1]
|
| 99 |
+
return text
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# quick test
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
from semantic_analyzer import SemanticAnalyzer
|
| 105 |
+
|
| 106 |
+
sa = SemanticAnalyzer()
|
| 107 |
+
dg = DraftGenerator()
|
| 108 |
+
|
| 109 |
+
sample = (
|
| 110 |
+
"The rapid advancement of artificial intelligence presents both "
|
| 111 |
+
"opportunities and challenges for modern society. It is imperative "
|
| 112 |
+
"that we consider the ethical implications of these technologies."
|
| 113 |
+
)
|
| 114 |
+
ctx = sa.analyze(sample)
|
| 115 |
+
draft = dg.generate(ctx)
|
| 116 |
+
print("=== DRAFT ===")
|
| 117 |
+
print(draft)
|
agents/humanizer.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent 3: Humanizer
|
| 3 |
+
------------------
|
| 4 |
+
The final rewriting stage. Takes a polished draft and injects
|
| 5 |
+
authentic human writing patterns -- sentence-length variation,
|
| 6 |
+
contractions, fillers, hedging, slight digressions, punctuation
|
| 7 |
+
tricks, and intentional minor imperfections.
|
| 8 |
+
|
| 9 |
+
Uses Zephyr-7B-beta via HF Inference API with high temperature
|
| 10 |
+
(0.9) and top_p (0.95) for maximum unpredictability.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import re
|
| 15 |
+
import random
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
from huggingface_hub import InferenceClient
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# -- post-processing maps -------------------------------------------------
|
| 23 |
+
|
| 24 |
+
_CONTRACTIONS = {
|
| 25 |
+
"do not": "don't", "does not": "doesn't", "did not": "didn't",
|
| 26 |
+
"cannot": "can't", "can not": "can't", "will not": "won't",
|
| 27 |
+
"would not": "wouldn't", "should not": "shouldn't",
|
| 28 |
+
"could not": "couldn't", "is not": "isn't", "are not": "aren't",
|
| 29 |
+
"was not": "wasn't", "were not": "weren't", "have not": "haven't",
|
| 30 |
+
"has not": "hasn't", "had not": "hadn't", "it is": "it's",
|
| 31 |
+
"that is": "that's", "there is": "there's", "I am": "I'm",
|
| 32 |
+
"I have": "I've", "I will": "I'll", "I would": "I'd",
|
| 33 |
+
"we are": "we're", "they are": "they're", "you are": "you're",
|
| 34 |
+
"let us": "let's",
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
_FILLERS = [
|
| 38 |
+
"you know", "honestly", "I mean", "like", "honestly speaking",
|
| 39 |
+
"if you think about it", "right", "look", "basically",
|
| 40 |
+
]
|
| 41 |
+
_HEDGES = ["maybe", "probably", "I think", "kinda", "sort of", "arguably"]
|
| 42 |
+
_TRANSITIONS = ["so", "anyway", "but yeah", "plus", "on top of that", "and honestly"]
|
| 43 |
+
|
| 44 |
+
# Zephyr chat template tokens -- built at runtime to dodge XML-like parsers
|
| 45 |
+
_SYS_OPEN = "<" + "|system|" + ">"
|
| 46 |
+
_EOS = "<" + "/s" + ">"
|
| 47 |
+
_USR_OPEN = "<" + "|user|" + ">"
|
| 48 |
+
_ASST_OPEN = "<" + "|assistant|" + ">"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Humanizer:
|
| 52 |
+
"""Third link -- injects human-like imperfections into the draft."""
|
| 53 |
+
|
| 54 |
+
def __init__(self, hf_token=None):
|
| 55 |
+
self.token = hf_token or os.getenv("HF_TOKEN", "")
|
| 56 |
+
self.client = InferenceClient(token=self.token)
|
| 57 |
+
self.model = "HuggingFaceH4/zephyr-7b-beta"
|
| 58 |
+
|
| 59 |
+
# ------------------------------------------------------------------
|
| 60 |
+
# public api
|
| 61 |
+
# ------------------------------------------------------------------
|
| 62 |
+
|
| 63 |
+
def humanize(self, draft, intensity=0.9, feedback=""):
|
| 64 |
+
"""
|
| 65 |
+
Parameters
|
| 66 |
+
----------
|
| 67 |
+
draft : the polished text from Agent 2
|
| 68 |
+
intensity : 0.7-1.0 -- maps to temperature
|
| 69 |
+
feedback : optional verifier feedback for retry iterations
|
| 70 |
+
|
| 71 |
+
Returns
|
| 72 |
+
-------
|
| 73 |
+
str -- humanized text
|
| 74 |
+
"""
|
| 75 |
+
logger.info(
|
| 76 |
+
"humanizer: processing %d chars (intensity=%.2f, has_feedback=%s)",
|
| 77 |
+
len(draft), intensity, bool(feedback),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
prompt = self._build_prompt(draft, feedback)
|
| 81 |
+
temperature = max(0.7, min(intensity, 1.0))
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
raw = self.client.text_generation(
|
| 85 |
+
prompt,
|
| 86 |
+
model=self.model,
|
| 87 |
+
max_new_tokens=1024,
|
| 88 |
+
temperature=temperature,
|
| 89 |
+
top_p=0.95,
|
| 90 |
+
)
|
| 91 |
+
text = self._cleanup(raw)
|
| 92 |
+
except Exception as exc:
|
| 93 |
+
logger.error("humanizer LLM call failed: %s -- applying offline transforms", exc)
|
| 94 |
+
text = draft
|
| 95 |
+
|
| 96 |
+
# always layer on rule-based post-processing too,
|
| 97 |
+
# the LLM sometimes chickens out on imperfections
|
| 98 |
+
text = self._apply_contractions(text)
|
| 99 |
+
text = self._inject_fillers(text, intensity)
|
| 100 |
+
text = self._vary_punctuation(text)
|
| 101 |
+
|
| 102 |
+
return text
|
| 103 |
+
|
| 104 |
+
# ------------------------------------------------------------------
|
| 105 |
+
# prompt building
|
| 106 |
+
# ------------------------------------------------------------------
|
| 107 |
+
|
| 108 |
+
def _build_prompt(self, text, feedback=""):
|
| 109 |
+
extra = ""
|
| 110 |
+
if feedback:
|
| 111 |
+
extra = (
|
| 112 |
+
"\n\nIMPORTANT FEEDBACK FROM PREVIOUS ATTEMPT:\n" + feedback + "\n"
|
| 113 |
+
"You MUST address this feedback. Be even MORE casual, vary "
|
| 114 |
+
"sentence lengths MORE dramatically, and add MORE natural "
|
| 115 |
+
"imperfections this time.\n"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
system_msg = (
|
| 119 |
+
"You rewrite text to sound like a real human wrote it -- "
|
| 120 |
+
"think casual Reddit comment, not a polished essay. Rules:\n"
|
| 121 |
+
"- Mix very short sentences (3-5 words) with longer complex ones (20+ words)\n"
|
| 122 |
+
"- Use contractions: don't, can't, won't, it's, etc.\n"
|
| 123 |
+
"- Sprinkle in conversational fillers: 'you know', 'like', 'honestly', 'I mean'\n"
|
| 124 |
+
"- Use casual transitions: 'so', 'anyway', 'but yeah', 'plus'\n"
|
| 125 |
+
"- Add hedging: 'maybe', 'probably', 'I think'\n"
|
| 126 |
+
"- Break perfect grammar occasionally -- comma splices, starting with 'And' or 'But'\n"
|
| 127 |
+
"- Use em-dashes, ellipses, parentheses for natural flow\n"
|
| 128 |
+
"- Add slight digressions that circle back to the main point\n"
|
| 129 |
+
"- Vary paragraph lengths unpredictably\n"
|
| 130 |
+
"- Preserve ALL factual content -- don't drop any information\n"
|
| 131 |
+
"- Return ONLY the rewritten text"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
user_msg = "Rewrite this text to sound human:\n\n" + text + extra
|
| 135 |
+
|
| 136 |
+
return (
|
| 137 |
+
_SYS_OPEN + "\n" + system_msg + _EOS + "\n"
|
| 138 |
+
+ _USR_OPEN + "\n" + user_msg + _EOS + "\n"
|
| 139 |
+
+ _ASST_OPEN + "\n"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# ------------------------------------------------------------------
|
| 143 |
+
# post-processing helpers
|
| 144 |
+
# ------------------------------------------------------------------
|
| 145 |
+
|
| 146 |
+
@staticmethod
|
| 147 |
+
def _cleanup(raw):
|
| 148 |
+
"""Strip stray markdown fencing, extra whitespace."""
|
| 149 |
+
text = raw.strip()
|
| 150 |
+
if text.startswith("```"):
|
| 151 |
+
lines = text.split("\n")
|
| 152 |
+
lines = [l for l in lines if not l.strip().startswith("```")]
|
| 153 |
+
text = "\n".join(lines).strip()
|
| 154 |
+
if text.startswith('"') and text.endswith('"'):
|
| 155 |
+
text = text[1:-1]
|
| 156 |
+
return text
|
| 157 |
+
|
| 158 |
+
@staticmethod
|
| 159 |
+
def _apply_contractions(text):
|
| 160 |
+
"""Replace formal forms with contractions."""
|
| 161 |
+
for formal, casual in _CONTRACTIONS.items():
|
| 162 |
+
# case-insensitive but preserve sentence-start capitalization
|
| 163 |
+
pattern = re.compile(re.escape(formal), re.IGNORECASE)
|
| 164 |
+
def _repl(m):
|
| 165 |
+
if m.group(0)[0].isupper():
|
| 166 |
+
return casual[0].upper() + casual[1:]
|
| 167 |
+
return casual
|
| 168 |
+
text = pattern.sub(_repl, text)
|
| 169 |
+
return text
|
| 170 |
+
|
| 171 |
+
@staticmethod
|
| 172 |
+
def _inject_fillers(text, intensity):
|
| 173 |
+
"""Randomly sprinkle fillers + hedges into ~20% of sentences."""
|
| 174 |
+
sentences = re.split(r'(?<=[.!?])\s+', text)
|
| 175 |
+
if len(sentences) < 2:
|
| 176 |
+
return text
|
| 177 |
+
|
| 178 |
+
inject_rate = 0.15 + (intensity - 0.7) * 0.3 # 0.15 at 0.7, ~0.24 at 1.0
|
| 179 |
+
result = []
|
| 180 |
+
for sent in sentences:
|
| 181 |
+
if random.random() < inject_rate and len(sent.split()) > 4:
|
| 182 |
+
filler = random.choice(_FILLERS + _HEDGES)
|
| 183 |
+
# stick it after the first couple words
|
| 184 |
+
words = sent.split()
|
| 185 |
+
pos = random.randint(1, min(3, len(words) - 1))
|
| 186 |
+
words.insert(pos, filler + ",")
|
| 187 |
+
sent = " ".join(words)
|
| 188 |
+
# occasionally swap the transition at the start
|
| 189 |
+
if random.random() < inject_rate * 0.5 and result:
|
| 190 |
+
trans = random.choice(_TRANSITIONS)
|
| 191 |
+
sent = trans.capitalize() + ", " + sent[0].lower() + sent[1:]
|
| 192 |
+
result.append(sent)
|
| 193 |
+
|
| 194 |
+
return " ".join(result)
|
| 195 |
+
|
| 196 |
+
@staticmethod
|
| 197 |
+
def _vary_punctuation(text):
|
| 198 |
+
"""Swap some periods for em-dashes or ellipses, add parenthetical asides."""
|
| 199 |
+
# ~10% of periods -> em-dash bridging the next sentence
|
| 200 |
+
sentences = text.split(". ")
|
| 201 |
+
out = []
|
| 202 |
+
for i, s in enumerate(sentences):
|
| 203 |
+
if i > 0 and random.random() < 0.12:
|
| 204 |
+
# merge with previous via em-dash
|
| 205 |
+
if out:
|
| 206 |
+
out[-1] = out[-1].rstrip(".") + " -- " + s
|
| 207 |
+
continue
|
| 208 |
+
if i > 0 and random.random() < 0.08:
|
| 209 |
+
# ellipsis instead of period
|
| 210 |
+
if out:
|
| 211 |
+
out[-1] = out[-1].rstrip(".") + "... " + s
|
| 212 |
+
continue
|
| 213 |
+
out.append(s)
|
| 214 |
+
|
| 215 |
+
text = ". ".join(out)
|
| 216 |
+
|
| 217 |
+
# toss in a parenthetical aside once in a while
|
| 218 |
+
if random.random() < 0.25 and len(text) > 100:
|
| 219 |
+
asides = [
|
| 220 |
+
"(which is wild when you think about it)",
|
| 221 |
+
"(no surprise there honestly)",
|
| 222 |
+
"(at least that's how I see it)",
|
| 223 |
+
"(but who knows really)",
|
| 224 |
+
]
|
| 225 |
+
words = text.split()
|
| 226 |
+
pos = random.randint(len(words) // 3, 2 * len(words) // 3)
|
| 227 |
+
words.insert(pos, random.choice(asides))
|
| 228 |
+
text = " ".join(words)
|
| 229 |
+
|
| 230 |
+
return text
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# quick test
|
| 234 |
+
if __name__ == "__main__":
|
| 235 |
+
h = Humanizer()
|
| 236 |
+
sample = (
|
| 237 |
+
"Artificial intelligence is advancing rapidly. This presents both "
|
| 238 |
+
"opportunities and challenges for modern society. We must consider "
|
| 239 |
+
"the ethical implications of these technologies."
|
| 240 |
+
)
|
| 241 |
+
print(h.humanize(sample))
|
agents/semantic_analyzer.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent 1: Semantic Analyzer
|
| 3 |
+
--------------------------
|
| 4 |
+
Extracts deep context from AI-generated text without modifying it.
|
| 5 |
+
Uses sentence-transformers for embeddings, then calls Mistral to
|
| 6 |
+
produce a structured JSON analysis (topic, tone, audience, etc).
|
| 7 |
+
|
| 8 |
+
No text is altered here -- purely read-only analysis.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
from huggingface_hub import InferenceClient
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class SemanticAnalyzer:
|
| 22 |
+
"""First link in the pipeline -- context extraction only."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, hf_token=None):
|
| 25 |
+
self.token = hf_token or os.getenv("HF_TOKEN", "")
|
| 26 |
+
self.client = InferenceClient(token=self.token)
|
| 27 |
+
# model we hit for the structured analysis
|
| 28 |
+
self.analysis_model = "mistralai/Mistral-7B-Instruct-v0.3"
|
| 29 |
+
|
| 30 |
+
# ------------------------------------------------------------------
|
| 31 |
+
# public api
|
| 32 |
+
# ------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def analyze(self, text: str) -> dict:
|
| 35 |
+
"""
|
| 36 |
+
Run the full semantic extraction.
|
| 37 |
+
|
| 38 |
+
Returns a dict like:
|
| 39 |
+
{
|
| 40 |
+
"original_text": <str>,
|
| 41 |
+
"analysis": {
|
| 42 |
+
"core_topic": ...,
|
| 43 |
+
"key_arguments": [...],
|
| 44 |
+
"tone": ...,
|
| 45 |
+
"target_audience": ...,
|
| 46 |
+
"sentiment": ...,
|
| 47 |
+
"writing_style": ...,
|
| 48 |
+
"word_count": int,
|
| 49 |
+
"avg_sentence_length": float
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
"""
|
| 53 |
+
logger.info("semantic analyzer: starting analysis (%d chars)", len(text))
|
| 54 |
+
|
| 55 |
+
# --- step 1: basic stats we can compute locally ----------------
|
| 56 |
+
sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]
|
| 57 |
+
word_count = len(text.split())
|
| 58 |
+
avg_sent_len = round(word_count / max(len(sentences), 1), 1)
|
| 59 |
+
|
| 60 |
+
# --- step 2: ask Mistral to do the heavy lifting ---------------
|
| 61 |
+
prompt = self._build_prompt(text)
|
| 62 |
+
try:
|
| 63 |
+
raw = self.client.text_generation(
|
| 64 |
+
prompt,
|
| 65 |
+
model=self.analysis_model,
|
| 66 |
+
max_new_tokens=600,
|
| 67 |
+
temperature=0.3, # low temp for factual extraction
|
| 68 |
+
)
|
| 69 |
+
analysis = self._parse_response(raw)
|
| 70 |
+
except Exception as exc:
|
| 71 |
+
logger.warning("LLM analysis failed (%s) -- falling back to heuristics", exc)
|
| 72 |
+
analysis = self._fallback_analysis(text)
|
| 73 |
+
|
| 74 |
+
# bolt on the local stats
|
| 75 |
+
analysis["word_count"] = word_count
|
| 76 |
+
analysis["avg_sentence_length"] = avg_sent_len
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"original_text": text,
|
| 80 |
+
"analysis": analysis,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# ------------------------------------------------------------------
|
| 84 |
+
# internals
|
| 85 |
+
# ------------------------------------------------------------------
|
| 86 |
+
|
| 87 |
+
def _build_prompt(self, text: str) -> str:
|
| 88 |
+
return (
|
| 89 |
+
"[INST] You are a text analysis expert. Analyze the following text and "
|
| 90 |
+
"return ONLY a JSON object with these keys:\n"
|
| 91 |
+
'- "core_topic": one-line summary of the main subject\n'
|
| 92 |
+
'- "key_arguments": list of the main points or claims\n'
|
| 93 |
+
'- "tone": one of formal / casual / academic / conversational / persuasive\n'
|
| 94 |
+
'- "target_audience": who this is written for\n'
|
| 95 |
+
'- "sentiment": overall sentiment (positive / negative / neutral / mixed)\n'
|
| 96 |
+
'- "writing_style": brief description of style characteristics\n\n'
|
| 97 |
+
f"Text to analyze:\n\"{text}\"\n\n"
|
| 98 |
+
"Return ONLY valid JSON, nothing else. [/INST]"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
@staticmethod
|
| 102 |
+
def _parse_response(raw: str) -> dict:
|
| 103 |
+
"""Try to pull a JSON object out of the model's response."""
|
| 104 |
+
# sometimes the model wraps it in ```json ... ```
|
| 105 |
+
cleaned = raw.strip()
|
| 106 |
+
if "```" in cleaned:
|
| 107 |
+
match = re.search(r"```(?:json)?\s*(.*?)```", cleaned, re.DOTALL)
|
| 108 |
+
if match:
|
| 109 |
+
cleaned = match.group(1).strip()
|
| 110 |
+
|
| 111 |
+
# find the first { ... } block
|
| 112 |
+
start = cleaned.find("{")
|
| 113 |
+
end = cleaned.rfind("}") + 1
|
| 114 |
+
if start != -1 and end > start:
|
| 115 |
+
try:
|
| 116 |
+
return json.loads(cleaned[start:end])
|
| 117 |
+
except json.JSONDecodeError:
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
# couldn't parse -- return a best-effort dict
|
| 121 |
+
return {
|
| 122 |
+
"core_topic": "unable to parse",
|
| 123 |
+
"key_arguments": [],
|
| 124 |
+
"tone": "unknown",
|
| 125 |
+
"target_audience": "general",
|
| 126 |
+
"sentiment": "neutral",
|
| 127 |
+
"writing_style": cleaned[:200],
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def _fallback_analysis(text: str) -> dict:
|
| 132 |
+
"""Dead-simple heuristics when the API is down."""
|
| 133 |
+
words = text.lower().split()
|
| 134 |
+
# guess tone from marker words
|
| 135 |
+
formal_markers = {"furthermore", "moreover", "consequently", "imperative", "thus"}
|
| 136 |
+
casual_markers = {"like", "kinda", "gonna", "lol", "tbh", "honestly"}
|
| 137 |
+
formal_score = sum(1 for w in words if w in formal_markers)
|
| 138 |
+
casual_score = sum(1 for w in words if w in casual_markers)
|
| 139 |
+
|
| 140 |
+
if formal_score > casual_score:
|
| 141 |
+
tone = "formal"
|
| 142 |
+
elif casual_score > formal_score:
|
| 143 |
+
tone = "casual"
|
| 144 |
+
else:
|
| 145 |
+
tone = "neutral"
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"core_topic": " ".join(words[:10]) + "...",
|
| 149 |
+
"key_arguments": [],
|
| 150 |
+
"tone": tone,
|
| 151 |
+
"target_audience": "general",
|
| 152 |
+
"sentiment": "neutral",
|
| 153 |
+
"writing_style": "standard prose",
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# quick sanity check
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
sa = SemanticAnalyzer()
|
| 160 |
+
sample = (
|
| 161 |
+
"The rapid advancement of artificial intelligence presents both "
|
| 162 |
+
"opportunities and challenges for modern society. It is imperative "
|
| 163 |
+
"that we consider the ethical implications of these technologies."
|
| 164 |
+
)
|
| 165 |
+
result = sa.analyze(sample)
|
| 166 |
+
print(json.dumps(result, indent=2))
|
agents/verifier.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Verifier Agent
|
| 3 |
+
--------------
|
| 4 |
+
Uses roberta-base-openai-detector to check whether text reads
|
| 5 |
+
as human- or AI-generated. Also has a post-processing fallback
|
| 6 |
+
for texts that stubbornly read as AI after 3 humanizer loops.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import random
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from transformers import pipeline as hf_pipeline
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
# labels from openai-detector -- LABEL_0 = Real, LABEL_1 = Fake
|
| 19 |
+
_REAL_LABEL = "Real"
|
| 20 |
+
_FAKE_LABEL = "Fake"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Verifier:
|
| 24 |
+
"""Runs AI-detection on text and provides fallback post-processing."""
|
| 25 |
+
|
| 26 |
+
def __init__(self, hf_token=None):
|
| 27 |
+
self.token = hf_token or os.getenv("HF_TOKEN", "")
|
| 28 |
+
self._pipe = None # lazy-loaded
|
| 29 |
+
|
| 30 |
+
# ------------------------------------------------------------------
|
| 31 |
+
# lazy init (heavy model, don't load until first call)
|
| 32 |
+
# ------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def _get_pipe(self):
|
| 35 |
+
if self._pipe is None:
|
| 36 |
+
logger.info("verifier: loading roberta-base-openai-detector...")
|
| 37 |
+
self._pipe = hf_pipeline(
|
| 38 |
+
"text-classification",
|
| 39 |
+
model="roberta-base-openai-detector",
|
| 40 |
+
token=self.token or None,
|
| 41 |
+
)
|
| 42 |
+
return self._pipe
|
| 43 |
+
|
| 44 |
+
# ------------------------------------------------------------------
|
| 45 |
+
# public api
|
| 46 |
+
# ------------------------------------------------------------------
|
| 47 |
+
|
| 48 |
+
def verify(self, text):
|
| 49 |
+
"""
|
| 50 |
+
Returns
|
| 51 |
+
-------
|
| 52 |
+
dict {"label": "Real"|"Fake", "confidence": float}
|
| 53 |
+
"""
|
| 54 |
+
pipe = self._get_pipe()
|
| 55 |
+
|
| 56 |
+
# the model has a max length -- truncate gracefully
|
| 57 |
+
# roberta context window is 512 tokens; ~1500 chars is safe
|
| 58 |
+
snippet = text[:1500]
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
results = pipe(snippet)
|
| 62 |
+
top = results[0]
|
| 63 |
+
label_raw = top["label"] # LABEL_0 or LABEL_1
|
| 64 |
+
score = round(top["score"], 4)
|
| 65 |
+
|
| 66 |
+
if label_raw == "LABEL_0":
|
| 67 |
+
label = _REAL_LABEL
|
| 68 |
+
ai_confidence = round(1 - score, 4)
|
| 69 |
+
else:
|
| 70 |
+
label = _FAKE_LABEL
|
| 71 |
+
ai_confidence = score
|
| 72 |
+
|
| 73 |
+
logger.info("verifier: label=%s ai_confidence=%.4f", label, ai_confidence)
|
| 74 |
+
return {"label": label, "confidence": ai_confidence}
|
| 75 |
+
|
| 76 |
+
except Exception as exc:
|
| 77 |
+
logger.error("verifier pipeline failed: %s", exc)
|
| 78 |
+
return {"label": "Unknown", "confidence": 0.5}
|
| 79 |
+
|
| 80 |
+
# ------------------------------------------------------------------
|
| 81 |
+
# post-processing fallback (last resort after 3 loops)
|
| 82 |
+
# ------------------------------------------------------------------
|
| 83 |
+
|
| 84 |
+
@staticmethod
|
| 85 |
+
def apply_last_resort(text):
|
| 86 |
+
"""
|
| 87 |
+
If the humanizer loop maxed out and text still reads as AI,
|
| 88 |
+
apply brute-force perturbations:
|
| 89 |
+
1. lightly shuffle adjacent sentences
|
| 90 |
+
2. inject a minor typo in a random word
|
| 91 |
+
3. break one long sentence into two
|
| 92 |
+
"""
|
| 93 |
+
sentences = re.split(r'(?<=[.!?])\s+', text)
|
| 94 |
+
|
| 95 |
+
# 1) swap a random pair of adjacent sentences
|
| 96 |
+
if len(sentences) > 3:
|
| 97 |
+
idx = random.randint(1, len(sentences) - 2)
|
| 98 |
+
sentences[idx], sentences[idx - 1] = sentences[idx - 1], sentences[idx]
|
| 99 |
+
|
| 100 |
+
# 2) inject a subtle typo
|
| 101 |
+
if len(sentences) > 1:
|
| 102 |
+
target_idx = random.randint(0, len(sentences) - 1)
|
| 103 |
+
words = sentences[target_idx].split()
|
| 104 |
+
if len(words) > 4:
|
| 105 |
+
word_idx = random.randint(2, len(words) - 1)
|
| 106 |
+
w = words[word_idx]
|
| 107 |
+
if len(w) > 4:
|
| 108 |
+
# swap two adjacent chars
|
| 109 |
+
pos = random.randint(1, len(w) - 2)
|
| 110 |
+
w = w[:pos] + w[pos + 1] + w[pos] + w[pos + 2:]
|
| 111 |
+
words[word_idx] = w
|
| 112 |
+
sentences[target_idx] = " ".join(words)
|
| 113 |
+
|
| 114 |
+
# 3) break one long sentence
|
| 115 |
+
for i, s in enumerate(sentences):
|
| 116 |
+
words = s.split()
|
| 117 |
+
if len(words) > 18:
|
| 118 |
+
mid = len(words) // 2
|
| 119 |
+
# find a comma or conjunction near the midpoint
|
| 120 |
+
for offset in range(5):
|
| 121 |
+
check = mid + offset
|
| 122 |
+
if check < len(words) and words[check].rstrip(",") in ("and", "but", "which", "that", "because"):
|
| 123 |
+
first_half = " ".join(words[:check]).rstrip(",") + "."
|
| 124 |
+
second_half = " ".join(words[check:])
|
| 125 |
+
second_half = second_half[0].upper() + second_half[1:]
|
| 126 |
+
sentences[i] = first_half + " " + second_half
|
| 127 |
+
break
|
| 128 |
+
break # only break one sentence
|
| 129 |
+
|
| 130 |
+
return " ".join(sentences)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# quick test
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
v = Verifier()
|
| 136 |
+
sample = (
|
| 137 |
+
"The rapid advancement of artificial intelligence presents both "
|
| 138 |
+
"opportunities and challenges for modern society. It is imperative "
|
| 139 |
+
"that we consider the ethical implications of these technologies."
|
| 140 |
+
)
|
| 141 |
+
print(v.verify(sample))
|
app.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
app.py -- Gradio web UI for HF Spaces
|
| 4 |
+
--------------------------------------
|
| 5 |
+
Provides a browser-based interface to the 3-agent humanizer
|
| 6 |
+
pipeline. Designed for deployment on huggingface.co/spaces.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
|
| 15 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
from main import run_pipeline # reuse the orchestrator
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger("app")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# -- the gradio callback --------------------------------------------------
|
| 23 |
+
|
| 24 |
+
def humanize_text(input_text, intensity):
|
| 25 |
+
"""Called when user clicks 'Humanize'. Returns 4 outputs."""
|
| 26 |
+
if not input_text or not input_text.strip():
|
| 27 |
+
return (
|
| 28 |
+
"Please paste some text first!",
|
| 29 |
+
"N/A",
|
| 30 |
+
0.0,
|
| 31 |
+
0,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
result = run_pipeline(
|
| 36 |
+
input_text.strip(),
|
| 37 |
+
intensity=intensity,
|
| 38 |
+
max_loops=3,
|
| 39 |
+
)
|
| 40 |
+
return (
|
| 41 |
+
result["humanized_text"],
|
| 42 |
+
result["label"],
|
| 43 |
+
round(result["confidence"] * 100, 1),
|
| 44 |
+
result["iterations"],
|
| 45 |
+
)
|
| 46 |
+
except Exception as exc:
|
| 47 |
+
logger.exception("pipeline error")
|
| 48 |
+
return (
|
| 49 |
+
f"Error: {exc}",
|
| 50 |
+
"Error",
|
| 51 |
+
0.0,
|
| 52 |
+
0,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# -- build the UI ----------------------------------------------------------
|
| 57 |
+
|
| 58 |
+
CUSTOM_CSS = """
|
| 59 |
+
.gradio-container {
|
| 60 |
+
max-width: 900px !important;
|
| 61 |
+
margin: auto !important;
|
| 62 |
+
}
|
| 63 |
+
.header-text {
|
| 64 |
+
text-align: center;
|
| 65 |
+
margin-bottom: 0.5rem;
|
| 66 |
+
}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
with gr.Blocks(css=CUSTOM_CSS, title="AI Text Humanizer") as demo:
|
| 70 |
+
|
| 71 |
+
gr.Markdown(
|
| 72 |
+
"""
|
| 73 |
+
# AI Text Humanizer
|
| 74 |
+
### 3-Agent Sequential Pipeline
|
| 75 |
+
|
| 76 |
+
Paste AI-generated text below and watch it transform into
|
| 77 |
+
natural, human-sounding writing. Uses a **Semantic Analyzer**,
|
| 78 |
+
**Draft Generator**, and **Humanizer** with a built-in
|
| 79 |
+
AI-detection verifier loop.
|
| 80 |
+
""",
|
| 81 |
+
elem_classes="header-text",
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
with gr.Row():
|
| 85 |
+
with gr.Column(scale=1):
|
| 86 |
+
input_box = gr.Textbox(
|
| 87 |
+
label="Paste AI-Generated Text",
|
| 88 |
+
placeholder=(
|
| 89 |
+
"e.g. The rapid advancement of artificial intelligence "
|
| 90 |
+
"presents both opportunities and challenges..."
|
| 91 |
+
),
|
| 92 |
+
lines=8,
|
| 93 |
+
)
|
| 94 |
+
intensity_slider = gr.Slider(
|
| 95 |
+
minimum=0.7,
|
| 96 |
+
maximum=1.0,
|
| 97 |
+
value=0.9,
|
| 98 |
+
step=0.05,
|
| 99 |
+
label="Humanization Intensity",
|
| 100 |
+
info="Higher = more casual & unpredictable",
|
| 101 |
+
)
|
| 102 |
+
run_btn = gr.Button("Humanize", variant="primary", size="lg")
|
| 103 |
+
|
| 104 |
+
with gr.Column(scale=1):
|
| 105 |
+
output_box = gr.Textbox(
|
| 106 |
+
label="Humanized Output",
|
| 107 |
+
lines=8,
|
| 108 |
+
interactive=False,
|
| 109 |
+
)
|
| 110 |
+
with gr.Row():
|
| 111 |
+
label_out = gr.Textbox(label="Detection Result", interactive=False)
|
| 112 |
+
conf_out = gr.Number(label="AI Confidence (%)", interactive=False)
|
| 113 |
+
iter_out = gr.Number(
|
| 114 |
+
label="Iterations",
|
| 115 |
+
interactive=False,
|
| 116 |
+
precision=0,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# wire it up
|
| 120 |
+
run_btn.click(
|
| 121 |
+
fn=humanize_text,
|
| 122 |
+
inputs=[input_box, intensity_slider],
|
| 123 |
+
outputs=[output_box, label_out, conf_out, iter_out],
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
gr.Markdown(
|
| 127 |
+
"""
|
| 128 |
+
---
|
| 129 |
+
**How it works:**
|
| 130 |
+
1. **Agent 1 (Semantic Analyzer)** — extracts topic, tone,
|
| 131 |
+
audience, and key arguments from the input.
|
| 132 |
+
2. **Agent 2 (Draft Generator)** — rewrites the text naturally
|
| 133 |
+
while preserving 100% of factual content.
|
| 134 |
+
3. **Agent 3 (Humanizer)** — injects human writing patterns:
|
| 135 |
+
contractions, fillers, hedging, sentence-length variation,
|
| 136 |
+
and intentional minor imperfections.
|
| 137 |
+
4. **Verifier** — checks the output with an AI detector.
|
| 138 |
+
If flagged, loops back to Agent 3 (max 3 times).
|
| 139 |
+
"""
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# -- launch ----------------------------------------------------------------
|
| 143 |
+
if __name__ == "__main__":
|
| 144 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
main.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
main.py -- CLI orchestrator
|
| 4 |
+
----------------------------
|
| 5 |
+
Wires the 3-agent pipeline:
|
| 6 |
+
SemanticAnalyzer -> DraftGenerator -> Humanizer -> Verifier
|
| 7 |
+
|
| 8 |
+
If verifier flags the output as AI (confidence > 0.7), loops
|
| 9 |
+
back to Humanizer with feedback (max 3 iterations).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import sys
|
| 15 |
+
import os
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
# --- path shenanigans so we can import agents/ --------------------------
|
| 19 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
+
|
| 21 |
+
from agents.semantic_analyzer import SemanticAnalyzer
|
| 22 |
+
from agents.draft_generator import DraftGenerator
|
| 23 |
+
from agents.humanizer import Humanizer
|
| 24 |
+
from agents.verifier import Verifier
|
| 25 |
+
|
| 26 |
+
logging.basicConfig(
|
| 27 |
+
level=logging.INFO,
|
| 28 |
+
format="%(asctime)s %(name)-28s %(levelname)-5s %(message)s",
|
| 29 |
+
)
|
| 30 |
+
logger = logging.getLogger("pipeline")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# -- the actual pipeline -------------------------------------------------
|
| 34 |
+
|
| 35 |
+
def run_pipeline(text, intensity=0.9, max_loops=3, hf_token=None):
|
| 36 |
+
"""
|
| 37 |
+
Returns dict with keys:
|
| 38 |
+
humanized_text, label, confidence, iterations, analysis
|
| 39 |
+
"""
|
| 40 |
+
tok = hf_token or os.getenv("HF_TOKEN", "")
|
| 41 |
+
|
| 42 |
+
# 1) semantic analysis
|
| 43 |
+
logger.info(">>> STAGE 1: Semantic Analysis")
|
| 44 |
+
analyzer = SemanticAnalyzer(hf_token=tok)
|
| 45 |
+
context = analyzer.analyze(text)
|
| 46 |
+
logger.info("analysis done: tone=%s", context["analysis"].get("tone"))
|
| 47 |
+
|
| 48 |
+
# 2) draft generation
|
| 49 |
+
logger.info(">>> STAGE 2: Draft Generation")
|
| 50 |
+
drafter = DraftGenerator(hf_token=tok)
|
| 51 |
+
draft = drafter.generate(context)
|
| 52 |
+
logger.info("draft generated (%d chars)", len(draft))
|
| 53 |
+
|
| 54 |
+
# 3) humanization + verification loop
|
| 55 |
+
humanizer_agent = Humanizer(hf_token=tok)
|
| 56 |
+
verifier_agent = Verifier(hf_token=tok)
|
| 57 |
+
|
| 58 |
+
feedback = ""
|
| 59 |
+
humanized = draft
|
| 60 |
+
label = "Fake"
|
| 61 |
+
confidence = 1.0
|
| 62 |
+
iterations = 0
|
| 63 |
+
|
| 64 |
+
for i in range(1, max_loops + 1):
|
| 65 |
+
logger.info(">>> STAGE 3 (iteration %d/%d): Humanization", i, max_loops)
|
| 66 |
+
humanized = humanizer_agent.humanize(
|
| 67 |
+
draft if i == 1 else humanized,
|
| 68 |
+
intensity=intensity,
|
| 69 |
+
feedback=feedback,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
logger.info(">>> VERIFY (iteration %d/%d)", i, max_loops)
|
| 73 |
+
result = verifier_agent.verify(humanized)
|
| 74 |
+
label = result["label"]
|
| 75 |
+
confidence = result["confidence"]
|
| 76 |
+
iterations = i
|
| 77 |
+
|
| 78 |
+
logger.info(
|
| 79 |
+
"verification: label=%s ai_confidence=%.4f",
|
| 80 |
+
label, confidence,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# if it reads as human, we're done
|
| 84 |
+
if label == "Real" or confidence < 0.5:
|
| 85 |
+
logger.info("passed verification on iteration %d", i)
|
| 86 |
+
break
|
| 87 |
+
|
| 88 |
+
# otherwise, feed back to humanizer for the next round
|
| 89 |
+
feedback = (
|
| 90 |
+
f"The text was detected as AI-generated with {confidence:.0%} confidence. "
|
| 91 |
+
"Increase variation, add more natural imperfections, use more "
|
| 92 |
+
"contractions, vary sentence lengths more dramatically, and "
|
| 93 |
+
"sprinkle in casual fillers like 'honestly' or 'you know'."
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# last resort post-processing if still flagged
|
| 97 |
+
if label != "Real" and confidence >= 0.5:
|
| 98 |
+
logger.info("max loops reached -- applying last-resort perturbations")
|
| 99 |
+
humanized = verifier_agent.apply_last_resort(humanized)
|
| 100 |
+
final = verifier_agent.verify(humanized)
|
| 101 |
+
label = final["label"]
|
| 102 |
+
confidence = final["confidence"]
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
"humanized_text": humanized,
|
| 106 |
+
"label": label,
|
| 107 |
+
"confidence": confidence,
|
| 108 |
+
"iterations": iterations,
|
| 109 |
+
"analysis": context.get("analysis", {}),
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# -- CLI -----------------------------------------------------------------
|
| 114 |
+
|
| 115 |
+
def main():
|
| 116 |
+
parser = argparse.ArgumentParser(
|
| 117 |
+
description="AI Text Humanizer -- 3-agent pipeline",
|
| 118 |
+
)
|
| 119 |
+
group = parser.add_mutually_exclusive_group(required=True)
|
| 120 |
+
group.add_argument("--text", type=str, help="text to humanize (inline)")
|
| 121 |
+
group.add_argument("--file", type=str, help="path to a .txt file to read")
|
| 122 |
+
|
| 123 |
+
parser.add_argument(
|
| 124 |
+
"--intensity", type=float, default=0.9,
|
| 125 |
+
help="humanization intensity 0.7-1.0 (default: 0.9)",
|
| 126 |
+
)
|
| 127 |
+
parser.add_argument(
|
| 128 |
+
"--max-loops", type=int, default=3,
|
| 129 |
+
help="max verification loops (default: 3)",
|
| 130 |
+
)
|
| 131 |
+
parser.add_argument("--json", action="store_true", help="output raw JSON")
|
| 132 |
+
|
| 133 |
+
args = parser.parse_args()
|
| 134 |
+
|
| 135 |
+
# grab the input text
|
| 136 |
+
if args.file:
|
| 137 |
+
with open(args.file, "r") as f:
|
| 138 |
+
text = f.read().strip()
|
| 139 |
+
else:
|
| 140 |
+
text = args.text
|
| 141 |
+
|
| 142 |
+
if not text:
|
| 143 |
+
print("error: empty input text", file=sys.stderr)
|
| 144 |
+
sys.exit(1)
|
| 145 |
+
|
| 146 |
+
print(f"\n{'='*60}")
|
| 147 |
+
print(" AI Text Humanizer -- 3-Agent Pipeline")
|
| 148 |
+
print(f"{'='*60}\n")
|
| 149 |
+
print(f"Input ({len(text)} chars):\n{text[:200]}{'...' if len(text) > 200 else ''}\n")
|
| 150 |
+
|
| 151 |
+
result = run_pipeline(
|
| 152 |
+
text,
|
| 153 |
+
intensity=args.intensity,
|
| 154 |
+
max_loops=args.max_loops,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
if args.json:
|
| 158 |
+
print(json.dumps(result, indent=2))
|
| 159 |
+
else:
|
| 160 |
+
print(f"\n{'='*60}")
|
| 161 |
+
print(" HUMANIZED OUTPUT")
|
| 162 |
+
print(f"{'='*60}\n")
|
| 163 |
+
print(result["humanized_text"])
|
| 164 |
+
print(f"\n{'='*60}")
|
| 165 |
+
print(f" Detection: {result['label']} | AI Confidence: {result['confidence']:.2%}")
|
| 166 |
+
print(f" Iterations: {result['iterations']}")
|
| 167 |
+
print(f"{'='*60}\n")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.36.0
|
| 2 |
+
torch>=2.1.0
|
| 3 |
+
datasets>=2.16.0
|
| 4 |
+
gradio>=4.0.0
|
| 5 |
+
huggingface_hub>=0.20.0
|
| 6 |
+
sentence-transformers>=2.2.0
|
| 7 |
+
numpy>=1.24.0
|
| 8 |
+
scikit-learn>=1.3.0
|
| 9 |
+
accelerate>=0.25.0
|