toolframe2frame / app.py
tamhonvotri's picture
update
a6ce189
import os, time
import gradio as gr
from runninghub_backend import (
start_runs, poll_many, call_outputs, download_urls_as_files
)
# ====== Brand & Access Config (ENV) ======
APP_PASSCODE = os.getenv("APP_PASSCODE", "").strip() # required to unlock app
BRAND_TITLE = os.getenv("BRAND_TITLE", "Tâm hồn vô tri").strip() # title text
AVATAR_URL = os.getenv("AVATAR_URL", "").strip() # avatar image url
# Up to 3 clickable banners (URL + HREF)
BANNER1_URL = os.getenv("BANNER1_URL", "").strip()
BANNER1_HREF = os.getenv("BANNER1_HREF", "").strip()
BANNER2_URL = os.getenv("BANNER2_URL", "").strip()
BANNER2_HREF = os.getenv("BANNER2_HREF", "").strip()
BANNER3_URL = os.getenv("BANNER3_URL", "").strip()
BANNER3_HREF = os.getenv("BANNER3_HREF", "").strip()
# ====== Theme colors (TCI-ish) ======
PRIMARY_BG = "#0a0b0e" # dark background
CARD_BG = "#0f1115" # card background
BORDER = "#1a1f2b" # borders
TEXT = "#e5e7eb" # text
MUTED = "#9ca3af" # muted text — greenish
ACCENT = "#34d399" # emerald accent
ACCENT_2 = "#22d3ee" # cyan accent
CSS = f"""
:root {{ --bg:{PRIMARY_BG}; --card:{CARD_BG}; --border:{BORDER}; --text:{TEXT}; --muted:{MUTED}; --accent:{ACCENT}; --accent2:{ACCENT_2}; }}
* {{ box-sizing: border-box; }}
html, body {{ background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }}
body {{ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }}
.container {{ max-width: 1120px; margin: 0 auto; padding: 16px; }}
.row {{ display:flex; gap:16px; align-items:stretch; }}
.col {{ display:flex; flex-direction:column; gap:16px; }}
.card {{ background: var(--card); border:1px solid var(--border); border-radius:16px; padding:16px; box-shadow: 0 10px 30px rgba(0,0,0,.25); }}
.card h3 {{ margin:0 0 8px; font-weight:800; letter-spacing:.2px; }}
.card p {{ margin:0; color: var(--muted); }}
.header {{ display:flex; align-items:center; gap:14px; padding:8px 4px 0; }}
.brand {{ display:flex; flex-direction:column; gap:2px; }}
.brand .title {{ font-size:20px; font-weight:900; letter-spacing:.3px; }}
.brand .subtitle {{ font-size:13px; color:var(--muted); }}
.avatar {{ width:56px; height:56px; border-radius:999px; overflow:hidden; background:rgba(52,211,153,.2); display:flex; align-items:center; justify-content:center; }}
.avatar img {{ width:100%; height:100%; object-fit:cover; display:block; }}
.banners {{ display:grid; grid-template-columns: 1fr; gap:16px; }}
@media (min-width: 900px) {{ .banners {{ grid-template-columns: repeat(3, 1fr); }} }}
.banner {{ position:relative; width:100%; aspect-ratio: 21/9; border-radius:16px; overflow:hidden; border:1px solid var(--border);
background:linear-gradient(135deg, rgba(52,211,153,.08), rgba(34,211,238,.08)); display:block; }}
.banner img {{ width:100%; height:100%; object-fit:cover; display:block; filter:saturate(1.05) contrast(1.02); }}
label, .gr-textbox label, .gr-slider label, .gr-image label, .gr-files label {{ color: var(--text) !important; font-weight:600 !important; }}
.gr-textbox textarea, .gr-textbox input, .gr-number input {{ background:#0b0d12 !important; border:1px solid var(--border) !important; border-radius:12px !important; }}
.gr-image {{ background:#0b0d12 !important; border:1px solid var(--border) !important; border-radius:12px !important; }}
.gr-button.primary {{ background: linear-gradient(135deg, var(--accent), var(--accent2)); border:none !important; color:#00221e !important; font-weight:800; border-radius:12px !important; box-shadow:0 8px 30px rgba(34,211,238,.25); }}
.gr-button.primary:hover {{ filter:brightness(1.03); transform: translateY(-1px); }}
#gallery {{ min-height: 560px; }}
#links {{ min-height: 120px; }}
#files {{ min-height: 120px; }}
#gallery .grid-wrap {{ width:100%; }}
.help {{ color:var(--muted); font-size:14px; line-height:1.5; }}
"""
def _banners_html(items):
out = []
for url, href in items:
if not url:
continue
href = href or "#"
out.append(f'<a class="banner" href="{href}" target="_blank" rel="noopener"><img src="{url}" alt="banner"/></a>')
return f'<div class="banners">{"".join(out)}</div>' if out else ""
def _is_image_url(u: str) -> bool:
return isinstance(u, str) and any(u.lower().endswith(ext) for ext in (".png",".jpg",".jpeg",".webp",".gif"))
def _stream_run(start_image_path, end_image_path, qty, text_input):
if not APP_PASSCODE:
yield [], "**LỖI cấu hình**: Thiếu APP_PASSCODE trong Variables của Space.", "", None
return
if not start_image_path:
yield [], "Vui lòng chọn **Ảnh đầu**.", "", None; return
if not end_image_path:
yield [], "Vui lòng chọn **Ảnh cuối**.", "", None; return
text_input = (text_input or "").strip()
if not text_input:
yield [], "Vui lòng nhập **Ô chữ** (prompt).", "", None; return
try:
qty = int(qty or 1)
if qty < 1: qty = 1
if qty > 10: qty = 10
except Exception:
qty = 1
try:
task_ids = start_runs(start_image_path, end_image_path, qty, text_input)
except Exception as e:
yield [], f"❌ Không khởi chạy được: {e}", "", None
return
pending = set(task_ids)
succeeded, failed = set(), set()
gallery_items, all_links = [], []
files_saved = None
last_files_emit_at = 0.0; started_at = time.time()
while pending:
done, running, fail = poll_many(list(pending))
for tid in done: succeeded.add(tid); pending.discard(tid)
for tid in fail: failed.add(tid); pending.discard(tid)
new_urls = []
for tid in done:
try:
urls = call_outputs(tid)
new_urls.extend(urls)
except Exception:
pass
if new_urls:
for u in new_urls:
if u not in all_links: all_links.append(u)
if _is_image_url(u): gallery_items.append(u)
files_md = None
now = time.time()
if all_links and now - last_files_emit_at > 3:
try:
files_saved = download_urls_as_files(all_links, base_name=text_input)
last_files_emit_at = now
except Exception:
pass
files_md = files_saved if files_saved else None
status = (
f"Đang xử lý… ✅ xong {len(succeeded)}/{len(task_ids)}"
+ (f" · ❌ lỗi {len(failed)}" if failed else "")
+ (f" · ⏳ còn {len(pending)}" if pending else "")
)
links_md = "\n".join(f"- [{u}]({u})" for u in all_links) if all_links else "_Chưa có file_"
files_md = files_md if files_md else None
yield gallery_items, status, links_md, files_md
time.sleep(1.0)
if time.time() - started_at > 3600:
yield gallery_items, "Quá thời gian tối đa 1 giờ, dừng lại.", "\n".join(f"- [{u}]({u})" for u in all_links), (files_saved or None)
return
if all_links and (not files_saved):
try: files_saved = download_urls_as_files(all_links, base_name=text_input)
except Exception: pass
final_status = f"Hoàn tất ✅ {len(succeeded)}/{len(task_ids)}" + (f" · ❌ lỗi {len(failed)}" if failed else "")
final_links = "\n".join(f"- [{u}]({u})" for u in all_links) if all_links else "_Không có file trả về_"
yield gallery_items, final_status, final_links, (files_saved or None)
def _check_pass(pw):
ok = bool(APP_PASSCODE) and (pw.strip() == APP_PASSCODE)
msg = "Đăng nhập thành công ✅" if ok else ("Sai passcode ❌" if APP_PASSCODE else "Thiếu APP_PASSCODE trong Variables của Space ❗")
return msg, gr.update(visible=not ok), gr.update(visible=ok)
with gr.Blocks(css=CSS, title="2 Ảnh + Prompt — Tâm hồn vô tri") as demo:
# ===== LOCK SCREEN =====
lock_group = gr.Group(visible=True)
with lock_group:
with gr.Column(elem_classes=["container", "card" ]):
gr.Markdown("### 🔒 Nhập passcode để sử dụng ứng dụng")
passbox = gr.Textbox(label="Passcode", type="password", placeholder="Nhập APP_PASSCODE", autofocus=True)
login_btn = gr.Button("Đăng nhập")
login_msg = gr.Markdown("")
# ===== APP UI =====
app_group = gr.Group(visible=False)
with app_group:
with gr.Column(elem_classes=["container"]):
# Header
with gr.Row(elem_classes=["header"]):
avatar_html = f'<div class="avatar"><img src="{AVATAR_URL}" /></div>' if AVATAR_URL else '<div class="avatar">👤</div>'
gr.HTML(avatar_html)
gr.HTML(f'<div class="brand"><div class="title">{BRAND_TITLE}</div><div class="subtitle">2 ảnh (đầu/cuối) + 1 prompt → RunningHub</div></div>')
# Banners
items = [(BANNER1_URL, BANNER1_HREF), (BANNER2_URL, BANNER2_HREF), (BANNER3_URL, BANNER3_HREF)]
if any(u for u,_ in items):
with gr.Column(elem_classes=["card"]):
gr.HTML(_banners_html(items))
# IO + Results
with gr.Row(elem_classes=["row"]):
# Left Card: Inputs
with gr.Column(scale=1, min_width=360, elem_classes=["col", "card"]):
img_start = gr.Image(label="Ảnh đầu (bắt buộc)", type="filepath", height=240, image_mode="RGB")
img_end = gr.Image(label="Ảnh cuối (bắt buộc)", type="filepath", height=240, image_mode="RGB")
name_text = gr.Textbox(label="Ô chữ Promt", value="", placeholder="ví dụ: POV go to school")
qty = gr.Slider(label="Số lượng job chạy song song", minimum=1, maximum=10, step=1, value=3)
run_btn = gr.Button("▶️ Chạy song song", elem_classes=["primary"]) # styled by CSS
# Right Card: Results
with gr.Column(scale=2, min_width=600, elem_classes=["col", "card"]):
gallery = gr.Gallery(label="Ảnh trả về (nếu có)", columns=[3], height=560, object_fit="contain", elem_id="gallery")
status = gr.Markdown("", elem_classes=["help"])
links = gr.Markdown("", elem_id="links")
files = gr.Files(label="File đã tải về (đặt tên theo ô chữ)", elem_id="files")
run_btn.click(
fn=_stream_run,
inputs=[img_start, img_end, qty, name_text],
outputs=[gallery, status, links, files],
api_name=False
)
# Login wiring
login_btn.click(_check_pass, inputs=[passbox], outputs=[login_msg, lock_group, app_group])
# Queue
demo.queue(default_concurrency_limit=1, max_size=32, status_update_rate=0.5)
if __name__ == "__main__":
demo.launch(show_api=False, quiet=True)