AbuAlone09 commited on
Commit
14106ee
·
verified ·
1 Parent(s): 2c074ce

Update webui/run_app.py

Browse files
Files changed (1) hide show
  1. webui/run_app.py +109 -86
webui/run_app.py CHANGED
@@ -1,8 +1,15 @@
 
 
 
 
 
 
1
 
2
  import os
3
  import sys
4
  import time
5
  import shutil
 
6
  import subprocess
7
  from uuid import uuid4
8
  from loguru import logger
@@ -31,6 +38,22 @@ app.add_middleware(
31
  allow_headers=["*"],
32
  )
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # --- 1. SECRETS & CLOUDFLARE DEFAULTS ---
35
  PEXELS_KEY = os.getenv("PEXELS_KEY", "").strip()
36
  CF_TOKEN = os.getenv("CF_API_TOKEN", "").strip()
@@ -65,7 +88,7 @@ async def get_system_status(key: str = "", request: Request = None):
65
  device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
66
  try:
67
  thread_data = licensing_client.get_thread_status_json()
68
- active_str = thread_data["busy_channels"] # Trả về chuỗi động thực tế ví dụ "2/6"
69
 
70
  is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id)
71
 
@@ -91,13 +114,24 @@ async def get_system_status(key: str = "", request: Request = None):
91
  except Exception:
92
  return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } }
93
 
94
- # --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP ---
 
 
 
 
 
 
 
 
 
 
 
 
95
  @app.post("/api/cancel-task")
96
  async def cancel_task(license_key: str = Form(""), request: Request = None):
97
  device_id = request.client.host if request else "MOBILE_NODE_2026"
98
  token = license_key.strip()
99
  try:
100
- # Tra cứu xem thiết bị hoặc key này đang giữ slot nào để giải phóng khẩn cấp
101
  released = licensing_client.force_abort_user_session(token, device_id)
102
  if released:
103
  return {"status": "success", "msg": "Task rendering forcefully terminated."}
@@ -105,10 +139,22 @@ async def cancel_task(license_key: str = Form(""), request: Request = None):
105
  except Exception as e:
106
  return {"status": "error", "msg": str(e)}
107
 
 
 
 
 
 
 
 
 
 
 
 
108
  # --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS ---
109
  @app.post("/api/generate")
110
  async def api_generate(
111
  request: Request,
 
112
  video_script: str = Form(...),
113
  clip_duration: int = Form(10),
114
  voice_rate: float = Form(1.0),
@@ -120,9 +166,6 @@ async def api_generate(
120
  if not video_script.strip():
121
  return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"})
122
 
123
- # -------------------------------------------------------------------------
124
- # LAYER 1: KIỂM TRA ĐIỀU PHỐI LUỒNG & PHÂN TẦNG KEY (CHẠY NGOÀI LOGIC TẠO VIDEO)
125
- # -------------------------------------------------------------------------
126
  token = license_key.strip()
127
  device_id = request.client.host if request.client else "MOBILE_NODE_2026"
128
 
@@ -143,10 +186,12 @@ async def api_generate(
143
  current_pid = os.getpid()
144
  licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid)
145
 
146
- # -------------------------------------------------------------------------
147
- # LAYER 2: TIẾN TRÌNH TẠO VIDEO GỐC NGUYÊN BẢN 100% CỦA ÔNG (XỬ THỰC TẾ)
148
- # -------------------------------------------------------------------------
149
- task_id = str(uuid4())
 
 
150
  params = VideoParams(
151
  video_subject=video_script[:30].strip(),
152
  video_script=video_script,
@@ -161,7 +206,9 @@ async def api_generate(
161
  )
162
 
163
  try:
164
- logger.info(f"Starting Video Pipeline for task {task_id}...")
 
 
165
  result = tm.start(task_id=task_id, params=params)
166
 
167
  if result and "videos" in result and len(result["videos"]) > 0:
@@ -169,8 +216,8 @@ async def api_generate(
169
  if os.path.exists(video_path):
170
  filename = os.path.basename(video_path)
171
 
172
- # CHÈN WATERMARK ĐỘNG NẾU KHÔNG CÓ KEY HỢP LỆ (FREE TIER TRỪ VIP/ADMIN)
173
  if not is_vip_key:
 
174
  wm_filename = f"wm_{filename}"
175
  watermarked_path = os.path.join(OUTPUT_DIR, wm_filename)
176
 
@@ -185,50 +232,40 @@ async def api_generate(
185
  watermarked_path
186
  ]
187
 
188
- logger.info(f"Executing Mobile-Compatible FFmpeg Pipeline...")
189
  subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
190
-
191
  if os.path.exists(watermarked_path):
192
  filename = wm_filename
193
- logger.info(f"Watermark appended successfully: {filename}")
194
- else:
195
- logger.warning("Watermark engine execution error, fallback to baseline stream.")
196
-
197
- # Ghi nhận cắn trừ lượt tạo thành công
198
  licensing_client.commit_generation_success(token, device_id, is_vip_key)
199
-
 
200
  return {
201
  "status": "success",
202
- "msg": "Video generated successfully!",
203
  "video_url": f"/static_videos/{filename}"
204
  }
205
 
206
- return JSONResponse(status_code=500, content={"status": "error", "msg": "Render completed but output file not found."})
207
 
208
  except Exception as e:
209
  logger.error(f"Execution Error: {str(e)}")
210
- return JSONResponse(status_code=500, content={"status": "error", "msg": f"Render Error: {str(e)}"})
211
  finally:
212
  licensing_client.release_thread_slot(allocated_slot)
 
 
213
 
214
- # --- 4. GIAO DIỆN CHÍNH (ĐÃ THAY ĐỔI TOÀN BỘ SANG TIẾNG ANH THEO DÀN Ý CHI TIẾT) ---
215
  @app.get("/", response_class=HTMLResponse)
216
  async def index_page():
217
  voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()])
218
-
219
- # Sinh khay lựa chọn động cho Clip Duration từ 5s đến 10s (Mặc định 10s)
220
- duration_select = "".join([f'<option value="{i}" {"selected" if i==10 else ""}>{i} Seconds</option>' for i in range(5, 11)])
221
-
222
- # Sinh khay lựa chọn động cho Voice Speed từ 0.5 đến 2.0 bước nhảy 0.1 (Mặc định 1.0)
223
- speeds = [round(0.5 + x * 0.1, 1) for x in range(16)]
224
- speed_select = "".join([f'<option value="{s}" {"selected" if s==1.0 else ""}>{s}x Speed</option>' for s in speeds])
225
  html_content = f"""
226
  <!DOCTYPE html>
227
  <html lang="en">
228
  <head>
229
  <meta charset="UTF-8">
230
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
231
- <title>AI VIDEO ENGINE | Production Control Center</title>
232
  <script src="https://cdn.tailwindcss.com"></script>
233
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
234
  <style>
@@ -237,8 +274,8 @@ async def index_page():
237
  .glass-card {{ background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid #ffffff; }}
238
  .loading-box {{ display: none; background: rgba(220, 38, 38, 0.05); border: 2px dashed #dc2626; color: #991b1b; }}
239
  .video-wrapper {{ border: none !important; background: transparent !important; box-shadow: none !important; width: 100%; max-width: 320px; margin: 0 auto; }}
240
- video {{ width: 100%; border-radius: 2rem; display: block; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); }}
241
- .log-box {{ background: #0f172a; color: #38bdf8; font-family: 'Courier New', Courier, monospace; overflow-y: auto; height: 160px; border-radius: 1.25rem; padding: 14px; font-size: 11px; line-height: 1.5; border: 1px solid #1e293b; }}
242
  </style>
243
  </head>
244
  <body class="p-4 md:p-12 lg:p-16">
@@ -286,7 +323,8 @@ async def index_page():
286
 
287
  <div id="adminTestPanel" class="hidden mt-4 p-4 bg-slate-900 text-slate-100 rounded-2xl border border-slate-700">
288
  <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>
289
- <button onclick="alert('Diagnostic pipeline integrity check: OK')" class="bg-emerald-600 px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider hover:bg-emerald-500 transition-all">Run Integrity Test</button>
 
290
  </div>
291
  </div>
292
 
@@ -311,7 +349,6 @@ async def index_page():
311
  </div>
312
 
313
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20">
314
-
315
  <div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200">
316
  <div class="flex items-center gap-4 mb-8">
317
  <div class="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
@@ -348,20 +385,8 @@ async def index_page():
348
  <label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Voice Speed Selection</label>
349
  <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">
350
  <option value="0.5">0.5x</option>
351
- <option value="0.6">0.6x</option>
352
- <option value="0.7">0.7x</option>
353
- <option value="0.8">0.8x</option>
354
- <option value="0.9">0.9x</option>
355
  <option value="1.0" selected>1.0x (Normal)</option>
356
- <option value="1.1">1.1x</option>
357
- <option value="1.2">1.2x</option>
358
- <option value="1.3">1.3x</option>
359
- <option value="1.4">1.4x</option>
360
  <option value="1.5">1.5x</option>
361
- <option value="1.6">1.6x</option>
362
- <option value="1.7">1.7x</option>
363
- <option value="1.8">1.8x</option>
364
- <option value="1.9">1.9x</option>
365
  <option value="2.0">2.0x</option>
366
  </select>
367
  </div>
@@ -406,7 +431,7 @@ async def index_page():
406
  <div class="flex items-center gap-2 text-red-700 font-bold mb-1">
407
  <i class="fa-solid fa-circle-notch animate-spin text-base"></i> <span>Processing Pipeline Active...</span>
408
  </div>
409
- Compilation cluster busy. You may follow live diagnostic readouts below or terminate the execution thread.
410
  </div>
411
 
412
  <div class="mb-4">
@@ -420,16 +445,14 @@ async def index_page():
420
  </video>
421
  </div>
422
  </div>
423
-
424
  </div>
425
 
426
  <footer class="border-t border-slate-200 pt-8 pb-8 text-center">
427
  <div class="bg-white inline-block px-8 py-4 rounded-2xl shadow-sm border border-slate-100">
428
  <p class="text-slate-400 text-[11px] font-bold tracking-widest uppercase mb-1">Copyright © 2024 Harry | Refactored © 2026 Abu Alone Project</p>
429
- <p class="text-slate-300 text-[9px] font-medium leading-relaxed">All video assets and engine rendering pipelines are fully responsive and compiled directly via mobile system.</p>
430
  </div>
431
  </footer>
432
-
433
  </div>
434
 
435
  <script>
@@ -437,7 +460,6 @@ async def index_page():
437
  let logPollingTimer = null;
438
  let runningStateActive = false;
439
 
440
- // BỘ ĐẾM VÀ GIỚI HẠN CHẶN PASTE QUÁ 1500 KÝ TỰ CỦA ÔNG
441
  const txtInput = document.getElementById("video_script");
442
  const lblCounter = document.getElementById("charCounter");
443
 
@@ -448,18 +470,14 @@ async def index_page():
448
  lblCounter.innerText = txtInput.value.length + " / 1500 Chars";
449
  }}
450
  txtInput.addEventListener("input", enforceInputConstraints);
451
- txtInput.addEventListener("paste", () => {{
452
- setTimeout(enforceInputConstraints, 10);
453
- }});
454
 
455
- // BỘ ĐỒNG BỘ REALTIME THEO DÕI HỆ THỐNG MỖI GIÂY
456
  async function runSystemStatusSync() {{
457
  const currentToken = document.getElementById("licenseKey").value;
458
  try {{
459
  const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken));
460
  const data = await response.json();
461
 
462
- // Đẩy tải luồng thực nhảy số liên tục lên màn hình
463
  document.getElementById("liveThreadMonitor").innerText = data.thread_string + " Running Threads";
464
 
465
  const keyDetails = document.getElementById("keyDetails");
@@ -472,15 +490,15 @@ async def index_page():
472
  }} else if(data.key_info.type === "vip") {{
473
  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>';
474
  adminPanel.classList.add("hidden");
475
- }} else if(data.key_info.type === "commercial") {{
476
  adminPanel.classList.add("hidden");
477
  keyDetails.innerHTML = `
478
  <div class="text-emerald-600 font-extrabold grid grid-cols-1 sm:grid-cols-2 gap-1 text-xs">
479
  <div><i class="fa-solid fa-check-circle"></i> SECURE TOKEN AUTHENTICATED</div>
480
- <div>🆔 TxID: ${{data.key_info.tx_id}}</div>
481
- <div>💰 Paid Metric: ${{data.key_info.amount}}</div>
482
- <div>📅 Registration: ${{data.key_info.issued_date}}</div>
483
- <div>⏳ Termination: ${{data.key_info.expiry_date}} (${{data.key_info.days_left}} days remaining)</div>
484
  </div>
485
  `;
486
  }}
@@ -490,9 +508,8 @@ async def index_page():
490
  }}
491
  }} catch(e) {{ }}
492
  }}
493
- setInterval(runSystemStatusSync, 1000); // Đếm nhịp nhảy mỗi giây
494
 
495
- // TỰ ĐỘNG CUỘN LOG XUỐNG DÒNG CUỐI CÙNG
496
  function fetchLiveTerminalOutput() {{
497
  if (!currentTaskId) return;
498
  fetch("/api/logs/" + currentTaskId)
@@ -500,62 +517,69 @@ async def index_page():
500
  .then(data => {{
501
  const consoleElement = document.getElementById("liveTerminalLog");
502
  consoleElement.innerText = data.logs;
503
- consoleElement.scrollTop = consoleElement.scrollHeight; // Ép thanh cuộn chạm đáy
504
  }});
505
  }}
506
 
507
- // ĐIỀU PHỐI NÚT CHẠY BIẾN ĐỔI MÀU / STOP INTERACTION
 
 
 
 
 
 
 
 
 
 
 
 
508
  async function handleRenderCycle(e) {{
509
  e.preventDefault();
510
  const targetBtn = document.getElementById("mainSubmitBtn");
511
  const iconObj = document.getElementById("btnIcon");
512
  const textObj = document.getElementById("btnText");
 
513
 
514
- // NẾU ĐANG CHẠY MÀ BẤM THÌ SẼ ĐÓNG VAI TRÒ LÀ NÚT STOP HỦY LỆNH KHẨN CẤP
515
  if(runningStateActive) {{
516
  if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{
517
- if(currentTaskId) {{
518
- await fetch("/api/stop-task/" + currentTaskId, {{method: "POST"}});
519
- revertUiLayoutToNormal();
520
- }}
521
  }}
522
  return;
523
  }}
524
 
525
- // CHUYỂN SANG TRẠNG THÁI CHẠY TIẾN TRÌNH (Chuyển nút sang màu đỏ)
526
  runningStateActive = true;
527
  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";
528
  textObj.innerText = "STOP & ABORT RENDERING";
529
  iconObj.className = "fa-solid fa-circle-stop";
530
 
531
  document.getElementById("loadingBox").style.display = "block";
532
- document.getElementById("liveTerminalLog").innerText = "Establishing secure worker container scope...";
 
 
 
 
533
 
534
  const formObj = document.getElementById("videoForm");
535
  const payloadData = new FormData(formObj);
536
- payloadData.append("license_key", document.getElementById("licenseKey").value);
537
 
538
  const player = document.getElementById("videoPlayer");
539
  const source = document.getElementById("videoSource");
540
  player.pause();
541
 
542
- currentTaskId = "TASK-" + Date.now();
543
- logPollingTimer = setInterval(fetchLiveTerminalOutput, 1200);
544
-
545
  try {{
546
  const response = await fetch("/api/generate", {{ method: "POST", body: payloadData }});
547
  const serverResponse = await response.json();
548
 
549
- if(serverResponse.task_id) currentTaskId = serverResponse.task_id;
550
- fetchLiveTerminalOutput();
551
-
552
  if(serverResponse.status === "success") {{
553
  source.src = serverResponse.video_url;
554
  player.src = serverResponse.video_url;
555
  player.load();
556
- setTimeout(() => {{
557
- player.play().catch(err => console.log("Autoplay interaction completed."));
558
- }}, 200);
559
  alert("✅ Video processing completed successfully!");
560
  }} else {{
561
  alert("❌ " + serverResponse.msg);
@@ -576,7 +600,6 @@ async def index_page():
576
  const iconObj = document.getElementById("btnIcon");
577
  const textObj = document.getElementById("btnText");
578
 
579
- // Trả nút về màu xanh Indigo nguyên bản ban đầu của ông
580
  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";
581
  textObj.innerText = "GENERATE PRODUCTION VIDEO";
582
  iconObj.className = "fa-solid fa-play";
@@ -594,4 +617,4 @@ async def index_page():
594
 
595
  if __name__ == "__main__":
596
  import uvicorn
597
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
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
 
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()
 
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
 
 
114
  except Exception:
115
  return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } }
116
 
117
+ # --- API TRÍCH XUẤT LOG DIAGNOSTICS ĐỂ ĐẨY LÊN TERMINAL UI TỪNG GIÂY ---
118
+ @app.get("/api/logs/{task_id}")
119
+ async def get_task_logs(task_id: str):
120
+ logs = GLOBAL_TERMINAL_LOGS.get(task_id, ["Connecting to master container log channel..."])
121
+ return {"logs": "\n".join(logs)}
122
+
123
+ # --- API KÍCH HOẠT QUY TRÌNH KIỂM THỬ GỌI FILE TESTER.PY CHO ADMIN ---
124
+ @app.post("/api/admin/run-test")
125
+ async def run_admin_test():
126
+ report = licensing_client.execute_admin_diagnostic_test()
127
+ return {"status": "success", "report": report}
128
+
129
+ # --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP HOẶC RỚT MẠNG ---
130
  @app.post("/api/cancel-task")
131
  async def cancel_task(license_key: str = Form(""), request: Request = None):
132
  device_id = request.client.host if request else "MOBILE_NODE_2026"
133
  token = license_key.strip()
134
  try:
 
135
  released = licensing_client.force_abort_user_session(token, device_id)
136
  if released:
137
  return {"status": "success", "msg": "Task rendering forcefully terminated."}
 
139
  except Exception as e:
140
  return {"status": "error", "msg": str(e)}
141
 
142
+ # --- CƠ CHẾ GIÁM SÁT NGẮT KẾT NỐI NGẦM (F5 / RỚT MẠNG / ĐÓNG TAB) ---
143
+ async def monitor_disconnect_stream(request: Request, token: str, device_id: str, slot: int):
144
+ """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."""
145
+ while True:
146
+ if await request.is_disconnected():
147
+ logger.warning(f"🔌 Disconnect Event Detected (F5 or Tab Closed) for Device {device_id}. Terminating stream.")
148
+ licensing_client.force_abort_user_session(token, device_id)
149
+ licensing_client.release_thread_slot(slot)
150
+ break
151
+ await time.sleep(1)
152
+
153
  # --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS ---
154
  @app.post("/api/generate")
155
  async def api_generate(
156
  request: Request,
157
+ background_tasks: BackgroundTasks,
158
  video_script: str = Form(...),
159
  clip_duration: int = Form(10),
160
  voice_rate: float = Form(1.0),
 
166
  if not video_script.strip():
167
  return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"})
168
 
 
 
 
169
  token = license_key.strip()
170
  device_id = request.client.host if request.client else "MOBILE_NODE_2026"
171
 
 
186
  current_pid = os.getpid()
187
  licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid)
188
 
189
+ # Đăng ký tiến trình kiểm tra rớt mạng song song
190
+ background_tasks.add_task(monitor_disconnect_stream, request, token, device_id, allocated_slot)
191
+
192
+ task_id = f"TASK-{int(time.time())}"
193
+ GLOBAL_TERMINAL_LOGS[task_id] = [f"🚀 System allocated Thread Slot #{allocated_slot} for PID {current_pid}"]
194
+
195
  params = VideoParams(
196
  video_subject=video_script[:30].strip(),
197
  video_script=video_script,
 
206
  )
207
 
208
  try:
209
+ logger.info(f"[{task_id}] Starting Video Pipeline workflow...")
210
+ GLOBAL_TERMINAL_LOGS[task_id].append("⚡ Fetching assets from Pexels API and preparing TTS narrative structure...")
211
+
212
  result = tm.start(task_id=task_id, params=params)
213
 
214
  if result and "videos" in result and len(result["videos"]) > 0:
 
216
  if os.path.exists(video_path):
217
  filename = os.path.basename(video_path)
218
 
 
219
  if not is_vip_key:
220
+ GLOBAL_TERMINAL_LOGS[task_id].append("⚠️ Free tier session detected: Injecting mobile fluid watermark layer...")
221
  wm_filename = f"wm_{filename}"
222
  watermarked_path = os.path.join(OUTPUT_DIR, wm_filename)
223
 
 
232
  watermarked_path
233
  ]
234
 
 
235
  subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
236
  if os.path.exists(watermarked_path):
237
  filename = wm_filename
238
+
 
 
 
 
239
  licensing_client.commit_generation_success(token, device_id, is_vip_key)
240
+ GLOBAL_TERMINAL_LOGS[task_id].append("✅ Rendering success. Exporting video stream to player node.")
241
+
242
  return {
243
  "status": "success",
244
+ "task_id": task_id,
245
  "video_url": f"/static_videos/{filename}"
246
  }
247
 
248
+ return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": "Render completed but output file not found."})
249
 
250
  except Exception as e:
251
  logger.error(f"Execution Error: {str(e)}")
252
+ return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": f"Render Error: {str(e)}"})
253
  finally:
254
  licensing_client.release_thread_slot(allocated_slot)
255
+ # 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
256
+ background_tasks.add_task(lambda: [time.sleep(30), GLOBAL_TERMINAL_LOGS.pop(task_id, None)])
257
 
258
+ # --- 4. GIAO DIỆN CHÍNH ---
259
  @app.get("/", response_class=HTMLResponse)
260
  async def index_page():
261
  voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()])
 
 
 
 
 
 
 
262
  html_content = f"""
263
  <!DOCTYPE html>
264
  <html lang="en">
265
  <head>
266
  <meta charset="UTF-8">
267
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
268
+ <title>AI VIDEO ENGINE | Control Center</title>
269
  <script src="https://cdn.tailwindcss.com"></script>
270
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
271
  <style>
 
274
  .glass-card {{ background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid #ffffff; }}
275
  .loading-box {{ display: none; background: rgba(220, 38, 38, 0.05); border: 2px dashed #dc2626; color: #991b1b; }}
276
  .video-wrapper {{ border: none !important; background: transparent !important; box-shadow: none !important; width: 100%; max-width: 320px; margin: 0 auto; }}
277
+ video {{ width: 100%; border-radius: 2rem; display: block; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }}
278
+ .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; }}
279
  </style>
280
  </head>
281
  <body class="p-4 md:p-12 lg:p-16">
 
323
 
324
  <div id="adminTestPanel" class="hidden mt-4 p-4 bg-slate-900 text-slate-100 rounded-2xl border border-slate-700">
325
  <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>
326
+ <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>
327
+ <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>
328
  </div>
329
  </div>
330
 
 
349
  </div>
350
 
351
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20">
 
352
  <div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200">
353
  <div class="flex items-center gap-4 mb-8">
354
  <div class="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
 
385
  <label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Voice Speed Selection</label>
386
  <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">
387
  <option value="0.5">0.5x</option>
 
 
 
 
388
  <option value="1.0" selected>1.0x (Normal)</option>
 
 
 
 
389
  <option value="1.5">1.5x</option>
 
 
 
 
390
  <option value="2.0">2.0x</option>
391
  </select>
392
  </div>
 
431
  <div class="flex items-center gap-2 text-red-700 font-bold mb-1">
432
  <i class="fa-solid fa-circle-notch animate-spin text-base"></i> <span>Processing Pipeline Active...</span>
433
  </div>
434
+ Compilation cluster busy. Streaming server core metrics into terminal framework below.
435
  </div>
436
 
437
  <div class="mb-4">
 
445
  </video>
446
  </div>
447
  </div>
 
448
  </div>
449
 
450
  <footer class="border-t border-slate-200 pt-8 pb-8 text-center">
451
  <div class="bg-white inline-block px-8 py-4 rounded-2xl shadow-sm border border-slate-100">
452
  <p class="text-slate-400 text-[11px] font-bold tracking-widest uppercase mb-1">Copyright © 2024 Harry | Refactored © 2026 Abu Alone Project</p>
453
+ <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>
454
  </div>
455
  </footer>
 
456
  </div>
457
 
458
  <script>
 
460
  let logPollingTimer = null;
461
  let runningStateActive = false;
462
 
 
463
  const txtInput = document.getElementById("video_script");
464
  const lblCounter = document.getElementById("charCounter");
465
 
 
470
  lblCounter.innerText = txtInput.value.length + " / 1500 Chars";
471
  }}
472
  txtInput.addEventListener("input", enforceInputConstraints);
473
+ txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }});
 
 
474
 
 
475
  async function runSystemStatusSync() {{
476
  const currentToken = document.getElementById("licenseKey").value;
477
  try {{
478
  const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken));
479
  const data = await response.json();
480
 
 
481
  document.getElementById("liveThreadMonitor").innerText = data.thread_string + " Running Threads";
482
 
483
  const keyDetails = document.getElementById("keyDetails");
 
490
  }} else if(data.key_info.type === "vip") {{
491
  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>';
492
  adminPanel.classList.add("hidden");
493
+ }} else {{
494
  adminPanel.classList.add("hidden");
495
  keyDetails.innerHTML = `
496
  <div class="text-emerald-600 font-extrabold grid grid-cols-1 sm:grid-cols-2 gap-1 text-xs">
497
  <div><i class="fa-solid fa-check-circle"></i> SECURE TOKEN AUTHENTICATED</div>
498
+ <div>🆔 TxID: \${data.key_info.tx_id}</div>
499
+ <div>💰 Paid Metric: \${data.key_info.amount}</div>
500
+ <div>📅 Registration: \${data.key_info.issued_date}</div>
501
+ <div>⏳ Termination: \${data.key_info.expiry_date} (\${data.key_info.days_left} days remaining)</div>
502
  </div>
503
  `;
504
  }}
 
508
  }}
509
  }} catch(e) {{ }}
510
  }}
511
+ setInterval(runSystemStatusSync, 1000);
512
 
 
513
  function fetchLiveTerminalOutput() {{
514
  if (!currentTaskId) return;
515
  fetch("/api/logs/" + currentTaskId)
 
517
  .then(data => {{
518
  const consoleElement = document.getElementById("liveTerminalLog");
519
  consoleElement.innerText = data.logs;
520
+ consoleElement.scrollTop = consoleElement.scrollHeight;
521
  }});
522
  }}
523
 
524
+ async function triggerAdminTesterScript() {{
525
+ const term = document.getElementById("diagnosticReportTerminal");
526
+ term.classList.remove("hidden");
527
+ term.innerText = "Executing tester.py pipeline, please hold...";
528
+ try {{
529
+ const res = await fetch("/api/admin/run-test", {{ method: "POST" }});
530
+ const data = await res.json();
531
+ term.innerText = data.report;
532
+ }} catch(e) {{
533
+ term.innerText = "❌ Diagnostic communication pipeline broken: " + e;
534
+ }}
535
+ }}
536
+
537
  async function handleRenderCycle(e) {{
538
  e.preventDefault();
539
  const targetBtn = document.getElementById("mainSubmitBtn");
540
  const iconObj = document.getElementById("btnIcon");
541
  const textObj = document.getElementById("btnText");
542
+ const currentToken = document.getElementById("licenseKey").value;
543
 
 
544
  if(runningStateActive) {{
545
  if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{
546
+ const fData = new FormData();
547
+ fData.append("license_key", currentToken);
548
+ await fetch("/api/cancel-task", {{ method: "POST", body: fData }});
549
+ revertUiLayoutToNormal();
550
  }}
551
  return;
552
  }}
553
 
 
554
  runningStateActive = true;
555
  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";
556
  textObj.innerText = "STOP & ABORT RENDERING";
557
  iconObj.className = "fa-solid fa-circle-stop";
558
 
559
  document.getElementById("loadingBox").style.display = "block";
560
+
561
+ currentTaskId = "TASK-" + Math.floor(Date.now() / 1000);
562
+ document.getElementById("liveTerminalLog").innerText = "Establishing secure worker container scope for " + currentTaskId + "...";
563
+
564
+ logPollingTimer = setInterval(fetchLiveTerminalOutput, 1000);
565
 
566
  const formObj = document.getElementById("videoForm");
567
  const payloadData = new FormData(formObj);
568
+ payloadData.append("license_key", currentToken);
569
 
570
  const player = document.getElementById("videoPlayer");
571
  const source = document.getElementById("videoSource");
572
  player.pause();
573
 
 
 
 
574
  try {{
575
  const response = await fetch("/api/generate", {{ method: "POST", body: payloadData }});
576
  const serverResponse = await response.json();
577
 
 
 
 
578
  if(serverResponse.status === "success") {{
579
  source.src = serverResponse.video_url;
580
  player.src = serverResponse.video_url;
581
  player.load();
582
+ setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
 
 
583
  alert("✅ Video processing completed successfully!");
584
  }} else {{
585
  alert("❌ " + serverResponse.msg);
 
600
  const iconObj = document.getElementById("btnIcon");
601
  const textObj = document.getElementById("btnText");
602
 
 
603
  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";
604
  textObj.innerText = "GENERATE PRODUCTION VIDEO";
605
  iconObj.className = "fa-solid fa-play";
 
617
 
618
  if __name__ == "__main__":
619
  import uvicorn
620
+ uvicorn.run(app, host="0.0.0.0", port=7860)