nixaut-codelabs commited on
Commit
eaee42c
·
verified ·
1 Parent(s): 87f60ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1422 -0
app.py CHANGED
@@ -1,4 +1,1426 @@
1
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
3
  document.getElementById('concurrentRequests').textContent = data.concurrent_requests;
4
  document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
 
1
  import os
2
+ import time
3
+ import threading
4
+ import torch
5
+ import base64
6
+ import io
7
+ import uuid
8
+ import requests
9
+ import numpy as np
10
+ from typing import List, Dict, Any, Optional, Union
11
+ from fastapi import FastAPI, HTTPException, Depends, Request, File, UploadFile
12
+ from fastapi.responses import HTMLResponse, JSONResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+ from fastapi.templating import Jinja2Templates
15
+ from pydantic import BaseModel, Field
16
+ from dotenv import load_dotenv
17
+ from huggingface_hub import snapshot_download
18
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
19
+ from detoxify import Detoxify
20
+ from PIL import Image
21
+ from tensorflow.keras.models import load_model
22
+ import uvicorn
23
+ from datetime import datetime, timedelta
24
+ from collections import defaultdict, deque
25
+ import tiktoken
26
+
27
+ load_dotenv()
28
+
29
+ os.makedirs("templates", exist_ok=True)
30
+ os.makedirs("static", exist_ok=True)
31
+
32
+ MODEL_REPO = "daniel-dona/gemma-3-270m-it"
33
+ LOCAL_DIR = os.path.join(os.getcwd(), "local_model")
34
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
35
+ os.environ.setdefault("OMP_NUM_THREADS", str(os.cpu_count() or 2))
36
+ os.environ.setdefault("MKL_NUM_THREADS", os.environ["OMP_NUM_THREADS"])
37
+ os.environ.setdefault("OMP_PROC_BIND", "TRUE")
38
+ torch.set_num_threads(int(os.environ["OMP_NUM_THREADS"]))
39
+ torch.set_num_interop_threads(1)
40
+ torch.set_float32_matmul_precision("high")
41
+
42
+ app = FastAPI(title="AI Content Moderator API", description="Advanced content moderation API powered by AI")
43
+
44
+ app.mount("/static", StaticFiles(directory="static"), name="static")
45
+ templates = Jinja2Templates(directory="templates")
46
+
47
+ def ensure_local_model(repo_id: str, local_dir: str, tries: int = 3, sleep_s: float = 3.0) -> str:
48
+ os.makedirs(local_dir, exist_ok=True)
49
+ for i in range(tries):
50
+ try:
51
+ snapshot_download(
52
+ repo_id=repo_id,
53
+ local_dir=local_dir,
54
+ local_dir_use_symlinks=False,
55
+ resume_download=True,
56
+ allow_patterns=["*.json", "*.model", "*.safetensors", "*.bin", "*.txt", "*.py"]
57
+ )
58
+ return local_dir
59
+ except Exception:
60
+ if i == tries - 1:
61
+ raise
62
+ time.sleep(sleep_s * (2 ** i))
63
+ return local_dir
64
+
65
+ print("Loading models...")
66
+ model_path = ensure_local_model(MODEL_REPO, LOCAL_DIR)
67
+ tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
68
+
69
+ gemma_chat_template_simplified = (
70
+ "{% for message in messages %}"
71
+ "{% if message['role'] == 'user' %}"
72
+ "{{ '<start_of_turn>user\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
73
+ "{% elif message['role'] == 'assistant' %}"
74
+ "{{ '<start_of_turn>model\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
75
+ "{% endif %}"
76
+ "{% endfor %}"
77
+ "{% if add_generation_prompt %}"
78
+ "{{ '<start_of_turn>model\\n' }}"
79
+ "{% endif %}"
80
+ )
81
+ if tokenizer.chat_template is None:
82
+ tokenizer.chat_template = gemma_chat_template_simplified
83
+
84
+ model = AutoModelForCausalLM.from_pretrained(
85
+ model_path,
86
+ local_files_only=True,
87
+ torch_dtype=torch.float32,
88
+ device_map=None
89
+ )
90
+ model.eval()
91
+
92
+ detoxify_model = Detoxify('multilingual')
93
+
94
+ teachable_machine_url = "https://teachablemachine.withgoogle.com/models/gJOADmf_u/"
95
+ model_url = teachable_machine_url + "model.json"
96
+ weights_url = teachable_machine_url + "weights.bin"
97
+
98
+ model_path = os.path.join(os.getcwd(), "teachable_machine_model")
99
+ os.makedirs(model_path, exist_ok=True)
100
+
101
+ if not os.path.exists(os.path.join(model_path, "model.json")):
102
+ response = requests.get(model_url)
103
+ with open(os.path.join(model_path, "model.json"), "wb") as f:
104
+ f.write(response.content)
105
+
106
+ if not os.path.exists(os.path.join(model_path, "weights.bin")):
107
+ response = requests.get(weights_url)
108
+ with open(os.path.join(model_path, "weights.bin"), "wb") as f:
109
+ f.write(response.content)
110
+
111
+ image_model = load_model(model_path)
112
+
113
+ MODERATION_SYSTEM_PROMPT = (
114
+ "You are a multilingual content moderation classifier. "
115
+ "You MUST respond with exactly one lowercase letter: 's' for safe, 'u' for unsafe. "
116
+ "No explanations, no punctuation, no extra words. "
117
+ "If the message contains hate speech, harassment, sexual content involving minors, "
118
+ "extreme violence, self-harm encouragement, or other unsafe material, respond 'u'. "
119
+ "Otherwise respond 's'."
120
+ )
121
+
122
+ request_durations = deque(maxlen=100)
123
+ request_timestamps = deque(maxlen=1000)
124
+ daily_requests = defaultdict(int)
125
+ daily_tokens = defaultdict(int)
126
+ concurrent_requests = 0
127
+ concurrent_requests_lock = threading.Lock()
128
+ encoding = tiktoken.get_encoding("cl100k_base")
129
+
130
+ def count_tokens(text):
131
+ return len(encoding.encode(text))
132
+
133
+ def track_request_metrics(start_time, tokens_count):
134
+ end_time = time.time()
135
+ duration = end_time - start_time
136
+ request_durations.append(duration)
137
+ request_timestamps.append(datetime.now())
138
+ today = datetime.now().strftime("%Y-%m-%d")
139
+ daily_requests[today] += 1
140
+ daily_tokens[today] += tokens_count
141
+
142
+ def get_performance_metrics():
143
+ global concurrent_requests
144
+ with concurrent_requests_lock:
145
+ current_concurrent = concurrent_requests
146
+
147
+ if not request_durations:
148
+ avg_request_time = 0
149
+ peak_request_time = 0
150
+ else:
151
+ avg_request_time = sum(request_durations) / len(request_durations)
152
+ peak_request_time = max(request_durations)
153
+
154
+ now = datetime.now()
155
+ one_minute_ago = now - timedelta(seconds=60)
156
+ requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
157
+
158
+ today = now.strftime("%Y-%m-%d")
159
+ today_requests = daily_requests.get(today, 0)
160
+ today_tokens = daily_tokens.get(today, 0)
161
+
162
+ last_7_days = []
163
+ for i in range(7):
164
+ date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
165
+ last_7_days.append({
166
+ "date": date,
167
+ "requests": daily_requests.get(date, 0),
168
+ "tokens": daily_tokens.get(date, 0)
169
+ })
170
+
171
+ return {
172
+ "avg_request_time_ms": avg_request_time * 1000,
173
+ "peak_request_time_ms": peak_request_time * 1000,
174
+ "requests_per_minute": requests_last_minute,
175
+ "concurrent_requests": current_concurrent,
176
+ "today_requests": today_requests,
177
+ "today_tokens": today_tokens,
178
+ "last_7_days": last_7_days
179
+ }
180
+
181
+ class TextContent(BaseModel):
182
+ type: str = Field("text", description="Type of content")
183
+ text: str = Field(..., description="Text content")
184
+
185
+ class ImageContent(BaseModel):
186
+ type: str = Field("image", description="Type of content")
187
+ url: Optional[str] = Field(None, description="URL of the image")
188
+ base64: Optional[str] = Field(None, description="Base64 encoded image")
189
+
190
+ class ModerationRequest(BaseModel):
191
+ input: Union[str, List[Union[str, TextContent, ImageContent]]] = Field(..., description="Content to moderate")
192
+ model: Optional[str] = Field("multimodal-moderator", description="Model to use for moderation")
193
+
194
+ class ModerationResponse(BaseModel):
195
+ id: str
196
+ object: str
197
+ created: int
198
+ model: str
199
+ results: List[Dict[str, Any]]
200
+
201
+ def build_prompt(message, max_ctx_tokens=128):
202
+ full_user_message = f"{MODERATION_SYSTEM_PROMPT}\n\nUser input: '{message}'"
203
+ messages = [{"role": "user", "content": full_user_message}]
204
+
205
+ text = tokenizer.apply_chat_template(
206
+ messages,
207
+ tokenize=False,
208
+ add_generation_prompt=True
209
+ )
210
+
211
+ while len(tokenizer(text, add_special_tokens=False).input_ids) > max_ctx_tokens and len(full_user_message) > 100:
212
+ full_user_message = full_user_message[:-50]
213
+ messages[0]['content'] = full_user_message
214
+ text = tokenizer.apply_chat_template(
215
+ messages,
216
+ tokenize=False,
217
+ add_generation_prompt=True
218
+ )
219
+ return text
220
+
221
+ def enforce_s_u(text: str) -> str:
222
+ text_lower = text.strip().lower()
223
+ if "u" in text_lower and "s" not in text_lower:
224
+ return "u"
225
+ if "unsafe" in text_lower:
226
+ return "u"
227
+ return "s"
228
+
229
+ def classify_text_with_gemma(message, max_tokens=3, temperature=0.1, top_p=0.95):
230
+ if not message.strip():
231
+ return {
232
+ "classification": "s",
233
+ "label": "SAFE",
234
+ "description": "Content appears to be safe and appropriate.",
235
+ "tokens_per_second": 0,
236
+ "processing_time": 0
237
+ }
238
+
239
+ text = build_prompt(message)
240
+ inputs = tokenizer([text], return_tensors="pt").to(model.device)
241
+ do_sample = bool(temperature and temperature > 0.0)
242
+ gen_kwargs = dict(
243
+ max_new_tokens=max_tokens,
244
+ do_sample=do_sample,
245
+ top_p=top_p,
246
+ temperature=temperature if do_sample else None,
247
+ use_cache=True,
248
+ eos_token_id=tokenizer.eos_token_id,
249
+ pad_token_id=tokenizer.eos_token_id
250
+ )
251
+
252
+ try:
253
+ streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True, skip_prompt=True)
254
+ except TypeError:
255
+ streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True)
256
+
257
+ thread = threading.Thread(
258
+ target=model.generate,
259
+ kwargs={**inputs, **{k: v for k, v in gen_kwargs.items() if v is not None}, "streamer": streamer}
260
+ )
261
+
262
+ partial_text = ""
263
+ token_count = 0
264
+ start_time = None
265
+
266
+ with torch.inference_mode():
267
+ thread.start()
268
+ try:
269
+ for chunk in streamer:
270
+ if start_time is None:
271
+ start_time = time.time()
272
+ partial_text += chunk
273
+ token_count += 1
274
+ finally:
275
+ thread.join()
276
+
277
+ final_label = enforce_s_u(partial_text)
278
+ end_time = time.time() if start_time else time.time()
279
+ duration = max(1e-6, end_time - start_time)
280
+ tps = token_count / duration if duration > 0 else 0.0
281
+
282
+ if final_label == "s":
283
+ label = "SAFE"
284
+ description = "Content appears to be safe and appropriate."
285
+ else:
286
+ label = "UNSAFE"
287
+ description = "Content may contain inappropriate or harmful material."
288
+
289
+ return {
290
+ "classification": final_label,
291
+ "label": label,
292
+ "description": description,
293
+ "tokens_per_second": tps,
294
+ "processing_time": duration
295
+ }
296
+
297
+ def classify_text_with_detoxify(text):
298
+ predictions = detoxify_model.predict([text])
299
+ single_prediction = {key: value[0] for key, value in predictions.items()}
300
+
301
+ category_keys = [
302
+ "toxicity", "severe_toxicity", "obscene", "threat",
303
+ "insult", "identity_attack", "sexual_explicit"
304
+ ]
305
+
306
+ scores = {}
307
+ for key in category_keys:
308
+ scores[key] = float(single_prediction.get(key, 0.0))
309
+
310
+ threshold = 0.5
311
+ bool_categories = {key: (scores[key] > threshold) for key in category_keys}
312
+ flagged = any(bool_categories.values())
313
+
314
+ return {
315
+ "flagged": flagged,
316
+ "categories": bool_categories,
317
+ "category_scores": scores
318
+ }
319
+
320
+ def classify_image(image_data):
321
+ try:
322
+ img = Image.open(io.BytesIO(image_data)).convert("RGB")
323
+ img = img.resize((224, 224))
324
+ img_array = np.array(img) / 255.0
325
+ img_array = np.expand_dims(img_array, axis=0)
326
+
327
+ predictions = image_model.predict(img_array)
328
+ class_idx = np.argmax(predictions[0])
329
+ classes = ["nothing", "nsfw"]
330
+ class_name = classes[class_idx]
331
+ confidence = float(predictions[0][class_idx])
332
+
333
+ return {
334
+ "classification": "u" if class_name == "nsfw" else "s",
335
+ "label": "NSFW" if class_name == "nsfw" else "SFW",
336
+ "description": "Content may contain inappropriate or harmful material." if class_name == "nsfw" else "Content appears to be safe and appropriate.",
337
+ "confidence": confidence,
338
+ "nsfw_score": confidence if class_name == "nsfw" else 1.0 - confidence
339
+ }
340
+ except Exception as e:
341
+ return {
342
+ "classification": "s",
343
+ "label": "ERROR",
344
+ "description": f"Error processing image: {str(e)}",
345
+ "confidence": 0.0,
346
+ "nsfw_score": 0.0
347
+ }
348
+
349
+ def process_content_item(item):
350
+ if isinstance(item, str):
351
+ gemma_result = classify_text_with_gemma(item)
352
+ detoxify_result = classify_text_with_detoxify(item)
353
+
354
+ flagged = gemma_result["classification"] == "u" or detoxify_result["flagged"]
355
+
356
+ return {
357
+ "flagged": flagged,
358
+ "categories": {
359
+ "hate": flagged,
360
+ "hate/threatening": flagged,
361
+ "harassment": flagged,
362
+ "harassment/threatening": flagged,
363
+ "self-harm": flagged,
364
+ "self-harm/intent": flagged,
365
+ "self-harm/instructions": flagged,
366
+ "sexual": flagged,
367
+ "sexual/minors": flagged,
368
+ "violence": flagged,
369
+ "violence/graphic": flagged,
370
+ "nsfw": detoxify_result["categories"].get("sexual_explicit", False)
371
+ },
372
+ "category_scores": {
373
+ "hate": 0.9 if flagged else 0.1,
374
+ "hate/threatening": 0.9 if flagged else 0.1,
375
+ "harassment": 0.9 if flagged else 0.1,
376
+ "harassment/threatening": 0.9 if flagged else 0.1,
377
+ "self-harm": 0.9 if flagged else 0.1,
378
+ "self-harm/intent": 0.9 if flagged else 0.1,
379
+ "self-harm/instructions": 0.9 if flagged else 0.1,
380
+ "sexual": detoxify_result["category_scores"].get("sexual_explicit", 0.1),
381
+ "sexual/minors": detoxify_result["category_scores"].get("sexual_explicit", 0.1) * 0.9,
382
+ "violence": 0.9 if flagged else 0.1,
383
+ "violence/graphic": 0.9 if flagged else 0.1,
384
+ "nsfw": detoxify_result["category_scores"].get("sexual_explicit", 0.1)
385
+ },
386
+ "text": item
387
+ }
388
+
389
+ elif isinstance(item, dict):
390
+ if item.get("type") == "text":
391
+ gemma_result = classify_text_with_gemma(item.get("text", ""))
392
+ detoxify_result = classify_text_with_detoxify(item.get("text", ""))
393
+
394
+ flagged = gemma_result["classification"] == "u" or detoxify_result["flagged"]
395
+
396
+ return {
397
+ "flagged": flagged,
398
+ "categories": {
399
+ "hate": flagged,
400
+ "hate/threatening": flagged,
401
+ "harassment": flagged,
402
+ "harassment/threatening": flagged,
403
+ "self-harm": flagged,
404
+ "self-harm/intent": flagged,
405
+ "self-harm/instructions": flagged,
406
+ "sexual": flagged,
407
+ "sexual/minors": flagged,
408
+ "violence": flagged,
409
+ "violence/graphic": flagged,
410
+ "nsfw": detoxify_result["categories"].get("sexual_explicit", False)
411
+ },
412
+ "category_scores": {
413
+ "hate": 0.9 if flagged else 0.1,
414
+ "hate/threatening": 0.9 if flagged else 0.1,
415
+ "harassment": 0.9 if flagged else 0.1,
416
+ "harassment/threatening": 0.9 if flagged else 0.1,
417
+ "self-harm": 0.9 if flagged else 0.1,
418
+ "self-harm/intent": 0.9 if flagged else 0.1,
419
+ "self-harm/instructions": 0.9 if flagged else 0.1,
420
+ "sexual": detoxify_result["category_scores"].get("sexual_explicit", 0.1),
421
+ "sexual/minors": detoxify_result["category_scores"].get("sexual_explicit", 0.1) * 0.9,
422
+ "violence": 0.9 if flagged else 0.1,
423
+ "violence/graphic": 0.9 if flagged else 0.1,
424
+ "nsfw": detoxify_result["category_scores"].get("sexual_explicit", 0.1)
425
+ },
426
+ "text": item.get("text", "")
427
+ }
428
+
429
+ elif item.get("type") == "image":
430
+ image_data = None
431
+
432
+ if item.get("url"):
433
+ try:
434
+ response = requests.get(item.get("url"))
435
+ image_data = response.content
436
+ except Exception:
437
+ return {
438
+ "flagged": False,
439
+ "categories": {
440
+ "hate": False,
441
+ "hate/threatening": False,
442
+ "harassment": False,
443
+ "harassment/threatening": False,
444
+ "self-harm": False,
445
+ "self-harm/intent": False,
446
+ "self-harm/instructions": False,
447
+ "sexual": False,
448
+ "sexual/minors": False,
449
+ "violence": False,
450
+ "violence/graphic": False,
451
+ "nsfw": False
452
+ },
453
+ "category_scores": {
454
+ "hate": 0.1,
455
+ "hate/threatening": 0.1,
456
+ "harassment": 0.1,
457
+ "harassment/threatening": 0.1,
458
+ "self-harm": 0.1,
459
+ "self-harm/intent": 0.1,
460
+ "self-harm/instructions": 0.1,
461
+ "sexual": 0.1,
462
+ "sexual/minors": 0.1,
463
+ "violence": 0.1,
464
+ "violence/graphic": 0.1,
465
+ "nsfw": 0.1
466
+ },
467
+ "image_url": item.get("url")
468
+ }
469
+
470
+ elif item.get("base64"):
471
+ try:
472
+ if item.get("base64").startswith("data:image"):
473
+ base64_data = item.get("base64").split(",")[1]
474
+ else:
475
+ base64_data = item.get("base64")
476
+
477
+ image_data = base64.b64decode(base64_data)
478
+ except Exception:
479
+ return {
480
+ "flagged": False,
481
+ "categories": {
482
+ "hate": False,
483
+ "hate/threatening": False,
484
+ "harassment": False,
485
+ "harassment/threatening": False,
486
+ "self-harm": False,
487
+ "self-harm/intent": False,
488
+ "self-harm/instructions": False,
489
+ "sexual": False,
490
+ "sexual/minors": False,
491
+ "violence": False,
492
+ "violence/graphic": False,
493
+ "nsfw": False
494
+ },
495
+ "category_scores": {
496
+ "hate": 0.1,
497
+ "hate/threatening": 0.1,
498
+ "harassment": 0.1,
499
+ "harassment/threatening": 0.1,
500
+ "self-harm": 0.1,
501
+ "self-harm/intent": 0.1,
502
+ "self-harm/instructions": 0.1,
503
+ "sexual": 0.1,
504
+ "sexual/minors": 0.1,
505
+ "violence": 0.1,
506
+ "violence/graphic": 0.1,
507
+ "nsfw": 0.1
508
+ },
509
+ "image_base64": item.get("base64")[:50] + "..." if len(item.get("base64", "")) > 50 else item.get("base64", "")
510
+ }
511
+
512
+ if image_data:
513
+ image_result = classify_image(image_data)
514
+ flagged = image_result["classification"] == "u"
515
+
516
+ return {
517
+ "flagged": flagged,
518
+ "categories": {
519
+ "hate": False,
520
+ "hate/threatening": False,
521
+ "harassment": False,
522
+ "harassment/threatening": False,
523
+ "self-harm": False,
524
+ "self-harm/intent": False,
525
+ "self-harm/instructions": False,
526
+ "sexual": flagged,
527
+ "sexual/minors": flagged,
528
+ "violence": False,
529
+ "violence/graphic": False,
530
+ "nsfw": flagged
531
+ },
532
+ "category_scores": {
533
+ "hate": 0.1,
534
+ "hate/threatening": 0.1,
535
+ "harassment": 0.1,
536
+ "harassment/threatening": 0.1,
537
+ "self-harm": 0.1,
538
+ "self-harm/intent": 0.1,
539
+ "self-harm/instructions": 0.1,
540
+ "sexual": image_result["nsfw_score"],
541
+ "sexual/minors": image_result["nsfw_score"] * 0.9,
542
+ "violence": 0.1,
543
+ "violence/graphic": 0.1,
544
+ "nsfw": image_result["nsfw_score"]
545
+ },
546
+ "image_url": item.get("url"),
547
+ "image_base64": item.get("base64")[:50] + "..." if item.get("base64") and len(item.get("base64", "")) > 50 else item.get("base64", "")
548
+ }
549
+
550
+ return {
551
+ "flagged": False,
552
+ "categories": {
553
+ "hate": False,
554
+ "hate/threatening": False,
555
+ "harassment": False,
556
+ "harassment/threatening": False,
557
+ "self-harm": False,
558
+ "self-harm/intent": False,
559
+ "self-harm/instructions": False,
560
+ "sexual": False,
561
+ "sexual/minors": False,
562
+ "violence": False,
563
+ "violence/graphic": False,
564
+ "nsfw": False
565
+ },
566
+ "category_scores": {
567
+ "hate": 0.1,
568
+ "hate/threatening": 0.1,
569
+ "harassment": 0.1,
570
+ "harassment/threatening": 0.1,
571
+ "self-harm": 0.1,
572
+ "self-harm/intent": 0.1,
573
+ "self-harm/instructions": 0.1,
574
+ "sexual": 0.1,
575
+ "sexual/minors": 0.1,
576
+ "violence": 0.1,
577
+ "violence/graphic": 0.1,
578
+ "nsfw": 0.1
579
+ }
580
+ }
581
+
582
+ def get_api_key(request: Request):
583
+ api_key = request.headers.get("Authorization") or request.query_params.get("api_key")
584
+ if not api_key:
585
+ raise HTTPException(status_code=401, detail="API key required")
586
+
587
+ if api_key.startswith("Bearer "):
588
+ api_key = api_key[7:]
589
+
590
+ env_api_key = os.getenv("API_KEY")
591
+ if not env_api_key or api_key != env_api_key:
592
+ raise HTTPException(status_code=401, detail="Invalid API key")
593
+
594
+ return api_key
595
+
596
+ @app.get("/", response_class=HTMLResponse)
597
+ async def get_home(request: Request):
598
+ return templates.TemplateResponse("index.html", {"request": request})
599
+
600
+ @app.post("/v1/moderations", response_model=ModerationResponse)
601
+ async def moderate_content(
602
+ request: ModerationRequest,
603
+ api_key: str = Depends(get_api_key)
604
+ ):
605
+ global concurrent_requests
606
+ with concurrent_requests_lock:
607
+ concurrent_requests += 1
608
+
609
+ start_time = time.time()
610
+ total_tokens = 0
611
+
612
+ try:
613
+ input_data = request.input
614
+
615
+ if isinstance(input_data, str):
616
+ items = [input_data]
617
+ total_tokens += count_tokens(input_data)
618
+ elif isinstance(input_data, list):
619
+ items = input_data
620
+ for item in items:
621
+ if isinstance(item, str):
622
+ total_tokens += count_tokens(item)
623
+ elif isinstance(item, dict) and item.get("type") == "text":
624
+ total_tokens += count_tokens(item.get("text", ""))
625
+ else:
626
+ raise HTTPException(status_code=400, detail="Invalid input format")
627
+
628
+ if len(items) > 10:
629
+ raise HTTPException(status_code=400, detail="Too many input items. Maximum 10 allowed.")
630
+
631
+ results = []
632
+ for item in items:
633
+ result = process_content_item(item)
634
+ results.append(result)
635
+
636
+ response_data = {
637
+ "id": f"modr_{uuid.uuid4().hex[:24]}",
638
+ "object": "moderation",
639
+ "created": int(time.time()),
640
+ "model": request.model,
641
+ "results": results
642
+ }
643
+
644
+ track_request_metrics(start_time, total_tokens)
645
+ return response_data
646
+
647
+ finally:
648
+ with concurrent_requests_lock:
649
+ concurrent_requests -= 1
650
+
651
+ @app.get("/v1/metrics")
652
+ async def get_metrics(api_key: str = Depends(get_api_key)):
653
+ return get_performance_metrics()
654
+
655
+ with open("templates/index.html", "w") as f:
656
+ f.write("""<!DOCTYPE html>
657
+ <html lang="en">
658
+ <head>
659
+ <meta charset="UTF-8">
660
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
661
+ <title>AI Content Moderator</title>
662
+ <script src="https://cdn.tailwindcss.com"></script>
663
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
664
+ <style>
665
+ @keyframes float {
666
+ 0% { transform: translateY(0px); }
667
+ 50% { transform: translateY(-10px); }
668
+ 100% { transform: translateY(0px); }
669
+ }
670
+ @keyframes pulse-border {
671
+ 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
672
+ 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
673
+ 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
674
+ }
675
+ .float-animation {
676
+ animation: float 3s ease-in-out infinite;
677
+ }
678
+ .pulse-border {
679
+ animation: pulse-border 2s infinite;
680
+ }
681
+ .gradient-bg {
682
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
683
+ }
684
+ .glass-effect {
685
+ background: rgba(255, 255, 255, 0.1);
686
+ backdrop-filter: blur(10px);
687
+ border-radius: 10px;
688
+ border: 1px solid rgba(255, 255, 255, 0.2);
689
+ }
690
+ .safe-gradient {
691
+ background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
692
+ }
693
+ .unsafe-gradient {
694
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
695
+ }
696
+ </style>
697
+ </head>
698
+ <body class="min-h-screen gradient-bg text-white">
699
+ <div class="container mx-auto px-4 py-8">
700
+ <header class="text-center mb-12">
701
+ <div class="inline-block p-4 rounded-full glass-effect float-animation mb-6">
702
+ <i class="fas fa-shield-alt text-5xl text-white"></i>
703
+ </div>
704
+ <h1 class="text-4xl md:text-5xl font-bold mb-4">AI Content Moderator</h1>
705
+ <p class="text-xl text-gray-200 max-w-2xl mx-auto">
706
+ Advanced, multilingual and multimodal content classification tool powered by AI
707
+ </p>
708
+ </header>
709
+ <main class="max-w-6xl mx-auto">
710
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
711
+ <div class="lg:col-span-1">
712
+ <div class="glass-effect p-6 rounded-xl h-full">
713
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
714
+ <i class="fas fa-key mr-2"></i> API Configuration
715
+ </h2>
716
+ <div class="mb-4">
717
+ <label class="block text-sm font-medium mb-2">API Key</label>
718
+ <div class="relative">
719
+ <input type="password" id="apiKey" placeholder="Enter your API key"
720
+ 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">
721
+ <button id="toggleApiKey" class="absolute right-3 top-3 text-gray-300 hover:text-white">
722
+ <i class="fas fa-eye"></i>
723
+ </button>
724
+ </div>
725
+ </div>
726
+ <div class="mb-4">
727
+ <label class="block text-sm font-medium mb-2">Model</label>
728
+ <select id="modelSelect" 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">
729
+ <option value="multimodal-moderator" selected>Multimodal Moderator</option>
730
+ </select>
731
+ </div>
732
+ <div class="mt-6">
733
+ <h3 class="text-lg font-semibold mb-2">API Endpoints</h3>
734
+ <div class="bg-black/20 p-4 rounded-lg text-sm font-mono">
735
+ <div class="mb-2">POST /v1/moderations</div>
736
+ <div>GET /v1/metrics</div>
737
+ </div>
738
+ </div>
739
+ </div>
740
+ </div>
741
+ <div class="lg:col-span-2">
742
+ <div class="glass-effect p-6 rounded-xl">
743
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
744
+ <i class="fas fa-check-circle mr-2"></i> Content Analysis
745
+ </h2>
746
+
747
+ <div class="flex border-b border-white/20 mb-6">
748
+ <button id="textTab" class="px-4 py-2 font-medium border-b-2 border-indigo-400 text-indigo-300 tab-active">
749
+ Text
750
+ </button>
751
+ <button id="imageTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
752
+ Image
753
+ </button>
754
+ <button id="mixedTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
755
+ Mixed Content
756
+ </button>
757
+ </div>
758
+
759
+ <div id="textContent" class="tab-content">
760
+ <div class="mb-6">
761
+ <label class="block text-sm font-medium mb-2">Text to Analyze</label>
762
+ <textarea id="textInput" rows="6" placeholder="Enter any text in any language for content moderation analysis..."
763
+ 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>
764
+ </div>
765
+
766
+ <div class="flex space-x-4">
767
+ <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">
768
+ <i class="fas fa-search mr-2"></i> Analyze Text
769
+ </button>
770
+ <button id="clearTextBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
771
+ <i class="fas fa-trash mr-2"></i> Clear
772
+ </button>
773
+ </div>
774
+ </div>
775
+
776
+ <div id="imageContent" class="tab-content hidden">
777
+ <div class="mb-6">
778
+ <label class="block text-sm font-medium mb-2">Image URL</label>
779
+ <input type="text" id="imageUrl" placeholder="https://example.com/image.jpg"
780
+ 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">
781
+ </div>
782
+
783
+ <div class="mb-6">
784
+ <label class="block text-sm font-medium mb-2">OR Upload Image</label>
785
+ <div class="flex items-center justify-center w-full">
786
+ <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">
787
+ <div class="flex flex-col items-center justify-center pt-5 pb-6">
788
+ <i class="fas fa-cloud-upload-alt text-4xl mb-4"></i>
789
+ <p class="mb-2 text-sm"><span class="font-semibold">Click to upload</span> or drag and drop</p>
790
+ <p class="text-xs">PNG, JPG, GIF up to 10MB</p>
791
+ </div>
792
+ <input id="imageUpload" type="file" class="hidden" accept="image/*" />
793
+ </label>
794
+ </div>
795
+ <div id="imagePreview" class="mt-4 hidden">
796
+ <img id="previewImg" class="max-h-64 mx-auto rounded-lg" />
797
+ </div>
798
+ </div>
799
+
800
+ <div class="flex space-x-4">
801
+ <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">
802
+ <i class="fas fa-search mr-2"></i> Analyze Image
803
+ </button>
804
+ <button id="clearImageBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
805
+ <i class="fas fa-trash mr-2"></i> Clear
806
+ </button>
807
+ </div>
808
+ </div>
809
+
810
+ <div id="mixedContent" class="tab-content hidden">
811
+ <div class="mb-6">
812
+ <label class="block text-sm font-medium mb-2">Content Items</label>
813
+ <div id="mixedItemsContainer">
814
+ <div class="mixed-item mb-4 p-4 rounded-lg bg-white/10">
815
+ <div class="flex justify-between items-center mb-2">
816
+ <select class="item-type bg-transparent border border-white/30 rounded px-2 py-1">
817
+ <option value="text">Text</option>
818
+ <option value="image">Image</option>
819
+ </select>
820
+ <button class="remove-item text-red-400 hover:text-red-300">
821
+ <i class="fas fa-times"></i>
822
+ </button>
823
+ </div>
824
+ <div class="item-content">
825
+ <textarea class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white" rows="3" placeholder="Enter text..."></textarea>
826
+ </div>
827
+ </div>
828
+ </div>
829
+ <button id="addItemBtn" class="mt-2 text-indigo-300 hover:text-indigo-200">
830
+ <i class="fas fa-plus-circle mr-1"></i> Add Item
831
+ </button>
832
+ </div>
833
+
834
+ <div class="flex space-x-4">
835
+ <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">
836
+ <i class="fas fa-search mr-2"></i> Analyze All
837
+ </button>
838
+ <button id="clearMixedBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
839
+ <i class="fas fa-trash mr-2"></i> Clear All
840
+ </button>
841
+ </div>
842
+ </div>
843
+
844
+ <div id="resultsSection" class="mt-8 hidden">
845
+ <h3 class="text-xl font-bold mb-4 flex items-center">
846
+ <i class="fas fa-chart-bar mr-2"></i> Analysis Results
847
+ </h3>
848
+ <div id="resultsContainer" class="space-y-4">
849
+ </div>
850
+ </div>
851
+ </div>
852
+ </div>
853
+ </div>
854
+
855
+ <div class="glass-effect p-6 rounded-xl mb-12">
856
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
857
+ <i class="fas fa-tachometer-alt mr-2"></i> Performance Metrics
858
+ </h2>
859
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
860
+ <div class="bg-white/10 p-4 rounded-lg">
861
+ <div class="text-sm text-gray-300">Avg. Response Time</div>
862
+ <div class="text-2xl font-bold" id="avgResponseTime">0ms</div>
863
+ </div>
864
+ <div class="bg-white/10 p-4 rounded-lg">
865
+ <div class="text-sm text-gray-300">Concurrent Requests</div>
866
+ <div class="text-2xl font-bold" id="concurrentRequests">0</div>
867
+ </div>
868
+ <div class="bg-white/10 p-4 rounded-lg">
869
+ <div class="text-sm text-gray-300">Requests/Minute</div>
870
+ <div class="text-2xl font-bold" id="requestsPerMinute">0</div>
871
+ </div>
872
+ <div class="bg-white/10 p-4 rounded-lg">
873
+ <div class="text-sm text-gray-300">Today's Requests</div>
874
+ <div class="text-2xl font-bold" id="todayRequests">0</div>
875
+ </div>
876
+ </div>
877
+ </div>
878
+
879
+ <div class="glass-effect p-6 rounded-xl mb-12">
880
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
881
+ <i class="fas fa-lightbulb mr-2"></i> Example Prompts
882
+ </h2>
883
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
884
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
885
+ <p class="text-sm">"Hello, how are you today? I hope you're having a wonderful time!"</p>
886
+ </div>
887
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
888
+ <p class="text-sm">"I hate you and I will find you and hurt you badly."</p>
889
+ </div>
890
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
891
+ <p class="text-sm">"C'est une belle journée pour apprendre la programmation et l'intelligence artificielle."</p>
892
+ </div>
893
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
894
+ <p class="text-sm">"I can't take this anymore. I want to end everything and disappear forever."</p>
895
+ </div>
896
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
897
+ <p class="text-sm">"¡Hola! Me encanta aprender nuevos idiomas y conocer diferentes culturas."</p>
898
+ </div>
899
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
900
+ <p class="text-sm">"You're absolutely worthless and nobody will ever love someone like you."</p>
901
+ </div>
902
+ </div>
903
+ </div>
904
+
905
+ <div class="glass-effect p-6 rounded-xl">
906
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
907
+ <i class="fas fa-info-circle mr-2"></i> About This Tool
908
+ </h2>
909
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
910
+ <div class="text-center">
911
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
912
+ <i class="fas fa-globe text-2xl text-indigo-300"></i>
913
+ </div>
914
+ <h3 class="text-lg font-semibold mb-2">Multilingual</h3>
915
+ <p class="text-gray-300">Supports content analysis in multiple languages with high accuracy.</p>
916
+ </div>
917
+ <div class="text-center">
918
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
919
+ <i class="fas fa-image text-2xl text-indigo-300"></i>
920
+ </div>
921
+ <h3 class="text-lg font-semibold mb-2">Multimodal</h3>
922
+ <p class="text-gray-300">Analyzes both text and images for comprehensive content moderation.</p>
923
+ </div>
924
+ <div class="text-center">
925
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
926
+ <i class="fas fa-shield-alt text-2xl text-indigo-300"></i>
927
+ </div>
928
+ <h3 class="text-lg font-semibold mb-2">Secure</h3>
929
+ <p class="text-gray-300">API key authentication ensures your requests remain secure and private.</p>
930
+ </div>
931
+ </div>
932
+ </div>
933
+ </main>
934
+ <footer class="mt-12 text-center text-gray-300">
935
+ <p>© 2023 AI Content Moderator. All rights reserved.</p>
936
+ </footer>
937
+ </div>
938
+
939
+ <div id="loadingModal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
940
+ <div class="glass-effect p-8 rounded-xl max-w-md w-full mx-4 text-center">
941
+ <div class="mb-4">
942
+ <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
943
+ </div>
944
+ <h3 class="text-xl font-bold mb-2">Analyzing Content</h3>
945
+ <p class="text-gray-300">Please wait while we process your request...</p>
946
+ </div>
947
+ </div>
948
+
949
+ <script>
950
+ const textTab = document.getElementById('textTab');
951
+ const imageTab = document.getElementById('imageTab');
952
+ const mixedTab = document.getElementById('mixedTab');
953
+ const textContent = document.getElementById('textContent');
954
+ const imageContent = document.getElementById('imageContent');
955
+ const mixedContent = document.getElementById('mixedContent');
956
+ const apiKeyInput = document.getElementById('apiKey');
957
+ const toggleApiKeyBtn = document.getElementById('toggleApiKey');
958
+ const textInput = document.getElementById('textInput');
959
+ const imageUrl = document.getElementById('imageUrl');
960
+ const imageUpload = document.getElementById('imageUpload');
961
+ const imagePreview = document.getElementById('imagePreview');
962
+ const previewImg = document.getElementById('previewImg');
963
+ const analyzeTextBtn = document.getElementById('analyzeTextBtn');
964
+ const analyzeImageBtn = document.getElementById('analyzeImageBtn');
965
+ const analyzeMixedBtn = document.getElementById('analyzeMixedBtn');
966
+ const clearTextBtn = document.getElementById('clearTextBtn');
967
+ const clearImageBtn = document.getElementById('clearImageBtn');
968
+ const clearMixedBtn = document.getElementById('clearMixedBtn');
969
+ const resultsSection = document.getElementById('resultsSection');
970
+ const resultsContainer = document.getElementById('resultsContainer');
971
+ const loadingModal = document.getElementById('loadingModal');
972
+ const mixedItemsContainer = document.getElementById('mixedItemsContainer');
973
+ const addItemBtn = document.getElementById('addItemBtn');
974
+ const exampleCards = document.querySelectorAll('.example-card');
975
+
976
+ textTab.addEventListener('click', () => {
977
+ textTab.classList.add('border-indigo-400', 'text-indigo-300');
978
+ textTab.classList.remove('border-transparent', 'text-gray-300');
979
+ imageTab.classList.add('border-transparent', 'text-gray-300');
980
+ imageTab.classList.remove('border-indigo-400', 'text-indigo-300');
981
+ mixedTab.classList.add('border-transparent', 'text-gray-300');
982
+ mixedTab.classList.remove('border-indigo-400', 'text-indigo-300');
983
+ textContent.classList.remove('hidden');
984
+ imageContent.classList.add('hidden');
985
+ mixedContent.classList.add('hidden');
986
+ });
987
+
988
+ imageTab.addEventListener('click', () => {
989
+ imageTab.classList.add('border-indigo-400', 'text-indigo-300');
990
+ imageTab.classList.remove('border-transparent', 'text-gray-300');
991
+ textTab.classList.add('border-transparent', 'text-gray-300');
992
+ textTab.classList.remove('border-indigo-400', 'text-indigo-300');
993
+ mixedTab.classList.add('border-transparent', 'text-gray-300');
994
+ mixedTab.classList.remove('border-indigo-400', 'text-indigo-300');
995
+ imageContent.classList.remove('hidden');
996
+ textContent.classList.add('hidden');
997
+ mixedContent.classList.add('hidden');
998
+ });
999
+
1000
+ mixedTab.addEventListener('click', () => {
1001
+ mixedTab.classList.add('border-indigo-400', 'text-indigo-300');
1002
+ mixedTab.classList.remove('border-transparent', 'text-gray-300');
1003
+ textTab.classList.add('border-transparent', 'text-gray-300');
1004
+ textTab.classList.remove('border-indigo-400', 'text-indigo-300');
1005
+ imageTab.classList.add('border-transparent', 'text-gray-300');
1006
+ imageTab.classList.remove('border-indigo-400', 'text-indigo-300');
1007
+ mixedContent.classList.remove('hidden');
1008
+ textContent.classList.add('hidden');
1009
+ imageContent.classList.add('hidden');
1010
+ });
1011
+
1012
+ toggleApiKeyBtn.addEventListener('click', () => {
1013
+ const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
1014
+ apiKeyInput.setAttribute('type', type);
1015
+ toggleApiKeyBtn.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
1016
+ });
1017
+
1018
+ imageUpload.addEventListener('change', (e) => {
1019
+ const file = e.target.files[0];
1020
+ if (file) {
1021
+ const reader = new FileReader();
1022
+ reader.onload = (event) => {
1023
+ previewImg.src = event.target.result;
1024
+ imagePreview.classList.remove('hidden');
1025
+ };
1026
+ reader.readAsDataURL(file);
1027
+ }
1028
+ });
1029
+
1030
+ exampleCards.forEach(card => {
1031
+ card.addEventListener('click', () => {
1032
+ textInput.value = card.querySelector('p').textContent;
1033
+ });
1034
+ });
1035
+
1036
+ clearTextBtn.addEventListener('click', () => {
1037
+ textInput.value = '';
1038
+ resultsSection.classList.add('hidden');
1039
+ });
1040
+
1041
+ clearImageBtn.addEventListener('click', () => {
1042
+ imageUrl.value = '';
1043
+ imageUpload.value = '';
1044
+ imagePreview.classList.add('hidden');
1045
+ resultsSection.classList.add('hidden');
1046
+ });
1047
+
1048
+ clearMixedBtn.addEventListener('click', () => {
1049
+ mixedItemsContainer.innerHTML = '';
1050
+ addMixedItem();
1051
+ resultsSection.classList.add('hidden');
1052
+ });
1053
+
1054
+ addItemBtn.addEventListener('click', addMixedItem);
1055
+
1056
+ function addMixedItem() {
1057
+ if (mixedItemsContainer.children.length >= 10) {
1058
+ showNotification('Maximum 10 items allowed', 'error');
1059
+ return;
1060
+ }
1061
+
1062
+ const itemDiv = document.createElement('div');
1063
+ itemDiv.className = 'mixed-item mb-4 p-4 rounded-lg bg-white/10';
1064
+ itemDiv.innerHTML = `
1065
+ <div class="flex justify-between items-center mb-2">
1066
+ <select class="item-type bg-transparent border border-white/30 rounded px-2 py-1">
1067
+ <option value="text">Text</option>
1068
+ <option value="image">Image</option>
1069
+ </select>
1070
+ <button class="remove-item text-red-400 hover:text-red-300">
1071
+ <i class="fas fa-times"></i>
1072
+ </button>
1073
+ </div>
1074
+ <div class="item-content">
1075
+ <textarea class="w-full px-3 py-2 rounded bg-white/10 border border-white/20 text-white" rows="3" placeholder="Enter text..."></textarea>
1076
+ </div>
1077
+ `;
1078
+
1079
+ mixedItemsContainer.appendChild(itemDiv);
1080
+
1081
+ const typeSelect = itemDiv.querySelector('.item-type');
1082
+ const contentDiv = itemDiv.querySelector('.item-content');
1083
+ const removeBtn = itemDiv.querySelector('.remove-item');
1084
+
1085
+ typeSelect.addEventListener('change', () => {
1086
+ if (typeSelect.value === 'text') {
1087
+ 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>';
1088
+ } else {
1089
+ contentDiv.innerHTML = `
1090
+ <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">
1091
+ <div class="flex items-center justify-center w-full">
1092
+ <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">
1093
+ <div class="flex flex-col items-center justify-center pt-2 pb-3">
1094
+ <i class="fas fa-cloud-upload-alt text-2xl mb-2"></i>
1095
+ <p class="text-xs">Upload image</p>
1096
+ </div>
1097
+ <input type="file" class="hidden" accept="image/*" />
1098
+ </label>
1099
+ </div>
1100
+ <div class="image-preview mt-2 hidden">
1101
+ <img class="max-h-32 mx-auto rounded" />
1102
+ </div>
1103
+ `;
1104
+
1105
+ const fileInput = contentDiv.querySelector('input[type="file"]');
1106
+ const preview = contentDiv.querySelector('.image-preview');
1107
+ const previewImg = contentDiv.querySelector('.image-preview img');
1108
+
1109
+ fileInput.addEventListener('change', (e) => {
1110
+ const file = e.target.files[0];
1111
+ if (file) {
1112
+ const reader = new FileReader();
1113
+ reader.onload = (event) => {
1114
+ previewImg.src = event.target.result;
1115
+ preview.classList.remove('hidden');
1116
+ };
1117
+ reader.readAsDataURL(file);
1118
+ }
1119
+ });
1120
+ }
1121
+ });
1122
+
1123
+ removeBtn.addEventListener('click', () => {
1124
+ itemDiv.remove();
1125
+ updateRemoveButtons();
1126
+ });
1127
+
1128
+ updateRemoveButtons();
1129
+ }
1130
+
1131
+ function updateRemoveButtons() {
1132
+ const items = mixedItemsContainer.querySelectorAll('.mixed-item');
1133
+ items.forEach(item => {
1134
+ const removeBtn = item.querySelector('.remove-item');
1135
+ removeBtn.style.display = items.length > 1 ? 'block' : 'none';
1136
+ });
1137
+ }
1138
+
1139
+ mixedItemsContainer.addEventListener('click', (e) => {
1140
+ if (e.target.closest('.remove-item')) {
1141
+ e.target.closest('.mixed-item').remove();
1142
+ updateRemoveButtons();
1143
+ }
1144
+ });
1145
+
1146
+ analyzeTextBtn.addEventListener('click', async () => {
1147
+ const text = textInput.value.trim();
1148
+ if (!text) {
1149
+ showNotification('Please enter text to analyze', 'error');
1150
+ return;
1151
+ }
1152
+
1153
+ const apiKey = apiKeyInput.value.trim();
1154
+ if (!apiKey) {
1155
+ showNotification('Please enter your API key', 'error');
1156
+ return;
1157
+ }
1158
+
1159
+ showLoading(true);
1160
+ try {
1161
+ const response = await fetch('/v1/moderations', {
1162
+ method: 'POST',
1163
+ headers: {
1164
+ 'Content-Type': 'application/json',
1165
+ 'Authorization': `Bearer ${apiKey}`
1166
+ },
1167
+ body: JSON.stringify({
1168
+ input: text,
1169
+ model: document.getElementById('modelSelect').value
1170
+ })
1171
+ });
1172
+
1173
+ if (!response.ok) {
1174
+ const errorData = await response.json();
1175
+ throw new Error(errorData.detail || 'An error occurred');
1176
+ }
1177
+
1178
+ const data = await response.json();
1179
+ displayResults(data.results);
1180
+ updateMetrics();
1181
+ } catch (error) {
1182
+ showNotification(`Error: ${error.message}`, 'error');
1183
+ } finally {
1184
+ showLoading(false);
1185
+ }
1186
+ });
1187
+
1188
+ analyzeImageBtn.addEventListener('click', async () => {
1189
+ const url = imageUrl.value.trim();
1190
+ const fileInput = document.querySelector('#imageUpload');
1191
+ const file = fileInput.files[0];
1192
+
1193
+ if (!url && !file) {
1194
+ showNotification('Please provide an image URL or upload an image', 'error');
1195
+ return;
1196
+ }
1197
+
1198
+ const apiKey = apiKeyInput.value.trim();
1199
+ if (!apiKey) {
1200
+ showNotification('Please enter your API key', 'error');
1201
+ return;
1202
+ }
1203
+
1204
+ let imageInput;
1205
+
1206
+ if (url) {
1207
+ imageInput = {
1208
+ type: "image",
1209
+ url: url
1210
+ };
1211
+ } else {
1212
+ const reader = new FileReader();
1213
+ const base64Promise = new Promise((resolve) => {
1214
+ reader.onload = (event) => resolve(event.target.result);
1215
+ });
1216
+ reader.readAsDataURL(file);
1217
+ const base64 = await base64Promise;
1218
+
1219
+ imageInput = {
1220
+ type: "image",
1221
+ base64: base64
1222
+ };
1223
+ }
1224
+
1225
+ showLoading(true);
1226
+ try {
1227
+ const response = await fetch('/v1/moderations', {
1228
+ method: 'POST',
1229
+ headers: {
1230
+ 'Content-Type': 'application/json',
1231
+ 'Authorization': `Bearer ${apiKey}`
1232
+ },
1233
+ body: JSON.stringify({
1234
+ input: [imageInput],
1235
+ model: document.getElementById('modelSelect').value
1236
+ })
1237
+ });
1238
+
1239
+ if (!response.ok) {
1240
+ const errorData = await response.json();
1241
+ throw new Error(errorData.detail || 'An error occurred');
1242
+ }
1243
+
1244
+ const data = await response.json();
1245
+ displayResults(data.results);
1246
+ updateMetrics();
1247
+ } catch (error) {
1248
+ showNotification(`Error: ${error.message}`, 'error');
1249
+ } finally {
1250
+ showLoading(false);
1251
+ }
1252
+ });
1253
+
1254
+ analyzeMixedBtn.addEventListener('click', async () => {
1255
+ const items = Array.from(mixedItemsContainer.querySelectorAll('.mixed-item'));
1256
+ if (items.length === 0) {
1257
+ showNotification('Please add at least one item to analyze', 'error');
1258
+ return;
1259
+ }
1260
+
1261
+ const apiKey = apiKeyInput.value.trim();
1262
+ if (!apiKey) {
1263
+ showNotification('Please enter your API key', 'error');
1264
+ return;
1265
+ }
1266
+
1267
+ const inputItems = [];
1268
+
1269
+ for (const item of items) {
1270
+ const type = item.querySelector('.item-type').value;
1271
+ const contentDiv = item.querySelector('.item-content');
1272
+
1273
+ if (type === 'text') {
1274
+ const textarea = contentDiv.querySelector('textarea');
1275
+ const text = textarea.value.trim();
1276
+ if (text) {
1277
+ inputItems.push({
1278
+ type: 'text',
1279
+ text: text
1280
+ });
1281
+ }
1282
+ } else {
1283
+ const urlInput = contentDiv.querySelector('input[type="text"]');
1284
+ const fileInput = contentDiv.querySelector('input[type="file"]');
1285
+ const preview = contentDiv.querySelector('.image-preview');
1286
+ const previewImg = contentDiv.querySelector('.image-preview img');
1287
+
1288
+ const url = urlInput.value.trim();
1289
+ const file = fileInput.files[0];
1290
+
1291
+ if (url) {
1292
+ inputItems.push({
1293
+ type: 'image',
1294
+ url: url
1295
+ });
1296
+ } else if (file || !preview.classList.contains('hidden')) {
1297
+ const imgSrc = previewImg.src;
1298
+ inputItems.push({
1299
+ type: 'image',
1300
+ base64: imgSrc
1301
+ });
1302
+ }
1303
+ }
1304
+ }
1305
+
1306
+ if (inputItems.length === 0) {
1307
+ showNotification('Please add content to at least one item', 'error');
1308
+ return;
1309
+ }
1310
+
1311
+ showLoading(true);
1312
+ try {
1313
+ const response = await fetch('/v1/moderations', {
1314
+ method: 'POST',
1315
+ headers: {
1316
+ 'Content-Type': 'application/json',
1317
+ 'Authorization': `Bearer ${apiKey}`
1318
+ },
1319
+ body: JSON.stringify({
1320
+ input: inputItems,
1321
+ model: document.getElementById('modelSelect').value
1322
+ })
1323
+ });
1324
+
1325
+ if (!response.ok) {
1326
+ const errorData = await response.json();
1327
+ throw new Error(errorData.detail || 'An error occurred');
1328
+ }
1329
+
1330
+ const data = await response.json();
1331
+ displayResults(data.results);
1332
+ updateMetrics();
1333
+ } catch (error) {
1334
+ showNotification(`Error: ${error.message}`, 'error');
1335
+ } finally {
1336
+ showLoading(false);
1337
+ }
1338
+ });
1339
+
1340
+ function displayResults(results) {
1341
+ resultsContainer.innerHTML = '';
1342
+
1343
+ results.forEach((result, index) => {
1344
+ const isFlagged = result.flagged;
1345
+ const cardClass = isFlagged ? 'unsafe-gradient' : 'safe-gradient';
1346
+ const icon = isFlagged ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle';
1347
+ const statusText = isFlagged ? 'UNSAFE' : 'SAFE';
1348
+ const statusDesc = isFlagged ?
1349
+ 'Content may contain inappropriate or harmful material.' :
1350
+ 'Content appears to be safe and appropriate.';
1351
+
1352
+ const categories = Object.entries(result.categories)
1353
+ .filter(([_, value]) => value)
1354
+ .map(([key, _]) => key.replace('/', ' '))
1355
+ .join(', ');
1356
+
1357
+ let contentPreview = '';
1358
+
1359
+ if (result.text) {
1360
+ contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4">
1361
+ <p class="text-sm font-mono break-words">${result.text}</p>
1362
+ </div>`;
1363
+ } else if (result.image_url) {
1364
+ contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4 text-center">
1365
+ <img src="${result.image_url}" class="max-h-48 mx-auto rounded" />
1366
+ </div>`;
1367
+ } else if (result.image_base64) {
1368
+ contentPreview = `<div class="bg-black/20 p-4 rounded-lg mb-4 text-center">
1369
+ <img src="${result.image_base64}" class="max-h-48 mx-auto rounded" />
1370
+ </div>`;
1371
+ }
1372
+
1373
+ const resultCard = document.createElement('div');
1374
+ resultCard.className = `p-6 rounded-xl text-white ${cardClass} shadow-lg`;
1375
+ resultCard.innerHTML = `
1376
+ <div class="flex items-start">
1377
+ <div class="mr-4 mt-1">
1378
+ <i class="${icon} text-3xl"></i>
1379
+ </div>
1380
+ <div class="flex-1">
1381
+ <div class="flex justify-between items-start mb-2">
1382
+ <h3 class="text-xl font-bold">${statusText}</h3>
1383
+ <span class="text-sm bg-black/20 px-2 py-1 rounded">Item ${index + 1}</span>
1384
+ </div>
1385
+ <p class="mb-4">${statusDesc}</p>
1386
+ ${contentPreview}
1387
+ ${isFlagged ? `
1388
+ <div class="mb-3">
1389
+ <h4 class="font-semibold mb-1">Flagged Categories:</h4>
1390
+ <p class="text-sm">${categories}</p>
1391
+ </div>
1392
+ ` : ''}
1393
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
1394
+ ${Object.entries(result.category_scores).map(([category, score]) => `
1395
+ <div class="bg-black/20 p-2 rounded">
1396
+ <div class="font-medium">${category.replace('/', ' ')}</div>
1397
+ <div class="w-full bg-gray-700 rounded-full h-1.5 mt-1">
1398
+ <div class="bg-white h-1.5 rounded-full" style="width: ${score * 100}%"></div>
1399
+ </div>
1400
+ <div class="text-right mt-1">${(score * 100).toFixed(0)}%</div>
1401
+ </div>
1402
+ `).join('')}
1403
+ </div>
1404
+ </div>
1405
+ </div>
1406
+ `;
1407
+
1408
+ resultsContainer.appendChild(resultCard);
1409
+ });
1410
+
1411
+ resultsSection.classList.remove('hidden');
1412
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
1413
+ }
1414
+
1415
+ async function updateMetrics() {
1416
+ const apiKey = apiKeyInput.value.trim() || 'temp-key-for-metrics';
1417
+ try {
1418
+ const response = await fetch('/v1/metrics', {
1419
+ headers: { 'Authorization': 'Bearer ' + apiKey }
1420
+ });
1421
+
1422
+ if (response.ok) {
1423
+ const data = await response.json();
1424
  document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
1425
  document.getElementById('concurrentRequests').textContent = data.concurrent_requests;
1426
  document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;