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

Update webui/run_app.py

Browse files
Files changed (1) hide show
  1. webui/run_app.py +313 -1
webui/run_app.py CHANGED
@@ -1,3 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <body class="p-4 md:p-12 lg:p-16">
2
  <div class="max-w-7xl mx-auto">
3
 
@@ -274,4 +561,29 @@
274
  player.load();
275
  setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
276
  alert("✅ Video processing completed successfully!");
277
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import shutil
5
+ import logging
6
+ import subprocess
7
+ from uuid import uuid4
8
+ from loguru import logger
9
+ from fastapi import FastAPI, Request, Form, HTTPException, BackgroundTasks
10
+ from fastapi.responses import HTMLResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+
14
+ # Import các logic cốt lõi tạo video (Giữ nguyên vẹn 100% cấu trúc của ông)
15
+ from app.config import config
16
+ from app.models.schema import VideoParams, VideoAspect, VideoConcatMode
17
+ from app.services import voice
18
+ from app.services import task as tm
19
+ from app.utils import utils
20
+
21
+ # Đồng bộ hóa chặt chẽ với module quản lý luồng bảo mật
22
+ import licensing_client
23
+
24
+ app = FastAPI(title="AI Video Engine Commercial")
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # --- BỘ NHỚ LƯU TRỮ LOG TẠM THỜI ĐỂ ĐẨY REALTIME LÊN GIAO DIỆN ---
35
+ GLOBAL_TERMINAL_LOGS = {}
36
+
37
+ class LiveTerminalLogHandler(logging.Handler):
38
+ def emit(self, record):
39
+ try:
40
+ msg = self.format(record)
41
+ for task_id in list(GLOBAL_TERMINAL_LOGS.keys()):
42
+ GLOBAL_TERMINAL_LOGS[task_id].append(msg)
43
+ except Exception:
44
+ pass
45
+
46
+ 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}")
47
+
48
+ # --- 1. SECRETS & CLOUDFLARE DEFAULTS ---
49
+ PEXELS_KEY = os.getenv("PEXELS_KEY", "").strip()
50
+ CF_TOKEN = os.getenv("CF_API_TOKEN", "").strip()
51
+ CF_ID = os.getenv("CF_ACCOUNT_ID", "").strip()
52
+
53
+ if PEXELS_KEY:
54
+ config.app["pexels_api_keys"] = [PEXELS_KEY]
55
+ if CF_TOKEN and CF_ID:
56
+ config.app["llm_provider"] = "cloudflare"
57
+ config.app["cloudflare_account_id"] = CF_ID
58
+ config.app["cloudflare_api_key"] = CF_TOKEN
59
+ config.app["cloudflare_model_name"] = "@cf/meta/llama-3-8b-instruct"
60
+
61
+ OUTPUT_DIR = utils.storage_dir("videos", True)
62
+ if os.path.exists(OUTPUT_DIR):
63
+ app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos")
64
+
65
+ def get_voice_list():
66
+ path = os.path.join(os.path.dirname(__file__), "voice-list.txt")
67
+ if os.path.exists(path):
68
+ try:
69
+ with open(path, "r", encoding="utf-8") as f:
70
+ voices = [line.split("Name:")[-1].strip() for line in f if "Name:" in line]
71
+ if voices: return voices
72
+ except Exception as e:
73
+ logger.error(f"Error reading voice list: {e}")
74
+ return ["en-US-AvaNeural", "vi-VN-HoaiMyNeural"]
75
+
76
+ # --- API CẬP NHẬT TRẠNG THÁI LUỒNG THỜI GIAN THỰC ĐỒNG BỘ MỖI GIÂY ---
77
+ @app.get("/api/system-status")
78
+ async def get_system_status(key: str = "", request: Request = None):
79
+ device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
80
+ try:
81
+ thread_data = licensing_client.get_thread_status_json()
82
+ active_str = thread_data["busy_channels"]
83
+
84
+ is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id)
85
+
86
+ if is_valid:
87
+ return {
88
+ "thread_string": active_str,
89
+ "key_info": {
90
+ "status": "success",
91
+ "type": key_info.get("tier", "FREE").lower(),
92
+ "tx_id": key_info.get("tx_name", "N/A"),
93
+ "amount": key_info.get("amount", "N/A"),
94
+ "issued_date": key_info.get("tx_date", "N/A"),
95
+ "expiry_date": key_info.get("expiry", "N/A"),
96
+ "days_left": key_info.get("days_left", 0),
97
+ "show_test_panel": key_info.get("show_test_panel", False)
98
+ }
99
+ }
100
+ else:
101
+ return {
102
+ "thread_string": active_str,
103
+ "key_info": { "status": "failed", "type": "free", "show_test_panel": False }
104
+ }
105
+ except Exception:
106
+ return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } }
107
+
108
+ # --- API CHUYÊN TRÁCH XỬ LÝ NÚT BẤM VALIDATE TOKEN ---
109
+ @app.post("/api/validate-key")
110
+ async def validate_user_key(request: Request):
111
+ try:
112
+ data = await request.json()
113
+ token = data.get("key", "").strip()
114
+ except Exception:
115
+ return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid raw payload structure."})
116
+
117
+ device_id = request.client.host if request else "MOBILE_DEFAULT_NODE"
118
+ if not token:
119
+ return JSONResponse(status_code=400, content={"status": "error", "message": "Access token cannot be empty!"})
120
+
121
+ is_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
122
+ if not is_valid:
123
+ return JSONResponse(status_code=401, content={"status": "error", "message": "Authentication Failed: Invalid or expired security token."})
124
+
125
+ return {
126
+ "status": "success",
127
+ "message": "Credentials verification payload synchronized.",
128
+ "tier": key_info.get("tier", "FREE").lower(),
129
+ "days_left": key_info.get("days_left", 0)
130
+ }
131
+
132
+ @app.get("/api/logs/{task_id}")
133
+ async def get_task_logs(task_id: str):
134
+ logs = GLOBAL_TERMINAL_LOGS.get(task_id, ["Connecting to master container log channel..."])
135
+ return {"logs": "\n".join(logs)}
136
+
137
+ @app.post("/api/admin/run-test")
138
+ async def run_admin_test():
139
+ report = licensing_client.execute_admin_diagnostic_test()
140
+ return {"status": "success", "report": report}
141
+
142
+ # --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP HOẶC RỚT MẠNG ---
143
+ @app.post("/api/cancel-task")
144
+ async def cancel_task(license_key: str = Form(""), request: Request = None):
145
+ device_id = request.client.host if request else "MOBILE_NODE_2026"
146
+ token = license_key.strip()
147
+ try:
148
+ released = licensing_client.force_abort_user_session(token, device_id)
149
+ if released:
150
+ return {"status": "success", "msg": "Task rendering forcefully terminated."}
151
+ return {"status": "error", "msg": "No active tasks found for this session."}
152
+ except Exception as e:
153
+ return {"status": "error", "msg": str(e)}
154
+
155
+ async def monitor_disconnect_stream(request: Request, token: str, device_id: str, slot: int):
156
+ while True:
157
+ if await request.is_disconnected():
158
+ logger.warning(f"🔌 Disconnect Event Detected (F5 or Tab Closed) for Device {device_id}. Terminating stream.")
159
+ licensing_client.force_abort_user_session(token, device_id)
160
+ licensing_client.release_thread_slot(slot)
161
+ break
162
+ await time.sleep(1)
163
+
164
+ # --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS ---
165
+ @app.post("/api/generate")
166
+ async def api_generate(
167
+ request: Request,
168
+ background_tasks: BackgroundTasks,
169
+ video_script: str = Form(...),
170
+ clip_duration: int = Form(10),
171
+ voice_rate: float = Form(1.0),
172
+ selected_voice: str = Form("en-US-AvaNeural"),
173
+ enable_subtitles: bool = Form(True),
174
+ enable_bgm: bool = Form(True),
175
+ license_key: str = Form("")
176
+ ):
177
+ if not video_script.strip():
178
+ return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"})
179
+
180
+ token = license_key.strip()
181
+ device_id = request.client.host if request.client else "MOBILE_NODE_2026"
182
+
183
+ is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
184
+ is_vip_key = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False
185
+ bypass_limits = key_info.get("bypass_limits", False)
186
+
187
+ if not bypass_limits:
188
+ allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip_key)
189
+ if not allowed:
190
+ return JSONResponse(status_code=400, content={"status": "error", "msg": limit_res})
191
+
192
+ success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip_key)
193
+ if not success_alloc:
194
+ return JSONResponse(status_code=400, content={"status": "error", "msg": slot_or_err})
195
+
196
+ allocated_slot = slot_or_err
197
+ current_pid = os.getpid()
198
+ licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid)
199
+
200
+ background_tasks.add_task(monitor_disconnect_stream, request, token, device_id, allocated_slot)
201
+
202
+ task_id = f"TASK-{int(time.time())}"
203
+ GLOBAL_TERMINAL_LOGS[task_id] = [f"🚀 System allocated Thread Slot #{allocated_slot} for PID {current_pid}"]
204
+
205
+ params = VideoParams(
206
+ video_subject=video_script[:30].strip(),
207
+ video_script=video_script,
208
+ video_aspect=VideoAspect.portrait,
209
+ video_concat_mode=VideoConcatMode.random,
210
+ video_clip_duration=clip_duration,
211
+ voice_name=selected_voice,
212
+ voice_rate=voice_rate,
213
+ subtitle_enabled=enable_subtitles,
214
+ bgm_type="random" if enable_bgm else "",
215
+ n_threads=2
216
+ )
217
+
218
+ try:
219
+ logger.info(f"[{task_id}] Starting Video Pipeline workflow...")
220
+ GLOBAL_TERMINAL_LOGS[task_id].append("⚡ Fetching assets from Pexels API and preparing TTS narrative structure...")
221
+
222
+ result = tm.start(task_id=task_id, params=params)
223
+
224
+ if result and "videos" in result and len(result["videos"]) > 0:
225
+ video_path = result["videos"][0]
226
+ if os.path.exists(video_path):
227
+ filename = os.path.basename(video_path)
228
+
229
+ if not is_vip_key:
230
+ GLOBAL_TERMINAL_LOGS[task_id].append("⚠️ Free tier session detected: Injecting mobile fluid watermark layer...")
231
+ wm_filename = f"wm_{filename}"
232
+ watermarked_path = os.path.join(OUTPUT_DIR, wm_filename)
233
+
234
+ drawtext_filter = "drawtext=text='Hugging/AbuAlone09':x='mod(t*35,w)':y='mod(t*15,h)':fontsize=24:fontcolor=white@0.35"
235
+ ffmpeg_cmd = [
236
+ 'ffmpeg', '-y',
237
+ '-i', video_path,
238
+ '-vf', drawtext_filter,
239
+ '-c:v', 'libx264',
240
+ '-pix_fmt', 'yuv420p',
241
+ '-c:a', 'copy',
242
+ watermarked_path
243
+ ]
244
+
245
+ subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
246
+ if os.path.exists(watermarked_path):
247
+ filename = wm_filename
248
+
249
+ licensing_client.commit_generation_success(token, device_id, is_vip_key)
250
+ GLOBAL_TERMINAL_LOGS[task_id].append("✅ Rendering success. Exporting video stream to player node.")
251
+
252
+ return {
253
+ "status": "success",
254
+ "task_id": task_id,
255
+ "video_url": f"/static_videos/{filename}"
256
+ }
257
+
258
+ return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": "Render completed but output file not found."})
259
+
260
+ except Exception as e:
261
+ logger.error(f"Execution Error: {str(e)}")
262
+ return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": f"Render Error: {str(e)}"})
263
+ finally:
264
+ licensing_client.release_thread_slot(allocated_slot)
265
+ background_tasks.add_task(lambda: [time.sleep(30), GLOBAL_TERMINAL_LOGS.pop(task_id, None)])
266
+
267
+ # --- 4. GIAO DIỆN CHÍNH ---
268
+ @app.get("/", response_class=HTMLResponse)
269
+ async def index_page():
270
+ voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()])
271
+ html_content = f"""<!DOCTYPE html>
272
+ <html lang="en">
273
+ <head>
274
+ <meta charset="UTF-8">
275
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
276
+ <title>AI VIDEO ENGINE | Control Center</title>
277
+ <script src="https://cdn.tailwindcss.com"></script>
278
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
279
+ <style>
280
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
281
+ 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; }}
282
+ .glass-card {{ background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid #ffffff; }}
283
+ .loading-box {{ display: none; background: rgba(239, 68, 68, 0.08); border: 2px dashed #ef4444; color: #991b1b; }}
284
+ .video-wrapper {{ border: none !important; background: transparent !important; box-shadow: none !important; width: 100%; max-width: 320px; margin: 0 auto; }}
285
+ video {{ width: 100%; border-radius: 2rem; display: block; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }}
286
+ </style>
287
+ </head>
288
  <body class="p-4 md:p-12 lg:p-16">
289
  <div class="max-w-7xl mx-auto">
290
 
 
561
  player.load();
562
  setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200);
563
  alert("✅ Video processing completed successfully!");
564
+ }} else {{
565
+ alert("❌ " + serverResponse.msg);
566
+ }}
567
+ }} catch(err) {{
568
+ alert("❌ Connection lost or pipeline aborted.");
569
+ }} finally {{
570
+ revertUiLayoutToNormal();
571
+ }}
572
+ }}
573
+
574
+ function revertUiLayoutToNormal() {{
575
+ runningStateActive = false;
576
+ document.getElementById("loadingBox").style.display = "none";
577
+
578
+ const targetBtn = document.getElementById("mainSubmitBtn");
579
+ const iconObj = document.getElementById("btnIcon");
580
+ const textObj = document.getElementById("btnText");
581
+
582
+ 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";
583
+ textObj.innerText = "GENERATE PRODUCTION VIDEO";
584
+ iconObj.className = "fa-solid fa-play";
585
+ }}
586
+ </script>
587
+ </body>
588
+ </html>"""
589
+ return HTMLResponse(content=html_content)