Spaces:
Sleeping
Sleeping
| 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 | |
| async def get_home(request: Request): | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| 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 | |
| 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) |