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:
@@ -258,6 +258,7 @@ class AiOutfitPanel(QWidget):
|
|||||||
self._retry_count.setRange(0, 10)
|
self._retry_count.setRange(0, 10)
|
||||||
self._resolution = QComboBox()
|
self._resolution = QComboBox()
|
||||||
self._resolution.addItems(_RESOLUTIONS)
|
self._resolution.addItems(_RESOLUTIONS)
|
||||||
|
self._resolution.currentIndexChanged.connect(self._refresh_preview)
|
||||||
self._quality = QComboBox()
|
self._quality = QComboBox()
|
||||||
self._quality.addItems(_QUALITIES)
|
self._quality.addItems(_QUALITIES)
|
||||||
|
|
||||||
@@ -472,17 +473,19 @@ class AiOutfitPanel(QWidget):
|
|||||||
def _refresh_preview(self):
|
def _refresh_preview(self):
|
||||||
if not hasattr(self, "_preview_view"):
|
if not hasattr(self, "_preview_view"):
|
||||||
return
|
return
|
||||||
|
from core.ai_outfit import build_output_requirements, render_prompt
|
||||||
template = self._prompt_edit.toPlainText()
|
template = self._prompt_edit.toPlainText()
|
||||||
self._preview_warn.setVisible("{title}" not in template)
|
self._preview_warn.setVisible("{title}" not in template)
|
||||||
if "{title}" not in template:
|
if "{title}" not in template:
|
||||||
self._preview_warn.setText("⚠ 话术缺少 {title} 占位符")
|
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
|
task = self._sample_combo.currentData() if hasattr(self, "_sample_combo") else None
|
||||||
if task is None:
|
if task is None:
|
||||||
self._preview_view.setPlainText(template)
|
self._preview_view.setPlainText(template + build_output_requirements(resolution))
|
||||||
else:
|
else:
|
||||||
rendered = template.replace("{title}", task.title).replace(
|
self._preview_view.setPlainText(render_prompt(template, task, resolution))
|
||||||
"{product_id}", task.product_id)
|
|
||||||
self._preview_view.setPlainText(rendered)
|
|
||||||
|
|
||||||
# -- run control ----------------------------------------------------
|
# -- run control ----------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+28
-4
@@ -25,10 +25,34 @@ QUALITY_PRESETS = {
|
|||||||
MAX_JPG_BYTES = 2 * 1024 * 1024
|
MAX_JPG_BYTES = 2 * 1024 * 1024
|
||||||
_INVALID_FILENAME_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
_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."""
|
def build_output_requirements(resolution):
|
||||||
return str(template).replace("{title}", task.title).replace("{product_id}", task.product_id)
|
"""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):
|
def safe_product_filename(product_id):
|
||||||
@@ -88,7 +112,7 @@ def generate_outfit_image(
|
|||||||
raise TypeError("task must be OutfitTask")
|
raise TypeError("task must be OutfitTask")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prompt = render_prompt(prompt_template, task)
|
prompt = render_prompt(prompt_template, task, resolution=resolution)
|
||||||
client = api_client or ImageApiClient(model_config)
|
client = api_client or ImageApiClient(model_config)
|
||||||
image_bytes = client.generate(prompt, task.garment_path, resolution=resolution)
|
image_bytes = client.generate(prompt, task.garment_path, resolution=resolution)
|
||||||
output_path = make_outfit_output_path(output_dir, task.product_id)
|
output_path = make_outfit_output_path(output_dir, task.product_id)
|
||||||
|
|||||||
@@ -1087,7 +1087,7 @@
|
|||||||
|
|
||||||
背景:旧项目最终提示词 = 用户话术 + 自动附加的输出要求(参考解析度 / 固定 1:1 / 结合标题与参考图 / 不可跑版);当前 cmbot 的 `render_prompt` 只替换 `{title}`,缺这段。决策:简体文案、始终自动附加(不做开关)。
|
背景:旧项目最终提示词 = 用户话术 + 自动附加的输出要求(参考解析度 / 固定 1:1 / 结合标题与参考图 / 不可跑版);当前 cmbot 的 `render_prompt` 只替换 `{title}`,缺这段。决策:简体文案、始终自动附加(不做开关)。
|
||||||
|
|
||||||
- [ ] `core/ai_outfit.py`:加 `OUTPUT_REQUIREMENTS` 常量 + `build_output_requirements(resolution)`;`render_prompt(template, task, resolution=None)` 在替换占位符后附加该段(`resolution` 为空不加);`generate_outfit_image` 传入当前 resolution
|
- [x] `core/ai_outfit.py`:加 `OUTPUT_REQUIREMENTS` 常量 + `build_output_requirements(resolution)`;`render_prompt(template, task, resolution=None)` 在替换占位符后附加该段(`resolution` 为空不加);`generate_outfit_image` 传入当前 resolution
|
||||||
- [ ] `app/widgets/ai_outfit_panel.py`:内嵌预览改用 `render_prompt`(带当前分辨率),分辨率下拉变化时刷新预览,使「预览 = 实际发送」
|
- [x] `app/widgets/ai_outfit_panel.py`:内嵌预览改用 `render_prompt`(带当前分辨率),分辨率下拉变化时刷新预览,使「预览 = 实际发送」
|
||||||
- [ ] 单测:带 `resolution` 追加尾巴且内容正确、无 `resolution` 不加;`build_output_requirements` 空/非空(加入 `tests/test_ai_outfit.py`)
|
- [x] 单测:带 `resolution` 追加尾巴且内容正确、无 `resolution` 不加;`build_output_requirements` 空/非空(加入 `tests/test_ai_outfit.py`,全套 12 文件绿)
|
||||||
- [ ] 同步 `docs/ui-ai-outfit.html` 预览框示例含该段并重渲 `.png`(可选)
|
- [ ] 同步 `docs/ui-ai-outfit.html` 预览框示例含该段并重渲 `.png`(可选,未做)
|
||||||
|
|||||||
+23
-1
@@ -39,8 +39,28 @@ class TestAiOutfitCore(unittest.TestCase):
|
|||||||
|
|
||||||
prompt = render_prompt("商品 {title} / {product_id}", self._task())
|
prompt = render_prompt("商品 {title} / {product_id}", self._task())
|
||||||
|
|
||||||
|
# No resolution -> no appended requirements tail.
|
||||||
self.assertEqual(prompt, "商品 纯棉短袖 / TY001")
|
self.assertEqual(prompt, "商品 纯棉短袖 / TY001")
|
||||||
|
|
||||||
|
def test_render_prompt_appends_requirements_with_resolution(self):
|
||||||
|
from core.ai_outfit import render_prompt
|
||||||
|
|
||||||
|
prompt = render_prompt("话术 {title}", self._task(), resolution="2K")
|
||||||
|
|
||||||
|
self.assertTrue(prompt.startswith("话术 纯棉短袖"))
|
||||||
|
self.assertIn("批量生成输出要求:", prompt)
|
||||||
|
self.assertIn("参考解析度:2K", prompt)
|
||||||
|
self.assertIn("不可跑版", prompt)
|
||||||
|
|
||||||
|
def test_build_output_requirements_empty_and_filled(self):
|
||||||
|
from core.ai_outfit import build_output_requirements
|
||||||
|
|
||||||
|
self.assertEqual(build_output_requirements(""), "")
|
||||||
|
self.assertEqual(build_output_requirements(None), "")
|
||||||
|
filled = build_output_requirements("4K")
|
||||||
|
self.assertIn("参考解析度:4K", filled)
|
||||||
|
self.assertIn("固定 1:1 正方形主图", filled)
|
||||||
|
|
||||||
def test_safe_product_filename_replaces_invalid_chars(self):
|
def test_safe_product_filename_replaces_invalid_chars(self):
|
||||||
from core.ai_outfit import safe_product_filename
|
from core.ai_outfit import safe_product_filename
|
||||||
|
|
||||||
@@ -83,7 +103,9 @@ class TestAiOutfitCore(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(result.success, result.error)
|
self.assertTrue(result.success, result.error)
|
||||||
self.assertTrue(Path(result.output_path).exists())
|
self.assertTrue(Path(result.output_path).exists())
|
||||||
self.assertEqual(client.prompt, "为 纯棉短袖 生成 TY001")
|
# generate passes resolution -> rendered prompt + appended requirements.
|
||||||
|
self.assertTrue(client.prompt.startswith("为 纯棉短袖 生成 TY001"))
|
||||||
|
self.assertIn("批量生成输出要求:", client.prompt)
|
||||||
self.assertTrue(client.image_path.endswith("garment.png"))
|
self.assertTrue(client.image_path.endswith("garment.png"))
|
||||||
|
|
||||||
def test_generate_outfit_image_failure(self):
|
def test_generate_outfit_image_failure(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user