smart-moderator / app.py
nixaut-codelabs's picture
Update app.py
337a776 verified
import os
import time
import threading
import torch
import base64
import io
import uuid
import requests
import numpy as np
import asyncio
from typing import List, Dict, Any, Optional, Union
from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field
from dotenv import load_dotenv
from huggingface_hub import snapshot_download
from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer, AutoModelForImageClassification, ViTImageProcessor
from detoxify import Detoxify
from PIL import Image
import uvicorn
from datetime import datetime, timedelta
from collections import defaultdict, deque
import tiktoken
load_dotenv()
os.makedirs("templates", exist_ok=True)
os.makedirs("static", exist_ok=True)
MODEL_REPO = "daniel-dona/gemma-3-270m-it"
LOCAL_DIR = os.path.join(os.getcwd(), "local_model")
os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
os.environ.setdefault("OMP_NUM_THREADS", str(os.cpu_count() or 2))
os.environ.setdefault("MKL_NUM_THREADS", os.environ["OMP_NUM_THREADS"])
os.environ.setdefault("OMP_PROC_BIND", "TRUE")
torch.set_num_threads(int(os.environ["OMP_NUM_THREADS"]))
torch.set_num_interop_threads(1)
torch.set_float32_matmul_precision("high")
app = FastAPI(title="Smart Moderator API", description="Advanced content moderation API powered by AI")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
def ensure_local_model(repo_id: str, local_dir: str, tries: int = 3, sleep_s: float = 3.0) -> str:
os.makedirs(local_dir, exist_ok=True)
for i in range(tries):
try:
snapshot_download(
repo_id=repo_id,
local_dir=local_dir,
local_dir_use_symlinks=False,
resume_download=True,
allow_patterns=["*.json", "*.model", "*.safetensors", "*.bin", "*.txt", "*.py"]
)
return local_dir
except Exception:
if i == tries - 1:
raise
time.sleep(sleep_s * (2 ** i))
return local_dir
print("Loading models...")
model_path = ensure_local_model(MODEL_REPO, LOCAL_DIR)
tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
gemma_chat_template_simplified = (
"{% for message in messages %}"
"{% if message['role'] == 'user' %}"
"{{ '<start_of_turn>user\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
"{% elif message['role'] == 'assistant' %}"
"{{ '<start_of_turn>model\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}"
"{{ '<start_of_turn>model\\n' }}"
"{% endif %}"
)
if tokenizer.chat_template is None:
tokenizer.chat_template = gemma_chat_template_simplified
model = AutoModelForCausalLM.from_pretrained(
model_path,
local_files_only=True,
torch_dtype=torch.float32,
device_map=None
)
model.eval()
detoxify_model = Detoxify('multilingual')
print("Loading NSFW image classification model...")
nsfw_model = AutoModelForImageClassification.from_pretrained("Falconsai/nsfw_image_detection")
nsfw_processor = ViTImageProcessor.from_pretrained('Falconsai/nsfw_image_detection')
nsfw_model.eval()
print("NSFW image classification model loaded.")
MODERATION_SYSTEM_PROMPT = (
"You are a multilingual content moderation classifier. "
"You MUST respond with exactly one lowercase letter: 's' for safe, 'u' for unsafe. "
"No explanations, no punctuation, no extra words. "
"If the message contains hate speech, harassment, sexual content involving minors, "
"extreme violence, self-harm encouragement, or other unsafe material, respond 'u'. "
"Otherwise respond 's'."
)
request_durations = deque(maxlen=100)
request_timestamps = deque(maxlen=1000)
daily_requests = defaultdict(int)
daily_tokens = defaultdict(int)
concurrent_requests = 0
concurrent_requests_lock = threading.Lock()
encoding = tiktoken.get_encoding("cl100k_base")
def count_tokens(text):
return len(encoding.encode(text))
def track_request_metrics(start_time, tokens_count):
end_time = time.time()
duration = end_time - start_time
request_durations.append(duration)
request_timestamps.append(datetime.now())
today = datetime.now().strftime("%Y-%m-%d")
daily_requests[today] += 1
daily_tokens[today] += tokens_count
def get_performance_metrics():
global concurrent_requests
with concurrent_requests_lock:
current_concurrent = concurrent_requests
if not request_durations:
avg_request_time = 0
peak_request_time = 0
else:
avg_request_time = sum(request_durations) / len(request_durations)
peak_request_time = max(request_durations)
now = datetime.now()
one_minute_ago = now - timedelta(seconds=60)
requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
today = now.strftime("%Y-%m-%d")
today_requests = daily_requests.get(today, 0)
today_tokens = daily_tokens.get(today, 0)
last_7_days = []
for i in range(7):
date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
last_7_days.append({
"date": date,
"requests": daily_requests.get(date, 0),
"tokens": daily_tokens.get(date, 0)
})
return {
"avg_request_time_ms": avg_request_time * 1000,
"peak_request_time_ms": peak_request_time * 1000,
"requests_per_minute": requests_last_minute,
"concurrent_requests": current_concurrent,
"today_requests": today_requests,
"today_tokens": today_tokens,
"last_7_days": last_7_days
}
class TextContent(BaseModel):
type: str = Field("text", description="Type of content")
text: str = Field(..., description="Text content")
class ImageContent(BaseModel):
type: str = Field("image", description="Type of content")
url: Optional[str] = Field(None, description="URL of the image")
base64: Optional[str] = Field(None, description="Base64 encoded image")
class ModerationRequest(BaseModel):
input: Union[str, List[Union[str, TextContent, ImageContent]]] = Field(..., description="Content to moderate")
model: Optional[str] = Field("gemma", description="Model to use for text moderation (gemma, detoxify, both)")
class ModerationResponse(BaseModel):
id: str
object: str
created: int
model: str
results: List[Dict[str, Any]]
def build_prompt(message, max_ctx_tokens=128):
full_user_message = f"{MODERATION_SYSTEM_PROMPT}\n\nUser input: '{message}'"
messages = [{"role": "user", "content": full_user_message}]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
while len(tokenizer(text, add_special_tokens=False).input_ids) > max_ctx_tokens and len(full_user_message) > 100:
full_user_message = full_user_message[:-50]
messages[0]['content'] = full_user_message
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
return text
def enforce_s_u(text: str) -> str:
text_lower = text.strip().lower()
if "u" in text_lower and "s" not in text_lower:
return "u"
if "unsafe" in text_lower:
return "u"
return "s"
def classify_text_with_gemma(message, max_tokens=3, temperature=0.1, top_p=0.95):
if not message.strip():
return {
"classification": "s",
"label": "SAFE",
"description": "Content appears to be safe and appropriate.",
"tokens_per_second": 0,
"processing_time": 0
}
text = build_prompt(message)
inputs = tokenizer([text], return_tensors="pt").to(model.device)
do_sample = bool(temperature and temperature > 0.0)
gen_kwargs = dict(
max_new_tokens=max_tokens,
do_sample=do_sample,
top_p=top_p,
temperature=temperature if do_sample else None,
use_cache=True,
eos_token_id=tokenizer.eos_token_id,
pad_token_id=tokenizer.eos_token_id
)
try:
streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True, skip_prompt=True)
except TypeError:
streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True)
thread = threading.Thread(
target=model.generate,
kwargs={**inputs, **{k: v for k, v in gen_kwargs.items() if v is not None}, "streamer": streamer}
)
partial_text = ""
token_count = 0
start_time = None
with torch.inference_mode():
thread.start()
try:
for chunk in streamer:
if start_time is None:
start_time = time.time()
partial_text += chunk
token_count += 1
finally:
thread.join()
final_label = enforce_s_u(partial_text)
end_time = time.time() if start_time else time.time()
duration = max(1e-6, end_time - start_time)
tps = token_count / duration if duration > 0 else 0.0
if final_label == "s":
label = "SAFE"
description = "Content appears to be safe and appropriate."
else:
label = "UNSAFE"
description = "Content may contain inappropriate or harmful material."
return {
"classification": final_label,
"label": label,
"description": description,
"tokens_per_second": tps,
"processing_time": duration
}
def classify_text_with_detoxify(text):
predictions = detoxify_model.predict([text])
single_prediction = {key: value[0] for key, value in predictions.items()}
category_keys = [
"toxicity", "severe_toxicity", "obscene", "threat",
"insult", "identity_attack", "sexual_explicit"
]
scores = {}
for key in category_keys:
scores[key] = float(single_prediction.get(key, 0.0))
threshold = 0.5
bool_categories = {key: (scores[key] > threshold) for key in category_keys}
flagged = any(bool_categories.values())
return {
"flagged": flagged,
"categories": bool_categories,
"category_scores": scores
}
def classify_image(image_data):
try:
img = Image.open(io.BytesIO(image_data)).convert("RGB")
with torch.no_grad():
inputs = nsfw_processor(images=img, return_tensors="pt")
inputs = {k: v.to(nsfw_model.device) for k, v in inputs.items()}
outputs = nsfw_model(**inputs)
logits = outputs.logits
predicted_label = logits.argmax(-1).item()
label = nsfw_model.config.id2label[predicted_label]
confidence = torch.softmax(logits, dim=-1)[0][predicted_label].item()
if label.lower() == "nsfw":
classification = "u"
nsfw_score = confidence
else:
classification = "s"
nsfw_score = 1.0 - confidence
return {
"classification": classification,
"label": "NSFW" if classification == 'u' else "SFW",
"description": "Content may contain inappropriate or harmful material." if classification == 'u' else "Content appears to be safe and appropriate.",
"confidence": confidence,
"nsfw_score": nsfw_score
}
except Exception as e:
print(f"Error in classify_image: {str(e)}")
return {
"classification": "s",
"label": "ERROR",
"description": f"Error processing image: {str(e)}",
"confidence": 0.0,
"nsfw_score": 0.0
}
def process_content_item(item: Union[str, TextContent, ImageContent], text_model: str = "gemma") -> Dict:
work_item = {}
if isinstance(item, str):
work_item = {"type": "text", "text": item}
elif isinstance(item, (TextContent, ImageContent)):
work_item = item.model_dump()
else:
# This case should ideally not be hit with proper Pydantic validation
return {
"flagged": False,
"error": f"Unsupported item type: {type(item).__name__}"
}
content_type = work_item.get("type")
if content_type == "text":
text = work_item.get("text", "")
if text_model == "gemma":
gemma_result = classify_text_with_gemma(text)
flagged = gemma_result["classification"] == "u"
scores = {
"hate": 0.9 if flagged else 0.1, "hate/threatening": 0.9 if flagged else 0.1,
"harassment": 0.9 if flagged else 0.1, "harassment/threatening": 0.9 if flagged else 0.1,
"self-harm": 0.9 if flagged else 0.1, "self-harm/intent": 0.9 if flagged else 0.1,
"self-harm/instructions": 0.9 if flagged else 0.1,
"sexual": 0.9 if flagged else 0.1, "sexual/minors": 0.9 if flagged else 0.1,
"violence": 0.9 if flagged else 0.1, "violence/graphic": 0.9 if flagged else 0.1,
"nsfw": 0.1,
}
return {"flagged": flagged, "categories": {k: (v > 0.5) for k, v in scores.items()}, "category_scores": scores, "text": text}
elif text_model == "detoxify":
d = classify_text_with_detoxify(text)
scores = {
"hate": d["category_scores"].get("toxicity", 0.1), "hate/threatening": d["category_scores"].get("threat", 0.1),
"harassment": d["category_scores"].get("insult", 0.1), "harassment/threatening": d["category_scores"].get("threat", 0.1),
"self-harm": 0.1, "self-harm/intent": 0.1, "self-harm/instructions": 0.1,
"sexual": d["category_scores"].get("sexual_explicit", 0.1), "sexual/minors": d["category_scores"].get("sexual_explicit", 0.1),
"violence": d["category_scores"].get("threat", 0.1), "violence/graphic": d["category_scores"].get("threat", 0.1),
"nsfw": d["category_scores"].get("sexual_explicit", 0.1),
}
return {"flagged": d["flagged"], "categories": {k: (v > 0.5) for k, v in scores.items()}, "category_scores": scores, "text": text}
elif text_model == "both":
gemma_result = classify_text_with_gemma(text)
detoxify_result = classify_text_with_detoxify(text)
flagged = gemma_result["classification"] == "u" or detoxify_result["flagged"]
scores = {
"hate": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("toxicity", 0.1)),
"hate/threatening": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("threat", 0.1)),
"harassment": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("insult", 0.1)),
"harassment/threatening": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("threat", 0.1)),
"self-harm": 0.9 if gemma_result["classification"] == "u" else 0.1,
"self-harm/intent": 0.9 if gemma_result["classification"] == "u" else 0.1,
"self-harm/instructions": 0.9 if gemma_result["classification"] == "u" else 0.1,
"sexual": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("sexual_explicit", 0.1)),
"sexual/minors": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("sexual_explicit", 0.1)),
"violence": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("threat", 0.1)),
"violence/graphic": max(0.9 if gemma_result["classification"] == "u" else 0.1, detoxify_result["category_scores"].get("threat", 0.1)),
"nsfw": detoxify_result["category_scores"].get("sexual_explicit", 0.1),
}
return {"flagged": flagged, "categories": {k: (v > 0.5) for k, v in scores.items()}, "category_scores": scores, "text": text}
elif content_type == "image":
image_data = None
image_url = work_item.get("url")
image_base64 = work_item.get("base64")
if image_url:
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
image_data = response.content
except requests.RequestException as e:
print(f"Error fetching image from URL {image_url}: {e}")
elif image_base64:
try:
if image_base64.startswith("data:image"):
image_base64 = image_base64.split(",")[1]
image_data = base64.b64decode(image_base64)
except Exception as e:
print(f"Error decoding base64 image: {e}")
if image_data:
image_result = classify_image(image_data)
flagged = image_result["classification"] == "u"
nsfw_score = image_result.get("nsfw_score", 0.1)
scores = {
"hate": 0.1, "hate/threatening": 0.1,
"harassment": 0.1, "harassment/threatening": 0.1,
"self-harm": 0.1, "self-harm/intent": 0.1, "self-harm/instructions": 0.1,
"sexual": nsfw_score, "sexual/minors": nsfw_score,
"violence": 0.1, "violence/graphic": 0.1,
"nsfw": nsfw_score,
}
return {
"flagged": flagged,
"categories": {k: (v > 0.5) for k, v in scores.items()},
"category_scores": scores,
"image_url": image_url,
"image_base64": work_item.get("base64"),
}
default_scores = {
"hate": 0.1, "hate/threatening": 0.1, "harassment": 0.1, "harassment/threatening": 0.1,
"self-harm": 0.1, "self-harm/intent": 0.1, "self-harm/instructions": 0.1,
"sexual": 0.1, "sexual/minors": 0.1, "violence": 0.1, "violence/graphic": 0.1,
"nsfw": 0.1
}
return {
"flagged": False,
"categories": {k: False for k in default_scores},
"category_scores": default_scores,
"error": f"Invalid or unprocessable item: {work_item}"
}
def get_api_key(request: Request):
api_key = request.headers.get("Authorization") or request.query_params.get("api_key")
if not api_key:
raise HTTPException(status_code=401, detail="API key required")
if api_key.startswith("Bearer "):
api_key = api_key[7:]
env_api_key = os.getenv("API_KEY")
if not env_api_key or api_key != env_api_key:
raise HTTPException(status_code=401, detail="Invalid API key")
return api_key
@app.get("/", response_class=HTMLResponse)
async def get_home(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/v1/moderations", response_model=ModerationResponse)
async def moderate_content(
request: ModerationRequest,
api_key: str = Depends(get_api_key)
):
global concurrent_requests
with concurrent_requests_lock:
concurrent_requests += 1
start_time = time.time()
total_tokens = 0
try:
input_data = request.input
text_model = request.model or "gemma"
items = []
if isinstance(input_data, str):
items.append(input_data)
total_tokens += count_tokens(input_data)
elif isinstance(input_data, list):
items.extend(input_data)
for item in items:
if isinstance(item, str):
total_tokens += count_tokens(item)
elif isinstance(item, TextContent):
total_tokens += count_tokens(item.text)
else:
raise HTTPException(status_code=400, detail="Invalid input format")
if len(items) > 10:
raise HTTPException(status_code=400, detail="Too many input items. Maximum 10 allowed.")
tasks = [run_in_threadpool(process_content_item, item, text_model) for item in items]
results = await asyncio.gather(*tasks)
response_data = {
"id": f"modr_{uuid.uuid4().hex[:24]}",
"object": "moderation",
"created": int(time.time()),
"model": text_model,
"results": list(results)
}
track_request_metrics(start_time, total_tokens)
return response_data
finally:
with concurrent_requests_lock:
concurrent_requests -= 1
@app.get("/v1/metrics")
async def get_metrics(api_key: str = Depends(get_api_key)):
return get_performance_metrics()
with open("templates/index.html", "w", encoding='utf-8') as f:
f.write("""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Moderator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
100% { transform: translateY(0px); }
}
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
.pulse-border {
animation: pulse-border 2s infinite;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.safe-gradient {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
}
.unsafe-gradient {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
</style>
</head>
<body class="min-h-screen gradient-bg text-white">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-12">
<div class="inline-block p-4 rounded-full glass-effect float-animation mb-6">
<i class="fas fa-shield-alt text-5xl text-white"></i>
</div>
<h1 class="text-4xl md:text-5xl font-bold mb-4">Smart Moderator</h1>
<p class="text-xl text-gray-200 max-w-2xl mx-auto">
Advanced, multilingual and multimodal content classification tool powered by AI
</p>
</header>
<main class="max-w-6xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
<div class="lg:col-span-1">
<div class="glass-effect p-6 rounded-xl h-full">
<h2 class="text-2xl font-bold mb-4 flex items-center">
<i class="fas fa-key mr-2"></i> API Configuration
</h2>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">API Key</label>
<div class="relative">
<input type="password" id="apiKey" placeholder="Enter your API key"
class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white">
<button id="toggleApiKey" class="absolute right-3 top-3 text-gray-300 hover:text-white">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Text Model</label>
<select id="textModelSelect" class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white">
<option value="gemma">Gemma (Google)</option>
<option value="detoxify">Detoxify (Unitary AI)</option>
<option value="both">Both (Most Accurate)</option>
</select>
</div>
<div class="mt-6">
<h3 class="text-lg font-semibold mb-2">API Endpoints</h3>
<div class="bg-black/20 p-4 rounded-lg text-sm font-mono">
<div class="mb-2">POST /v1/moderations</div>
<div>GET /v1/metrics</div>
</div>
</div>
</div>
</div>
<div class="lg:col-span-2">
<div class="glass-effect p-6 rounded-xl">
<h2 class="text-2xl font-bold mb-4 flex items-center">
<i class="fas fa-check-circle mr-2"></i> Content Analysis
</h2>
<div class="flex border-b border-white/20 mb-6">
<button id="textTab" class="px-4 py-2 font-medium border-b-2 border-indigo-400 text-indigo-300 tab-active">
Text
</button>
<button id="imageTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
Image
</button>
<button id="mixedTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
Mixed Content
</button>
</div>
<div id="textContent" class="tab-content">
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Text to Analyze</label>
<textarea id="textInput" rows="6" placeholder="Enter any text in any language for content moderation analysis..."
class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white resize-none"></textarea>
</div>
<div class="flex space-x-4">
<button id="analyzeTextBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105 pulse-border">
<i class="fas fa-search mr-2"></i> Analyze Text
</button>
<button id="clearTextBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
<i class="fas fa-trash mr-2"></i> Clear
</button>
</div>
</div>
<div id="imageContent" class="tab-content hidden">
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Image URL</label>
<input type="text" id="imageUrl" placeholder="https://example.com/image.jpg"
class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white">
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">OR Upload Image</label>
<div class="flex items-center justify-center w-full">
<label for="imageUpload" class="flex flex-col items-center justify-center w-full h-64 border-2 border-white/30 border-dashed rounded-lg cursor-pointer bg-white/5 hover:bg-white/10">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i class="fas fa-cloud-upload-alt text-4xl mb-4"></i>
<p class="mb-2 text-sm"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs">PNG, JPG, GIF up to 10MB</p>
</div>
<input id="imageUpload" type="file" class="hidden" accept="image/*" />
</label>
</div>
<div id="imagePreview" class="mt-4 hidden">
<img id="previewImg" class="max-h-64 mx-auto rounded-lg" />
</div>
</div>
<div class="flex space-x-4">
<button id="analyzeImageBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105 pulse-border">
<i class="fas fa-search mr-2"></i> Analyze Image
</button>
<button id="clearImageBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
<i class="fas fa-trash mr-2"></i> Clear
</button>
</div>
</div>
<div id="mixedContent" class="tab-content hidden">
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Content Items</label>
<div id="mixedItemsContainer">
<div class="mixed-item mb-4 p-4 rounded-lg bg-white/10">
<div class="flex justify-between items-center mb-2">
<select class="item-type bg-transparent border border-white/30 rounded px-2 py-1">
<option value="text">Text</option>
<option value="image">Image</option>
</select>
<button class="remove-item text-red-400 hover:text-red-300">
<i class="fas fa-times"></i>
</button>
</div>
<div class="item-content">
<textarea class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white" rows="3" placeholder="Enter text..."></textarea>
</div>
</div>
</div>
<button id="addItemBtn" class="mt-2 text-indigo-300 hover:text-indigo-200">
<i class="fas fa-plus-circle mr-1"></i> Add Item
</button>
</div>
<div class="flex space-x-4">
<button id="analyzeMixedBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105 pulse-border">
<i class="fas fa-search mr-2"></i> Analyze All
</button>
<button id="clearMixedBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
<i class="fas fa-trash mr-2"></i> Clear All
</button>
</div>
</div>
<div id="resultsSection" class="mt-8 hidden">
<h3 class="text-xl font-bold mb-4 flex items-center">
<i class="fas fa-chart-bar mr-2"></i> Analysis Results
</h3>
<div id="resultsContainer" class="space-y-4">
</div>
</div>
</div>
</div>
</div>
<div class="glass-effect p-6 rounded-xl mb-12">
<h2 class="text-2xl font-bold mb-4 flex items-center">
<i class="fas fa-tachometer-alt mr-2"></i> Performance Metrics
</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-white/10 p-4 rounded-lg">
<div class="text-sm text-gray-300">Avg. Response Time</div>
<div class="text-2xl font-bold" id="avgResponseTime">0ms</div>
</div>
<div class="bg-white/10 p-4 rounded-lg">
<div class="text-sm text-gray-300">Concurrent Requests</div>
<div class="text-2xl font-bold" id="concurrentRequests">0</div>
</div>
<div class="bg-white/10 p-4 rounded-lg">
<div class="text-sm text-gray-300">Requests/Minute</div>
<div class="text-2xl font-bold" id="requestsPerMinute">0</div>
</div>
<div class="bg-white/10 p-4 rounded-lg">
<div class="text-sm text-gray-300">Today's Requests</div>
<div class="text-2xl font-bold" id="todayRequests">0</div>
</div>
</div>
</div>
<div class="glass-effect p-6 rounded-xl mb-12">
<h2 class="text-2xl font-bold mb-4 flex items-center">
<i class="fas fa-lightbulb mr-2"></i> Example Prompts
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"Hello, how are you today? I hope you're having a wonderful time!"</p>
</div>
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"I hate you and I will find you and hurt you badly."</p>
</div>
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"C'est une belle journée pour apprendre la programmation et l'intelligence artificielle."</p>
</div>
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"I can't take this anymore. I want to end everything and disappear forever."</p>
</div>
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"¡Hola! Me encanta aprender nuevos idiomas y conocer diferentes culturas."</p>
</div>
<div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
<p class="text-sm">"You're absolutely worthless and nobody will ever love someone like you."</p>
</div>
</div>
</div>
<div class="glass-effect p-6 rounded-xl">
<h2 class="text-2xl font-bold mb-4 flex items-center">
<i class="fas fa-info-circle mr-2"></i> About This Tool
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
<i class="fas fa-globe text-2xl text-indigo-300"></i>
</div>
<h3 class="text-lg font-semibold mb-2">Multilingual</h3>
<p class="text-gray-300">Supports content analysis in multiple languages with high accuracy.</p>
</div>
<div class="text-center">
<div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
<i class="fas fa-image text-2xl text-indigo-300"></i>
</div>
<h3 class="text-lg font-semibold mb-2">Multimodal</h3>
<p class="text-gray-300">Analyzes both text and images for comprehensive content moderation. (Falcons.AI for multimodal model)</p>
</div>
<div class="text-center">
<div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
<i class="fas fa-shield-alt text-2xl text-indigo-300"></i>
</div>
<h3 class="text-lg font-semibold mb-2">Secure</h3>
<p class="text-gray-300">API key authentication ensures your requests remain secure and private.</p>
</div>
</div>
</div>
</main>
<footer class="mt-12 text-center text-gray-300">
<p>© 2025 Smart Moderator. All rights reserved.</p>
</footer>
</div>
<div id="loadingModal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
<div class="glass-effect p-8 rounded-xl max-w-md w-full mx-4 text-center">
<div class="mb-4">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
</div>
<h3 class="text-xl font-bold mb-2">Analyzing Content</h3>
<p class="text-gray-300">Please wait while we process your request...</p>
</div>
</div>
<script>
const textTab = document.getElementById('textTab');
const imageTab = document.getElementById('imageTab');
const mixedTab = document.getElementById('mixedTab');
const textContent = document.getElementById('textContent');
const imageContent = document.getElementById('imageContent');
const mixedContent = document.getElementById('mixedContent');
const apiKeyInput = document.getElementById('apiKey');
const toggleApiKeyBtn = document.getElementById('toggleApiKey');
const textInput = document.getElementById('textInput');
const imageUrl = document.getElementById('imageUrl');
const imageUpload = document.getElementById('imageUpload');
const imagePreview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
const analyzeTextBtn = document.getElementById('analyzeTextBtn');
const analyzeImageBtn = document.getElementById('analyzeImageBtn');
const analyzeMixedBtn = document.getElementById('analyzeMixedBtn');
const clearTextBtn = document.getElementById('clearTextBtn');
const clearImageBtn = document.getElementById('clearImageBtn');
const clearMixedBtn = document.getElementById('clearMixedBtn');
const resultsSection = document.getElementById('resultsSection');
const resultsContainer = document.getElementById('resultsContainer');
const loadingModal = document.getElementById('loadingModal');
const mixedItemsContainer = document.getElementById('mixedItemsContainer');
const addItemBtn = document.getElementById('addItemBtn');
const textModelSelect = document.getElementById('textModelSelect');
const exampleCards = document.querySelectorAll('.example-card');
textTab.addEventListener('click', () => {
textTab.classList.add('border-indigo-400', 'text-indigo-300');
textTab.classList.remove('border-transparent', 'text-gray-300');
imageTab.classList.add('border-transparent', 'text-gray-300');
imageTab.classList.remove('border-indigo-400', 'text-indigo-300');
mixedTab.classList.add('border-transparent', 'text-gray-300');
mixedTab.classList.remove('border-indigo-400', 'text-indigo-300');
textContent.classList.remove('hidden');
imageContent.classList.add('hidden');
mixedContent.classList.add('hidden');
});
imageTab.addEventListener('click', () => {
imageTab.classList.add('border-indigo-400', 'text-indigo-300');
imageTab.classList.remove('border-transparent', 'text-gray-300');
textTab.classList.add('border-transparent', 'text-gray-300');
textTab.classList.remove('border-indigo-400', 'text-indigo-300');
mixedTab.classList.add('border-transparent', 'text-gray-300');
mixedTab.classList.remove('border-indigo-400', 'text-indigo-300');
imageContent.classList.remove('hidden');
textContent.classList.add('hidden');
mixedContent.classList.add('hidden');
});
mixedTab.addEventListener('click', () => {
mixedTab.classList.add('border-indigo-400', 'text-indigo-300');
mixedTab.classList.remove('border-transparent', 'text-gray-300');
textTab.classList.add('border-transparent', 'text-gray-300');
textTab.classList.remove('border-indigo-400', 'text-indigo-300');
imageTab.classList.add('border-transparent', 'text-gray-300');
imageTab.classList.remove('border-indigo-400', 'text-indigo-300');
mixedContent.classList.remove('hidden');
textContent.classList.add('hidden');
imageContent.classList.add('hidden');
});
toggleApiKeyBtn.addEventListener('click', () => {
const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
apiKeyInput.setAttribute('type', type);
toggleApiKeyBtn.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
});
imageUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
previewImg.src = event.target.result;
imagePreview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
});
exampleCards.forEach(card => {
card.addEventListener('click', () => {
textInput.value = card.querySelector('p').textContent;
});
});
clearTextBtn.addEventListener('click', () => {
textInput.value = '';
resultsSection.classList.add('hidden');
});
clearImageBtn.addEventListener('click', () => {
imageUrl.value = '';
imageUpload.value = '';
imagePreview.classList.add('hidden');
resultsSection.classList.add('hidden');
});
clearMixedBtn.addEventListener('click', () => {
mixedItemsContainer.innerHTML = '';
addMixedItem();
resultsSection.classList.add('hidden');
});
addItemBtn.addEventListener('click', addMixedItem);
function addMixedItem() {
if (mixedItemsContainer.children.length >= 10) {
showNotification('Maximum 10 items allowed', 'error');
return;
}
const itemDiv = document.createElement('div');
itemDiv.className = 'mixed-item mb-4 p-4 rounded-lg bg-white/10';
itemDiv.innerHTML = `
<div class="flex justify-between items-center mb-2">
<select class="item-type bg-transparent border border-white/30 rounded px-2 py-1">
<option value="text">Text</option>
<option value="image">Image</option>
</select>
<button class="remove-item text-red-400 hover:text-red-300">
<i class="fas fa-times"></i>
</button>
</div>
<div class="item-content">
<textarea class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white" rows="3" placeholder="Enter text..."></textarea>
</div>
`;
mixedItemsContainer.appendChild(itemDiv);
const typeSelect = itemDiv.querySelector('.item-type');
const contentDiv = itemDiv.querySelector('.item-content');
const removeBtn = itemDiv.querySelector('.remove-item');
typeSelect.addEventListener('change', () => {
if (typeSelect.value === 'text') {
contentDiv.innerHTML = '<textarea class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white" rows="3" placeholder="Enter text..."></textarea>';
} else {
contentDiv.innerHTML = `
<input type="text" class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white mb-2" placeholder="Image URL or leave empty to upload">
<div class="flex items-center justify-center w-full">
<label class="flex flex-col items-center justify-center w-full h-32 border-2 border-white/30 border-dashed rounded-lg cursor-pointer bg-white/5 hover:bg-white/10">
<div class="flex flex-col items-center justify-center pt-2 pb-3">
<i class="fas fa-cloud-upload-alt text-2xl mb-2"></i>
<p class="text-xs">Upload image</p>
</div>
<input type="file" class="hidden" accept="image/*" />
</label>
</div>
<div class="image-preview mt-2 hidden">
<img class="max-h-32 mx-auto rounded" />
</div>
`;
const fileInput = contentDiv.querySelector('input[type="file"]');
const preview = contentDiv.querySelector('.image-preview');
const previewImg = contentDiv.querySelector('.image-preview img');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
previewImg.src = event.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
});
}
});
removeBtn.addEventListener('click', () => {
itemDiv.remove();
updateRemoveButtons();
});
updateRemoveButtons();
}
function updateRemoveButtons() {
const items = mixedItemsContainer.querySelectorAll('.mixed-item');
items.forEach(item => {
const removeBtn = item.querySelector('.remove-item');
removeBtn.style.display = items.length > 1 ? 'block' : 'none';
});
}
mixedItemsContainer.addEventListener('click', (e) => {
if (e.target.closest('.remove-item')) {
e.target.closest('.mixed-item').remove();
updateRemoveButtons();
}
});
async function analyze(payload) {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
showNotification('Please enter your API key', 'error');
return;
}
showLoading(true);
try {
const response = await fetch('/v1/moderations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayResults(data.results);
updateMetrics();
} catch (error) {
showNotification(`Error: ${error.message}`, 'error');
} finally {
showLoading(false);
}
}
analyzeTextBtn.addEventListener('click', async () => {
const text = textInput.value.trim();
if (!text) {
showNotification('Please enter text to analyze', 'error');
return;
}
await analyze({
input: text,
model: textModelSelect.value
});
});
analyzeImageBtn.addEventListener('click', async () => {
const url = imageUrl.value.trim();
const file = imageUpload.files[0];
if (!url && !file) {
showNotification('Please provide an image URL or upload an image', 'error');
return;
}
let imageInput;
if (url) {
imageInput = { type: "image", url: url };
} else {
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.readAsDataURL(file);
});
imageInput = { type: "image", base64: base64 };
}
await analyze({
input: [imageInput],
model: textModelSelect.value
});
});
analyzeMixedBtn.addEventListener('click', async () => {
const items = Array.from(mixedItemsContainer.querySelectorAll('.mixed-item'));
if (items.length === 0) {
showNotification('Please add at least one item to analyze', 'error');
return;
}
const inputPromises = items.map(async (item) => {
const type = item.querySelector('.item-type').value;
const contentDiv = item.querySelector('.item-content');
if (type === 'text') {
const text = contentDiv.querySelector('textarea').value.trim();
return text ? { type: 'text', text: text } : null;
} else {
const url = contentDiv.querySelector('input[type="text"]').value.trim();
const file = contentDiv.querySelector('input[type="file"]').files[0];
if (url) {
return { type: 'image', url: url };
} else if (file) {
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.readAsDataURL(file);
});
return { type: 'image', base64: base64 };
}
return null;
}
});
const inputItems = (await Promise.all(inputPromises)).filter(Boolean);
if (inputItems.length === 0) {
showNotification('Please add content to at least one item', 'error');
return;
}
await analyze({
input: inputItems,
model: textModelSelect.value
});
});
function displayResults(results) {
resultsContainer.innerHTML = '';
results.forEach((result, index) => {
const isFlagged = result.flagged;
const cardClass = isFlagged ? 'unsafe-gradient' : 'safe-gradient';
const icon = isFlagged ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle';
const statusText = isFlagged ? 'UNSAFE' : 'SAFE';
const statusDesc = isFlagged ?
'Content may contain inappropriate or harmful material.' :
'Content appears to be safe and appropriate.';
const categories = result.categories ? Object.entries(result.categories)
.filter(([_, value]) => value)
.map(([key, _]) => key.replace('/', ' '))
.join(', ') : 'N/A';
let contentPreview = '';
if (result.text) {
contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4">
<p class="text-sm font-mono break-words">${result.text}</p>
</div>`;
} else if (result.image_url) {
contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4 text-center">
<img src="${result.image_url}" class="max-h-48 mx-auto rounded" />
</div>`;
} else if (result.image_base64) {
contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4 text-center">
<img src="${result.image_base64}" class="max-h-48 mx-auto rounded" />
</div>`;
}
const resultCard = document.createElement('div');
resultCard.className = `p-6 rounded-xl text-white ${cardClass} shadow-lg`;
resultCard.innerHTML = `
<div class="flex items-start">
<div class="mr-4 mt-1">
<i class="${icon} text-3xl"></i>
</div>
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<h3 class="text-xl font-bold">${statusText}</h3>
<span class="text-sm bg-black/20 px-2 py-1 rounded">Item ${index + 1}</span>
</div>
<p class="mb-4">${statusDesc}</p>
${contentPreview}
${isFlagged ? `
<div class="mb-3">
<h4 class="font-semibold mb-1">Flagged Categories:</h4>
<p class="text-sm">${categories}</p>
</div>
` : ''}
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
${result.category_scores ? Object.entries(result.category_scores).map(([category, score]) => `
<div class="bg-black/20 p-2 rounded">
<div class="font-medium">${category.replace('/', ' ')}</div>
<div class="w-full bg-gray-700 rounded-full h-1.5 mt-1">
<div class="bg-white h-1.5 rounded-full" style="width: ${score * 100}%"></div>
</div>
<div class="text-right mt-1">${(score * 100).toFixed(0)}%</div>
</div>
`).join('') : ''}
</div>
</div>
</div>
`;
resultsContainer.appendChild(resultCard);
});
resultsSection.classList.remove('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
async function updateMetrics() {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) return;
try {
const response = await fetch('/v1/metrics', {
headers: { 'Authorization': 'Bearer ' + apiKey }
});
if (response.ok) {
const data = await response.json();
document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
document.getElementById('concurrentRequests').textContent = data.concurrent_requests;
document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
document.getElementById('todayRequests').textContent = data.today_requests;
}
} catch (error) {
console.error('Error updating metrics:', error);
}
}
function showLoading(show) {
loadingModal.style.display = show ? 'flex' : 'none';
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
type === 'error' ? 'bg-red-500' : 'bg-indigo-500'
} text-white`;
notification.innerHTML = `<div class="flex items-center"><i class="fas ${type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i><span>${message}</span></div>`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.5s';
setTimeout(() => {
document.body.removeChild(notification);
}, 500);
}, 3000);
}
document.addEventListener('DOMContentLoaded', () => {
addMixedItem();
updateMetrics();
setInterval(updateMetrics, 30000);
});
</script>
</body>
</html>""")
print("Initializing models...")
with torch.inference_mode():
_ = model.generate(
**tokenizer(["Hello"], return_tensors="pt").to(model.device),
max_new_tokens=1, do_sample=False, use_cache=True
)
print("🚀 Starting AI Content Moderator API...")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860)