This commit is contained in:
YUU13579 2026-03-28 16:18:00 +00:00 committed by GitHub
commit 4087505661
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 12707 additions and 231 deletions

29
.claude/launch.json Normal file
View File

@ -0,0 +1,29 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "Fooocus WebUI",
"runtimeExecutable": "python",
"runtimeArgs": ["launch.py"],
"port": 7865
},
{
"name": "Fooocus WebUI (ブラウザ自動起動)",
"runtimeExecutable": "python",
"runtimeArgs": ["launch.py", "--in-browser"],
"port": 7865
},
{
"name": "Fooocus WebUI (アップデート付き起動)",
"runtimeExecutable": "python",
"runtimeArgs": ["entry_with_update.py"],
"port": 7865
},
{
"name": "Prompt Forge Bridge (単独テスト)",
"runtimeExecutable": "python",
"runtimeArgs": ["-c", "import prompt_forge_bridge; import time; prompt_forge_bridge.start_bridge(); print('Bridge running on http://127.0.0.1:8080'); time.sleep(86400)"],
"port": 8080
}
]
}

View File

@ -415,4 +415,127 @@ div:has(> #positive_prompt) {
#inpaint_brush_color input[type=color]{
background: none;
}
/* ── ブラッシュアップ コンパクトボタン ── */
.brushup_btn_col button {
padding: 3px 6px !important;
font-size: 11px !important;
min-height: 28px !important;
height: 28px !important;
line-height: 1.2 !important;
}
#brushup_bar {
align-items: flex-start !important;
gap: 6px !important;
}
/* ── OpenCV エディットパネル ── */
#edit_panel {
border-top: 1px solid rgba(255,255,255,0.08) !important;
padding-top: 8px !important;
margin-top: 4px !important;
}
/* プリセットボタン */
#edit_presets button, .preset_btn button {
padding: 3px 5px !important;
font-size: 10px !important;
min-height: 26px !important;
height: 26px !important;
line-height: 1.1 !important;
}
/* 回転・反転・B/Aボタン */
#edit_panel .gr-row button {
padding: 3px 6px !important;
font-size: 11px !important;
min-height: 26px !important;
}
/* スライダーラベル */
#edit_panel label {
font-size: 11px !important;
margin-bottom: 0 !important;
}
/* スライダー間隔を詰める */
#edit_panel .gr-form {
gap: 2px !important;
}
/*
統一感テーマ アクセントカラー: #7c4dff ()
*/
/* ── アコーディオン共通 ── */
#pf_accordion > .label-wrap,
#user_sp_accordion > .label-wrap {
border-left: 3px solid #7c4dff !important;
padding-left: 8px !important;
}
/* ── ブラッシュアップバー 背景帯 ── */
#brushup_bar {
background: rgba(124, 77, 255, 0.04) !important;
border-radius: 8px !important;
padding: 6px 8px !important;
border: 1px solid rgba(124, 77, 255, 0.12) !important;
}
/* ── 微調整・強変更ボタン: プライマリ色をテーマ紫に ── */
.brushup_btn_col button.secondary {
border-color: rgba(124, 77, 255, 0.4) !important;
color: rgba(180, 150, 255, 0.9) !important;
}
.brushup_btn_col button.secondary:hover {
background: rgba(124, 77, 255, 0.15) !important;
border-color: #7c4dff !important;
}
/* ── 変更して生成ボタン ── */
.brushup_btn_col button.primary {
background: linear-gradient(135deg, #7c4dff 0%, #5c35cc 100%) !important;
border: none !important;
}
/* ── エディットパネル ── */
#edit_panel {
background: rgba(124, 77, 255, 0.03) !important;
border-radius: 6px !important;
border: 1px solid rgba(124, 77, 255, 0.10) !important;
border-top: 1px solid rgba(124, 77, 255, 0.15) !important;
padding: 8px !important;
}
/* ── 設定プリセット アコーディオン内 ── */
#user_sp_accordion {
border-radius: 8px !important;
overflow: hidden !important;
}
/* ── 生成ボタンは維持しつつ補助ボタンをトーン統一 ── */
#generate_button {
background: linear-gradient(135deg, #7c4dff 0%, #3a1d96 100%) !important;
box-shadow: 0 2px 12px rgba(124, 77, 255, 0.35) !important;
}
/* ── セクション区切り (Prompt Forge 上部) ── */
#pf_accordion {
border-radius: 8px !important;
overflow: hidden !important;
}
/* ── プリセットボタン ホバー ── */
#edit_presets button:hover, .preset_btn button:hover {
background: rgba(124, 77, 255, 0.2) !important;
border-color: #7c4dff !important;
color: #d0b8ff !important;
}
/* ── ステータス HTML テキストカラー ── */
#brushup_status_html {
color: rgba(200, 185, 255, 0.85) !important;
font-size: 12px !important;
}

104
modules/image_editor.py Normal file
View File

@ -0,0 +1,104 @@
"""OpenCV-based image adjustment utilities for the gallery editor."""
import cv2
import numpy as np
# ────────────────────────────────────────────────
# ユーティリティ
# ────────────────────────────────────────────────
def resize_for_preview(img_array, max_size=768):
"""プレビュー用にアスペクト比を保ってリサイズ(高速化)。"""
if img_array is None:
return None
h, w = img_array.shape[:2]
if max(h, w) <= max_size:
return img_array
scale = max_size / max(h, w)
nw, nh = int(w * scale), int(h * scale)
return cv2.resize(img_array, (nw, nh), interpolation=cv2.INTER_AREA)
def rotate_image(img_array, degrees):
"""画像を 90 / 180 / 270 度回転RGB 入出力)。"""
if img_array is None:
return None
degrees = int(degrees) % 360
if degrees == 90:
return cv2.rotate(img_array, cv2.ROTATE_90_CLOCKWISE)
elif degrees == 180:
return cv2.rotate(img_array, cv2.ROTATE_180)
elif degrees == 270:
return cv2.rotate(img_array, cv2.ROTATE_90_COUNTERCLOCKWISE)
return img_array
def flip_image(img_array, direction='h'):
"""水平('h') または垂直('v') 反転RGB 入出力)。"""
if img_array is None:
return None
code = 1 if direction == 'h' else 0
return cv2.flip(img_array, code)
# ────────────────────────────────────────────────
# メイン調整関数
# ────────────────────────────────────────────────
def apply_adjustments(img_array,
brightness=0, contrast=1.0,
saturation=1.0, hue_shift=0,
sharpness=1.0, temperature=0):
"""OpenCV で画像調整を適用する。
Parameters
----------
img_array : numpy ndarray (H, W, 3) RGB
brightness : int -100 100 加算
contrast : float 0.1 3.0 乗算
saturation : float 0.0 3.0 HSV S 倍率
hue_shift : int -90 90 HSV H 加算
sharpness : float 0.0 3.0 1.0=変化なし / >1=鮮明 / <1=ぼかし
temperature : int -100 100 =クール() / =ウォーム()
Returns
-------
numpy ndarray (H, W, 3) RGB
"""
if img_array is None:
return None
img = np.clip(img_array, 0, 255).astype(np.uint8)
bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# ── 明るさ / コントラスト ──────────────────────
if brightness != 0 or contrast != 1.0:
bgr = cv2.convertScaleAbs(bgr, alpha=float(contrast), beta=float(brightness))
# ── 彩度 / 色相 ────────────────────────────────
if saturation != 1.0 or hue_shift != 0:
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
hsv[:, :, 0] = (hsv[:, :, 0] + float(hue_shift)) % 180
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * float(saturation), 0, 255)
bgr = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
# ── 色温度 ─────────────────────────────────────
if temperature != 0:
b, g, r = cv2.split(bgr.astype(np.float32))
t = float(temperature)
if t > 0: # ウォーム (赤+, 青-)
r = np.clip(r + t * 0.8, 0, 255)
b = np.clip(b - t * 0.4, 0, 255)
else: # クール (青+, 赤-)
r = np.clip(r + t * 0.4, 0, 255)
b = np.clip(b - t * 0.8, 0, 255)
bgr = cv2.merge([b.astype(np.uint8), g.astype(np.uint8), r.astype(np.uint8)])
# ── シャープネス(アンシャープマスク)─────────
if sharpness != 1.0:
blur = cv2.GaussianBlur(bgr, (0, 0), 3.0)
strength = float(sharpness) - 1.0
bgr = cv2.addWeighted(bgr, 1.0 + strength, blur, -strength, 0)
bgr = np.clip(bgr, 0, 255).astype(np.uint8)
return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)

323
prompt_forge_bridge.py Normal file
View File

@ -0,0 +1,323 @@
"""
Prompt Forge Bridge Server
--------------------------
Fooocusプロセス内でバックグラウンドスレッドとして動作するHTTPサーバー
- ポート8080でPrompt ForgeのHTMLを配信
- /v1/generation/text-to-image REST APIを提供
- Fooocusのworkerキューに直接AsyncTaskを投入して画像生成
"""
import json
import os
import base64
import threading
import time
import io
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
BRIDGE_PORT = 8080
_html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompt_forge_v3_11.html')
# ──────────────────────────────────────────────────────────────────
# AsyncTask args ビルダー
# ──────────────────────────────────────────────────────────────────
def _build_args(req: dict) -> list:
"""リクエストJSONからAsyncTask argsリストを構築する。"""
import modules.config as config
import modules.flags as flags
import args_manager
# ── ユーザー指定パラメータ ──
prompt = req.get('prompt', '')
negative_prompt = req.get('negative_prompt', '')
styles = req.get('style_selections', list(config.default_styles))
if isinstance(styles, str):
styles = [styles]
performance = req.get('performance_selection', 'Speed')
image_number = int(req.get('image_number', 1))
output_format = req.get('output_format', 'png')
image_seed = int(req.get('image_seed', -1))
sharpness = float(req.get('sharpness', config.default_sample_sharpness))
guidance_scale = float(req.get('guidance_scale', config.default_cfg_scale))
# アスペクト比を Fooocus 形式に変換 ("1024*1024" → "1024×1024 | 1:1")
aspect_raw = str(req.get('aspect_ratios_selection', '1024*1024'))
aspect_raw = aspect_raw.replace('×', '*').replace('x', '*').replace('X', '*')
if '*' not in aspect_raw:
aspect_raw = '1024*1024'
aspect_sel = config.add_ratio(aspect_raw)
args = [
False, # generate_image_grid
prompt,
negative_prompt,
styles,
performance,
aspect_sel,
image_number,
output_format,
image_seed,
False, # read_wildcards_in_order
sharpness,
guidance_scale,
config.default_base_model_name,
config.default_refiner_model_name,
config.default_refiner_switch,
]
# LoRA (default_max_lora_number x 3: enabled, name, weight)
# default_loras は必ず default_max_lora_number 個にパディングされている
loras_to_add = config.default_loras[:config.default_max_lora_number]
# 不足分を 'None' で補填
while len(loras_to_add) < config.default_max_lora_number:
loras_to_add.append([True, 'None', 1.0])
for lora in loras_to_add:
args.extend([bool(lora[0]), str(lora[1]), float(lora[2])])
args += [
False, # input_image_checkbox
'uov', # current_tab
flags.disabled, # uov_method ('Disabled')
None, # uov_input_image
[], # outpaint_selections
{'image': None, 'mask': None}, # inpaint_input_image
'', # inpaint_additional_prompt
None, # inpaint_mask_image_upload
False, # disable_preview
False, # disable_intermediate_results
False, # disable_seed_increment
config.default_black_out_nsfw,
1.5, # adm_scaler_positive
0.8, # adm_scaler_negative
0.3, # adm_scaler_end
config.default_cfg_tsnr, # adaptive_cfg
config.default_clip_skip, # clip_skip
config.default_sampler, # sampler_name
config.default_scheduler, # scheduler_name
flags.default_vae, # vae_name
-1, # overwrite_step
-1, # overwrite_switch
-1, # overwrite_width
-1, # overwrite_height
-1, # overwrite_vary_strength
-1, # overwrite_upscale_strength
False, # mixing_image_prompt_and_vary_upscale
False, # mixing_image_prompt_and_inpaint
False, # debugging_cn_preprocessor
False, # skipping_cn_preprocessor
100, # canny_low_threshold
200, # canny_high_threshold
flags.refiner_swap_method, # 'joint'
0.25, # controlnet_softness
False, # freeu_enabled
1.01, # freeu_b1
1.02, # freeu_b2
0.99, # freeu_s1
0.95, # freeu_s2
# ── inpaint_ctrls ──
False, # debugging_inpaint_preprocessor
False, # inpaint_disable_initial_latent
config.default_inpaint_engine_version, # inpaint_engine
1.0, # inpaint_strength
0.618, # inpaint_respective_field
False, # inpaint_advanced_masking_checkbox
False, # invert_mask_checkbox
0, # inpaint_erode_or_dilate
]
if not args_manager.args.disable_image_log:
args.append(config.default_save_only_final_enhanced_image)
if not args_manager.args.disable_metadata:
args.append(config.default_save_metadata_to_images)
args.append('fooocus') # metadata_scheme
# ControlNet IP (default_controlnet_image_count x 4)
for _ in range(config.default_controlnet_image_count):
args += [None, 0.5, 0.6, flags.default_ip] # img, stop, weight, type
# enhance 制御
args += [
False, # debugging_dino
0, # dino_erode_or_dilate
False, # debugging_enhance_masks_checkbox
None, # enhance_input_image
False, # enhance_checkbox
config.default_enhance_uov_method,
config.default_enhance_uov_processing_order,
config.default_enhance_uov_prompt_type,
]
# enhance タブ (default_enhance_tabs x 16)
for _ in range(config.default_enhance_tabs):
args += [
False, # enhance_enabled
'', # enhance_mask_dino_prompt_text
'', # enhance_prompt
'', # enhance_negative_prompt
config.default_enhance_inpaint_mask_model, # 'sam'
config.default_inpaint_mask_cloth_category, # 'full'
config.default_inpaint_mask_sam_model, # 'vit_b'
0.25, # enhance_mask_text_threshold
0.3, # enhance_mask_box_threshold
config.default_sam_max_detections,
False, # enhance_inpaint_disable_initial_latent
config.default_inpaint_engine_version,
0.5, # enhance_inpaint_strength
0.618, # enhance_inpaint_respective_field
0, # enhance_inpaint_erode_or_dilate
False, # enhance_mask_invert
]
print(f'[PromptForge] args構築完了: {len(args)}'
f'(max_lora={config.default_max_lora_number}, '
f'cn_count={config.default_controlnet_image_count}, '
f'enhance_tabs={config.default_enhance_tabs})')
return args
# ──────────────────────────────────────────────────────────────────
# 画像生成メイン
# ──────────────────────────────────────────────────────────────────
def _generate(req: dict) -> list:
"""
Fooocusのworkerキューに生成タスクを投入し完成した画像をbase64リストで返す
"""
import modules.async_worker as worker
from modules.async_worker import AsyncTask
args = _build_args(req)
task = AsyncTask(args=args)
worker.async_tasks.append(task)
print(f'[PromptForge] 生成開始: "{task.prompt[:60]}"')
timeout = 600 # 10分
start = time.time()
results_b64 = []
while time.time() - start < timeout:
time.sleep(0.3)
if task.last_stop:
raise RuntimeError('生成がキャンセルされました')
# yields をすべて処理
while task.yields:
flag, product = task.yields.pop(0)
if flag == 'finish':
for img in product:
try:
if isinstance(img, str) and os.path.exists(img):
with open(img, 'rb') as f:
b64 = base64.b64encode(f.read()).decode('utf-8')
elif hasattr(img, 'save'):
buf = io.BytesIO()
img.save(buf, format='PNG')
b64 = base64.b64encode(buf.getvalue()).decode('utf-8')
else:
continue
results_b64.append({
'base64': b64,
'url': '',
'seed': task.seed if hasattr(task, 'seed') else req.get('image_seed', -1),
'finish_reason': 'SUCCESS',
'meta': {}
})
except Exception as e:
print(f'[PromptForge] 画像読込エラー: {e}')
if results_b64:
print(f'[PromptForge] 完了: {len(results_b64)}')
return results_b64
raise RuntimeError('画像データを取得できませんでした')
raise TimeoutError('生成タイムアウト (600秒)')
# ──────────────────────────────────────────────────────────────────
# HTTP ハンドラ
# ──────────────────────────────────────────────────────────────────
class _Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
pass # コンソールへの詳細ログを抑制
def _cors(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Accept')
def do_OPTIONS(self):
self.send_response(200)
self._cors()
self.end_headers()
def do_GET(self):
path = urlparse(self.path).path.rstrip('/') or '/'
if path in ('/', '/index.html'):
if os.path.exists(_html_path):
with open(_html_path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self._cors()
self.end_headers()
self.wfile.write(data)
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'prompt_forge_v3_11.html not found')
elif path == '/ping':
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self._cors()
self.end_headers()
self.wfile.write(b'pong')
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
path = urlparse(self.path).path
if path == '/v1/generation/text-to-image':
try:
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
req = json.loads(body.decode('utf-8')) if body else {}
images = _generate(req)
resp = json.dumps(images).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self._cors()
self.end_headers()
self.wfile.write(resp)
except Exception as e:
import traceback
msg = traceback.format_exc()
print(f'[PromptForge] エラー:\n{msg}')
resp = json.dumps({'error': str(e), 'traceback': msg}).encode('utf-8')
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self._cors()
self.end_headers()
self.wfile.write(resp)
else:
self.send_response(404)
self.end_headers()
# ──────────────────────────────────────────────────────────────────
# 起動関数webui.py から呼ばれる)
# ──────────────────────────────────────────────────────────────────
def start_bridge(port: int = BRIDGE_PORT):
"""ブリッジサーバーをデーモンスレッドで起動する。"""
server = HTTPServer(('127.0.0.1', port), _Handler)
t = threading.Thread(target=server.serve_forever, daemon=True)
t.start()
print(f'\n{"="*55}')
print(f' 🎨 Prompt Forge が起動しました!')
print(f' ブラウザで以下を開いてください:')
print(f' http://127.0.0.1:{port}')
print(f'{"="*55}\n')
return server, t

7710
prompt_forge_v3_11.html Normal file

File diff suppressed because it is too large Load Diff

33
run_forge.bat Normal file
View File

@ -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
)

433
run_forge_launcher.py Normal file
View File

@ -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()

3
ui_settings.json Normal file
View File

@ -0,0 +1,3 @@
{
"path_outputs": "E:\\マイドライブ\\AI画像入れ"
}

30
user_setting_presets.json Normal file
View File

@ -0,0 +1,30 @@
{
"アルビノ": {
"prompt": "white skin,clause up,__pf_angle__,masterpiece, high score, great score, absurdres, very aesthetic, smooth hair, neat hair, tidy hair, clean lineart, crisp detail, official art, highres, well-groomed hair, sleek hair, perfect hair, styled hair, hair in place, pure white background, simple background, white background, 1girl, looking at viewer, sensitive,red eyes, distinct pupils, detailed eyes, beautiful eyes, half-closed eyes, white skin, albino, porcelain skin, enigmatic, mysterious, distant, gloomy, white hair, __pf_hairstyle__,detailed white eyelashes,__pf_fetish__,__pf_costume_casual__",
"negative_prompt": "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry, sketch, doodle, rough draft, messy lines, rough lines, traditional media, watercolor, pencil, background, scenery, building, nature, detailed background, head out of frame, no sclera, colored sclera, solid eyes, frizzy hair, messy hair, flyaway hair, bad hair day, ahoge, stray hair, unruly hair, wild hair,sunmissing_limb,lace fabric,background,bad eyes, deformed eyes, thick eyelashes, clumped eyelashes, fused eyelashes, solid eyelashes, heavy eyelashes, messy eyelashes, overly dramatic eyelashes, clumpy mascara",
"styles": "[]",
"performance": "Quality",
"resolution": "(1024, 1024)",
"image_number": 1,
"guidance_scale": 6.0,
"sharpness": 2.0,
"base_model": "animagineXL40_v4Opt.safetensors",
"refiner_model": "None",
"refiner_switch": 0.5,
"sampler": "dpmpp_2m_sde_gpu",
"scheduler": "karras",
"vae": "Default (model)",
"adm_guidance": "(1.5, 0.8, 0.3)",
"refiner_swap_method": "joint",
"adaptive_cfg": 7.0,
"clip_skip": 2,
"input_image_enabled": false,
"enhance_enabled": true,
"uov_method": "Disabled",
"lora_combined_1": "True : add-detail-xl.safetensors : 0.58",
"lora_combined_2": "True : SDXL_FILM_PHOTOGRAPHY_STYLE_V1.safetensors : 0.38",
"lora_combined_3": "True : sexy_details_v4.safetensors : 0.22",
"lora_combined_4": "True : None : 1",
"lora_combined_5": "True : None : 1"
}
}

1040
webui.py

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff