feat(product-suite): add cmhub vision AI writing
This commit is contained in:
@@ -97,6 +97,7 @@ class AITests(TempDirMixin, unittest.TestCase):
|
||||
"base_url": "https://cmhub.example.com",
|
||||
"title_alias": "title-standard",
|
||||
"image_alias": "image-hd",
|
||||
"vision_alias": "vision-standard",
|
||||
"connect_timeout": 3,
|
||||
"download_with_curl": "false",
|
||||
"check_balance_before_batch": False,
|
||||
@@ -454,6 +455,144 @@ class AITests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_cmhub_analyze_product_images_uses_vision_alias_and_safe_metadata(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, key_path = self._cmhub_config(temp_dir)
|
||||
first = os.path.join(temp_dir, "first.png")
|
||||
second = os.path.join(temp_dir, "second.jpg")
|
||||
with open(first, "wb") as fh:
|
||||
fh.write(b"first-image")
|
||||
with open(second, "wb") as fh:
|
||||
fh.write(b"second-image")
|
||||
calls = []
|
||||
|
||||
def fake_request(method, url, **kwargs):
|
||||
calls.append((method, url, kwargs))
|
||||
return _RequestsResponse(
|
||||
{
|
||||
"text": "浅绿色连帽上衣,突出宽松版型与日常穿搭场景。",
|
||||
"alias": "vision-standard",
|
||||
"model_used": "vision-provider",
|
||||
"points_cost": 1,
|
||||
"points_balance": 231,
|
||||
"call_id": "vision-call-1",
|
||||
}
|
||||
)
|
||||
|
||||
with mock.patch.object(ai._cmhub_session(), "request", side_effect=fake_request):
|
||||
result = ai.analyze_product_images(
|
||||
"补充卖点:避免夸大",
|
||||
"输出语言:繁体中文",
|
||||
[first, second],
|
||||
config=cfg,
|
||||
cmhub_config_path=key_path,
|
||||
)
|
||||
|
||||
self.assertEqual("浅绿色连帽上衣,突出宽松版型与日常穿搭场景。", result["text"])
|
||||
self.assertEqual(2, result["image_count"])
|
||||
self.assertEqual(1, result["metadata"]["points_cost"])
|
||||
self.assertEqual(231, result["metadata"]["points_balance"])
|
||||
self.assertNotIn("image_base64", result)
|
||||
self.assertEqual("POST", calls[0][0])
|
||||
self.assertEqual(
|
||||
"https://cmhub.example.com/api/v1/analyze/images",
|
||||
calls[0][1],
|
||||
)
|
||||
payload = calls[0][2]["json"]
|
||||
self.assertEqual("vision-standard", payload["model"])
|
||||
self.assertEqual(2, len(payload["images"]))
|
||||
self.assertTrue(payload["images"][0]["image_base64"].startswith("data:image/png;base64,"))
|
||||
self.assertTrue(payload["images"][1]["image_base64"].startswith("data:image/jpeg;base64,"))
|
||||
self.assertEqual({"temperature": 0.2}, payload["parameters"])
|
||||
self.assertEqual((3, ai.CMHUB_VISION_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_cmhub_analyze_product_images_requires_vision_alias_and_local_limits(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, key_path = self._cmhub_config(temp_dir)
|
||||
source = os.path.join(temp_dir, "source.jpg")
|
||||
with open(source, "wb") as fh:
|
||||
fh.write(b"source")
|
||||
cfg["ai"]["cmhub"]["vision_alias"] = ""
|
||||
|
||||
with self.assertRaises(ai.CMHubError) as raised:
|
||||
ai.analyze_product_images(
|
||||
"提示",
|
||||
"输出语言:繁体中文",
|
||||
[source],
|
||||
config=cfg,
|
||||
cmhub_config_path=key_path,
|
||||
)
|
||||
|
||||
self.assertEqual("cmhub_not_configured", raised.exception.code)
|
||||
self.assertIn("图片理解别名", str(raised.exception))
|
||||
|
||||
cfg["ai"]["cmhub"]["vision_alias"] = "vision-standard"
|
||||
paths = []
|
||||
for index in range(ai.CMHUB_VISION_MAX_IMAGES + 1):
|
||||
path = os.path.join(temp_dir, "source-%d.jpg" % index)
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(b"image")
|
||||
paths.append(path)
|
||||
with mock.patch.object(ai._cmhub_session(), "request") as request:
|
||||
with self.assertRaises(ai.AIError) as too_many:
|
||||
ai.analyze_product_images(
|
||||
"提示",
|
||||
"输出语言:繁体中文",
|
||||
paths,
|
||||
config=cfg,
|
||||
cmhub_config_path=key_path,
|
||||
)
|
||||
self.assertIn("最多支持", str(too_many.exception))
|
||||
request.assert_not_called()
|
||||
|
||||
oversized = os.path.join(temp_dir, "oversized.jpg")
|
||||
with open(oversized, "wb") as fh:
|
||||
fh.truncate(ai.CMHUB_VISION_MAX_IMAGE_BYTES + 1)
|
||||
with mock.patch.object(ai._cmhub_session(), "request") as request:
|
||||
with self.assertRaises(ai.AIError) as too_large:
|
||||
ai.analyze_product_images(
|
||||
"提示",
|
||||
"输出语言:繁体中文",
|
||||
[oversized],
|
||||
config=cfg,
|
||||
cmhub_config_path=key_path,
|
||||
)
|
||||
self.assertIn("超过10MiB", str(too_large.exception))
|
||||
request.assert_not_called()
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_cmhub_analyze_product_images_read_timeout_is_not_retried(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg, key_path = self._cmhub_config(temp_dir)
|
||||
source = os.path.join(temp_dir, "source.jpg")
|
||||
with open(source, "wb") as fh:
|
||||
fh.write(b"source")
|
||||
calls = []
|
||||
|
||||
def fake_request(method, url, **kwargs):
|
||||
calls.append((method, url, kwargs))
|
||||
raise ai.requests.exceptions.ReadTimeout("slow")
|
||||
|
||||
with mock.patch.object(ai._cmhub_session(), "request", side_effect=fake_request):
|
||||
with self.assertRaises(ai.CMHubError) as raised:
|
||||
ai.analyze_product_images(
|
||||
"提示",
|
||||
"输出语言:繁体中文",
|
||||
[source],
|
||||
config=cfg,
|
||||
cmhub_config_path=key_path,
|
||||
)
|
||||
|
||||
self.assertEqual("read_timeout", raised.exception.code)
|
||||
self.assertIn("结果未确认", str(raised.exception))
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual((3, ai.CMHUB_VISION_READ_TIMEOUT_SECONDS), calls[0][2]["timeout"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_cmhub_gen_cover_downloads_image_url_safely(self):
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
Reference in New Issue
Block a user