nixaut-codelabs commited on
Commit
478bcd0
·
verified ·
1 Parent(s): 769144f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -803
app.py CHANGED
@@ -1,870 +1,470 @@
1
  import os
 
2
  import time
3
  import threading
4
- import torch
5
- import json
6
- from typing import List, Dict, Any, Optional
7
- from fastapi import FastAPI, HTTPException, Depends, Request
8
- from fastapi.responses import HTMLResponse, JSONResponse
9
- from fastapi.staticfiles import StaticFiles
10
- from fastapi.templating import Jinja2Templates
11
- from pydantic import BaseModel, Field
12
- from dotenv import load_dotenv
13
- from huggingface_hub import snapshot_download
14
- from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
15
- import uvicorn
16
-
17
- # Load environment variables
18
- load_dotenv()
19
-
20
- # Create necessary directories
21
- os.makedirs("templates", exist_ok=True)
22
- os.makedirs("static", exist_ok=True)
23
-
24
- # Model configuration
25
- MODEL_REPO = "daniel-dona/gemma-3-270m-it"
26
- LOCAL_DIR = os.path.join(os.getcwd(), "local_model")
27
- os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
28
- os.environ.setdefault("OMP_NUM_THREADS", str(os.cpu_count() or 2))
29
- os.environ.setdefault("MKL_NUM_THREADS", os.environ["OMP_NUM_THREADS"])
30
- os.environ.setdefault("OMP_PROC_BIND", "TRUE")
31
- torch.set_num_threads(int(os.environ["OMP_NUM_THREADS"]))
32
- torch.set_num_interop_threads(1)
33
- torch.set_float32_matmul_precision("high")
34
-
35
- # Initialize FastAPI app
36
- app = FastAPI(title="AI Content Moderator API", description="Advanced content moderation API powered by AI")
37
-
38
- # Mount static files and templates
39
- app.mount("/static", StaticFiles(directory="static"), name="static")
40
- templates = Jinja2Templates(directory="templates")
41
-
42
- # Ensure model is downloaded
43
- def ensure_local_model(repo_id: str, local_dir: str, tries: int = 3, sleep_s: float = 3.0) -> str:
44
- os.makedirs(local_dir, exist_ok=True)
45
- for i in range(tries):
46
- try:
47
- snapshot_download(
48
- repo_id=repo_id,
49
- local_dir=local_dir,
50
- local_dir_use_symlinks=False,
51
- resume_download=True,
52
- allow_patterns=["*.json", "*.model", "*.safetensors", "*.bin", "*.txt", "*.py"]
53
- )
54
- return local_dir
55
- except Exception:
56
- if i == tries - 1:
57
- raise
58
- time.sleep(sleep_s * (2 ** i))
59
- return local_dir
60
-
61
- # Load model and tokenizer
62
- print("Loading model...")
63
- model_path = ensure_local_model(MODEL_REPO, LOCAL_DIR)
64
- tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
65
-
66
- # Define chat template
67
- gemma_chat_template_simplified = (
68
- "{% for message in messages %}"
69
- "{% if message['role'] == 'user' %}"
70
- "{{ '<start_of_turn>user\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
71
- "{% elif message['role'] == 'assistant' %}"
72
- "{{ '<start_of_turn>model\\n' + message['content'] | trim + '<end_of_turn>\\n' }}"
73
- "{% endif %}"
74
- "{% endfor %}"
75
- "{% if add_generation_prompt %}"
76
- "{{ '<start_of_turn>model\\n' }}"
77
- "{% endif %}"
78
- )
79
-
80
- if tokenizer.chat_template is None:
81
- tokenizer.chat_template = gemma_chat_template_simplified
82
-
83
- model = AutoModelForCausalLM.from_pretrained(
84
- model_path,
85
- local_files_only=True,
86
- torch_dtype=torch.float32,
87
- device_map=None
88
- )
89
- model.eval()
90
-
91
- # System prompt for moderation
92
- MODERATION_SYSTEM_PROMPT = (
93
- "You are a multilingual content moderation classifier. "
94
- "You MUST respond with exactly one lowercase letter: 's' for safe, 'u' for unsafe. "
95
- "No explanations, no punctuation, no extra words. "
96
- "If the message contains hate speech, harassment, sexual content involving minors, "
97
- "extreme violence, self-harm encouragement, or other unsafe material, respond 'u'. "
98
- "Otherwise respond 's'."
99
- )
100
-
101
- # API Models
102
- class ModerationRequest(BaseModel):
103
- input: str = Field(..., description="Text to moderate")
104
- model: Optional[str] = Field("gemma-3-270m-it", description="Model to use for moderation")
105
-
106
- class BatchModerationRequest(BaseModel):
107
- inputs: List[str] = Field(..., description="List of texts to moderate")
108
- model: Optional[str] = Field("gemma-3-270m-it", description="Model to use for moderation")
109
-
110
- class ModerationResponse(BaseModel):
111
- id: str
112
- object: str
113
- created: int
114
- model: str
115
- results: List[Dict[str, Any]]
116
-
117
- # Helper functions
118
- def build_prompt(message, max_ctx_tokens=128):
119
- full_user_message = f"{MODERATION_SYSTEM_PROMPT}\n\nUser input: '{message}'"
120
- messages = [{"role": "user", "content": full_user_message}]
121
 
122
- text = tokenizer.apply_chat_template(
123
- messages,
124
- tokenize=False,
125
- add_generation_prompt=True
126
- )
127
 
128
- while len(tokenizer(text, add_special_tokens=False).input_ids) > max_ctx_tokens and len(full_user_message) > 100:
129
- full_user_message = full_user_message[:-50]
130
- messages[0]['content'] = full_user_message
131
- text = tokenizer.apply_chat_template(
132
- messages,
133
- tokenize=False,
134
- add_generation_prompt=True
135
- )
136
- return text
137
-
138
- def enforce_s_u(text: str) -> str:
139
- text_lower = text.strip().lower()
140
- if "u" in text_lower and "s" not in text_lower:
141
- return "u"
142
- if "unsafe" in text_lower:
143
- return "u"
144
- return "s"
145
-
146
- def classify_text(message, max_tokens=3, temperature=0.1, top_p=0.95):
147
- if not message.strip():
148
- return {
149
- "classification": "s",
150
- "label": "SAFE",
151
- "description": "Content appears to be safe and appropriate.",
152
- "tokens_per_second": 0,
153
- "processing_time": 0
154
- }
155
-
156
- text = build_prompt(message)
157
- inputs = tokenizer([text], return_tensors="pt").to(model.device)
158
- do_sample = bool(temperature and temperature > 0.0)
159
- gen_kwargs = dict(
160
- max_new_tokens=max_tokens,
161
- do_sample=do_sample,
162
- top_p=top_p,
163
- temperature=temperature if do_sample else None,
164
- use_cache=True,
165
- eos_token_id=tokenizer.eos_token_id,
166
- pad_token_id=tokenizer.eos_token_id
167
- )
168
 
169
- try:
170
- streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True, skip_prompt=True)
171
- except TypeError:
172
- streamer = TextIteratorStreamer(tokenizer, skip_special_tokens=True)
173
 
174
- thread = threading.Thread(
175
- target=model.generate,
176
- kwargs={**inputs, **{k: v for k, v in gen_kwargs.items() if v is not None}, "streamer": streamer}
177
- )
178
 
179
- partial_text = ""
180
- token_count = 0
181
- start_time = None
 
 
 
 
 
 
182
 
183
- with torch.inference_mode():
184
- thread.start()
185
- try:
186
- for chunk in streamer:
187
- if start_time is None:
188
- start_time = time.time()
189
- partial_text += chunk
190
- token_count += 1
191
- finally:
192
- thread.join()
193
 
194
- final_label = enforce_s_u(partial_text)
195
- end_time = time.time() if start_time else time.time()
196
- duration = max(1e-6, end_time - start_time)
197
- tps = token_count / duration if duration > 0 else 0.0
198
 
199
- if final_label == "s":
200
- label = "SAFE"
201
- description = "Content appears to be safe and appropriate."
202
- else:
203
- label = "UNSAFE"
204
- description = "Content may contain inappropriate or harmful material."
 
 
 
 
 
 
 
205
 
206
- return {
207
- "classification": final_label,
208
- "label": label,
209
- "description": description,
210
- "tokens_per_second": tps,
211
- "processing_time": duration
 
 
 
 
 
 
212
  }
213
-
214
- # API Key validation
215
- def get_api_key(request: Request):
216
- api_key = request.headers.get("Authorization") or request.query_params.get("api_key")
217
- if not api_key:
218
- raise HTTPException(status_code=401, detail="API key required")
219
 
220
- # Remove "Bearer " prefix if present
221
- if api_key.startswith("Bearer "):
222
- api_key = api_key[7:]
223
 
224
- # Validate against environment variable
225
- env_api_key = os.getenv("API_KEY")
226
- if not env_api_key or api_key != env_api_key:
227
- raise HTTPException(status_code=401, detail="Invalid API key")
228
 
229
- return api_key
230
-
231
- # API Endpoints
232
- @app.get("/", response_class=HTMLResponse)
233
- async def get_home(request: Request):
234
- return templates.TemplateResponse("index.html", {"request": request})
235
-
236
- @app.post("/v1/moderations", response_model=ModerationResponse)
237
- async def moderate_content(
238
- request: ModerationRequest,
239
- api_key: str = Depends(get_api_key)
240
- ):
241
- result = classify_text(request.input)
242
 
243
- response_data = {
244
- "id": f"modr_{int(time.time())}",
245
- "object": "moderation",
246
- "created": int(time.time()),
247
- "model": request.model,
248
- "results": [
249
- {
250
- "flagged": result["classification"] == "u",
251
- "categories": {
252
- "hate": result["classification"] == "u",
253
- "hate/threatening": result["classification"] == "u",
254
- "harassment": result["classification"] == "u",
255
- "harassment/threatening": result["classification"] == "u",
256
- "self-harm": result["classification"] == "u",
257
- "self-harm/intent": result["classification"] == "u",
258
- "self-harm/instructions": result["classification"] == "u",
259
- "sexual": result["classification"] == "u",
260
- "sexual/minors": result["classification"] == "u",
261
- "violence": result["classification"] == "u",
262
- "violence/graphic": result["classification"] == "u"
263
- },
264
- "category_scores": {
265
- "hate": 0.9 if result["classification"] == "u" else 0.1,
266
- "hate/threatening": 0.9 if result["classification"] == "u" else 0.1,
267
- "harassment": 0.9 if result["classification"] == "u" else 0.1,
268
- "harassment/threatening": 0.9 if result["classification"] == "u" else 0.1,
269
- "self-harm": 0.9 if result["classification"] == "u" else 0.1,
270
- "self-harm/intent": 0.9 if result["classification"] == "u" else 0.1,
271
- "self-harm/instructions": 0.9 if result["classification"] == "u" else 0.1,
272
- "sexual": 0.9 if result["classification"] == "u" else 0.1,
273
- "sexual/minors": 0.9 if result["classification"] == "u" else 0.1,
274
- "violence": 0.9 if result["classification"] == "u" else 0.1,
275
- "violence/graphic": 0.9 if result["classification"] == "u" else 0.1
276
- },
277
- "text": request.input
278
- }
279
- ]
280
  }
 
 
 
 
 
 
 
 
281
 
282
- return response_data
283
-
284
- @app.post("/v1/moderations/batch", response_model=ModerationResponse)
285
- async def moderate_content_batch(
286
- request: BatchModerationRequest,
287
- api_key: str = Depends(get_api_key)
288
- ):
289
- results = []
290
 
291
- for text in request.inputs:
292
- result = classify_text(text)
293
- results.append({
294
- "flagged": result["classification"] == "u",
295
- "categories": {
296
- "hate": result["classification"] == "u",
297
- "hate/threatening": result["classification"] == "u",
298
- "harassment": result["classification"] == "u",
299
- "harassment/threatening": result["classification"] == "u",
300
- "self-harm": result["classification"] == "u",
301
- "self-harm/intent": result["classification"] == "u",
302
- "self-harm/instructions": result["classification"] == "u",
303
- "sexual": result["classification"] == "u",
304
- "sexual/minors": result["classification"] == "u",
305
- "violence": result["classification"] == "u",
306
- "violence/graphic": result["classification"] == "u"
307
- },
308
- "category_scores": {
309
- "hate": 0.9 if result["classification"] == "u" else 0.1,
310
- "hate/threatening": 0.9 if result["classification"] == "u" else 0.1,
311
- "harassment": 0.9 if result["classification"] == "u" else 0.1,
312
- "harassment/threatening": 0.9 if result["classification"] == "u" else 0.1,
313
- "self-harm": 0.9 if result["classification"] == "u" else 0.1,
314
- "self-harm/intent": 0.9 if result["classification"] == "u" else 0.1,
315
- "self-harm/instructions": 0.9 if result["classification"] == "u" else 0.1,
316
- "sexual": 0.9 if result["classification"] == "u" else 0.1,
317
- "sexual/minors": 0.9 if result["classification"] == "u" else 0.1,
318
- "violence": 0.9 if result["classification"] == "u" else 0.1,
319
- "violence/graphic": 0.9 if result["classification"] == "u" else 0.1
320
- },
321
- "text": text
322
- })
323
 
324
- response_data = {
325
- "id": f"modr_batch_{int(time.time())}",
326
- "object": "moderation",
327
- "created": int(time.time()),
328
- "model": request.model,
329
- "results": results
330
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
- return response_data
333
 
334
- # Create the HTML template with Tailwind CSS
335
- with open("templates/index.html", "w") as f:
336
- f.write("""
 
 
337
  <!DOCTYPE html>
338
  <html lang="en">
339
  <head>
340
  <meta charset="UTF-8">
341
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
342
- <title>AI Content Moderator</title>
343
  <script src="https://cdn.tailwindcss.com"></script>
 
344
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
 
345
  <style>
346
- @keyframes float {
347
- 0% { transform: translateY(0px); }
348
- 50% { transform: translateY(-10px); }
349
- 100% { transform: translateY(0px); }
350
- }
351
- @keyframes pulse-border {
352
- 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
353
- 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
354
- 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
355
- }
356
- .float-animation {
357
- animation: float 3s ease-in-out infinite;
358
- }
359
- .pulse-border {
360
- animation: pulse-border 2s infinite;
361
- }
362
- .gradient-bg {
363
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
364
- }
365
- .glass-effect {
366
- background: rgba(255, 255, 255, 0.1);
367
- backdrop-filter: blur(10px);
368
- border-radius: 10px;
369
- border: 1px solid rgba(255, 255, 255, 0.2);
370
- }
371
- .safe-gradient {
372
- background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
373
- }
374
- .unsafe-gradient {
375
- background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
376
- }
377
  </style>
378
  </head>
379
- <body class="min-h-screen gradient-bg text-white">
380
- <div class="container mx-auto px-4 py-8">
381
- <header class="text-center mb-12">
382
- <div class="inline-block p-4 rounded-full glass-effect float-animation mb-6">
383
- <i class="fas fa-shield-alt text-5xl text-white"></i>
384
- </div>
385
- <h1 class="text-4xl md:text-5xl font-bold mb-4">AI Content Moderator</h1>
386
- <p class="text-xl text-gray-200 max-w-2xl mx-auto">
387
- Advanced, multilingual content classification tool powered by AI
388
- </p>
389
- </header>
390
-
391
- <main class="max-w-6xl mx-auto">
392
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
393
- <!-- API Key Section -->
394
- <div class="lg:col-span-1">
395
- <div class="glass-effect p-6 rounded-xl h-full">
396
- <h2 class="text-2xl font-bold mb-4 flex items-center">
397
- <i class="fas fa-key mr-2"></i> API Configuration
398
- </h2>
399
- <div class="mb-4">
400
- <label class="block text-sm font-medium mb-2">API Key</label>
401
- <div class="relative">
402
- <input type="password" id="apiKey" placeholder="Enter your API key"
403
- 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">
404
- <button id="toggleApiKey" class="absolute right-3 top-3 text-gray-300 hover:text-white">
405
- <i class="fas fa-eye"></i>
406
- </button>
407
- </div>
408
- </div>
409
- <div class="mb-4">
410
- <label class="block text-sm font-medium mb-2">Model</label>
411
- <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">
412
- <option value="gemma-3-270m-it" selected>Gemma 3 270M IT</option>
413
- </select>
414
- </div>
415
- <div class="mt-6">
416
- <h3 class="text-lg font-semibold mb-2">API Endpoints</h3>
417
- <div class="bg-black/20 p-4 rounded-lg text-sm font-mono">
418
- <div class="mb-2">POST /v1/moderations</div>
419
- <div>POST /v1/moderations/batch</div>
420
- </div>
421
- </div>
422
- </div>
423
- </div>
424
-
425
- <!-- Main Content Section -->
426
- <div class="lg:col-span-2">
427
- <div class="glass-effect p-6 rounded-xl">
428
- <h2 class="text-2xl font-bold mb-4 flex items-center">
429
- <i class="fas fa-check-circle mr-2"></i> Content Analysis
430
- </h2>
431
-
432
- <!-- Tabs -->
433
- <div class="flex border-b border-white/20 mb-6">
434
- <button id="singleTab" class="px-4 py-2 font-medium border-b-2 border-indigo-400 text-indigo-300 tab-active">
435
- Single Text
436
- </button>
437
- <button id="batchTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
438
- Batch Processing
439
- </button>
440
- </div>
441
-
442
- <!-- Single Text Tab -->
443
- <div id="singleContent" class="tab-content">
444
- <div class="mb-6">
445
- <label class="block text-sm font-medium mb-2">Text to Analyze</label>
446
- <textarea id="textInput" rows="6" placeholder="Enter any text in any language for content moderation analysis..."
447
- 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>
448
- </div>
449
-
450
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
451
- <div>
452
- <label class="block text-sm font-medium mb-2">Max Tokens</label>
453
- <input type="range" id="maxTokens" min="1" max="10" value="3" step="1"
454
- class="w-full accent-indigo-500">
455
- <div class="text-center mt-1">
456
- <span id="maxTokensValue" class="text-sm">3</span>
457
- </div>
458
- </div>
459
- <div>
460
- <label class="block text-sm font-medium mb-2">Temperature</label>
461
- <input type="range" id="temperature" min="0" max="1" value="0.1" step="0.1"
462
- class="w-full accent-indigo-500">
463
- <div class="text-center mt-1">
464
- <span id="temperatureValue" class="text-sm">0.1</span>
465
- </div>
466
- </div>
467
- <div>
468
- <label class="block text-sm font-medium mb-2">Top-p</label>
469
- <input type="range" id="topP" min="0.1" max="1" value="0.95" step="0.05"
470
- class="w-full accent-indigo-500">
471
- <div class="text-center mt-1">
472
- <span id="topPValue" class="text-sm">0.95</span>
473
- </div>
474
- </div>
475
- </div>
476
-
477
- <div class="flex space-x-4">
478
- <button id="analyzeBtn" 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">
479
- <i class="fas fa-search mr-2"></i> Analyze Content
480
- </button>
481
- <button id="clearBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
482
- <i class="fas fa-trash mr-2"></i> Clear
483
- </button>
484
- </div>
485
- </div>
486
-
487
- <!-- Batch Processing Tab -->
488
- <div id="batchContent" class="tab-content hidden">
489
- <div class="mb-6">
490
- <label class="block text-sm font-medium mb-2">Texts to Analyze (one per line)</label>
491
- <textarea id="batchTextInput" rows="8" placeholder="Enter multiple texts, one per line..."
492
- 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>
493
- </div>
494
-
495
- <div class="flex space-x-4">
496
- <button id="batchAnalyzeBtn" 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">
497
- <i class="fas fa-layer-group mr-2"></i> Analyze Batch
498
- </button>
499
- <button id="batchClearBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
500
- <i class="fas fa-trash mr-2"></i> Clear
501
- </button>
502
- </div>
503
- </div>
504
-
505
- <!-- Results Section -->
506
- <div id="resultsSection" class="mt-8 hidden">
507
- <h3 class="text-xl font-bold mb-4 flex items-center">
508
- <i class="fas fa-chart-bar mr-2"></i> Analysis Results
509
- </h3>
510
- <div id="resultsContainer" class="space-y-4">
511
- <!-- Results will be dynamically inserted here -->
512
- </div>
513
- </div>
514
- </div>
515
  </div>
 
516
  </div>
517
-
518
- <!-- Examples Section -->
519
- <div class="glass-effect p-6 rounded-xl mb-12">
520
- <h2 class="text-2xl font-bold mb-4 flex items-center">
521
- <i class="fas fa-lightbulb mr-2"></i> Example Prompts
522
- </h2>
523
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
524
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
525
- <p class="text-sm">"Hello, how are you today? I hope you're having a wonderful time!"</p>
526
- </div>
527
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
528
- <p class="text-sm">"I hate you and I will find you and hurt you badly."</p>
529
- </div>
530
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
531
- <p class="text-sm">"C'est une belle journée pour apprendre la programmation et l'intelligence artificielle."</p>
532
- </div>
533
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
534
- <p class="text-sm">"I can't take this anymore. I want to end everything and disappear forever."</p>
535
- </div>
536
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
537
- <p class="text-sm">"¡Hola! Me encanta aprender nuevos idiomas y conocer diferentes culturas."</p>
538
- </div>
539
- <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
540
- <p class="text-sm">"You're absolutely worthless and nobody will ever love someone like you."</p>
541
- </div>
542
- </div>
543
  </div>
544
-
545
- <!-- Information Section -->
546
- <div class="glass-effect p-6 rounded-xl">
547
- <h2 class="text-2xl font-bold mb-4 flex items-center">
548
- <i class="fas fa-info-circle mr-2"></i> About This Tool
549
- </h2>
550
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
551
- <div class="text-center">
552
- <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
553
- <i class="fas fa-globe text-2xl text-indigo-300"></i>
554
- </div>
555
- <h3 class="text-lg font-semibold mb-2">Multilingual</h3>
556
- <p class="text-gray-300">Supports content analysis in multiple languages with high accuracy.</p>
557
- </div>
558
- <div class="text-center">
559
- <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
560
- <i class="fas fa-bolt text-2xl text-indigo-300"></i>
561
- </div>
562
- <h3 class="text-lg font-semibold mb-2">Fast Processing</h3>
563
- <p class="text-gray-300">Optimized model for quick content analysis with real-time results.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  </div>
565
- <div class="text-center">
566
- <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
567
- <i class="fas fa-shield-alt text-2xl text-indigo-300"></i>
568
- </div>
569
- <h3 class="text-lg font-semibold mb-2">Secure</h3>
570
- <p class="text-gray-300">API key authentication ensures your requests remain secure and private.</p>
 
 
 
571
  </div>
572
  </div>
573
  </div>
574
- </main>
575
-
576
- <footer class="mt-12 text-center text-gray-300">
577
- <p>© 2023 AI Content Moderator. All rights reserved.</p>
578
- </footer>
579
- </div>
580
-
581
- <!-- Loading Modal -->
582
- <div id="loadingModal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
583
- <div class="glass-effect p-8 rounded-xl max-w-md w-full mx-4 text-center">
584
- <div class="mb-4">
585
- <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
586
- </div>
587
- <h3 class="text-xl font-bold mb-2">Analyzing Content</h3>
588
- <p class="text-gray-300">Please wait while we process your request...</p>
589
  </div>
590
- </div>
591
-
592
  <script>
593
- // DOM Elements
594
- const singleTab = document.getElementById('singleTab');
595
- const batchTab = document.getElementById('batchTab');
596
- const singleContent = document.getElementById('singleContent');
597
- const batchContent = document.getElementById('batchContent');
598
- const apiKeyInput = document.getElementById('apiKey');
599
- const toggleApiKeyBtn = document.getElementById('toggleApiKey');
600
- const textInput = document.getElementById('textInput');
601
- const batchTextInput = document.getElementById('batchTextInput');
602
- const maxTokensSlider = document.getElementById('maxTokens');
603
- const maxTokensValue = document.getElementById('maxTokensValue');
604
- const temperatureSlider = document.getElementById('temperature');
605
- const temperatureValue = document.getElementById('temperatureValue');
606
- const topPSlider = document.getElementById('topP');
607
- const topPValue = document.getElementById('topPValue');
608
- const analyzeBtn = document.getElementById('analyzeBtn');
609
- const batchAnalyzeBtn = document.getElementById('batchAnalyzeBtn');
610
- const clearBtn = document.getElementById('clearBtn');
611
- const batchClearBtn = document.getElementById('batchClearBtn');
612
- const resultsSection = document.getElementById('resultsSection');
613
- const resultsContainer = document.getElementById('resultsContainer');
614
- const loadingModal = document.getElementById('loadingModal');
615
- const exampleCards = document.querySelectorAll('.example-card');
616
-
617
- // Tab switching
618
- singleTab.addEventListener('click', () => {
619
- singleTab.classList.add('border-indigo-400', 'text-indigo-300');
620
- singleTab.classList.remove('border-transparent', 'text-gray-300');
621
- batchTab.classList.add('border-transparent', 'text-gray-300');
622
- batchTab.classList.remove('border-indigo-400', 'text-indigo-300');
623
- singleContent.classList.remove('hidden');
624
- batchContent.classList.add('hidden');
625
- });
626
-
627
- batchTab.addEventListener('click', () => {
628
- batchTab.classList.add('border-indigo-400', 'text-indigo-300');
629
- batchTab.classList.remove('border-transparent', 'text-gray-300');
630
- singleTab.classList.add('border-transparent', 'text-gray-300');
631
- singleTab.classList.remove('border-indigo-400', 'text-indigo-300');
632
- batchContent.classList.remove('hidden');
633
- singleContent.classList.add('hidden');
634
- });
635
-
636
- // Toggle API key visibility
637
- toggleApiKeyBtn.addEventListener('click', () => {
638
- const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
639
- apiKeyInput.setAttribute('type', type);
640
- toggleApiKeyBtn.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
641
- });
642
-
643
- // Slider value updates
644
- maxTokensSlider.addEventListener('input', () => {
645
- maxTokensValue.textContent = maxTokensSlider.value;
646
- });
647
-
648
- temperatureSlider.addEventListener('input', () => {
649
- temperatureValue.textContent = temperatureSlider.value;
650
- });
651
-
652
- topPSlider.addEventListener('input', () => {
653
- topPValue.textContent = topPSlider.value;
654
- });
655
-
656
- // Example cards
657
- exampleCards.forEach(card => {
658
- card.addEventListener('click', () => {
659
- textInput.value = card.querySelector('p').textContent;
660
- });
661
- });
662
-
663
- // Clear buttons
664
- clearBtn.addEventListener('click', () => {
665
- textInput.value = '';
666
- resultsSection.classList.add('hidden');
667
- });
668
-
669
- batchClearBtn.addEventListener('click', () => {
670
- batchTextInput.value = '';
671
- resultsSection.classList.add('hidden');
672
  });
673
 
674
- // Analyze button
675
- analyzeBtn.addEventListener('click', async () => {
676
- const text = textInput.value.trim();
677
- if (!text) {
678
- showNotification('Please enter text to analyze', 'error');
679
- return;
680
- }
681
-
682
- const apiKey = apiKeyInput.value.trim();
683
- if (!apiKey) {
684
- showNotification('Please enter your API key', 'error');
685
- return;
686
- }
687
-
688
- showLoading(true);
689
-
690
  try {
691
- const response = await fetch('/v1/moderations', {
692
- method: 'POST',
693
- headers: {
694
- 'Content-Type': 'application/json',
695
- 'Authorization': `Bearer ${apiKey}`
696
- },
697
- body: JSON.stringify({
698
- input: text,
699
- model: document.getElementById('modelSelect').value
700
- })
701
- });
702
-
703
- if (!response.ok) {
704
- const errorData = await response.json();
705
- throw new Error(errorData.detail || 'An error occurred');
706
- }
707
-
708
  const data = await response.json();
709
- displayResults([data.results[0]]);
710
- } catch (error) {
711
- showNotification(`Error: ${error.message}`, 'error');
712
- } finally {
713
- showLoading(false);
714
- }
715
- });
 
 
 
 
 
 
 
716
 
717
- // Batch analyze button
718
- batchAnalyzeBtn.addEventListener('click', async () => {
719
- const texts = batchTextInput.value.trim().split('\\n').filter(text => text.trim());
720
- if (texts.length === 0) {
721
- showNotification('Please enter at least one text to analyze', 'error');
722
- return;
723
- }
 
 
 
 
 
 
 
 
 
 
724
 
725
- const apiKey = apiKeyInput.value.trim();
726
- if (!apiKey) {
727
- showNotification('Please enter your API key', 'error');
728
- return;
729
- }
730
 
731
- showLoading(true);
 
 
732
 
733
  try {
734
- const response = await fetch('/v1/moderations/batch', {
735
  method: 'POST',
736
- headers: {
737
- 'Content-Type': 'application/json',
738
- 'Authorization': `Bearer ${apiKey}`
739
- },
740
- body: JSON.stringify({
741
- inputs: texts,
742
- model: document.getElementById('modelSelect').value
743
- })
744
  });
745
-
746
- if (!response.ok) {
747
- const errorData = await response.json();
748
- throw new Error(errorData.detail || 'An error occurred');
749
- }
750
-
751
  const data = await response.json();
752
- displayResults(data.results);
 
753
  } catch (error) {
754
- showNotification(`Error: ${error.message}`, 'error');
755
  } finally {
756
- showLoading(false);
 
757
  }
758
  });
759
 
760
- // Display results
761
- function displayResults(results) {
762
  resultsContainer.innerHTML = '';
763
 
764
- results.forEach((result, index) => {
765
- const isFlagged = result.flagged;
766
- const cardClass = isFlagged ? 'unsafe-gradient' : 'safe-gradient';
767
- const icon = isFlagged ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle';
768
- const statusText = isFlagged ? 'UNSAFE' : 'SAFE';
769
- const statusDesc = isFlagged ?
770
- 'Content may contain inappropriate or harmful material.' :
771
- 'Content appears to be safe and appropriate.';
772
-
773
- const categories = Object.entries(result.categories)
774
- .filter(([_, value]) => value)
775
- .map(([key, _]) => key.replace('/', ' '))
776
- .join(', ');
777
-
778
- const resultCard = document.createElement('div');
779
- resultCard.className = `p-6 rounded-xl text-white ${cardClass} shadow-lg`;
780
- resultCard.innerHTML = `
781
- <div class="flex items-start">
782
- <div class="mr-4 mt-1">
783
- <i class="${icon} text-3xl"></i>
784
- </div>
785
- <div class="flex-1">
786
- <div class="flex justify-between items-start mb-2">
787
- <h3 class="text-xl font-bold">${statusText}</h3>
788
- <span class="text-sm bg-black/20 px-2 py-1 rounded">Result ${index + 1}</span>
789
- </div>
790
- <p class="mb-4">${statusDesc}</p>
791
- <div class="bg-black/20 p-4 rounded-lg mb-4">
792
- <p class="text-sm font-mono break-words">${result.text}</p>
793
- </div>
794
- ${isFlagged ? `
795
- <div class="mb-3">
796
- <h4 class="font-semibold mb-1">Flagged Categories:</h4>
797
- <p class="text-sm">${categories}</p>
798
- </div>
799
- ` : ''}
800
- <div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
801
- ${Object.entries(result.category_scores).map(([category, score]) => `
802
- <div class="bg-black/20 p-2 rounded">
803
- <div class="font-medium">${category.replace('/', ' ')}</div>
804
- <div class="w-full bg-gray-700 rounded-full h-1.5 mt-1">
805
- <div class="bg-white h-1.5 rounded-full" style="width: ${score * 100}%"></div>
806
- </div>
807
- <div class="text-right mt-1">${(score * 100).toFixed(0)}%</div>
808
- </div>
809
- `).join('')}
810
- </div>
811
- </div>
812
- </div>
813
- `;
814
-
815
- resultsContainer.appendChild(resultCard);
816
- });
817
-
818
- resultsSection.classList.remove('hidden');
819
- resultsSection.scrollIntoView({ behavior: 'smooth' });
820
- }
821
-
822
- // Show/hide loading modal
823
- function showLoading(show) {
824
- if (show) {
825
- loadingModal.classList.remove('hidden');
826
- } else {
827
- loadingModal.classList.add('hidden');
828
  }
 
829
  }
830
 
831
- // Show notification
832
- function showNotification(message, type = 'info') {
833
- const notification = document.createElement('div');
834
- notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
835
- type === 'error' ? 'bg-red-500' : 'bg-indigo-500'
836
- } text-white`;
837
- notification.innerHTML = `
838
- <div class="flex items-center">
839
- <i class="fas ${type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i>
840
- <span>${message}</span>
841
- </div>
842
- `;
843
-
844
- document.body.appendChild(notification);
845
-
846
- setTimeout(() => {
847
- notification.style.opacity = '0';
848
- notification.style.transition = 'opacity 0.5s';
849
- setTimeout(() => {
850
- document.body.removeChild(notification);
851
- }, 500);
852
- }, 3000);
853
- }
854
  </script>
855
  </body>
856
  </html>
857
- """)
858
-
859
- # Initialize the model
860
- print("Initializing model...")
861
- with torch.inference_mode():
862
- _ = model.generate(
863
- **tokenizer(["Hello"], return_tensors="pt").to(model.device),
864
- max_new_tokens=1, do_sample=False, use_cache=True
865
- )
866
-
867
- print("🚀 Starting AI Content Moderator API...")
868
-
869
- if __name__ == "__main__":
870
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import os
2
+ import uuid
3
  import time
4
  import threading
5
+ import io
6
+ from datetime import datetime, timedelta
7
+ from collections import defaultdict, deque
8
+
9
+ from flask import Flask, request, jsonify, render_template
10
+ from detoxify import Detoxify
11
+ import numpy as np
12
+ import requests
13
+ from PIL import Image
14
+ from tensorflow.keras.models import load_model
15
+
16
+ app = Flask(__name__, static_folder='static', template_folder='templates')
17
+ app.logger.setLevel('INFO')
18
+
19
+ API_KEY = os.environ.get('API_KEY')
20
+ if not API_KEY:
21
+ raise ValueError("API_KEY environment variable not set.")
22
+
23
+ print("Loading Detoxify model for text moderation...")
24
+ detoxify_model = Detoxify('multilingual')
25
+ print("Detoxify model loaded successfully.")
26
+
27
+ MODEL_PATH = 'keras_model.h5'
28
+ LABELS_PATH = 'labels.txt'
29
+ image_model = None
30
+ image_labels = None
31
+
32
+ try:
33
+ print("Loading Teachable Machine model for image moderation...")
34
+ image_model = load_model(MODEL_PATH, compile=False)
35
+ with open(LABELS_PATH, 'r') as f:
36
+ image_labels = [line.strip().split(' ')[1] for line in f.readlines()]
37
+ print("Image moderation model loaded successfully.")
38
+ except Exception as e:
39
+ app.logger.warning(f"Could not load image moderation model. Image moderation will be disabled. Error: {e}")
40
+ image_model = None
41
+ image_labels = None
42
+
43
+ request_durations = deque(maxlen=100)
44
+ request_timestamps = deque(maxlen=1000)
45
+ daily_requests = defaultdict(int)
46
+ concurrent_requests = 0
47
+ concurrent_requests_lock = threading.Lock()
48
+
49
+ def is_url(string):
50
+ return isinstance(string, str) and string.strip().startswith(('http://', 'https://'))
51
+
52
+ def classify_image(image_bytes):
53
+ if not image_model or not image_labels:
54
+ raise RuntimeError("Image moderation model is not available.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
57
+ image = image.resize((224, 224))
 
 
 
58
 
59
+ image_array = np.asarray(image)
60
+ normalized_image_array = (image_array.astype(np.float32) / 127.5) - 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ data = np.ndarray(shape=(1, 224, 224, 3), dtype=np.float32)
63
+ data[0] = normalized_image_array
 
 
64
 
65
+ prediction = image_model.predict(data)
 
 
 
66
 
67
+ scores = {label.lower(): float(score) for label, score in zip(image_labels, prediction[0])}
68
+ return scores
69
+
70
+ def transform_text_predictions(prediction_dict):
71
+ category_keys = [
72
+ "toxicity", "severe_toxicity", "obscene", "threat",
73
+ "insult", "identity_attack", "sexual_explicit"
74
+ ]
75
+ scores = {key: float(prediction_dict.get(key, 0.0)) for key in category_keys}
76
 
77
+ threshold = 0.5
78
+ categories = {key: (scores[key] > threshold) for key in category_keys}
79
+ flagged = any(categories.values())
 
 
 
 
 
 
 
80
 
81
+ return flagged, categories, scores
82
+
83
+ def transform_image_predictions(prediction_dict):
84
+ nsfw_score = prediction_dict.get('nsfw', 0.0)
85
 
86
+ categories = {
87
+ "sexual": nsfw_score > 0.8,
88
+ "hate": False,
89
+ "harassment": False,
90
+ "self-harm": False,
91
+ "sexual/minors": nsfw_score > 0.9,
92
+ "hate/threatening": False,
93
+ "violence/graphic": False,
94
+ "self-harm/intent": False,
95
+ "self-harm/instructions": False,
96
+ "harassment/threatening": False,
97
+ "violence": False,
98
+ }
99
 
100
+ category_scores = {
101
+ "sexual": nsfw_score,
102
+ "hate": 0.0,
103
+ "harassment": 0.0,
104
+ "self-harm": 0.0,
105
+ "sexual/minors": nsfw_score,
106
+ "hate/threatening": 0.0,
107
+ "violence/graphic": 0.0,
108
+ "self-harm/intent": 0.0,
109
+ "self-harm/instructions": 0.0,
110
+ "harassment/threatening": 0.0,
111
+ "violence": 0.0,
112
  }
 
 
 
 
 
 
113
 
114
+ flagged = any(categories.values())
 
 
115
 
116
+ return flagged, categories, category_scores
 
 
 
117
 
118
+ def track_request_metrics(start_time):
119
+ duration = time.time() - start_time
120
+ request_durations.append(duration)
121
+ request_timestamps.append(datetime.now())
122
+ today = datetime.now().strftime("%Y-%m-%d")
123
+ daily_requests[today] += 1
124
+
125
+ def get_performance_metrics():
126
+ with concurrent_requests_lock:
127
+ current_concurrent = concurrent_requests
 
 
 
128
 
129
+ avg_request_time = sum(request_durations) / len(request_durations) if request_durations else 0
130
+ peak_request_time = max(request_durations) if request_durations else 0
131
+
132
+ now = datetime.now()
133
+ one_minute_ago = now - timedelta(seconds=60)
134
+ requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
135
+ today_requests = daily_requests.get(now.strftime("%Y-%m-%d"), 0)
136
+
137
+ last_7_days = []
138
+ for i in range(7):
139
+ date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
140
+ last_7_days.append({
141
+ "date": date,
142
+ "requests": daily_requests.get(date, 0),
143
+ })
144
+
145
+ return {
146
+ "avg_request_time_ms": avg_request_time * 1000,
147
+ "peak_request_time_ms": peak_request_time * 1000,
148
+ "requests_per_minute": requests_last_minute,
149
+ "concurrent_requests": current_concurrent,
150
+ "today_requests": today_requests,
151
+ "last_7_days": last_7_days
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
+
154
+ @app.route('/')
155
+ def home():
156
+ return render_template('index.html')
157
+
158
+ @app.route('/v1/moderations', methods=['POST'])
159
+ def moderations():
160
+ global concurrent_requests
161
 
162
+ auth_header = request.headers.get('Authorization')
163
+ if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY:
164
+ return jsonify({"error": {"message": "Incorrect API key provided.", "type": "invalid_request_error", "code": "invalid_api_key"}}), 401
165
+
166
+ with concurrent_requests_lock:
167
+ concurrent_requests += 1
 
 
168
 
169
+ start_time = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
+ try:
172
+ data = request.get_json()
173
+ if not data:
174
+ return jsonify({"error": "Invalid JSON body"}), 400
175
+
176
+ raw_input = data.get('input')
177
+ if raw_input is None:
178
+ return jsonify({"error": "'input' field is required"}), 400
179
+
180
+ inputs = [raw_input] if isinstance(raw_input, str) else raw_input
181
+ if not isinstance(inputs, list):
182
+ return jsonify({"error": "'input' must be a string or a list of strings/URLs"}), 400
183
+
184
+ results = []
185
+ texts_to_process = []
186
+ text_indices = []
187
+
188
+ for i, item in enumerate(inputs):
189
+ if is_url(item):
190
+ try:
191
+ response = requests.get(item, timeout=10)
192
+ response.raise_for_status()
193
+ image_scores = classify_image(response.content)
194
+ flagged, categories, category_scores = transform_image_predictions(image_scores)
195
+ results.append((i, {"flagged": flagged, "categories": categories, "category_scores": category_scores}))
196
+ except requests.RequestException as e:
197
+ results.append((i, {"error": f"Failed to download image: {e}"}))
198
+ except Exception as e:
199
+ results.append((i, {"error": f"Failed to process image: {e}"}))
200
+ elif isinstance(item, str):
201
+ texts_to_process.append(item)
202
+ text_indices.append(i)
203
+ else:
204
+ results.append((i, {"error": "Invalid input type. Must be a string or URL."}))
205
+
206
+ if texts_to_process:
207
+ text_predictions = detoxify_model.predict(texts_to_process)
208
+ for i, original_index in enumerate(text_indices):
209
+ single_prediction = {key: value[i] for key, value in text_predictions.items()}
210
+ flagged, categories, category_scores = transform_text_predictions(single_prediction)
211
+ results.append((original_index, {"flagged": flagged, "categories": categories, "category_scores": category_scores}))
212
+
213
+ results.sort(key=lambda x: x[0])
214
+ final_results = [res for _, res in results]
215
+
216
+ response_data = {
217
+ "id": "modr-" + uuid.uuid4().hex[:24],
218
+ "model": "smart-moderator-multimodal-v1",
219
+ "results": final_results
220
+ }
221
+
222
+ return jsonify(response_data)
223
+
224
+ except Exception as e:
225
+ app.logger.error(f"An error occurred: {e}", exc_info=True)
226
+ return jsonify({"error": "An internal server error occurred."}), 500
227
+ finally:
228
+ track_request_metrics(start_time)
229
+ with concurrent_requests_lock:
230
+ concurrent_requests -= 1
231
+
232
+ @app.route('/v1/metrics', methods=['GET'])
233
+ def metrics():
234
+ auth_header = request.headers.get('Authorization')
235
+ if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY:
236
+ return jsonify({"error": "Unauthorized"}), 401
237
 
238
+ return jsonify(get_performance_metrics())
239
 
240
+ def create_app_structure():
241
+ os.makedirs('templates', exist_ok=True)
242
+ os.makedirs('static', exist_ok=True)
243
+
244
+ index_html_content = r'''
245
  <!DOCTYPE html>
246
  <html lang="en">
247
  <head>
248
  <meta charset="UTF-8">
249
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
250
+ <title>Smart Moderator API</title>
251
  <script src="https://cdn.tailwindcss.com"></script>
252
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
253
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
254
+ <script>
255
+ tailwind.config = { darkMode: 'class' }
256
+ </script>
257
  <style>
258
+ .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
259
+ .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); }
260
+ .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; }
261
+ @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </style>
263
  </head>
264
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen font-sans">
265
+ <header class="gradient-bg text-white shadow-lg">
266
+ <div class="container mx-auto px-4 py-6 flex justify-between items-center">
267
+ <div class="flex items-center space-x-3">
268
+ <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center">
269
+ <i class="fas fa-shield-alt text-indigo-600 text-xl"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  </div>
271
+ <h1 class="text-2xl font-bold">Smart Moderator</h1>
272
  </div>
273
+ <div>
274
+ <button id="darkModeToggle" class="bg-white/20 p-2 rounded-lg hover:bg-white/30 transition">
275
+ <i class="fas fa-moon dark:hidden"></i>
276
+ <i class="fas fa-sun hidden dark:inline"></i>
277
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  </div>
279
+ </div>
280
+ </header>
281
+
282
+ <main class="container mx-auto px-4 py-8">
283
+ <section class="mb-12">
284
+ <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-tachometer-alt mr-3 text-indigo-500"></i>Performance Metrics</h2>
285
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
286
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response</p><p class="text-2xl font-bold" id="avgResponseTime">0ms</p></div>
287
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Requests / Minute</p><p class="text-2xl font-bold" id="requestsPerMinute">0</p></div>
288
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Peak Response</p><p class="text-2xl font-bold" id="peakResponseTime">0ms</p></div>
289
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Today's Requests</p><p class="text-2xl font-bold" id="todayRequests">0</p></div>
290
+ </div>
291
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
292
+ <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3>
293
+ <div class="h-64"><canvas id="activityChart"></canvas></div>
294
+ </div>
295
+ </section>
296
+
297
+ <section class="mb-12">
298
+ <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-vial mr-3 text-indigo-500"></i>API Tester</h2>
299
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
300
+ <form id="apiTestForm">
301
+ <div class="mb-4"><label class="block text-sm font-medium mb-2" for="apiKey">API Key</label><input type="password" id="apiKey" class="w-full px-4 py-2 rounded-lg border bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your API key"></div>
302
+ <div class="mb-4"><label class="block text-sm font-medium mb-2">Input (Text or Image URL)</label><textarea id="apiInput" class="w-full px-4 py-2 rounded-lg border bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500" rows="4" placeholder="Enter text to moderate, or a public image URL. For multiple items, separate them with a new line."></textarea></div>
303
+ <button type="submit" id="analyzeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-6 rounded-lg transition"><i class="fas fa-search mr-2"></i>Analyze</button>
304
+ </form>
305
+ </div>
306
+ </section>
307
+
308
+ <section id="resultsSection" class="hidden">
309
+ <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-clipboard-check mr-3 text-indigo-500"></i>Analysis Results</h2>
310
+ <div id="resultsContainer" class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"></div>
311
+ </section>
312
+
313
+ <section>
314
+ <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-book-open mr-3 text-indigo-500"></i>API Documentation</h2>
315
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
316
+ <h3 class="text-lg font-semibold mb-2">Endpoint</h3>
317
+ <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">POST /v1/moderations</code>
318
+ <h3 class="text-lg font-semibold mb-2">Headers</h3>
319
+ <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">Authorization: Bearer YOUR_API_KEY<br>Content-Type: application/json</code>
320
+ <h3 class="text-lg font-semibold mb-2">Request Body</h3>
321
+ <p class="text-sm mb-2">The `input` field can be a single string/URL or a list of strings/URLs.</p>
322
+ <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">{"input": "Text to moderate"}</code>
323
+ <h3 class="text-lg font-semibold mb-2">Usage Example (cURL)</h3>
324
+ <div class="space-y-4">
325
+ <div>
326
+ <h4 class="font-semibold text-md mb-1">Text Moderation</h4>
327
+ <pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto text-sm"><code>curl -X POST https://nixaut-codelabs-smart-moderator.hf.space/v1/moderations \
328
+ -H "Authorization: Bearer YOUR_API_KEY" \
329
+ -H "Content-Type: application/json" \
330
+ -d '{"input": "You are stupid and I hate you."}'</code></pre>
331
  </div>
332
+ <div>
333
+ <h4 class="font-semibold text-md mb-1">Multimodal Moderation (Text + Image)</h4>
334
+ <pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto text-sm"><code>curl -X POST https://nixaut-codelabs-smart-moderator.hf.space/v1/moderations \
335
+ -H "Authorization: Bearer YOUR_API_KEY" \
336
+ -H "Content-Type: application/json" \
337
+ -d '{"input": [
338
+ "This is a perfectly normal sentence.",
339
+ "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg"
340
+ ]}'</code></pre>
341
  </div>
342
  </div>
343
  </div>
344
+ </section>
345
+ </main>
346
+ <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
347
+ <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400 text-sm">
348
+ © 2024 Smart Moderator by Nix-Aut Codelabs | <a href="https://nixaut-codelabs-smart-moderator.hf.space" class="hover:underline">nixaut-codelabs-smart-moderator.hf.space</a>
 
 
 
 
 
 
 
 
 
 
349
  </div>
350
+ </footer>
 
351
  <script>
352
+ const darkModeToggle = document.getElementById('darkModeToggle');
353
+ if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
354
+ document.documentElement.classList.add('dark');
355
+ }
356
+ darkModeToggle.addEventListener('click', () => {
357
+ document.documentElement.classList.toggle('dark');
358
+ localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  });
360
 
361
+ let activityChart;
362
+ async function fetchMetrics() {
363
+ const apiKey = document.getElementById('apiKey').value || 'temp-key';
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  try {
365
+ const response = await fetch('/v1/metrics', { headers: { 'Authorization': 'Bearer ' + apiKey } });
366
+ if (!response.ok) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  const data = await response.json();
368
+ document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
369
+ document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms';
370
+ document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
371
+ document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString();
372
+
373
+ if (activityChart) {
374
+ const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse();
375
+ const requests = data.last_7_days.map(d => d.requests).reverse();
376
+ activityChart.data.labels = labels;
377
+ activityChart.data.datasets[0].data = requests;
378
+ activityChart.update();
379
+ }
380
+ } catch (e) { console.error("Could not fetch metrics", e); }
381
+ }
382
 
383
+ function initChart() {
384
+ const ctx = document.getElementById('activityChart').getContext('2d');
385
+ const isDark = document.documentElement.classList.contains('dark');
386
+ activityChart = new Chart(ctx, {
387
+ type: 'bar',
388
+ data: { labels: [], datasets: [{ label: 'Requests', data: [], backgroundColor: 'rgba(99, 102, 241, 0.6)' }] },
389
+ options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } }
390
+ });
391
+ }
392
+
393
+ document.getElementById('apiTestForm').addEventListener('submit', async (e) => {
394
+ e.preventDefault();
395
+ const apiKey = document.getElementById('apiKey').value;
396
+ if (!apiKey) { alert('Please enter an API key.'); return; }
397
+
398
+ const rawInput = document.getElementById('apiInput').value.trim();
399
+ if (!rawInput) { alert('Please enter text or a URL to analyze.'); return; }
400
 
401
+ const inputs = rawInput.split('\\n').map(item => item.trim()).filter(Boolean);
402
+ const body = { input: inputs.length === 1 ? inputs[0] : inputs };
 
 
 
403
 
404
+ const analyzeBtn = document.getElementById('analyzeBtn');
405
+ analyzeBtn.disabled = true;
406
+ analyzeBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 border-2 border-white rounded-full mr-2"></div>Analyzing...';
407
 
408
  try {
409
+ const response = await fetch('/v1/moderations', {
410
  method: 'POST',
411
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
412
+ body: JSON.stringify(body)
 
 
 
 
 
 
413
  });
 
 
 
 
 
 
414
  const data = await response.json();
415
+ displayResults(data);
416
+ fetchMetrics();
417
  } catch (error) {
418
+ alert('An error occurred: ' + error.message);
419
  } finally {
420
+ analyzeBtn.disabled = false;
421
+ analyzeBtn.innerHTML = '<i class="fas fa-search mr-2"></i>Analyze';
422
  }
423
  });
424
 
425
+ function displayResults(data) {
426
+ const resultsContainer = document.getElementById('resultsContainer');
427
  resultsContainer.innerHTML = '';
428
 
429
+ if (data.error) {
430
+ resultsContainer.innerHTML = `<div class="p-4 text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg"><p><strong>Error:</strong> ${data.error.message || JSON.stringify(data.error)}</p></div>`;
431
+ } else if (data.results) {
432
+ data.results.forEach((result, index) => {
433
+ const resultCard = document.createElement('div');
434
+ resultCard.className = 'border-t border-gray-200 dark:border-gray-700 pt-4 mt-4 first:mt-0 first:border-t-0 first:pt-0';
435
+ const flaggedBadge = result.flagged
436
+ ? '<span class="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">Flagged</span>'
437
+ : '<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">Safe</span>';
438
+
439
+ let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => {
440
+ const isFlagged = result.categories[category];
441
+ return `<div class="flex justify-between py-1"><span class="${isFlagged ? 'font-bold text-red-500' : ''}">${category.replace("/", " / ")}</span><span class="font-mono text-sm">${score.toFixed(4)}</span></div>`;
442
+ }).join('');
443
+
444
+ resultCard.innerHTML = `<div class="flex justify-between items-center mb-2"><h4 class="font-bold">Input ${index + 1}</h4>${flaggedBadge}</div><div>${categoriesHtml}</div>`;
445
+ resultsContainer.appendChild(resultCard);
446
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
+ document.getElementById('resultsSection').classList.remove('hidden');
449
  }
450
 
451
+ document.addEventListener('DOMContentLoaded', () => {
452
+ initChart();
453
+ fetchMetrics();
454
+ setInterval(fetchMetrics, 20000);
455
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  </script>
457
  </body>
458
  </html>
459
+ '''
460
+ index_path = os.path.join('templates', 'index.html')
461
+ if not os.path.exists(index_path):
462
+ with open(index_path, 'w', encoding='utf-8') as f:
463
+ f.write(index_html_content)
464
+
465
+ if __name__ == '__main__':
466
+ create_app_structure()
467
+ port = int(os.environ.get('PORT', 7860))
468
+ # For production, use a proper WSGI server like Gunicorn
469
+ # gunicorn --bind 0.0.0.0:7860 app:app
470
+ app.run(host='0.0.0.0', port=port, debug=False)