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'') return f'
{"".join(out)}
' 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'
' if AVATAR_URL else '
👤
' gr.HTML(avatar_html) gr.HTML(f'
{BRAND_TITLE}
2 ảnh (đầu/cuối) + 1 prompt → RunningHub
') # 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)