feat(ai-outfit): AI 穿搭 UI tab (§19.3) + config plumbing (§19.4)
- main_window: wrap workflow in a QStackedWidget (page 0 = print, page 1 = AI outfit); enable tab 2「AI 穿搭」, switch pages on tab change. - app/widgets/ai_outfit_panel.py: three-column page per docs/11 §10 — left settings (Excel/output/model/prompt editor+save+insert+preview dialog/batch options), center (recent-results thumbnails + detail table), right (progress/stats/start/stop/export failures/log). - Threading: QThread + _OutfitWorker(QObject) wraps OutfitBatchRunner; queued signals refresh UI, each row written back to Excel on the worker thread; finish summary + failure-list CSV export. - config_service: load_ai_models()/load_outfit_prompt()/save_outfit_prompt() + outfit_* keys in app_config; panel persists via config_changed signal. - tests/test_config_service.py: 8 cases for the new helpers. Full suite (12 files) green on Python 3.7; offscreen MainWindow smoke test passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,29 @@ DEFAULT_CONFIG = {
|
||||
"update_source": "", # HTTP(S) manifest source (empty = no update check)
|
||||
"update_user": "", # HTTP Basic Auth user (empty = anonymous)
|
||||
"update_pass": "", # HTTP Basic Auth password (empty = anonymous)
|
||||
# AI 穿搭(docs/11 §11):上次路径与批量设置并入 app_config(密钥不在此,见 ai_models.json)
|
||||
"outfit_excel": "",
|
||||
"outfit_output_dir": "",
|
||||
"outfit_model": "", # last-selected model name
|
||||
"outfit_concurrency": 1,
|
||||
"outfit_request_interval": 2.0,
|
||||
"outfit_task_cooldown": 1.0,
|
||||
"outfit_retry_count": 2,
|
||||
"outfit_resolution": "1K",
|
||||
"outfit_quality": "均衡",
|
||||
"outfit_retry_failed": False,
|
||||
}
|
||||
|
||||
_CONFIG_FILENAME = "app_config.json"
|
||||
_AI_MODELS_FILENAME = "ai_models.json"
|
||||
_OUTFIT_PROMPT_FILENAME = "outfit_prompt.txt"
|
||||
|
||||
# Default outfit prompt (docs/11 §7). Persisted to outfit_prompt.txt on first save.
|
||||
DEFAULT_OUTFIT_PROMPT = (
|
||||
"为商品「{title}」(货号 {product_id})生成人物上身实穿效果图:真人模特正面"
|
||||
"穿着这件衣服,完整保留款式、版型、颜色与印花图案,自然光、纯色棚拍背景,"
|
||||
"电商主图风格,不加文字与促销标签。"
|
||||
)
|
||||
|
||||
|
||||
def load_config():
|
||||
@@ -65,3 +85,56 @@ def save_config(data):
|
||||
logger.info("Config saved to %s", config_file)
|
||||
except OSError as exc:
|
||||
logger.error("Failed to save config to %s: %s", config_file, exc)
|
||||
|
||||
|
||||
def load_ai_models():
|
||||
"""Load AI model configs from ai_models.json.
|
||||
|
||||
Returns a list of dicts (each with a ``name`` plus AiModelConfig fields).
|
||||
Missing/corrupt file → empty list (the UI then prompts the admin to add one).
|
||||
Keys live only in ~/.cmbot and are never committed (docs/11 §11).
|
||||
"""
|
||||
from services.file_service import get_config_path
|
||||
models_file = get_config_path(_AI_MODELS_FILENAME)
|
||||
if not models_file.exists():
|
||||
logger.info("AI models file not found: %s", models_file)
|
||||
return []
|
||||
try:
|
||||
with open(str(models_file), encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, ValueError, OSError) as exc:
|
||||
logger.warning("AI models file unreadable (%s): %s", exc, models_file)
|
||||
return []
|
||||
|
||||
models = data.get("models") if isinstance(data, dict) else data
|
||||
if not isinstance(models, list):
|
||||
logger.warning("AI models file has no model list: %s", models_file)
|
||||
return []
|
||||
return [m for m in models if isinstance(m, dict)]
|
||||
|
||||
|
||||
def load_outfit_prompt():
|
||||
"""Return the saved outfit prompt template, or the built-in default."""
|
||||
from services.file_service import get_config_path
|
||||
prompt_file = get_config_path(_OUTFIT_PROMPT_FILENAME)
|
||||
if not prompt_file.exists():
|
||||
return DEFAULT_OUTFIT_PROMPT
|
||||
try:
|
||||
with open(str(prompt_file), encoding="utf-8-sig") as f:
|
||||
return f.read()
|
||||
except OSError as exc:
|
||||
logger.warning("Outfit prompt unreadable (%s): %s", exc, prompt_file)
|
||||
return DEFAULT_OUTFIT_PROMPT
|
||||
|
||||
|
||||
def save_outfit_prompt(text):
|
||||
"""Persist the outfit prompt template (utf-8, no BOM). Does not raise."""
|
||||
from services.file_service import get_config_path
|
||||
prompt_file = get_config_path(_OUTFIT_PROMPT_FILENAME)
|
||||
try:
|
||||
prompt_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(str(prompt_file), "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
logger.info("Outfit prompt saved to %s", prompt_file)
|
||||
except OSError as exc:
|
||||
logger.error("Failed to save outfit prompt to %s: %s", prompt_file, exc)
|
||||
|
||||
Reference in New Issue
Block a user