""" ============================================================================= APP.PY — OrbitBot AI Studio (HuggingFace Spaces) ============================================================================= """ import os import sys import time import threading import importlib import importlib.util import requests import tempfile import json import gradio as gr from pathlib import Path # ============================================================================== # LOADER — inline (menggantikan loader.py) # ============================================================================== HF_TOKEN = os.environ.get("HF_TOKEN", "") DATASET_REPO = "malikrf22/abcx" RAW_BASE = f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main" _LOCAL_CORE = Path(tempfile.gettempdir()) / "geminicore_live.py" CORE_TTL = 60 REGISTRY_TTL = 30 _lock = threading.Lock() _core_module = None _core_fetched_at = 0.0 _registry_cache = {} _registry_fetched = 0.0 def _loader_raw_headers(): return {"Authorization": f"Bearer {HF_TOKEN}"} def _loader_fetch_text(path: str): url = f"{RAW_BASE}/{path}?raw=true&nocache={int(time.time())}" try: r = requests.get(url, headers=_loader_raw_headers(), timeout=20) if r.status_code == 200: return r.text print(f"[LOADER] fetch gagal {path}: HTTP {r.status_code}") except Exception as e: print(f"[LOADER] fetch error {path}: {e}") return None def _load_core_module(source_code: str): _LOCAL_CORE.write_text(source_code, encoding="utf-8") spec = importlib.util.spec_from_file_location("geminicore_live", str(_LOCAL_CORE)) mod = importlib.util.module_from_spec(spec) sys.modules["geminicore_live"] = mod spec.loader.exec_module(mod) return mod def _maybe_reload_core(force: bool = False): global _core_module, _core_fetched_at now = time.time() if not force and _core_module and (now - _core_fetched_at) < CORE_TTL: return _core_module with _lock: now = time.time() if not force and _core_module and (now - _core_fetched_at) < CORE_TTL: return _core_module print("[LOADER] Mengunduh geminicore.py dari dataset...") source = _loader_fetch_text("geminicore.py") if source: try: _core_module = _load_core_module(source) _core_fetched_at = time.time() print("[LOADER] geminicore.py berhasil di-load ✓") except Exception as e: print(f"[LOADER] ERROR saat load geminicore: {e}") if _core_module is None: raise RuntimeError(f"Gagal load geminicore: {e}") else: if _core_module is None: raise RuntimeError("Gagal unduh geminicore.py.") print("[LOADER] Gagal unduh, pakai versi sebelumnya.") return _core_module def get_core(): return _maybe_reload_core() def loader_call(func_name: str, *args, **kwargs): return getattr(get_core(), func_name)(*args, **kwargs) def loader_call_stream(func_name: str, *args, **kwargs): yield from getattr(get_core(), func_name)(*args, **kwargs) def get_user_registry(force: bool = False) -> dict: global _registry_cache, _registry_fetched now = time.time() if not force and _registry_cache and (now - _registry_fetched) < REGISTRY_TTL: return _registry_cache text = _loader_fetch_text("useropal.json") if text: try: data = json.loads(text) _registry_cache = data.get("users", {}) _registry_fetched = time.time() except Exception as e: print(f"[LOADER] Gagal parse useropal.json: {e}") return _registry_cache def authenticate_user(access_code: str): registry = get_user_registry() code = access_code.strip() if code in registry: info = dict(registry[code]) info["username"] = code return info return None # Init try: _maybe_reload_core(force=True) get_user_registry(force=True) print("[LOADER] Init selesai ✓") except Exception as e: print(f"[LOADER] Init GAGAL: {e}") # ============================================================================== # CSS — OrbitBot AI Studio Theme # ============================================================================== CSS = r""" @import url('https://fonts.googleapis.com/css2?family=Syne:wght@600;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); /* ══════════════════════════════════════════════ BASE ══════════════════════════════════════════════ */ :root { color-scheme: light dark; } body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background: #f5f4f1 !important; color: #1c1917 !important; font-size: 15px !important; line-height: 1.6 !important; } footer { display: none !important; } /* Full-width, padding-safe untuk mobile */ .gradio-container { max-width: 100% !important; width: 100% !important; padding: 10px 14px !important; box-sizing: border-box !important; overflow-x: hidden !important; } /* ══ Labels ══ */ label, .gr-form label, .block label { color: #44403c !important; font-size: 0.93em !important; font-weight: 600 !important; } /* ══ Input / Textarea / Select ══ FIX UTAMA: Jangan apply style "input umum" ke radio/checkbox/range, karena itu bikin radio jadi besar (width:100%, border, dll). */ input:not([type="radio"]):not([type="checkbox"]):not([type="range"]), textarea, select { background: #ffffff !important; border: 1.5px solid #d6d3ce !important; color: #1c1917 !important; font-size: 0.96em !important; border-radius: 10px !important; line-height: 1.65 !important; font-family: 'Plus Jakarta Sans', sans-serif !important; box-sizing: border-box !important; width: 100% !important; } input:not([type="radio"]):not([type="checkbox"]):not([type="range"]):focus, textarea:focus { border-color: #0d9488 !important; box-shadow: 0 0 0 3px rgba(13,148,136,0.12) !important; outline: none !important; } input::placeholder, textarea::placeholder { color: #a8a29e !important; } /* Radio/checkbox jangan full width */ input[type="radio"], input[type="checkbox"] { width: auto !important; } /* ══ Buttons ══ */ button, .gr-button { font-family: 'Plus Jakarta Sans', sans-serif !important; font-size: 0.95em !important; font-weight: 600 !important; border-radius: 10px !important; transition: all 0.18s ease !important; cursor: pointer !important; white-space: nowrap !important; } .gr-button.primary, button.primary { background: #0d9488 !important; border: none !important; color: #fff !important; box-shadow: 0 2px 10px rgba(13,148,136,0.22) !important; } .gr-button.primary:hover, button.primary:hover { background: #0f766e !important; transform: translateY(-1px) !important; box-shadow: 0 5px 16px rgba(13,148,136,0.32) !important; } .gr-button.stop, button.stop { background: #b91c1c !important; border: none !important; color: #fff !important; } .gr-button.stop:hover, button.stop:hover { background: #991b1b !important; } .gr-button:not(.primary):not(.stop), button:not(.primary):not(.stop) { background: #ffffff !important; border: 1.5px solid #d6d3ce !important; color: #44403c !important; } .gr-button:not(.primary):not(.stop):hover, button:not(.primary):not(.stop):hover { background: #f0fdf9 !important; border-color: #0d9488 !important; color: #0d9488 !important; } /* ══ Tabs ══ */ .tabs, .tab-nav { border-bottom: 2px solid #e7e5e0 !important; background: transparent !important; } .tab-nav button { color: #78716c !important; font-size: 0.93em !important; font-weight: 600 !important; font-family: 'Plus Jakarta Sans', sans-serif !important; padding: 9px 14px !important; white-space: nowrap !important; } .tab-nav button.selected { color: #0d9488 !important; border-bottom: 3px solid #0d9488 !important; background: transparent !important; } .tab-nav button:hover { color: #0d9488 !important; background: #f0fdf9 !important; } /* ══ Markdown ══ */ .gr-markdown, .gr-markdown p, .gr-markdown li { color: #292524 !important; font-size: 0.96em !important; line-height: 1.78 !important; } .gr-markdown h2 { color: #1c1917 !important; font-size: 1.2em !important; font-weight: 700 !important; margin-top: 1.2em !important; } .gr-markdown h3 { color: #292524 !important; font-size: 1.05em !important; font-weight: 700 !important; } .gr-markdown strong { color: #1c1917 !important; } .gr-markdown code { background: #e6f7f5 !important; color: #0f766e !important; border-radius: 5px !important; padding: 1px 6px !important; font-size: 0.88em !important; } .gr-markdown hr { border-color: #e7e5e0 !important; } /* ══ Panels / Groups ══ */ .gr-panel, .gr-group, .gr-box { background: #ffffff !important; border: 1.5px solid #e7e5e0 !important; border-radius: 14px !important; box-shadow: 0 1px 4px rgba(0,0,0,0.05) !important; } /* ══ Radio & Checkbox (umum) ══ */ .gr-radio span, .gr-checkbox span { color: #44403c !important; font-size: 0.95em !important; } .gr-slider input { accent-color: #0d9488 !important; } /* ══════════════════════════════════════════════ SUPER COMPACT RADIO (DOT) khusus Mode & Ratio ══════════════════════════════════════════════ */ .orbit-radio-dots .wrap, .orbit-radio-dots fieldset, .orbit-radio-dots .gr-radio { display: flex !important; flex-wrap: wrap !important; gap: 10px !important; } .orbit-radio-dots label { display: inline-flex !important; align-items: center !important; padding: 4px 8px !important; margin: 0 !important; border-radius: 999px !important; background: rgba(255,255,255,0.75) !important; border: 1px solid rgba(214,211,206,0.9) !important; box-shadow: none !important; } .orbit-radio-dots input[type="radio"]{ appearance: auto !important; -webkit-appearance: auto !important; width: 12px !important; height: 12px !important; margin: 0 6px 0 0 !important; accent-color: #0d9488 !important; transform: scale(0.9) !important; } .orbit-radio-dots span{ font-size: 0.86em !important; line-height: 1.1 !important; font-weight: 600 !important; } /* ══ Dataframe ══ */ .gr-dataframe { background: #fff !important; border: 1.5px solid #e7e5e0 !important; border-radius: 10px !important; overflow-x: auto !important; } .gr-dataframe th { background: #f0fdf9 !important; color: #0f766e !important; font-size: 0.92em !important; font-weight: 700 !important; } .gr-dataframe td { color: #292524 !important; border-color: #e7e5e0 !important; font-size: 0.92em !important; } /* ══ Chatbot ══ */ .gr-chatbot { background: #faf9f7 !important; border: 1.5px solid #e7e5e0 !important; border-radius: 14px !important; } .gr-chatbot .message { border-radius: 12px !important; font-size: 0.96em !important; line-height: 1.72 !important; } .gr-chatbot .message.user { background: #e6f7f5 !important; color: #134e4a !important; } .gr-chatbot .message.bot { background: #ffffff !important; color: #1c1917 !important; border: 1px solid #e7e5e0 !important; } /* ══════════════════════════════════════════════ TOPBAR — warm teal gradient, welcoming ══════════════════════════════════════════════ */ #orbit-topbar { background: linear-gradient(135deg, #134e4a 0%, #0d9488 60%, #14b8a6 100%); padding: 12px 18px; border-radius: 14px; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; box-shadow: 0 3px 16px rgba(13,148,136,0.22); position: relative; overflow: hidden; box-sizing: border-box; width: 100%; } #orbit-topbar::after { content: ''; position: absolute; top: -40px; right: -40px; width: 160px; height: 160px; background: radial-gradient(circle, rgba(255,255,255,0.10) 0%, transparent 65%); pointer-events: none; } #orbit-topbar .brand { color: #fff; font-family: 'Syne', sans-serif; font-size: 1.15em; font-weight: 800; display: flex; align-items: center; gap: 8px; flex-shrink: 0; } #orbit-topbar .brand span.accent { color: #99f6e4; } #orbit-topbar .pill { background: rgba(255,255,255,0.18); border: 1px solid rgba(255,255,255,0.32); color: #ccfbf1; border-radius: 20px; padding: 2px 9px; font-size: 0.68em; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; } #orbit-topbar .stats { color: rgba(255,255,255,0.78); font-size: 0.86em; } #orbit-topbar .uinfo { color: rgba(255,255,255,0.92); font-size: 0.86em; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-weight: 500; } #orbit-topbar .uinfo b { color: #fff; font-weight: 700; } /* ══════════════════════════════════════════════ LOGIN CARD ══════════════════════════════════════════════ */ #login-wrap { max-width: 420px; width: 100%; margin: 40px auto; background: #ffffff; border: 1.5px solid #e7e5e0; border-radius: 20px; padding: 40px 32px; box-shadow: 0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(13,148,136,0.06); position: relative; overflow: hidden; box-sizing: border-box; } #login-wrap::before { content: ''; position: absolute; top: -60px; right: -60px; width: 180px; height: 180px; background: radial-gradient(circle, rgba(13,148,136,0.07) 0%, transparent 70%); pointer-events: none; } #login-logo { text-align: center; margin-bottom: 28px; } #login-logo .logo-icon { font-size: 3em; display: block; margin-bottom: 10px; filter: drop-shadow(0 3px 10px rgba(13,148,136,0.28)); } #login-logo h2 { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 1.55em; color: #1c1917; margin: 0 0 6px; } #login-logo h2 span { color: #0d9488; } #login-logo p { color: #78716c; font-size: 0.92em; margin: 0; line-height: 1.55; } /* ══════════════════════════════════════════════ FIXED-SIZE PREVIEW BOXES ══════════════════════════════════════════════ */ .orbit-video-box video, .orbit-video-box .gr-video { width: 100% !important; height: 280px !important; object-fit: contain !important; background: #f5f4f1 !important; border-radius: 10px !important; border: 1.5px solid #e7e5e0 !important; } .orbit-gallery-box .gr-gallery, .orbit-gallery-box .gallery-container { height: 300px !important; overflow-y: auto !important; } .orbit-gallery-box .thumbnail-item img { object-fit: cover !important; border-radius: 8px !important; } /* ══════════════════════════════════════════════ ANIMATED LIVE LOG ══════════════════════════════════════════════ */ .orbit-log textarea { font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace !important; font-size: 0.88em !important; background: #0c1a16 !important; color: #5eead4 !important; border: 1.5px solid #134e4a !important; border-radius: 10px !important; line-height: 1.7 !important; padding: 12px 14px !important; scrollbar-width: thin !important; scrollbar-color: #1f4038 #0c1a16 !important; width: 100% !important; box-sizing: border-box !important; } .orbit-log label { color: #0d9488 !important; font-size: 0.82em !important; font-weight: 700 !important; letter-spacing: 0.07em !important; text-transform: uppercase !important; } .log-indicator { display: inline-flex; align-items: center; gap: 7px; color: #0d9488; font-size: 0.80em; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 4px; } .log-indicator .dot { width: 8px; height: 8px; border-radius: 50%; background: #14b8a6; box-shadow: 0 0 0 0 rgba(20,184,166,0.5); animation: logPulse 1.6s ease-in-out infinite; flex-shrink: 0; } @keyframes logPulse { 0% { box-shadow: 0 0 0 0 rgba(20,184,166,0.5); } 60% { box-shadow: 0 0 0 7px rgba(20,184,166,0); } 100% { box-shadow: 0 0 0 0 rgba(20,184,166,0); } } @keyframes logScan { 0% { background-position: 0 -100%; } 100% { background-position: 0 200%; } } .orbit-log.generating textarea { background: linear-gradient(180deg, transparent 0%, rgba(20,184,166,0.05) 50%, transparent 100%), #0c1a16 !important; background-size: 100% 36px !important; animation: logScan 2s linear infinite !important; color: #99f6e4 !important; } /* ══════════════════════════════════════════════ STATUS CARD ══════════════════════════════════════════════ */ .status-card { background: #f0fdf9; border: 1.5px solid #99f6e4; border-radius: 10px; padding: 9px 16px; font-size: 0.94em; color: #134e4a; font-weight: 500; box-sizing: border-box; width: 100%; } /* ══════════════════════════════════════════════ GENERATE BUTTON ══════════════════════════════════════════════ */ #generate-main-btn { background: #0d9488 !important; font-size: 1.02em !important; font-weight: 700 !important; padding: 13px 20px !important; border-radius: 11px !important; border: none !important; color: #fff !important; box-shadow: 0 3px 14px rgba(13,148,136,0.28) !important; transition: all 0.2s ease !important; width: 100% !important; } #generate-main-btn:hover { background: #0f766e !important; transform: translateY(-1px) !important; box-shadow: 0 6px 22px rgba(13,148,136,0.36) !important; } #generate-main-btn:active { transform: translateY(0) !important; } /* ══════════════════════════════════════════════ SECTION LABEL ══════════════════════════════════════════════ */ .section-label { color: #0d9488 !important; font-size: 0.72em !important; font-weight: 800 !important; letter-spacing: 0.13em !important; text-transform: uppercase !important; margin-bottom: 8px !important; padding-bottom: 5px !important; border-bottom: 2px solid #ccfbf1 !important; display: block !important; } /* ══════════════════════════════════════════════ GLOBAL BACKGROUND ala TOPBAR (seluruh app) ══════════════════════════════════════════════ */ html, body, .gradio-container { min-height: 100vh !important; } @media (prefers-color-scheme: light) { body, .gradio-container { background: radial-gradient(circle at 12% 10%, rgba(153,246,228,0.35) 0%, transparent 45%), radial-gradient(circle at 88% 20%, rgba(20,184,166,0.25) 0%, transparent 46%), linear-gradient(135deg, #134e4a 0%, #0d9488 55%, #14b8a6 100%) !important; background-attachment: fixed !important; color: #1c1917 !important; } .gr-panel, .gr-group, .gr-box { background: rgba(255,255,255,0.90) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; } } @media (prefers-color-scheme: dark) { body, .gradio-container { background: radial-gradient(circle at 12% 10%, rgba(20,184,166,0.20) 0%, transparent 45%), radial-gradient(circle at 88% 20%, rgba(153,246,228,0.10) 0%, transparent 46%), linear-gradient(135deg, #061b17 0%, #0b5f58 55%, #0f766e 100%) !important; background-attachment: fixed !important; color: #f3f4f6 !important; } label, .gr-form label, .block label { color: #e5e7eb !important; } .gr-markdown, .gr-markdown p, .gr-markdown li { color: #e5e7eb !important; } .gr-panel, .gr-group, .gr-box { background: rgba(17,24,39,0.72) !important; border-color: rgba(148,163,184,0.18) !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important; } input:not([type="radio"]):not([type="checkbox"]):not([type="range"]), textarea, select { background: rgba(255,255,255,0.06) !important; border-color: rgba(148,163,184,0.25) !important; color: #f3f4f6 !important; } .orbit-radio-dots label{ background: rgba(255,255,255,0.06) !important; border-color: rgba(148,163,184,0.25) !important; } } /* ══════════════════════════════════════════════ MOBILE RESPONSIVENESS ══════════════════════════════════════════════ */ @media (max-width: 768px) { body, .gradio-container { font-size: 14px !important; } .gradio-container { padding: 8px 10px !important; } .gr-row, [class*="row"] { flex-direction: column !important; flex-wrap: wrap !important; } .gr-row > *, [class*="row"] > * { width: 100% !important; min-width: 0 !important; max-width: 100% !important; flex: none !important; } #orbit-topbar { flex-direction: column !important; align-items: flex-start !important; padding: 12px 14px !important; gap: 6px !important; border-radius: 12px !important; } #orbit-topbar .brand { font-size: 1.05em !important; } #orbit-topbar .stats, #orbit-topbar .uinfo { font-size: 0.82em !important; } #login-wrap { margin: 16px auto !important; padding: 28px 20px !important; border-radius: 16px !important; } #login-logo h2 { font-size: 1.35em !important; } .orbit-video-box video, .orbit-video-box .gr-video { height: 220px !important; } .orbit-gallery-box .gr-gallery, .orbit-gallery-box .gallery-container { height: 240px !important; } .tab-nav { overflow-x: auto !important; -webkit-overflow-scrolling: touch !important; display: flex !important; flex-wrap: nowrap !important; scrollbar-width: none !important; } .tab-nav::-webkit-scrollbar { display: none !important; } .tab-nav button { padding: 8px 12px !important; font-size: 0.88em !important; flex-shrink: 0 !important; } .orbit-log textarea { font-size: 0.82em !important; } .gr-row .gr-textbox, .gr-row .gr-button { width: 100% !important; } } @media (max-width: 480px) { .gradio-container { padding: 6px 8px !important; } #login-wrap { padding: 22px 16px !important; } #orbit-topbar { padding: 10px 12px !important; border-radius: 10px !important; } .tab-nav button { padding: 7px 10px !important; font-size: 0.84em !important; } .orbit-video-box video, .orbit-video-box .gr-video { height: 190px !important; } } """ # ============================================================================== # SESSION DEFAULT # ============================================================================== def _default_session(): return { "logged_in": False, "username": None, "max_cookies": 0, "label": "", } # ============================================================================== # MASK USERNAME (sensor tampilan) # ============================================================================== def _mask_username(s: str) -> str: if not s: return "" s = str(s) if len(s) <= 2: return "•" * len(s) # contoh: mmm123 -> m•••23 return s[0] + ("•" * (len(s) - 3)) + s[-2:] # ============================================================================== # TOPBAR HTML # ============================================================================== def _topbar_html(session: dict) -> str: if not session.get("logged_in"): return "" uname = session["username"] uname_disp = _mask_username(uname) max_c = session.get("max_cookies", 0) label = session.get("label", "") try: usage = loader_call("get_usage_summary", uname) udata = loader_call("load_user_data", uname) n_cook = len(udata.get("cookies", [])) except Exception: usage, n_cook = "—", 0 dots = "".join( '' if i < n_cook else '' for i in range(max_c) ) return f"""
🛸 OrbitBot AI Studio BETA
{usage} {uname_disp} · {label} · {dots} ({n_cook}/{max_c})
""" # ============================================================================== # GRADIO APP # ============================================================================== with gr.Blocks( title="OrbitBot AI Studio", ) as demo: sess = gr.State(_default_session()) stop_flag = gr.State([False]) # ========================================================= # LOGIN PANEL # ========================================================= with gr.Column(visible=True) as login_panel: gr.HTML("""
""") login_code = gr.Textbox( label="Kode Akses", placeholder="Masukkan kode akses...", max_lines=1, type="password", ) login_btn = gr.Button("🚀 Masuk", variant="primary", size="lg") login_err = gr.Markdown("") # ========================================================= # MAIN PANEL # ========================================================= with gr.Column(visible=False) as main_panel: with gr.Row(): with gr.Column(scale=5): pass with gr.Column(scale=1, min_width=120): logout_btn = gr.Button("🚪 Logout", size="sm") topbar = gr.HTML("") with gr.Tabs() as tabs: # ------------------------------------------------- # TAB 1 — Generate # ------------------------------------------------- with gr.Tab("🎬 Generate"): with gr.Tabs() as gen_tabs: # ── Sub-tab: VEO 3.1 Video ────────────── with gr.Tab("📹 VEO 3.1 Video"): generation_mode_veo = gr.Radio( choices=["Text to Video", "Image to Video"], value="Text to Video", label="🎯 Mode", elem_classes=["orbit-radio-dots"], ) # ---- PANEL: Text to Video ---- with gr.Group(visible=True) as veo_t2v_panel: with gr.Row(): with gr.Column(scale=1): gr.Markdown("
⚙️ Parameter
") vt_prompt = gr.Textbox( label="Prompt", placeholder="Deskripsikan video yang ingin dibuat...", lines=5, ) vt_ratio = gr.Radio( choices=["9:16 (Vertical)", "16:9 (Horizontal)"], value="9:16 (Vertical)", label="📐 Aspect Ratio", elem_classes=["orbit-radio-dots"], ) vt_btn = gr.Button( "▶ Generate Video", variant="primary", elem_id="generate-main-btn", ) vt_stop = gr.Button("⏹ Stop", variant="stop", elem_id="stop-btn") with gr.Column(scale=1): gr.Markdown("
🎞️ Output
") vt_out = gr.Video(label="Hasil Video", height=300, elem_classes=["orbit-video-box"]) vt_status = gr.Textbox(label="Status", lines=2, interactive=False) gr.HTML("
LIVE LOG
") vt_log = gr.Textbox( label="Realtime Log", lines=4, interactive=False, elem_classes=["orbit-log"], ) # ---- PANEL: Image to Video ---- with gr.Group(visible=False) as veo_i2v_panel: with gr.Row(): with gr.Column(scale=1): gr.Markdown("
⚙️ Parameter
") with gr.Row(): vi_image = gr.Image(type="pil", label="🖼 First Frame", height=180) vi_last_image = gr.Image(type="pil", label="🖼 Last Frame (Opsional)", height=180) vi_prompt = gr.Textbox( label="Prompt", placeholder="Deskripsikan gerakan yang diinginkan...", lines=4, ) vi_ratio = gr.Radio( choices=["9:16 (Vertical)", "16:9 (Horizontal)"], value="9:16 (Vertical)", label="📐 Aspect Ratio", elem_classes=["orbit-radio-dots"], ) vi_btn = gr.Button( "▶ Generate Video", variant="primary", elem_id="generate-main-btn", ) vi_stop = gr.Button("⏹ Stop", variant="stop", elem_id="stop-btn") with gr.Column(scale=1): gr.Markdown("
🎞️ Output
") vi_out = gr.Video(label="Hasil Video", height=300, elem_classes=["orbit-video-box"]) vi_status = gr.Textbox(label="Status", lines=2, interactive=False) gr.HTML("
LIVE LOG
") vi_log = gr.Textbox( label="Realtime Log", lines=4, interactive=False, elem_classes=["orbit-log"], ) def _toggle_veo_mode(mode): return ( gr.update(visible=mode == "Text to Video"), gr.update(visible=mode == "Image to Video"), ) generation_mode_veo.change( fn=_toggle_veo_mode, inputs=generation_mode_veo, outputs=[veo_t2v_panel, veo_i2v_panel], ) # ── Sub-tab: Image Generation ──────────── with gr.Tab("🖼 Image Generation"): generation_mode_img = gr.Radio( choices=["Text to Image", "Image to Image"], value="Text to Image", label="🎯 Mode", elem_classes=["orbit-radio-dots"], ) # ---- PANEL: Text to Image ---- with gr.Group(visible=True) as img_t2i_panel: with gr.Row(): with gr.Column(scale=1): gr.Markdown("
⚙️ Parameter
") it_prompt = gr.Textbox( label="Prompt", placeholder="Deskripsikan gambar yang ingin dibuat...", lines=5, ) it_ratio = gr.Radio( choices=["9:16 (Vertical)", "16:9 (Horizontal)", "1:1 (Square)"], value="1:1 (Square)", label="📐 Aspect Ratio", elem_classes=["orbit-radio-dots"], ) it_count = gr.Slider(minimum=1, maximum=3, value=1, step=1, label="🔢 Jumlah Foto") with gr.Row(): it_btn = gr.Button("🎨 Generate", variant="primary", elem_id="generate-main-btn", scale=3) it_stop = gr.Button("⏹ Stop", variant="stop", scale=1) with gr.Column(scale=2): gr.Markdown("
📸 Hasil
") it_gallery = gr.Gallery( label="Hasil Foto", columns=3, height=260, elem_classes=["orbit-gallery-box"], object_fit="cover", ) it_status = gr.Textbox(label="Status", lines=2, interactive=False) gr.HTML("
LIVE LOG
") it_log = gr.Textbox( label="Realtime Log", lines=4, interactive=False, elem_classes=["orbit-log"], ) # ---- PANEL: Image to Image ---- with gr.Group(visible=False) as img_i2i_panel: with gr.Row(): with gr.Column(scale=1): gr.Markdown("
⚙️ Parameter
") ii_image = gr.Image(type="pil", label="🖼 Gambar Referensi", height=180) ii_prompt = gr.Textbox( label="Prompt", placeholder="Deskripsikan modifikasi yang diinginkan...", lines=4, ) ii_ratio = gr.Radio( choices=["9:16 (Vertical)", "16:9 (Horizontal)", "1:1 (Square)"], value="1:1 (Square)", label="📐 Aspect Ratio", elem_classes=["orbit-radio-dots"], ) ii_count = gr.Slider(minimum=1, maximum=3, value=1, step=1, label="🔢 Jumlah Foto") with gr.Row(): ii_btn = gr.Button("🎨 Generate", variant="primary", elem_id="generate-main-btn", scale=3) ii_stop = gr.Button("⏹ Stop", variant="stop", scale=1) with gr.Column(scale=2): gr.Markdown("
📸 Hasil
") ii_gallery = gr.Gallery( label="Hasil Foto", columns=3, height=260, elem_classes=["orbit-gallery-box"], object_fit="cover", ) ii_status = gr.Textbox(label="Status", lines=2, interactive=False) gr.HTML("
LIVE LOG
") ii_log = gr.Textbox( label="Realtime Log", lines=4, interactive=False, elem_classes=["orbit-log"], ) def _toggle_img_mode(mode): return ( gr.update(visible=mode == "Text to Image"), gr.update(visible=mode == "Image to Image"), ) generation_mode_img.change( fn=_toggle_img_mode, inputs=generation_mode_img, outputs=[img_t2i_panel, img_i2i_panel], ) # ------------------------------------------------- # TAB 2 — Gemini Chat # ------------------------------------------------- with gr.Tab("💬 AI Chat"): chatbot = gr.Chatbot(label="Percakapan", height=460) chat_stat = gr.Markdown( "💬 0 turn  ·  👤 0 karakter  ·  🤖 0 karakter", elem_classes=["status-card"], ) with gr.Row(): chat_input = gr.Textbox( label="", placeholder="Ketik pesan lalu Enter atau klik Kirim...", lines=2, scale=5, show_label=False, ) chat_send = gr.Button("📤 Kirim", variant="primary", scale=1) with gr.Row(): chat_clear = gr.Button("🗑️ Hapus Chat", variant="stop", size="sm") chat_regen = gr.Button("🔄 Regenerate", size="sm") # ------------------------------------------------- # TAB 3 — Pengaturan # ------------------------------------------------- with gr.Tab("⚙️ Pengaturan"): with gr.Group(): gr.Markdown("### 🔑 Manajemen Cookie Akun") gr.Markdown( "_Masukkan `GEMINI_REFRESH_COOKIE` dari browser. " "Setelah disimpan, cookie **dikunci 7 hari** dan tidak bisa diganti._" ) sett_info = gr.Markdown("") with gr.Row(): sett_cookie = gr.Textbox( label="GEMINI_REFRESH_COOKIE", placeholder="OAuthRefreshToken=1//...", type="password", lines=2, scale=4, ) sett_save = gr.Button("💾 Simpan Cookie", variant="primary", scale=1) sett_result = gr.Textbox(label="Hasil", lines=2, interactive=False) gr.Markdown("") with gr.Group(): gr.Markdown("### 🌐 Pengaturan Proxy") with gr.Row(): sett_proxy = gr.Textbox( label="Proxy URL", placeholder="http://user:pass@host:port", scale=4, ) sett_use_proxy = gr.Checkbox(label="Aktifkan Proxy", value=False, scale=1) sett_proxy_save = gr.Button("💾 Simpan", scale=1) sett_proxy_result = gr.Textbox(label="Status Proxy", lines=1, interactive=False) gr.Markdown("") with gr.Group(): gr.Markdown("### 📋 Status Cookie & Slot") sett_status_btn = gr.Button("🔄 Refresh Status", size="sm") sett_status_tbl = gr.Dataframe( headers=["No", "Tersimpan", "Dikunci Hingga", "Status"], label="Daftar Cookie", interactive=False, ) gr.Markdown("") with gr.Group(): gr.Markdown("### ⚡ Reload Modul") gr.Markdown( "_Paksa unduh ulang `geminicore.py` dan `useropal.json` dari dataset. " "Biasanya otomatis setiap 60 detik._" ) reload_btn = gr.Button("⚡ Reload Sekarang", size="sm") reload_status = gr.Textbox(label="Status Reload", lines=1, interactive=False) # ------------------------------------------------- # TAB — Riwayat Generate # ------------------------------------------------- with gr.Tab("📜 Riwayat"): gr.Markdown("### 📜 Riwayat Generate 24 Jam Terakhir") with gr.Row(): with gr.Column(scale=2): riwayat_refresh_btn = gr.Button("🔄 Refresh Data Tabel", size="sm") # Kita gunakan Tabel agar bisa di-klik barisnya riwayat_table = gr.Dataframe( headers=["Waktu", "Tipe", "Prompt", "URL ID"], interactive=False, wrap=True, type="array" ) riwayat_load_btn = gr.Button("⬇️ Load Media Terpilih", variant="primary", interactive=False) riwayat_status = gr.Textbox(label="Status Load", interactive=False, lines=1) with gr.Column(scale=1): gr.Markdown("
📺 Media Viewer
") # Tempat untuk memunculkan media yang diload ulang riwayat_video = gr.Video(label="Video", visible=False, elem_classes=["orbit-video-box"]) riwayat_image = gr.Image(label="Gambar", visible=False, elem_classes=["orbit-gallery-box"]) # Menyimpan state baris yang sedang diklik riwayat_selected = gr.State({}) # ========================================================= # EVENT HANDLERS # ========================================================= def do_login(code, current_sess): user = authenticate_user(code) if not user: return ( current_sess, "❌ Kode akses tidak valid atau tidak ditemukan.", gr.update(visible=True), gr.update(visible=False), "", ) new_sess = { "logged_in": True, "username": user["username"], "max_cookies": user.get("max_cookies", 1), "label": user.get("label", ""), } return ( new_sess, "", gr.update(visible=False), gr.update(visible=True), _topbar_html(new_sess), ) login_btn.click( fn=do_login, inputs=[login_code, sess], outputs=[sess, login_err, login_panel, main_panel, topbar], ) login_code.submit( fn=do_login, inputs=[login_code, sess], outputs=[sess, login_err, login_panel, main_panel, topbar], ) def do_logout(): return ( _default_session(), gr.update(visible=True), gr.update(visible=False), "", ) logout_btn.click( fn=do_logout, outputs=[sess, login_panel, main_panel, topbar], ) def _set_stop(flag, val): flag[0] = val return flag def do_vt(prompt, ratio, session, flag): if not session.get("logged_in"): yield None, "❌ Silakan login.", "" return if not prompt.strip(): yield None, "❌ Prompt tidak boleh kosong.", "" return flag[0] = False for path, msg, log_txt in loader_call_stream( "generate_video_stream", session["username"], prompt, ratio, stop_flag=flag ): yield path, msg, log_txt def do_vi(image, last_image, prompt, ratio, session, flag): if not session.get("logged_in"): yield None, "❌ Silakan login.", "" return if image is None: yield None, "❌ Silakan upload First Frame.", "" return if not prompt.strip(): yield None, "❌ Prompt tidak boleh kosong.", "" return flag[0] = False # Tambahkan last_image=last_image ke loader_call_stream for path, msg, log_txt in loader_call_stream( "generate_video_stream", session["username"], prompt, ratio, input_image=image, last_image=last_image, stop_flag=flag ): yield path, msg, log_txt vt_btn.click(fn=do_vt, inputs=[vt_prompt, vt_ratio, sess, stop_flag], outputs=[vt_out, vt_status, vt_log]) vt_stop.click(fn=lambda f: (f.__setitem__(0, True) or f), inputs=[stop_flag], outputs=[stop_flag]) vi_btn.click( fn=do_vi, inputs=[vi_image, vi_last_image, vi_prompt, vi_ratio, sess, stop_flag], outputs=[vi_out, vi_status, vi_log] ) vi_stop.click(fn=lambda f: (f.__setitem__(0, True) or f), inputs=[stop_flag], outputs=[stop_flag]) def do_it(prompt, ratio, count, session, flag): if not session.get("logged_in"): yield [], "❌ Silakan login.", "" return if not prompt.strip(): yield [], "❌ Prompt tidak boleh kosong.", "" return flag[0] = False for imgs, msg, log_txt in loader_call_stream( "generate_images_stream", session["username"], prompt, ratio, int(count), stop_flag=flag ): yield imgs, msg, log_txt def do_ii(image, prompt, ratio, count, session, flag): if not session.get("logged_in"): yield [], "❌ Silakan login.", "" return if image is None: yield [], "❌ Silakan upload gambar referensi.", "" return if not prompt.strip(): yield [], "❌ Prompt tidak boleh kosong.", "" return flag[0] = False for imgs, msg, log_txt in loader_call_stream( "generate_images_stream", session["username"], prompt, ratio, int(count), input_image=image, stop_flag=flag ): yield imgs, msg, log_txt it_btn.click(fn=do_it, inputs=[it_prompt, it_ratio, it_count, sess, stop_flag], outputs=[it_gallery, it_status, it_log]) it_stop.click(fn=lambda f: (f.__setitem__(0, True) or f), inputs=[stop_flag], outputs=[stop_flag]) ii_btn.click(fn=do_ii, inputs=[ii_image, ii_prompt, ii_ratio, ii_count, sess, stop_flag], outputs=[ii_gallery, ii_status, ii_log]) ii_stop.click(fn=lambda f: (f.__setitem__(0, True) or f), inputs=[stop_flag], outputs=[stop_flag]) def user_submit_chat(msg, history, session): if not session.get("logged_in"): history = history or [] history.append({"role": "assistant", "content": "❌ Silakan login terlebih dahulu."}) return "", history if not msg.strip(): return "", history or [] history = history or [] history.append({"role": "user", "content": msg.strip()}) return "", history def bot_respond_chat(history, session): if not history: yield history return last = history[-1] if last.get("role") != "user": yield history return uname = session.get("username", "") user_txt = last["content"] before = history[:-1] history.append({"role": "assistant", "content": "⏳ Sedang memproses..."}) yield history reply, err = loader_call("chat_gemini", uname, user_txt, before) history[-1]["content"] = reply if not err else f"❌ {err}" yield history def chat_stats_fn(history): if not history: return "💬 0 turn  ·  👤 0 karakter  ·  🤖 0 karakter" users = [m for m in history if m.get("role") == "user"] bots = [m for m in history if m.get("role") == "assistant"] uc = sum(len(m.get("content", "")) for m in users) bc = sum(len(m.get("content", "")) for m in bots) return ( f"💬 {len(users)} turn  ·  " f"👤 {uc} karakter  ·  " f"🤖 {bc} karakter" ) def regen_chat(history, session): if history and history[-1].get("role") == "assistant": history = history[:-1] yield from bot_respond_chat(history, session) ( chat_send.click( fn=user_submit_chat, inputs=[chat_input, chatbot, sess], outputs=[chat_input, chatbot], queue=False, ) .then(bot_respond_chat, inputs=[chatbot, sess], outputs=chatbot) .then(chat_stats_fn, inputs=chatbot, outputs=chat_stat) ) ( chat_input.submit( fn=user_submit_chat, inputs=[chat_input, chatbot, sess], outputs=[chat_input, chatbot], queue=False, ) .then(bot_respond_chat, inputs=[chatbot, sess], outputs=chatbot) .then(chat_stats_fn, inputs=chatbot, outputs=chat_stat) ) chat_clear.click( fn=lambda: ([], "💬 0 turn  ·  👤 0 karakter  ·  🤖 0 karakter"), outputs=[chatbot, chat_stat], ) chat_regen.click(fn=regen_chat, inputs=[chatbot, sess], outputs=chatbot).then( chat_stats_fn, inputs=chatbot, outputs=chat_stat ) def load_sett_info(session): if not session.get("logged_in"): return "Belum login." udata = loader_call("load_user_data", session["username"]) n = len(udata.get("cookies", [])) max_c = session.get("max_cookies", 0) label = session.get("label", "-") return f"**Slot cookie:** {n}/{max_c} terisi  ·  Paket: **{label}**" def load_proxy_settings(session): if not session.get("logged_in"): return "", False data = loader_call("load_user_data", session["username"]) return data.get("proxy", ""), data.get("use_proxy", False) tabs.select(fn=load_sett_info, inputs=sess, outputs=sett_info) tabs.select(fn=load_proxy_settings, inputs=sess, outputs=[sett_proxy, sett_use_proxy]) def save_cookie_fn(cookie_val, session): if not session.get("logged_in"): return "❌ Silakan login." if not cookie_val.strip(): return "❌ Cookie tidak boleh kosong." data = loader_call("load_user_data", session["username"]) proxy = data.get("proxy", "") if data.get("use_proxy", False) else None ok, msg = loader_call( "validate_and_save_cookie", session["username"], cookie_val, session.get("max_cookies", 0), proxy=proxy, use_proxy=data.get("use_proxy", False), ) return msg sett_save.click(fn=save_cookie_fn, inputs=[sett_cookie, sess], outputs=sett_result) def save_proxy_fn(proxy_val, use_proxy, session): if not session.get("logged_in"): return "❌ Silakan login." ok = loader_call("save_proxy_settings", session["username"], proxy_val, use_proxy) return "✅ Proxy disimpan." if ok else "❌ Gagal menyimpan proxy." sett_proxy_save.click( fn=save_proxy_fn, inputs=[sett_proxy, sett_use_proxy, sess], outputs=sett_proxy_result, ) def refresh_cookie_status(session): if not session.get("logged_in"): return [] data = loader_call("load_user_data", session["username"]) cookies = data.get("cookies", []) rows = [] for i, c in enumerate(cookies): locked = loader_call("is_cookie_locked", c) until = loader_call("cookie_unlock_date", c) status = "🔒 Terkunci" if locked else "🔓 Bisa diganti" saved = c.get("saved_at", "-")[:16].replace("T", " ") rows.append([i + 1, saved, until, status]) return rows sett_status_btn.click(fn=refresh_cookie_status, inputs=sess, outputs=sett_status_tbl) def update_history_table(session): if not session.get("logged_in"): return [["", "Belum Login", "", ""]] history = loader_call("get_history_24h", session["username"]) if not history: return [["", "Kosong", "Tidak ada riwayat 24 jam terakhir", ""]] rows = [] for h in reversed(history): at_str = h.get("at", "")[:16].replace("T", " ") kind = h.get("kind", "") url = h.get("url", "") prompt = h.get("prompt", "")[:100] icon = "📹 Video" if kind == "video" else "🖼️ Gambar" rows.append([at_str, icon, prompt, url]) return rows # Saat tombol refresh di-klik, perbarui isi tabel riwayat_refresh_btn.click( fn=update_history_table, inputs=sess, outputs=riwayat_table ) # Saat user mengklik baris pada tabel def on_select_table(evt: gr.SelectData, current_table): row_idx = evt.index[0] if current_table[row_idx][1] in ["Belum Login", "Kosong"]: return {}, "❌ Pilih data yang valid", gr.update(interactive=False) kind = "video" if "Video" in current_table[row_idx][1] else "image" url = current_table[row_idx][3] return {"kind": kind, "url": url}, f"Pilihan: {kind} (Siap di-load)", gr.update(interactive=True) riwayat_table.select( fn=on_select_table, inputs=riwayat_table, outputs=[riwayat_selected, riwayat_status, riwayat_load_btn] ) # Mengeksekusi fetch (unduh) dari Google Opal pake cookie user def fetch_media_from_history(sel_state, session): if not sel_state.get("url"): return gr.update(), gr.update(), "❌ Gagal: Tidak ada link yang dipilih." url = sel_state["url"] kind = sel_state["kind"] blob_id = url.split("/")[-1] try: # Menggunakan _fetch_blob dari geminicore (ini otomatis memakai cookie user yang tersimpan) data_bytes = loader_call("_fetch_blob", blob_id, session["username"]) if not data_bytes: return gr.update(), gr.update(), "❌ Gagal memuat file. (Cookie mati / Limit)" # Simpan bytes ke file sementara (Temp file) agar Gradio bisa membacanya ext = ".mp4" if kind == "video" else ".jpg" import tempfile tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext) tmp.write(data_bytes) tmp.close() if kind == "video": return gr.update(value=tmp.name, visible=True), gr.update(visible=False), "✅ Video berhasil dimuat!" else: return gr.update(visible=False), gr.update(value=tmp.name, visible=True), "✅ Gambar berhasil dimuat!" except Exception as e: return gr.update(), gr.update(), f"❌ Error: {str(e)}" # Saat tombol Load diklik riwayat_load_btn.click( fn=fetch_media_from_history, inputs=[riwayat_selected, sess], outputs=[riwayat_video, riwayat_image, riwayat_status] ) def force_reload(session): try: _maybe_reload_core(force=True) get_user_registry(force=True) return "✅ Core module & registry berhasil di-reload dari dataset." except Exception as e: return f"❌ Gagal reload: {e}" reload_btn.click(fn=force_reload, inputs=sess, outputs=reload_status) # ============================================================================== # LAUNCH # ============================================================================== if __name__ == "__main__": demo.queue( max_size=200, default_concurrency_limit=50, ).launch( server_name="0.0.0.0", server_port=7860, show_error=True, max_threads=200, css=CSS, theme=gr.themes.Soft( primary_hue="sky", secondary_hue="indigo", neutral_hue="slate", ) )