"""find_agent 的确定性年龄画像标准化实现。""" from __future__ import annotations import json import re from typing import Any def _number(value: Any) -> float | None: if value is None or isinstance(value, bool): return None text = str(value).strip().replace("%", "") if not text: return None try: return float(text) except ValueError: return None def _ratio(value: Any) -> float | None: number = _number(value) if number is None: return None if number > 1: number /= 100 return min(max(number, 0.0), 1.0) def _age_dimension(portrait: dict[str, Any] | None) -> dict[str, Any]: if not isinstance(portrait, dict): return {} for wrapper_key in ("portrait_data", "content", "account"): wrapped = portrait.get(wrapper_key) if isinstance(wrapped, dict): wrapped_dimension = _age_dimension(wrapped) if wrapped_dimension: return wrapped_dimension for key in ("年龄", "age", "Age", "年龄分布"): value = portrait.get(key) if isinstance(value, dict): return value if portrait and all(isinstance(value, dict) for value in portrait.values()): return portrait return {} def _bucket_kind(label: str) -> str: compact = ( label.strip() .lower() .replace("岁", "") .replace("以上", "+") .replace("及", "") .replace(" ", "") ) if compact in {"50-", "50+", ">=50", "≥50", "51+", "60+", ">=60", "≥60"}: return "older" if re.search(r"(?:^|[^0-9])(50|51|60)\+$", compact): return "older" numbers = [int(value) for value in re.findall(r"\d+", compact)] if not numbers: return "unknown" if len(numbers) == 1: if numbers[0] >= 50 and any( marker in compact for marker in ("+", ">=", "≥", "以上") ): return "older" return "unknown" lower, upper = min(numbers), max(numbers) if lower >= 50: return "older" if lower >= 40 or upper >= 50: return "mature" return "younger" def _normalize_portrait(portrait: dict[str, Any] | None) -> dict[str, Any]: dimension = _age_dimension(portrait) buckets: list[dict[str, Any]] = [] older_ratio = 0.0 mature_ratio = 0.0 older_tgi_values: list[tuple[float, float]] = [] for label, raw_metrics in dimension.items(): metrics = raw_metrics if isinstance(raw_metrics, dict) else {} percentage = _ratio( metrics.get("percentage") if "percentage" in metrics else metrics.get("ratio") ) tgi = _number( metrics.get("preference") if "preference" in metrics else metrics.get("tgi") ) kind = _bucket_kind(str(label)) buckets.append( { "label": str(label), "kind": kind, "ratio": percentage, "tgi": tgi, } ) if percentage is None: continue if kind == "older": older_ratio += percentage if tgi is not None: older_tgi_values.append((tgi, percentage)) elif kind == "mature": mature_ratio += percentage weighted_tgi = None tgi_weight = sum(weight for _, weight in older_tgi_values) if tgi_weight: weighted_tgi = sum(tgi * weight for tgi, weight in older_tgi_values) / tgi_weight if not dimension: strength = "missing" elif ( older_ratio >= 0.45 or (older_ratio >= 0.35 and (weighted_tgi or 0) >= 100) or (older_ratio >= 0.20 and (weighted_tgi or 0) >= 130) ): strength = "strong" elif ( older_ratio >= 0.20 or (older_ratio > 0 and (weighted_tgi or 0) >= 100) or mature_ratio >= 0.30 ): strength = "moderate" else: strength = "weak" return { "has_age_portrait": bool(dimension), "older_ratio": round(older_ratio, 6), "older_tgi": round(weighted_tgi, 4) if weighted_tgi is not None else None, "mature_ratio": round(mature_ratio, 6), "strength": strength, "buckets": buckets, } def normalize_age_portrait_pair( content_portrait: dict[str, Any] | None, account_portrait: dict[str, Any] | None = None, ) -> dict[str, Any]: """Return the deterministic two-sided age normalization payload.""" content = _normalize_portrait(content_portrait) account = _normalize_portrait(account_portrait) content_has = content["has_age_portrait"] account_has = account["has_age_portrait"] if content_has and account_has: strong_set = {"strong", "moderate"} consistency = ( "aligned" if (content["strength"] in strong_set) == (account["strength"] in strong_set) else "conflict" ) cap = 1.0 elif account_has: consistency = "account_only" cap = 0.85 if account["strength"] == "strong" else 0.75 elif content_has: consistency = "content_only" cap = 1.0 else: consistency = "missing" cap = 0.50 return { "content": content, "account": account, "consistency": consistency, "elder_score_cap": cap, } def normalize_age_portraits( content_portrait: dict[str, Any], account_portrait: dict[str, Any] | None = None, ) -> str: """ 标准化视频点赞画像与作者粉丝画像中的年龄桶。 能识别接口实际返回的 `50-`,以及 `50+ / 50岁以上 / >=50 / 41-50` 等表达,统一输出直接老年比例、TGI、成熟人群代理比例和证据强度。 本工具只标准化证据,不把点赞用户伪装成转发用户。 Args: content_portrait: 视频 portrait_data,或其中的年龄字典。 account_portrait: 可选作者 portrait_data,或其中的年龄字典。 Returns: JSON,包含 content、account、consistency 和 elder_score_cap。 """ normalized = normalize_age_portrait_pair(content_portrait, account_portrait) content = normalized["content"] account = normalized["account"] consistency = normalized["consistency"] cap = normalized["elder_score_cap"] result = { "title": "年龄画像标准化", **normalized, "output": ( f"视频侧={content['strength']},作者侧={account['strength']}," f"一致性={consistency},E上限={cap}" ), } return json.dumps(result, ensure_ascii=False)