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

Update webui/run_app.py

Browse files
Files changed (1) hide show
  1. webui/run_app.py +44 -486
webui/run_app.py CHANGED
@@ -1,318 +1,3 @@
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
- "tx_id": key_info.get("tx_name", "N/A"),
102
- "amount": key_info.get("amount", "N/A"),
103
- "issued_date": key_info.get("tx_date", "N/A"),
104
- "expiry_date": key_info.get("expiry", "N/A"),
105
- "days_left": key_info.get("days_left", 0),
106
- "show_test_panel": key_info.get("show_test_panel", False)
107
- }
108
- }
109
- else:
110
- return {
111
- "thread_string": active_str,
112
- "key_info": { "status": "failed", "type": "free", "show_test_panel": False }
113
- }
114
- except Exception:
115
- return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } }
116
- # --- API CHUYÊN TRÁCH XỬ LÝ NÚT BẤM VALIDATE TOKEN (THÊM MỚI ĐỂ VÁ LỖI) ---
117
- @app.post("/api/validate-key")
118
- async def validate_user_key(request: Request):
119
- try:
120
- data = await request.json()
121
- token = data.get("key", "").strip()
122
- except Exception:
123
- return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid raw payload structure."})
124
-
125
- device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
126
-
127
- # Nếu để trống key, chặn ngay từ vòng gửi xe
128
- if not token:
129
- return JSONResponse(status_code=400, content={"status": "error", "message": "Access token cannot be empty!"})
130
-
131
- # Gọi licensing_client để kiểm tra tính hợp lệ thực tế
132
- is_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
133
-
134
- # NẾU KEY SAI/LỤI: Trả về trạng thái lỗi 401 Unauthorized để chặn đứng Frontend
135
- if not is_valid:
136
- return JSONResponse(
137
- status_code=401,
138
- content={
139
- "status": "error",
140
- "message": "Authentication Failed: Invalid or expired security token."
141
- }
142
- )
143
-
144
- # NẾU KEY ĐÚNG: Trả về thành công và đồng bộ dữ liệu VIP
145
- return {
146
- "status": "success",
147
- "message": "Credentials verification payload synchronized.",
148
- "tier": key_info.get("tier", "FREE").lower(),
149
- "days_left": key_info.get("days_left", 0)
150
- }
151
-
152
- # --- API TRÍCH XUẤT LOG DIAGNOSTICS ĐỂ ĐẨY LÊN TERMINAL UI TỪNG GIÂY ---
153
- @app.get("/api/logs/{task_id}")
154
- async def get_task_logs(task_id: str):
155
- logs = GLOBAL_TERMINAL_LOGS.get(task_id, ["Connecting to master container log channel..."])
156
- return {"logs": "\n".join(logs)}
157
-
158
- # --- API KÍCH HOẠT QUY TRÌNH KIỂM THỬ GỌI FILE TESTER.PY CHO ADMIN ---
159
- @app.post("/api/admin/run-test")
160
- async def run_admin_test():
161
- report = licensing_client.execute_admin_diagnostic_test()
162
- return {"status": "success", "report": report}
163
-
164
- # --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP HOẶC RỚT MẠNG ---
165
- @app.post("/api/cancel-task")
166
- async def cancel_task(license_key: str = Form(""), request: Request = None):
167
- device_id = request.client.host if request else "MOBILE_NODE_2026"
168
- token = license_key.strip()
169
- try:
170
- released = licensing_client.force_abort_user_session(token, device_id)
171
- if released:
172
- return {"status": "success", "msg": "Task rendering forcefully terminated."}
173
- return {"status": "error", "msg": "No active tasks found for this session."}
174
- except Exception as e:
175
- return {"status": "error", "msg": str(e)}
176
-
177
- # --- CƠ CHẾ GIÁM SÁT NGẮT KẾT NỐI NGẦM (F5 / RỚT MẠNG / ĐÓNG TAB) ---
178
- async def monitor_disconnect_stream(request: Request, token: str, device_id: str, slot: int):
179
- """Vòng lặp ngầm theo dõi trạng thái mạng của thiết bị di động. Nếu ngắt kết nối -> Hủy tiến trình ngay lập tức."""
180
- while True:
181
- if await request.is_disconnected():
182
- logger.warning(f"🔌 Disconnect Event Detected (F5 or Tab Closed) for Device {device_id}. Terminating stream.")
183
- licensing_client.force_abort_user_session(token, device_id)
184
- licensing_client.release_thread_slot(slot)
185
- break
186
- await time.sleep(1)
187
-
188
- # --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS ---
189
- @app.post("/api/generate")
190
- async def api_generate(
191
- request: Request,
192
- background_tasks: BackgroundTasks,
193
- video_script: str = Form(...),
194
- clip_duration: int = Form(10),
195
- voice_rate: float = Form(1.0),
196
- selected_voice: str = Form("en-US-AvaNeural"),
197
- enable_subtitles: bool = Form(True),
198
- enable_bgm: bool = Form(True),
199
- license_key: str = Form("")
200
- ):
201
- if not video_script.strip():
202
- return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"})
203
-
204
- token = license_key.strip()
205
- device_id = request.client.host if request.client else "MOBILE_NODE_2026"
206
-
207
- is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
208
- is_vip_key = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False
209
- bypass_limits = key_info.get("bypass_limits", False)
210
-
211
- if not bypass_limits:
212
- allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip_key)
213
- if not allowed:
214
- return JSONResponse(status_code=400, content={"status": "error", "msg": limit_res})
215
-
216
- success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip_key)
217
- if not success_alloc:
218
- return JSONResponse(status_code=400, content={"status": "error", "msg": slot_or_err})
219
-
220
- allocated_slot = slot_or_err
221
- current_pid = os.getpid()
222
- licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid)
223
-
224
- # Đăng ký tiến trình kiểm tra rớt mạng song song
225
- background_tasks.add_task(monitor_disconnect_stream, request, token, device_id, allocated_slot)
226
-
227
- task_id = f"TASK-{int(time.time())}"
228
- GLOBAL_TERMINAL_LOGS[task_id] = [f"🚀 System allocated Thread Slot #{allocated_slot} for PID {current_pid}"]
229
-
230
- params = VideoParams(
231
- video_subject=video_script[:30].strip(),
232
- video_script=video_script,
233
- video_aspect=VideoAspect.portrait,
234
- video_concat_mode=VideoConcatMode.random,
235
- video_clip_duration=clip_duration,
236
- voice_name=selected_voice,
237
- voice_rate=voice_rate,
238
- subtitle_enabled=enable_subtitles,
239
- bgm_type="random" if enable_bgm else "",
240
- n_threads=2
241
- )
242
-
243
- try:
244
- logger.info(f"[{task_id}] Starting Video Pipeline workflow...")
245
- GLOBAL_TERMINAL_LOGS[task_id].append("⚡ Fetching assets from Pexels API and preparing TTS narrative structure...")
246
-
247
- result = tm.start(task_id=task_id, params=params)
248
-
249
- if result and "videos" in result and len(result["videos"]) > 0:
250
- video_path = result["videos"][0]
251
- if os.path.exists(video_path):
252
- filename = os.path.basename(video_path)
253
-
254
- if not is_vip_key:
255
- GLOBAL_TERMINAL_LOGS[task_id].append("⚠️ Free tier session detected: Injecting mobile fluid watermark layer...")
256
- wm_filename = f"wm_{filename}"
257
- watermarked_path = os.path.join(OUTPUT_DIR, wm_filename)
258
-
259
- drawtext_filter = "drawtext=text='Hugging/AbuAlone09':x='mod(t*35,w)':y='mod(t*15,h)':fontsize=24:fontcolor=white@0.35"
260
- ffmpeg_cmd = [
261
- 'ffmpeg', '-y',
262
- '-i', video_path,
263
- '-vf', drawtext_filter,
264
- '-c:v', 'libx264',
265
- '-pix_fmt', 'yuv420p',
266
- '-c:a', 'copy',
267
- watermarked_path
268
- ]
269
-
270
- subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
271
- if os.path.exists(watermarked_path):
272
- filename = wm_filename
273
-
274
- licensing_client.commit_generation_success(token, device_id, is_vip_key)
275
- GLOBAL_TERMINAL_LOGS[task_id].append("✅ Rendering success. Exporting video stream to player node.")
276
-
277
- return {
278
- "status": "success",
279
- "task_id": task_id,
280
- "video_url": f"/static_videos/{filename}"
281
- }
282
-
283
- return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": "Render completed but output file not found."})
284
-
285
- except Exception as e:
286
- logger.error(f"Execution Error: {str(e)}")
287
- return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": f"Render Error: {str(e)}"})
288
- finally:
289
- licensing_client.release_thread_slot(allocated_slot)
290
- # Giữ log thêm 30 giây để giao diện AJAX kịp kéo dòng cuối cùng về trước khi xóa sạch bộ nhớ tạm
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
- .log-box {{ background: #0f172a; color: #38bdf8; font-family: 'Courier New', Courier, monospace; overflow-y: auto; height: 180px; border-radius: 1.25rem; padding: 14px; font-size: 11px; line-height: 1.5; border: 1px solid #1e293b; }}
314
- </style>
315
- </head>
316
  <body class="p-4 md:p-12 lg:p-16">
317
  <div class="max-w-7xl mx-auto">
318
 
@@ -363,26 +48,6 @@ async def index_page():
363
  </div>
364
  </div>
365
 
366
- <div class="glass-card rounded-[2.5rem] p-6 shadow-lg border border-slate-200 mb-12">
367
- <div class="text-slate-400 font-bold uppercase text-[10px] tracking-widest mb-4 text-center md:text-left">
368
- <i class="fa-solid fa-chart-line mr-1 text-indigo-500"></i> Server Workspace Allocation Limits
369
- </div>
370
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4 text-center font-medium text-sm text-slate-600">
371
- <div class="bg-slate-50/60 p-4 rounded-2xl border border-slate-100">
372
- 📋 Videos / Batch:<br><span class="text-indigo-600 font-black text-base">1 Video</span>
373
- </div>
374
- <div class="bg-slate-50/60 p-4 rounded-2xl border border-slate-100">
375
- 📅 Daily Limit:<br><span class="text-indigo-600 font-black text-base">3 Videos / Day</span>
376
- </div>
377
- <div class="bg-slate-50/60 p-4 rounded-2xl border border-slate-100">
378
- ⏱️ Cooldown:<br><span class="text-indigo-600 font-black text-base">3 Hours</span>
379
- </div>
380
- <div class="bg-indigo-50/70 p-4 rounded-2xl border border-indigo-100 font-bold text-indigo-950">
381
- ⚙️ Active Cluster Load:<br><span id="liveThreadMonitor" class="text-indigo-700 font-black text-base">0/6 Running Threads</span>
382
- </div>
383
- </div>
384
- </div>
385
-
386
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20">
387
  <div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200">
388
  <div class="flex items-center gap-4 mb-8">
@@ -451,7 +116,7 @@ async def index_page():
451
  </form>
452
  </div>
453
 
454
- <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">
455
  <div class="flex items-center gap-4 mb-6">
456
  <div class="w-12 h-12 bg-emerald-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-emerald-200">
457
  <i class="fa-solid fa-film text-xl"></i>
@@ -462,19 +127,14 @@ async def index_page():
462
  </div>
463
  </div>
464
 
465
- <div id="loadingBox" class="loading-box rounded-2xl p-5 mb-4 text-xs md:text-sm leading-relaxed font-medium">
466
- <div class="flex items-center gap-2 text-red-700 font-bold mb-1">
467
- <i class="fa-solid fa-circle-notch animate-spin text-base"></i> <span>Processing Pipeline Active...</span>
468
  </div>
469
- Compilation cluster busy. Streaming server core metrics into terminal framework below.
470
- </div>
471
-
472
- <div class="mb-4">
473
- <div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5"><i class="fa-solid fa-terminal"></i> Build Terminal Log Diagnostics</div>
474
- <div id="liveTerminalLog" class="log-box">Console idling. Initiate generation sequence to pipe live metrics...</div>
475
  </div>
476
 
477
- <div class="video-wrapper my-auto py-2">
478
  <video id="videoPlayer" controls playsinline poster="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=400">
479
  <source id="videoSource" src="" type="video/mp4">
480
  </video>
@@ -492,40 +152,6 @@ async def index_page():
492
 
493
  <script>
494
  let currentTaskId = null;
495
- let logPollingTimer = null;
496
- let runningStateActive = false;
497
-
498
- const txtInput = document.getElementById("video_script");
499
- const lblCounter = document.getElementById("charCounter");
500
-
501
- function enforceInputConstraints() {{
502
- if(txtInput.value.length > 1500) {{
503
- txtInput.value = txtInput.value.substring(0, 1500);
504
- }}
505
- lblCounter.innerText = txtInput.value.length + " / 1500 Chars";
506
- }}
507
- txtInput.addEventListener("input", enforceInputConstraints);
508
- txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }});
509
-
510
- async function runSystemStatusSync() {{
511
- const currentToken = document.getElementById("licenseKey").value;
512
- try {{
513
- const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken));
514
- const data = await response.json();
515
-
516
- document.getElementById("liveThreadMonitor").innerText = data.thread_string + " Running Threads";
517
-
518
- const keyDetails = document.getElementById("keyDetails");
519
- const adminPanel = document.getElementById("adminTestPanel");
520
-
521
- if(data.key_info.status === "success") {{
522
- if(data.key_info.type === "admin") {{
523
- keyDetails.innerHTML = '<div class="text-emerald-600"><i class="fa-solid fa-crown text-base mr-1"></i> Core System Status: ACCESS GRANTED (ADMIN MASTER CONTROL)</div>';
524
- adminPanel.classList.remove("hidden");
525
-
526
- <script>
527
- let currentTaskId = null;
528
- let logPollingTimer = null;
529
  let runningStateActive = false;
530
 
531
  const txtInput = document.getElementById("video_script");
@@ -540,91 +166,53 @@ async def index_page():
540
  txtInput.addEventListener("input", enforceInputConstraints);
541
  txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }});
542
 
543
- async function runSystemStatusSync() {{
544
- const currentToken = document.getElementById("licenseKey").value.trim();
545
- try {{
546
- const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken));
547
- const data = await response.json();
548
-
549
- document.getElementById("liveThreadMonitor").innerText = data.thread_string + " Running Threads";
550
-
551
- const keyDetails = document.getElementById("keyDetails");
552
- const adminPanel = document.getElementById("adminTestPanel");
553
-
554
- if(data.key_info.status === "success") {{
555
- if(data.key_info.type === "admin") {{
556
- keyDetails.innerHTML = '<div class="text-emerald-600"><i class="fa-solid fa-crown text-base mr-1"></i> Core System Status: ACCESS GRANTED (ADMIN MASTER CONTROL)</div>';
557
- adminPanel.classList.remove("hidden");
558
- }} else if(data.key_info.type === "vip") {{
559
- keyDetails.innerHTML = '<div class="text-indigo-600"><i class="fa-solid fa-gem text-base mr-1"></i> Core System Status: ACCESS GRANTED (VIP PRIVILEGES UNLOCKED)</div>';
560
- adminPanel.classList.add("hidden");
561
- }} else {{
562
- adminPanel.classList.add("hidden");
563
- keyDetails.innerHTML = `
564
- <div class="text-emerald-600 font-extrabold grid grid-cols-1 sm:grid-cols-2 gap-1 text-xs">
565
- <div><i class="fa-solid fa-check-circle"></i> SECURE TOKEN AUTHENTICATED</div>
566
- <div>🆔 TxID: $${{data.key_info.tx_id}}</div>
567
- <div>💰 Paid Metric: $${{data.key_info.amount}}</div>
568
- <div>📅 Registration: $${{data.key_info.issued_date}}</div>
569
- <div>⏳ Termination: $${{data.key_info.expiry_date}} ($${{data.key_info.days_left}} days remaining)</div>
570
- </div>
571
- `;
572
- }}
573
- }} else {{
574
- adminPanel.classList.add("hidden");
575
- keyDetails.innerHTML = '<div><i class="fa-solid fa-circle-exclamation mr-1"></i> License Status: Unverified (Free Tier Restrictions Active)</div>';
576
- }}
577
- }} catch(e) {{ }}
578
- }}
579
- setInterval(runSystemStatusSync, 1000);
580
-
581
- // --- HÀM XỬ LÝ CHECK SỰ KIỆN VALIDATE TOKEN KHI BẤM NÚT (SỬA LỖI NHẬP LỤI) ---
582
  async function triggerVerificationHandshake() {{
583
  const currentToken = document.getElementById("licenseKey").value.trim();
584
  const keyDetails = document.getElementById("keyDetails");
 
585
 
586
  if (!currentToken) {{
587
- alert(" Please enter a token before validation!");
588
  return;
589
  }}
590
 
 
 
591
  try {{
592
- // Gọi trực tiếp đến API POST kiểm tra key chuyên dụng ở Backend
593
- const response = await fetch("/api/validate-key", {{
594
- method: "POST",
595
- headers: {{ "Content-Type": "application/json" }},
596
- body: JSON.stringify({{ key: currentToken }})
597
- }});
598
-
599
  const data = await response.json();
600
 
601
- if (!response.ok) {{
602
- // Nếu backend trả về 401 hoặc lỗi chặn, quăng lỗi xuống block catch bên dưới
603
- throw new Error(data.message || "Invalid or expired security token.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  }}
605
-
606
- // CHỈ BẮN THÔNG BÁO KHI KEY THỰC SỰ CHUẨN
607
- alert("💚 " + data.message);
608
- await runSystemStatusSync(); // Đồng bộ lại giao diện VIP/ADMIN ngay lập tức
609
-
610
  }} catch (error) {{
611
- // NẾU KEY SAI HOẶC BẤM LỤI: Báo lỗi đỏ và khóa quyền
612
- alert(" " + error.message);
613
- keyDetails.innerHTML = '<div><i class="fa-solid fa-circle-exclamation mr-1"></i> License Status: Authentication Failed (Token Rejected)</div>';
614
  }}
615
  }}
616
 
617
- function fetchLiveTerminalOutput() {{
618
- if (!currentTaskId) return;
619
- fetch("/api/logs/" + currentTaskId)
620
- .then(res => res.json())
621
- .then(data => {{
622
- const consoleElement = document.getElementById("liveTerminalLog");
623
- consoleElement.innerText = data.logs;
624
- consoleElement.scrollTop = consoleElement.scrollHeight;
625
- }});
626
- }}
627
-
628
  async function triggerAdminTesterScript() {{
629
  const term = document.getElementById("diagnosticReportTerminal");
630
  term.classList.remove("hidden");
@@ -638,6 +226,7 @@ async def index_page():
638
  }}
639
  }}
640
 
 
641
  async function handleRenderCycle(e) {{
642
  e.preventDefault();
643
  const targetBtn = document.getElementById("mainSubmitBtn");
@@ -647,9 +236,13 @@ async def index_page():
647
 
648
  if(runningStateActive) {{
649
  if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{
 
650
  const fData = new FormData();
651
  fData.append("license_key", currentToken);
652
- await fetch("/api/cancel-task", {{ method: "POST", body: fData }});
 
 
 
653
  revertUiLayoutToNormal();
654
  }}
655
  return;
@@ -661,11 +254,7 @@ async def index_page():
661
  iconObj.className = "fa-solid fa-circle-stop";
662
 
663
  document.getElementById("loadingBox").style.display = "block";
664
-
665
  currentTaskId = "TASK-" + Math.floor(Date.now() / 1000);
666
- document.getElementById("liveTerminalLog").innerText = "Establishing secure worker container scope for " + currentTaskId + "...";
667
-
668
- logPollingTimer = setInterval(fetchLiveTerminalOutput, 1000);
669
 
670
  const formObj = document.getElementById("videoForm");
671
  const payloadData = new FormData(formObj);
@@ -685,35 +274,4 @@ async def index_page():
685
  player.load();
686
  setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
687
  alert("✅ Video processing completed successfully!");
688
- }} else {{
689
- alert("❌ " + serverResponse.msg);
690
- }}
691
- }} catch(err) {{
692
- alert("❌ Connection lost or pipeline aborted.");
693
- }} finally {{
694
- revertUiLayoutToNormal();
695
- }}
696
- }}
697
-
698
- function revertUiLayoutToNormal() {{
699
- runningStateActive = false;
700
- clearInterval(logPollingTimer);
701
- document.getElementById("loadingBox").style.display = "none";
702
-
703
- const targetBtn = document.getElementById("mainSubmitBtn");
704
- const iconObj = document.getElementById("btnIcon");
705
- const textObj = document.getElementById("btnText");
706
-
707
- 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";
708
- textObj.innerText = "GENERATE PRODUCTION VIDEO";
709
- iconObj.className = "fa-solid fa-play";
710
- }}
711
- </script>
712
- </body>
713
- </html>
714
- """
715
- return HTMLResponse(content=html_content)
716
-
717
- if __name__ == "__main__":
718
- import uvicorn
719
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <body class="p-4 md:p-12 lg:p-16">
2
  <div class="max-w-7xl mx-auto">
3
 
 
48
  </div>
49
  </div>
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20">
52
  <div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200">
53
  <div class="flex items-center gap-4 mb-8">
 
116
  </form>
117
  </div>
118
 
119
+ <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">
120
  <div class="flex items-center gap-4 mb-6">
121
  <div class="w-12 h-12 bg-emerald-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-emerald-200">
122
  <i class="fa-solid fa-film text-xl"></i>
 
127
  </div>
128
  </div>
129
 
130
+ <div id="loadingBox" class="loading-box rounded-2xl p-6 mb-6 text-sm leading-relaxed font-bold">
131
+ <div class="flex items-center gap-2 text-red-700 font-extrabold mb-2 text-base">
132
+ <i class="fa-solid fa-circle-notch animate-spin text-lg"></i> <span>Pipeline Rendering Operational...</span>
133
  </div>
134
+ Your video is being generated. This process takes a minimum of 5 minutes, please be patient. You can monitor the live rendering logs directly inside your Hugging Face space log console.
 
 
 
 
 
135
  </div>
136
 
137
+ <div class="video-wrapper my-auto py-4">
138
  <video id="videoPlayer" controls playsinline poster="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=400">
139
  <source id="videoSource" src="" type="video/mp4">
140
  </video>
 
152
 
153
  <script>
154
  let currentTaskId = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  let runningStateActive = false;
156
 
157
  const txtInput = document.getElementById("video_script");
 
166
  txtInput.addEventListener("input", enforceInputConstraints);
167
  txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }});
168
 
169
+ // HÀM NHẤN NÚT KIỂM TRA KEY: TRẢ VỀ TICK XANH HOẶC X ĐỎ KÈM BẢNG THÔNG TIN CHI TIẾT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  async function triggerVerificationHandshake() {{
171
  const currentToken = document.getElementById("licenseKey").value.trim();
172
  const keyDetails = document.getElementById("keyDetails");
173
+ const adminPanel = document.getElementById("adminTestPanel");
174
 
175
  if (!currentToken) {{
176
+ keyDetails.innerHTML = '<div class="text-amber-600 font-bold"><i class="fa-solid fa-triangle-exclamation mr-1"></i> Please enter an access token before validation!</div>';
177
  return;
178
  }}
179
 
180
+ keyDetails.innerHTML = '<div class="text-blue-600 font-bold animate-pulse"><i class="fa-solid fa-spinner animate-spin mr-1"></i> Authenticating with license server cluster...</div>';
181
+
182
  try {{
183
+ const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken));
 
 
 
 
 
 
184
  const data = await response.json();
185
 
186
+ if (data.key_info && data.key_info.status === "success") {{
187
+ if (data.key_info.type === "admin") {{
188
+ adminPanel.classList.remove("hidden");
189
+ }} else {{
190
+ adminPanel.classList.add("hidden");
191
+ }}
192
+
193
+ // HIỂN THỊ TICK XANH VÀ BẢNG THÔNG TIN CHI TIẾT CỦA KEY
194
+ keyDetails.innerHTML = `
195
+ <div class="text-emerald-600 font-extrabold grid grid-cols-1 gap-1.5 text-xs">
196
+ <div class="text-emerald-700 font-black text-sm border-b border-emerald-100 pb-1 flex items-center gap-1">
197
+ <i class="fa-solid fa-circle-check text-base"></i> [✅ KEY HỢP LỆ / VALID KEY]
198
+ </div>
199
+ <div>🆔 <strong>Mã giao dịch (TxID):</strong> ${{data.key_info.tx_id}}</div>
200
+ <div>👤 <strong>Tên người dùng (Tier):</strong> ${{data.key_info.type.toUpperCase()}}</div>
201
+ <div>💰 <strong>Số tiền (Amount Paid):</strong> ${{data.key_info.amount}}</div>
202
+ <div>📅 <strong>Ngày cấp key (Issued Date):</strong> ${{data.key_info.issued_date}}</div>
203
+ <div>⏳ <strong>Hạn sử dụng (Expiry Date):</strong> ${{data.key_info.expiry_date}} (${{data.key_info.days_left}} days left)</div>
204
+ </div>
205
+ `;
206
+ }} else {{
207
+ adminPanel.classList.add("hidden");
208
+ keyDetails.innerHTML = '<div class="text-rose-600 font-black text-sm flex items-center gap-1"><i class="fa-solid fa-circle-xmark text-base"></i> [❌ KEY KHÔNG HỢP LỆ / INVALID KEY] Key không tồn tại hoặc đã hết hạn!</div>';
209
  }}
 
 
 
 
 
210
  }} catch (error) {{
211
+ adminPanel.classList.add("hidden");
212
+ keyDetails.innerHTML = '<div class="text-rose-600 font-black text-xs"><i class="fa-solid fa-circle-xmark mr-1"></i> [❌ LỖI KẾT NỐI] Node xác thực không phản hồi.</div>';
 
213
  }}
214
  }}
215
 
 
 
 
 
 
 
 
 
 
 
 
216
  async function triggerAdminTesterScript() {{
217
  const term = document.getElementById("diagnosticReportTerminal");
218
  term.classList.remove("hidden");
 
226
  }}
227
  }}
228
 
229
+ // HÀM XỬ LÝ CLICK TẠO VIDEO & NÚT STOP THAY ĐỔI THEO FORM DATA ĐỒNG BỘ BACKEND
230
  async function handleRenderCycle(e) {{
231
  e.preventDefault();
232
  const targetBtn = document.getElementById("mainSubmitBtn");
 
236
 
237
  if(runningStateActive) {{
238
  if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{
239
+ // ĐÓNG GÓI FORM DATA ĐỂ CHẠY CHUẨN LỆNH CANCEL-TASK Ở BACKEND
240
  const fData = new FormData();
241
  fData.append("license_key", currentToken);
242
+
243
+ try {{
244
+ await fetch("/api/cancel-task", {{ method: "POST", body: fData }});
245
+ }} catch(err) {{ }}
246
  revertUiLayoutToNormal();
247
  }}
248
  return;
 
254
  iconObj.className = "fa-solid fa-circle-stop";
255
 
256
  document.getElementById("loadingBox").style.display = "block";
 
257
  currentTaskId = "TASK-" + Math.floor(Date.now() / 1000);
 
 
 
258
 
259
  const formObj = document.getElementById("videoForm");
260
  const payloadData = new FormData(formObj);
 
274
  player.load();
275
  setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
276
  alert("✅ Video processing completed successfully!");
277
+ }