Spaces:
Sleeping
Sleeping
Update licensing_client.py
Browse files- licensing_client.py +370 -569
licensing_client.py
CHANGED
|
@@ -1,619 +1,420 @@
|
|
| 1 |
# =========================================================
|
| 2 |
-
# MODULE:
|
| 3 |
-
# SYSTEM: ADVANCED RESOURCE & CONCURRENCY
|
| 4 |
-
# FIX:
|
| 5 |
# AUTHOR: Abu Alone © 2026
|
| 6 |
# =========================================================
|
| 7 |
|
| 8 |
import os
|
| 9 |
-
import
|
| 10 |
import time
|
| 11 |
-
import
|
| 12 |
-
import
|
| 13 |
import subprocess
|
| 14 |
-
from
|
| 15 |
from loguru import logger
|
| 16 |
-
from fastapi import FastAPI, Request, Form, HTTPException, BackgroundTasks
|
| 17 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 18 |
-
from fastapi.staticfiles import StaticFiles
|
| 19 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
from app.services import task as tm
|
| 26 |
-
from app.utils import utils
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
class LiveTerminalLogHandler(logging.Handler):
|
| 45 |
-
def emit(self, record):
|
| 46 |
-
try:
|
| 47 |
-
msg = self.format(record)
|
| 48 |
-
# Ghi log chung vào mọi task đang active để user nào cũng nhìn thấy tiến trình hệ thống
|
| 49 |
-
for task_id in list(GLOBAL_TERMINAL_LOGS.keys()):
|
| 50 |
-
GLOBAL_TERMINAL_LOGS[task_id].append(msg)
|
| 51 |
-
except Exception:
|
| 52 |
-
pass
|
| 53 |
-
|
| 54 |
-
# Cấu hình loguru để đổ dữ liệu stream vào giao diện người dùng
|
| 55 |
-
logger.add(lambda msg: [GLOBAL_TERMINAL_LOGS[tid].append(msg.strip()) for tid in list(GLOBAL_TERMINAL_LOGS.keys())], format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}")
|
| 56 |
-
|
| 57 |
-
# --- 1. SECRETS & CLOUDFLARE DEFAULTS ---
|
| 58 |
-
PEXELS_KEY = os.getenv("PEXELS_KEY", "").strip()
|
| 59 |
-
CF_TOKEN = os.getenv("CF_API_TOKEN", "").strip()
|
| 60 |
-
CF_ID = os.getenv("CF_ACCOUNT_ID", "").strip()
|
| 61 |
-
|
| 62 |
-
if PEXELS_KEY:
|
| 63 |
-
config.app["pexels_api_keys"] = [PEXELS_KEY]
|
| 64 |
-
if CF_TOKEN and CF_ID:
|
| 65 |
-
config.app["llm_provider"] = "cloudflare"
|
| 66 |
-
config.app["cloudflare_account_id"] = CF_ID
|
| 67 |
-
config.app["cloudflare_api_key"] = CF_TOKEN
|
| 68 |
-
config.app["cloudflare_model_name"] = "@cf/meta/llama-3-8b-instruct"
|
| 69 |
|
| 70 |
-
|
| 71 |
-
if os.path.exists(OUTPUT_DIR):
|
| 72 |
-
app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos")
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
try:
|
| 78 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 79 |
-
voices = [line.split("Name:")[-1].strip() for line in f if "Name:" in line]
|
| 80 |
-
if voices: return voices
|
| 81 |
-
except Exception as e:
|
| 82 |
-
logger.error(f"Error reading voice list: {e}")
|
| 83 |
-
return ["en-US-AvaNeural", "vi-VN-HoaiMyNeural"]
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
async def get_system_status(key: str = "", request: Request = None):
|
| 88 |
-
device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
|
| 89 |
try:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id)
|
| 94 |
-
|
| 95 |
-
if is_valid:
|
| 96 |
-
return {
|
| 97 |
-
"thread_string": active_str,
|
| 98 |
-
"key_info": {
|
| 99 |
-
"status": "success",
|
| 100 |
-
"type": key_info.get("tier", "FREE").lower(),
|
| 101 |
-
"username": key_info.get("username", "N/A"), # Trả thêm trường tên người dùng
|
| 102 |
-
"tx_id": key_info.get("tx_name", "N/A"),
|
| 103 |
-
"amount": key_info.get("amount", "N/A"),
|
| 104 |
-
"issued_date": key_info.get("tx_date", "N/A"),
|
| 105 |
-
"expiry_date": key_info.get("expiry", "N/A"),
|
| 106 |
-
"days_left": key_info.get("days_left", 0),
|
| 107 |
-
"show_test_panel": key_info.get("show_test_panel", False)
|
| 108 |
-
}
|
| 109 |
-
}
|
| 110 |
-
else:
|
| 111 |
-
return {
|
| 112 |
-
"thread_string": active_str,
|
| 113 |
-
"key_info": { "status": "failed", "type": "free", "show_test_panel": False }
|
| 114 |
-
}
|
| 115 |
except Exception:
|
| 116 |
-
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
async def validate_user_key(request: Request):
|
| 121 |
try:
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
except Exception:
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
content={
|
| 138 |
-
"status": "error",
|
| 139 |
-
"message": "Authentication Failed: Invalid or expired security token."
|
| 140 |
-
}
|
| 141 |
-
)
|
| 142 |
-
|
| 143 |
-
return {
|
| 144 |
-
"status": "success",
|
| 145 |
-
"message": "Credentials verification payload synchronized.",
|
| 146 |
-
"type": key_info.get("tier", "FREE").lower(),
|
| 147 |
-
"username": key_info.get("username", "N/A"),
|
| 148 |
-
"tx_id": key_info.get("tx_name", "N/A"),
|
| 149 |
-
"amount": key_info.get("amount", "N/A"),
|
| 150 |
-
"issued_date": key_info.get("tx_date", "N/A"),
|
| 151 |
-
"expiry_date": key_info.get("expiry", "N/A"),
|
| 152 |
-
"days_left": key_info.get("days_left", 0)
|
| 153 |
-
}
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
#
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
report = licensing_client.execute_admin_diagnostic_test()
|
| 165 |
-
return {"status": "success", "report": report}
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
device_id = request.client.host if request else "MOBILE_NODE_2026"
|
| 171 |
-
token = license_key.strip()
|
| 172 |
-
try:
|
| 173 |
-
released = licensing_client.force_abort_user_session(token, device_id)
|
| 174 |
-
if released:
|
| 175 |
-
return {"status": "success", "msg": "Task rendering forcefully terminated."}
|
| 176 |
-
return {"status": "error", "msg": "No active tasks found for this session."}
|
| 177 |
-
except Exception as e:
|
| 178 |
-
return {"status": "error", "msg": str(e)}
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
if await request.is_disconnected():
|
| 184 |
-
logger.warning(f"🔌 Disconnect Event Detected (F5 or Tab Closed) for Device {device_id}. Terminating stream.")
|
| 185 |
-
licensing_client.force_abort_user_session(token, device_id)
|
| 186 |
-
licensing_client.release_thread_slot(slot)
|
| 187 |
-
break
|
| 188 |
-
await time.sleep(1)
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
background_tasks: BackgroundTasks,
|
| 195 |
-
video_script: str = Form(...),
|
| 196 |
-
clip_duration: int = Form(10),
|
| 197 |
-
voice_rate: float = Form(1.0),
|
| 198 |
-
selected_voice: str = Form("en-US-AvaNeural"),
|
| 199 |
-
enable_subtitles: bool = Form(True),
|
| 200 |
-
enable_bgm: bool = Form(True),
|
| 201 |
-
license_key: str = Form("")
|
| 202 |
-
):
|
| 203 |
-
if not video_script.strip():
|
| 204 |
-
return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"})
|
| 205 |
-
|
| 206 |
-
token = license_key.strip()
|
| 207 |
-
device_id = request.client.host if request.client else "MOBILE_NODE_2026"
|
| 208 |
-
|
| 209 |
-
is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
|
| 210 |
-
is_vip_key = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False
|
| 211 |
-
bypass_limits = key_info.get("bypass_limits", False)
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
if
|
| 251 |
-
|
| 252 |
-
if
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
GLOBAL_TERMINAL_LOGS[task_id].append("⚠️ Free tier session detected: Injecting mobile fluid watermark layer...")
|
| 257 |
-
wm_filename = f"wm_{filename}"
|
| 258 |
-
watermarked_path = os.path.join(OUTPUT_DIR, wm_filename)
|
| 259 |
-
|
| 260 |
-
drawtext_filter = "drawtext=text='Hugging/AbuAlone09':x='mod(t*35,w)':y='mod(t*15,h)':fontsize=24:fontcolor=white@0.35"
|
| 261 |
-
ffmpeg_cmd = [
|
| 262 |
-
'ffmpeg', '-y',
|
| 263 |
-
'-i', video_path,
|
| 264 |
-
'-vf', drawtext_filter,
|
| 265 |
-
'-c:v', 'libx264',
|
| 266 |
-
'-pix_fmt', 'yuv420p',
|
| 267 |
-
'-c:a', 'copy',
|
| 268 |
-
watermarked_path
|
| 269 |
-
]
|
| 270 |
-
|
| 271 |
-
subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 272 |
-
if os.path.exists(watermarked_path):
|
| 273 |
-
filename = wm_filename
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
"
|
| 281 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
-
|
| 284 |
-
return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": "Render completed but output file not found."})
|
| 285 |
-
|
| 286 |
except Exception as e:
|
| 287 |
-
|
| 288 |
-
return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": f"Render Error: {str(e)}"})
|
| 289 |
-
finally:
|
| 290 |
-
licensing_client.release_thread_slot(allocated_slot)
|
| 291 |
-
background_tasks.add_task(lambda: [time.sleep(30), GLOBAL_TERMINAL_LOGS.pop(task_id, None)])
|
| 292 |
-
|
| 293 |
-
# --- 4. GIAO DIỆN CHÍNH ---
|
| 294 |
-
@app.get("/", response_class=HTMLResponse)
|
| 295 |
-
async def index_page():
|
| 296 |
-
voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()])
|
| 297 |
-
html_content = f"""
|
| 298 |
-
<!DOCTYPE html>
|
| 299 |
-
<html lang="en">
|
| 300 |
-
<head>
|
| 301 |
-
<meta charset="UTF-8">
|
| 302 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 303 |
-
<title>AI VIDEO ENGINE | Control Center</title>
|
| 304 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 305 |
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
| 306 |
-
<style>
|
| 307 |
-
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
| 308 |
-
body {{ font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f1f5f9; background-image: radial-gradient(#cbd5e1 0.5px, transparent 0.5px); background-size: 24px 24px; }}
|
| 309 |
-
.glass-card {{ background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid #ffffff; }}
|
| 310 |
-
.loading-box {{ display: none; background: rgba(220, 38, 38, 0.05); border: 2px dashed #dc2626; color: #991b1b; }}
|
| 311 |
-
.video-wrapper {{ border: none !important; background: transparent !important; box-shadow: none !important; width: 100%; max-width: 320px; margin: 0 auto; }}
|
| 312 |
-
video {{ width: 100%; border-radius: 2rem; display: block; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }}
|
| 313 |
-
</style>
|
| 314 |
-
</head>
|
| 315 |
-
<body class="p-4 md:p-12 lg:p-16">
|
| 316 |
-
<div class="max-w-7xl mx-auto">
|
| 317 |
-
|
| 318 |
-
<header class="text-center mb-12">
|
| 319 |
-
<div class="inline-block bg-indigo-100 text-indigo-700 px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest mb-6">Commercial Video System</div>
|
| 320 |
-
<h1 class="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter italic">AI VIDEO ENGINE</h1>
|
| 321 |
-
<p class="text-slate-500 mt-4 font-semibold uppercase tracking-[0.3em] text-sm md:text-base">v3.1.0 Production Workshop</p>
|
| 322 |
-
</header>
|
| 323 |
-
|
| 324 |
-
<div class="glass-card rounded-[2.5rem] p-6 md:p-8 shadow-sm border border-slate-200 mb-8 text-sm text-slate-600 space-y-4 leading-relaxed">
|
| 325 |
-
<p>🌟 <strong>Automated Production Suite:</strong> An instant AI video factory engineered tailored for Shorts, TikTok, and Reels. It automatically aggregates premium background clips from Pexels, structures high-fidelity AI narrative voices, blends automated audio beds, and burns fully aligned dynamic subtitles—all rendered seamlessly with just 1-click.</p>
|
| 326 |
-
<p>💡 <strong>Our Mission:</strong> We are committed to engineering a highly accessible, low-cost automated content rendering architecture to help creators maximize efficiency while cutting software expenses down to zero.</p>
|
| 327 |
-
<p>⚠️ <strong>Hosting Environment Notice:</strong> This platform is deployed on a <strong>Free Tier Shared Server (2 vCPU, 16GB RAM, Shared Core Containers)</strong> on Hugging Face. Due to unallocated shared hardware limits, video generation cycles can be quite lengthy. To actively protect our instance against memory crashes or sudden cluster restarts, strict thread concurrency thresholds have been applied. We sincerely apologize for any processing delays and thank you for your understanding.</p>
|
| 328 |
-
<p>☕ <strong>Empower the Infrastructure:</strong> To expand our server performance capacities, maintain uninterrupted deployment pipelines, and invest in our upcoming formal platform architectures, your contributions are crucial! Our commercial premium license matrix remains incredibly cost-effective, with the highest tier priced under the cost of two standard cups of coffee. Secure your high-speed access keys here:</p>
|
| 329 |
-
<div class="bg-indigo-50 border border-indigo-100 p-4 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
| 330 |
-
<span class="text-xs font-bold text-indigo-900 uppercase tracking-wide"><i class="fa-solid fa-cookie-bite mr-2"></i> Free tier users: 3 video creations/day (1 video per batch, 3-hour cooldown) with mobile fluid watermark.</span>
|
| 331 |
-
<a href="https://huggingface.co/spaces/AbuAlone09/my-licensify-server" target="_blank" class="bg-indigo-600 text-white font-extrabold px-6 py-2.5 rounded-xl text-xs uppercase tracking-wider hover:bg-indigo-700 transition-all shadow-md shrink-0 text-center w-full sm:w-auto">
|
| 332 |
-
<i class="fa-solid fa-key mr-1.5"></i> Purchase Premium Keys
|
| 333 |
-
</a>
|
| 334 |
-
</div>
|
| 335 |
-
</div>
|
| 336 |
-
|
| 337 |
-
<div class="glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200 mb-8">
|
| 338 |
-
<div class="flex items-center gap-4 mb-6">
|
| 339 |
-
<div class="w-12 h-12 bg-indigo-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-indigo-200">
|
| 340 |
-
<i class="fa-solid fa-lock text-xl"></i>
|
| 341 |
-
</div>
|
| 342 |
-
<div>
|
| 343 |
-
<h3 class="text-xl font-extrabold text-slate-800">🔐 System Authentication</h3>
|
| 344 |
-
<p class="text-slate-400 text-sm">Validate credentials to remove output limitations and stream watermarks</p>
|
| 345 |
-
</div>
|
| 346 |
-
</div>
|
| 347 |
-
<div class="flex flex-col md:flex-row gap-4">
|
| 348 |
-
<input type="password" id="licenseKey" placeholder="Enter valid access token or security key..." class="flex-grow bg-slate-50 border-2 border-slate-100 rounded-2xl px-5 py-4 outline-none focus:border-indigo-500 focus:bg-white transition-all font-mono text-base shadow-inner">
|
| 349 |
-
<button onclick="triggerVerificationHandshake()" class="bg-slate-900 text-white px-8 py-4 rounded-2xl font-black text-base hover:bg-black transition-all shadow-md active:scale-95 shrink-0">
|
| 350 |
-
VALIDATE TOKEN
|
| 351 |
-
</button>
|
| 352 |
-
</div>
|
| 353 |
-
|
| 354 |
-
<div id="keyDetails" class="text-xs font-bold text-amber-600 uppercase tracking-wider mt-4 flex flex-col gap-2 p-3 bg-amber-50/40 rounded-xl border border-amber-100">
|
| 355 |
-
<div><i class="fa-solid fa-circle-exclamation mr-1"></i> Status: Unverified (Free Tier System Active)</div>
|
| 356 |
-
</div>
|
| 357 |
-
|
| 358 |
-
<div id="adminTestPanel" class="hidden mt-4 p-4 bg-slate-900 text-slate-100 rounded-2xl border border-slate-700">
|
| 359 |
-
<div class="text-xs font-black text-emerald-400 uppercase tracking-widest mb-2"><i class="fa-solid fa-terminal"></i> Admin Diagnostic Interface</div>
|
| 360 |
-
<button onclick="triggerAdminTesterScript()" class="bg-emerald-600 px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider hover:bg-emerald-500 transition-all mb-3">Execute tester.py Report</button>
|
| 361 |
-
<div id="diagnosticReportTerminal" class="hidden bg-black text-emerald-500 p-4 rounded-xl font-mono text-xs overflow-x-auto whitespace-pre-wrap max-h-60 border border-slate-800">Waiting for testing process pipeline...</div>
|
| 362 |
-
</div>
|
| 363 |
-
</div>
|
| 364 |
-
|
| 365 |
-
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20">
|
| 366 |
-
<div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200">
|
| 367 |
-
<div class="flex items-center gap-4 mb-8">
|
| 368 |
-
<div class="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
|
| 369 |
-
<i class="fa-solid fa-sliders text-xl"></i>
|
| 370 |
-
</div>
|
| 371 |
-
<div>
|
| 372 |
-
<h3 class="text-xl font-extrabold text-slate-800">⚙️ Video Configuration</h3>
|
| 373 |
-
<p class="text-slate-400 text-sm">Tune rendering parameters and narration variables</p>
|
| 374 |
-
</div>
|
| 375 |
-
</div>
|
| 376 |
-
|
| 377 |
-
<form id="videoForm" onsubmit="handleRenderCycle(event)" class="space-y-6">
|
| 378 |
-
<div>
|
| 379 |
-
<div class="flex justify-between items-center mb-2">
|
| 380 |
-
<label class="text-xs font-black text-slate-500 uppercase tracking-wider">Video Narrative Script</label>
|
| 381 |
-
<span id="charCounter" class="text-xs font-bold text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-md">0 / 1500 Chars</span>
|
| 382 |
-
</div>
|
| 383 |
-
<textarea name="video_script" id="video_script" rows="6" placeholder="Write or paste your script framework..." class="w-full bg-slate-50 border-2 border-slate-100 rounded-2xl px-5 py-4 outline-none focus:border-blue-500 focus:bg-white transition-all font-sans text-base shadow-inner leading-relaxed" required></textarea>
|
| 384 |
-
</div>
|
| 385 |
-
|
| 386 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 387 |
-
<div>
|
| 388 |
-
<label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Clip Duration Selection</label>
|
| 389 |
-
<select name="clip_duration" class="w-full bg-slate-50 border-2 border-slate-100 rounded-xl px-4 py-3 text-slate-800 focus:border-blue-500 outline-none font-medium">
|
| 390 |
-
<option value="5">5 Seconds</option>
|
| 391 |
-
<option value="6">6 Seconds</option>
|
| 392 |
-
<option value="7">7 Seconds</option>
|
| 393 |
-
<option value="8">8 Seconds</option>
|
| 394 |
-
<option value="9">9 Seconds</option>
|
| 395 |
-
<option value="10" selected>10 Seconds (Default)</option>
|
| 396 |
-
</select>
|
| 397 |
-
</div>
|
| 398 |
-
<div>
|
| 399 |
-
<label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Voice Speed Selection</label>
|
| 400 |
-
<select name="voice_rate" class="w-full bg-slate-50 border-2 border-slate-100 rounded-xl px-4 py-3 text-slate-800 focus:border-blue-500 outline-none font-medium">
|
| 401 |
-
<option value="0.5">0.5x</option>
|
| 402 |
-
<option value="1.0" selected>1.0x (Normal)</option>
|
| 403 |
-
<option value="1.5">1.5x</option>
|
| 404 |
-
<option value="2.0">2.0x</option>
|
| 405 |
-
</select>
|
| 406 |
-
</div>
|
| 407 |
-
</div>
|
| 408 |
-
|
| 409 |
-
<div>
|
| 410 |
-
<label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Azure TTS Voice Engine</label>
|
| 411 |
-
<select name="selected_voice" class="w-full bg-slate-50 border-2 border-slate-100 rounded-xl px-4 py-3.5 text-slate-800 focus:border-blue-500 outline-none font-medium">
|
| 412 |
-
{voices_options}
|
| 413 |
-
</select>
|
| 414 |
-
</div>
|
| 415 |
-
|
| 416 |
-
<div class="flex flex-wrap gap-6 bg-slate-50/80 p-4 rounded-2xl border border-slate-100">
|
| 417 |
-
<label class="flex items-center gap-2 cursor-pointer select-none">
|
| 418 |
-
<input type="checkbox" name="enable_subtitles" checked id="subCheck" class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300">
|
| 419 |
-
<span class="text-sm font-bold text-slate-700">Render Subtitles</span>
|
| 420 |
-
</label>
|
| 421 |
-
<label class="flex items-center gap-2 cursor-pointer select-none">
|
| 422 |
-
<input type="checkbox" name="enable_bgm" checked id="bgmCheck" class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300">
|
| 423 |
-
<span class="text-sm font-bold text-slate-700">Background Music (BGM)</span>
|
| 424 |
-
</label>
|
| 425 |
-
</div>
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
</form>
|
| 431 |
-
</div>
|
| 432 |
-
|
| 433 |
-
<div class="lg:col-span-5 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200 h-full flex flex-col justify-between">
|
| 434 |
-
<div class="flex items-center gap-4 mb-6">
|
| 435 |
-
<div class="w-12 h-12 bg-emerald-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-emerald-200">
|
| 436 |
-
<i class="fa-solid fa-film text-xl"></i>
|
| 437 |
-
</div>
|
| 438 |
-
<div>
|
| 439 |
-
<h3 class="text-xl font-extrabold text-slate-800">🎬 Studio Result</h3>
|
| 440 |
-
<p class="text-slate-400 text-sm">Realtime output file delivery node</p>
|
| 441 |
-
</div>
|
| 442 |
-
</div>
|
| 443 |
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
</div>
|
| 466 |
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
txtInput.addEventListener("input", enforceInputConstraints);
|
| 481 |
-
txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }});
|
| 482 |
-
|
| 483 |
-
// --- HÀM XỬ LÝ KHI NGƯỜI DÙNG NHẤN NÚT KiỂM TRA KEY ---
|
| 484 |
-
async function triggerVerificationHandshake() {{
|
| 485 |
-
const currentToken = document.getElementById("licenseKey").value.trim();
|
| 486 |
-
const keyDetails = document.getElementById("keyDetails");
|
| 487 |
-
const adminPanel = document.getElementById("adminTestPanel");
|
| 488 |
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
}}
|
| 493 |
|
| 494 |
-
|
|
|
|
| 495 |
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
if (!response.ok) {{
|
| 506 |
-
throw new Error(data.message || "Key không tồn tại hoặc không hợp lệ.");
|
| 507 |
-
}}
|
| 508 |
-
|
| 509 |
-
// TRƯỜNG HỢP KEY ĐÚNG -> HIỆN TICK XANH (✅) VÀ BẢNG THÔNG TIN CHI TIẾT
|
| 510 |
-
if(data.type === "admin") {{
|
| 511 |
-
adminPanel.classList.remove("hidden");
|
| 512 |
-
}} else {{
|
| 513 |
-
adminPanel.classList.add("hidden");
|
| 514 |
-
}}
|
| 515 |
-
|
| 516 |
-
keyDetails.innerHTML = `
|
| 517 |
-
<div class="text-emerald-600 font-extrabold grid grid-cols-1 sm:grid-cols-2 gap-1 text-xs">
|
| 518 |
-
<div class="col-span-1 sm:col-span-2 text-emerald-700 font-black text-sm mb-1">
|
| 519 |
-
<i class="fa-solid fa-circle-check text-base mr-1"></i> [✅ KEY HỢP LỆ] KÍCH HOẠT THÀNH CÔNG
|
| 520 |
-
</div>
|
| 521 |
-
<div>🆔 Mã giao dịch (TxID): \${{data.tx_id}}</div>
|
| 522 |
-
<div>👤 Tên người dùng: \${{data.username}}</div>
|
| 523 |
-
<div>💰 Số tiền đã thanh toán: \${{data.amount}}</div>
|
| 524 |
-
<div>📅 Ngày cấp key: \${{data.issued_date}}</div>
|
| 525 |
-
<div class="col-span-1 sm:col-span-2 text-indigo-700 mt-1">⏳ Hạn sử dụng: \${{data.expiry_date}} (\${{data.days_left}} ngày còn lại)</div>
|
| 526 |
-
</div>
|
| 527 |
-
`;
|
| 528 |
-
|
| 529 |
-
}} catch (error) {{
|
| 530 |
-
// TRƯỜNG HỢP KEY SAI -> HIỆN X ĐỎ (❌) KHÔNG HỢP LỆ
|
| 531 |
-
adminPanel.classList.add("hidden");
|
| 532 |
-
keyDetails.innerHTML = '<div class="text-rose-600 font-black text-sm"><i class="fa-solid fa-circle-xmark text-base mr-1"></i> [❌ KEY KHÔNG HỢP LỆ] Key không tồn tại, sai định dạng hoặc đã hết hạn!</div>';
|
| 533 |
-
}}
|
| 534 |
-
}}
|
| 535 |
-
|
| 536 |
-
async function triggerAdminTesterScript() {{
|
| 537 |
-
const term = document.getElementById("diagnosticReportTerminal");
|
| 538 |
-
term.classList.remove("hidden");
|
| 539 |
-
term.innerText = "Executing tester.py pipeline, please hold...";
|
| 540 |
-
try {{
|
| 541 |
-
const res = await fetch("/api/admin/run-test", {{ method: "POST" }});
|
| 542 |
-
const data = await res.json();
|
| 543 |
-
term.innerText = data.report;
|
| 544 |
-
}} catch(e) {{
|
| 545 |
-
term.innerText = "❌ Diagnostic communication pipeline broken: " + e;
|
| 546 |
-
}}
|
| 547 |
-
}}
|
| 548 |
-
|
| 549 |
-
async function handleRenderCycle(e) {{
|
| 550 |
-
e.preventDefault();
|
| 551 |
-
const targetBtn = document.getElementById("mainSubmitBtn");
|
| 552 |
-
const iconObj = document.getElementById("btnIcon");
|
| 553 |
-
const textObj = document.getElementById("btnText");
|
| 554 |
-
const currentToken = document.getElementById("licenseKey").value.trim();
|
| 555 |
-
|
| 556 |
-
if(runningStateActive) {{
|
| 557 |
-
if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{
|
| 558 |
-
const fData = new FormData();
|
| 559 |
-
fData.append("license_key", currentToken);
|
| 560 |
-
await fetch("/api/cancel-task", {{ method: "POST", body: fData }});
|
| 561 |
-
revertUiLayoutToNormal();
|
| 562 |
-
}}
|
| 563 |
-
return;
|
| 564 |
-
}}
|
| 565 |
-
|
| 566 |
-
runningStateActive = true;
|
| 567 |
-
targetBtn.className = "w-full bg-red-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-red-700 transition-all shadow-xl active:scale-95 flex items-center justify-center gap-2";
|
| 568 |
-
textObj.innerText = "STOP & ABORT RENDERING";
|
| 569 |
-
iconObj.className = "fa-solid fa-circle-stop";
|
| 570 |
-
|
| 571 |
-
document.getElementById("loadingBox").style.display = "block";
|
| 572 |
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
player.pause();
|
| 582 |
|
| 583 |
-
|
| 584 |
-
const response = await fetch("/api/generate", {{ method: "POST", body: payloadData }});
|
| 585 |
-
const serverResponse = await response.json();
|
| 586 |
-
|
| 587 |
-
if(serverResponse.status === "success") {{
|
| 588 |
-
source.src = serverResponse.video_url;
|
| 589 |
-
player.src = serverResponse.video_url;
|
| 590 |
-
player.load();
|
| 591 |
-
setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
|
| 592 |
-
alert("✅ Video processing completed successfully!");
|
| 593 |
-
}} else {{
|
| 594 |
-
alert("❌ " + serverResponse.msg);
|
| 595 |
-
}}
|
| 596 |
-
}} catch(err) {{
|
| 597 |
-
alert("❌ Connection lost or pipeline aborted.");
|
| 598 |
-
}} finally {{
|
| 599 |
-
revertUiLayoutToNormal();
|
| 600 |
-
}}
|
| 601 |
-
}}
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
""
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# =========================================================
|
| 2 |
+
# MODULE: licensing_client.py
|
| 3 |
+
# SYSTEM: ADVANCED RESOURCE & CONCURRENCY THREAD MANAGER
|
| 4 |
+
# FIX: DYNAMIC FILE-BASED TRANSACTION SHARING FOR MULTI-WORKERS
|
| 5 |
# AUTHOR: Abu Alone © 2026
|
| 6 |
# =========================================================
|
| 7 |
|
| 8 |
import os
|
| 9 |
+
import json
|
| 10 |
import time
|
| 11 |
+
import signal
|
| 12 |
+
import requests
|
| 13 |
import subprocess
|
| 14 |
+
from datetime import datetime
|
| 15 |
from loguru import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# Cấu hình đường dẫn lưu trữ và thông tin Server cổng thanh toán dịch vụ
|
| 18 |
+
STORAGE_FILE = "Hugging AbuAlone09/AI-Video-Engine-storage"
|
| 19 |
+
# File trạng thái phân luồng dùng chung giữa toàn bộ các Worker tiến trình để số nhảy thực tế từng giây
|
| 20 |
+
THREAD_STATUS_FILE = "Hugging AbuAlone09/AI-Video-Engine-threads"
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
SERVER_URL = os.getenv("LICENSIFY_SERVER_URL", "https://abualone09-my-licensify-server.hf.space").strip()
|
| 23 |
+
SECRET_API_KEY = os.getenv("SECRET_API_KEY", "YOUR_SECRET_API_KEY_HERE").strip()
|
| 24 |
|
| 25 |
+
# Khởi tạo thư mục và tệp cấu trúc lưu trữ
|
| 26 |
+
os.makedirs(os.path.dirname(STORAGE_FILE), exist_ok=True)
|
| 27 |
|
| 28 |
+
def _init_system_files():
|
| 29 |
+
if not os.path.exists(STORAGE_FILE):
|
| 30 |
+
with open(STORAGE_FILE, "w", encoding="utf-8") as f:
|
| 31 |
+
json.dump({"keys": {}, "free_devices": {}}, f, indent=4)
|
| 32 |
+
|
| 33 |
+
# Khởi tạo pool 6 luồng thực tế vào file dùng chung nếu chưa có
|
| 34 |
+
if not os.path.exists(THREAD_STATUS_FILE):
|
| 35 |
+
initial_pool = {str(i): {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0} for i in range(1, 7)}
|
| 36 |
+
with open(THREAD_STATUS_FILE, "w", encoding="utf-8") as f:
|
| 37 |
+
json.dump(initial_pool, f, indent=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
_init_system_files()
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
# =========================================================
|
| 42 |
+
# THÀNH PHẦN CORE: ĐỒNG BỘ THỜI GIAN THỰC ĐA TIẾN TRÌNH (FILE-BASED MONITOR)
|
| 43 |
+
# =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
def _load_threads_state() -> dict:
|
| 46 |
+
"""Đọc trực tiếp trạng thái luồng từ tệp dùng chung để tránh lệch số giữa các API worker"""
|
|
|
|
|
|
|
| 47 |
try:
|
| 48 |
+
if os.path.exists(THREAD_STATUS_FILE):
|
| 49 |
+
with open(THREAD_STATUS_FILE, "r", encoding="utf-8") as f:
|
| 50 |
+
return json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
except Exception:
|
| 52 |
+
pass
|
| 53 |
+
return {str(i): {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0} for i in range(1, 7)}
|
| 54 |
|
| 55 |
+
def _save_threads_state(state: dict):
|
| 56 |
+
"""Ghi trạng thái luồng xuống tệp khóa ngay lập tức để đồng bộ hóa"""
|
|
|
|
| 57 |
try:
|
| 58 |
+
with open(THREAD_STATUS_FILE, "w", encoding="utf-8") as f:
|
| 59 |
+
json.dump(state, f, indent=4)
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Failed to write thread state cluster: {e}")
|
| 62 |
+
|
| 63 |
+
def get_active_threads_count() -> int:
|
| 64 |
+
"""Quét sạch tiến trình chết rớt mạng rồi tính tổng số luồng thực tế đang bận render"""
|
| 65 |
+
clean_dead_or_zombie_threads()
|
| 66 |
+
state = _load_threads_state()
|
| 67 |
+
return sum(1 for t in state.values() if t["status"] == "rendering")
|
| 68 |
+
|
| 69 |
+
def clean_dead_or_zombie_threads():
|
| 70 |
+
"""Kiểm tra PID hệ thống Linux để giải phóng luồng ngay lập tức nếu người dùng đóng tab, rớt mạng, F5"""
|
| 71 |
+
state = _load_threads_state()
|
| 72 |
+
changed = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
for slot_id, thread in state.items():
|
| 75 |
+
if thread["status"] == "rendering":
|
| 76 |
+
pid = thread.get("pid")
|
| 77 |
+
if pid:
|
| 78 |
+
try:
|
| 79 |
+
# Gửi tín hiệu 0 để kiểm tra xem tiến trình tạo video đó còn sống thực tế không
|
| 80 |
+
os.kill(pid, 0)
|
| 81 |
+
except (ProcessLookupError, PermissionError):
|
| 82 |
+
# Tiến trình đã chết (người dùng hủy tab hoặc crash giữa chừng) -> Trả luồng về idle lập tức
|
| 83 |
+
logger.warning(f"🧹 Clean-up monitor detected dead PID {pid} on Slot {slot_id}. Reverting to idle.")
|
| 84 |
+
state[slot_id] = {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0}
|
| 85 |
+
changed = True
|
| 86 |
+
|
| 87 |
+
if changed:
|
| 88 |
+
_save_threads_state(state)
|
| 89 |
|
| 90 |
+
# =========================================================
|
| 91 |
+
# CORE 1: XỬ LÝ ĐỌC / GHI & XÁC THỰC API KEY TỪ XA TỚI SERVER
|
| 92 |
+
# =========================================================
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
def _load_storage():
|
| 95 |
+
with open(STORAGE_FILE, "r", encoding="utf-8") as f:
|
| 96 |
+
return json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
def _save_storage(data):
|
| 99 |
+
with open(STORAGE_FILE, "w", encoding="utf-8") as f:
|
| 100 |
+
json.dump(data, f, indent=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
def clean_expired_keys():
|
| 103 |
+
data = _load_storage()
|
| 104 |
+
now_ts = time.time()
|
| 105 |
+
changed = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
for k, info in list(data["keys"].items()):
|
| 108 |
+
if now_ts > info.get("expiry_timestamp", 0):
|
| 109 |
+
del data["keys"][k]
|
| 110 |
+
changed = True
|
| 111 |
+
logger.warning(f"Key {k} expired and has been automatically purged from storage.")
|
| 112 |
|
| 113 |
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
| 114 |
+
for dev, info in list(data["free_devices"].items()):
|
| 115 |
+
if info.get("last_date") != today_str:
|
| 116 |
+
info["daily_batches"] = 0
|
| 117 |
+
info["last_date"] = today_str
|
| 118 |
+
changed = True
|
| 119 |
+
|
| 120 |
+
if changed:
|
| 121 |
+
_save_storage(data)
|
| 122 |
+
|
| 123 |
+
def verify_and_get_license_info(key_input: str, device_id: str) -> tuple:
|
| 124 |
+
clean_expired_keys()
|
| 125 |
+
token = key_input.strip()
|
| 126 |
|
| 127 |
+
admin_secret = os.getenv("ADMIN_KEY", "ADMIN_ABUALONE_2026").strip()
|
| 128 |
+
vip_secret = os.getenv("VIP_KEY", "VIP_PROMO_2026").strip()
|
| 129 |
|
| 130 |
+
if token == admin_secret:
|
| 131 |
+
return True, {
|
| 132 |
+
"tier": "ADMIN",
|
| 133 |
+
"tx_name": "System Master Administrator",
|
| 134 |
+
"amount": "$0.00 (Root Privilege)",
|
| 135 |
+
"tx_date": datetime.now().strftime("%Y-%m-%d"),
|
| 136 |
+
"expiry": "Permanent Access",
|
| 137 |
+
"days_left": 9999,
|
| 138 |
+
"msg": "📋 SYSTEM STATUS: Admin Master Active. Full hardware diagnostic test layer unlocked.",
|
| 139 |
+
"show_test_panel": True,
|
| 140 |
+
"remove_watermark": True,
|
| 141 |
+
"bypass_limits": True
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if token == vip_secret:
|
| 145 |
+
return True, {
|
| 146 |
+
"tier": "VIP",
|
| 147 |
+
"tx_name": "VIP Promotional Access",
|
| 148 |
+
"amount": "$0.00 (Promo Tier)",
|
| 149 |
+
"tx_date": datetime.now().strftime("%Y-%m-%d"),
|
| 150 |
+
"expiry": "2026-12-31",
|
| 151 |
+
"days_left": 200,
|
| 152 |
+
"msg": "👑 VIP STATUS: Promo Key Verified. Watermarks and daily rendering limits removed.",
|
| 153 |
+
"show_test_panel": False,
|
| 154 |
+
"remove_watermark": True,
|
| 155 |
+
"bypass_limits": True
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
data = _load_storage()
|
| 159 |
|
| 160 |
+
if token in data["keys"]:
|
| 161 |
+
info = data["keys"][token]
|
| 162 |
+
days_left = int((info["expiry_timestamp"] - time.time()) / 86400)
|
| 163 |
+
return True, {
|
| 164 |
+
"tier": "VIP", "tx_name": info["tx_name"], "amount": info["amount"],
|
| 165 |
+
"tx_date": info["tx_date"], "expiry": info["expiry"], "days_left": max(0, days_left),
|
| 166 |
+
"msg": f"👑 VIP ACTIVE | User: {info['tx_name']} | {max(0, days_left)} days remaining.",
|
| 167 |
+
"show_test_panel": False,
|
| 168 |
+
"remove_watermark": True,
|
| 169 |
+
"bypass_limits": False
|
| 170 |
+
}
|
| 171 |
|
| 172 |
+
try:
|
| 173 |
+
headers = {"X-API-Key": SECRET_API_KEY, "Content-Type": "application/json"}
|
| 174 |
+
payload = {"key": token, "hwid": device_id}
|
| 175 |
+
response = requests.post(f"{SERVER_URL}/api/verify-key", json=payload, headers=headers, timeout=10)
|
| 176 |
|
| 177 |
+
if response.status_code == 200:
|
| 178 |
+
res_data = response.json()
|
| 179 |
+
if res_data.get("status") == "success":
|
| 180 |
+
tx_info = res_data.get("data", {})
|
| 181 |
+
expiry_str = tx_info.get("expiry_date")
|
| 182 |
+
expiry_ts = time.mktime(time.strptime(expiry_str, "%Y-%m-%d"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
data["keys"][token] = {
|
| 185 |
+
"tx_name": tx_info.get("buyer_name", "Anonymous User"),
|
| 186 |
+
"amount": tx_info.get("amount_paid", "$2.99"),
|
| 187 |
+
"tx_date": tx_info.get("payment_date", datetime.now().strftime("%Y-%m-%d")),
|
| 188 |
+
"expiry": expiry_str,
|
| 189 |
+
"expiry_timestamp": expiry_ts
|
| 190 |
+
}
|
| 191 |
+
_save_storage(data)
|
| 192 |
|
| 193 |
+
days_left = int((expiry_ts - time.time()) / 86400)
|
| 194 |
+
return True, {
|
| 195 |
+
"tier": "VIP", "tx_name": tx_info.get("buyer_name"), "amount": tx_info.get("amount_paid"),
|
| 196 |
+
"tx_date": tx_info.get("payment_date"), "expiry": expiry_str, "days_left": max(0, days_left),
|
| 197 |
+
"msg": f"👑 VIP KEY VERIFIED | Owner: {tx_info.get('buyer_name')} | Expiry: {expiry_str}.",
|
| 198 |
+
"show_test_panel": False,
|
| 199 |
+
"remove_watermark": True,
|
| 200 |
+
"bypass_limits": False
|
| 201 |
}
|
| 202 |
+
return False, {"msg": "❌ Invalid Access Key or communication failure with payment gateway!", "show_test_panel": False, "remove_watermark": False, "tier": "FREE"}
|
|
|
|
|
|
|
| 203 |
except Exception as e:
|
| 204 |
+
return False, {"msg": f"⚠️ Connection error to Licensify Server! Info: {str(e)}", "show_test_panel": False, "remove_watermark": False, "tier": "FREE"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
# =========================================================
|
| 207 |
+
# CORE 2: CƠ CHẾ PHÂN LUỒNG THÔNG MINH (5 VIP - 1 FREE, ÉP TRỤC XUẤT)
|
| 208 |
+
# =========================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
def get_thread_status_json():
|
| 211 |
+
"""Hàm này nhảy số thực tế từng giây dựa trên trạng thái tệp tập trung"""
|
| 212 |
+
clean_dead_or_zombie_threads()
|
| 213 |
+
state = _load_threads_state()
|
| 214 |
+
busy_count = sum(1 for t in state.values() if t["status"] == "rendering")
|
| 215 |
+
vip_count = sum(1 for t in state.values() if t["type"] == "VIP")
|
| 216 |
+
free_count = sum(1 for t in state.values() if t["type"] == "FREE")
|
| 217 |
+
return {
|
| 218 |
+
"busy_channels": f"{busy_count}/6",
|
| 219 |
+
"vip_active": vip_count,
|
| 220 |
+
"free_active": free_count,
|
| 221 |
+
"pool": state
|
| 222 |
+
}
|
| 223 |
|
| 224 |
+
def allocate_render_thread(key_input: str, device_id: str, is_vip: bool) -> tuple:
|
| 225 |
+
"""Cơ chế phân luồng nghiêm ngặt: 5 luồng VIP độc quyền (1-5), luồng 6 cho Free.
|
| 226 |
+
Nếu VIP vào mà hệ thống full, Free ở luồng VIP hoặc luồng 6 sẽ bị ngắt tiến trình lập tức và không tính lượt."""
|
| 227 |
+
clean_dead_or_zombie_threads()
|
| 228 |
+
state = _load_threads_state()
|
| 229 |
+
token = key_input.strip()
|
| 230 |
+
|
| 231 |
+
# Chặn đứng trường hợp chính Key đó hoặc Device đó đang render trùng lặp
|
| 232 |
+
for slot_id, thread in state.items():
|
| 233 |
+
if thread["status"] == "rendering":
|
| 234 |
+
if is_vip and thread["key"] == token:
|
| 235 |
+
return False, "❌ This VIP Key is already running a rendering process! Multi-thread allocation denied."
|
| 236 |
+
if not is_vip and thread["device"] == device_id:
|
| 237 |
+
return False, "❌ Your device is currently rendering a video. Please wait until it completes!"
|
| 238 |
+
|
| 239 |
+
target_slot = None
|
| 240 |
+
|
| 241 |
+
if is_vip:
|
| 242 |
+
# 1. Tìm vị trí trống trong 5 slot đầu của VIP
|
| 243 |
+
for i in range(1, 6):
|
| 244 |
+
if state[str(i)]["status"] == "idle":
|
| 245 |
+
target_slot = str(i)
|
| 246 |
+
break
|
| 247 |
+
|
| 248 |
+
# 2. Nếu 5 slot đầu bận, kiểm tra slot 6 của Free xem có trống không
|
| 249 |
+
if not target_slot and state["6"]["status"] == "idle":
|
| 250 |
+
target_slot = "6"
|
| 251 |
+
|
| 252 |
+
# 3. [CƠ CHẾ ÉP TRỤC XUẤT]: Nếu toàn bộ máy chủ 6 luồng đều bận, tìm luồng nào của USER THƯỜNG đang chạy ké để ngắt khẩn cấp
|
| 253 |
+
if not target_slot:
|
| 254 |
+
for i in ["6", "1", "2", "3", "4", "5"]:
|
| 255 |
+
if state[i]["type"] == "FREE":
|
| 256 |
+
free_pid = state[i]["pid"]
|
| 257 |
+
logger.warning(f"👑 VIP Eviction Triggered: Terminating Free PID {free_pid} at Slot {i} to liberate resources.")
|
| 258 |
+
try:
|
| 259 |
+
if free_pid:
|
| 260 |
+
os.kill(free_pid, signal.SIGKILL) # Đánh sập tiến trình tạo video của User thường lập tức
|
| 261 |
+
except ProcessLookupError:
|
| 262 |
+
pass
|
| 263 |
+
|
| 264 |
+
target_slot = i
|
| 265 |
+
state[i] = {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0}
|
| 266 |
+
break
|
| 267 |
+
else:
|
| 268 |
+
# Đối với User thường: Kiểm tra slot 6 độc quyền trước
|
| 269 |
+
if state["6"]["status"] == "idle":
|
| 270 |
+
target_slot = "6"
|
| 271 |
+
else:
|
| 272 |
+
# Nếu slot 6 bận, cho phép dùng ké các slot VIP từ 1-5 nếu chúng đang rảnh hoàn toàn
|
| 273 |
+
for i in range(1, 6):
|
| 274 |
+
if state[str(i)]["status"] == "idle":
|
| 275 |
+
target_slot = str(i)
|
| 276 |
+
break
|
| 277 |
+
|
| 278 |
+
if not target_slot:
|
| 279 |
+
return False, "⚠️ All rendering channels are currently full. Please try again in a few moments!"
|
| 280 |
+
|
| 281 |
+
return True, int(target_slot)
|
| 282 |
+
|
| 283 |
+
def register_process_to_slot(slot: int, key_input: str, device_id: str, is_vip: bool, pid: int):
|
| 284 |
+
state = _load_threads_state()
|
| 285 |
+
state[str(slot)] = {
|
| 286 |
+
"status": "rendering",
|
| 287 |
+
"key": key_input.strip() if is_vip else None,
|
| 288 |
+
"type": "VIP" if is_vip else "FREE",
|
| 289 |
+
"pid": pid,
|
| 290 |
+
"device": device_id,
|
| 291 |
+
"start_time": time.time()
|
| 292 |
+
}
|
| 293 |
+
_save_threads_state(state)
|
| 294 |
|
| 295 |
+
def release_thread_slot(slot: int):
|
| 296 |
+
state = _load_threads_state()
|
| 297 |
+
slot_str = str(slot)
|
| 298 |
+
if slot_str in state:
|
| 299 |
+
state[slot_str] = {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0}
|
| 300 |
+
_save_threads_state(state)
|
|
|
|
| 301 |
|
| 302 |
+
# =========================================================
|
| 303 |
+
# CORE 3: GIÁM SÁT HẠN MỨC CHẶN CHẶT CHẼ
|
| 304 |
+
# =========================================================
|
| 305 |
|
| 306 |
+
def check_generation_limits(key_input: str, device_id: str, is_vip: bool) -> tuple:
|
| 307 |
+
clean_expired_keys()
|
| 308 |
+
data = _load_storage()
|
| 309 |
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
| 310 |
+
now_ts = time.time()
|
| 311 |
+
|
| 312 |
+
if is_vip:
|
| 313 |
+
if key_input not in data["keys"]:
|
| 314 |
+
return False, "License error: Key missing from verified list."
|
| 315 |
|
| 316 |
+
vip_data = data["keys"][key_input]
|
| 317 |
+
if vip_data.get("last_date_used") != today_str:
|
| 318 |
+
vip_data["last_date_used"] = today_str
|
| 319 |
+
vip_data["daily_batches"] = 0
|
| 320 |
+
vip_data["videos_this_batch"] = 0
|
| 321 |
+
vip_data["cooldown_until"] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
+
if now_ts < vip_data.get("cooldown_until", 0):
|
| 324 |
+
wait_min = int((vip_data["cooldown_until"] - now_ts) / 60)
|
| 325 |
+
return False, f"⏱️ Cooldown active! Please return in {wait_min} minutes."
|
|
|
|
| 326 |
|
| 327 |
+
if vip_data.get("daily_batches", 0) >= 5:
|
| 328 |
+
return False, "❌ You have exhausted your daily limit of 5 batches. Please return tomorrow!"
|
| 329 |
|
| 330 |
+
remaining_batches = 5 - vip_data.get("daily_batches", 0)
|
| 331 |
+
return True, {"status_str": f"VIP Usage: Batch {vip_data.get('daily_batches', 0)}/5"}
|
| 332 |
+
else:
|
| 333 |
+
if device_id not in data["free_devices"]:
|
| 334 |
+
data["free_devices"][device_id] = {
|
| 335 |
+
"last_date": today_str,
|
| 336 |
+
"daily_batches": 0,
|
| 337 |
+
"cooldown_until": 0
|
| 338 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
+
free_data = data["free_devices"][device_id]
|
| 341 |
+
if free_data.get("last_date") != today_str:
|
| 342 |
+
free_data["last_date"] = today_str
|
| 343 |
+
free_data["daily_batches"] = 0
|
| 344 |
+
free_data["cooldown_until"] = 0
|
| 345 |
|
| 346 |
+
if now_ts < free_data.get("cooldown_until", 0):
|
| 347 |
+
wait_hours = int((free_data["cooldown_until"] - now_ts) / 3600)
|
| 348 |
+
return False, f"⏱️ Free Tier Cooldown: Please wait {wait_hours + 1} hours before generating again, or upgrade to VIP Premium!"
|
| 349 |
|
| 350 |
+
if free_data.get("daily_batches", 0) >= 3:
|
| 351 |
+
return False, "❌ Free Tier Exhausted! Maximum 3 daily videos reached."
|
|
|
|
| 352 |
|
| 353 |
+
return True, {"status_str": f"Free Tier Usage: {free_data.get('daily_batches', 0)}/3 Videos"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
+
def commit_generation_success(key_input: str, device_id: str, is_vip: bool):
|
| 356 |
+
"""Chỉ cắn trừ lượt tạo thực tế khi video đã render thành công hoàn toàn ra file output"""
|
| 357 |
+
data = _load_storage()
|
| 358 |
+
now_ts = time.time()
|
| 359 |
+
|
| 360 |
+
if is_vip:
|
| 361 |
+
if key_input in data["keys"]:
|
| 362 |
+
vip_data = data["keys"][key_input]
|
| 363 |
+
vip_data["videos_this_batch"] = vip_data.get("videos_this_batch", 0) + 1
|
| 364 |
+
if vip_data["videos_this_batch"] >= 2:
|
| 365 |
+
vip_data["daily_batches"] = vip_data.get("daily_batches", 0) + 1
|
| 366 |
+
vip_data["videos_this_batch"] = 0
|
| 367 |
+
vip_data["cooldown_until"] = now_ts + 3600
|
| 368 |
+
else:
|
| 369 |
+
if device_id in data["free_devices"]:
|
| 370 |
+
free_data = data["free_devices"][device_id]
|
| 371 |
+
free_data["daily_batches"] = free_data.get("daily_batches", 0) + 1
|
| 372 |
+
free_data["cooldown_until"] = now_ts + (3 * 3600)
|
| 373 |
+
|
| 374 |
+
_save_storage(data)
|
| 375 |
+
|
| 376 |
+
# =========================================================
|
| 377 |
+
# CORE 4: FORCE STOP - KHÔNG TÍNH LƯỢT KHI HỦY HOẶC NÉT MẠNG RỚT
|
| 378 |
+
# =========================================================
|
| 379 |
+
|
| 380 |
+
def force_abort_user_session(key_input: str, device_id: str) -> bool:
|
| 381 |
+
"""Khi người dùng nhấn nút STOP đỏ hoặc hệ thống quét F5: Hủy tiến trình Linux lập tức,
|
| 382 |
+
luồng giải phóng hoàn toàn về idle và TUYỆT ĐỐI KHÔNG cắn trừ lượt tạo video của user."""
|
| 383 |
+
state = _load_threads_state()
|
| 384 |
+
token = key_input.strip()
|
| 385 |
+
released = False
|
| 386 |
+
|
| 387 |
+
for slot_id, thread in state.items():
|
| 388 |
+
if thread["status"] == "rendering":
|
| 389 |
+
# Đối soát chuẩn xác phiên làm việc dựa trên key (VIP) hoặc IP thiết bị (Free)
|
| 390 |
+
is_match = (thread["key"] == token) if thread["type"] == "VIP" else (thread["device"] == device_id)
|
| 391 |
+
if is_match:
|
| 392 |
+
pid = thread.get("pid")
|
| 393 |
+
if pid:
|
| 394 |
+
try:
|
| 395 |
+
logger.warning(f"🛑 Manual Force Abort triggered. Killing Video Engine Process PID {pid} instantly.")
|
| 396 |
+
os.kill(pid, signal.SIGKILL)
|
| 397 |
+
except ProcessLookupError:
|
| 398 |
+
pass
|
| 399 |
+
|
| 400 |
+
state[slot_id] = {"status": "idle", "key": None, "type": None, "pid": None, "device": None, "start_time": 0}
|
| 401 |
+
released = True
|
| 402 |
+
|
| 403 |
+
if released:
|
| 404 |
+
_save_threads_state(state)
|
| 405 |
+
return released
|
| 406 |
+
|
| 407 |
+
def execute_admin_diagnostic_test() -> str:
|
| 408 |
+
test_script = "tester.py"
|
| 409 |
+
if not os.path.exists(test_script):
|
| 410 |
+
return f"❌ FILE MISSING: Tệp '{test_script}' không tồn tại!"
|
| 411 |
+
try:
|
| 412 |
+
result = subprocess.run(["python", test_script], capture_output=True, text=True, timeout=15)
|
| 413 |
+
if result.returncode == 0:
|
| 414 |
+
return f"✅ [TESTER REPORT SUCCESS]:\n{result.stdout.strip()}"
|
| 415 |
+
else:
|
| 416 |
+
return f"❌ [TESTER REPORT CRASHED WITH EXIT CODE {result.returncode}]:\n{result.stderr.strip()}"
|
| 417 |
+
except subprocess.TimeoutExpired:
|
| 418 |
+
return "⚠️ [TESTER TIMEOUT]: Tiến trình kiểm thử vượt ngưỡng 15 giây!"
|
| 419 |
+
except Exception as e:
|
| 420 |
+
return f"❌ [SYSTEM CRASH]: Lỗi: {str(e)}"
|