Files
cmbot/tests/test_ai_title.py
T
adminandClaude Opus 4.8 2fa488bd21 feat(ai-outfit): 左栏新增「标题生成」(看图→提示词→AI 文字标题→写回A列) (§19.18)
AI 穿搭页左栏新增独立的「生成标题」流程:用户写标题提示词,AI 看该行
衣服图(目录行取首图)生成电商标题,逐行立即写回 Excel A 列并刷新明细表;
完成后重载 Excel,紧接「开始生成」跑图即用新标题。移除原「最终生成要求预览」
腾出版面(docs/11 §17)。

- ai_text_service.py:AiTextClient 复用图像服务 HTTP 管道做文本输出;
  chat/gemini 带图视觉,images/images_edits 明确报错;extract_text 取首条标题
- ai_title.py + TitleResult:单行编排,never raises
- excel_service.write_title_result:只写 A 列、不动 D/E/F
- config_service:title_model 默认 + load/save_title_prompt + 默认标题话术
- 面板:标题生成组(提示词+标题模型下拉+保存+生成标题)置于话术组上方;
  _TitleWorker 顺序逐行+立即回填+刷新;与「开始生成」互斥;删预览相关组件
- 测试:文本解析/payload、generate_title(单文件/目录首图/失败)、
  write_title_result、面板标题组存在且无预览;全套 py37 通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:04:38 +08:00

109 lines
3.5 KiB
Python

"""Tests for AI title generation core (docs/11 §17)."""
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from PIL import Image
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
class TestAiTitleCore(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
def tearDown(self):
shutil.rmtree(str(self.tmp), ignore_errors=True)
def _make_image(self, path):
Image.new("RGB", (32, 32), (120, 80, 40)).save(str(path), format="PNG")
def _task(self, garment_path, title="占位标题", product_id=""):
from core.models import OutfitTask
return OutfitTask(row_index=2, title=title, product_id=product_id,
garment_path=str(garment_path))
def test_render_title_prompt_replaces_placeholders(self):
from core.ai_title import render_title_prompt
task = self._task("g.png", title="旧标题", product_id="TY001")
out = render_title_prompt("参考 {title} / {product_id}", task)
self.assertEqual(out, "参考 旧标题 / TY001")
def test_generate_title_single_file_success(self):
from core.ai_title import generate_title
garment = self.tmp / "shirt.png"
self._make_image(garment)
client = _RecordingTextClient("纯棉短袖T恤")
result = generate_title(self._task(garment), "起个标题 {title}",
model_config={}, api_client=client)
self.assertTrue(result.success, result.error)
self.assertEqual(result.generated_title, "纯棉短袖T恤")
self.assertEqual(client.image_paths, [str(garment)])
def test_generate_title_directory_uses_first_image(self):
from core.ai_title import generate_title
d = self.tmp / "FG201"
d.mkdir()
for name in ("b.png", "a.png", "c.png"):
self._make_image(d / name)
client = _RecordingTextClient("印花连衣裙")
result = generate_title(self._task(str(d) + "/"), "话术",
model_config={}, api_client=client)
self.assertTrue(result.success, result.error)
# Sorted: a.png is the first reference image.
self.assertEqual(client.image_paths, [str(d / "a.png")])
def test_generate_title_empty_directory_fails(self):
from core.ai_title import generate_title
d = self.tmp / "empty"
d.mkdir()
result = generate_title(self._task(str(d) + "/"), "话术",
model_config={}, api_client=_RecordingTextClient("x"))
self.assertFalse(result.success)
self.assertIn("没有图片", result.error)
def test_generate_title_client_error_aggregated(self):
from core.ai_title import generate_title
garment = self.tmp / "shirt.png"
self._make_image(garment)
result = generate_title(self._task(garment), "话术",
model_config={}, api_client=_FailingTextClient())
self.assertFalse(result.success)
self.assertIn("boom", result.error)
class _RecordingTextClient:
def __init__(self, title):
self._title = title
self.image_paths = []
def generate_text(self, prompt, image_path=None, resolution="1K"):
self.image_paths.append(str(image_path))
return self._title
class _FailingTextClient:
def generate_text(self, prompt, image_path=None, resolution="1K"):
raise RuntimeError("boom")
if __name__ == "__main__":
unittest.main()