feat(product-suite): add cmhub vision AI writing
This commit is contained in:
@@ -47,6 +47,10 @@ class CMHubError(AIError):
|
||||
|
||||
CMHUB_IMAGE_MAX_BYTES = 20 * 1024 * 1024
|
||||
CMHUB_TITLE_READ_TIMEOUT_SECONDS = 600
|
||||
CMHUB_VISION_READ_TIMEOUT_SECONDS = 120
|
||||
CMHUB_VISION_MAX_IMAGES = 8
|
||||
CMHUB_VISION_MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
CMHUB_VISION_MAX_TOTAL_BYTES = 32 * 1024 * 1024
|
||||
CMHUB_IMAGE_READ_TIMEOUT_SECONDS = 900
|
||||
CMHUB_IMAGE_SUBMIT_READ_TIMEOUT_SECONDS = 36
|
||||
CMHUB_IMAGE_POLL_READ_TIMEOUT_SECONDS = 15
|
||||
@@ -162,6 +166,55 @@ def gen_title(
|
||||
return text
|
||||
|
||||
|
||||
def analyze_product_images(
|
||||
instruction,
|
||||
context,
|
||||
image_paths,
|
||||
*,
|
||||
config=None,
|
||||
cmhub_config_path=appconfig.CMHUB_CONFIG_PATH,
|
||||
on_event=None,
|
||||
):
|
||||
"""Use the dedicated cmhub vision route for product-suite AI writing."""
|
||||
|
||||
cfg = appconfig.load_config() if config is None else config
|
||||
ai_cfg = appconfig.ai_config(cfg)
|
||||
if _ai_backend(ai_cfg) != "cmhub":
|
||||
raise AIError("商品套图AI帮写需要使用 cmhub 图片理解服务,请到⑤设置配置 cmhub。")
|
||||
|
||||
paths = _validate_cmhub_vision_images(image_paths)
|
||||
runtime = _cmhub_runtime(cfg, "vision", cmhub_config_path)
|
||||
payload = {
|
||||
"prompt": _compose_product_suite_vision_prompt(instruction, context),
|
||||
"model": runtime["alias"],
|
||||
"images": [{"image_base64": _image_data_url(path)} for path in paths],
|
||||
"parameters": {"temperature": 0.2},
|
||||
}
|
||||
try:
|
||||
data = _cmhub_call_with_retry(
|
||||
"POST",
|
||||
appconfig.cmhub_request_url(runtime["base_url"], "/api/v1/analyze/images"),
|
||||
runtime["api_key"],
|
||||
payload=payload,
|
||||
connect_timeout=runtime["connect_timeout"],
|
||||
read_timeout=CMHUB_VISION_READ_TIMEOUT_SECONDS,
|
||||
attempts=1,
|
||||
on_retry=None,
|
||||
)
|
||||
except CMHubError as exc:
|
||||
raise _vision_cmhub_error(exc) from exc
|
||||
|
||||
_emit_cmhub_metadata(on_event, data, "vision_request")
|
||||
text = _extract_text(data).strip()
|
||||
if not text:
|
||||
raise AIError("图片理解服务未返回可用卖点,请稍后重试")
|
||||
return {
|
||||
"text": text,
|
||||
"image_count": len(paths),
|
||||
"metadata": _cmhub_metadata(data),
|
||||
}
|
||||
|
||||
|
||||
def gen_cover(
|
||||
cover_prompt,
|
||||
old_cover_path,
|
||||
@@ -1338,17 +1391,67 @@ def _download_and_save_cmhub_cover(request_result, on_step=None):
|
||||
return saved_path
|
||||
|
||||
|
||||
def _validate_cmhub_vision_images(image_paths):
|
||||
paths = [str(path or "").strip() for path in list(image_paths or [])]
|
||||
if not paths:
|
||||
raise AIError("请先添加至少一张可用商品原图")
|
||||
if len(paths) > CMHUB_VISION_MAX_IMAGES:
|
||||
raise AIError("图片理解最多支持%d张商品原图" % CMHUB_VISION_MAX_IMAGES)
|
||||
|
||||
total_size = 0
|
||||
for index, path in enumerate(paths, 1):
|
||||
if not path or not os.path.isfile(path):
|
||||
raise AIError("第%d张商品原图尚未下载完成,请稍后重试" % index)
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
except OSError as exc:
|
||||
raise AIError("第%d张商品原图无法读取,请稍后重试" % index) from exc
|
||||
if size > CMHUB_VISION_MAX_IMAGE_BYTES:
|
||||
raise AIError("第%d张商品原图超过10MiB,无法进行AI帮写" % index)
|
||||
total_size += size
|
||||
if total_size > CMHUB_VISION_MAX_TOTAL_BYTES:
|
||||
raise AIError("商品原图总大小超过32MiB,无法进行AI帮写")
|
||||
return paths
|
||||
|
||||
|
||||
def _vision_cmhub_error(exc):
|
||||
code = str(getattr(exc, "code", "") or "unknown")
|
||||
if code == "read_timeout":
|
||||
message = "等待图片理解结果超时,结果未确认,请先查看点数余额或稍后重试"
|
||||
elif code in {"connect_timeout", "network_error"}:
|
||||
message = "连接图片理解服务失败,结果未确认,请检查网络后稍后重试"
|
||||
else:
|
||||
message = _cmhub_user_message(code, "")
|
||||
if not message or message == code:
|
||||
message = "图片理解服务暂时不可用,请稍后重试"
|
||||
return CMHubError(
|
||||
code,
|
||||
message,
|
||||
status=getattr(exc, "status", None),
|
||||
retryable=False,
|
||||
retry_after=getattr(exc, "retry_after", None),
|
||||
)
|
||||
|
||||
|
||||
def _cmhub_runtime(config, operation, cmhub_config_path):
|
||||
hub = appconfig.cmhub_config(config)
|
||||
api_key = appconfig.get_cmhub_api_key(path=cmhub_config_path)
|
||||
alias_key = "title_alias" if operation == "title" else "image_alias"
|
||||
operation_config = {
|
||||
"title": ("title_alias", "生文别名"),
|
||||
"image": ("image_alias", "生图别名"),
|
||||
"vision": ("vision_alias", "图片理解别名"),
|
||||
}
|
||||
try:
|
||||
alias_key, alias_label = operation_config[str(operation or "")]
|
||||
except KeyError as exc:
|
||||
raise AIError("cmhub 操作类型无效") from exc
|
||||
missing = []
|
||||
if not hub.get("base_url"):
|
||||
missing.append("Base URL")
|
||||
if not api_key:
|
||||
missing.append("API Key")
|
||||
if not hub.get(alias_key):
|
||||
missing.append("生文别名" if operation == "title" else "生图别名")
|
||||
missing.append(alias_label)
|
||||
if missing:
|
||||
raise CMHubError(
|
||||
"cmhub_not_configured",
|
||||
@@ -1379,6 +1482,18 @@ def _compose_title_prompt(title_prompt, old_title):
|
||||
return "请只返回新标题,不要解释。"
|
||||
|
||||
|
||||
def _compose_product_suite_vision_prompt(instruction, context):
|
||||
return (
|
||||
"请分析当前电商商品原图,并根据图片可见信息生成可直接编辑的「商品卖点与要求」。"
|
||||
"请使用任务指定的输出语言,只输出卖点与画面要求正文,不要解释分析过程。"
|
||||
"内容应包含商品名称或品类、颜色、款式、可见细节、核心卖点、目标人群、使用场景"
|
||||
"和适合套图生成的画面要求。已有要求仅是补充约束,不能当作图片事实。"
|
||||
"不要虚构材质、尺寸、功能、认证、价格、物流承诺或图片中不可确认的信息。"
|
||||
"\n\n任务要求:\n%s\n\n任务上下文:\n%s"
|
||||
% (str(instruction or "").strip(), str(context or "").strip())
|
||||
)
|
||||
|
||||
|
||||
def _normalize_cmhub_resolution(resolution):
|
||||
value = str(resolution or "1k").strip().lower()
|
||||
mapping = {
|
||||
@@ -1697,14 +1812,20 @@ def _notify_cmhub_retry(callback, step, attempt, attempts, exc):
|
||||
pass
|
||||
|
||||
|
||||
def _emit_cmhub_metadata(callback, data, step):
|
||||
if callback is None or not isinstance(data, dict):
|
||||
return
|
||||
metadata = {
|
||||
def _cmhub_metadata(data):
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return {
|
||||
key: data.get(key)
|
||||
for key in ("alias", "model_used", "points_cost", "points_balance", "call_id")
|
||||
if data.get(key) is not None
|
||||
}
|
||||
|
||||
|
||||
def _emit_cmhub_metadata(callback, data, step):
|
||||
if callback is None:
|
||||
return
|
||||
metadata = _cmhub_metadata(data)
|
||||
if not metadata:
|
||||
return
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user