""" app.py — Gradio Cloud Studio for Hugging Face Spaces ──────────────────────────────────────────────────── Features: - Instant Live Video Preview with Bounding Box Overlay (100% reliable across all Gradio versions). - 1-Click Fast Presets (Bottom, Bottom-Low, Middle) + Step Nudge Buttons for mobile touch. - True Unicode Vietnamese Fonts (DejaVu Sans) — No more square [][][][][] boxes. - 90% Solid Masking Box to 100% cover old Chinese subtitles. - 25 AI Models Matrix (Groq, Gemini, OpenRouter, NVIDIA NIM, xKiro). """ import os import sys import time from pathlib import Path # Add current directory to sys.path BASE_DIR = Path(__file__).resolve().parent if str(BASE_DIR) not in sys.path: sys.path.insert(0, str(BASE_DIR)) import gradio as gr try: import spaces except ImportError: class MockSpaces: @staticmethod def GPU(*args, **kwargs): def decorator(f): return f return decorator spaces = MockSpaces() from app.core.cloud_pipeline import CloudPipeline pipeline = CloudPipeline(base_dir=BASE_DIR) @spaces.GPU(duration=10) def _zerogpu_keepalive(): return True def get_all_video_paths(video_file, video_urls=""): """Robust extraction of all video filepaths across all Gradio versions.""" paths = [] def _extract_single(v): if isinstance(v, str) and Path(v).exists(): return v if hasattr(v, "path") and v.path and Path(str(v.path)).exists(): return str(v.path) if hasattr(v, "name") and v.name and Path(str(v.name)).exists(): return str(v.name) if isinstance(v, dict): p = v.get("path") or v.get("name") if p and Path(str(p)).exists(): return str(p) try: cand = str(v) if Path(cand).exists(): return cand except Exception: pass return None if video_file is not None: if isinstance(video_file, (list, tuple)): for v in video_file: p = _extract_single(v) if p: paths.append(p) else: p = _extract_single(video_file) if p: paths.append(p) if video_urls and video_urls.strip(): urls = [u.strip() for u in video_urls.split('\n') if u.strip()] for url in urls: saved_path = BASE_DIR / "temp" / f"url_video_{int(time.time()*1000)}_{len(paths)}.mp4" saved_path.parent.mkdir(parents=True, exist_ok=True) try: import subprocess cmd = ["yt-dlp", "-f", "best[ext=mp4]/best", "-o", str(saved_path), url] res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) if res.returncode == 0 and saved_path.exists(): paths.append(str(saved_path)) except Exception: pass return paths def get_actual_path(video_file, video_url=""): """Backward compatibility for preview/logo extraction.""" paths = get_all_video_paths(video_file, video_url) return paths[0] if paths else None def _resolve_logo_path(logo_file): if logo_file is not None: p = get_actual_path(logo_file, "") if p and Path(p).exists(): return p default = BASE_DIR / "assets" / "logo_ins_drippy4.png" if default.exists(): return str(default) alt = Path(r"C:\Users\Admin\Pictures\_imagine_prompt__The_letters__INS_202606162118.jpeg") if alt.exists(): return str(alt) return "" def _resolve_cta_path(cta_file): if cta_file is not None: p = get_actual_path(cta_file, "") if p and Path(p).exists(): return p default = BASE_DIR / "assets" / "cta_ins_drippy4.mp4" if default.exists(): return str(default) alt = Path(r"C:\Users\Admin\Downloads\ODR QUẦN ÁO, PKIỆN, TẢ GHÉ INS DRIPPY.4 #unbo(4).mp4") if alt.exists(): return str(alt) return "" def _error_image(msg): import numpy as np import cv2 import base64 img = np.zeros((720, 1280, 3), dtype=np.uint8) font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(img, "LOI PREVIEW:", (50, 300), font, 1.5, (0, 0, 255), 3, cv2.LINE_AA) cv2.putText(img, str(msg)[:80], (50, 380), font, 1.2, (0, 0, 255), 2, cv2.LINE_AA) ret, buffer = cv2.imencode('.jpg', img) if ret: return "data:image/jpeg;base64," + base64.b64encode(buffer).decode('utf-8') return "" def update_inputs_state(video_file, url_input): paths = get_all_video_paths(video_file, url_input) new_mask_state = {} for p in paths: new_mask_state[p] = {"y": 72, "h": 22} import os choices = [os.path.basename(p) for p in paths] selected = choices[0] if choices else None status_lines = [] for p in paths: status_lines.append(f"- 🎬 **{os.path.basename(p)}** `[Y=72%, H=22%]`") status_text = "\n".join(status_lines) if status_lines else "*Chưa có video nào*" return paths, new_mask_state, 0, gr.update(choices=choices, value=selected), status_text def on_selector_change(selected_name, paths, mask_state): if not paths or not selected_name: return 0, 72, 22 import os try: idx = next(i for i, p in enumerate(paths) if os.path.basename(p) == selected_name) except StopIteration: idx = 0 p = paths[idx] y = mask_state.get(p, {}).get("y", 72) h = mask_state.get(p, {}).get("h", 22) return idx, y, h def on_mask_change(y, h, apply_all, idx, paths, mask_state): if not paths or idx >= len(paths): return mask_state, "*Chưa có video nào*" if apply_all: for p in paths: if p not in mask_state: mask_state[p] = {} mask_state[p]["y"] = y mask_state[p]["h"] = h else: p = paths[idx] if p not in mask_state: mask_state[p] = {} mask_state[p]["y"] = y mask_state[p]["h"] = h import os status_lines = [] for p in paths: cy = mask_state.get(p, {}).get("y", 72) ch = mask_state.get(p, {}).get("h", 22) status_lines.append(f"- 🎬 **{os.path.basename(p)}** `[Y={cy}%, H={ch}%]`") return mask_state, "\n".join(status_lines) def parse_cropper_coords(json_str, paths, idx, apply_all, mask_state): import json if not json_str: import gradio as gr return gr.update(), gr.update(), mask_state, "" try: data = json.loads(json_str) y = int(data.get("y", 72)) h = int(data.get("h", 22)) mask_state, q_status = on_mask_change(y, h, apply_all, idx, paths, mask_state) return y, h, mask_state, q_status except: import gradio as gr return gr.update(), gr.update(), mask_state, "" def update_preview_box(paths, idx, custom_x, custom_y, custom_w, custom_h, preview_sec, logo_file=None, logo_enabled=False, logo_preset="bottom_right", logo_scale=18, cta_file=None, cta_enabled=False, cta_preset="bottom_center", cta_scale=48): try: from pathlib import Path if not paths or idx >= len(paths): return _error_image("Vui lòng chọn 1 video để xem trước") vpath = paths[idx] if not Path(vpath).exists(): return _error_image("Video không tồn tại") logo_path = _resolve_logo_path(logo_file) if logo_enabled else "" cta_path = _resolve_cta_path(cta_file) if cta_enabled else "" b64_str = pipeline.generate_preview_frame( video_path=str(vpath), region_preset="custom", custom_y_pct=float(custom_y), custom_h_pct=0.0, custom_x_pct=0.0, custom_w_pct=0.0, sec=float(preview_sec), return_type="base64", logo_enabled=bool(logo_enabled and logo_path and Path(logo_path).exists()), logo_path=str(logo_path) if logo_path else "", logo_preset=str(logo_preset), logo_scale=float(logo_scale), logo_opacity=1.0, logo_chromakey=True, cta_enabled=bool(cta_enabled and cta_path and Path(cta_path).exists()), cta_path=str(cta_path) if cta_path else "", cta_preset=str(cta_preset), cta_scale=float(cta_scale), cta_opacity=1.0, cta_interval=5.0, cta_duration=2.0, cta_chromakey=True ) if b64_str: return "data:image/jpeg;base64," + b64_str return "" except Exception as e: print(f"Lỗi preview: {e}") import traceback traceback.print_exc() return _error_image(f"Loi: {e}") # Helper functions for 1-click preset buttons def preset_bottom(): return 0, 72, 100, 22 # x, y, w, h def preset_bottom_low(): return 0, 82, 100, 16 # x, y, w, h def preset_middle(): return 0, 38, 100, 24 # x, y, w, h def nudge_up(y): return max(0, y - 5) def nudge_down(y): return min(95, y + 5) def nudge_expand(h): return min(60, h + 4) def nudge_shrink(h): return max(4, h - 4) def process_batch( video_paths, mask_state, mode, sub_mask_mode, sub_style, custom_x, custom_w, voice, source_lang, speed, volume, mute_original=False, logo_file=None, logo_enabled=False, logo_preset="bottom_right", logo_scale=18, cta_file=None, cta_enabled=False, cta_preset="bottom_center", cta_scale=48, progress=gr.Progress() ): if not video_paths: yield None, "❌ Vui lòng chọn ít nhất 1 video hoặc nhập link hợp lệ!" return total = len(video_paths) logs = [f"🚀 Đã nhận {total} video để xử lý hàng loạt..."] yield [], "\n".join(logs) completed_files = [] logo_path = _resolve_logo_path(logo_file) if logo_enabled else "" cta_path = _resolve_cta_path(cta_file) if cta_enabled else "" for i, actual_video_path in enumerate(video_paths): logs.append(f"\n--- Đang xử lý video {i+1}/{total} ---") yield completed_files, "\n".join(logs) def log_cb(msg: str): logs.append(f"[{time.strftime('%H:%M:%S')}] {msg}") # Note: We can't yield from a callback directly in Gradio easily without Queue streaming, # but we can yield at the end of each video. The user will see progress via the gr.Progress. def progress_cb(pct: int, stage: str): progress(pct / 100.0, desc=f"Video {i+1}/{total}: {stage} ({pct}%)") exec_pipeline = CloudPipeline( base_dir=BASE_DIR, log_callback=log_cb, progress_callback=progress_cb ) cy = mask_state.get(actual_video_path, {}).get("y", 72) ch = mask_state.get(actual_video_path, {}).get("h", 22) out_video = exec_pipeline.run_video( video_path=str(actual_video_path), mode=mode, source_lang=source_lang, voice=voice, speed=speed, pitch=0, volume=int(volume), region_preset="custom", sub_mask_mode=sub_mask_mode, sub_style=sub_style, custom_y_pct=float(cy), custom_h_pct=float(ch), custom_x_pct=float(custom_x), custom_w_pct=float(custom_w), mute_original_audio=bool(mute_original), logo_enabled=bool(logo_enabled and logo_path and Path(logo_path).exists()), logo_path=str(logo_path) if logo_path else "", logo_preset=str(logo_preset), logo_scale=float(logo_scale), logo_opacity=1.0, cta_enabled=bool(cta_enabled and cta_path and Path(cta_path).exists()), cta_path=str(cta_path) if cta_path else "", cta_preset=str(cta_preset), cta_scale=float(cta_scale), cta_opacity=1.0, cta_interval=5.0, cta_duration=2.0 ) if out_video and Path(out_video).exists(): completed_files.append(str(out_video)) logs.append(f"✅ HOÀN THÀNH video {i+1}/{total}!") else: logs.append(f"❌ LỖI xử lý video {i+1}/{total}!") yield completed_files, "\n".join(logs) logs.append(f"\n🎉 XONG! Đã xử lý thành công {len(completed_files)}/{total} video.") yield completed_files, "\n".join(logs) head_html = """ """ with gr.Blocks(title="Trung Sáng Việt Cloud Studio", head=head_html) as demo: gr.Markdown( """ # 🎬 TRUNG SÁNG VIỆT CLOUD STUDIO ### Dịch • Lồng Tiếng • Che Sub Cũ & Đè Sub Mới Chuẩn Studio • 100% Cloud-Native """ ) with gr.Row(): # Cột 1: Video & Live Preview Khung Vùng Che with gr.Column(scale=1): gr.Markdown("### 📥 1. Chọn Video & Xem Trước Trực Quan (Live Preview)") video_input = gr.File( label="📁 Bấm vào đây để chọn NHIỀU video từ Thư viện ảnh / Tệp tin điện thoại", file_types=["video", ".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"], file_count="multiple" ) url_input = gr.Textbox( label="Hoặc dán NHIỀU Link video (Douyin / TikTok), mỗi link 1 dòng", placeholder="https://v.douyin.com/...\nhttps://www.tiktok.com/...", lines=5 ) video_paths_state = gr.State([]) mask_state = gr.State({}) preview_idx_state = gr.State(0) with gr.Row(): video_selector = gr.Dropdown(label="🔍 Chọn Video để Căn Chỉnh", choices=[], interactive=True) apply_to_all = gr.Checkbox(label="✅ Áp dụng vị trí che này cho tất cả video trong danh sách", value=True) queue_status = gr.Markdown(value="*Chưa có video nào trong hàng đợi*") # Live preview frame with box preview_base64 = gr.Textbox(visible=True, elem_id="preview-base64") cropper_coords = gr.Textbox(visible=False, elem_id="cropper-coords") gr.HTML("""
""") preview_sec = gr.Slider( minimum=0.0, maximum=30.0, value=3.0, step=0.5, label="⏱️ Tua giây video (để chọn khung hình có phụ đề rõ nhất)" ) # Fast 1-Click Alignment Buttons gr.Markdown("##### 🎯 Căn Chỉnh Vị Trí Nhanh (1-Chạm Trên Điện Thoại):") with gr.Row(): btn_preset_bottom = gr.Button("📍 Đáy Màn Hình (Chuẩn TikTok)", size="sm") btn_preset_bottom_low = gr.Button("📍 Đáy Sát Mép", size="sm") btn_preset_middle = gr.Button("📍 Giữa Video", size="sm") with gr.Row(): btn_up = gr.Button("🔼 Dịch Lên (-5%)", size="sm") btn_down = gr.Button("🔽 Dịch Xuống (+5%)", size="sm") btn_expand = gr.Button("➕ Cao Thêm (+4%)", size="sm") btn_shrink = gr.Button("➖ Thu Gọn (-4%)", size="sm") with gr.Row(): custom_y = gr.Slider(minimum=0, maximum=95, value=72, step=1, label="↕️ Vị trí Y (%)", elem_id="custom_y_slider") custom_h = gr.Slider(minimum=4, maximum=60, value=22, step=1, label="↕️ Chiều Cao H (%)", elem_id="custom_h_slider") with gr.Row(visible=False): custom_x = gr.Number(value=0) custom_w = gr.Number(value=100) mode_input = gr.Radio( choices=[ ("🎙️ Cloud ASR (Groq Whisper - Siêu tốc 1.5s/video)", "asr"), ("👁️ Cloud OCR (25 AI Vision Models - Quét chữ màn hình)", "ocr") ], value="asr", label="Phương thức bóc tách phụ đề" ) # Cột 2: Cấu hình Giọng Đọc & Sub Mới with gr.Column(scale=1): gr.Markdown("### 🎙️ 2. Cấu Hình Che Sub Cũ & Giọng Đọc Tiếng Việt") sub_mask_mode = gr.Dropdown( choices=[ ("⬛ Hộp Đen Mờ Che Kín (Phủ 90% - Che sạch 100% chữ Hán)", "box"), ("🌫️ Làm Mờ Sub Cũ (Delogo Blur) - Tự nhiên", "delogo"), ("🚫 Không Che (Chỉ Đè Sub Mới Viền Đậm)", "none") ], value="box", label="Kiểu che / xóa phụ đề cũ" ) voice_input = gr.Dropdown( choices=[ ("🎙️ Nam Minh (Edge-TTS - Trầm ấm, chuyên nghiệp)", "vi-VN-NamMinhNeural"), ("🎙️ Hoài My (Edge-TTS - Truyền cảm, ngọt ngào)", "vi-VN-HoaiMyNeural") ], value="vi-VN-NamMinhNeural", label="Giọng đọc tiếng Việt (Edge-TTS Miễn Phí)" ) source_lang_input = gr.Dropdown( choices=[("🇨🇳 Tiếng Trung (Chinese - zh)", "zh"), ("🇺🇸 Tiếng Anh (English - en)", "en")], value="zh", label="Ngôn ngữ gốc của video" ) sub_style = gr.Dropdown( choices=[ ("✨ Chữ Trắng Viền Đen Nổi Bật (Arial Bold 24)", "white_bold"), ("🟡 Chữ Vàng Viền Đen (Review/Phim)", "yellow_bold"), ("🟢 Chữ Xanh Neon Cyberpunk", "neon_cyan") ], value="white_bold", label="Kiểu chữ phụ đề tiếng Việt mới (Hardsub Style)" ) with gr.Row(): speed_input = gr.Slider(minimum=0.8, maximum=1.5, value=1.0, step=0.05, label="Tốc độ đọc (Speed)") vol_input = gr.Slider(minimum=50, maximum=150, value=100, step=5, label="Âm lượng (%)") mute_original = gr.Checkbox(label="🔇 Tắt tiếng gốc (chỉ giữ giọng Việt)", value=False) gr.Markdown("#### 🖼️ Logo INS DRIPPY 4 (nền xanh tự xóa)") logo_file = gr.File(label="Chọn logo (PNG/JPG) - để trống dùng logo mặc định", file_types=["image", ".png", ".jpg", ".jpeg"]) logo_enabled = gr.Checkbox(label="✅ Bật logo", value=False) logo_preset = gr.Dropdown(choices=["top_left","top_right","bottom_left","bottom_right","center","top_center","bottom_center","custom"], value="bottom_right", label="Vị trí logo (bắt buộc chọn 1 ví dụ)") logo_scale = gr.Slider(minimum=5, maximum=40, value=18, step=1, label="Kích thước logo (% chiều rộng) - TikTok safe") gr.Markdown("#### 🎬 CTA Video ODR #unbo(4) (nền xanh, hiện mỗi 5s - TikTok 48% rõ hơn)") cta_file = gr.File(label="Chọn CTA video (MP4) - để trống dùng CTA mặc định 2.5s", file_types=["video", ".mp4", ".mov"]) cta_enabled = gr.Checkbox(label="✅ Bật CTA video (mỗi 5s)", value=False) cta_preset = gr.Dropdown(choices=["top_left","top_right","bottom_left","bottom_right","center","top_center","bottom_center","custom"], value="bottom_center", label="Vị trí CTA (bắt buộc chọn 1 ví dụ)") cta_scale = gr.Slider(minimum=15, maximum=60, value=48, step=1, label="Kích thước CTA (% chiều rộng) - TikTok 48%") start_btn = gr.Button("🚀 BẮT ĐẦU DỊCH & LỒNG TIẾNG CLOUD", variant="primary") # ── Live Preview Dynamic Event Listeners (logo + CTA) ───────────────────── preview_inputs = [ video_paths_state, preview_idx_state, custom_x, custom_y, custom_w, custom_h, preview_sec, logo_file, logo_enabled, logo_preset, logo_scale, cta_file, cta_enabled, cta_preset, cta_scale ] # When inputs change, update states update_inputs_ev = dict( fn=update_inputs_state, inputs=[video_input, url_input], outputs=[video_paths_state, mask_state, preview_idx_state, video_selector, queue_status] ) js_init = """(b64) => { if (typeof window.initCropper === 'function') { window.initCropper(b64); } else { const container = document.getElementById('cropper-container'); if (container) container.innerHTML = '

Lỗi: Javascript chưa tải xong (window.initCropper is undefined). Hãy refresh lại trang!

'; } return []; }""" js_update = "(y, h) => { if(window.updateCropperBox) window.updateCropperBox(y, h); return []; }" video_input.upload(**update_inputs_ev).then(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) video_input.clear(**update_inputs_ev).then(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) url_input.change(**update_inputs_ev).then(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) # When selector changes, update preview_idx and UI sliders video_selector.change( fn=on_selector_change, inputs=[video_selector, video_paths_state, mask_state], outputs=[preview_idx_state, custom_y, custom_h] ).then(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) # When Y, H sliders change, update mask_state and Cropper box mask_change_ev = dict( fn=on_mask_change, inputs=[custom_y, custom_h, apply_to_all, preview_idx_state, video_paths_state, mask_state], outputs=[mask_state, queue_status] ) custom_y.change(**mask_change_ev).then(fn=None, inputs=[custom_y, custom_h], outputs=None, js=js_update) custom_h.change(**mask_change_ev).then(fn=None, inputs=[custom_y, custom_h], outputs=None, js=js_update) # Handle JS Cropper to Python via hidden coords textbox cropper_coords.change( fn=parse_cropper_coords, inputs=[cropper_coords, video_paths_state, preview_idx_state, apply_to_all, mask_state], outputs=[custom_y, custom_h, mask_state, queue_status] ) # Visual updates preview_sec.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) logo_file.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) logo_enabled.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) logo_preset.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) logo_scale.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) cta_file.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) cta_enabled.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) cta_preset.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) cta_scale.change(fn=update_preview_box, inputs=preview_inputs, outputs=preview_base64).then(fn=None, inputs=[preview_base64], outputs=None, js=js_init) # Fast Preset Button Events btn_preset_bottom.click( fn=preset_bottom, outputs=[custom_x, custom_y, custom_w, custom_h] ) btn_preset_bottom_low.click( fn=preset_bottom_low, outputs=[custom_x, custom_y, custom_w, custom_h] ) btn_preset_middle.click( fn=preset_middle, outputs=[custom_x, custom_y, custom_w, custom_h] ) # Nudge Button Events btn_up.click(fn=nudge_up, inputs=[custom_y], outputs=[custom_y]) btn_down.click(fn=nudge_down, inputs=[custom_y], outputs=[custom_y]) btn_expand.click(fn=nudge_expand, inputs=[custom_h], outputs=[custom_h]) btn_shrink.click(fn=nudge_shrink, inputs=[custom_h], outputs=[custom_h]) with gr.Row(): with gr.Column(): gr.Markdown("### 🎉 3. Video Thành Phẩm & Nhật Ký Xử Lý") file_output = gr.File(label="Tải về các video đã hoàn thành (Nhiều file sẽ có nút Tải Tất Cả .ZIP)", file_count="multiple") log_output = gr.Textbox(label="Nhật ký tiến độ tổng thể (Live Logs)", lines=8) start_btn.click( fn=process_batch, inputs=[ video_paths_state, mask_state, mode_input, sub_mask_mode, sub_style, custom_x, custom_w, voice_input, source_lang_input, speed_input, vol_input, mute_original, logo_file, logo_enabled, logo_preset, logo_scale, cta_file, cta_enabled, cta_preset, cta_scale ], outputs=[file_output, log_output] ) # ── Secure Authentication via Hugging Face Space Secrets ────────────────────── # Mật khẩu được ẩn hoàn toàn trong mục Settings > Secrets của Hugging Face ACCOUNTS = {} env_auth = os.environ.get("AUTH_USERS", "") if env_auth: for item in env_auth.split(","): if ":" in item: u, p = item.strip().split(":", 1) ACCOUNTS[u.strip().lower()] = p.strip() # Fallback nếu chưa cấu hình Secret if not ACCOUNTS: default_user = os.environ.get("AUTH_USER", "admin") default_pass = os.environ.get("AUTH_PASSWORD", "DrippySecret2026@") ACCOUNTS[default_user.lower()] = default_pass def verify_login(username: str, password: str) -> bool: """ Xác thực đăng nhập an toàn chống nhìn trộm mã nguồn: - Bỏ qua chữ hoa/chữ thường ở username. - Tự động cắt khoảng trắng thừa ở đầu/cuối trên điện thoại. """ if not username or not password: return False clean_user = username.strip().lower() clean_pass = password.strip() return ACCOUNTS.get(clean_user) == clean_pass # --- AUTO-SAVE SETTINGS FEATURE --- settings_comps = [ mode_input, sub_mask_mode, voice_input, source_lang_input, sub_style, speed_input, vol_input, mute_original, logo_enabled, logo_preset, logo_scale, cta_enabled, cta_preset, cta_scale ] load_js = ''' function() { let saved = localStorage.getItem('drippy4_settings'); if (saved) { try { let s = JSON.parse(saved); return [ s.mode !== undefined ? s.mode : "asr", s.mask !== undefined ? s.mask : "box", s.voice !== undefined ? s.voice : "vi-VN-NamMinhNeural", s.lang !== undefined ? s.lang : "zh", s.style !== undefined ? s.style : "white_bold", s.speed !== undefined ? s.speed : 1.15, s.vol !== undefined ? s.vol : 100, s.mute !== undefined ? s.mute : false, s.logo_e !== undefined ? s.logo_e : false, s.logo_p !== undefined ? s.logo_p : "bottom_right", s.logo_s !== undefined ? s.logo_s : 18.0, s.cta_e !== undefined ? s.cta_e : false, s.cta_p !== undefined ? s.cta_p : "bottom_center", s.cta_s !== undefined ? s.cta_s : 48.0 ]; } catch(e) {} } return ["asr", "box", "vi-VN-NamMinhNeural", "zh", "white_bold", 1.0, 100, false, false, "bottom_right", 18.0, false, "bottom_center", 48.0]; } ''' save_js = ''' function(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) { let settings = { mode: v1, mask: v2, voice: v3, lang: v4, style: v5, speed: v6, vol: v7, mute: v8, logo_e: v9, logo_p: v10, logo_s: v11, cta_e: v12, cta_p: v13, cta_s: v14 }; localStorage.setItem('drippy4_settings', JSON.stringify(settings)); return []; } ''' cropper_js = """ function() { window.cropperInstance = null; window.isCropperUpdating = false; // Foolproof watcher for base64 changes setInterval(() => { let tb = null; const container = document.querySelector('#preview-base64'); if (container) { if (container.shadowRoot) { tb = container.shadowRoot.querySelector('textarea') || container.shadowRoot.querySelector('input'); } else { tb = container.querySelector('textarea') || container.querySelector('input'); } } if (tb && tb.value && tb.value !== window.lastB64) { window.lastB64 = tb.value; window.initCropper(tb.value); } }, 500); // Watcher for Slider changes (to sync UI buttons back to Cropper) setInterval(() => { if (window.isCropperUpdating || !window.cropperInstance) return; let custom_y_el = null; const y_container = document.querySelector('#custom_y_slider'); if (y_container) { custom_y_el = y_container.shadowRoot ? y_container.shadowRoot.querySelector('input[type="range"]') || y_container.shadowRoot.querySelector('input') : y_container.querySelector('input[type="range"]') || y_container.querySelector('input'); } let custom_h_el = null; const h_container = document.querySelector('#custom_h_slider'); if (h_container) { custom_h_el = h_container.shadowRoot ? h_container.shadowRoot.querySelector('input[type="range"]') || h_container.shadowRoot.querySelector('input') : h_container.querySelector('input[type="range"]') || h_container.querySelector('input'); } if (custom_y_el && custom_h_el) { let y_val = parseFloat(custom_y_el.value); let h_val = parseFloat(custom_h_el.value); if (y_val !== window.lastSliderY || h_val !== window.lastSliderH) { window.lastSliderY = y_val; window.lastSliderH = h_val; window.updateCropperBox(y_val, h_val); } } }, 300); window.initCropper = function(b64_or_url) { const img = document.getElementById('cropper-img'); if (!img) return; const setupCropper = () => { if (window.cropperInstance) { window.cropperInstance.destroy(); } let custom_y_el = null; const y_container = document.querySelector('#custom_y_slider'); if (y_container) { custom_y_el = y_container.shadowRoot ? y_container.shadowRoot.querySelector('input[type="range"]') || y_container.shadowRoot.querySelector('input') : y_container.querySelector('input[type="range"]') || y_container.querySelector('input'); } let custom_h_el = null; const h_container = document.querySelector('#custom_h_slider'); if (h_container) { custom_h_el = h_container.shadowRoot ? h_container.shadowRoot.querySelector('input[type="range"]') || h_container.shadowRoot.querySelector('input') : h_container.querySelector('input[type="range"]') || h_container.querySelector('input'); } let init_y = custom_y_el ? parseFloat(custom_y_el.value) : 72; let init_h = custom_h_el ? parseFloat(custom_h_el.value) : 22; window.cropperInstance = new Cropper(img, { zoomable: false, viewMode: 1, autoCropArea: 1, background: false, ready() { const imgData = this.cropper.getImageData(); this.cropper.setData({ x: 0, y: (init_y / 100) * imgData.naturalHeight, width: imgData.naturalWidth, height: (init_h / 100) * imgData.naturalHeight }); }, cropend(event) { window.isCropperUpdating = true; const data = this.cropper.getData(true); const imgData = this.cropper.getImageData(); let y_pct = (data.y / imgData.naturalHeight) * 100; let h_pct = (data.height / imgData.naturalHeight) * 100; y_pct = Math.max(0, Math.min(100, Math.round(y_pct))); h_pct = Math.max(1, Math.min(100, Math.round(h_pct))); let coords_tb = null; const coords_container = document.querySelector('#cropper-coords'); if (coords_container) { coords_tb = coords_container.shadowRoot ? coords_container.shadowRoot.querySelector('textarea') || coords_container.shadowRoot.querySelector('input') : coords_container.querySelector('textarea') || coords_container.querySelector('input'); } if(coords_tb) { coords_tb.value = JSON.stringify({y: y_pct, h: h_pct, rand: Math.random()}); coords_tb.dispatchEvent(new Event('input', { bubbles: true })); } setTimeout(() => window.isCropperUpdating = false, 300); } }); }; if (img.src !== b64_or_url && b64_or_url) { img.onload = setupCropper; img.src = b64_or_url; } else if (b64_or_url) { setupCropper(); } }; window.updateCropperBox = function(y_pct, h_pct) { if (window.cropperInstance && !window.isCropperUpdating) { const imgData = window.cropperInstance.getImageData(); window.cropperInstance.setData({ x: 0, y: (y_pct / 100) * imgData.naturalHeight, width: imgData.naturalWidth, height: (h_pct / 100) * imgData.naturalHeight }); } } return []; } """ demo.load(fn=None, inputs=None, outputs=None, js=cropper_js) demo.load(fn=None, inputs=None, outputs=settings_comps, js=load_js) for comp in settings_comps: comp.change(fn=None, inputs=settings_comps, outputs=None, js=save_js) if __name__ == "__main__": demo.queue(max_size=20).launch( auth=verify_login, auth_message="🎬 TRUNG SÁNG VIỆT STUDIO • Vui lòng đăng nhập để sử dụng\n📱 Lưu ý: Hãy mở qua link https://hoangtaiii-drippy4.hf.space để đăng nhập mượt nhất trên điện thoại" )