""" ============================================================================= 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"""
Masukkan kode akses Anda untuk melanjutkan