Spaces:
Running
Running
eheguy commited on
Commit ·
d720fd0
0
Parent(s):
Deploy humanizer backend
Browse files- .gitignore +5 -0
- Dockerfile +27 -0
- README.md +44 -0
- detector.py +28 -0
- evaluator.py +22 -0
- humanizer.py +87 -0
- main.py +57 -0
- 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
|