""" price_parser.py 统一价格结构,所有模型类型输出相同字段: tier_min : 档位下限(token数 或 秒数),0 表示从0开始,None 表示无档位 tier_max : 档位上限(token数 或 秒数),None 表示无上限 tier_unit : 档位单位,"tokens" 或 "seconds",None 表示无档位 input_price : 输入价格(元/百万tokens 或 0),视频/图像为 0 output_price : 输出价格(元/百万tokens)或视频/图像的生成价格 currency : "CNY" unit : 计费单位原始字符串 label : 原始 key 视频规格 -> 秒数映射: 480P -> 0 ~ 480 720P -> 0 ~ 720 (或 481 ~ 720) 1080P -> 0 ~ 1080 (或 721 ~ 1080) 4K -> 0 ~ 2160 """ from __future__ import annotations import re from typing import Any, Dict, List, Optional # ── 视频规格 -> 最大秒数 ──────────────────────────────────────────────────────── _VIDEO_SPEC_MAX: Dict[str, int] = { "480P": 480, "480p": 480, "720P": 720, "720p": 720, "1080P": 1080, "1080p": 1080, "2K": 1440, "4K": 2160, } # 非 token 计费单位 _NON_TOKEN_UNITS = re.compile(r"每秒|每张|每次|每帧|/秒|/张|/次", re.I) # token 阶梯 key 正则 # 情况1:input<=128k / 32k Optional[int]: """把 '32k'/'128K'/'1M' 转成 token 整数。""" s = str(val).strip().upper().replace(",", "") m = re.match(r"^([\d.]+)\s*([KMG]?)$", s) if not m: return None num = float(m.group(1)) suffix = m.group(2) if suffix == "K": return int(num * 1_000) if suffix == "M": return int(num * 1_000_000) return int(num) def _parse_price(obj: Any) -> Optional[float]: if isinstance(obj, (int, float)): return float(obj) if not isinstance(obj, dict): return None # 优先取 price_original(原价),爬虫把折扣价和原价分开存 for key in ("price_original", "price", "price_current"): v = obj.get(key) if v is not None: try: return float(v) except (TypeError, ValueError): continue # 嵌套结构:{"8元": {"price": 8}, "24元": {"price": 24}},取最大 candidates: List[float] = [] for sub_v in obj.values(): if isinstance(sub_v, dict): p = _parse_price(sub_v) if p is not None: candidates.append(p) elif isinstance(sub_v, list): for item in sub_v: if isinstance(item, dict): p = _parse_price(item) if p is not None: candidates.append(p) if candidates: return max(candidates) return None def _parse_unit(obj: Any) -> Optional[str]: if isinstance(obj, dict): # 直接有 unit 字段 u = obj.get("unit") if u: return u # 新格式:嵌套结构,递归搜索 for sub_v in obj.values(): if isinstance(sub_v, dict): u = sub_v.get("unit") if u: return u elif isinstance(sub_v, list): for item in sub_v: if isinstance(item, dict): u = item.get("unit") if u: return u return None def _parse_tier_key(key: str): """解析 token 阶梯 key,返回 (min_tokens, max_tokens) 或 None。""" k = key.strip().lower().replace(" ", "") m = _TIER_RE.match(k) if m: lo_str, hi_str = m.group(1), m.group(2) lo = _to_tokens(lo_str) if lo_str else 0 hi = _to_tokens(hi_str) if hi_str else None return (lo, hi) # 只有下限:256k Optional[str]: """从 label 中提取视频规格,如 '视频生成(720P)' -> '720P'。""" m = re.search(r"[((]([^))]+)[))]", label) if m: spec = m.group(1).strip() if spec.upper() in {k.upper() for k in _VIDEO_SPEC_MAX}: return spec.upper() # 直接在 label 里找 for spec in _VIDEO_SPEC_MAX: if spec.upper() in label.upper(): return spec.upper() return None def _build_video_tiers(items: List[Dict]) -> List[Dict]: """ 把多个视频规格条目转成连续区间: 720P(0.6) + 1080P(1.0) -> [0, 720, input=0, output=0.6] [721, 1080, input=0, output=1.0] """ # 按 tier_max 排序 sorted_items = sorted(items, key=lambda x: x["_spec_max"]) result = [] prev_max = 0 for item in sorted_items: spec_max = item["_spec_max"] result.append({ "label": item["label"], "tier_min": prev_max + (1 if prev_max > 0 else 0), "tier_max": spec_max, "tier_unit": "seconds", "input_price": 0.0, "output_price": item["price"], "currency": item["currency"], "unit": item["unit"], }) prev_max = spec_max return result def parse_prices(prices: Dict[str, Any]) -> List[Dict]: result: List[Dict] = [] video_items: List[Dict] = [] input_entry: Optional[Dict] = None _CACHE_RE = re.compile(r"缓存|cache", re.I) # 预扫描:是否存在阶梯 key _has_tiers = any(_parse_tier_key(k) is not None for k in prices) # 有阶梯时,预先提取顶层 "output"/"输出" 里的所有独立 output 价格 # 爬虫把所有 output 价格汇总在顶层 output key 里,需要按大小分配给各阶梯 _output_prices: List[float] = [] if _has_tiers: for _ok in ("output", "输出"): if _ok in prices and isinstance(prices[_ok], dict): for _sub_v in prices[_ok].values(): _p = _parse_price(_sub_v) if _p is not None: _output_prices.append(_p) break _output_prices.sort() # 从小到大,与阶梯从低到高对应 for key, val in prices.items(): # 跳过缓存相关条目 if _CACHE_RE.search(key): continue if not isinstance(val, dict): continue # ── token 阶梯 ── tier = _parse_tier_key(key) if tier is not None: entry: Dict = { "label": key, "tier_min": tier[0], "tier_max": tier[1], "tier_unit": "tokens", "input_price": None, "output_price": None, "currency": "CNY", "unit": None, } for sub_key, sub_val in val.items(): sk = sub_key.strip() if _CACHE_RE.search(sk): continue price = _parse_price(sub_val) unit = _parse_unit(sub_val) if unit: entry["unit"] = unit if re.match(r"^输入|^input", sk, re.I): entry["input_price"] = price elif re.match(r"^输出|^output", sk, re.I): entry["output_price"] = price elif price is not None and entry["input_price"] is None: # 无标签的纯价格 → 当作输入价格 entry["input_price"] = price result.append(entry) continue # 有阶梯时,顶层 output 已预提取,跳过 if _has_tiers and re.match(r"^(输出|output)$", key.strip(), re.I): continue price = _parse_price(val) unit = _parse_unit(val) # ── 视频/图像按单位计费 ── if _NON_TOKEN_UNITS.search(unit or ""): spec = _extract_video_spec(key) if spec and spec in _VIDEO_SPEC_MAX: video_items.append({ "label": key, "_spec_max": _VIDEO_SPEC_MAX[spec], "price": price, "currency": "CNY", "unit": unit, }) else: result.append({ "label": key, "tier_min": None, "tier_max": None, "tier_unit": None, "input_price": 0.0, "output_price": price, "currency": "CNY", "unit": unit, }) continue # ── 简单非阶梯:输入 → 暂存,输出 → 配对 ── if re.match(r"^输入|^input", key.strip(), re.I): input_entry = {"price": price, "unit": unit} continue if re.match(r"^输出|^output", key.strip(), re.I): result.append({ "label": "input/output", "tier_min": None, "tier_max": None, "tier_unit": None, "input_price": input_entry["price"] if input_entry else None, "output_price": price, "currency": "CNY", "unit": unit or (input_entry["unit"] if input_entry else None), }) input_entry = None continue # 其他普通标签 result.append({ "label": key, "tier_min": None, "tier_max": None, "tier_unit": None, "input_price": price, "output_price": None, "currency": "CNY", "unit": unit, }) # 有阶梯时,把预提取的 output 价格按顺序分配给各阶梯 if _has_tiers and _output_prices: # 按 tier_min 从小到大排序(与 output 价格从小到大对应) tier_entries = [r for r in result if r.get("tier_unit") == "tokens"] tier_entries.sort(key=lambda e: e["tier_min"] or 0) for i, entry in enumerate(tier_entries): if i < len(_output_prices) and entry["output_price"] is None: entry["output_price"] = _output_prices[i] # 只有输入没有输出 → 单独一条 if input_entry: result.append({ "label": "input", "tier_min": None, "tier_max": None, "tier_unit": None, "input_price": input_entry["price"], "output_price": None, "currency": "CNY", "unit": input_entry["unit"], }) # 视频条目转成连续区间 if video_items: result.extend(_build_video_tiers(video_items)) return result