Spaces:
Running
Running
| import os | |
| import time | |
| import uuid | |
| import secrets | |
| import base64 | |
| import requests as py_requests | |
| from flask import Blueprint, request, jsonify, send_file | |
| from backend.core.decorators import get_current_user, require_api_key_auth | |
| from backend.database.db_manager import get_user_db_conn | |
| from backend.services.translation import get_engine | |
| developer_bp = Blueprint("developer", __name__) | |
| ADMIN_GEMINI_KEY = os.environ.get("ADMIN_GEMINI_KEY", "") | |
| def serialize_row(row): | |
| if not row: | |
| return {} | |
| d = dict(row) | |
| for k, v in d.items(): | |
| if hasattr(v, "isoformat"): | |
| d[k] = v.isoformat() | |
| return d | |
| def deduct_developer_balance(api_key, amount, model, tokens=0, status_code=200): | |
| conn = get_user_db_conn() | |
| try: | |
| conn.execute( | |
| "INSERT INTO api_usage (api_key, model, tokens, cost, status_code) VALUES (?, ?, ?, ?, ?)", | |
| (api_key, model, tokens, amount, status_code) | |
| ) | |
| conn.execute( | |
| "UPDATE users SET api_balance = api_balance - ? WHERE id = (SELECT user_id FROM api_keys WHERE api_key = ?)", | |
| (amount, api_key) | |
| ) | |
| conn.commit() | |
| except Exception as e: | |
| print(f"[API GATEWAY ERROR] Failed to deduct balance: {e}") | |
| finally: | |
| conn.close() | |
| def api_dev_keys_list(): | |
| user = get_current_user() | |
| print(f"[DEBUG api_dev_keys_list] Current user from auth: {user}") | |
| if not user: | |
| return jsonify({"error": "Vui lòng đăng nhập."}), 401 | |
| conn = get_user_db_conn() | |
| keys = conn.execute( | |
| "SELECT api_key, name, status, created_at, last_used_at FROM api_keys WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC", | |
| (user["id"],) | |
| ).fetchall() | |
| user_row = conn.execute("SELECT api_balance FROM users WHERE id = ?", (user["id"],)).fetchone() | |
| conn.close() | |
| balance = user_row["api_balance"] if user_row and user_row["api_balance"] is not None else 0.0 | |
| print(f"[DEBUG api_dev_keys_list] Queried user_id: {user['id']}, found balance: {balance}, keys count: {len(keys)}") | |
| return jsonify({"balance": balance, "keys": [serialize_row(k) for k in keys]}) | |
| def api_dev_keys_create(): | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({"error": "Vui lòng đăng nhập."}), 401 | |
| data = request.json or {} | |
| name = data.get("name", "Default Key").strip()[:50] | |
| new_key = f"sk-tc-{secrets.token_hex(16)}" | |
| conn = get_user_db_conn() | |
| conn.execute( | |
| "INSERT INTO api_keys (user_id, api_key, name, status) VALUES (?, ?, ?, 'active')", | |
| (user["id"], new_key, name) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return jsonify({"success": True, "api_key": new_key, "name": name}) | |
| def api_dev_keys_delete(): | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({"error": "Vui lòng đăng nhập."}), 401 | |
| data = request.json or {} | |
| api_key = data.get("api_key", "").strip() | |
| if not api_key: | |
| return jsonify({"error": "Thiếu API Key cần xóa."}), 400 | |
| conn = get_user_db_conn() | |
| conn.execute( | |
| "UPDATE api_keys SET status = 'revoked' WHERE user_id = ? AND api_key = ?", | |
| (user["id"], api_key) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return jsonify({"success": True}) | |
| def api_dev_usage(): | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({"error": "Vui lòng đăng nhập."}), 401 | |
| conn = get_user_db_conn() | |
| keys_rows = conn.execute("SELECT api_key FROM api_keys WHERE user_id = ?", (user["id"],)).fetchall() | |
| keys = [k["api_key"] for k in keys_rows] | |
| if not keys: | |
| conn.close() | |
| return jsonify({"usage": []}) | |
| placeholders = ",".join(["?"] * len(keys)) | |
| usage = conn.execute( | |
| f"SELECT * FROM api_usage WHERE api_key IN ({placeholders}) ORDER BY timestamp DESC LIMIT 100", | |
| keys | |
| ).fetchall() | |
| conn.close() | |
| return jsonify({"usage": [serialize_row(u) for u in usage]}) | |
| def openai_chat_completions(): | |
| api_key = request.api_key | |
| data = request.json or {} | |
| messages = data.get("messages", []) | |
| model = data.get("model", "gemini-1.5-flash") | |
| if not messages: | |
| return jsonify({"error": "Missing messages array"}), 400 | |
| prompt_length = sum(len(m.get("content", "")) for m in messages) | |
| cost = max(20.0, prompt_length * 0.02) | |
| if request.api_balance < cost: | |
| return jsonify({"error": f"Số dư không đủ. Chi phí ước tính: {cost:.2f}đ, Số dư: {request.api_balance:.2f}đ"}), 402 | |
| if not ADMIN_GEMINI_KEY: | |
| return jsonify({"error": "Hệ thống chưa được Admin cấu hình khóa Gemini."}), 501 | |
| try: | |
| contents = [] | |
| system_instruction = "" | |
| for msg in messages: | |
| role = msg.get("role") | |
| content = msg.get("content", "") | |
| if role == "system": | |
| system_instruction = content | |
| else: | |
| contents.append({ | |
| "role": "user" if role == "user" else "model", | |
| "parts": [{"text": content}] | |
| }) | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={ADMIN_GEMINI_KEY}" | |
| payload = {"contents": contents} | |
| if system_instruction: | |
| payload["systemInstruction"] = {"parts": [{"text": system_instruction}]} | |
| res = py_requests.post(url, json=payload, timeout=30) | |
| if res.status_code != 200: | |
| deduct_developer_balance(api_key, 0.0, model, prompt_length, res.status_code) | |
| return jsonify({"error": f"Google Gemini API error: {res.text}"}), res.status_code | |
| gemini_data = res.json() | |
| response_text = gemini_data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "") | |
| deduct_developer_balance(api_key, cost, model, prompt_length + len(response_text), 200) | |
| res_id = f"chatcmpl-{uuid.uuid4().hex}" | |
| return jsonify({ | |
| "id": res_id, | |
| "object": "chat.completion", | |
| "created": int(time.time()), | |
| "model": model, | |
| "choices": [{ | |
| "index": 0, | |
| "message": {"role": "assistant", "content": response_text}, | |
| "finish_reason": "stop" | |
| }], | |
| "usage": { | |
| "prompt_tokens": int(prompt_length / 4), | |
| "completion_tokens": int(len(response_text) / 4), | |
| "total_tokens": int((prompt_length + len(response_text)) / 4) | |
| } | |
| }) | |
| except Exception as e: | |
| deduct_developer_balance(api_key, 0.0, model, prompt_length, 500) | |
| return jsonify({"error": f"API Gateway Exception: {str(e)}"}), 500 | |
| def api_v1_translate(): | |
| api_key = request.api_key | |
| data = request.json or {} | |
| texts = data.get("texts", []) | |
| mode = data.get("mode", "fast") | |
| if not texts: | |
| return jsonify({"error": "Missing texts array"}), 400 | |
| total_chars = sum(len(t) for t in texts) | |
| cost = total_chars * 0.01 | |
| if request.api_balance < cost: | |
| return jsonify({"error": f"Số dư không đủ. Chi phí ước tính: {cost:.2f}đ, Số dư: {request.api_balance:.2f}đ"}), 402 | |
| try: | |
| eng = get_engine() | |
| translations = [] | |
| for text in texts: | |
| if not text.strip(): | |
| translations.append(text) | |
| else: | |
| translations.append(eng.translate(text, multi_option=False, mode=mode)) | |
| deduct_developer_balance(api_key, cost, f"vietphrase-{mode}", total_chars, 200) | |
| return jsonify({"translations": translations, "characters": total_chars, "cost": cost}) | |
| except Exception as e: | |
| deduct_developer_balance(api_key, 0.0, f"vietphrase-{mode}", total_chars, 500) | |
| return jsonify({"error": f"Translation Error: {str(e)}"}), 500 | |
| def openai_audio_speech(): | |
| api_key = request.api_key | |
| data = request.json or {} | |
| input_text = data.get("input", "").strip() | |
| if not input_text: | |
| return jsonify({"error": "Missing 'input' text"}), 400 | |
| cost = len(input_text) * 0.1 | |
| if request.api_balance < cost: | |
| return jsonify({"error": f"Số dư không đủ. Chi phí: {cost:.2f}đ"}), 402 | |
| # 1. Check if Local TTS Server is configured (host machine) | |
| local_tts_url = os.environ.get("LOCAL_TTS_URL", "") | |
| if local_tts_url: | |
| try: | |
| payload = { | |
| "input": input_text, | |
| "speed": float(data.get("speed", 1.0)), | |
| "voice": data.get("voice", "the_gioi_hoan_my"), | |
| "ref_audio": data.get("ref_audio"), | |
| "ref_text": data.get("ref_text") | |
| } | |
| if "bypass_cache" in data: | |
| payload["bypass_cache"] = data["bypass_cache"] | |
| if "temperature" in data: | |
| payload["temperature"] = data["temperature"] | |
| if "steps" in data: | |
| payload["steps"] = data["steps"] | |
| print(f"[Local TTS Proxy] Forwarding request to {local_tts_url} with voice={payload['voice']}, speed={payload['speed']}, bypass_cache={payload.get('bypass_cache')}") | |
| r = py_requests.post(local_tts_url, json=payload, timeout=(2.0, 60.0)) | |
| if r.status_code == 200: | |
| import io | |
| deduct_developer_balance(api_key, cost, f"local-tts-{payload['voice']}", len(input_text), 200) | |
| return send_file( | |
| io.BytesIO(r.content), | |
| mimetype="audio/wav", | |
| as_attachment=False | |
| ) | |
| print(f"[Local TTS Proxy Error]: {r.status_code} - {r.text}") | |
| except Exception as e: | |
| print(f"[Local TTS Proxy Exception]: {e}") | |
| # 2. RunPod Fallback | |
| runpod_api_key = os.environ.get("RUNPOD_API_TOKEN", os.environ.get("RUNPOD_API_KEY", "")) | |
| runpod_endpoint_id = os.environ.get("RUNPOD_TTS_ENDPOINT_ID", "") | |
| if runpod_api_key and runpod_endpoint_id: | |
| try: | |
| url = f"https://api.runpod.ai/v1/{runpod_endpoint_id}/runsync" | |
| headers = {"Authorization": f"Bearer {runpod_api_key}", "Content-Type": "application/json"} | |
| payload = { | |
| "input": { | |
| "text": input_text, | |
| "speed": 1.0, | |
| "voice": data.get("voice", "the_gioi_hoan_my") | |
| } | |
| } | |
| r = py_requests.post(url, json=payload, headers=headers, timeout=45) | |
| res = r.json() | |
| if r.status_code == 200 and res.get("status") == "COMPLETED": | |
| audio_b64 = res.get("output", {}).get("audio_base64", "") | |
| if audio_b64: | |
| import io | |
| audio_data = base64.b64decode(audio_b64) | |
| deduct_developer_balance(api_key, cost, "runpod-matcha-tts", len(input_text), 200) | |
| return send_file( | |
| io.BytesIO(audio_data), | |
| mimetype="audio/wav" if res.get("output", {}).get("format") == "wav" else "audio/mpeg", | |
| as_attachment=False | |
| ) | |
| print(f"[RunPod TTS Error]: {res}") | |
| except Exception as e: | |
| print(f"[RunPod TTS exception]: {e}") | |
| # Fallback: Generate a tiny 1-second silent WAV for testing/sandbox purposes | |
| try: | |
| import struct | |
| import io | |
| num_samples = 8000 | |
| # 8-bit PCM silence is represented by 128 | |
| data_bytes = bytearray([128] * num_samples) | |
| header = struct.pack( | |
| '<4sI4s4sIHHIIHH4sI', | |
| b'RIFF', | |
| 36 + num_samples, | |
| b'WAVE', | |
| b'fmt ', | |
| 16, | |
| 1, # PCM | |
| 1, # Mono | |
| 8000, # Sample rate | |
| 8000, # Byte rate | |
| 1, # Block align | |
| 8, # Bits per sample | |
| b'data', | |
| num_samples | |
| ) | |
| audio_data = bytes(header + data_bytes) | |
| deduct_developer_balance(api_key, cost, "local-silent-tts-fallback", len(input_text), 200) | |
| return send_file( | |
| io.BytesIO(audio_data), | |
| mimetype="audio/wav", | |
| as_attachment=False | |
| ) | |
| except Exception as e: | |
| return jsonify({"error": f"Failed to generate fallback TTS: {str(e)}"}), 500 | |
| def api_developer_all_sessions(): | |
| user = get_current_user() | |
| if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: | |
| return jsonify({"error": "Quyền truy cập bị từ chối."}), 403 | |
| conn = get_user_db_conn() | |
| try: | |
| rows = conn.execute( | |
| """SELECT lh.id, lh.user_id, u.username, lh.ip_address, lh.os, lh.browser, lh.device_type, lh.login_time, lh.last_active, lh.status | |
| FROM login_history lh | |
| JOIN users u ON lh.user_id = u.id | |
| ORDER BY lh.login_time DESC LIMIT 100""" | |
| ).fetchall() | |
| sessions = [] | |
| for r in rows: | |
| s = dict(r) | |
| if hasattr(s["login_time"], "isoformat"): | |
| s["login_time"] = s["login_time"].isoformat() | |
| if hasattr(s["last_active"], "isoformat"): | |
| s["last_active"] = s["last_active"].isoformat() | |
| sessions.append(s) | |
| return jsonify({ | |
| "success": True, | |
| "sessions": sessions | |
| }) | |
| except Exception as e: | |
| return jsonify({"error": str(e), "success": False}), 500 | |
| finally: | |
| conn.close() | |
| def api_developer_revoke_session(): | |
| user = get_current_user() | |
| if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: | |
| return jsonify({"error": "Quyền truy cập bị từ chối."}), 403 | |
| data = request.json or {} | |
| session_id = data.get("session_id") | |
| if not session_id: | |
| return jsonify({"error": "Missing session_id", "success": False}), 400 | |
| conn = get_user_db_conn() | |
| try: | |
| row = conn.execute( | |
| "SELECT token FROM login_history WHERE id = ?", | |
| (session_id,) | |
| ).fetchone() | |
| if not row: | |
| return jsonify({"error": "Session not found", "success": False}), 404 | |
| token = row["token"] | |
| conn.execute("UPDATE login_history SET status = 'logged_out' WHERE id = ?", (session_id,)) | |
| if token: | |
| conn.execute("UPDATE refresh_tokens SET revoked = 1 WHERE token = ?", (token,)) | |
| conn.commit() | |
| return jsonify({"message": "Đã đăng xuất thiết bị thành công", "success": True}) | |
| except Exception as e: | |
| return jsonify({"error": str(e), "success": False}), 500 | |
| finally: | |
| conn.close() | |
| def api_get_releases(): | |
| conn = get_user_db_conn() | |
| try: | |
| rows = conn.execute( | |
| "SELECT platform, version, download_url, patch_url, file_size, release_notes, updated_at FROM app_releases ORDER BY updated_at DESC" | |
| ).fetchall() | |
| releases = {} | |
| for r in rows: | |
| plat = r["platform"] | |
| if plat not in releases: | |
| releases[plat] = { | |
| "version": r["version"], | |
| "download_url": r["download_url"], | |
| "patch_url": r["patch_url"], | |
| "file_size": r["file_size"], | |
| "release_notes": r["release_notes"], | |
| "updated_at": r["updated_at"].isoformat() if hasattr(r["updated_at"], "isoformat") else r["updated_at"], | |
| "history": [] | |
| } | |
| releases[plat]["history"].append({ | |
| "version": r["version"], | |
| "download_url": r["download_url"], | |
| "patch_url": r["patch_url"], | |
| "file_size": r["file_size"], | |
| "release_notes": r["release_notes"], | |
| "updated_at": r["updated_at"].isoformat() if hasattr(r["updated_at"], "isoformat") else r["updated_at"] | |
| }) | |
| return jsonify({"success": True, "releases": releases}) | |
| except Exception as e: | |
| return jsonify({"error": str(e), "success": False}), 500 | |
| finally: | |
| conn.close() | |
| def api_update_release(): | |
| user = get_current_user() | |
| if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: | |
| return jsonify({"error": "Quyền truy cập bị từ chối. Chỉ dành cho admin.", "success": False}), 403 | |
| data = request.json or {} | |
| platform = data.get("platform") | |
| version = data.get("version") | |
| download_url = data.get("download_url") | |
| file_size = data.get("file_size") | |
| release_notes = data.get("release_notes") | |
| if not platform or platform not in ['extension', 'desktop_linux', 'desktop_windows']: | |
| return jsonify({"error": "Platform không hợp lệ.", "success": False}), 400 | |
| conn = get_user_db_conn() | |
| try: | |
| existing = conn.execute("SELECT 1 FROM app_releases WHERE platform = ? AND version = ?", (platform, version)).fetchone() | |
| if existing: | |
| conn.execute( | |
| """UPDATE app_releases | |
| SET download_url = ?, file_size = ?, release_notes = ?, updated_at = CURRENT_TIMESTAMP | |
| WHERE platform = ? AND version = ?""", | |
| (download_url, file_size, release_notes, platform, version) | |
| ) | |
| else: | |
| conn.execute( | |
| """INSERT INTO app_releases (platform, version, download_url, file_size, release_notes) | |
| VALUES (?, ?, ?, ?, ?)""", | |
| (platform, version, download_url, file_size, release_notes) | |
| ) | |
| conn.commit() | |
| return jsonify({"message": f"Cập nhật phiên bản {platform} v{version} thành công.", "success": True}) | |
| except Exception as e: | |
| return jsonify({"error": str(e), "success": False}), 500 | |
| finally: | |
| conn.close() | |