xbi / app.py
malikrf22's picture
Update app.py
e25778d verified
"""
=============================================================================
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(
'<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">&nbsp;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>
"""
# ==============================================================================
# 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("""
<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("")
# =========================================================
# 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("<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"],
)
# ---- PANEL: Image to Video ----
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],
)
# ── 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("<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"],
)
# ---- PANEL: Image to Image ----
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],
)
# -------------------------------------------------
# TAB 2 β€” Gemini Chat
# -------------------------------------------------
with gr.Tab("πŸ’¬ AI Chat"):
chatbot = gr.Chatbot(label="Percakapan", height=460)
chat_stat = gr.Markdown(
"πŸ’¬ 0 turn &nbsp;Β·&nbsp; πŸ‘€ 0 karakter &nbsp;Β·&nbsp; πŸ€– 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("<div class='section-label'>πŸ“Ί Media Viewer</div>")
# 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 &nbsp;Β·&nbsp; πŸ‘€ 0 karakter &nbsp;Β·&nbsp; πŸ€– 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 &nbsp;Β·&nbsp; "
f"πŸ‘€ {uc} karakter &nbsp;Β·&nbsp; "
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 &nbsp;Β·&nbsp; πŸ‘€ 0 karakter &nbsp;Β·&nbsp; πŸ€– 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 &nbsp;Β·&nbsp; 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",
)
)