| """ |
| ============================================================================= |
| 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 |
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| 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 = 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; } |
| } |
| """ |
|
|
| |
| |
| |
|
|
| def _default_session(): |
| return { |
| "logged_in": False, |
| "username": None, |
| "max_cookies": 0, |
| "label": "", |
| } |
|
|
| |
| |
| |
|
|
| def _mask_username(s: str) -> str: |
| if not s: |
| return "" |
| s = str(s) |
| if len(s) <= 2: |
| return "β’" * len(s) |
| |
| return s[0] + ("β’" * (len(s) - 3)) + s[-2:] |
|
|
|
|
| |
| |
| |
|
|
| 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( |
| '<span style="color:#22c55e;font-size:1em">β</span>' if i < n_cook |
| else '<span style="color:#d1d5db;font-size:1em">β</span>' |
| for i in range(max_c) |
| ) |
|
|
| return f""" |
| <div id="orbit-topbar"> |
| <div class="brand"> |
| πΈ <span>OrbitBot</span><span class="accent"> AI Studio</span> |
| <span class="pill">BETA</span> |
| </div> |
| <span class="stats">{usage}</span> |
| <span class="uinfo"> |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.85)" |
| stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> |
| <circle cx="12" cy="7" r="4"/> |
| </svg> |
| <b>{uname_disp}</b> |
| <span style="color:rgba(255,255,255,0.4)">Β·</span> |
| <span style="color:rgba(255,255,255,0.75)">{label}</span> |
| <span style="color:rgba(255,255,255,0.4)">Β·</span> |
| {dots} <span style="color:rgba(255,255,255,0.6);font-size:0.9em">({n_cook}/{max_c})</span> |
| </span> |
| </div> |
| """ |
|
|
|
|
| |
| |
| |
|
|
| with gr.Blocks( |
| title="OrbitBot AI Studio", |
| ) as demo: |
|
|
| sess = gr.State(_default_session()) |
| stop_flag = gr.State([False]) |
|
|
| |
| |
| |
| with gr.Column(visible=True) as login_panel: |
| gr.HTML(""" |
| <div id="login-wrap"> |
| <div id="login-logo"> |
| <span class="logo-icon">πΈ</span> |
| <h2>OrbitBot <span>AI Studio</span></h2> |
| <p>Masukkan kode akses Anda untuk melanjutkan</p> |
| </div> |
| </div> |
| """) |
| 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("") |
|
|
| |
| |
| |
| 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: |
|
|
| |
| |
| |
| with gr.Tab("π¬ Generate"): |
| with gr.Tabs() as gen_tabs: |
|
|
| |
| 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"], |
| ) |
|
|
| |
| with gr.Group(visible=True) as veo_t2v_panel: |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("<div class='section-label'>βοΈ Parameter</div>") |
| 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("<div class='section-label'>ποΈ Output</div>") |
| 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("<div class='log-indicator'><div class='dot'></div> LIVE LOG</div>") |
| vt_log = gr.Textbox( |
| label="Realtime Log", |
| lines=4, |
| interactive=False, |
| elem_classes=["orbit-log"], |
| ) |
|
|
| |
| with gr.Group(visible=False) as veo_i2v_panel: |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("<div class='section-label'>βοΈ Parameter</div>") |
| 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("<div class='section-label'>ποΈ Output</div>") |
| 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("<div class='log-indicator'><div class='dot'></div> LIVE LOG</div>") |
| 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], |
| ) |
|
|
| |
| 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"], |
| ) |
|
|
| |
| with gr.Group(visible=True) as img_t2i_panel: |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("<div class='section-label'>βοΈ Parameter</div>") |
| 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("<div class='section-label'>πΈ Hasil</div>") |
| 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("<div class='log-indicator'><div class='dot'></div> LIVE LOG</div>") |
| it_log = gr.Textbox( |
| label="Realtime Log", lines=4, interactive=False, |
| elem_classes=["orbit-log"], |
| ) |
|
|
| |
| with gr.Group(visible=False) as img_i2i_panel: |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("<div class='section-label'>βοΈ Parameter</div>") |
| 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("<div class='section-label'>πΈ Hasil</div>") |
| 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("<div class='log-indicator'><div class='dot'></div> LIVE LOG</div>") |
| 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], |
| ) |
|
|
| |
| |
| |
| 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") |
|
|
| |
| |
| |
| 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) |
| |
| |
| |
| |
| 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") |
| |
| 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("<div class='section-label'>πΊ Media Viewer</div>") |
| |
| 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"]) |
| |
| |
| riwayat_selected = gr.State({}) |
|
|
| |
| |
| |
|
|
| 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 |
| |
| |
| 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 |
|
|
| |
| riwayat_refresh_btn.click( |
| fn=update_history_table, |
| inputs=sess, |
| outputs=riwayat_table |
| ) |
|
|
| |
| 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] |
| ) |
|
|
| |
| 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: |
| |
| 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)" |
| |
| |
| 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)}" |
|
|
| |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| 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", |
| ) |
| ) |