Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| import time | |
| import shutil | |
| import logging | |
| import subprocess | |
| from uuid import uuid4 | |
| from loguru import logger | |
| from fastapi import FastAPI, Request, Form, HTTPException, BackgroundTasks | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from app.config import config | |
| from app.models.schema import VideoParams, VideoAspect, VideoConcatMode | |
| from app.services import voice | |
| from app.services import task as tm | |
| from app.utils import utils | |
| import licensing_client | |
| app = FastAPI(title="AI Video Engine Commercial") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # --- BỘ NHỚ LƯU TRỮ LOG TẠM THỜI ĐC TỐI ƯU HÓA PHÂN LUỒNG --- | |
| GLOBAL_TERMINAL_LOGS = {} | |
| def secure_terminal_log_dispatcher(msg): | |
| """ | |
| Bộ điều phối log thông minh: Phân tích cú pháp dòng log, | |
| chỉ đẩy log của TASK nào vào đúng hộp lưu trữ của TASK đó. | |
| """ | |
| log_text = msg.strip() | |
| # Tìm kiếm xem dòng log có chứa nhãn định danh [TASK-xxxx] hay không | |
| for task_id in list(GLOBAL_TERMINAL_LOGS.keys()): | |
| if task_id in log_text: | |
| GLOBAL_TERMINAL_LOGS[task_id].append(log_text) | |
| # Hoặc nếu là log khởi tạo luồng chung của chính hệ thống cấp slot | |
| elif "allocated Thread Slot" in log_text and task_id in log_text: | |
| GLOBAL_TERMINAL_LOGS[task_id].append(log_text) | |
| # Đăng ký bộ cấu hình log chuẩn hóa của Loguru | |
| logger.remove() # Loại bỏ handler mặc định để tránh in trùng lặp log | |
| logger.add(sys.stderr, format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level}</level> | {message}") | |
| logger.add(secure_terminal_log_dispatcher, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") | |
| # --- 1. SECRETS & CLOUDFLARE DEFAULTS --- | |
| PEXELS_KEY = os.getenv("PEXELS_KEY", "").strip() | |
| CF_TOKEN = os.getenv("CF_API_TOKEN", "").strip() | |
| CF_ID = os.getenv("CF_ACCOUNT_ID", "").strip() | |
| if PEXELS_KEY: | |
| config.app["pexels_api_keys"] = [PEXELS_KEY] | |
| if CF_TOKEN and CF_ID: | |
| config.app["llm_provider"] = "cloudflare" | |
| config.app["cloudflare_account_id"] = CF_ID | |
| config.app["cloudflare_api_key"] = CF_TOKEN | |
| config.app["cloudflare_model_name"] = "@cf/meta/llama-3-8b-instruct" | |
| OUTPUT_DIR = utils.storage_dir("videos", True) | |
| if os.path.exists(OUTPUT_DIR): | |
| app.mount("/static_videos", StaticFiles(directory=OUTPUT_DIR), name="static_videos") | |
| def get_voice_list(): | |
| path = os.path.join(os.path.dirname(__file__), "voice-list.txt") | |
| if os.path.exists(path): | |
| try: | |
| with open(path, "r", encoding="utf-8") as f: | |
| voices = [line.split("Name:")[-1].strip() for line in f if "Name:" in line] | |
| if voices: return voices | |
| except Exception as e: | |
| logger.error(f"Error reading voice list: {e}") | |
| return ["en-US-AvaNeural", "vi-VN-HoaiMyNeural"] | |
| # --- API CẬP NHẬT TRẠNG THÁI LUỒNG THỜI GIAN THỰC ĐỒNG BỘ MỖI GIÂY --- | |
| async def get_system_status(key: str = "", request: Request = None): | |
| device_id = request.client.host if request else "MOBILE_DEFAULT_NODE" | |
| try: | |
| thread_data = licensing_client.get_thread_status_json() | |
| active_str = thread_data["busy_channels"] | |
| is_valid, key_info = licensing_client.verify_and_get_license_info(key.strip(), device_id) | |
| if is_valid: | |
| return { | |
| "thread_string": active_str, | |
| "key_info": { | |
| "status": "success", | |
| "type": key_info.get("tier", "FREE").lower(), | |
| "tx_id": key_info.get("tx_name", "N/A"), | |
| "amount": key_info.get("amount", "N/A"), | |
| "issued_date": key_info.get("tx_date", "N/A"), | |
| "expiry_date": key_info.get("expiry", "N/A"), | |
| "days_left": key_info.get("days_left", 0), | |
| "show_test_panel": key_info.get("show_test_panel", False) | |
| } | |
| } | |
| else: | |
| return { | |
| "thread_string": active_str, | |
| "key_info": { "status": "failed", "type": "free", "show_test_panel": False } | |
| } | |
| except Exception: | |
| return { "thread_string": "0/6", "key_info": { "status": "failed", "type": "free", "show_test_panel": False } } | |
| # --- API CHUYÊN TRÁCH XỬ LÝ NÚT BẤM VALIDATE TOKEN --- | |
| async def validate_user_key(request: Request): | |
| try: | |
| data = await request.json() | |
| token = data.get("key", "").strip() | |
| except Exception: | |
| return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid raw payload structure."}) | |
| device_id = request.client.host if request else "MOBILE_DEFAULT_NODE" | |
| if not token: | |
| return JSONResponse(status_code=400, content={"status": "error", "message": "Access token cannot be empty!"}) | |
| is_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id) | |
| if not is_valid: | |
| return JSONResponse(status_code=401, content={"status": "error", "message": "Authentication Failed: Invalid or expired security token."}) | |
| return { | |
| "status": "success", | |
| "message": "Credentials verification payload synchronized.", | |
| "tier": key_info.get("tier", "FREE").lower(), | |
| "days_left": key_info.get("days_left", 0) | |
| } | |
| async def get_task_logs(task_id: str): | |
| logs = GLOBAL_TERMINAL_LOGS.get(task_id, ["Connecting to master container log channel..."]) | |
| return {"logs": "\n".join(logs)} | |
| async def run_admin_test(): | |
| report = licensing_client.execute_admin_diagnostic_test() | |
| return {"status": "success", "report": report} | |
| # --- API NGẮT TIẾN TRÌNH KHẨN CẤP KHI NHẤN NÚT STOP HOẶC RỚT MẠNG --- | |
| async def cancel_task(license_key: str = Form(""), request: Request = None): | |
| device_id = request.client.host if request else "MOBILE_NODE_2026" | |
| token = license_key.strip() | |
| try: | |
| released = licensing_client.force_abort_user_session(token, device_id) | |
| if released: | |
| return {"status": "success", "msg": "Task rendering forcefully terminated."} | |
| return {"status": "error", "msg": "No active tasks found for this session."} | |
| except Exception as e: | |
| return {"status": "error", "msg": str(e)} | |
| async def monitor_disconnect_stream(request: Request, token: str, device_id: str, slot: int): | |
| while True: | |
| if await request.is_disconnected(): | |
| logger.warning(f"🔌 Disconnect Event Detected (F5 or Tab Closed) for Device {device_id}. Terminating stream.") | |
| licensing_client.force_abort_user_session(token, device_id) | |
| licensing_client.release_thread_slot(slot) | |
| break | |
| await time.sleep(1) | |
| # --- 3. CORE PROCESSING INTERFACE WITH WATERMARK & LIMITS --- | |
| async def api_generate( | |
| request: Request, | |
| background_tasks: BackgroundTasks, | |
| video_script: str = Form(...), | |
| clip_duration: int = Form(10), | |
| voice_rate: float = Form(1.0), | |
| selected_voice: str = Form("en-US-AvaNeural"), | |
| enable_subtitles: bool = Form(True), | |
| enable_bgm: bool = Form(True), | |
| license_key: str = Form("") | |
| ): | |
| if not video_script.strip(): | |
| return JSONResponse(status_code=400, content={"status": "error", "msg": "Please enter your script before generating!"}) | |
| token = license_key.strip() | |
| device_id = request.client.host if request.client else "MOBILE_NODE_2026" | |
| is_key_valid, key_info = licensing_client.verify_and_get_license_info(token, device_id) | |
| is_vip_key = (key_info.get("tier") in ["VIP", "ADMIN"]) if is_key_valid else False | |
| bypass_limits = key_info.get("bypass_limits", False) | |
| if not bypass_limits: | |
| allowed, limit_res = licensing_client.check_generation_limits(token, device_id, is_vip_key) | |
| if not allowed: | |
| return JSONResponse(status_code=400, content={"status": "error", "msg": limit_res}) | |
| success_alloc, slot_or_err = licensing_client.allocate_render_thread(token, device_id, is_vip_key) | |
| if not success_alloc: | |
| return JSONResponse(status_code=400, content={"status": "error", "msg": slot_or_err}) | |
| allocated_slot = slot_or_err | |
| current_pid = os.getpid() | |
| licensing_client.register_process_to_slot(allocated_slot, token, device_id, is_vip_key, current_pid) | |
| background_tasks.add_task(monitor_disconnect_stream, request, token, device_id, allocated_slot) | |
| task_id = f"TASK-{int(time.time())}" | |
| GLOBAL_TERMINAL_LOGS[task_id] = [f"🚀 System allocated Thread Slot #{allocated_slot} for PID {current_pid}"] | |
| params = VideoParams( | |
| video_subject=video_script[:30].strip(), | |
| video_script=video_script, | |
| video_aspect=VideoAspect.portrait, | |
| video_concat_mode=VideoConcatMode.random, | |
| video_clip_duration=clip_duration, | |
| voice_name=selected_voice, | |
| voice_rate=voice_rate, | |
| subtitle_enabled=enable_subtitles, | |
| bgm_type="random" if enable_bgm else "", | |
| n_threads=2 | |
| ) | |
| try: | |
| logger.info(f"[{task_id}] Starting Video Pipeline workflow...") | |
| GLOBAL_TERMINAL_LOGS[task_id].append("⚡ Fetching assets from Pexels API and preparing TTS narrative structure...") | |
| result = tm.start(task_id=task_id, params=params) | |
| if result and "videos" in result and len(result["videos"]) > 0: | |
| video_path = result["videos"][0] | |
| if os.path.exists(video_path): | |
| filename = os.path.basename(video_path) | |
| if not is_vip_key: | |
| GLOBAL_TERMINAL_LOGS[task_id].append("⚠️ Free tier session detected: Injecting mobile fluid watermark layer...") | |
| wm_filename = f"wm_{filename}" | |
| watermarked_path = os.path.join(OUTPUT_DIR, wm_filename) | |
| drawtext_filter = "drawtext=text='Hugging/AiVideoEngine':x='mod(t*35,w)':y='mod(t*15,h)':fontsize=24:fontcolor=white@0.35" | |
| ffmpeg_cmd = [ | |
| 'ffmpeg', '-y', | |
| '-i', video_path, | |
| '-vf', drawtext_filter, | |
| '-c:v', 'libx264', | |
| '-pix_fmt', 'yuv420p', | |
| '-c:a', 'copy', | |
| watermarked_path | |
| ] | |
| subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| if os.path.exists(watermarked_path): | |
| filename = wm_filename | |
| licensing_client.commit_generation_success(token, device_id, is_vip_key) | |
| GLOBAL_TERMINAL_LOGS[task_id].append("✅ Rendering success. Exporting video stream to player node.") | |
| return { | |
| "status": "success", | |
| "task_id": task_id, | |
| "video_url": f"/static_videos/{filename}" | |
| } | |
| return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": "Render completed but output file not found."}) | |
| except Exception as e: | |
| logger.error(f"Execution Error: {str(e)}") | |
| return JSONResponse(status_code=500, content={"status": "error", "task_id": task_id, "msg": f"Render Error: {str(e)}"}) | |
| finally: | |
| licensing_client.release_thread_slot(allocated_slot) | |
| background_tasks.add_task(lambda: [time.sleep(30), GLOBAL_TERMINAL_LOGS.pop(task_id, None)]) | |
| # --- 4. GIAO DIỆN CHÍNH --- | |
| async def index_page(): | |
| voices_options = "".join([f'<option value="{v}">{v}</option>' for v in get_voice_list()]) | |
| html_content = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI VIDEO ENGINE | Control Center</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght=400;500;600;700;800&display=swap'); | |
| 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; }} | |
| .glass-card {{ background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid #ffffff; }} | |
| .loading-box {{ display: none; background: rgba(239, 68, 68, 0.08); border: 2px dashed #ef4444; color: #991b1b; }} | |
| .video-wrapper {{ border: none !important; background: transparent !important; box-shadow: none !important; width: 100%; max-width: 320px; margin: 0 auto; }} | |
| video {{ width: 100%; border-radius: 2rem; display: block; box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }} | |
| </style> | |
| </head> | |
| <body class="p-4 md:p-12 lg:p-16"> | |
| <div class="max-w-7xl mx-auto"> | |
| <header class="text-center mb-12"> | |
| <div class="inline-block bg-indigo-100 text-indigo-700 px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest mb-6">Commercial Video System</div> | |
| <h1 class="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter italic">AI VIDEO ENGINE</h1> | |
| <p class="text-slate-500 mt-4 font-semibold uppercase tracking-[0.3em] text-sm md:text-base">v3.1.0 Production Workshop</p> | |
| </header> | |
| <div class="glass-card rounded-[2.5rem] p-6 md:p-8 shadow-sm border border-slate-200 mb-8 text-sm text-slate-600 space-y-4 leading-relaxed"> | |
| <p>🌟 <strong>Automated Production Suite:</strong> An instant AI video factory engineered tailored for Shorts, TikTok, and Reels. It automatically aggregates premium background clips from Pexels, structures high-fidelity AI narrative voices, blends automated audio beds, and burns fully aligned dynamic subtitles—all rendered seamlessly with just 1-click.</p> | |
| <p>💡 <strong>Our Mission:</strong> We are committed to engineering a highly accessible, low-cost automated content rendering architecture to help creators maximize efficiency while cutting software expenses down to zero.</p> | |
| <p>⚠️ <strong>Hosting Environment Notice:</strong> This platform is deployed on a <strong>Free Tier Shared Server (2 vCPU, 16GB RAM, Shared Core Containers)</strong> on Hugging Face. Due to unallocated shared hardware limits, video generation cycles can be quite lengthy. To actively protect our instance against memory crashes or sudden cluster restarts, strict thread concurrency thresholds have been applied. We sincerely apologize for any processing delays and thank you for your understanding.</p> | |
| <p>☕ <strong>Empower the Infrastructure:</strong> To expand our server performance capacities, maintain uninterrupted deployment pipelines, and invest in our upcoming formal platform architectures, your contributions are crucial! Our commercial premium license matrix remains incredibly cost-effective, with the highest tier priced under the cost of two standard cups of coffee. Secure your high-speed access keys here:</p> | |
| <div class="bg-indigo-50 border border-indigo-100 p-4 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4"> | |
| <span class="text-xs font-bold text-indigo-900 uppercase tracking-wide"><i class="fa-solid fa-cookie-bite mr-2"></i> Free tier users: 3 video creations/day (1 video per batch, 3-hour cooldown) with mobile fluid watermark. For comprehensive service tier details and premium pricing models, please click our licensing gateway.</span> | |
| <a href="https://huggingface.co/spaces/AbuAlone09/my-licensify-server" target="_blank" class="bg-indigo-600 text-white font-extrabold px-6 py-2.5 rounded-xl text-xs uppercase tracking-wider hover:bg-indigo-700 transition-all shadow-md shrink-0 text-center w-full sm:w-auto"> | |
| <i class="fa-solid fa-key mr-1.5"></i> Purchase Premium Keys | |
| </a> | |
| </div> | |
| </div> | |
| <div class="glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200 mb-8"> | |
| <div class="flex items-center gap-4 mb-6"> | |
| <div class="w-12 h-12 bg-slate-900 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-indigo-200"> | |
| <i class="fa-solid fa-lock text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-xl font-extrabold text-slate-800">🔐 System Authentication</h3> | |
| <p class="text-slate-400 text-sm">Validate credentials to remove output limitations and stream watermarks</p> | |
| </div> | |
| </div> | |
| <div class="flex flex-col md:flex-row gap-4"> | |
| <input type="password" id="licenseKey" placeholder="Enter valid access token or security key..." class="flex-grow bg-slate-50 border-2 border-slate-100 rounded-2xl px-5 py-4 outline-none focus:border-indigo-500 focus:bg-white transition-all font-mono text-base shadow-inner"> | |
| <button onclick="triggerVerificationHandshake()" class="bg-slate-900 text-white px-8 py-4 rounded-2xl font-black text-base hover:bg-black transition-all shadow-md active:scale-95 shrink-0"> | |
| VALIDATE TOKEN | |
| </button> | |
| </div> | |
| <div id="keyDetails" class="text-xs font-bold text-amber-600 uppercase tracking-wider mt-4 flex flex-col gap-2 p-3 bg-amber-50/40 rounded-xl border border-amber-100"> | |
| <div><i class="fa-solid fa-circle-exclamation mr-1"></i> Status: Unverified (Free Tier System Active)</div> | |
| </div> | |
| <div id="adminTestPanel" class="hidden mt-4 p-4 bg-slate-900 text-slate-100 rounded-2xl border border-slate-700"> | |
| <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> | |
| <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> | |
| <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> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start mb-20"> | |
| <div class="lg:col-span-7 glass-card rounded-[3rem] p-6 md:p-10 shadow-xl border border-slate-200"> | |
| <div class="flex items-center gap-4 mb-8"> | |
| <div class="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-200"> | |
| <i class="fa-solid fa-sliders text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-xl font-extrabold text-slate-800">⚙️ Video Configuration</h3> | |
| <p class="text-slate-400 text-sm">Tune rendering parameters and narration variables</p> | |
| </div> | |
| </div> | |
| <form id="videoForm" onsubmit="handleRenderCycle(event)" class="space-y-6"> | |
| <div> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label class="text-xs font-black text-slate-500 uppercase tracking-wider">Video Narrative Script</label> | |
| <span id="charCounter" class="text-xs font-bold text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-md">0 / 1500 Chars</span> | |
| </div> | |
| <textarea name="video_script" id="video_script" rows="6" placeholder="Write or paste your script framework..." class="w-full bg-slate-50 border-2 border-slate-100 rounded-2xl px-5 py-4 outline-none focus:border-blue-500 focus:bg-white transition-all font-sans text-base shadow-inner leading-relaxed" required></textarea> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Clip Duration Selection</label> | |
| <select name="clip_duration" class="w-full bg-slate-50 border-2 border-slate-100 rounded-xl px-4 py-3 text-slate-800 focus:border-blue-500 outline-none font-medium"> | |
| <option value="5">5 Seconds</option> | |
| <option value="6">6 Seconds</option> | |
| <option value="7">7 Seconds</option> | |
| <option value="8">8 Seconds</option> | |
| <option value="9">9 Seconds</option> | |
| <option value="10" selected>10 Seconds (Default)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Voice Speed Selection</label> | |
| <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"> | |
| <option value="0.5">0.5x</option> | |
| <option value="0.6">0.6x</option> | |
| <option value="0.7">0.7x</option> | |
| <option value="0.8">0.8x</option> | |
| <option value="0.9">0.9x</option> | |
| <option value="1.0" selected>1.0x (Normal)</option> | |
| <option value="1.1">1.1x</option> | |
| <option value="1.2">1.2x</option> | |
| <option value="1.3">1.3x</option> | |
| <option value="1.4">1.4x</option> | |
| <option value="1.5">1.5x</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="text-xs font-black text-slate-500 uppercase tracking-wider block mb-2">Azure TTS Voice Engine</label> | |
| <select name="selected_voice" class="w-full bg-slate-50 border-2 border-slate-100 rounded-xl px-4 py-3.5 text-slate-800 focus:border-blue-500 outline-none font-medium"> | |
| {voices_options} | |
| </select> | |
| </div> | |
| <div class="flex flex-wrap gap-6 bg-slate-50/80 p-4 rounded-2xl border border-slate-100"> | |
| <label class="flex items-center gap-2 cursor-pointer select-none"> | |
| <input type="checkbox" name="enable_subtitles" checked id="subCheck" class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300"> | |
| <span class="text-sm font-bold text-slate-700">Render Subtitles</span> | |
| </label> | |
| <label class="flex items-center gap-2 cursor-pointer select-none"> | |
| <input type="checkbox" name="enable_bgm" checked id="bgmCheck" class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300"> | |
| <span class="text-sm font-bold text-slate-700">Background Music (BGM)</span> | |
| </label> | |
| </div> | |
| <button type="submit" id="mainSubmitBtn" class="w-full bg-indigo-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-100 active:scale-95 flex items-center justify-center gap-2"> | |
| <i class="fa-solid fa-play" id="btnIcon"></i> <span id="btnText">GENERATE PRODUCTION VIDEO</span> | |
| </button> | |
| </form> | |
| </div> | |
| <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"> | |
| <div class="flex items-center gap-4 mb-6"> | |
| <div class="w-12 h-12 bg-emerald-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-emerald-200"> | |
| <i class="fa-solid fa-film text-xl"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-xl font-extrabold text-slate-800">🎬 Studio Result</h3> | |
| <p class="text-slate-400 text-sm">Realtime output file delivery node</p> | |
| </div> | |
| </div> | |
| <div id="loadingBox" class="loading-box rounded-2xl p-6 mb-6 text-sm leading-relaxed font-bold"> | |
| <div class="flex items-center gap-2 text-red-700 font-extrabold mb-2 text-base"> | |
| <i class="fa-solid fa-circle-notch animate-spin text-lg"></i> <span>Pipeline Rendering Operational...</span> | |
| </div> | |
| 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. | |
| </div> | |
| <div class="video-wrapper my-auto py-4"> | |
| <video id="videoPlayer" controls playsinline poster="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=400"> | |
| <source id="videoSource" src="" type="video/mp4"> | |
| </video> | |
| </div> | |
| </div> | |
| </div> | |
| <footer class="border-t border-slate-200 pt-8 pb-8 text-center"> | |
| <div class="bg-white inline-block px-8 py-4 rounded-2xl shadow-sm border border-slate-100"> | |
| <p class="text-slate-400 text-[11px] font-bold tracking-widest uppercase mb-1">Copyright © 2024 Harry | Refactored © 2026 Abu Alone Project</p> | |
| <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> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| let currentTaskId = null; | |
| let runningStateActive = false; | |
| const txtInput = document.getElementById("video_script"); | |
| const lblCounter = document.getElementById("charCounter"); | |
| function enforceInputConstraints() {{ | |
| if(txtInput.value.length > 1500) {{ | |
| txtInput.value = txtInput.value.substring(0, 1500); | |
| }} | |
| lblCounter.innerText = txtInput.value.length + " / 1500 Chars"; | |
| }} | |
| txtInput.addEventListener("input", enforceInputConstraints); | |
| txtInput.addEventListener("paste", () => {{ setTimeout(enforceInputConstraints, 10); }}); | |
| // 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 | |
| async function triggerVerificationHandshake() {{ | |
| const currentToken = document.getElementById("licenseKey").value.trim(); | |
| const keyDetails = document.getElementById("keyDetails"); | |
| const adminPanel = document.getElementById("adminTestPanel"); | |
| if (!currentToken) {{ | |
| 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>'; | |
| return; | |
| }} | |
| 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>'; | |
| try {{ | |
| const response = await fetch("/api/system-status?key=" + encodeURIComponent(currentToken)); | |
| const data = await response.json(); | |
| if (data.key_info && data.key_info.status === "success") {{ | |
| if (data.key_info.type === "admin") {{ | |
| adminPanel.classList.remove("hidden"); | |
| }} else {{ | |
| adminPanel.classList.add("hidden"); | |
| }} | |
| // HIỂN THỊ TICK XANH VÀ BẢNG THÔNG TIN CHI TIẾT CỦA KEY (ĐÃ NHÂN ĐÔI NGOẶC NHỌN CHO PYTHON) | |
| keyDetails.innerHTML = ` | |
| <div class="text-emerald-600 font-extrabold grid grid-cols-1 gap-1.5 text-xs"> | |
| <div class="text-emerald-700 font-black text-sm border-b border-emerald-100 pb-1 flex items-center gap-1"> | |
| <i class="fa-solid fa-circle-check text-base"></i> [✅ VALID ACCESS KEY] | |
| </div> | |
| <div>🆔 <strong>Transaction Identifier (TxID):</strong> ${{data.key_info.tx_id}}</div> | |
| <div>👤 <strong>Subscription Model (Tier):</strong> ${{data.key_info.type.toUpperCase()}}</div> | |
| <div>💰 <strong>Amount Paid:</strong> ${{data.key_info.amount}}</div> | |
| <div>📅 <strong>Issued Date:</strong> ${{data.key_info.issued_date}}</div> | |
| <div>⏳ <strong>Expiration Frame (Expiry Date):</strong> ${{data.key_info.expiry_date}} (${{data.key_info.days_left}} days left)</div> | |
| </div> | |
| `; | |
| }} else {{ | |
| adminPanel.classList.add("hidden"); | |
| 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> [❌ INVALID SECURITY KEY] The access token specified does not exist or has expired!</div>'; | |
| }} | |
| }} catch (error) {{ | |
| adminPanel.classList.add("hidden"); | |
| keyDetails.innerHTML = '<div class="text-rose-600 font-black text-xs"><i class="fa-solid fa-circle-xmark mr-1"></i> [❌ NODE ERROR] Authentication node failed to respond.</div>'; | |
| }} | |
| }} | |
| async function triggerAdminTesterScript() {{ | |
| const term = document.getElementById("diagnosticReportTerminal"); | |
| term.classList.remove("hidden"); | |
| term.innerText = "Executing tester.py pipeline, please hold..."; | |
| try {{ | |
| const res = await fetch("/api/admin/run-test", {{ method: "POST" }}); | |
| const data = await res.json(); | |
| term.innerText = data.report; | |
| }} catch(e) {{ | |
| term.innerText = "❌ Diagnostic communication pipeline broken: " + e; | |
| }} | |
| }} | |
| // HÀM XỬ LÝ CLICK TẠO VIDEO & NÚT STOP THAY ĐỔI THEO FORM DATA ĐỒNG BỘ BACKEND | |
| async function handleRenderCycle(e) {{ | |
| e.preventDefault(); | |
| const targetBtn = document.getElementById("mainSubmitBtn"); | |
| const iconObj = document.getElementById("btnIcon"); | |
| const textObj = document.getElementById("btnText"); | |
| const currentToken = document.getElementById("licenseKey").value.trim(); | |
| if(runningStateActive) {{ | |
| if(confirm("Do you want to terminate the active rendering cycle immediately?")) {{ | |
| // ĐÓNG GÓI FORM DATA ĐỂ CHẠY CHUẨN LỆNH CANCEL-TASK Ở BACKEND | |
| const fData = new FormData(); | |
| fData.append("license_key", currentToken); | |
| try {{ | |
| await fetch("/api/cancel-task", {{ method: "POST", body: fData }}); | |
| }} catch(err) {{ }} | |
| revertUiLayoutToNormal(); | |
| }} | |
| return; | |
| }} | |
| runningStateActive = true; | |
| 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"; | |
| textObj.innerText = "STOP & ABORT RENDERING"; | |
| iconObj.className = "fa-solid fa-circle-stop"; | |
| document.getElementById("loadingBox").style.display = "block"; | |
| currentTaskId = "TASK-" + Math.floor(Date.now() / 1000); | |
| const formObj = document.getElementById("videoForm"); | |
| const payloadData = new FormData(formObj); | |
| payloadData.append("license_key", currentToken); | |
| const player = document.getElementById("videoPlayer"); | |
| const source = document.getElementById("videoSource"); | |
| player.pause(); | |
| try {{ | |
| const response = await fetch("/api/generate", {{ method: "POST", body: payloadData }}); | |
| const serverResponse = await response.json(); | |
| if(serverResponse.status === "success") {{ | |
| source.src = serverResponse.video_url; | |
| player.src = serverResponse.video_url; | |
| player.load(); | |
| setTimeout(() => {{ player.play().catch(e => {{}}); }}, 200); | |
| alert("✅ Video processing completed successfully!"); | |
| }} else {{ | |
| alert("❌ " + serverResponse.msg); | |
| }} | |
| }} catch(err) {{ | |
| alert("❌ Connection lost or pipeline aborted."); | |
| }} finally {{ | |
| revertUiLayoutToNormal(); | |
| }} | |
| }} | |
| function revertUiLayoutToNormal() {{ | |
| runningStateActive = false; | |
| document.getElementById("loadingBox").style.display = "none"; | |
| const targetBtn = document.getElementById("mainSubmitBtn"); | |
| const iconObj = document.getElementById("btnIcon"); | |
| const textObj = document.getElementById("btnText"); | |
| 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"; | |
| textObj.innerText = "GENERATE PRODUCTION VIDEO"; | |
| iconObj.className = "fa-solid fa-play"; | |
| }} | |
| </script> | |
| </body> | |
| </html>""" | |
| return HTMLResponse(content=html_content) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |