nixaut-codelabs commited on
Commit
cb79076
·
verified ·
1 Parent(s): ad3ac5b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +437 -609
app.py CHANGED
@@ -1,703 +1,531 @@
1
- from flask import Flask, request, jsonify, render_template
2
  import os
3
- import uuid
4
  import time
5
  import threading
6
- import tiktoken
7
- import requests
8
  import base64
9
- from datetime import datetime, timedelta
10
- from collections import defaultdict, deque
11
- from detoxify import Detoxify
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  import logging
13
 
14
  logging.basicConfig(level=logging.INFO)
 
15
 
16
- app = Flask(__name__, static_folder='static', template_folder='templates')
17
 
18
- print("Loading Detoxify model... This may take a moment.")
19
- detoxify_model = Detoxify('multilingual')
20
- print("Detoxify model loaded successfully.")
21
 
22
- API_KEY = os.getenv('API_KEY', 'your-api-key-here')
 
 
 
 
23
 
24
- request_durations = deque(maxlen=100)
25
- request_timestamps = deque(maxlen=1000)
26
-
27
- daily_requests = defaultdict(int)
28
- daily_tokens = defaultdict(int)
29
  concurrent_requests = 0
30
- concurrent_requests_lock = threading.Lock()
31
-
32
- encoding = tiktoken.get_encoding("cl100k_base")
33
-
34
- NSFW_MODEL_URL = "https://teachablemachine.withgoogle.com/models/gJOADmf_u/"
35
-
36
- def count_tokens(text):
37
- return len(encoding.encode(text))
38
-
39
- def classify_image_nsfw(image_data):
40
- try:
41
- response = requests.post(
42
- NSFW_MODEL_URL,
43
- json={"instances": [{"image_bytes": {"b64": image_data}}]},
44
- timeout=10
45
- )
46
- if response.status_code == 200:
47
- predictions = response.json().get('predictions', [{}])[0]
48
- nsfw_score = predictions.get('nsfw', 0.0)
49
- return nsfw_score > 0.5
50
- return False
51
- except:
52
- return False
53
-
54
- def transform_predictions(prediction_dict):
55
- category_keys = [
56
- "toxicity", "severe_toxicity", "obscene", "threat",
57
- "insult", "identity_attack", "sexual_explicit"
58
- ]
59
-
60
- scores = {}
61
- for key in category_keys:
62
- scores[key] = float(prediction_dict.get(key, 0.0))
63
-
64
- threshold = 0.5
65
- bool_categories = {key: (scores[key] > threshold) for key in category_keys}
66
- flagged = any(bool_categories.values())
67
-
68
- return flagged, bool_categories, scores
69
 
70
- def track_request_metrics(start_time, tokens_count):
71
- end_time = time.time()
72
- duration = end_time - start_time
73
-
74
- app.logger.info(f"Server-side processing for moderation request took {duration * 1000:.2f} ms.")
75
-
76
- request_durations.append(duration)
77
- request_timestamps.append(datetime.now())
 
 
78
 
79
- today = datetime.now().strftime("%Y-%m-%d")
80
- daily_requests[today] += 1
81
- daily_tokens[today] += tokens_count
 
82
 
83
- def get_performance_metrics():
84
- global concurrent_requests
85
- with concurrent_requests_lock:
86
- current_concurrent = concurrent_requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- if not request_durations:
89
- avg_request_time = 0
90
- peak_request_time = 0
91
- else:
92
- avg_request_time = sum(request_durations) / len(request_durations)
93
- peak_request_time = max(request_durations)
94
-
95
- now = datetime.now()
96
- one_minute_ago = now - timedelta(seconds=60)
97
- requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- today = now.strftime("%Y-%m-%d")
100
- today_requests = daily_requests.get(today, 0)
101
- today_tokens = daily_tokens.get(today, 0)
102
 
103
- last_7_days = []
104
- for i in range(7):
105
- date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
106
- last_7_days.append({
107
- "date": date,
108
- "requests": daily_requests.get(date, 0),
109
- "tokens": daily_tokens.get(date, 0)
110
- })
111
 
112
- return {
113
- "avg_request_time_ms": avg_request_time * 1000,
114
- "peak_request_time_ms": peak_request_time * 1000,
115
- "requests_per_minute": requests_last_minute,
116
- "concurrent_requests": current_concurrent,
117
- "today_requests": today_requests,
118
- "today_tokens": today_tokens,
119
- "last_7_days": last_7_days
120
  }
121
 
122
- @app.route('/')
123
- def home():
124
- return render_template('index.html')
125
 
126
- @app.route('/v1/moderations', methods=['POST'])
127
- def moderations():
128
- global concurrent_requests
129
-
130
- with concurrent_requests_lock:
131
- concurrent_requests += 1
132
 
133
- start_time = time.time()
134
- total_tokens = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- response = None
137
- try:
138
- auth_header = request.headers.get('Authorization')
139
- if not auth_header or not auth_header.startswith("Bearer "):
140
- response = jsonify({"error": "Unauthorized"}), 401
141
- return response
142
-
143
- provided_api_key = auth_header.split(" ")[1]
144
- if provided_api_key != API_KEY:
145
- response = jsonify({"error": "Unauthorized"}), 401
146
- return response
 
 
 
 
 
 
 
 
 
 
147
 
148
- data = request.get_json()
149
- raw_input = data.get('input')
150
-
151
- if raw_input is None:
152
- response = jsonify({"error": "Invalid input, 'input' field is required"}), 400
153
- return response
154
-
155
- if isinstance(raw_input, str):
156
- texts = [raw_input]
157
- elif isinstance(raw_input, list):
158
- texts = raw_input
159
  else:
160
- response = jsonify({"error": "Invalid input format, expected string or list of strings"}), 400
161
- return response
162
-
163
- if not texts:
164
- response = jsonify({"error": "Input list cannot be empty"}), 400
165
- return response
166
 
167
- if len(texts) > 10:
168
- response = jsonify({"error": "Too many input items. Maximum 10 allowed."}), 400
169
- return response
170
-
171
- results = []
172
- for text in texts:
173
- if not isinstance(text, str):
174
- if isinstance(text, dict) and 'type' in text and text['type'] == 'image_url':
175
- image_data = text.get('image_url', {}).get('url', '')
176
- if image_data.startswith('data:image'):
177
- image_b64 = image_data.split(',')[1]
178
- is_nsfw = classify_image_nsfw(image_b64)
179
-
180
- results.append({
181
- "flagged": is_nsfw,
182
- "categories": {
183
- "sexual": is_nsfw,
184
- "sexual_explicit": is_nsfw
185
- },
186
- "category_scores": {
187
- "sexual": 0.9 if is_nsfw else 0.1,
188
- "sexual_explicit": 0.9 if is_nsfw else 0.1
189
- }
190
- })
191
- continue
192
- response = jsonify({"error": "Each input item must be a string or image object"}), 400
193
- return response
194
-
195
- if len(text.encode('utf-8')) > 300000:
196
- response = jsonify({"error": "Each input item must have a maximum of 300k bytes."}), 400
197
- return response
198
-
199
- total_tokens += count_tokens(text)
200
-
201
- predictions = detoxify_model.predict([text])
202
- single_prediction = {key: value[0] for key, value in predictions.items()}
203
- flagged, bool_categories, scores = transform_predictions(single_prediction)
204
-
205
- results.append({
206
- "flagged": flagged,
207
- "categories": bool_categories,
208
- "category_scores": scores,
209
- })
210
-
211
- response_data = {
212
- "id": "modr-" + uuid.uuid4().hex[:24],
213
- "model": "text-moderation-detoxify-multilingual",
214
- "results": results
215
- }
216
-
217
- response = jsonify(response_data)
218
- return response
219
-
220
- except Exception as e:
221
- app.logger.error(f"An error occurred: {e}", exc_info=True)
222
- response = jsonify({"error": "An internal server error occurred."}), 500
223
- return response
224
- finally:
225
- if response and (response[1] < 400 if isinstance(response, tuple) else response.status_code < 400):
226
- track_request_metrics(start_time, total_tokens)
227
-
228
- with concurrent_requests_lock:
229
- concurrent_requests -= 1
230
-
231
- @app.route('/v1/metrics', methods=['GET'])
232
- def metrics():
233
- auth_header = request.headers.get('Authorization')
234
- if not auth_header or not auth_header.startswith("Bearer "):
235
- return jsonify({"error": "Unauthorized"}), 401
236
 
237
- provided_api_key = auth_header.split(" ")[1]
238
- if provided_api_key != API_KEY:
239
- return jsonify({"error": "Unauthorized"}), 401
 
 
240
 
241
- return jsonify(get_performance_metrics())
242
-
243
 
244
- def create_directories_and_files():
245
- os.makedirs('templates', exist_ok=True)
246
- os.makedirs('static', exist_ok=True)
247
-
248
- index_path = os.path.join('templates', 'index.html')
249
- if not os.path.exists(index_path):
250
- with open(index_path, 'w', encoding='utf-8') as f:
251
- f.write('''<!DOCTYPE html>
252
  <html lang="en">
253
  <head>
254
  <meta charset="UTF-8">
255
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
256
- <title>Smart Moderator</title>
257
  <script src="https://cdn.tailwindcss.com"></script>
258
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
259
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
260
- <script>
261
- tailwind.config = {
262
- darkMode: 'class',
263
- theme: {
264
- extend: {
265
- colors: {
266
- primary: {
267
- 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
268
- 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a',
269
- }
270
- }
271
- }
272
- }
273
- }
274
- </script>
275
  <style>
276
  .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
277
- .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); }
278
  .glass-effect {
279
- background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
 
 
280
  border: 1px solid rgba(255, 255, 255, 0.2);
281
  }
282
- .dark .glass-effect { background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(100, 116, 139, 0.3); }
283
- .category-card { transition: all 0.3s ease; }
284
- .category-card:hover { transform: translateY(-5px); }
285
- .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; }
286
- @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
287
  </style>
288
  </head>
289
- <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen font-sans">
290
- <header class="gradient-bg text-white shadow-lg">
291
- <div class="container mx-auto px-4 py-6 flex justify-between items-center">
292
- <div class="flex items-center space-x-3">
293
- <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center">
294
- <i class="fas fa-shield-alt text-primary-600 text-xl"></i>
295
- </div>
296
- <h1 class="text-2xl font-bold">Smart Moderator</h1>
297
- </div>
298
- <div class="flex items-center space-x-4">
299
- <button id="refreshMetrics" class="glass-effect px-4 py-2 rounded-lg hover:bg-white/20 transition">
300
- <i class="fas fa-sync-alt mr-2"></i>Refresh Metrics
301
- </button>
302
- <button id="darkModeToggle" class="glass-effect p-2 rounded-lg hover:bg-white/20 transition">
303
- <i class="fas fa-moon dark:hidden"></i>
304
- <i class="fas fa-sun hidden dark:inline"></i>
305
- </button>
306
- </div>
307
- </div>
308
- </header>
309
-
310
- <main class="container mx-auto px-4 py-8">
311
- <section class="mb-12">
312
- <h2 class="text-2xl font-bold mb-6 flex items-center">
313
- <i class="fas fa-chart-line mr-3 text-primary-600"></i>
314
- Performance Metrics
315
- </h2>
316
-
317
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
318
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
319
- <div class="flex items-center justify-between">
320
- <div>
321
- <p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response (last 100)</p>
322
- <p class="text-2xl font-bold" id="avgResponseTime">0ms</p>
323
- </div>
324
- <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
325
- <i class="fas fa-clock text-primary-600 dark:text-primary-400"></i>
326
- </div>
327
- </div>
328
- </div>
329
-
330
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
331
- <div class="flex items-center justify-between">
332
- <div>
333
- <p class="text-gray-500 dark:text-gray-400 text-sm">Requests / Minute</p>
334
- <p class="text-2xl font-bold" id="requestsPerMinute">0</p>
335
- </div>
336
- <div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
337
- <i class="fas fa-tachometer-alt text-green-600 dark:text-green-400"></i>
338
  </div>
339
- </div>
340
- </div>
341
-
342
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
343
- <div class="flex items-center justify-between">
344
- <div>
345
- <p class="text-gray-500 dark:text-gray-400 text-sm">Peak Response (last 100)</p>
346
- <p class="text-2xl font-bold" id="peakResponseTime">0ms</p>
347
- </div>
348
- <div class="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
349
- <i class="fas fa-exclamation-triangle text-red-600 dark:text-red-400"></i>
350
- </div>
351
- </div>
352
- </div>
353
-
354
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
355
- <div class="flex items-center justify-between">
356
- <div>
357
- <p class="text-gray-500 dark:text-gray-400 text-sm">Today's Requests</p>
358
- <p class="text-2xl font-bold" id="todayRequests">0</p>
359
  </div>
360
- <div class="w-12 h-12 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
361
- <i class="fas fa-list-ol text-yellow-600 dark:text-yellow-400"></i>
 
 
 
362
  </div>
363
  </div>
364
  </div>
365
- </div>
366
-
367
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
368
- <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3>
369
- <div class="h-64">
370
- <canvas id="activityChart"></canvas>
371
- </div>
372
- </div>
373
- </section>
374
-
375
- <section class="mb-12">
376
- <h2 class="text-2xl font-bold mb-6 flex items-center">
377
- <i class="fas fa-code mr-3 text-primary-600"></i>
378
- API Tester
379
- </h2>
380
-
381
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
382
- <form id="apiTestForm">
383
- <div class="mb-6">
384
- <label class="block text-sm font-medium mb-2" for="apiKey">API Key</label>
385
- <input type="password" id="apiKey" class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="Enter your API key">
386
- </div>
387
-
388
- <div class="mb-6">
389
- <label class="block text-sm font-medium mb-2">Text Inputs</label>
390
- <div id="textInputsContainer">
391
- <div class="text-input-group mb-4">
392
- <textarea class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" rows="3" placeholder="Enter text to moderate..."></textarea>
393
- <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700 hidden">
394
- <i class="fas fa-trash-alt mr-1"></i> Remove
395
- </button>
396
  </div>
397
  </div>
398
- <button type="button" id="addTextInput" class="mt-2 text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300">
399
- <i class="fas fa-plus-circle mr-1"></i> Add another text input
400
- </button>
401
- </div>
402
-
403
- <div class="flex justify-between items-center">
404
- <div>
405
- <button type="submit" id="analyzeBtn" class="bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-6 rounded-lg transition">
406
- <i class="fas fa-search mr-2"></i> Analyze Text
407
- </button>
408
- <button type="button" id="clearBtn" class="ml-2 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium py-2 px-6 rounded-lg transition">
409
- <i class="fas fa-eraser mr-2"></i> Clear
410
- </button>
411
- </div>
412
- <div class="text-sm text-gray-500 dark:text-gray-400">
413
- <i class="fas fa-info-circle mr-1"></i> Maximum 10 text inputs allowed
414
  </div>
 
 
 
 
 
 
415
  </div>
416
- </form>
417
- </div>
418
- </section>
419
-
420
- <section id="resultsSection" class="hidden">
421
- <h2 class="text-2xl font-bold mb-6 flex items-center">
422
- <i class="fas fa-clipboard-check mr-3 text-primary-600"></i>
423
- Analysis Results
424
- </h2>
425
-
426
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 mb-6">
427
- <div class="flex justify-between items-center mb-4">
428
- <h3 class="text-lg font-semibold">Summary</h3>
429
- <div class="text-sm text-gray-500 dark:text-gray-400">
430
- <i class="fas fa-stopwatch mr-1"></i> Round-trip time: <span id="responseTime">0ms</span>
431
- </div>
432
- </div>
433
-
434
- <div id="resultsContainer" class="space-y-6">
435
  </div>
436
  </div>
437
- </section>
438
-
439
- <section>
440
- <h2 class="text-2xl font-bold mb-6 flex items-center">
441
- <i class="fas fa-book mr-3 text-primary-600"></i>
442
- API Documentation
443
- </h2>
444
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
445
- <h3 class="text-lg font-semibold mb-4">Endpoint</h3>
446
- <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">POST /v1/moderations</div>
447
- <h3 class="text-lg font-semibold mb-4">Site URL</h3>
448
- <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">https://nixaut-codelabs-smart-moderator.hf.space</div>
449
- <h3 class="text-lg font-semibold mb-4">Request Body</h3>
450
- <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 overflow-x-auto">
451
- <pre class="text-sm"><code>{
452
- "input": "Text to moderate"
453
- }</code></pre>
454
- </div>
455
- <h3 class="text-lg font-semibold mb-4">Response</h3>
456
- <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
457
- <pre class="text-sm"><code>{
458
- "id": "modr-1234567890abcdef",
459
- "model": "text-moderation-detoxify-multilingual",
460
- "results": [
461
- {
462
- "flagged": true,
463
- "categories": {
464
- "toxicity": true,
465
- "severe_toxicity": false,
466
- /* ... other categories */
467
- },
468
- "category_scores": {
469
- "toxicity": 0.95,
470
- "severe_toxicity": 0.1,
471
- /* ... other scores */
472
- }
473
- }
474
- ]
475
- }</code></pre>
476
- </div>
477
- </div>
478
- </section>
479
- </main>
480
 
481
- <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
482
- <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400">
483
- 2025 Smart Moderator. All rights reserved.
484
- </div>
485
- </footer>
 
486
 
487
  <script>
488
- const darkModeToggle = document.getElementById('darkModeToggle');
489
- const html = document.documentElement;
 
 
 
 
 
 
 
 
 
 
490
 
491
- if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
492
- html.classList.add('dark');
493
- }
494
-
495
- darkModeToggle.addEventListener('click', () => {
496
- html.classList.toggle('dark');
497
- localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
 
 
498
  });
499
 
500
- let activityChart;
501
-
502
- function initActivityChart() {
503
- if (activityChart) { activityChart.destroy(); }
504
- const ctx = document.getElementById('activityChart').getContext('2d');
505
- const isDarkMode = document.documentElement.classList.contains('dark');
506
- const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
507
- const textColor = isDarkMode ? '#e5e7eb' : '#374151';
508
-
509
- activityChart = new Chart(ctx, {
510
- type: 'bar',
511
- data: { labels: [], datasets: [
512
- { label: 'Requests', data: [], backgroundColor: 'rgba(59, 130, 246, 0.6)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1 },
513
- { label: 'Tokens', data: [], backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgba(16, 185, 129, 1)', borderWidth: 1, yAxisID: 'y1' }
514
- ]},
515
- options: {
516
- responsive: true, maintainAspectRatio: false,
517
- scales: {
518
- y: { beginAtZero: true, position: 'left', title: { display: true, text: 'Requests', color: textColor }, ticks: { color: textColor }, grid: { color: gridColor } },
519
- y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Tokens', color: textColor }, ticks: { color: textColor }, grid: { drawOnChartArea: false } }
520
- },
521
- plugins: { legend: { labels: { color: textColor } } }
522
- }
523
- });
524
- }
525
-
526
- async function fetchMetrics() {
527
- const apiKey = document.getElementById('apiKey').value || 'temp-key-for-metrics';
528
- try {
529
- const response = await fetch('/v1/metrics', {
530
- headers: { 'Authorization': 'Bearer ' + apiKey }
531
- });
532
- if (!response.ok) {
533
- const error = await response.json();
534
- console.error('Failed to fetch metrics:', error.error);
535
- if(response.status === 401) {
536
- }
537
- return;
538
- }
539
- const data = await response.json();
540
- updateMetricsDisplay(data);
541
- } catch (error) {
542
- console.error('Error fetching metrics:', error);
543
  }
544
- }
545
-
546
- function updateMetricsDisplay(data) {
547
- document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
548
- document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms';
549
- document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
550
- document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString();
551
-
552
- if (activityChart) {
553
- const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse();
554
- const requests = data.last_7_days.map(d => d.requests).reverse();
555
- const tokens = data.last_7_days.map(d => d.tokens).reverse();
556
-
557
- activityChart.data.labels = labels;
558
- activityChart.data.datasets[0].data = requests;
559
- activityChart.data.datasets[1].data = tokens;
560
- activityChart.update();
561
  }
 
 
 
562
  }
563
-
564
- document.getElementById('addTextInput').addEventListener('click', () => {
565
- const container = document.getElementById('textInputsContainer');
566
- if (container.children.length >= 10) {
567
- alert('Maximum 10 text inputs allowed');
568
- return;
569
- }
570
- const newGroup = container.firstElementChild.cloneNode(true);
571
- newGroup.querySelector('textarea').value = '';
572
- newGroup.querySelector('.remove-input').classList.remove('hidden');
573
- container.appendChild(newGroup);
574
- updateRemoveButtons();
575
- });
576
 
577
- document.getElementById('textInputsContainer').addEventListener('click', function(e) {
578
- if (e.target.closest('.remove-input')) {
579
- e.target.closest('.text-input-group').remove();
580
- updateRemoveButtons();
 
581
  }
582
- });
583
 
584
- function updateRemoveButtons() {
585
- const groups = document.querySelectorAll('.text-input-group');
586
- groups.forEach(group => {
587
- group.querySelector('.remove-input').classList.toggle('hidden', groups.length <= 1);
 
 
 
 
588
  });
589
- }
590
 
591
- document.getElementById('clearBtn').addEventListener('click', () => {
592
- document.getElementById('apiTestForm').reset();
593
- const container = document.getElementById('textInputsContainer');
594
- container.innerHTML = container.firstElementChild.outerHTML;
595
- container.querySelector('textarea').value = '';
596
- updateRemoveButtons();
597
- document.getElementById('resultsSection').classList.add('hidden');
598
- });
599
 
600
- document.getElementById('apiTestForm').addEventListener('submit', async (e) => {
601
- e.preventDefault();
602
-
603
- const apiKey = document.getElementById('apiKey').value;
604
- if (!apiKey) { alert('Please enter your API key'); return; }
605
-
606
- const texts = Array.from(document.querySelectorAll('#textInputsContainer textarea'))
607
- .map(t => t.value.trim()).filter(Boolean);
608
- if (texts.length === 0) { alert('Please enter at least one text to analyze'); return; }
609
-
610
- const analyzeBtn = document.getElementById('analyzeBtn');
611
- const originalBtnContent = analyzeBtn.innerHTML;
612
- analyzeBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></div> Analyzing...';
613
  analyzeBtn.disabled = true;
614
-
615
- const startTime = Date.now();
616
-
617
  try {
618
  const response = await fetch('/v1/moderations', {
619
  method: 'POST',
620
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
621
- body: JSON.stringify({ input: texts.length === 1 ? texts[0] : texts })
 
 
 
622
  });
623
-
624
- const responseTime = Date.now() - startTime;
625
-
626
  const data = await response.json();
627
- if (!response.ok) throw new Error(data.error || 'Failed to analyze text');
628
-
629
- displayResults(data, responseTime, texts);
630
- fetchMetrics();
631
-
632
  } catch (error) {
633
- alert('Error: ' + error.message);
 
634
  } finally {
635
- analyzeBtn.innerHTML = originalBtnContent;
636
  analyzeBtn.disabled = false;
 
637
  }
638
  });
639
 
640
- function displayResults(data, responseTime, texts) {
641
- const resultsSection = document.getElementById('resultsSection');
642
- const resultsContainer = document.getElementById('resultsContainer');
643
-
644
- document.getElementById('responseTime').textContent = responseTime + 'ms';
645
  resultsContainer.innerHTML = '';
646
-
647
- data.results.forEach((result, index) => {
648
- const resultCard = document.createElement('div');
649
- resultCard.className = 'border border-gray-200 dark:border-gray-700 rounded-lg p-4';
650
-
651
- const flaggedBadge = result.flagged
652
- ? '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100"><i class="fas fa-exclamation-triangle mr-1"></i> Flagged</span>'
653
- : '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100"><i class="fas fa-check-circle mr-1"></i> Safe</span>';
654
-
655
- let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => {
656
- const isFlagged = result.categories[category];
657
- const categoryClass = isFlagged ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400';
658
- const scoreClass = score > 0.7 ? 'text-red-600 dark:text-red-400' : score > 0.4 ? 'text-yellow-600 dark:text-yellow-400' : 'text-green-600 dark:text-green-400';
659
- return `
660
- <div class="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
661
- <span class="font-medium capitalize">${category.replace(/_/g, ' ')}</span>
662
- <div class="flex items-center">
663
- <span class="text-sm ${scoreClass} font-mono">${score.toFixed(4)}</span>
664
- </div>
665
- </div>
666
- `;
667
- }).join('');
668
 
669
- resultCard.innerHTML = `
670
- <div class="flex justify-between items-start mb-3">
671
- <h4 class="text-lg font-semibold">Input ${index + 1}</h4>
672
- ${flaggedBadge}
 
 
 
 
 
673
  </div>
674
- <blockquote class="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg text-sm border-l-4 border-gray-300 dark:border-gray-500">${texts[index]}</blockquote>
675
- <div class="category-card">
676
- <h5 class="font-medium mb-2">Category Scores</h5>
677
- <div class="bg-white dark:bg-gray-800/50 rounded-lg p-2">
678
- ${categoriesHtml}
679
- </div>
 
 
 
680
  </div>
 
681
  `;
682
- resultsContainer.appendChild(resultCard);
683
  });
684
-
685
  resultsSection.classList.remove('hidden');
686
- resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
687
  }
688
 
689
- document.addEventListener('DOMContentLoaded', () => {
690
- initActivityChart();
691
- document.getElementById('refreshMetrics').addEventListener('click', fetchMetrics);
692
- fetchMetrics();
693
- setInterval(fetchMetrics, 15000);
694
- });
 
 
 
 
 
 
 
 
 
 
 
695
  </script>
696
  </body>
697
  </html>
698
- ''')
699
 
700
- if __name__ == '__main__':
701
- create_directories_and_files()
702
- port = int(os.getenv('PORT', 7860))
703
- app.run(host='0.0.0.0', port=port, debug=True, use_reloader=False)
 
 
1
  import os
 
2
  import time
3
  import threading
4
+ import torch
 
5
  import base64
6
+ import io
7
+ import requests
8
+ import uuid
9
+ import numpy as np
10
+ from typing import List, Dict, Any, Optional, Union
11
+ from fastapi import FastAPI, HTTPException, Depends, Request
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, field_validator
16
+ from dotenv import load_dotenv
17
+ from huggingface_hub import snapshot_download
18
+ from transformers import AutoTokenizer, AutoModelForCausalLM
19
+ from collections import deque
20
+ from PIL import Image
21
+ from tensorflow.keras.models import load_model
22
+ from urllib.request import urlretrieve
23
+ import uvicorn
24
  import logging
25
 
26
  logging.basicConfig(level=logging.INFO)
27
+ logger = logging.getLogger(__name__)
28
 
29
+ load_dotenv()
30
 
31
+ os.makedirs("templates", exist_ok=True)
32
+ os.makedirs("static", exist_ok=True)
33
+ os.makedirs("image_model", exist_ok=True)
34
 
35
+ app = FastAPI(
36
+ title="Multimodal AI Content Moderation API",
37
+ description="An advanced, multilingual, and multimodal content moderation API.",
38
+ version="1.0.0"
39
+ )
40
 
41
+ request_times = deque(maxlen=100)
 
 
 
 
42
  concurrent_requests = 0
43
+ request_lock = threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ @app.middleware("http")
46
+ async def track_metrics(request: Request, call_next):
47
+ global concurrent_requests
48
+ with request_lock:
49
+ concurrent_requests += 1
50
+
51
+ start_time = time.time()
52
+ response = await call_next(request)
53
+ process_time = time.time() - start_time
54
+ request_times.append(process_time)
55
 
56
+ with request_lock:
57
+ concurrent_requests -= 1
58
+
59
+ return response
60
 
61
+ app.mount("/static", StaticFiles(directory="static"), name="static")
62
+ templates = Jinja2Templates(directory="templates")
63
+
64
+ device = "cuda" if torch.cuda.is_available() else "cpu"
65
+ logger.info(f"Using device: {device}")
66
+
67
+ def download_file(url, path):
68
+ if not os.path.exists(path):
69
+ logger.info(f"Downloading {os.path.basename(path)}...")
70
+ urlretrieve(url, path)
71
+
72
+ logger.info("Downloading and loading models...")
73
+
74
+ MODELS = {}
75
+
76
+ logger.info("Loading text moderation model: detoxify-multilingual")
77
+ from detoxify import Detoxify
78
+ MODELS['detoxify-multilingual'] = Detoxify('multilingual', device=device)
79
+ logger.info("Detoxify model loaded.")
80
+
81
+ GEMMA_REPO = "daniel-dona/gemma-3-270m-it"
82
+ LOCAL_GEMMA_DIR = os.path.join(os.getcwd(), "gemma_model")
83
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
84
+
85
+ def ensure_local_model(repo_id: str, local_dir: str) -> str:
86
+ os.makedirs(local_dir, exist_ok=True)
87
+ snapshot_download(
88
+ repo_id=repo_id,
89
+ local_dir=local_dir,
90
+ local_dir_use_symlinks=False,
91
+ resume_download=True,
92
+ )
93
+ return local_dir
94
+
95
+ logger.info("Loading text moderation model: gemma-3-270m-it")
96
+ gemma_path = ensure_local_model(GEMMA_REPO, LOCAL_GEMMA_DIR)
97
+ gemma_tokenizer = AutoTokenizer.from_pretrained(gemma_path, local_files_only=True)
98
+ gemma_model = AutoModelForCausalLM.from_pretrained(
99
+ gemma_path,
100
+ local_files_only=True,
101
+ torch_dtype=torch.float32,
102
+ device_map=device
103
+ )
104
+ gemma_model.eval()
105
+ MODELS['gemma-3-270m-it'] = (gemma_model, gemma_tokenizer)
106
+ logger.info("Gemma model loaded.")
107
+
108
+ NSFW_MODEL_URL = "https://teachablemachine.withgoogle.com/models/gJOADmf_u/keras_model.h5"
109
+ NSFW_LABELS_URL = "https://teachablemachine.withgoogle.com/models/gJOADmf_u/labels.txt"
110
+ NSFW_MODEL_PATH = "image_model/keras_model.h5"
111
+ NSFW_LABELS_PATH = "image_model/labels.txt"
112
+
113
+ download_file(NSFW_MODEL_URL, NSFW_MODEL_PATH)
114
+ download_file(NSFW_LABELS_URL, NSFW_LABELS_PATH)
115
+
116
+ logger.info("Loading image moderation model: nsfw-image-classifier")
117
+ nsfw_model = load_model(NSFW_MODEL_PATH, compile=False)
118
+ with open(NSFW_LABELS_PATH, "r") as f:
119
+ nsfw_labels = [line.strip().split(' ')[1] for line in f]
120
+ MODELS['nsfw-image-classifier'] = (nsfw_model, nsfw_labels)
121
+ logger.info("NSFW image model loaded.")
122
+
123
+ class InputItem(BaseModel):
124
+ text: Optional[str] = None
125
+ image_url: Optional[str] = None
126
+ image_base64: Optional[str] = None
127
+
128
+ @field_validator('*')
129
+ @classmethod
130
+ def check_one_field(cls, v, info):
131
+ if sum(1 for value in info.data.values() if value is not None) > 1:
132
+ raise ValueError("Only one of text, image_url, or image_base64 can be provided.")
133
+ return v
134
+
135
+ class ModerationRequest(BaseModel):
136
+ input: Union[str, List[Union[str, InputItem]]] = Field(..., max_length=10)
137
+ model: str = "auto"
138
+
139
+ class ModerationResponse(BaseModel):
140
+ id: str
141
+ model: str
142
+ results: List[Dict[str, Any]]
143
+
144
+ def format_openai_result(flagged: bool, categories: Dict[str, bool], scores: Dict[str, float]):
145
+ return {
146
+ "flagged": flagged,
147
+ "categories": categories,
148
+ "category_scores": scores
149
+ }
150
+
151
+ def classify_text_detoxify(text: str):
152
+ predictions = MODELS['detoxify-multilingual'].predict(text)
153
 
154
+ categories = {
155
+ "hate": predictions['identity_attack'] > 0.5 or predictions['toxicity'] > 0.7,
156
+ "hate/threatening": predictions['threat'] > 0.5,
157
+ "harassment": predictions['insult'] > 0.5,
158
+ "harassment/threatening": predictions['threat'] > 0.5,
159
+ "self-harm": predictions['severe_toxicity'] > 0.6,
160
+ "sexual": predictions['sexual_explicit'] > 0.5,
161
+ "sexual/minors": False,
162
+ "violence": predictions['toxicity'] > 0.8,
163
+ "violence/graphic": predictions['severe_toxicity'] > 0.8,
164
+ }
165
+ scores = {
166
+ "hate": float(max(predictions.get('identity_attack', 0), predictions.get('toxicity', 0))),
167
+ "hate/threatening": float(predictions.get('threat', 0)),
168
+ "harassment": float(predictions.get('insult', 0)),
169
+ "harassment/threatening": float(predictions.get('threat', 0)),
170
+ "self-harm": float(predictions.get('severe_toxicity', 0)),
171
+ "sexual": float(predictions.get('sexual_explicit', 0)),
172
+ "sexual/minors": 0.0,
173
+ "violence": float(predictions.get('toxicity', 0)),
174
+ "violence/graphic": float(predictions.get('severe_toxicity', 0)),
175
+ }
176
+ flagged = any(categories.values())
177
+ return format_openai_result(flagged, categories, scores)
178
+
179
+ def process_image(image_data: bytes) -> np.ndarray:
180
+ image = Image.open(io.BytesIO(image_data)).convert("RGB")
181
+ image = image.resize((224, 224))
182
+ image_array = np.asarray(image)
183
+ normalized_image_array = (image_array.astype(np.float32) / 127.5) - 1
184
+ return np.expand_dims(normalized_image_array, axis=0)
185
+
186
+ def classify_image(image_data: bytes):
187
+ model, labels = MODELS['nsfw-image-classifier']
188
+ processed_image = process_image(image_data)
189
+ prediction = model.predict(processed_image, verbose=0)
190
 
191
+ scores = {label: float(score) for label, score in zip(labels, prediction[0])}
192
+ is_nsfw = scores.get('nsfw', 0.0) > 0.7
 
193
 
194
+ categories = {
195
+ "hate": False, "hate/threatening": False, "harassment": False, "harassment/threatening": False,
196
+ "self-harm": False, "sexual": is_nsfw, "sexual/minors": is_nsfw, "violence": False, "violence/graphic": is_nsfw,
197
+ }
 
 
 
 
198
 
199
+ category_scores = {
200
+ "hate": 0.0, "hate/threatening": 0.0, "harassment": 0.0, "harassment/threatening": 0.0,
201
+ "self-harm": 0.0, "sexual": scores.get('nsfw', 0.0), "sexual/minors": scores.get('nsfw', 0.0),
202
+ "violence": 0.0, "violence/graphic": scores.get('nsfw', 0.0),
 
 
 
 
203
  }
204
 
205
+ return format_openai_result(is_nsfw, categories, category_scores)
 
 
206
 
207
+ def get_api_key(request: Request):
208
+ api_key = request.headers.get("Authorization")
209
+ if not api_key or not api_key.startswith("Bearer "):
210
+ raise HTTPException(status_code=401, detail="API key is missing or invalid.")
 
 
211
 
212
+ api_key = api_key.split(" ")[1]
213
+ env_api_key = os.getenv("API_KEY")
214
+ if not env_api_key or api_key != env_api_key:
215
+ raise HTTPException(status_code=401, detail="Invalid API key.")
216
+ return api_key
217
+
218
+ @app.get("/", response_class=HTMLResponse)
219
+ async def get_home(request: Request):
220
+ return templates.TemplateResponse("index.html", {"request": request})
221
+
222
+ @app.get("/v1/metrics", response_class=JSONResponse)
223
+ async def get_metrics(api_key: str = Depends(get_api_key)):
224
+ avg_time = sum(request_times) / len(request_times) if request_times else 0
225
+ return {
226
+ "concurrent_requests": concurrent_requests,
227
+ "average_response_time_ms_last_100": avg_time * 1000,
228
+ "tracked_request_count": len(request_times)
229
+ }
230
+
231
+ @app.post("/v1/moderations", response_model=ModerationResponse)
232
+ async def moderate_content(
233
+ request: ModerationRequest,
234
+ api_key: str = Depends(get_api_key)
235
+ ):
236
+ inputs = request.input
237
+ if isinstance(inputs, str):
238
+ inputs = [inputs]
239
+
240
+ if len(inputs) > 10:
241
+ raise HTTPException(status_code=400, detail="Maximum of 10 items per request is allowed.")
242
+
243
+ results = []
244
 
245
+ for item in inputs:
246
+ result = None
247
+ if isinstance(item, str):
248
+ result = classify_text_detoxify(item)
249
+ elif isinstance(item, InputItem):
250
+ if item.text:
251
+ result = classify_text_detoxify(item.text)
252
+ elif item.image_url:
253
+ try:
254
+ response = requests.get(item.image_url, stream=True, timeout=10)
255
+ response.raise_for_status()
256
+ image_bytes = response.content
257
+ result = classify_image(image_bytes)
258
+ except requests.RequestException as e:
259
+ raise HTTPException(status_code=400, detail=f"Could not fetch image from URL: {e}")
260
+ elif item.image_base64:
261
+ try:
262
+ image_bytes = base64.b64decode(item.image_base64)
263
+ result = classify_image(image_bytes)
264
+ except Exception as e:
265
+ raise HTTPException(status_code=400, detail=f"Invalid base64 image data: {e}")
266
 
267
+ if result:
268
+ results.append(result)
 
 
 
 
 
 
 
 
 
269
  else:
270
+ raise HTTPException(status_code=400, detail="Invalid input item format provided.")
 
 
 
 
 
271
 
272
+ model_name = request.model if request.model != "auto" else "multimodal-moderator"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ response_data = {
275
+ "id": f"modr-{uuid.uuid4().hex}",
276
+ "model": model_name,
277
+ "results": results,
278
+ }
279
 
280
+ return response_data
 
281
 
282
+ with open("templates/index.html", "w") as f:
283
+ f.write("""
284
+ <!DOCTYPE html>
 
 
 
 
 
285
  <html lang="en">
286
  <head>
287
  <meta charset="UTF-8">
288
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
289
+ <title>Multimodal AI Content Moderator</title>
290
  <script src="https://cdn.tailwindcss.com"></script>
 
291
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  <style>
293
  .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
 
294
  .glass-effect {
295
+ background: rgba(255, 255, 255, 0.1);
296
+ backdrop-filter: blur(10px);
297
+ border-radius: 10px;
298
  border: 1px solid rgba(255, 255, 255, 0.2);
299
  }
 
 
 
 
 
300
  </style>
301
  </head>
302
+ <body class="min-h-screen gradient-bg text-white font-sans">
303
+ <div class="container mx-auto px-4 py-8">
304
+ <header class="text-center mb-10">
305
+ <h1 class="text-4xl md:text-5xl font-bold mb-4">Multimodal AI Content Moderator</h1>
306
+ <p class="text-xl text-gray-200 max-w-3xl mx-auto">
307
+ Advanced, multilingual, and multimodal content analysis for text and images.
308
+ </p>
309
+ </header>
310
+
311
+ <main class="max-w-6xl mx-auto">
312
+ <div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
313
+ <div class="lg:col-span-2">
314
+ <div class="glass-effect p-6 rounded-xl h-full flex flex-col">
315
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
316
+ <i class="fas fa-cogs mr-3"></i>Configuration & Status
317
+ </h2>
318
+ <div class="mb-4">
319
+ <label class="block text-sm font-medium mb-2">API Key</label>
320
+ <input type="password" id="apiKey" placeholder="Enter your API key"
321
+ 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">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  </div>
323
+ <div class="mt-4 border-t border-white/20 pt-4">
324
+ <h3 class="text-lg font-semibold mb-3">Server Metrics</h3>
325
+ <div class="space-y-3 text-sm">
326
+ <div class="flex justify-between"><span>Concurrent Requests:</span> <span id="concurrentRequests" class="font-mono">0</span></div>
327
+ <div class="flex justify-between"><span>Avg. Response (last 100):</span> <span id="avgResponseTime" class="font-mono">0.00 ms</span></div>
328
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  </div>
330
+ <div class="mt-auto pt-4">
331
+ <h3 class="text-lg font-semibold mb-2">API Endpoint</h3>
332
+ <div class="bg-black/20 p-3 rounded-lg text-xs font-mono">
333
+ POST /v1/moderations
334
+ </div>
335
  </div>
336
  </div>
337
  </div>
338
+
339
+ <div class="lg:col-span-3">
340
+ <div class="glass-effect p-6 rounded-xl">
341
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
342
+ <i class="fas fa-vial mr-3"></i>Live Tester
343
+ </h2>
344
+
345
+ <div id="input-container" class="space-y-3 mb-4">
346
+ <div class="input-item">
347
+ <textarea name="text" rows="2" placeholder="Enter text to analyze..." class="w-full p-2 rounded bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  </div>
349
  </div>
350
+
351
+ <div class="flex space-x-2 mb-6">
352
+ <button id="add-text" class="text-sm bg-white/10 hover:bg-white/20 py-1 px-3 rounded"><i class="fas fa-plus mr-1"></i> Text</button>
353
+ <button id="add-image-url" class="text-sm bg-white/10 hover:bg-white/20 py-1 px-3 rounded"><i class="fas fa-link mr-1"></i> Image URL</button>
354
+ <button id="add-image-file" class="text-sm bg-white/10 hover:bg-white/20 py-1 px-3 rounded"><i class="fas fa-upload mr-1"></i> Image File</button>
 
 
 
 
 
 
 
 
 
 
 
355
  </div>
356
+
357
+ <input type="file" id="image-file-input" class="hidden" accept="image/*">
358
+
359
+ <button id="analyzeBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
360
+ <i class="fas fa-search mr-2"></i> Analyze Content
361
+ </button>
362
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  </div>
364
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
+ <div id="resultsSection" class="mt-8 hidden">
367
+ <h3 class="text-xl font-bold mb-4">Analysis Results</h3>
368
+ <div id="resultsContainer" class="space-y-4"></div>
369
+ </div>
370
+ </main>
371
+ </div>
372
 
373
  <script>
374
+ const apiKeyInput = document.getElementById('apiKey');
375
+ const inputContainer = document.getElementById('input-container');
376
+ const analyzeBtn = document.getElementById('analyzeBtn');
377
+ const resultsSection = document.getElementById('resultsSection');
378
+ const resultsContainer = document.getElementById('resultsContainer');
379
+ const concurrentRequestsEl = document.getElementById('concurrentRequests');
380
+ const avgResponseTimeEl = document.getElementById('avgResponseTime');
381
+ const imageFileInput = document.getElementById('image-file-input');
382
+
383
+ document.getElementById('add-text').addEventListener('click', () => addInput('text'));
384
+ document.getElementById('add-image-url').addEventListener('click', () => addInput('image_url'));
385
+ document.getElementById('add-image-file').addEventListener('click', () => imageFileInput.click());
386
 
387
+ imageFileInput.addEventListener('change', (event) => {
388
+ if (event.target.files && event.target.files[0]) {
389
+ const file = event.target.files[0];
390
+ const reader = new FileReader();
391
+ reader.onload = (e) => {
392
+ addInput('image_base64', e.target.result);
393
+ };
394
+ reader.readAsDataURL(file);
395
+ }
396
  });
397
 
398
+ function addInput(type, value = '') {
399
+ if (inputContainer.children.length >= 10) {
400
+ alert('Maximum of 10 items per request.');
401
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  }
403
+ const itemDiv = document.createElement('div');
404
+ itemDiv.className = 'input-item relative';
405
+ let inputHtml = '';
406
+ if (type === 'text') {
407
+ inputHtml = `<textarea name="text" rows="2" placeholder="Enter text..." class="w-full p-2 rounded bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400">${value}</textarea>`;
408
+ } else if (type === 'image_url') {
409
+ inputHtml = `<input type="text" name="image_url" placeholder="Enter image URL..." value="${value}" class="w-full p-2 rounded bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400">`;
410
+ } else if (type === 'image_base64') {
411
+ inputHtml = `
412
+ <div class="flex items-center space-x-2 p-2 rounded bg-white/10 border border-white/20">
413
+ <img src="${value}" class="h-10 w-10 object-cover rounded">
414
+ <span class="text-sm truncate">Image File Uploaded</span>
415
+ <input type="hidden" name="image_base64" value="${value.split(',')[1]}">
416
+ </div>
417
+ `;
 
 
418
  }
419
+ const removeBtn = `<button class="absolute -top-1 -right-1 text-red-400 hover:text-red-200 bg-gray-800 rounded-full h-5 w-5 flex items-center justify-center text-xs" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>`;
420
+ itemDiv.innerHTML = inputHtml + removeBtn;
421
+ inputContainer.appendChild(itemDiv);
422
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
+ analyzeBtn.addEventListener('click', async () => {
425
+ const apiKey = apiKeyInput.value.trim();
426
+ if (!apiKey) {
427
+ alert('Please enter your API key.');
428
+ return;
429
  }
 
430
 
431
+ const inputs = [];
432
+ document.querySelectorAll('.input-item').forEach(item => {
433
+ const text = item.querySelector('textarea[name="text"]');
434
+ const imageUrl = item.querySelector('input[name="image_url"]');
435
+ const imageBase64 = item.querySelector('input[name="image_base64"]');
436
+ if (text && text.value.trim()) inputs.push({ text: text.value.trim() });
437
+ if (imageUrl && imageUrl.value.trim()) inputs.push({ image_url: imageUrl.value.trim() });
438
+ if (imageBase64 && imageBase64.value) inputs.push({ image_base64: imageBase64.value });
439
  });
 
440
 
441
+ if (inputs.length === 0) {
442
+ alert('Please add at least one item to analyze.');
443
+ return;
444
+ }
 
 
 
 
445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  analyzeBtn.disabled = true;
447
+ analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Analyzing...';
448
+
 
449
  try {
450
  const response = await fetch('/v1/moderations', {
451
  method: 'POST',
452
+ headers: {
453
+ 'Content-Type': 'application/json',
454
+ 'Authorization': `Bearer ${apiKey}`
455
+ },
456
+ body: JSON.stringify({ input: inputs })
457
  });
458
+
 
 
459
  const data = await response.json();
460
+ if (!response.ok) {
461
+ throw new Error(data.detail || 'An error occurred.');
462
+ }
463
+ displayResults(data.results);
 
464
  } catch (error) {
465
+ alert(`Error: ${error.message}`);
466
+ resultsSection.classList.add('hidden');
467
  } finally {
 
468
  analyzeBtn.disabled = false;
469
+ analyzeBtn.innerHTML = '<i class="fas fa-search mr-2"></i> Analyze Content';
470
  }
471
  });
472
 
473
+ function displayResults(results) {
 
 
 
 
474
  resultsContainer.innerHTML = '';
475
+ results.forEach((result, index) => {
476
+ const flagged = result.flagged;
477
+ const card = document.createElement('div');
478
+ card.className = `glass-effect p-4 rounded-lg border-l-4 ${flagged ? 'border-red-400' : 'border-green-400'}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
+ let flaggedCategories = Object.entries(result.categories)
481
+ .filter(([_, value]) => value === true)
482
+ .map(([key]) => key)
483
+ .join(', ');
484
+
485
+ let scoresHtml = Object.entries(result.category_scores).map(([key, score]) => `
486
+ <div class="flex justify-between text-xs my-1">
487
+ <span>${key.replace(/_/g, ' ')}</span>
488
+ <span class="font-mono">${(score * 100).toFixed(2)}%</span>
489
  </div>
490
+ <div class="w-full bg-white/10 rounded-full h-1.5">
491
+ <div class="h-1.5 rounded-full ${score > 0.5 ? 'bg-red-400' : 'bg-green-400'}" style="width: ${score * 100}%"></div>
492
+ </div>
493
+ `).join('');
494
+
495
+ card.innerHTML = `
496
+ <div class="flex justify-between items-center mb-2">
497
+ <h4 class="font-bold">Item ${index + 1} - ${flagged ? 'FLAGGED' : 'SAFE'}</h4>
498
+ ${flagged ? `<span class="text-xs text-red-300">${flaggedCategories}</span>` : ''}
499
  </div>
500
+ <div>${scoresHtml}</div>
501
  `;
502
+ resultsContainer.appendChild(card);
503
  });
 
504
  resultsSection.classList.remove('hidden');
 
505
  }
506
 
507
+ async function fetchMetrics() {
508
+ const apiKey = apiKeyInput.value.trim();
509
+ if (!apiKey) return;
510
+ try {
511
+ const response = await fetch('/v1/metrics', {
512
+ headers: { 'Authorization': `Bearer ${apiKey}` }
513
+ });
514
+ if (response.ok) {
515
+ const data = await response.json();
516
+ concurrentRequestsEl.textContent = data.concurrent_requests;
517
+ avgResponseTimeEl.textContent = `${data.average_response_time_ms_last_100.toFixed(2)} ms`;
518
+ }
519
+ } catch (error) {
520
+ console.error("Failed to fetch metrics");
521
+ }
522
+ }
523
+ setInterval(fetchMetrics, 3000);
524
  </script>
525
  </body>
526
  </html>
527
+ """)
528
 
529
+ if __name__ == "__main__":
530
+ logger.info("Starting AI Content Moderator API...")
531
+ uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))