Spaces:
Running
Running
| """ | |
| 模型优化服务 - Flask 前端 | |
| 第一版:本地测试模式(先跑通完整通路) | |
| 后续部署到 HF Space 时可切换为 Gradio 版本 | |
| """ | |
| from flask import Flask, request, render_template_string, send_file, jsonify | |
| import os | |
| import uuid | |
| import shutil | |
| from datetime import datetime | |
| app = Flask(__name__) | |
| # === 配置 === | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| INBOX = os.path.join(BASE_DIR, "..", "local", "tmp", "inbox") | |
| OUTBOX = os.path.join(BASE_DIR, "..", "local", "tmp", "outbox") | |
| os.makedirs(INBOX, exist_ok=True) | |
| os.makedirs(OUTBOX, exist_ok=True) | |
| SUPPORTED_FORMATS = [".glb", ".gltf", ".fbx", ".obj"] | |
| def log(msg): | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| print(f"[{timestamp}] [WEB] {msg}") | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>3D 模型优化服务</title> | |
| <meta charset="utf-8"> | |
| <style> | |
| body { font-family: -apple-system, sans-serif; max-width: 700px; margin: 50px auto; padding: 20px; background: #f5f5f5; } | |
| .card { background: white; border-radius: 12px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } | |
| h1 { text-align: center; color: #333; } | |
| .upload-area { border: 2px dashed #ccc; border-radius: 8px; padding: 40px; text-align: center; margin: 20px 0; } | |
| .upload-area:hover { border-color: #667eea; } | |
| input[type="file"] { margin: 10px 0; } | |
| button { background: #667eea; color: white; border: none; padding: 12px 30px; border-radius: 6px; cursor: pointer; font-size: 16px; } | |
| button:hover { background: #5a67d8; } | |
| .result { padding: 15px; border-radius: 8px; margin: 15px 0; } | |
| .success { background: #d4edda; color: #155724; } | |
| .waiting { background: #fff3cd; color: #856404; } | |
| .error { background: #f8d7da; color: #721c24; } | |
| input[type="text"] { padding: 10px; width: 200px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; } | |
| .info { color: #666; font-size: 14px; } | |
| a.download { display: inline-block; background: #28a745; color: white; padding: 10px 20px; border-radius: 6px; text-decoration: none; margin-top: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🛠️ 3D 模型优化服务</h1> | |
| <div class="card"> | |
| <h2>📤 上传模型</h2> | |
| <form action="/upload" method="post" enctype="multipart/form-data"> | |
| <div class="upload-area"> | |
| <p>选择 3D 模型文件</p> | |
| <input type="file" name="model" accept=".glb,.gltf,.fbx,.obj"> | |
| <p class="info">支持格式: GLB, GLTF, FBX, OBJ</p> | |
| </div> | |
| <button type="submit">🚀 提交优化</button> | |
| </form> | |
| {% if upload_msg %} | |
| <div class="result {{ upload_class }}">{{ upload_msg }}</div> | |
| {% endif %} | |
| </div> | |
| <div class="card"> | |
| <h2>🔍 查询结果</h2> | |
| <form action="/status" method="get"> | |
| <input type="text" name="task_id" placeholder="输入任务ID" value="{{ query_id or '' }}"> | |
| <button type="submit">查询</button> | |
| </form> | |
| {% if status_msg %} | |
| <div class="result {{ status_class }}">{{ status_msg }}</div> | |
| {% endif %} | |
| {% if download_ready %} | |
| <a class="download" href="/download/{{ download_id }}">⬇️ 下载优化结果</a> | |
| {% endif %} | |
| </div> | |
| <div class="info" style="text-align:center; margin-top: 30px;"> | |
| 输出格式: GLB | Worker 每30秒检查一次任务 | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def upload(): | |
| file = request.files.get("model") | |
| if not file or file.filename == "": | |
| return render_template_string(HTML_TEMPLATE, | |
| upload_msg="请选择文件", upload_class="error") | |
| filename = file.filename | |
| ext = os.path.splitext(filename)[1].lower() | |
| log(f"收到上传: {filename}") | |
| if ext not in SUPPORTED_FORMATS: | |
| log(f"格式不支持: {ext}") | |
| return render_template_string(HTML_TEMPLATE, | |
| upload_msg=f"不支持的格式: {ext}", upload_class="error") | |
| # 生成任务ID并保存文件 | |
| task_id = str(uuid.uuid4())[:8] | |
| target_path = os.path.join(INBOX, f"{task_id}{ext}") | |
| file.save(target_path) | |
| file_size = os.path.getsize(target_path) | |
| log(f"文件已保存: {task_id}{ext} ({file_size} bytes), 任务ID: {task_id}") | |
| return render_template_string(HTML_TEMPLATE, | |
| upload_msg=f"✅ 上传成功!任务ID: {task_id} (请保存此ID用于查询结果)", | |
| upload_class="success") | |
| def status(): | |
| task_id = request.args.get("task_id", "").strip() | |
| if not task_id: | |
| return render_template_string(HTML_TEMPLATE, | |
| status_msg="请输入任务ID", status_class="error", query_id=task_id) | |
| log(f"查询状态: {task_id}") | |
| # 检查 outbox | |
| result_file = os.path.join(OUTBOX, f"{task_id}_optimized.glb") | |
| if os.path.exists(result_file): | |
| size = os.path.getsize(result_file) | |
| log(f"任务 {task_id} 已完成 ({size} bytes)") | |
| return render_template_string(HTML_TEMPLATE, | |
| status_msg=f"✅ 优化完成!文件大小: {size} bytes", | |
| status_class="success", download_ready=True, | |
| download_id=task_id, query_id=task_id) | |
| # 检查 inbox | |
| for f in os.listdir(INBOX): | |
| if f.startswith(task_id): | |
| log(f"任务 {task_id} 仍在队列中") | |
| return render_template_string(HTML_TEMPLATE, | |
| status_msg="⏳ 处理中,请稍后再查询...", | |
| status_class="waiting", query_id=task_id) | |
| log(f"任务 {task_id} 未找到") | |
| return render_template_string(HTML_TEMPLATE, | |
| status_msg="❌ 未找到该任务,请检查ID是否正确", | |
| status_class="error", query_id=task_id) | |
| def download(task_id): | |
| result_file = os.path.join(OUTBOX, f"{task_id}_optimized.glb") | |
| if os.path.exists(result_file): | |
| log(f"用户下载: {task_id}_optimized.glb") | |
| return send_file(result_file, as_attachment=True, | |
| download_name=f"{task_id}_optimized.glb") | |
| return "文件不存在", 404 | |
| if __name__ == "__main__": | |
| log("=" * 40) | |
| log("Web 前端启动") | |
| log(f" Inbox: {INBOX}") | |
| log(f" Outbox: {OUTBOX}") | |
| log("=" * 40) | |
| app.run(host="0.0.0.0", port=5000, debug=True) | |