Commit
·
6d3d699
1
Parent(s):
8963da2
Optimized api
Browse files- Dockerfile +57 -22
- app.py +88 -159
- convert_model.py +16 -0
- model_quantizer.py +21 -0
- optimized_model/model_optimized.onnx +3 -0
- requirements.txt +26 -15
Dockerfile
CHANGED
@@ -1,32 +1,67 @@
|
|
1 |
-
FROM python:3.9-slim
|
2 |
|
3 |
-
|
|
|
4 |
|
5 |
-
#
|
6 |
-
ENV
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
|
11 |
-
#
|
12 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
13 |
-
|
14 |
-
|
|
|
15 |
&& rm -rf /var/lib/apt/lists/*
|
16 |
|
17 |
-
|
18 |
-
RUN mkdir -p ${HF_HOME} && chmod 777 ${HF_HOME} && \
|
19 |
-
useradd -m appuser && chown -R appuser /code ${HF_HOME}
|
20 |
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
-
#
|
24 |
-
COPY
|
25 |
-
RUN pip install --no-cache-dir --upgrade pip && \
|
26 |
-
pip install --no-cache-dir -r requirements.txt
|
27 |
|
28 |
-
#
|
29 |
-
|
30 |
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
|
2 |
+
# Use minimal Python image with Intel MKL optimizations
|
3 |
+
FROM python:3.9-slim-bullseye
|
4 |
|
5 |
+
# Configure environment for CPU optimization
|
6 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
7 |
+
OMP_NUM_THREADS=4 \
|
8 |
+
PORT=7860 \
|
9 |
+
MAX_WORKERS=2
|
10 |
|
11 |
+
# Install system dependencies
|
12 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
13 |
+
gcc \
|
14 |
+
libgl1 \
|
15 |
+
poppler-utils \
|
16 |
&& rm -rf /var/lib/apt/lists/*
|
17 |
|
18 |
+
WORKDIR /app
|
|
|
|
|
19 |
|
20 |
+
# Install Python dependencies with CPU-optimized versions
|
21 |
+
COPY requirements.txt .
|
22 |
+
RUN pip install --no-cache-dir -U pip && \
|
23 |
+
pip install --no-cache-dir \
|
24 |
+
-r requirements.txt \
|
25 |
+
--timeout 600 \
|
26 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
27 |
|
28 |
+
# Copy application files
|
29 |
+
COPY . .
|
|
|
|
|
30 |
|
31 |
+
# Start the server
|
32 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
|
33 |
|
34 |
+
|
35 |
+
|
36 |
+
# FROM python:3.9-slim
|
37 |
+
|
38 |
+
# WORKDIR /code
|
39 |
+
|
40 |
+
# # Hugging Face Space requirements
|
41 |
+
# ENV HF_HOME=/tmp/cache \
|
42 |
+
# TRANSFORMERS_CACHE=/tmp/cache \
|
43 |
+
# SENTENCE_TRANSFORMERS_HOME=/tmp/cache \
|
44 |
+
# PATH="/home/appuser/.local/bin:${PATH}"
|
45 |
+
|
46 |
+
# # System dependencies
|
47 |
+
# RUN apt-get update && apt-get install -y --no-install-recommends \
|
48 |
+
# build-essential \
|
49 |
+
# git \
|
50 |
+
# && rm -rf /var/lib/apt/lists/*
|
51 |
+
|
52 |
+
# # Create cache directory and non-root user
|
53 |
+
# RUN mkdir -p ${HF_HOME} && chmod 777 ${HF_HOME} && \
|
54 |
+
# useradd -m appuser && chown -R appuser /code ${HF_HOME}
|
55 |
+
|
56 |
+
# USER appuser
|
57 |
+
|
58 |
+
# # Install Python dependencies
|
59 |
+
# COPY --chown=appuser:appuser requirements.txt .
|
60 |
+
# RUN pip install --no-cache-dir --upgrade pip && \
|
61 |
+
# pip install --no-cache-dir -r requirements.txt
|
62 |
+
|
63 |
+
# # Copy application code
|
64 |
+
# COPY --chown=appuser:appuser app.py .
|
65 |
+
|
66 |
+
# # Hugging Face Space-specific CMD
|
67 |
+
# CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
CHANGED
@@ -1,183 +1,112 @@
|
|
1 |
# app.py: AI Detection and Plagiarism Check API
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
5 |
from fastapi.responses import JSONResponse
|
|
|
|
|
6 |
from sentence_transformers import SentenceTransformer
|
7 |
-
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
8 |
from PyPDF2 import PdfReader
|
9 |
-
from sklearn.metrics.pairwise import cosine_similarity
|
10 |
-
import torch
|
11 |
-
import os
|
12 |
-
import numpy as np
|
13 |
-
import shutil
|
14 |
-
import uuid
|
15 |
import tempfile
|
16 |
-
import
|
17 |
-
import time
|
18 |
-
from typing import Dict, Any
|
19 |
-
|
20 |
-
# Configure logging
|
21 |
-
logging.basicConfig(
|
22 |
-
level=logging.INFO,
|
23 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
24 |
-
)
|
25 |
-
logger = logging.getLogger(__name__)
|
26 |
-
|
27 |
-
app = FastAPI(
|
28 |
-
title="Essay Analysis API",
|
29 |
-
version="1.0.0",
|
30 |
-
docs_url="/docs",
|
31 |
-
redoc_url=None
|
32 |
-
)
|
33 |
|
34 |
# Configuration
|
35 |
-
CACHE_DIR = "/tmp/cache"
|
36 |
-
PLAGIARISM_THRESHOLD = 0.82
|
37 |
-
MAX_TEXT_LENGTH = 512
|
38 |
MODEL_NAME = "Essay-Grader/roberta-ai-detector-20250401_232702"
|
39 |
-
SENTENCE_MODEL = "sentence-transformers/all-
|
40 |
-
|
41 |
-
#
|
42 |
-
|
43 |
-
"model_loaded": False,
|
44 |
-
"last_error": None
|
45 |
-
}
|
46 |
|
47 |
-
|
48 |
-
embedder = None
|
49 |
-
ai_tokenizer = None
|
50 |
-
ai_model = None
|
51 |
|
52 |
-
|
53 |
-
|
|
|
54 |
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
# Load models
|
62 |
-
logger.info("Loading models...")
|
63 |
-
embedder = SentenceTransformer(SENTENCE_MODEL)
|
64 |
-
ai_tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
65 |
-
ai_model = AutoModelForSequenceClassification.from_pretrained(
|
66 |
-
MODEL_NAME,
|
67 |
-
device_map="auto",
|
68 |
-
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32
|
69 |
-
).eval()
|
70 |
-
|
71 |
-
# Warmup
|
72 |
-
test_text = "Model initialization text. " * 50
|
73 |
-
inputs = ai_tokenizer(test_text, return_tensors="pt", truncation=True)
|
74 |
-
with torch.no_grad():
|
75 |
-
ai_model(**inputs.to(ai_model.device))
|
76 |
-
|
77 |
-
model_status.update({"model_loaded": True, "last_error": None})
|
78 |
-
return True
|
79 |
-
|
80 |
-
except Exception as e:
|
81 |
-
error_msg = f"Model load failed: {str(e)}"
|
82 |
-
logger.error(error_msg)
|
83 |
-
model_status.update({"model_loaded": False, "last_error": error_msg})
|
84 |
-
return False
|
85 |
-
|
86 |
-
@app.on_event("startup")
|
87 |
-
async def startup_event():
|
88 |
-
for _ in range(3):
|
89 |
-
if initialize_models():
|
90 |
-
return
|
91 |
-
time.sleep(5)
|
92 |
-
logger.error("Failed to initialize models")
|
93 |
-
|
94 |
-
def extract_text_from_pdf(pdf_path: str) -> str:
|
95 |
-
try:
|
96 |
-
return " ".join(page.extract_text() for page in PdfReader(pdf_path).pages)
|
97 |
-
except Exception as e:
|
98 |
-
logger.error(f"PDF error: {str(e)}")
|
99 |
-
raise HTTPException(400, "Invalid PDF file")
|
100 |
-
|
101 |
-
def chunk_text(text: str) -> list:
|
102 |
-
sentences = [s.strip() for s in text.split('.') if s.strip()]
|
103 |
-
return ['. '.join(sentences[i:i+5]) + '.' for i in range(0, len(sentences), 5)]
|
104 |
-
|
105 |
-
def analyze_content(text: str) -> Dict[str, float]:
|
106 |
-
try:
|
107 |
-
inputs = ai_tokenizer(
|
108 |
-
text,
|
109 |
-
truncation=True,
|
110 |
-
padding='max_length',
|
111 |
-
max_length=MAX_TEXT_LENGTH,
|
112 |
-
return_tensors="pt"
|
113 |
-
).to(ai_model.device)
|
114 |
-
|
115 |
-
with torch.no_grad():
|
116 |
-
outputs = ai_model(**inputs)
|
117 |
-
probs = torch.softmax(outputs.logits, dim=1).squeeze()
|
118 |
-
|
119 |
-
return {
|
120 |
-
"Human_Written": round(probs[0].item() * 100, 2),
|
121 |
-
"AI_Generated": round(probs[1].item() * 100, 2)
|
122 |
-
}
|
123 |
-
except Exception as e:
|
124 |
-
logger.error(f"AI analysis failed: {str(e)}")
|
125 |
-
raise
|
126 |
-
|
127 |
-
def calculate_plagiarism(chunks: list) -> float:
|
128 |
-
if len(chunks) < 2:
|
129 |
-
return 0.0
|
130 |
|
131 |
-
|
132 |
-
similarity_matrix = cosine_similarity(embeddings)
|
133 |
-
np.fill_diagonal(similarity_matrix, 0)
|
134 |
|
135 |
-
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
|
145 |
-
|
146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
|
|
|
|
|
148 |
try:
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
# Process
|
156 |
-
text = extract_text_from_pdf(file_path)
|
157 |
-
if not text.strip():
|
158 |
-
raise HTTPException(400, "Empty PDF content")
|
159 |
|
160 |
-
|
161 |
-
|
162 |
-
**analyze_content(text),
|
163 |
-
"Plagiarism_Score": calculate_plagiarism(chunk_text(text))
|
164 |
-
},
|
165 |
-
"status": "success"
|
166 |
-
}
|
167 |
-
|
168 |
-
except HTTPException:
|
169 |
-
raise
|
170 |
except Exception as e:
|
171 |
-
|
172 |
-
raise HTTPException(500, "Analysis error")
|
173 |
|
174 |
@app.get("/health")
|
175 |
-
async def health_check()
|
176 |
-
return {"status": "
|
177 |
-
|
178 |
-
@app.get("/")
|
179 |
-
async def root():
|
180 |
-
return {"message": "Essay Analysis API - POST PDFs to /analyze"}
|
181 |
|
182 |
|
183 |
# from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
|
|
|
1 |
# app.py: AI Detection and Plagiarism Check API
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
import logging
|
5 |
+
import numpy as np
|
6 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
7 |
from fastapi.responses import JSONResponse
|
8 |
+
from optimum.onnxruntime import ORTModelForSequenceClassification
|
9 |
+
from transformers import AutoTokenizer, pipeline
|
10 |
from sentence_transformers import SentenceTransformer
|
|
|
11 |
from PyPDF2 import PdfReader
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
import tempfile
|
13 |
+
import torch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
# Configuration
|
|
|
|
|
|
|
16 |
MODEL_NAME = "Essay-Grader/roberta-ai-detector-20250401_232702"
|
17 |
+
SENTENCE_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
18 |
+
PLAGIARISM_THRESHOLD = 0.75
|
19 |
+
MAX_TEXT_LENGTH = 2048 # Reduced for CPU efficiency
|
20 |
+
BATCH_SIZE = 4
|
|
|
|
|
|
|
21 |
|
22 |
+
app = FastAPI(title="Essay Analyzer Pro", version="5.0")
|
|
|
|
|
|
|
23 |
|
24 |
+
# Initialize models
|
25 |
+
def load_models():
|
26 |
+
global ai_detector, embedder, tokenizer
|
27 |
|
28 |
+
# Load optimized ONNX model
|
29 |
+
ai_detector = ORTModelForSequenceClassification.from_pretrained(
|
30 |
+
MODEL_NAME,
|
31 |
+
provider="CPUExecutionProvider",
|
32 |
+
file_name="model_optimized.onnx"
|
33 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
|
|
|
|
36 |
|
37 |
+
# Initialize embedding model with CPU optimizations
|
38 |
+
embedder = SentenceTransformer(
|
39 |
+
SENTENCE_MODEL,
|
40 |
+
device="cpu",
|
41 |
+
modules_kwargs={"onnx_execution_provider": "CPUExecutionProvider"}
|
42 |
+
)
|
43 |
+
|
44 |
+
def process_pdf(file: UploadFile) -> str:
|
45 |
+
"""Memory-efficient PDF processing"""
|
46 |
+
with tempfile.NamedTemporaryFile() as tmp:
|
47 |
+
tmp.write(file.file.read())
|
48 |
+
text = " ".join(
|
49 |
+
page.extract_text() or ""
|
50 |
+
for page in PdfReader(tmp.name).pages
|
51 |
+
)
|
52 |
+
return text.strip()
|
53 |
+
|
54 |
+
def analyze_text(text: str) -> dict:
|
55 |
+
"""CPU-optimized analysis pipeline"""
|
56 |
+
start_time = time.time()
|
57 |
|
58 |
+
# Text preprocessing
|
59 |
+
text = text[:5000] # Strict length limit
|
60 |
+
chunks = [text[i:i+512] for i in range(0, len(text), 384)][:8]
|
61 |
+
|
62 |
+
# AI Detection
|
63 |
+
inputs = tokenizer(
|
64 |
+
chunks,
|
65 |
+
padding=True,
|
66 |
+
truncation=True,
|
67 |
+
max_length=512,
|
68 |
+
return_tensors="pt"
|
69 |
+
)
|
70 |
|
71 |
+
outputs = ai_detector(**inputs)
|
72 |
+
probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
|
73 |
+
human = probs[:, 0].mean().item() * 100
|
74 |
+
ai = probs[:, 1].mean().item() * 100
|
75 |
+
|
76 |
+
# Plagiarism Check
|
77 |
+
embeddings = embedder.encode(chunks, batch_size=BATCH_SIZE)
|
78 |
+
similarity = (embeddings @ embeddings.T) > PLAGIARISM_THRESHOLD
|
79 |
+
plagiarism = similarity.mean() * 100
|
80 |
+
|
81 |
+
return {
|
82 |
+
"human_written": round(human, 2),
|
83 |
+
"ai_generated": round(ai, 2),
|
84 |
+
"plagiarism_risk": round(plagiarism, 2),
|
85 |
+
"processing_time": round(time.time() - start_time, 2)
|
86 |
+
}
|
87 |
+
|
88 |
+
@app.on_event("startup")
|
89 |
+
async def startup_event():
|
90 |
+
load_models()
|
91 |
|
92 |
+
@app.post("/analyze")
|
93 |
+
async def analyze(file: UploadFile = File(...)):
|
94 |
try:
|
95 |
+
if not file.filename.lower().endswith(".pdf"):
|
96 |
+
raise HTTPException(400, "Only PDF files accepted")
|
97 |
+
|
98 |
+
text = process_pdf(file)
|
99 |
+
if len(text) < 300:
|
100 |
+
raise HTTPException(400, "Text too short for analysis")
|
|
|
|
|
|
|
|
|
101 |
|
102 |
+
return JSONResponse(analyze_text(text))
|
103 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
except Exception as e:
|
105 |
+
raise HTTPException(500, f"Analysis failed: {str(e)}")
|
|
|
106 |
|
107 |
@app.get("/health")
|
108 |
+
async def health_check():
|
109 |
+
return {"status": "ready", "device": "cpu"}
|
|
|
|
|
|
|
|
|
110 |
|
111 |
|
112 |
# from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
|
convert_model.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from optimum.onnxruntime import ORTModelForSequenceClassification
|
2 |
+
|
3 |
+
# Convert and optimize model
|
4 |
+
model = ORTModelForSequenceClassification.from_pretrained(
|
5 |
+
"Essay-Grader/roberta-ai-detector-20250401_232702",
|
6 |
+
export=True,
|
7 |
+
provider="CPUExecutionProvider"
|
8 |
+
)
|
9 |
+
|
10 |
+
# Save optimized model
|
11 |
+
model.save_pretrained(
|
12 |
+
"./optimized_model",
|
13 |
+
file_name="model_optimized.onnx"
|
14 |
+
)
|
15 |
+
|
16 |
+
|
model_quantizer.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import AutoModelForSequenceClassification
|
2 |
+
from optimum.onnxruntime import ORTOptimizer, ORTModelForSequenceClassification
|
3 |
+
from optimum.onnxruntime.configuration import OptimizationConfig
|
4 |
+
|
5 |
+
model = ORTModelForSequenceClassification.from_pretrained(
|
6 |
+
"Essay-Grader/roberta-ai-detector-20250401_232702",
|
7 |
+
from_transformers=True
|
8 |
+
)
|
9 |
+
|
10 |
+
optimizer = ORTOptimizer.from_pretrained(model)
|
11 |
+
optimization_config = OptimizationConfig(
|
12 |
+
optimization_level=99,
|
13 |
+
enable_transformers_specific_optimizations=True,
|
14 |
+
optimize_for_gpu=True,
|
15 |
+
fp16=True
|
16 |
+
)
|
17 |
+
|
18 |
+
optimizer.optimize(
|
19 |
+
save_dir="./optimized_model",
|
20 |
+
optimization_config=optimization_config
|
21 |
+
)
|
optimized_model/model_optimized.onnx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:dba5166ad9db9ba648c1032ebbd34dcd0d085b50023b839ef5c68ca1db93a563
|
3 |
+
size 4
|
requirements.txt
CHANGED
@@ -1,18 +1,29 @@
|
|
1 |
# requirements.txt
|
2 |
|
3 |
-
fastapi==0.
|
4 |
-
uvicorn==0.
|
5 |
-
transformers==
|
6 |
-
|
7 |
-
torch==2.
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
safetensors==0.4.3
|
13 |
-
huggingface_hub>=0.23.0,<1.0
|
14 |
python-multipart==0.0.9
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# requirements.txt
|
2 |
|
3 |
+
fastapi==0.109.2
|
4 |
+
uvicorn==0.27.1
|
5 |
+
sentence-transformers==2.6.1
|
6 |
+
transformers==4.38.2
|
7 |
+
torch==2.2.1+cpu --extra-index-url https://download.pytorch.org/whl/cpu
|
8 |
+
onnxruntime==1.17.1
|
9 |
+
optimum==1.17.1
|
10 |
+
pypdf2==3.0.1
|
11 |
+
nest-asyncio==1.6.0
|
|
|
|
|
12 |
python-multipart==0.0.9
|
13 |
+
|
14 |
+
# fastapi==0.115.0
|
15 |
+
# uvicorn==0.34.0
|
16 |
+
# transformers==4.41.0
|
17 |
+
# sentence-transformers==2.7.0
|
18 |
+
# torch==2.3.0
|
19 |
+
# scikit-learn==1.4.0
|
20 |
+
# PyPDF2==3.0.1
|
21 |
+
# numpy==1.26.4
|
22 |
+
# requests==2.31.0
|
23 |
+
# safetensors==0.4.3
|
24 |
+
# huggingface_hub>=0.23.0,<1.0
|
25 |
+
# python-multipart==0.0.9
|
26 |
+
# click==8.1.7
|
27 |
+
# accelerate>=0.30.0
|
28 |
+
# bitsandbytes>=0.43.0
|
29 |
+
# protobuf>=4.25.3
|