feat(ai-studio): clarify hosted model tiers
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""User-facing helpers for cmhub hosted model aliases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
TIER_DEFAULT = "default"
|
||||
TIER_HIGH_QUALITY = "high_quality"
|
||||
TIER_ECONOMICAL = "economical"
|
||||
|
||||
TIER_LABELS = {
|
||||
TIER_DEFAULT: "默认档",
|
||||
TIER_HIGH_QUALITY: "高质量档",
|
||||
TIER_ECONOMICAL: "省点档",
|
||||
}
|
||||
|
||||
TIER_DESCRIPTIONS = {
|
||||
TIER_DEFAULT: "日常批量,质量、速度和成本平衡",
|
||||
TIER_HIGH_QUALITY: "重点商品或失败重试,质量优先,成本可能更高",
|
||||
TIER_ECONOMICAL: "低价值 SKU 或初稿,省点优先,上线前需人工复核",
|
||||
}
|
||||
|
||||
|
||||
def normalize_tier(value):
|
||||
text = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
||||
if text in {"high", "quality", "high_quality", "premium", "pro", "sol"}:
|
||||
return TIER_HIGH_QUALITY
|
||||
if text in {"low", "cheap", "economy", "economical", "saving", "lite", "luna"}:
|
||||
return TIER_ECONOMICAL
|
||||
if text in {"default", "standard", "balanced", "balance", "terra"}:
|
||||
return TIER_DEFAULT
|
||||
return ""
|
||||
|
||||
|
||||
def model_tier(model):
|
||||
model = model if isinstance(model, dict) else {}
|
||||
for key in ("tier", "model_tier", "quality_tier", "cmhub_tier", "recommended_tier"):
|
||||
tier = normalize_tier(model.get(key))
|
||||
if tier:
|
||||
return tier
|
||||
haystack = " ".join(_model_text_values(model)).lower()
|
||||
if any(token in haystack for token in ("省点", "低成本", "高频", "high-volume", "luna", "economy", "cheap", "lite")):
|
||||
return TIER_ECONOMICAL
|
||||
if any(token in haystack for token in ("高质量", "旗舰", "重点", "失败重试", "premium", "high-quality", "sol")):
|
||||
return TIER_HIGH_QUALITY
|
||||
if any(token in haystack for token in ("平衡", "默认", "日常", "standard", "balanced", "terra")):
|
||||
return TIER_DEFAULT
|
||||
alias = str(model.get("alias") or "").lower()
|
||||
if alias.endswith("-hd") or alias.endswith("_hd") or alias.endswith(".hd") or alias == "hd":
|
||||
return TIER_HIGH_QUALITY
|
||||
return TIER_DEFAULT
|
||||
|
||||
|
||||
def tier_label(tier):
|
||||
return TIER_LABELS.get(normalize_tier(tier) or tier, TIER_LABELS[TIER_DEFAULT])
|
||||
|
||||
|
||||
def tier_description(tier):
|
||||
return TIER_DESCRIPTIONS.get(normalize_tier(tier) or tier, TIER_DESCRIPTIONS[TIER_DEFAULT])
|
||||
|
||||
|
||||
def model_display_name(model):
|
||||
model = model if isinstance(model, dict) else {}
|
||||
for key in ("display_name", "name", "label"):
|
||||
value = str(model.get(key) or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return str(model.get("alias") or "").strip()
|
||||
|
||||
|
||||
def price_text(prices):
|
||||
if not isinstance(prices, list):
|
||||
return ""
|
||||
parts = []
|
||||
for item in prices[:3]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
cost = item.get("points_cost")
|
||||
if cost is None:
|
||||
cost = item.get("cost")
|
||||
if cost is None:
|
||||
continue
|
||||
resolution = item.get("resolution") or item.get("name") or ""
|
||||
parts.append(f"{resolution}:{cost}点" if resolution else f"{cost}点")
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def alias_label(model):
|
||||
model = model if isinstance(model, dict) else {}
|
||||
alias = str(model.get("alias") or "").strip()
|
||||
display_name = model_display_name(model)
|
||||
tier = model_tier(model)
|
||||
parts = [tier_label(tier)]
|
||||
if display_name and display_name != alias:
|
||||
parts.append(display_name)
|
||||
elif alias:
|
||||
parts.append(alias)
|
||||
prices = price_text(model.get("prices"))
|
||||
if prices:
|
||||
parts.append(prices)
|
||||
if model.get("requires_image"):
|
||||
parts.append("需参考图")
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def alias_tooltip(model):
|
||||
model = model if isinstance(model, dict) else {}
|
||||
alias = str(model.get("alias") or "").strip()
|
||||
tier = model_tier(model)
|
||||
usage = _recommended_for_text(model)
|
||||
lines = [f"{tier_label(tier)}:{usage or tier_description(tier)}"]
|
||||
if alias:
|
||||
lines.append(f"cmhub 别名:{alias}")
|
||||
prices = price_text(model.get("prices"))
|
||||
if prices:
|
||||
lines.append(f"扣点:{prices}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def configured_alias_summary(alias, *, model=None):
|
||||
alias = str(alias or "").strip()
|
||||
if model is None:
|
||||
model = {"alias": alias}
|
||||
else:
|
||||
model = dict(model)
|
||||
model.setdefault("alias", alias)
|
||||
tier = model_tier(model)
|
||||
if alias:
|
||||
return f"cmhub 托管{tier_label(tier)},生图别名 {alias},扣点以返回结果为准"
|
||||
return "cmhub 托管默认档,未配置生图别名;请先到⑤设置选择生图别名"
|
||||
|
||||
|
||||
def _recommended_for_text(model):
|
||||
for key in ("recommended_for", "usage", "description"):
|
||||
value = model.get(key)
|
||||
if isinstance(value, (list, tuple)):
|
||||
text = "、".join(str(item).strip() for item in value if str(item).strip())
|
||||
else:
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
def _model_text_values(model):
|
||||
for key in (
|
||||
"alias",
|
||||
"display_name",
|
||||
"name",
|
||||
"label",
|
||||
"tier",
|
||||
"model_tier",
|
||||
"quality_tier",
|
||||
"recommended_tier",
|
||||
"recommended_for",
|
||||
"description",
|
||||
):
|
||||
value = model.get(key)
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
for item in value:
|
||||
text = str(item or "").strip()
|
||||
if text:
|
||||
yield text
|
||||
elif isinstance(value, dict):
|
||||
for item in value.values():
|
||||
text = str(item or "").strip()
|
||||
if text:
|
||||
yield text
|
||||
else:
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
yield text
|
||||
tags = model.get("tags")
|
||||
if isinstance(tags, (list, tuple, set)):
|
||||
for tag in tags:
|
||||
text = str(tag or "").strip()
|
||||
if text:
|
||||
yield text
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
from PySide6.QtCore import QMimeData
|
||||
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
||||
|
||||
from ... import accounts, appconfig, db, image_studio, image_studio_export, prompts
|
||||
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, prompts
|
||||
from .. import file_manager
|
||||
from ..widgets import *
|
||||
from ..workers import (
|
||||
@@ -278,6 +278,7 @@ class ImageStudioTab(QWidget):
|
||||
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
self._refresh_model_hint()
|
||||
self.refresh_accounts()
|
||||
self.refresh_templates()
|
||||
self.refresh_projects()
|
||||
@@ -434,7 +435,7 @@ class ImageStudioTab(QWidget):
|
||||
form.addRow("比例", self.aspect_combo)
|
||||
layout.addLayout(form)
|
||||
|
||||
self.billing_label = QLabel("cmhub 托管模型:扣点以返回结果为准")
|
||||
self.billing_label = QLabel("cmhub 托管默认档:扣点以返回结果为准")
|
||||
self.billing_label.setObjectName("imageStudioBillingLabel")
|
||||
self.billing_label.setWordWrap(True)
|
||||
layout.addWidget(self.billing_label)
|
||||
@@ -1045,7 +1046,7 @@ class ImageStudioTab(QWidget):
|
||||
self.progress_bar.setRange(0, count)
|
||||
self.progress_bar.setValue(0)
|
||||
self.log_view.clear()
|
||||
self._append_log(f"[AI工场] 本轮生图开始:{count} 张,来源 cmhub 托管模型")
|
||||
self._append_log(f"[AI工场] 本轮生图开始:{count} 张,使用 {self._cmhub_image_model_summary()}")
|
||||
worker = ImageStudioGenerateJobsWorker(
|
||||
self.current_project.id,
|
||||
source.id,
|
||||
@@ -1189,7 +1190,7 @@ class ImageStudioTab(QWidget):
|
||||
self.progress_bar.setRange(0, total)
|
||||
self.progress_bar.setValue(done)
|
||||
if payload.get("points_balance") is not None:
|
||||
text = f"cmhub 托管模型:余额 {payload.get('points_balance')}"
|
||||
text = f"{self._cmhub_image_model_summary()};余额 {payload.get('points_balance')}"
|
||||
if payload.get("points_cost") is not None:
|
||||
text += f",本张扣点 {payload.get('points_cost')}"
|
||||
self.billing_label.setText(text)
|
||||
@@ -1339,6 +1340,13 @@ class ImageStudioTab(QWidget):
|
||||
scrollbar = self.log_view.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
def _refresh_model_hint(self):
|
||||
self.billing_label.setText(self._cmhub_image_model_summary())
|
||||
|
||||
def _cmhub_image_model_summary(self):
|
||||
alias = appconfig.cmhub_config(self.config).get("image_alias", "")
|
||||
return cmhub_models.configured_alias_summary(alias)
|
||||
|
||||
def _message(self, title, text):
|
||||
box = QMessageBox(self)
|
||||
box.setWindowTitle(str(title or "提示"))
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
|
||||
from ... import ai as ai_module
|
||||
from ... import chrome
|
||||
from ... import cmhub_models
|
||||
from ..widgets import *
|
||||
from ..workers import AIModelTestWorker as _RealAIModelTestWorker
|
||||
from ..workers import CMHubSettingsWorker as _RealCMHubSettingsWorker
|
||||
@@ -1250,7 +1251,8 @@ class SettingsTab(QWidget):
|
||||
alias = str(model.get("alias") or "").strip()
|
||||
if not alias or alias in added:
|
||||
continue
|
||||
combo.addItem(self._cmhub_alias_label(model), alias)
|
||||
combo.addItem(cmhub_models.alias_label(model), alias)
|
||||
combo.setItemData(combo.count() - 1, cmhub_models.alias_tooltip(model), Qt.ToolTipRole)
|
||||
added.add(alias)
|
||||
if selected and selected not in added:
|
||||
combo.addItem(f"{selected}(已保存)", selected)
|
||||
@@ -1272,32 +1274,6 @@ class SettingsTab(QWidget):
|
||||
items.append(model)
|
||||
return items
|
||||
|
||||
def _cmhub_alias_label(self, model):
|
||||
alias = str(model.get("alias") or "").strip()
|
||||
price_text = self._cmhub_price_text(model.get("prices"))
|
||||
parts = [alias]
|
||||
if price_text:
|
||||
parts.append(price_text)
|
||||
if model.get("requires_image"):
|
||||
parts.append("需参考图")
|
||||
return " · ".join(parts)
|
||||
|
||||
def _cmhub_price_text(self, prices):
|
||||
if not isinstance(prices, list):
|
||||
return ""
|
||||
parts = []
|
||||
for item in prices[:3]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
cost = item.get("points_cost")
|
||||
if cost is None:
|
||||
cost = item.get("cost")
|
||||
if cost is None:
|
||||
continue
|
||||
resolution = item.get("resolution") or item.get("name") or ""
|
||||
parts.append(f"{resolution}:{cost}点" if resolution else f"{cost}点")
|
||||
return "/".join(parts)
|
||||
|
||||
def _cmhub_alias_count(self, operation):
|
||||
combo = self.cmhub_title_alias_combo if operation == "title" else self.cmhub_image_alias_combo
|
||||
return sum(1 for index in range(combo.count()) if combo.itemData(index))
|
||||
|
||||
+12
-1
@@ -57,7 +57,7 @@ def _format_image_studio_event(event):
|
||||
if result:
|
||||
prefix += f":{result}"
|
||||
if detail:
|
||||
prefix += f",{diagnostics.redact_log_text(detail)}"
|
||||
prefix += f",{_image_studio_user_detail(detail)}"
|
||||
if event.get("points_cost") is not None:
|
||||
prefix += f",扣点 {event.get('points_cost')}"
|
||||
if event.get("points_balance") is not None:
|
||||
@@ -65,6 +65,17 @@ def _format_image_studio_event(event):
|
||||
return prefix
|
||||
|
||||
|
||||
def _image_studio_user_detail(detail):
|
||||
text = diagnostics.redact_log_text(str(detail or "")).replace("\r", " ").replace("\n", " ").strip()
|
||||
text = _USER_LOG_URL_RE.sub("[链接已隐藏]", text)
|
||||
text = _USER_LOG_PATH_RE.sub("[接口路径已隐藏]", text)
|
||||
text = text.replace("GET [链接已隐藏]", "请求 cmhub")
|
||||
text = text.replace("POST [链接已隐藏]", "请求 cmhub")
|
||||
if len(text) > 180:
|
||||
return text[:177] + "..."
|
||||
return text
|
||||
|
||||
|
||||
class ImageStudioPullImagesWorker(BaseWorker):
|
||||
"""Read Shopee main image URLs for one AI studio project in background."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user