feat(ai-outfit): auto-append output requirements to prompt (§19.6)

- core/ai_outfit: OUTPUT_REQUIREMENTS constant + build_output_requirements();
  render_prompt(template, task, resolution=None) appends the block when a
  resolution is given; generate_outfit_image passes the current resolution.
- ai_outfit_panel: inline preview now uses render_prompt/build_output_requirements
  with the selected resolution and refreshes when the resolution combo changes,
  so the preview equals what is actually sent.
- tests: +3 cases (tail with resolution, none without, helper empty/filled);
  updated the success test for the appended tail. Full suite (12 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 10:40:28 +08:00
co-authored by Claude Opus 4.8
parent 031c420f33
commit d4082ec477
4 changed files with 62 additions and 13 deletions
+7 -4
View File
@@ -258,6 +258,7 @@ class AiOutfitPanel(QWidget):
self._retry_count.setRange(0, 10)
self._resolution = QComboBox()
self._resolution.addItems(_RESOLUTIONS)
self._resolution.currentIndexChanged.connect(self._refresh_preview)
self._quality = QComboBox()
self._quality.addItems(_QUALITIES)
@@ -472,17 +473,19 @@ class AiOutfitPanel(QWidget):
def _refresh_preview(self):
if not hasattr(self, "_preview_view"):
return
from core.ai_outfit import build_output_requirements, render_prompt
template = self._prompt_edit.toPlainText()
self._preview_warn.setVisible("{title}" not in template)
if "{title}" not in template:
self._preview_warn.setText("⚠ 话术缺少 {title} 占位符")
# Mirror what actually gets sent: append the output-requirements block
# for the currently selected resolution (docs/11 §7.1).
resolution = self._resolution.currentText() if hasattr(self, "_resolution") else None
task = self._sample_combo.currentData() if hasattr(self, "_sample_combo") else None
if task is None:
self._preview_view.setPlainText(template)
self._preview_view.setPlainText(template + build_output_requirements(resolution))
else:
rendered = template.replace("{title}", task.title).replace(
"{product_id}", task.product_id)
self._preview_view.setPlainText(rendered)
self._preview_view.setPlainText(render_prompt(template, task, resolution))
# -- run control ----------------------------------------------------
+28 -4
View File
@@ -25,10 +25,34 @@ QUALITY_PRESETS = {
MAX_JPG_BYTES = 2 * 1024 * 1024
_INVALID_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
# Auto-appended to every prompt (ported from 标题生成产品图工具; docs/11 §7.1).
# Only 参考解析度 is dynamic (the chosen resolution); the rest is fixed.
OUTPUT_REQUIREMENTS = (
"\n\n批量生成输出要求:"
"\n- 参考解析度:{resolution}"
"\n- 固定 1:1 正方形主图"
"\n- 必须结合商品标题与参考商品图片"
"\n- 服装本身、版型、颜色与图案不可跑版"
)
def render_prompt(template, task):
"""Render an outfit prompt for one task."""
return str(template).replace("{title}", task.title).replace("{product_id}", task.product_id)
def build_output_requirements(resolution):
"""Return the auto-appended output-requirements block (empty if no resolution)."""
if not resolution:
return ""
return OUTPUT_REQUIREMENTS.format(resolution=resolution)
def render_prompt(template, task, resolution=None):
"""Render the final prompt for one task.
Replaces {title}/{product_id}, then appends the fixed output-requirements
block when *resolution* is given (docs/11 §7.1). The user's 话术 box holds
only the creative part; structural requirements are appended by code.
"""
rendered = str(template).replace("{title}", task.title).replace(
"{product_id}", task.product_id)
return rendered + build_output_requirements(resolution)
def safe_product_filename(product_id):
@@ -88,7 +112,7 @@ def generate_outfit_image(
raise TypeError("task must be OutfitTask")
try:
prompt = render_prompt(prompt_template, task)
prompt = render_prompt(prompt_template, task, resolution=resolution)
client = api_client or ImageApiClient(model_config)
image_bytes = client.generate(prompt, task.garment_path, resolution=resolution)
output_path = make_outfit_output_path(output_dir, task.product_id)