AbuAlone09 commited on
Commit
d380586
·
verified ·
1 Parent(s): 8bd3161

Update licensing_client.py

Browse files
Files changed (1) hide show
  1. licensing_client.py +569 -371
licensing_client.py CHANGED
@@ -1,421 +1,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ữ 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 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)}"
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)