Spaces:
Sleeping
Sleeping
| import os | |
| import uuid | |
| import time | |
| import threading | |
| import io | |
| from datetime import datetime, timedelta | |
| from collections import defaultdict, deque | |
| from flask import Flask, request, jsonify, render_template | |
| from detoxify import Detoxify | |
| import numpy as np | |
| import requests | |
| from PIL import Image | |
| from tensorflow.keras.models import load_model | |
| app = Flask(__name__, static_folder='static', template_folder='templates') | |
| app.logger.setLevel('INFO') | |
| API_KEY = os.environ.get('API_KEY') | |
| if not API_KEY: | |
| raise ValueError("API_KEY environment variable not set.") | |
| print("Loading Detoxify model for text moderation...") | |
| detoxify_model = Detoxify('multilingual') | |
| print("Detoxify model loaded successfully.") | |
| MODEL_PATH = 'keras_model.h5' | |
| LABELS_PATH = 'labels.txt' | |
| image_model = None | |
| image_labels = None | |
| try: | |
| print("Loading Teachable Machine model for image moderation...") | |
| image_model = load_model(MODEL_PATH, compile=False) | |
| with open(LABELS_PATH, 'r') as f: | |
| image_labels = [line.strip().split(' ')[1] for line in f.readlines()] | |
| print("Image moderation model loaded successfully.") | |
| except Exception as e: | |
| app.logger.warning(f"Could not load image moderation model. Image moderation will be disabled. Error: {e}") | |
| image_model = None | |
| image_labels = None | |
| request_durations = deque(maxlen=100) | |
| request_timestamps = deque(maxlen=1000) | |
| daily_requests = defaultdict(int) | |
| concurrent_requests = 0 | |
| concurrent_requests_lock = threading.Lock() | |
| def is_url(string): | |
| return isinstance(string, str) and string.strip().startswith(('http://', 'https://')) | |
| def classify_image(image_bytes): | |
| if not image_model or not image_labels: | |
| raise RuntimeError("Image moderation model is not available.") | |
| image = Image.open(io.BytesIO(image_bytes)).convert('RGB') | |
| image = image.resize((224, 224)) | |
| image_array = np.asarray(image) | |
| normalized_image_array = (image_array.astype(np.float32) / 127.5) - 1 | |
| data = np.ndarray(shape=(1, 224, 224, 3), dtype=np.float32) | |
| data[0] = normalized_image_array | |
| prediction = image_model.predict(data) | |
| scores = {label.lower(): float(score) for label, score in zip(image_labels, prediction[0])} | |
| return scores | |
| def transform_text_predictions(prediction_dict): | |
| category_keys = [ | |
| "toxicity", "severe_toxicity", "obscene", "threat", | |
| "insult", "identity_attack", "sexual_explicit" | |
| ] | |
| scores = {key: float(prediction_dict.get(key, 0.0)) for key in category_keys} | |
| threshold = 0.5 | |
| categories = {key: (scores[key] > threshold) for key in category_keys} | |
| flagged = any(categories.values()) | |
| return flagged, categories, scores | |
| def transform_image_predictions(prediction_dict): | |
| nsfw_score = prediction_dict.get('nsfw', 0.0) | |
| categories = { | |
| "sexual": nsfw_score > 0.8, | |
| "hate": False, | |
| "harassment": False, | |
| "self-harm": False, | |
| "sexual/minors": nsfw_score > 0.9, | |
| "hate/threatening": False, | |
| "violence/graphic": False, | |
| "self-harm/intent": False, | |
| "self-harm/instructions": False, | |
| "harassment/threatening": False, | |
| "violence": False, | |
| } | |
| category_scores = { | |
| "sexual": nsfw_score, | |
| "hate": 0.0, | |
| "harassment": 0.0, | |
| "self-harm": 0.0, | |
| "sexual/minors": nsfw_score, | |
| "hate/threatening": 0.0, | |
| "violence/graphic": 0.0, | |
| "self-harm/intent": 0.0, | |
| "self-harm/instructions": 0.0, | |
| "harassment/threatening": 0.0, | |
| "violence": 0.0, | |
| } | |
| flagged = any(categories.values()) | |
| return flagged, categories, category_scores | |
| def track_request_metrics(start_time): | |
| duration = time.time() - start_time | |
| request_durations.append(duration) | |
| request_timestamps.append(datetime.now()) | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| daily_requests[today] += 1 | |
| def get_performance_metrics(): | |
| with concurrent_requests_lock: | |
| current_concurrent = concurrent_requests | |
| avg_request_time = sum(request_durations) / len(request_durations) if request_durations else 0 | |
| peak_request_time = max(request_durations) if request_durations else 0 | |
| now = datetime.now() | |
| one_minute_ago = now - timedelta(seconds=60) | |
| requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago) | |
| today_requests = daily_requests.get(now.strftime("%Y-%m-%d"), 0) | |
| last_7_days = [] | |
| for i in range(7): | |
| date = (now - timedelta(days=i)).strftime("%Y-%m-%d") | |
| last_7_days.append({ | |
| "date": date, | |
| "requests": daily_requests.get(date, 0), | |
| }) | |
| return { | |
| "avg_request_time_ms": avg_request_time * 1000, | |
| "peak_request_time_ms": peak_request_time * 1000, | |
| "requests_per_minute": requests_last_minute, | |
| "concurrent_requests": current_concurrent, | |
| "today_requests": today_requests, | |
| "last_7_days": last_7_days | |
| } | |
| def home(): | |
| return render_template('index.html') | |
| def moderations(): | |
| global concurrent_requests | |
| auth_header = request.headers.get('Authorization') | |
| if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY: | |
| return jsonify({"error": {"message": "Incorrect API key provided.", "type": "invalid_request_error", "code": "invalid_api_key"}}), 401 | |
| with concurrent_requests_lock: | |
| concurrent_requests += 1 | |
| start_time = time.time() | |
| try: | |
| data = request.get_json() | |
| if not data: | |
| return jsonify({"error": "Invalid JSON body"}), 400 | |
| raw_input = data.get('input') | |
| if raw_input is None: | |
| return jsonify({"error": "'input' field is required"}), 400 | |
| inputs = [raw_input] if isinstance(raw_input, str) else raw_input | |
| if not isinstance(inputs, list): | |
| return jsonify({"error": "'input' must be a string or a list of strings/URLs"}), 400 | |
| results = [] | |
| texts_to_process = [] | |
| text_indices = [] | |
| for i, item in enumerate(inputs): | |
| if is_url(item): | |
| try: | |
| response = requests.get(item, timeout=10) | |
| response.raise_for_status() | |
| image_scores = classify_image(response.content) | |
| flagged, categories, category_scores = transform_image_predictions(image_scores) | |
| results.append((i, {"flagged": flagged, "categories": categories, "category_scores": category_scores})) | |
| except requests.RequestException as e: | |
| results.append((i, {"error": f"Failed to download image: {e}"})) | |
| except Exception as e: | |
| results.append((i, {"error": f"Failed to process image: {e}"})) | |
| elif isinstance(item, str): | |
| texts_to_process.append(item) | |
| text_indices.append(i) | |
| else: | |
| results.append((i, {"error": "Invalid input type. Must be a string or URL."})) | |
| if texts_to_process: | |
| text_predictions = detoxify_model.predict(texts_to_process) | |
| for i, original_index in enumerate(text_indices): | |
| single_prediction = {key: value[i] for key, value in text_predictions.items()} | |
| flagged, categories, category_scores = transform_text_predictions(single_prediction) | |
| results.append((original_index, {"flagged": flagged, "categories": categories, "category_scores": category_scores})) | |
| results.sort(key=lambda x: x[0]) | |
| final_results = [res for _, res in results] | |
| response_data = { | |
| "id": "modr-" + uuid.uuid4().hex[:24], | |
| "model": "smart-moderator-multimodal-v1", | |
| "results": final_results | |
| } | |
| return jsonify(response_data) | |
| except Exception as e: | |
| app.logger.error(f"An error occurred: {e}", exc_info=True) | |
| return jsonify({"error": "An internal server error occurred."}), 500 | |
| finally: | |
| track_request_metrics(start_time) | |
| with concurrent_requests_lock: | |
| concurrent_requests -= 1 | |
| def metrics(): | |
| auth_header = request.headers.get('Authorization') | |
| if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY: | |
| return jsonify({"error": "Unauthorized"}), 401 | |
| return jsonify(get_performance_metrics()) | |
| def create_app_structure(): | |
| os.makedirs('templates', exist_ok=True) | |
| os.makedirs('static', exist_ok=True) | |
| index_html_content = r''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Smart Moderator API</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script> | |
| tailwind.config = { darkMode: 'class' } | |
| </script> | |
| <style> | |
| .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } | |
| .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); } | |
| .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; } | |
| @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen font-sans"> | |
| <header class="gradient-bg text-white shadow-lg"> | |
| <div class="container mx-auto px-4 py-6 flex justify-between items-center"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center"> | |
| <i class="fas fa-shield-alt text-indigo-600 text-xl"></i> | |
| </div> | |
| <h1 class="text-2xl font-bold">Smart Moderator</h1> | |
| </div> | |
| <div> | |
| <button id="darkModeToggle" class="bg-white/20 p-2 rounded-lg hover:bg-white/30 transition"> | |
| <i class="fas fa-moon dark:hidden"></i> | |
| <i class="fas fa-sun hidden dark:inline"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <main class="container mx-auto px-4 py-8"> | |
| <section class="mb-12"> | |
| <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> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> | |
| <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> | |
| <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> | |
| <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> | |
| <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> | |
| </div> | |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> | |
| <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3> | |
| <div class="h-64"><canvas id="activityChart"></canvas></div> | |
| </div> | |
| </section> | |
| <section class="mb-12"> | |
| <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> | |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> | |
| <form id="apiTestForm"> | |
| <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> | |
| <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> | |
| <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> | |
| </form> | |
| </div> | |
| </section> | |
| <section id="resultsSection" class="hidden"> | |
| <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> | |
| <div id="resultsContainer" class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"></div> | |
| </section> | |
| <section> | |
| <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> | |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> | |
| <h3 class="text-lg font-semibold mb-2">Endpoint</h3> | |
| <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">POST /v1/moderations</code> | |
| <h3 class="text-lg font-semibold mb-2">Headers</h3> | |
| <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> | |
| <h3 class="text-lg font-semibold mb-2">Request Body</h3> | |
| <p class="text-sm mb-2">The `input` field can be a single string/URL or a list of strings/URLs.</p> | |
| <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">{"input": "Text to moderate"}</code> | |
| <h3 class="text-lg font-semibold mb-2">Usage Example (cURL)</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <h4 class="font-semibold text-md mb-1">Text Moderation</h4> | |
| <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 \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"input": "You are stupid and I hate you."}'</code></pre> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold text-md mb-1">Multimodal Moderation (Text + Image)</h4> | |
| <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 \ | |
| -H "Authorization: Bearer YOUR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"input": [ | |
| "This is a perfectly normal sentence.", | |
| "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg" | |
| ]}'</code></pre> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12"> | |
| <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400 text-sm"> | |
| © 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> | |
| </div> | |
| </footer> | |
| <script> | |
| const darkModeToggle = document.getElementById('darkModeToggle'); | |
| if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| darkModeToggle.addEventListener('click', () => { | |
| document.documentElement.classList.toggle('dark'); | |
| localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light'); | |
| }); | |
| let activityChart; | |
| async function fetchMetrics() { | |
| const apiKey = document.getElementById('apiKey').value || 'temp-key'; | |
| try { | |
| const response = await fetch('/v1/metrics', { headers: { 'Authorization': 'Bearer ' + apiKey } }); | |
| if (!response.ok) return; | |
| const data = await response.json(); | |
| document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms'; | |
| document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms'; | |
| document.getElementById('requestsPerMinute').textContent = data.requests_per_minute; | |
| document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString(); | |
| if (activityChart) { | |
| const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse(); | |
| const requests = data.last_7_days.map(d => d.requests).reverse(); | |
| activityChart.data.labels = labels; | |
| activityChart.data.datasets[0].data = requests; | |
| activityChart.update(); | |
| } | |
| } catch (e) { console.error("Could not fetch metrics", e); } | |
| } | |
| function initChart() { | |
| const ctx = document.getElementById('activityChart').getContext('2d'); | |
| const isDark = document.documentElement.classList.contains('dark'); | |
| activityChart = new Chart(ctx, { | |
| type: 'bar', | |
| data: { labels: [], datasets: [{ label: 'Requests', data: [], backgroundColor: 'rgba(99, 102, 241, 0.6)' }] }, | |
| options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } | |
| }); | |
| } | |
| document.getElementById('apiTestForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const apiKey = document.getElementById('apiKey').value; | |
| if (!apiKey) { alert('Please enter an API key.'); return; } | |
| const rawInput = document.getElementById('apiInput').value.trim(); | |
| if (!rawInput) { alert('Please enter text or a URL to analyze.'); return; } | |
| const inputs = rawInput.split('\\n').map(item => item.trim()).filter(Boolean); | |
| const body = { input: inputs.length === 1 ? inputs[0] : inputs }; | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| analyzeBtn.disabled = true; | |
| analyzeBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 border-2 border-white rounded-full mr-2"></div>Analyzing...'; | |
| try { | |
| const response = await fetch('/v1/moderations', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey }, | |
| body: JSON.stringify(body) | |
| }); | |
| const data = await response.json(); | |
| displayResults(data); | |
| fetchMetrics(); | |
| } catch (error) { | |
| alert('An error occurred: ' + error.message); | |
| } finally { | |
| analyzeBtn.disabled = false; | |
| analyzeBtn.innerHTML = '<i class="fas fa-search mr-2"></i>Analyze'; | |
| } | |
| }); | |
| function displayResults(data) { | |
| const resultsContainer = document.getElementById('resultsContainer'); | |
| resultsContainer.innerHTML = ''; | |
| if (data.error) { | |
| 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>`; | |
| } else if (data.results) { | |
| data.results.forEach((result, index) => { | |
| const resultCard = document.createElement('div'); | |
| 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'; | |
| const flaggedBadge = result.flagged | |
| ? '<span class="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">Flagged</span>' | |
| : '<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">Safe</span>'; | |
| let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => { | |
| const isFlagged = result.categories[category]; | |
| 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>`; | |
| }).join(''); | |
| resultCard.innerHTML = `<div class="flex justify-between items-center mb-2"><h4 class="font-bold">Input ${index + 1}</h4>${flaggedBadge}</div><div>${categoriesHtml}</div>`; | |
| resultsContainer.appendChild(resultCard); | |
| }); | |
| } | |
| document.getElementById('resultsSection').classList.remove('hidden'); | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initChart(); | |
| fetchMetrics(); | |
| setInterval(fetchMetrics, 20000); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| index_path = os.path.join('templates', 'index.html') | |
| if not os.path.exists(index_path): | |
| with open(index_path, 'w', encoding='utf-8') as f: | |
| f.write(index_html_content) | |
| if __name__ == '__main__': | |
| create_app_structure() | |
| port = int(os.environ.get('PORT', 7860)) | |
| # For production, use a proper WSGI server like Gunicorn | |
| # gunicorn --bind 0.0.0.0:7860 app:app | |
| app.run(host='0.0.0.0', port=port, debug=False) |