From f950594e8af7f42d131aedfcda81efcb23cbaa73 Mon Sep 17 00:00:00 2001 From: kyyuu Date: Sat, 28 Mar 2026 14:24:18 +0900 Subject: [PATCH] feat: add standalone GUI launcher (run_forge.bat + run_forge_launcher.py) Double-click run_forge.bat to launch a dark-themed tkinter GUI that manages Fooocus (port 7865) and Prompt Forge Bridge (port 8080). Includes server status dots, Start/Stop/Restart controls, launch options (preset, --in-browser, --listen), real-time log streaming, and quick-open browser links. Co-Authored-By: Claude Sonnet 4.6 --- run_forge.bat | 33 ++++ run_forge_launcher.py | 433 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 466 insertions(+) create mode 100644 run_forge.bat create mode 100644 run_forge_launcher.py diff --git a/run_forge.bat b/run_forge.bat new file mode 100644 index 00000000..ac667bac --- /dev/null +++ b/run_forge.bat @@ -0,0 +1,33 @@ +@echo off +chcp 65001 > nul +title FOOOCUS FORGE LAUNCHER + +cd /d "%~dp0" + +REM ── Python を探す (embedded > PATH) ────────────────────────────── +if exist "python_embedded\python.exe" ( + set "PYTHON=python_embedded\python.exe" + echo [INFO] Embedded Python を使用します +) else ( + set "PYTHON=python" +) + +REM ── tkinter が使えるか確認 ─────────────────────────────────────── +%PYTHON% -c "import tkinter" 2>nul +if errorlevel 1 ( + echo. + echo [ERROR] tkinter が見つかりません。 + echo Python を公式サイト (python.org) からインストールしてください。 + echo インストール時に "tcl/tk and IDLE" にチェックを入れてください。 + pause + exit /b 1 +) + +REM ── ランチャー起動 ─────────────────────────────────────────────── +%PYTHON% run_forge_launcher.py + +if errorlevel 1 ( + echo. + echo [ERROR] 起動中にエラーが発生しました。 + pause +) diff --git a/run_forge_launcher.py b/run_forge_launcher.py new file mode 100644 index 00000000..7cd18ca6 --- /dev/null +++ b/run_forge_launcher.py @@ -0,0 +1,433 @@ +""" +FOOOCUS FORGE LAUNCHER +tkinter dark-themed GUI launcher for Fooocus + Prompt Forge Bridge +""" +import sys +import os +import threading +import subprocess +import socket +import time +import webbrowser +import queue +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox + +# ── カラーテーマ ────────────────────────────────────────────────── +BG = "#0a0a0f" +BG2 = "#12121a" +BG3 = "#1a1a28" +BORDER = "#2a2a3d" +FG = "#e8e8f0" +FG2 = "#8888aa" +ACCENT = "#7c4dff" +ACCENT2 = "#b388ff" +ACCENT3 = "#00e5ff" +GREEN = "#00e676" +RED = "#ff1744" +YELLOW = "#ffea00" +ORANGE = "#ff6d00" +X_BLUE = "#1da1f2" + +# ── ポート定義 ───────────────────────────────────────────────────── +PORT_FOOOCUS = 7865 +PORT_BRIDGE = 8080 + +# ── 起動コマンド ─────────────────────────────────────────────────── +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +def _py(): + emb = os.path.join(BASE_DIR, "python_embedded", "python.exe") + return emb if os.path.exists(emb) else sys.executable + +def build_fooocus_cmd(preset="", in_browser=False, listen=False): + cmd = [_py(), "launch.py"] + if preset: + cmd += ["--preset", preset] + if in_browser: + cmd.append("--in-browser") + if listen: + cmd.append("--listen") + return cmd + +def is_port_open(port: int) -> bool: + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.5): + return True + except OSError: + return False + + +# ══════════════════════════════════════════════════════════════════ +class LauncherApp(tk.Tk): + def __init__(self): + super().__init__() + + self.title("FOOOCUS FORGE LAUNCHER") + self.configure(bg=BG) + self.resizable(True, True) + self.minsize(680, 520) + + # プロセス管理 + self._proc_fooocus: subprocess.Popen | None = None + self._log_queue: queue.Queue = queue.Queue() + + # ── ウィジェット構築 ── + self._build_header() + self._build_status_bar() + self._build_options() + self._build_controls() + self._build_log() + self._build_links() + + # ── イベント ── + self.bind("", lambda _: self._on_close()) + self.protocol("WM_DELETE_WINDOW", self._on_close) + + # ── ポーリング開始 ── + self._poll_status() + self._drain_log_queue() + + # ── ヘッダー ────────────────────────────────────────────────── + def _build_header(self): + f = tk.Frame(self, bg=BG, pady=12) + f.pack(fill="x", padx=20) + tk.Label(f, text="⚡ FOOOCUS FORGE LAUNCHER", + bg=BG, fg=ACCENT2, + font=("Consolas", 16, "bold")).pack(side="left") + tk.Label(f, text="v1.0", + bg=BG, fg=FG2, + font=("Consolas", 10)).pack(side="left", padx=(8, 0), pady=4) + + # ── ステータスバー ──────────────────────────────────────────── + def _build_status_bar(self): + outer = tk.Frame(self, bg=BG3, bd=0, highlightbackground=BORDER, + highlightthickness=1) + outer.pack(fill="x", padx=20, pady=(0, 8)) + + inner = tk.Frame(outer, bg=BG3, pady=8) + inner.pack(fill="x", padx=12) + + # Fooocus + fc = tk.Frame(inner, bg=BG3) + fc.pack(side="left", padx=(0, 24)) + self._dot_fooocus = tk.Label(fc, text="●", fg=RED, bg=BG3, + font=("Consolas", 14)) + self._dot_fooocus.pack(side="left") + tk.Label(fc, text=f" Fooocus :{PORT_FOOOCUS}", + bg=BG3, fg=FG, font=("Consolas", 11)).pack(side="left") + self._lbl_fooocus = tk.Label(fc, text="停止中", + bg=BG3, fg=RED, + font=("Consolas", 10)) + self._lbl_fooocus.pack(side="left", padx=(6, 0)) + + sep = tk.Frame(inner, bg=BORDER, width=1, height=24) + sep.pack(side="left", padx=12) + + # Bridge + bc = tk.Frame(inner, bg=BG3) + bc.pack(side="left") + self._dot_bridge = tk.Label(bc, text="●", fg=RED, bg=BG3, + font=("Consolas", 14)) + self._dot_bridge.pack(side="left") + tk.Label(bc, text=f" Bridge :{PORT_BRIDGE}", + bg=BG3, fg=FG, font=("Consolas", 11)).pack(side="left") + self._lbl_bridge = tk.Label(bc, text="停止中", + bg=BG3, fg=RED, + font=("Consolas", 10)) + self._lbl_bridge.pack(side="left", padx=(6, 0)) + + # ── 起動オプション ──────────────────────────────────────────── + def _build_options(self): + outer = tk.Frame(self, bg=BG2, bd=0, highlightbackground=BORDER, + highlightthickness=1) + outer.pack(fill="x", padx=20, pady=(0, 8)) + + inner = tk.Frame(outer, bg=BG2, pady=8, padx=12) + inner.pack(fill="x") + + tk.Label(inner, text="起動オプション", bg=BG2, fg=FG2, + font=("Consolas", 9, "bold")).grid( + row=0, column=0, columnspan=6, sticky="w", pady=(0, 6)) + + # プリセット + tk.Label(inner, text="プリセット:", bg=BG2, fg=FG, + font=("Consolas", 10)).grid(row=1, column=0, sticky="w", padx=(0, 6)) + self._preset_var = tk.StringVar(value="なし") + preset_cb = ttk.Combobox(inner, textvariable=self._preset_var, + values=["なし", "anime", "realistic", "lcm"], + state="readonly", width=12, + font=("Consolas", 10)) + preset_cb.grid(row=1, column=1, sticky="w", padx=(0, 20)) + self._style_combobox(preset_cb) + + # ブラウザ自動起動 + self._browser_var = tk.BooleanVar(value=False) + chk_browser = tk.Checkbutton(inner, text="ブラウザ自動起動", + variable=self._browser_var, + bg=BG2, fg=FG, selectcolor=BG3, + activebackground=BG2, activeforeground=ACCENT2, + font=("Consolas", 10)) + chk_browser.grid(row=1, column=2, sticky="w", padx=(0, 20)) + + # --listen + self._listen_var = tk.BooleanVar(value=False) + chk_listen = tk.Checkbutton(inner, text="--listen (LAN公開)", + variable=self._listen_var, + bg=BG2, fg=FG, selectcolor=BG3, + activebackground=BG2, activeforeground=ACCENT2, + font=("Consolas", 10)) + chk_listen.grid(row=1, column=3, sticky="w") + + # ── コントロールボタン ──────────────────────────────────────── + def _build_controls(self): + f = tk.Frame(self, bg=BG, pady=4) + f.pack(fill="x", padx=20) + + btn_cfg = dict(font=("Consolas", 11, "bold"), cursor="hand2", + relief="flat", bd=0, padx=18, pady=7) + + self._btn_start = tk.Button( + f, text="▶ 起動", bg=ACCENT, fg="white", + activebackground=ACCENT2, activeforeground="white", + command=self._start_all, **btn_cfg) + self._btn_start.pack(side="left", padx=(0, 8)) + + self._btn_stop = tk.Button( + f, text="■ 停止", bg=BG3, fg=RED, + activebackground=BORDER, activeforeground=RED, + command=self._stop_all, state="disabled", **btn_cfg) + self._btn_stop.pack(side="left", padx=(0, 8)) + + self._btn_restart = tk.Button( + f, text="↺ 再起動", bg=BG3, fg=YELLOW, + activebackground=BORDER, activeforeground=YELLOW, + command=self._restart_all, state="disabled", **btn_cfg) + self._btn_restart.pack(side="left", padx=(0, 8)) + + tk.Button(f, text="ログクリア", bg=BG3, fg=FG2, + activebackground=BORDER, activeforeground=FG, + command=self._clear_log, **btn_cfg).pack(side="right") + + # ── ログ ────────────────────────────────────────────────────── + def _build_log(self): + f = tk.Frame(self, bg=BG) + f.pack(fill="both", expand=True, padx=20, pady=(8, 4)) + + tk.Label(f, text="ログ", bg=BG, fg=FG2, + font=("Consolas", 9, "bold")).pack(anchor="w", pady=(0, 3)) + + self._log = scrolledtext.ScrolledText( + f, bg=BG2, fg=FG, insertbackground=FG, + font=("Consolas", 9), relief="flat", + bd=0, highlightbackground=BORDER, highlightthickness=1, + state="disabled", wrap="word") + self._log.pack(fill="both", expand=True) + + # タグ色 + self._log.tag_config("info", foreground=FG2) + self._log.tag_config("ok", foreground=GREEN) + self._log.tag_config("warn", foreground=YELLOW) + self._log.tag_config("err", foreground=RED) + self._log.tag_config("accent", foreground=ACCENT3) + + # ── クイックリンク ──────────────────────────────────────────── + def _build_links(self): + f = tk.Frame(self, bg=BG, pady=8) + f.pack(fill="x", padx=20) + + link_cfg = dict(font=("Consolas", 10), cursor="hand2", + relief="flat", bd=0, padx=12, pady=5) + + tk.Button(f, text="🌐 Prompt Forge http://127.0.0.1:8080", + bg=BG3, fg=ACCENT3, + activebackground=BORDER, activeforeground=ACCENT3, + command=lambda: webbrowser.open("http://127.0.0.1:8080"), + **link_cfg).pack(side="left", padx=(0, 8)) + + tk.Button(f, text="🌐 Fooocus UI http://127.0.0.1:7865", + bg=BG3, fg=ACCENT2, + activebackground=BORDER, activeforeground=ACCENT2, + command=lambda: webbrowser.open("http://127.0.0.1:7865"), + **link_cfg).pack(side="left") + + # ── Combobox スタイル ───────────────────────────────────────── + def _style_combobox(self, cb): + style = ttk.Style(self) + style.theme_use("default") + style.configure("TCombobox", + fieldbackground=BG3, + background=BG3, + foreground=FG, + selectbackground=ACCENT, + selectforeground="white", + arrowcolor=FG2) + style.map("TCombobox", + fieldbackground=[("readonly", BG3)], + foreground=[("readonly", FG)], + background=[("readonly", BG3)]) + self.option_add("*TCombobox*Listbox.background", BG3) + self.option_add("*TCombobox*Listbox.foreground", FG) + self.option_add("*TCombobox*Listbox.selectBackground", ACCENT) + self.option_add("*TCombobox*Listbox.font", ("Consolas", 10)) + + # ── ログ書き込み ────────────────────────────────────────────── + def _log_write(self, text: str, tag: str = "info"): + self._log.configure(state="normal") + self._log.insert("end", text + "\n", tag) + self._log.see("end") + self._log.configure(state="disabled") + + def _clear_log(self): + self._log.configure(state="normal") + self._log.delete("1.0", "end") + self._log.configure(state="disabled") + + # ── ログキューをUIスレッドで消費 ────────────────────────────── + def _drain_log_queue(self): + try: + while True: + text, tag = self._log_queue.get_nowait() + self._log_write(text, tag) + except queue.Empty: + pass + self.after(100, self._drain_log_queue) + + def _queue_log(self, text: str, tag: str = "info"): + self._log_queue.put((text, tag)) + + # ── ステータスポーリング ────────────────────────────────────── + def _poll_status(self): + fooocus_up = is_port_open(PORT_FOOOCUS) + bridge_up = is_port_open(PORT_BRIDGE) + + # Fooocus + if fooocus_up: + self._dot_fooocus.config(fg=GREEN) + self._lbl_fooocus.config(text="起動中", fg=GREEN) + else: + if self._proc_fooocus and self._proc_fooocus.poll() is None: + self._dot_fooocus.config(fg=YELLOW) + self._lbl_fooocus.config(text="起動中...", fg=YELLOW) + else: + self._dot_fooocus.config(fg=RED) + self._lbl_fooocus.config(text="停止中", fg=RED) + + # Bridge (Fooocusの中で起動されるためプロセスを別管理しない) + if bridge_up: + self._dot_bridge.config(fg=GREEN) + self._lbl_bridge.config(text="起動中", fg=GREEN) + else: + self._dot_bridge.config(fg=RED) + self._lbl_bridge.config(text="停止中", fg=RED) + + # ボタン状態 + running = (self._proc_fooocus is not None and + self._proc_fooocus.poll() is None) + self._btn_start.config(state="disabled" if running else "normal") + self._btn_stop.config(state="normal" if running else "disabled") + self._btn_restart.config(state="normal" if running else "disabled") + + self.after(2000, self._poll_status) + + # ── 起動 ────────────────────────────────────────────────────── + def _start_all(self): + preset = self._preset_var.get() + browser = self._browser_var.get() + listen = self._listen_var.get() + + cmd = build_fooocus_cmd( + preset = "" if preset == "なし" else preset, + in_browser = browser, + listen = listen + ) + + self._log_write(f"▶ 起動コマンド: {' '.join(cmd)}", "accent") + + try: + self._proc_fooocus = subprocess.Popen( + cmd, + cwd=BASE_DIR, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1 + ) + except FileNotFoundError as e: + self._log_write(f"[ERROR] Python が見つかりません: {e}", "err") + return + + self._log_write(f"PID: {self._proc_fooocus.pid}", "info") + threading.Thread(target=self._stream_output, + args=(self._proc_fooocus,), daemon=True).start() + + def _stream_output(self, proc: subprocess.Popen): + try: + for line in proc.stdout: + line = line.rstrip("\n") + if not line: + continue + low = line.lower() + if any(k in low for k in ("error", "exception", "traceback", "failed")): + tag = "err" + elif any(k in low for k in ("warning", "warn")): + tag = "warn" + elif any(k in low for k in ("running on", "http://", "startup", "ready", "started")): + tag = "ok" + else: + tag = "info" + self._queue_log(line, tag) + except Exception: + pass + self._queue_log("── プロセス終了 ──", "warn") + + # ── 停止 ────────────────────────────────────────────────────── + def _stop_all(self): + if self._proc_fooocus and self._proc_fooocus.poll() is None: + self._log_write("■ 停止中...", "warn") + try: + self._proc_fooocus.terminate() + try: + self._proc_fooocus.wait(timeout=8) + except subprocess.TimeoutExpired: + self._proc_fooocus.kill() + self._log_write("停止しました。", "ok") + except Exception as e: + self._log_write(f"[ERROR] 停止失敗: {e}", "err") + self._proc_fooocus = None + + # ── 再起動 ──────────────────────────────────────────────────── + def _restart_all(self): + self._log_write("↺ 再起動中...", "accent") + self._stop_all() + time.sleep(1) + self._start_all() + + # ── 終了 ────────────────────────────────────────────────────── + def _on_close(self): + if (self._proc_fooocus and self._proc_fooocus.poll() is None): + if messagebox.askyesno( + "確認", + "Fooocus が起動中です。終了しますか?\n(プロセスも停止されます)", + parent=self + ): + self._stop_all() + else: + return + self.destroy() + + +# ── エントリーポイント ───────────────────────────────────────────── +if __name__ == "__main__": + app = LauncherApp() + # ウィンドウを画面中央に + app.update_idletasks() + w, h = 700, 580 + sw = app.winfo_screenwidth() + sh = app.winfo_screenheight() + x = (sw - w) // 2 + y = (sh - h) // 2 + app.geometry(f"{w}x{h}+{x}+{y}") + app.mainloop()