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 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: 6.5.1
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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