eheguy commited on
Commit
d720fd0
·
0 Parent(s):

Deploy humanizer backend

Browse files
Files changed (8) hide show
  1. .gitignore +5 -0
  2. Dockerfile +27 -0
  3. README.md +44 -0
  4. detector.py +28 -0
  5. evaluator.py +22 -0
  6. humanizer.py +87 -0
  7. main.py +57 -0
  8. requirements.txt +9 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ build-essential \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for layer caching
12
+ COPY requirements.txt .
13
+
14
+ # Install Python dependencies
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy all backend files
18
+ COPY main.py .
19
+ COPY humanizer.py .
20
+ COPY detector.py .
21
+ COPY evaluator.py .
22
+
23
+ # Expose port 7860 (HuggingFace Spaces default port)
24
+ EXPOSE 7860
25
+
26
+ # Start the server on port 7860
27
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Humanizer API
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # Humanizer API
11
+
12
+ FastAPI backend for the AI text humanizer.
13
+
14
+ ## Endpoints
15
+
16
+ - `POST /humanize` — Humanize AI-generated text
17
+
18
+ ## Request
19
+
20
+ ```json
21
+ {
22
+ "text": "Your AI-generated text here",
23
+ "mode": "standard"
24
+ }
25
+ ```
26
+
27
+ ## Response
28
+
29
+ ```json
30
+ {
31
+ "humanized": "Rewritten text here",
32
+ "mode": "standard",
33
+ "score_before": 0.94,
34
+ "score_after": 0.21,
35
+ "similarity_score": 0.91,
36
+ "meaning_preserved": true
37
+ }
38
+ ```
39
+
40
+ ## Modes
41
+
42
+ - `simple` — Light cleanup only
43
+ - `standard` — Full restructuring
44
+ - `enhanced` — Maximum humanization
detector.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline
2
+
3
+ # Cache the classifier pipeline globally after first load
4
+ _classifier = None
5
+
6
+ def get_ai_score(text: str) -> float:
7
+ global _classifier
8
+ if _classifier is None:
9
+ # Load the pipeline using the specified model
10
+ _classifier = pipeline(
11
+ "text-classification",
12
+ model="openai-community/roberta-base-openai-detector"
13
+ )
14
+
15
+ # Truncate input text to 512 tokens max before scoring to respect model limits
16
+ results = _classifier(text, truncation=True, max_length=512)
17
+ result = results[0]
18
+
19
+ label = result["label"]
20
+ score = float(result["score"])
21
+
22
+ # Map labels:
23
+ # "Fake" score represents AI probability.
24
+ # "Real" score represents human probability, so AI probability = 1 - score.
25
+ if label == "Fake":
26
+ return score
27
+ else:
28
+ return 1.0 - score
evaluator.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers import SentenceTransformer, util
2
+
3
+ # Cache the SentenceTransformer model globally after first load
4
+ _model = None
5
+
6
+ def get_similarity(text_a: str, text_b: str) -> float:
7
+ global _model
8
+ if _model is None:
9
+ _model = SentenceTransformer('all-MiniLM-L6-v2')
10
+
11
+ # Encode both inputs into embeddings
12
+ embedding_a = _model.encode(text_a, convert_to_tensor=True)
13
+ embedding_b = _model.encode(text_b, convert_to_tensor=True)
14
+
15
+ # Compute cosine similarity
16
+ similarity = util.cos_sim(embedding_a, embedding_b)
17
+
18
+ return float(similarity[0][0])
19
+
20
+ def meaning_preserved(text_a: str, text_b: str, threshold: float = 0.85) -> bool:
21
+ similarity = get_similarity(text_a, text_b)
22
+ return similarity >= threshold
humanizer.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+
4
+ HF_API_URL = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.3"
5
+
6
+ SIMPLE_PROMPT = """You are a light text editor. Rewrite the text the user sends with these rules only:
7
+ 1. Remove any of these phrases entirely: "It is important to note", "In today's fast-paced world", "Furthermore", "Moreover", "In conclusion", "When it comes to", "It is worth noting", "Needless to say", "In the realm of", "It goes without saying".
8
+ 2. Replace obvious synonym swaps where words feel robotic or overly formal.
9
+ 3. Do NOT change sentence structure, length, or order.
10
+ 4. Do NOT change the meaning, facts, or argument.
11
+ 5. Output ONLY the rewritten text. No preamble. No explanation."""
12
+
13
+ STANDARD_PROMPT = """You are a text restructuring engine. Rewrite the text the user sends with these rules:
14
+ 1. Vary sentence lengths. Mix short sentences (under 8 words) with longer ones (over 25 words). Never have 3 sentences of similar length in a row.
15
+ 2. Remove any of these phrases entirely: "It is important to note", "In today's fast-paced world", "Furthermore", "Moreover", "In conclusion", "When it comes to", "It is worth noting", "Needless to say", "In the realm of", "It goes without saying".
16
+ 3. Flip at least 2 passive voice constructions to active voice.
17
+ 4. Move a subordinate clause from the end of a sentence to the beginning in at least 2 places.
18
+ 5. Add 1 or 2 contractions where formal phrasing sits (e.g. "it is" -> "it's").
19
+ 6. Do NOT change the meaning, facts, or argument.
20
+ 7. Output ONLY the rewritten text. No preamble. No explanation."""
21
+
22
+ ENHANCED_PROMPT = """You are an aggressive text humanizer. Rewrite the text the user sends to sound completely natural and human. Follow all of these rules:
23
+ 1. Vary sentence lengths dramatically. Short punchy sentences (3-6 words) must appear. Long flowing sentences (30+ words) must appear. No rhythm should feel uniform.
24
+ 2. Remove any of these phrases entirely: "It is important to note", "In today's fast-paced world", "Furthermore", "Moreover", "In conclusion", "When it comes to", "It is worth noting", "Needless to say", "In the realm of", "It goes without saying".
25
+ 3. Flip all passive voice constructions to active voice.
26
+ 4. Move subordinate clauses to unexpected positions.
27
+ 5. Add contractions naturally throughout (aim for 30% of sentences).
28
+ 6. Add one parenthetical aside somewhere in the text that feels natural.
29
+ 7. Replace at least one transition word per paragraph with a casual connector or none at all.
30
+ 8. Introduce one slightly informal word choice per paragraph.
31
+ 9. Do NOT change the meaning, facts, or argument. All information must be preserved.
32
+ 10. Output ONLY the rewritten text. No preamble. No explanation."""
33
+
34
+ PROMPTS = {
35
+ "simple": SIMPLE_PROMPT,
36
+ "standard": STANDARD_PROMPT,
37
+ "enhanced": ENHANCED_PROMPT,
38
+ }
39
+
40
+
41
+ def _build_prompt(system: str, text: str) -> str:
42
+ """Format prompt in Mistral instruct format."""
43
+ return f"<s>[INST] {system}\n\nText to rewrite:\n{text} [/INST]"
44
+
45
+
46
+ async def humanize_text(text: str, mode: str = "standard") -> str:
47
+ hf_token = os.getenv("HF_TOKEN")
48
+ if not hf_token:
49
+ raise ValueError("HF_TOKEN not set in environment variables.")
50
+
51
+ system_prompt = PROMPTS.get(mode, STANDARD_PROMPT)
52
+ prompt = _build_prompt(system_prompt, text)
53
+
54
+ headers = {"Authorization": f"Bearer {hf_token}"}
55
+ payload = {
56
+ "inputs": prompt,
57
+ "parameters": {
58
+ "max_new_tokens": 2000,
59
+ "temperature": 0.7,
60
+ "top_p": 0.95,
61
+ "do_sample": True,
62
+ "return_full_text": False,
63
+ }
64
+ }
65
+
66
+ response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=60)
67
+
68
+ if response.status_code == 503:
69
+ # Model is loading — wait and retry once
70
+ import time
71
+ time.sleep(20)
72
+ response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=60)
73
+
74
+ if response.status_code != 200:
75
+ raise ValueError(f"HuggingFace API error {response.status_code}: {response.text}")
76
+
77
+ result = response.json()
78
+
79
+ # HF returns a list with one dict: [{"generated_text": "..."}]
80
+ if isinstance(result, list) and len(result) > 0:
81
+ generated = result[0].get("generated_text", "")
82
+ # Strip any accidental prompt echo if return_full_text was ignored
83
+ if "[/INST]" in generated:
84
+ generated = generated.split("[/INST]")[-1].strip()
85
+ return generated.strip()
86
+
87
+ raise ValueError(f"Unexpected response format from HuggingFace: {result}")
main.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import traceback
3
+ from fastapi import FastAPI, HTTPException
4
+ from pydantic import BaseModel
5
+ from dotenv import load_dotenv
6
+
7
+ # Load environment variables from .env file
8
+ load_dotenv()
9
+
10
+ from humanizer import humanize_text
11
+ from detector import get_ai_score
12
+ from evaluator import get_similarity, meaning_preserved
13
+
14
+ app = FastAPI(title="AI Humanizer API")
15
+
16
+ class HumanizeRequest(BaseModel):
17
+ text: str
18
+ mode: str = "standard"
19
+
20
+ class HumanizeResponse(BaseModel):
21
+ humanized: str
22
+ mode: str
23
+ score_before: float
24
+ score_after: float
25
+ similarity_score: float
26
+ meaning_preserved: bool
27
+
28
+ @app.post("/humanize", response_model=HumanizeResponse)
29
+ async def humanize(request: HumanizeRequest):
30
+ if request.mode not in ("simple", "standard", "enhanced"):
31
+ raise HTTPException(status_code=400, detail="mode must be one of: simple, standard, enhanced")
32
+
33
+ try:
34
+ score_before = get_ai_score(request.text)
35
+ humanized_text = await humanize_text(request.text, mode=request.mode)
36
+
37
+ if not meaning_preserved(request.text, humanized_text):
38
+ humanized_text = await humanize_text(request.text, mode=request.mode)
39
+
40
+ score_after = get_ai_score(humanized_text)
41
+ similarity_score = get_similarity(request.text, humanized_text)
42
+ preserved = meaning_preserved(request.text, humanized_text)
43
+
44
+ return HumanizeResponse(
45
+ humanized=humanized_text,
46
+ mode=request.mode,
47
+ score_before=score_before,
48
+ score_after=score_after,
49
+ similarity_score=similarity_score,
50
+ meaning_preserved=preserved
51
+ )
52
+ except Exception as e:
53
+ import traceback
54
+ raise HTTPException(status_code=500, detail=traceback.format_exc())
55
+ if __name__ == "__main__":
56
+ import uvicorn
57
+ uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-dotenv
4
+ requests
5
+
6
+ # Large installs: pip install may take a few minutes
7
+ transformers
8
+ torch
9
+ sentence-transformers