AbuAlone09 commited on
Commit
5c07aeb
·
verified ·
1 Parent(s): 783846c

Update webui/run_app.py

Browse files
Files changed (1) hide show
  1. webui/run_app.py +136 -126
webui/run_app.py CHANGED
@@ -1,31 +1,27 @@
1
- # =========================================================
2
- # VERSION 3.1.0 - COMMERCIAL WORKSHOP DISPATCHER
3
- # APPLICATION ENTRYPOINT: run_app.py
4
- # FIX: LOG LIVE TERMINAL, SELECT DROPDOWNS, COUNTERS & CANCEL ENGINE
5
- # AUTHOR: Abu Alone © 2026
6
- # =========================================================
7
 
8
  import os
9
  import sys
10
- import asyncio
 
11
  import subprocess
12
  from uuid import uuid4
13
  from loguru import logger
14
- from fastapi import FastAPI, Request, Form, HTTPException
15
  from fastapi.responses import HTMLResponse, JSONResponse
16
  from fastapi.staticfiles import StaticFiles
17
  from fastapi.middleware.cors import CORSMiddleware
18
 
19
- # Giữ nguyên vẹn cấu trúc lõi của ông từ ứng dụng chính
20
  from app.config import config
21
  from app.models.schema import VideoParams, VideoAspect, VideoConcatMode
 
22
  from app.services import task as tm
23
  from app.utils import utils
24
 
25
- # Nhúng module điều phối bảo mật vừa đồng bộ trên
26
  import licensing_client
27
 
28
- app = FastAPI(title="AI Video Engine Production Hub")
29
 
30
  app.add_middleware(
31
  CORSMiddleware,
@@ -35,41 +31,42 @@ app.add_middleware(
35
  allow_headers=["*"],
36
  )
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  OUTPUT_DIR = utils.storage_dir("videos", True)
39
  if os.path.exists(OUTPUT_DIR):
40
  app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos")
41
 
42
- # Cấu trúc lưu trữ quản lý tín hiệu dừng (Stop Signal) và Log trực tiếp cho Web Terminal
43
- ACTIVE_TASKS_SIGNALS = {}
44
- TASK_LIVE_LOGS = {}
45
-
46
- def write_live_log(task_id: str, msg: str):
47
- if task_id not in TASK_LIVE_LOGS:
48
- TASK_LIVE_LOGS[task_id] = []
49
- log_line = f"[{time.strftime('%H:%M:%S')}] {msg}"
50
- TASK_LIVE_LOGS[task_id].append(log_line)
51
- logger.info(f"Task {task_id[:6]}: {msg}")
52
-
53
  def get_voice_list():
54
  path = os.path.join(os.path.dirname(__file__), "voice-list.txt")
55
  if os.path.exists(path):
56
  try:
57
  with open(path, "r", encoding="utf-8") as f:
58
- return [line.split("Name:")[-1].strip() for line in f if "Name:" in line]
59
- except Exception:
60
- pass
 
61
  return ["en-US-AvaNeural", "vi-VN-HoaiMyNeural"]
62
 
63
- # --- API ĐỒNG BỘ TRẠNG THÁI LUỒNG CHÈN KEY MỖI GIÂY ---
64
- import time
65
  @app.get("/api/system-status")
66
- async def get_system_status(key: str = "", device_id: str = "MOBILE_DEFAULT_NODE"):
 
67
  try:
68
- # 1. Gọi bộ dữ liệu luồng tổng hợp từ module nâng cao mới để lấy trạng thái thực tế
69
  thread_data = licensing_client.get_thread_status_json()
70
- active_str = thread_data["busy_channels"] # Bốc ra chuỗi định dạng dạng "0/6" hoặc "1/6"
71
 
72
- # 2. Thực hiện đối soát phân tầng bảo mật từ Secret hoặc Server công khai
73
  is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id)
74
 
75
  if is_valid:
@@ -89,129 +86,142 @@ async def get_system_status(key: str = "", device_id: str = "MOBILE_DEFAULT_NODE
89
  else:
90
  return {
91
  "thread_string": active_str,
92
- "key_info": {
93
- "status": "failed",
94
- "type": "free",
95
- "show_test_panel": False
96
- }
97
- }
98
-
99
- except Exception as sys_err:
100
- # Biên chặn an toàn chống sập Container: Nếu có lỗi phát sinh, tự động trả về trạng thái Free Tier để chạy tiếp
101
- return {
102
- "thread_string": "0/6",
103
- "key_info": {
104
- "status": "failed",
105
- "type": "free",
106
- "show_test_panel": False,
107
- "error_debug": str(sys_err)
108
  }
109
- }
110
-
111
-
112
- # --- API XUẤT TERMINAL LOGS TỰ ĐỘNG CUỘN ---
113
- @app.get("/api/logs/{task_id}")
114
- async def get_task_logs(task_id: str):
115
- logs = TASK_LIVE_LOGS.get(task_id, ["Pipeline standing by..."])
116
- return {"logs": "\n".join(logs)}
117
 
118
- # --- API NGẮT TIẾN TRÌNH TẠO VIDEO LẬP TỨC (NÚT STOP) ---
119
- @app.post("/api/stop-task/{task_id}")
120
- async def stop_task_execution(task_id: str):
121
- ACTIVE_TASKS_SIGNALS[task_id] = "STOP_SIGNALED"
122
- write_live_log(task_id, "⚠️ ABORT SIGNAL: Process interruption requested by user. Killing task pipeline...")
123
- return {"status": "terminated"}
124
-
125
- # --- API GENERATE VIDEO CHÍNH THỨC ---
 
 
 
 
 
 
 
126
  @app.post("/api/generate")
127
- async def generate_video_api(
128
  request: Request,
129
  video_script: str = Form(...),
130
  clip_duration: int = Form(10),
131
- voice_speed: str = Form("1.0"),
132
- voice_engine: str = Form("af-ZA-AdriNeural"),
133
- render_subtitles: bool = Form(True),
134
- background_music: bool = Form(True),
135
  license_key: str = Form("")
136
  ):
137
- # 1. Khởi tạo hoặc định danh Node thiết bị di động dựa trên IP để kiểm soát thiết bị Free
138
- device_id = request.client.host if request.client else "MOBILE_NODE_2026"
139
 
140
- # Chuẩn hóa chuỗi Key nhập vào (loại bỏ khoảng trắng thừa)
 
 
141
  token = license_key.strip()
142
-
143
- # 2. Thẩm định phân tầng quyền lực của Key (Admin / VIP / Commercial / Free)
144
- is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
145
-
146
- # Xác định tài khoản có phải phân lớp Premium (VIP hoặc ADMIN) hay không
147
- is_vip = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False
148
 
149
- # Kiểm tra đặc quyền bypass (bỏ qua hoàn toàn giới hạn) đối với Admin Key hoặc Promo Key từ Secret
 
150
  bypass_limits = key_info.get("bypass_limits", False)
151
-
152
- # 3. Áp dụng biên chặn hạn mức tạo video trong ngày (Nếu không có quyền đặc quyền bypass)
153
  if not bypass_limits:
154
- allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip)
155
  if not allowed:
156
- # Trả thông báo lỗi hạn mức trực tiếp lên khung tương tác UI
157
- return {"status": "failed", "msg": limit_res}
158
-
159
- # 4. Kích hoạt bộ điều phối: Cấp phát và xếp hàng vào 1 trong 6 luồng động của Server Cluster
160
- success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip)
161
  if not success_alloc:
162
- # Trường hợp cả 6 kênh render đều đạt đỉnh tải, báo bận để người dùng chờ
163
- return {"status": "failed", "msg": slot_or_err}
164
 
165
  allocated_slot = slot_or_err
166
- task_id = f"TASK-{int(time.time())}-SLOT{allocated_slot}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  try:
169
- # 5. Đăng tiến trình vào sơ đồ luồng hệ thống
170
- # Sử dụng PID của chính worker hiện tại để ghim quyền kiểm soát tài nguyên
171
- current_pid = os.getpid()
172
- licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip, current_pid)
173
 
174
- logger.info(f"🚀 [PIPELINE INITIALIZED] - Task: {task_id} assigned to Slot {allocated_slot} (VIP: {is_vip})")
175
-
176
- # =========================================================================
177
- # 🔥 KHU VỰC LÕI: GIỮ NGUYÊN 100% CẤU TRÚC LOGIC TẠO VIDEO VÀ THAM SỐ GỐC CỦA ÔNG
178
- # Tuyệt đối không có bất kỳ sự thay đổi nào vào hệ thống sinh phim phía dưới này
179
- # =========================================================================
180
-
181
- # [Đoạn code kết nối sang file main.py hoặc tiến trình chạy render video của ông]
182
- # dụ các biến đầu vào của ông đã sẵn sàng để nạp:
183
- # video_script, clip_duration, voice_speed, voice_engine, render_subtitles, background_music
184
-
185
- # Giả lập thời gian xử lý render hệ thống (Đoạn này sẽ chạy code tạo video thực tế của ông)
186
- # -------------------------------------------------------------------------
187
-
188
- # -------------------------------------------------------------------------
189
-
190
- # 6. GHI NHẬN THÀNH CÔNG: Chỉ khi video đã xuất xưởng 100%, mới tiến hành trừ lượt cắn hạn mức
191
- # Nếu tài khoản Free bị VIP đẩy tiến trình giữa chừng -> sập hàm -> không bị trừ lượt oan
192
- licensing_client.commit_generation_success(token, device_id, is_vip)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- # Trả đường dẫn file video đã dựng hoàn thiện về cho ô trình chiếu VideoPlayer trên WebUI
195
- return {
196
- "status": "success",
197
- "task_id": task_id,
198
- "video_url": "/outputs/generated_production_video.mp4" # Thay bằng biến chứa url video thực tế của ông
199
- }
200
-
201
- except Exception as pipeline_err:
202
- logger.error(f"❌ [CRITICAL PIPELINE FAILURE] - Task {task_id} crashed: {str(pipeline_err)}")
203
- return {"status": "failed", "msg": f"Pipeline internal crash: {str(pipeline_err)}"}
204
 
 
 
 
205
  finally:
206
- # 7. GIẢI PHÓNG SLOT: Bắt buộc phải giải phóng kênh dù tiến trình render thành công hay thất bại
207
- # Chống nghẽn mạch hệ thống, đưa Slot về trạng thái 'idle' chuẩn bị cho lượt tạo kế tiếp
208
  licensing_client.release_thread_slot(allocated_slot)
209
- logger.info(f"♻️ [RESOURCE RELEASED] - Slot {allocated_slot} has been reset to idle state.")
210
 
211
- # --- RENDER GIAO DIỆN PREMIUM GLASSMORPHISM TIẾNG ANH ---
212
  @app.get("/", response_class=HTMLResponse)
213
  async def index_page():
214
  voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()])
 
 
 
 
 
 
 
215
  html_content = f"""
216
  <!DOCTYPE html>
217
  <html lang="en">
 
 
 
 
 
 
 
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
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 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,
 
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()
37
+ CF_ID = os.getenv("CF_ACCOUNT_ID", "").strip()
38
+
39
+ if PEXELS_KEY:
40
+ config.app["pexels_api_keys"] = [PEXELS_KEY]
41
+ if CF_TOKEN and CF_ID:
42
+ config.app["llm_provider"] = "cloudflare"
43
+ config.app["cloudflare_account_id"] = CF_ID
44
+ config.app["cloudflare_api_key"] = CF_TOKEN
45
+ config.app["cloudflare_model_name"] = "@cf/meta/llama-3-8b-instruct"
46
+
47
  OUTPUT_DIR = utils.storage_dir("videos", True)
48
  if os.path.exists(OUTPUT_DIR):
49
  app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos")
50
 
 
 
 
 
 
 
 
 
 
 
 
51
  def get_voice_list():
52
  path = os.path.join(os.path.dirname(__file__), "voice-list.txt")
53
  if os.path.exists(path):
54
  try:
55
  with open(path, "r", encoding="utf-8") as f:
56
+ voices = [line.split("Name:")[-1].strip() for line in f if "Name:" in line]
57
+ if voices: return voices
58
+ except Exception as e:
59
+ logger.error(f"Error reading voice list: {e}")
60
  return ["en-US-AvaNeural", "vi-VN-HoaiMyNeural"]
61
 
62
+ # --- API CẬP NHẬT TRẠNG THÁI LUỒNG THỜI GIAN THỰC ĐỒNG BỘ MỖI GIÂY ---
 
63
  @app.get("/api/system-status")
64
+ 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ế dụ "2/6"
69
 
 
70
  is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id)
71
 
72
  if is_valid:
 
86
  else:
87
  return {
88
  "thread_string": active_str,
89
+ "key_info": { "status": "failed", "type": "free", "show_test_panel": False }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
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."}
104
+ return {"status": "error", "msg": "No active tasks found for this session."}
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),
115
+ selected_voice: str = Form("en-US-AvaNeural"),
116
+ enable_subtitles: bool = Form(True),
117
+ enable_bgm: bool = Form(True),
118
  license_key: str = Form("")
119
  ):
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
 
129
+ is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id)
130
+ is_vip_key = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False
131
  bypass_limits = key_info.get("bypass_limits", False)
132
+
 
133
  if not bypass_limits:
134
+ allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip_key)
135
  if not allowed:
136
+ return JSONResponse(status_code=400, content={"status": "error", "msg": limit_res})
137
+
138
+ success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip_key)
 
 
139
  if not success_alloc:
140
+ return JSONResponse(status_code=400, content={"status": "error", "msg": slot_or_err})
 
141
 
142
  allocated_slot = slot_or_err
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Ử LÝ THỰC TẾ)
148
+ # -------------------------------------------------------------------------
149
+ task_id = str(uuid4())
150
+ params = VideoParams(
151
+ video_subject=video_script[:30].strip(),
152
+ video_script=video_script,
153
+ video_aspect=VideoAspect.portrait,
154
+ video_concat_mode=VideoConcatMode.random,
155
+ video_clip_duration=clip_duration,
156
+ voice_name=selected_voice,
157
+ voice_rate=voice_rate,
158
+ subtitle_enabled=enable_subtitles,
159
+ bgm_type="random" if enable_bgm else "",
160
+ n_threads=2
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:
168
+ video_path = result["videos"][0]
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
+
177
+ drawtext_filter = "drawtext=text='Hugging/AbuAlone09':x='mod(t*35,w)':y='mod(t*15,h)':fontsize=24:fontcolor=white@0.35"
178
+ ffmpeg_cmd = [
179
+ 'ffmpeg', '-y',
180
+ '-i', video_path,
181
+ '-vf', drawtext_filter,
182
+ '-c:v', 'libx264',
183
+ '-pix_fmt', 'yuv420p',
184
+ '-c:a', 'copy',
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">