Просмотр исходного кода

创建广告素材,内部成本限制,外部uv限制

wangyunpeng 16 часов назад
Родитель
Сommit
af6b84fd50

+ 2 - 0
examples/auto_put_ad_mini/DEPLOYMENT.md

@@ -237,7 +237,9 @@ SLS_ERROR_LOGSTORE=error-log
 | `EXTERNAL_RECALL_CANDIDATE_LIMIT` | 外部素材进入 UV 排序的候选上限 | `300` | 否 |
 | `EXTERNAL_RECALL_EDIT_LIMIT_PER_LANDING` | 单个承接视频最多尝试清理的外部素材数 | `3` | 否 |
 | `EXTERNAL_RECALL_UV_WINDOW_DAYS` | 外部素材访问 UV 汇总窗口天数 | `90` | 否 |
+| `EXTERNAL_RECALL_MIN_UV` | 外部合作素材访问UV最低门槛(窗口求和,<此值排除) | `1000` | 否 |
 | `EXTERNAL_IMAGE_MODEL` | 外部素材参考图编辑模型 | 复用 `OPENROUTER_IMAGE_MODEL` | 否 |
+| `RECALL_MIN_COST_YUAN` | 素材召回历史成本最低门槛(元);历史投放素材与外部合作素材共用;无成本数据(cost is None)的素材严格排除,即使设为 0 | `1000` | 否 |
 
 ### 白名单配置
 

+ 5 - 0
examples/auto_put_ad_mini/config.py

@@ -1026,6 +1026,11 @@ RECALL_PARALLEL_MAX_WORKERS = int(os.getenv("RECALL_PARALLEL_MAX_WORKERS", "4"))
 RECALL_QUERY_LIMIT_PER_VIDEO = int(os.getenv("RECALL_QUERY_LIMIT_PER_VIDEO", "12"))
 RECALL_MIN_IMPRESSIONS = 0                        # 保留兼容配置;当前不作为硬筛。
 RECALL_MIN_CTR = 0.0                              # 保留兼容配置;当前不作为硬筛。
+# --- 素材召回历史成本门槛(2026-08-06 用户确认)---
+# 历史投放素材与外部合作素材共用:历史成本(cost, 元) >= 此值才进入候选池。
+# 严格语义:无历史成本数据(cost is None,从未投放过)的素材同样排除。
+# 注:设为 0 只关闭金额门槛,cost is None 仍排除(严格语义)。
+RECALL_MIN_COST_YUAN = int(os.getenv("RECALL_MIN_COST_YUAN", "1000"))
 RECALL_SOURCE_LABELS = ["内部素材"]               # 只要内部
 MAX_SAME_LANDING_PER_AD_IN_RUN = int(os.getenv("MAX_SAME_LANDING_PER_AD_IN_RUN", "1"))
 RECALL_CONFIG_CODES_FULL = [

+ 17 - 4
examples/auto_put_ad_mini/tools/external_recalled_material.py

@@ -40,6 +40,9 @@ EXTERNAL_RECALL_EDIT_LIMIT_PER_LANDING = int(
 EXTERNAL_RECALL_UV_WINDOW_DAYS = int(
     os.getenv("EXTERNAL_RECALL_UV_WINDOW_DAYS", "90")
 )
+EXTERNAL_RECALL_MIN_UV = int(
+    os.getenv("EXTERNAL_RECALL_MIN_UV", "1000")
+)
 EXTERNAL_IMAGE_MODEL = os.getenv(
     "EXTERNAL_IMAGE_MODEL", OPENROUTER_IMAGE_MODEL
 ).strip()
@@ -215,8 +218,9 @@ GROUP BY card_cover_id
 
 def rank_external_materials_by_visit_uv(
     materials: list[Material],
+    min_uv: int | None = None,
 ) -> list[Material]:
-    """Attach configured-window visit UV and sort by UV, then similarity."""
+    """Attach configured-window visit UV, filter by min_uv, sort by UV then similarity."""
     if not materials:
         return []
     totals, start_dt, end_dt = _query_visit_uv_30d(
@@ -227,14 +231,20 @@ def rank_external_materials_by_visit_uv(
         material.raw["visit_uv_30d"] = material.visit_uv_30d
         material.raw["uv_window_start"] = start_dt
         material.raw["uv_window_end"] = end_dt
+    before = len(materials)
+    if min_uv is not None:
+        materials = [
+            m for m in materials if (m.visit_uv_30d or 0) >= min_uv
+        ]
+    dropped_uv = before - len(materials)
     ranked = sorted(
         materials,
         key=lambda material: (material.visit_uv_30d or 0, material.score or 0),
         reverse=True,
     )
     logger.info(
-        "[external_material] UV排序 candidates=%d days=%d window=%s-%s top=%s",
-        len(ranked), EXTERNAL_RECALL_UV_WINDOW_DAYS, start_dt, end_dt,
+        "[external_material] UV排序 candidates=%d dropped_uv=%d min_uv=%s days=%d window=%s-%s top=%s",
+        before, dropped_uv, min_uv, EXTERNAL_RECALL_UV_WINDOW_DAYS, start_dt, end_dt,
         [
             (material.material_id, material.visit_uv_30d, round(material.score, 4))
             for material in ranked[:5]
@@ -255,7 +265,10 @@ def recall_external_materials_for_video(
         element_features=element_features,
         apply_cover_blacklist=False,
     )
-    ranked = rank_external_materials_by_visit_uv(materials)
+    ranked = rank_external_materials_by_visit_uv(
+        materials,
+        min_uv=EXTERNAL_RECALL_MIN_UV,
+    )
     return ranked[:max(1, EXTERNAL_RECALL_CANDIDATE_LIMIT)]
 
 

+ 24 - 6
examples/auto_put_ad_mini/tools/material_recall.py

@@ -368,6 +368,7 @@ def _items_to_materials(
     sim_threshold: float,
     *,
     apply_cover_blacklist: bool = True,
+    min_cost: Optional[float] = None,
 ) -> tuple:
     """把召回 items 过滤 + 转 Material。
 
@@ -375,6 +376,7 @@ def _items_to_materials(
     - modality=MATERIAL(防御性)
     - cover URL 不在黑名单
     - score >= sim_threshold
+    - min_cost is not None 时:历史成本(cost, 元) >= min_cost(严格,无成本数据也排除)
 
     CTR / impressions 只作为审批展示和兜底排序参考,不再作为硬筛。
     返回 (materials, stats)。
@@ -383,6 +385,7 @@ def _items_to_materials(
     stats = {
         "blacklist": 0,
         "low_score": 0,
+        "low_cost": 0,
         "low_imp": 0,
         "low_ctr": 0,
     }
@@ -404,13 +407,18 @@ def _items_to_materials(
             continue
         md = it.get("materialDetail") or {}
         q = md.get("quality") or {}
+        raw_cost = q.get("cost")
+        cost = _as_float(raw_cost) if raw_cost is not None else None
+        if min_cost is not None and (cost is None or cost < min_cost):
+            stats["low_cost"] += 1
+            continue
         out.append(Material(
             material_id=str(mid),
             score=score,
             title=it.get("title") or "",
             cover=cover,
             video_url=it.get("videoUrl") or "",
-            cost=_as_float(q.get("cost")) if q.get("cost") is not None else None,
+            cost=cost,
             ctr=_as_float(q.get("ctr")) if q.get("ctr") is not None else None,
             cvr=_as_float(q.get("cvr")) if q.get("cvr") is not None else None,
             roi=_as_float(q.get("roi")) if q.get("roi") is not None else None,
@@ -577,6 +585,7 @@ def recall_materials_for_video(
     source_labels: Optional[List[str]] = None,
     element_features: Optional[Iterable] = None,
     apply_cover_blacklist: bool = True,
+    min_cost: Optional[float] = None,
 ) -> List[Material]:
     """素材召回:用 ODPS 多维特征并行召回并合并排序。
 
@@ -585,16 +594,21 @@ def recall_materials_for_video(
       2. 多 query 并行调用 batchByText。
       3. 汇总、material_id 去重、score>=阈值、按 cost 倒序。
 
-    当前硬筛只保留相似度阈值;曝光/CTR 进入审批表但不拦截。
+    硬筛:相似度阈值 + 历史成本(cost>=min_cost,min_cost 默认取 config.RECALL_MIN_COST_YUAN)。
+    曝光/CTR 进入审批表但不拦截。
     """
     from config import (
         RECALL_ALPHA, RECALL_DAYS, RECALL_DECONSTRUCT_BOOST,
-        RECALL_DISPLAY_K, RECALL_PARALLEL_MAX_WORKERS, RECALL_QUERY_LIMIT_PER_VIDEO,
+        RECALL_DISPLAY_K, RECALL_MIN_COST_YUAN, RECALL_PARALLEL_MAX_WORKERS,
+        RECALL_QUERY_LIMIT_PER_VIDEO,
         RECALL_SIM_THRESHOLD, RECALL_SOURCE_LABELS,
         RECALL_W_CTR, RECALL_W_CVR, RECALL_W_FISSION_RATE,
         RECALL_W_OPEN_RATE, RECALL_W_ROI,
     )
 
+    if min_cost is None:
+        min_cost = RECALL_MIN_COST_YUAN
+
     if element_features is None:
         element_features = landing.raw.get("element_features") or []
     queries = _build_recall_queries_from_features(
@@ -646,13 +660,15 @@ def recall_materials_for_video(
                 items,
                 RECALL_SIM_THRESHOLD,
                 apply_cover_blacklist=apply_cover_blacklist,
+                min_cost=min_cost,
             )
             query_materials.append((query, mats))
             logger.info(
-                "[material_recall]   策略=%s q=%r configCode=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d → 保留 %d",
+                "[material_recall]   策略=%s q=%r configCode=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d,⊘ cost<%.0f %d → 保留 %d",
                 query.strategy_name, query.query_text[:30], query.config_code,
                 len(items), stats["blacklist"], RECALL_SIM_THRESHOLD,
-                stats["low_score"], len(mats),
+                stats["low_score"], min_cost, stats["low_cost"],
+                len(mats),
             )
 
     merged = _merge_materials_by_policy(query_materials)
@@ -684,6 +700,7 @@ def recall_materials_for_video(
                 fallback_items,
                 RECALL_SIM_THRESHOLD,
                 apply_cover_blacklist=apply_cover_blacklist,
+                min_cost=min_cost,
             )
             fquery = RecallQuery(
                 query_text=query.query_text,
@@ -696,9 +713,10 @@ def recall_materials_for_video(
             )
             fmats = _merge_materials_by_policy([(fquery, fmats)])
             logger.info(
-                "[material_recall]     fallback=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d → 保留 %d(cost desc)",
+                "[material_recall]     fallback=%s 返回 %d 条,⊘ 黑名单 %d,⊘ score<%.2f %d,⊘ cost<%.0f %d → 保留 %d(cost desc)",
                 fallback_cc, len(fallback_items), fstats["blacklist"],
                 RECALL_SIM_THRESHOLD, fstats["low_score"],
+                min_cost, fstats["low_cost"],
                 len(fmats),
             )
             if fmats: