Files

331 lines
11 KiB
Python
Raw Permalink Normal View History

2026-07-11 14:49:39 +08:00
"""User-facing helpers for cmhub hosted model aliases."""
from __future__ import annotations
import copy
from decimal import Decimal, InvalidOperation
import threading
import time
2026-07-11 14:49:39 +08:00
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 或初稿,省点优先,上线前需人工复核",
}
# The catalog is deliberately process-local. It only caches public model metadata,
# never the API key or a price estimate chosen by a user.
MODEL_CATALOG_CACHE_TTL_SECONDS = 300
_MODEL_CATALOG_CACHE = {}
_MODEL_CATALOG_CACHE_LOCK = threading.RLock()
_CATALOG_ALL_ALIASES_KEY = "*"
_PRICE_CONDITION_KEYS = (
"resolution",
"name",
"quality",
"size",
"ratio",
"aspect_ratio",
"tier",
"condition",
"conditions",
"min_images",
"max_images",
"image_count",
)
def cache_model_catalog(base_url, models, *, now=None):
"""Cache one fetched catalog in memory for all aliases it contains."""
normalized_base_url = _catalog_base_url(base_url)
if not normalized_base_url:
return
copied_models = [copy.deepcopy(model) for model in list(models or []) if isinstance(model, dict)]
fetched_at = float(time.monotonic() if now is None else now)
entry = (fetched_at, copied_models)
aliases = {
str(model.get("alias") or "").strip()
for model in copied_models
if str(model.get("alias") or "").strip()
}
with _MODEL_CATALOG_CACHE_LOCK:
_MODEL_CATALOG_CACHE[(normalized_base_url, _CATALOG_ALL_ALIASES_KEY)] = entry
for alias in aliases:
_MODEL_CATALOG_CACHE[(normalized_base_url, alias)] = entry
def cached_model_catalog(base_url, alias="", *, max_age_seconds=None, now=None):
"""Return a fresh catalog copy for a normalized gateway/alias pair, if available."""
normalized_base_url = _catalog_base_url(base_url)
normalized_alias = str(alias or "").strip()
if not normalized_base_url:
return None
max_age = MODEL_CATALOG_CACHE_TTL_SECONDS if max_age_seconds is None else max_age_seconds
try:
max_age = float(max_age)
except (TypeError, ValueError):
return None
current = float(time.monotonic() if now is None else now)
keys = [(normalized_base_url, normalized_alias)] if normalized_alias else []
keys.append((normalized_base_url, _CATALOG_ALL_ALIASES_KEY))
with _MODEL_CATALOG_CACHE_LOCK:
for key in keys:
entry = _MODEL_CATALOG_CACHE.get(key)
if entry is None:
continue
fetched_at, models = entry
if current - fetched_at > max_age:
continue
return [copy.deepcopy(model) for model in models]
return None
def clear_model_catalog_cache():
"""Clear the transient catalog cache. Intended for tests and gateway changes."""
with _MODEL_CATALOG_CACHE_LOCK:
_MODEL_CATALOG_CACHE.clear()
def unambiguous_points_cost(models, alias, operation_type, *, requires_image=None):
"""Return the unique unconditional price for one configured alias, otherwise ``None``."""
normalized_alias = str(alias or "").strip()
normalized_operation = str(operation_type or "").strip().lower()
matches = [
model
for model in list(models or [])
if isinstance(model, dict) and str(model.get("alias") or "").strip() == normalized_alias
]
if len(matches) != 1:
return None
model = matches[0]
if str(model.get("operation_type") or "").strip().lower() != normalized_operation:
return None
if str(model.get("pricing_status") or "").strip().lower() != "priced":
return None
if requires_image is True and model.get("requires_image") is not True:
return None
if requires_image is False and model.get("requires_image") is not False:
return None
prices = model.get("prices")
if not isinstance(prices, list) or len(prices) != 1 or not isinstance(prices[0], dict):
return None
price = prices[0]
if any(_has_value(price.get(key)) for key in _PRICE_CONDITION_KEYS):
return None
return _points_cost_value(price)
def format_points_cost(value):
"""Format a catalog points value without inventing precision."""
if value is None:
return ""
decimal_value = _points_cost_decimal(value)
if decimal_value is None:
return ""
text = format(decimal_value.normalize(), "f")
if "." in text:
text = text.rstrip("0").rstrip(".")
return text or "0"
def _catalog_base_url(base_url):
return str(base_url or "").strip().rstrip("/").lower()
def _has_value(value):
return value not in (None, "", [], {}, ())
def _points_cost_value(price):
if not isinstance(price, dict):
return None
if "points_cost" not in price:
return None
return _points_cost_decimal(price.get("points_cost"))
def _points_cost_decimal(value):
if isinstance(value, bool) or value is None:
return None
try:
decimal_value = Decimal(str(value))
except (InvalidOperation, ValueError):
return None
if not decimal_value.is_finite() or decimal_value < 0:
return None
return decimal_value
2026-07-11 14:49:39 +08:00
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 托管默认档,未配置生图别名;请先到设置选择生图别名"
2026-07-11 14:49:39 +08:00
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