Files

331 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""User-facing helpers for cmhub hosted model aliases."""
from __future__ import annotations
import copy
from decimal import Decimal, InvalidOperation
import threading
import time
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
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