| import os |
| import json |
| import base64 |
| import requests |
| from datetime import datetime |
| from flask import Flask, request, jsonify, render_template_string |
| from threading import Lock |
|
|
| app = Flask(__name__) |
|
|
| moderation_data = {} |
| data_lock = Lock() |
|
|
| NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "") |
| NVIDIA_ENDPOINT = "https://integrate.api.nvidia.com/v1/chat/completions" |
| MODEL_NAME = "qwen/qwen3.5-397b-a17b" |
|
|
| SYSTEM_PROMPT = """你是一个图片内容审核AI助手。请分析用户提供的图片内容,判断是否包含以下违规内容: |
| 1. 色情/低俗内容 |
| 2. 暴力/血腥内容 |
| 3. 政治敏感内容 |
| 4. 恐怖/惊悚内容 |
| 5. 赌博/诈骗内容 |
| 6. 其他违反法律法规的内容 |
| |
| 请基于图片分析结果,严格判断: |
| - 如果图片内容安全,不包含任何违规元素,返回 "TRUE" |
| - 如果图片包含任何违规内容或无法确定安全性,返回 "FALSE" |
| |
| 注意:只返回TRUE或FALSE,不要返回其他内容。""" |
|
|
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>图片审核系统</title> |
| <style> |
| * { box-sizing: border-box; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| max-width: 900px; |
| margin: 0 auto; |
| padding: 20px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| } |
| h1 { color: white; text-align: center; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } |
| .container { background: white; border-radius: 15px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); } |
| |
| .upload-section { text-align: center; padding: 30px; border: 2px dashed #ccc; border-radius: 10px; } |
| .upload-section:hover { border-color: #667eea; background: #f8f9ff; } |
| input[type="file"] { display: none; } |
| .upload-btn { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 15px 40px; |
| border: none; |
| border-radius: 25px; |
| cursor: pointer; |
| font-size: 18px; |
| font-weight: bold; |
| transition: transform 0.2s; |
| } |
| .upload-btn:hover { transform: scale(1.05); } |
| |
| #preview { max-width: 300px; margin: 20px auto; display: none; } |
| #preview img { max-width: 100%; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); } |
| |
| .loading { text-align: center; padding: 30px; display: none; } |
| .spinner { |
| border: 4px solid #f3f3f3; |
| border-top: 4px solid #667eea; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| animation: spin 1s linear infinite; |
| margin: 0 auto; |
| } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| |
| .result { |
| margin-top: 20px; |
| padding: 25px; |
| border-radius: 10px; |
| display: none; |
| text-align: center; |
| } |
| .result h2 { margin: 10px 0; } |
| .result.passed { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; } |
| .result.rejected { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); color: white; } |
| |
| .action-btns { display: flex; gap: 15px; justify-content: center; margin-top: 15px; } |
| .btn { padding: 12px 30px; border: none; border-radius: 25px; cursor: pointer; font-size: 16px; font-weight: bold; } |
| .btn-approve { background: #11998e; color: white; } |
| .btn-reject { background: #eb3349; color: white; } |
| .btn:hover { opacity: 0.9; transform: scale(1.05); } |
| |
| .history { margin-top: 30px; } |
| .history h2 { color: #333; border-bottom: 2px solid #667eea; padding-bottom: 10px; } |
| .history-item { |
| background: #f8f9fa; |
| padding: 15px; |
| margin: 10px 0; |
| border-radius: 10px; |
| display: flex; |
| align-items: center; |
| gap: 15px; |
| } |
| .history-item img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; } |
| .history-item .info { flex: 1; } |
| |
| .status-badge { padding: 5px 15px; border-radius: 20px; font-size: 14px; font-weight: bold; } |
| .status-ai-pass { background: #11998e; color: white; } |
| .status-ai-reject { background: #eb3349; color: white; } |
| .status-pending { background: #f39c12; color: white; } |
| .status-approved { background: #3498db; color: white; } |
| .status-rejected { background: #9b59b6; color: white; } |
| |
| .api-docs { margin-top: 30px; background: #f8f9fa; padding: 20px; border-radius: 10px; } |
| .api-docs h3 { color: #333; } |
| .api-docs code { background: #e9ecef; padding: 2px 6px; border-radius: 4px; font-size: 13px; } |
| </style> |
| </head> |
| <body> |
| <h1>🖼️ 图片审核系统</h1> |
| <div class="container"> |
| <div class="upload-section"> |
| <label class="upload-btn">📁 选择图片审核</label> |
| <input type="file" id="fileInput" accept="image/*"> |
| <div id="preview"></div> |
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <p>🤖 AI正在审核中,请稍候...</p> |
| </div> |
| <div class="result" id="result"></div> |
| </div> |
| |
| <div class="history"> |
| <h2>📋 审核记录</h2> |
| <div id="historyList"></div> |
| </div> |
| |
| <div class="api-docs"> |
| <h3>🔌 API 接口文档</h3> |
| <p><strong>审核图片:</strong> <code>POST /api/moderate</code></p> |
| <p><strong>人工通过:</strong> <code>POST /api/approve/{id}</code></p> |
| <p><strong>人工拒绝:</strong> <code>POST /api/reject/{id}</code></p> |
| <p><strong>查看详情:</strong> <code>GET /api/status/{id}</code></p> |
| </div> |
| </div> |
| |
| <script> |
| let currentId = null; |
| |
| document.getElementById('fileInput').addEventListener('change', async (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = async (event) => { |
| const base64 = event.target.result.split(',')[1]; |
| document.getElementById('preview').innerHTML = '<img src="' + event.target.result + '">'; |
| document.getElementById('preview').style.display = 'block'; |
| document.getElementById('result').style.display = 'none'; |
| document.getElementById('loading').style.display = 'block'; |
| |
| try { |
| const response = await fetch('/api/moderate', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({image: base64}) |
| }); |
| const data = await response.json(); |
| if (data.error) { |
| alert('审核失败: ' + data.error); |
| } else { |
| currentId = data.id; |
| showResult(data); |
| } |
| } catch (err) { |
| alert('审核失败: ' + err); |
| } finally { |
| document.getElementById('loading').style.display = 'none'; |
| } |
| }; |
| reader.readAsDataURL(file); |
| }); |
| |
| function showResult(data) { |
| const resultDiv = document.getElementById('result'); |
| resultDiv.style.display = 'block'; |
| resultDiv.className = 'result ' + data.status; |
| |
| if (data.ai_result === 'TRUE') { |
| resultDiv.innerHTML = '<h2>✅ AI审核通过</h2><p>图片内容安全</p>'; |
| } else { |
| resultDiv.innerHTML = '<h2>❌ AI审核未通过</h2><p>等待人工复审</p>' + |
| '<div class="action-btns">' + |
| '<button class="btn btn-approve" onclick="approve()">✓ 人工通过</button>' + |
| '<button class="btn btn-reject" onclick="reject()">✗ 人工拒绝</button></div>'; |
| } |
| loadHistory(); |
| } |
| |
| async function approve() { |
| await fetch('/api/approve/' + currentId, {method: 'POST'}); |
| showResult({status: 'approved', ai_result: 'MANUAL_APPROVE'}); |
| } |
| |
| async function reject() { |
| await fetch('/api/reject/' + currentId, {method: 'POST'}); |
| showResult({status: 'rejected', ai_result: 'MANUAL_REJECT'}); |
| } |
| |
| async function loadHistory() { |
| const response = await fetch('/api/history'); |
| const data = await response.json(); |
| const list = document.getElementById('historyList'); |
| if (data.items && data.items.length > 0) { |
| list.innerHTML = data.items.map(item => ` |
| <div class="history-item"> |
| <img src="${item.image}"> |
| <div class="info"> |
| <div><strong>AI结果:</strong> <span class="status-badge status-${item.ai_result === 'TRUE' ? 'ai-pass' : 'ai-reject'}">${item.ai_result}</span></div> |
| <div><strong>最终状态:</strong> <span class="status-badge status-${item.status}">${item.status_text}</span></div> |
| <div><small>${item.time}</small></div> |
| </div> |
| </div> |
| `).join(''); |
| } else { |
| list.innerHTML = '<p style="text-align:center;color:#666;">暂无审核记录</p>'; |
| } |
| } |
| |
| loadHistory(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
|
|
| def encode_image_to_base64(image_data): |
| try: |
| return base64.b64decode(image_data) |
| except: |
| return None |
|
|
|
|
| def call_nvidia_api(image_base64): |
| if not NVIDIA_API_KEY: |
| return None, "API密钥未配置,请在Space设置中添加NVIDIA_API_KEY环境变量" |
|
|
| headers = { |
| "Authorization": f"Bearer {NVIDIA_API_KEY}", |
| "Content-Type": "application/json", |
| } |
|
|
| payload = { |
| "model": MODEL_NAME, |
| "messages": [ |
| { |
| "role": "user", |
| "content": [ |
| {"type": "text", "text": SYSTEM_PROMPT}, |
| { |
| "type": "image_url", |
| "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}, |
| }, |
| ], |
| } |
| ], |
| "max_tokens": 10, |
| "thinking": {"type": "off"}, |
| } |
|
|
| try: |
| response = requests.post( |
| NVIDIA_ENDPOINT, headers=headers, json=payload, timeout=120 |
| ) |
| if response.status_code == 200: |
| result = response.json() |
| content = result["choices"][0]["message"]["content"].strip().upper() |
| return content, None |
| else: |
| return None, f"API错误: {response.status_code} - {response.text}" |
| except Exception as e: |
| return None, str(e) |
|
|
|
|
| @app.route("/") |
| def index(): |
| return render_template_string(HTML_TEMPLATE) |
|
|
|
|
| @app.route("/api/moderate", methods=["POST"]) |
| def moderate(): |
| data = request.json |
| image_data = data.get("image", "") |
|
|
| if not image_data: |
| return jsonify({"error": "没有图片数据"}), 400 |
|
|
| image_bytes = encode_image_to_base64(image_data) |
| if not image_bytes: |
| return jsonify({"error": "图片格式错误"}), 400 |
|
|
| ai_result, error = call_nvidia_api(image_data) |
|
|
| if error: |
| return jsonify({"error": error}), 500 |
|
|
| with data_lock: |
| import uuid |
|
|
| item_id = str(uuid.uuid4())[:8] |
| status = "ai-pass" if ai_result == "TRUE" else "ai-reject" |
| moderation_data[item_id] = { |
| "id": item_id, |
| "image": f"data:image/jpeg;base64,{image_data}", |
| "ai_result": ai_result, |
| "status": status, |
| "final_status": None, |
| "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), |
| } |
|
|
| return jsonify({"id": item_id, "ai_result": ai_result, "status": status}) |
|
|
|
|
| @app.route("/api/approve/<item_id>", methods=["POST"]) |
| def approve(item_id): |
| with data_lock: |
| if item_id in moderation_data: |
| moderation_data[item_id]["status"] = "approved" |
| moderation_data[item_id]["final_status"] = "approved" |
| return jsonify({"success": True, "status": "approved"}) |
| return jsonify({"error": "未找到记录"}), 404 |
|
|
|
|
| @app.route("/api/reject/<item_id>", methods=["POST"]) |
| def reject(item_id): |
| with data_lock: |
| if item_id in moderation_data: |
| moderation_data[item_id]["status"] = "rejected" |
| moderation_data[item_id]["final_status"] = "rejected" |
| return jsonify({"success": True, "status": "rejected"}) |
| return jsonify({"error": "未找到记录"}), 404 |
|
|
|
|
| @app.route("/api/status/<item_id>", methods=["GET"]) |
| def get_status(item_id): |
| with data_lock: |
| if item_id in moderation_data: |
| item = moderation_data[item_id] |
| return jsonify( |
| { |
| "id": item["id"], |
| "ai_result": item["ai_result"], |
| "status": item["final_status"] |
| if item["final_status"] |
| else item["status"], |
| "time": item["time"], |
| } |
| ) |
| return jsonify({"error": "未找到记录"}), 404 |
|
|
|
|
| @app.route("/api/history", methods=["GET"]) |
| def history(): |
| with data_lock: |
| items = list(moderation_data.values())[-20:][::-1] |
| status_text = { |
| "ai-pass": "AI通过", |
| "ai-reject": "待复审", |
| "approved": "人工通过", |
| "rejected": "人工拒绝", |
| } |
| return jsonify( |
| { |
| "items": [ |
| { |
| "id": item["id"], |
| "image": item["image"], |
| "ai_result": item["ai_result"], |
| "status": item["final_status"] |
| if item["final_status"] |
| else item["status"], |
| "status_text": status_text.get( |
| item["final_status"] |
| if item["final_status"] |
| else item["status"], |
| "未知", |
| ), |
| "time": item["time"], |
| } |
| for item in items |
| ] |
| } |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| port = int(os.environ.get("PORT", 7860)) |
| app.run(host="0.0.0.0", port=port) |
|
|