Spaces:
Sleeping
Sleeping
Update licensing_client.py
Browse files- licensing_client.py +569 -371
licensing_client.py
CHANGED
|
@@ -1,421 +1,619 @@
|
|
| 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 |
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
os.makedirs(os.path.dirname(STORAGE_FILE), exist_ok=True)
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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 |
-
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
def
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
try:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
except Exception:
|
| 52 |
-
|
| 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 |
-
|
| 56 |
-
|
|
|
|
| 57 |
try:
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
except Exception
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 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 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 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 |
-
|
| 128 |
-
|
|
|
|
| 129 |
|
| 130 |
-
if
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
"
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
| 176 |
|
| 177 |
-
if
|
| 178 |
-
|
| 179 |
-
if
|
| 180 |
-
|
| 181 |
-
expiry_str = tx_info.get("expiry_date")
|
| 182 |
-
expiry_ts = time.mktime(time.strptime(expiry_str, "%Y-%m-%d"))
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
"
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
"
|
| 196 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
| 203 |
except Exception as e:
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 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 |
-
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 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 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 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
| 326 |
|
| 327 |
-
|
| 328 |
-
return False, "❌ You have exhausted your daily limit of 5 batches. Please return tomorrow!"
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
|
| 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 |
-
|
| 351 |
-
return False, "❌ Free Tier Exhausted! Maximum 3 daily videos reached."
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 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 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# =========================================================
|
| 2 |
+
# MODULE: run_app.py
|
| 3 |
+
# SYSTEM: ADVANCED RESOURCE & CONCURRENCY WORKSPACE CONTROL
|
| 4 |
+
# FIX: REALTIME TERMINAL LOG PIPELINE & DISCONNECT MONITOR
|
| 5 |
# AUTHOR: Abu Alone © 2026
|
| 6 |
# =========================================================
|
| 7 |
|
| 8 |
import os
|
| 9 |
+
import sys
|
| 10 |
import time
|
| 11 |
+
import shutil
|
| 12 |
+
import logging
|
| 13 |
import subprocess
|
| 14 |
+
from uuid import uuid4
|
| 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 |
+
# Import các logic cốt lõi tạo video (Giữ nguyên vẹn 100% cấu trúc của ông)
|
| 22 |
+
from app.config import config
|
| 23 |
+
from app.models.schema import VideoParams, VideoAspect, VideoConcatMode
|
| 24 |
+
from app.services import voice
|
| 25 |
+
from app.services import task as tm
|
| 26 |
+
from app.utils import utils
|
| 27 |
|
| 28 |
+
# Đồng bộ hóa chặt chẽ với module quản lý luồng bảo mật
|
| 29 |
+
import licensing_client
|
| 30 |
|
| 31 |
+
app = FastAPI(title="AI Video Engine Commercial")
|
|
|
|
| 32 |
|
| 33 |
+
app.add_middleware(
|
| 34 |
+
CORSMiddleware,
|
| 35 |
+
allow_origins=["*"],
|
| 36 |
+
allow_credentials=True,
|
| 37 |
+
allow_methods=["*"],
|
| 38 |
+
allow_headers=["*"],
|
| 39 |
+
)
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
# --- BỘ NHỚ LƯU TRỮ LOG TẠM THỜI ĐỂ ĐẨY REALTIME LÊN GIAO DIỆN ---
|
| 42 |
+
GLOBAL_TERMINAL_LOGS = {}
|
| 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 |
+
OUTPUT_DIR = utils.storage_dir("videos", True)
|
| 71 |
+
if os.path.exists(OUTPUT_DIR):
|
| 72 |
+
app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos")
|
| 73 |
|
| 74 |
+
def get_voice_list():
|
| 75 |
+
path = os.path.join(os.path.dirname(__file__), "voice-list.txt")
|
| 76 |
+
if os.path.exists(path):
|
| 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 |
+
# --- API CẬP NHẬT TRẠNG THÁI LUỒNG THỜI GIAN THỰC ĐỒNG BỘ MỖI GIÂY ---
|
| 86 |
+
@app.get("/api/system-status")
|
| 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 |
+
thread_data = licensing_client.get_thread_status_json()
|
| 91 |
+
active_str = thread_data["busy_channels"]
|
| 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 |
+
return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } }
|
|
|
|
| 117 |
|
| 118 |
+
# --- API CHUYÊN TRÁCH XỬ LÝ NÚT BẤM VALIDATE TOKEN ---
|
| 119 |
+
@app.post("/api/validate-key")
|
| 120 |
+
async def validate_user_key(request: Request):
|
| 121 |
try:
|
| 122 |
+
data = await request.json()
|
| 123 |
+
token = data.get("key", "").strip()
|
| 124 |
+
except Exception:
|
| 125 |
+
return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid raw payload structure."})
|
| 126 |
+
|
| 127 |
+
device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
if not token:
|
| 130 |
+
return JSONResponse(status_code=400, content={"status": "error", "message": "Access token cannot be empty!"})
|
| 131 |
+
|
| 132 |
+
is_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
|
| 133 |
+
|
| 134 |
+
if not is_valid:
|
| 135 |
+
return JSONResponse(
|
| 136 |
+
status_code=401,
|
| 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 |
+
# --- API TRÍCH XUẤT LOG DIAGNOSTICS ĐỂ ĐẨY LÊN TERMINAL UI TỪNG GIÂY ---
|
| 156 |
+
@app.get("/api/logs/{task_id}")
|
| 157 |
+
async def get_task_logs(task_id: str):
|
| 158 |
+
logs = GLOBAL_TERMINAL_LOGS.get(task_id, ["Connecting to master container log channel..."])
|
| 159 |
+
return {"logs": "\n".join(logs)}
|
| 160 |
|
| 161 |
+
# --- API KÍCH HOẠT QUY TRÌNH KIỂM THỬ GỌI FILE TESTER.PY CHO ADMIN ---
|
| 162 |
+
@app.post("/api/admin/run-test")
|
| 163 |
+
async def run_admin_test():
|
| 164 |
+
report = licensing_client.execute_admin_diagnostic_test()
|
| 165 |
+
return {"status": "success", "report": report}
|
| 166 |
|
| 167 |
+
# --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP HOẶC RỚT MẠNG ---
|
| 168 |
+
@app.post("/api/cancel-task")
|
| 169 |
+
async def cancel_task(license_key: str = Form(""), request: Request = None):
|
| 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 |
+
# --- CƠ CHẾ GIÁM SÁT NGẮT KẾT NỐI NGẦM (F5 / RỚT MẠNG / ĐÓNG TAB) ---
|
| 181 |
+
async def monitor_disconnect_stream(request: Request, token: str, device_id: str, slot: int):
|
| 182 |
+
while True:
|
| 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 |
+
# --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS ---
|
| 191 |
+
@app.post("/api/generate")
|
| 192 |
+
async def api_generate(
|
| 193 |
+
request: Request,
|
| 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 |
+
if not bypass_limits:
|
| 214 |
+
allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip_key)
|
| 215 |
+
if not allowed:
|
| 216 |
+
return JSONResponse(status_code=400, content={"status": "error", "msg": limit_res})
|
| 217 |
+
|
| 218 |
+
success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip_key)
|
| 219 |
+
if not success_alloc:
|
| 220 |
+
return JSONResponse(status_code=400, content={"status": "error", "msg": slot_or_err})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
+
allocated_slot = slot_or_err
|
| 223 |
+
current_pid = os.getpid()
|
| 224 |
+
licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid)
|
| 225 |
+
|
| 226 |
+
background_tasks.add_task(monitor_disconnect_stream, request, token, device_id, allocated_slot)
|
| 227 |
+
|
| 228 |
+
task_id = f"TASK-{int(time.time())}"
|
| 229 |
+
GLOBAL_TERMINAL_LOGS[task_id] = [f"🚀 System allocated Thread Slot #{allocated_slot} for PID {current_pid}"]
|
| 230 |
+
|
| 231 |
+
params = VideoParams(
|
| 232 |
+
video_subject=video_script[:30].strip(),
|
| 233 |
+
video_script=video_script,
|
| 234 |
+
video_aspect=VideoAspect.portrait,
|
| 235 |
+
video_concat_mode=VideoConcatMode.random,
|
| 236 |
+
video_clip_duration=clip_duration,
|
| 237 |
+
voice_name=selected_voice,
|
| 238 |
+
voice_rate=voice_rate,
|
| 239 |
+
subtitle_enabled=enable_subtitles,
|
| 240 |
+
bgm_type="random" if enable_bgm else "",
|
| 241 |
+
n_threads=2
|
| 242 |
+
)
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
try:
|
| 245 |
+
logger.info(f"[{task_id}] Starting Video Pipeline workflow...")
|
| 246 |
+
GLOBAL_TERMINAL_LOGS[task_id].append("⚡ Fetching assets from Pexels API and preparing TTS narrative structure...")
|
| 247 |
+
|
| 248 |
+
result = tm.start(task_id=task_id, params=params)
|
| 249 |
|
| 250 |
+
if result and "videos" in result and len(result["videos"]) > 0:
|
| 251 |
+
video_path = result["videos"][0]
|
| 252 |
+
if os.path.exists(video_path):
|
| 253 |
+
filename = os.path.basename(video_path)
|
|
|
|
|
|
|
| 254 |
|
| 255 |
+
if not is_vip_key:
|
| 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 |
+
licensing_client.commit_generation_success(token, device_id, is_vip_key)
|
| 276 |
+
GLOBAL_TERMINAL_LOGS[task_id].append("✅ Rendering success. Exporting video stream to player node.")
|
| 277 |
|
| 278 |
+
return {
|
| 279 |
+
"status": "success",
|
| 280 |
+
"task_id": task_id,
|
| 281 |
+
"video_url": f"/static_videos/{filename}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
logger.error(f"Execution Error: {str(e)}")
|
| 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 |
+
<button type="submit" id="mainSubmitBtn" class="w-full bg-indigo-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 active:scale-95 flex items-center justify-center gap-2">
|
| 428 |
+
<i class="fa-solid fa-play" id="btnIcon"></i> <span id="btnText">GENERATE PRODUCTION VIDEO</span>
|
| 429 |
+
</button>
|
| 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 |
+
<div id="loadingBox" class="loading-box rounded-2xl p-5 mb-4 text-xs md:text-sm leading-relaxed font-medium">
|
| 445 |
+
<div class="flex items-center gap-2 text-red-700 font-bold mb-2">
|
| 446 |
+
<i class="fa-solid fa-circle-notch animate-spin text-base"></i> <span>Processing Pipeline Active...</span>
|
| 447 |
+
</div>
|
| 448 |
+
Video generation process initiated. This pipeline takes a minimum of 5 minutes to fully compile assets, render frames, and synchronize data nodes. Please be patient and maintain connection. You can actively track the technical generation progress logs inside your Hugging Face Space log panel.
|
| 449 |
+
</div>
|
| 450 |
+
|
| 451 |
+
<div class="video-wrapper my-auto py-2">
|
| 452 |
+
<video id="videoPlayer" controls playsinline poster="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=400">
|
| 453 |
+
<source id="videoSource" src="" type="video/mp4">
|
| 454 |
+
</video>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
|
| 459 |
+
<footer class="border-t border-slate-200 pt-8 pb-8 text-center">
|
| 460 |
+
<div class="bg-white inline-block px-8 py-4 rounded-2xl shadow-sm border border-slate-100">
|
| 461 |
+
<p class="text-slate-400 text-[11px] font-bold tracking-widest uppercase mb-1">Copyright © 2024 Harry | Refactored © 2026 Abu Alone Project</p>
|
| 462 |
+
<p class="text-slate-300 text-[9px] font-medium leading-relaxed">All video assets and engine rendering pipelines are compiled directly via mobile system.</p>
|
| 463 |
+
</div>
|
| 464 |
+
</footer>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<script>
|
| 468 |
+
let currentTaskId = null;
|
| 469 |
+
let runningStateActive = false;
|
| 470 |
+
|
| 471 |
+
const txtInput = document.getElementById("video_script");
|
| 472 |
+
const lblCounter = document.getElementById("charCounter");
|
| 473 |
|
| 474 |
+
function enforceInputConstraints() {{
|
| 475 |
+
if(txtInput.value.length > 1500) {{
|
| 476 |
+
txtInput.value = txtInput.value.substring(0, 1500);
|
| 477 |
+
}}
|
| 478 |
+
lblCounter.innerText = txtInput.value.length + " / 1500 Chars";
|
| 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 |
+
if (!currentToken) {{
|
| 490 |
+
keyDetails.innerHTML = '<div class="text-amber-600 font-bold"><i class="fa-solid fa-triangle-exclamation mr-1"></i> Vui lòng nhập key trước khi kiểm tra!</div>';
|
| 491 |
+
return;
|
| 492 |
+
}}
|
| 493 |
|
| 494 |
+
keyDetails.innerHTML = '<div class="text-blue-600 font-bold"><i class="fa-solid fa-spinner animate-spin mr-1"></i> Đang kết nối tới máy chủ xác thực...</div>';
|
|
|
|
| 495 |
|
| 496 |
+
try {{
|
| 497 |
+
const response = await fetch("/api/validate-key", {{
|
| 498 |
+
method: "POST",
|
| 499 |
+
headers: {{ "Content-Type": "application/json" }},
|
| 500 |
+
body: JSON.stringify({{ key: currentToken }})
|
| 501 |
+
}});
|
| 502 |
+
|
| 503 |
+
const data = await response.json();
|
| 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 |
+
currentTaskId = "TASK-" + Math.floor(Date.now() / 1000);
|
|
|
|
| 574 |
|
| 575 |
+
const formObj = document.getElementById("videoForm");
|
| 576 |
+
const payloadData = new FormData(formObj);
|
| 577 |
+
payloadData.append("license_key", currentToken);
|
| 578 |
+
|
| 579 |
+
const player = document.getElementById("videoPlayer");
|
| 580 |
+
const source = document.getElementById("videoSource");
|
| 581 |
+
player.pause();
|
| 582 |
+
|
| 583 |
+
try {{
|
| 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 |
+
function revertUiLayoutToNormal() {{
|
| 604 |
+
runningStateActive = false;
|
| 605 |
+
document.getElementById("loadingBox").style.display = "none";
|
| 606 |
+
|
| 607 |
+
const targetBtn = document.getElementById("mainSubmitBtn");
|
| 608 |
+
const iconObj = document.getElementById("btnIcon");
|
| 609 |
+
const textObj = document.getElementById("btnText");
|
| 610 |
+
|
| 611 |
+
targetBtn.className = "w-full bg-indigo-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 active:scale-95 flex items-center justify-center gap-2";
|
| 612 |
+
textObj.innerText = "GENERATE PRODUCTION VIDEO";
|
| 613 |
+
iconObj.className = "fa-solid fa-play";
|
| 614 |
+
}}
|
| 615 |
+
</script>
|
| 616 |
+
</body>
|
| 617 |
+
</html>
|
| 618 |
+
"""
|
| 619 |
+
return HTMLResponse(content=html_content)
|