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 <noreply@anthropic.com>
This commit is contained in:
parent
99d956be78
commit
f950594e8a
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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("<Escape>", 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()
|
||||
Loading…
Reference in New Issue