feat(ai-outfit): multiple named prompt templates (§19.8)
- config_service: outfit_prompts.json load/save (utf-8-sig read, no-BOM write, corrupt/empty -> migrate legacy outfit_prompt.txt or seed 默认; always >=1; _normalize_prompts drops invalid). app_config gains outfit_prompt_name. - ai_outfit_panel: 通用话术 group gets a template dropdown + 新建/另存为/重命名/ 删除 (keeps 插入标题/保存). Switching loads the template + refreshes preview; dirty edits prompt Save/Discard/Cancel before switching/new/save-as (cancel reverts the combo); unique names; can't delete the last one; selection persisted via config_changed; 开始生成 stores the editor into the template. - tests: +6 config cases (14 total); offscreen CRUD/dirty/rename/delete verified; full suite (12 files) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,13 +26,16 @@ DEFAULT_CONFIG = {
|
||||
"outfit_resolution": "1K",
|
||||
"outfit_quality": "均衡",
|
||||
"outfit_retry_failed": False,
|
||||
"outfit_prompt_name": "默认", # last-selected 话术模板 name (docs/11 §7.2)
|
||||
}
|
||||
|
||||
_CONFIG_FILENAME = "app_config.json"
|
||||
_AI_MODELS_FILENAME = "ai_models.json"
|
||||
_OUTFIT_PROMPT_FILENAME = "outfit_prompt.txt"
|
||||
_OUTFIT_PROMPT_FILENAME = "outfit_prompt.txt" # legacy single prompt (migrated)
|
||||
_OUTFIT_PROMPTS_FILENAME = "outfit_prompts.json" # multi named templates (§7.2)
|
||||
DEFAULT_OUTFIT_PROMPT_NAME = "默认"
|
||||
|
||||
# Default outfit prompt (docs/11 §7). Persisted to outfit_prompt.txt on first save.
|
||||
# Default outfit prompt (docs/11 §7). Seeded into outfit_prompts.json on first run.
|
||||
DEFAULT_OUTFIT_PROMPT = (
|
||||
"为商品「{title}」生成人物上身实穿效果图:真人模特正面穿着这件衣服,"
|
||||
"完整保留款式、版型、颜色与印花图案,自然光、纯色棚拍背景,"
|
||||
@@ -139,3 +142,51 @@ def save_outfit_prompt(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)
|
||||
|
||||
|
||||
def _normalize_prompts(data):
|
||||
"""Keep only valid {name, text} entries (non-empty name, string text)."""
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
out = []
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
name = str(item.get("name", "")).strip()
|
||||
text = item.get("text", "")
|
||||
if name and isinstance(text, str):
|
||||
out.append({"name": name, "text": text})
|
||||
return out
|
||||
|
||||
|
||||
def load_outfit_prompts():
|
||||
"""Load named 话术 templates (docs/11 §7.2); always returns >= 1.
|
||||
|
||||
Missing/corrupt → migrate the legacy outfit_prompt.txt into a single
|
||||
「默认」template, or seed it from DEFAULT_OUTFIT_PROMPT.
|
||||
"""
|
||||
from services.file_service import get_config_path
|
||||
prompts_file = get_config_path(_OUTFIT_PROMPTS_FILENAME)
|
||||
if prompts_file.exists():
|
||||
try:
|
||||
with open(str(prompts_file), encoding="utf-8-sig") as f:
|
||||
prompts = _normalize_prompts(json.load(f))
|
||||
if prompts:
|
||||
return prompts
|
||||
except (json.JSONDecodeError, ValueError, OSError) as exc:
|
||||
logger.warning("Outfit prompts unreadable (%s): %s", exc, prompts_file)
|
||||
|
||||
# Seed / migrate (load_outfit_prompt reads the legacy txt or the default).
|
||||
return [{"name": DEFAULT_OUTFIT_PROMPT_NAME, "text": load_outfit_prompt()}]
|
||||
|
||||
|
||||
def save_outfit_prompts(prompts):
|
||||
"""Persist named 话术 templates as JSON (utf-8, no BOM). Does not raise."""
|
||||
from services.file_service import get_config_path
|
||||
prompts_file = get_config_path(_OUTFIT_PROMPTS_FILENAME)
|
||||
try:
|
||||
prompts_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(str(prompts_file), "w", encoding="utf-8") as f:
|
||||
json.dump(list(prompts), f, ensure_ascii=False, indent=2)
|
||||
logger.info("Outfit prompts saved to %s", prompts_file)
|
||||
except OSError as exc:
|
||||
logger.error("Failed to save outfit prompts to %s: %s", prompts_file, exc)
|
||||
|
||||
Reference in New Issue
Block a user