| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- """AIGC 平台接口:把 CFA 入池视频推进 AIGC 生成计划。
- 3 个接口(文档见 `tech_documents/AIGC 接口调用说明(create_crawler_plan_by_{douyin,kuaishou}_content_id 链路).md`):
- 1. 建爬取计划 `/aigc/crawler/plan/save`(传一批平台内容 ID,抖音 aweme_id / 快手 photo_id,返回 crawler_plan_id)
- 2. 查生成计划详情 `/aigc/produce/plan/detail`
- 3. 存生成计划 `/aigc/produce/plan/save`(把 crawler_plan_id 作为输入源增量追加)
- 本模块只做纯接口调用,不含 dry-run / 安全闸等编排逻辑(那些在 scripts/push_runs_to_aigc.py)。
- """
- from __future__ import annotations
- from typing import Any
- import httpx
- # 各平台建爬取计划的固定参数(crawlerMode=5 按内容 ID 抓取)。
- # 抖音文档 §2.2:channel=2、filterMatchMode=2;快手文档 §2.2:channel=7、filterMatchMode=1。
- _PLATFORM_CRAWLER_PARAMS: dict[str, dict[str, Any]] = {
- "douyin": {
- "channel": 2,
- "contentModal": 4,
- "crawlerComment": 0,
- "crawlerMode": 5,
- "filterAccountMatchMode": 2,
- "filterContentMatchMode": 2,
- "frequencyType": 2,
- "planType": 2,
- "searchModeValues": [],
- "srtExtractFlag": 1,
- "videoKeyFrameType": 1,
- "voiceExtractFlag": 1,
- },
- "kuaishou": {
- "channel": 7,
- "contentModal": 4,
- "crawlerComment": 0,
- "crawlerMode": 5,
- "filterAccountMatchMode": 1,
- "filterContentMatchMode": 1,
- "frequencyType": 2,
- "planType": 2,
- "searchModeValues": [],
- "srtExtractFlag": 1,
- "videoKeyFrameType": 1,
- "voiceExtractFlag": 1,
- },
- }
- # 绑进生成计划时,匹配输入源组 + 新输入源的固定字段(channel 随平台:抖音 2 / 快手 7,文档 4.2)。
- _PLATFORM_INPUT_MATCH: dict[str, dict[str, Any]] = {
- "douyin": {"inputSourceModal": 4, "inputSourceChannel": 2, "contentType": 1},
- "kuaishou": {"inputSourceModal": 4, "inputSourceChannel": 7, "contentType": 1},
- }
- class AigcPlatformError(RuntimeError):
- pass
- class AigcPlatformClient:
- def __init__(
- self,
- *,
- base_url: str,
- token: str,
- paths: dict[str, str],
- timeout: float = 30.0,
- ) -> None:
- self._base_url = base_url.rstrip("/")
- self._token = token
- self._paths = paths
- self._timeout = timeout
- @classmethod
- def from_env(cls, env: dict[str, str]) -> "AigcPlatformClient":
- token = env.get("CONTENTFIND_API_AIGC_TOKEN") or env.get("AIGC_TOKEN") or ""
- if not token:
- raise AigcPlatformError("缺 CONTENTFIND_API_AIGC_TOKEN")
- return cls(
- base_url=env.get("CONTENTFIND_API_AIGC_BASE_URL") or "https://aigc-api.aiddit.com",
- token=token,
- paths={
- "crawler_plan_save": env.get("CONTENTFIND_AIGC_CRAWLER_PLAN_SAVE_PATH") or "/aigc/crawler/plan/save",
- "produce_plan_detail": env.get("CONTENTFIND_AIGC_PRODUCE_PLAN_DETAIL_PATH") or "/aigc/produce/plan/detail",
- "produce_plan_save": env.get("CONTENTFIND_AIGC_PRODUCE_PLAN_SAVE_PATH") or "/aigc/produce/plan/save",
- },
- )
- def _post(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
- body = {"baseInfo": {"token": self._token}, "params": params}
- response = httpx.post(
- f"{self._base_url}{path}",
- json=body,
- headers={"Content-Type": "application/json"},
- timeout=self._timeout,
- )
- response.raise_for_status()
- data = response.json()
- if not isinstance(data, dict):
- raise AigcPlatformError(f"{path}: 非法响应 {data!r}")
- if data.get("code") not in (0, "0"):
- raise AigcPlatformError(f"{path}: 业务失败 code={data.get('code')} msg={data.get('msg')}")
- return data.get("data") or {}
- def create_crawler_plan(self, content_ids: list[str], *, platform: str, name: str) -> str:
- """接口一:按平台建爬取计划(抖音/快手,按内容 ID 抓取),返回 crawler_plan_id(响应 data.id)。"""
- crawler_params = _PLATFORM_CRAWLER_PARAMS.get(platform)
- if crawler_params is None:
- raise AigcPlatformError(f"不支持的平台:{platform}")
- params = {**crawler_params, "inputModeValues": list(content_ids), "name": name}
- data = self._post(self._paths["crawler_plan_save"], params)
- plan_id = str(data.get("id") or "")
- if not plan_id:
- raise AigcPlatformError("接口一未返回 data.id")
- return plan_id
- def get_produce_plan(self, produce_plan_id: str) -> dict[str, Any]:
- """接口二:查生成计划完整详情(含 inputSourceGroups)。"""
- data = self._post(self._paths["produce_plan_detail"], {"id": produce_plan_id})
- if not data:
- raise AigcPlatformError(f"接口二空 data,produce_plan_id={produce_plan_id}")
- return data
- def bind_crawler_to_produce(
- self,
- *,
- crawler_plan_id: str,
- produce_plan_id: str,
- label: str,
- platform: str,
- ) -> None:
- """接口二+三:把 crawler_plan_id 作为新输入源,增量追加进生成计划并整体保存。
- 文档 4.4 要求:把接口二返回的完整 data 原样带上,只增量追加,避免字段缺失保存失败。
- """
- input_match = _PLATFORM_INPUT_MATCH.get(platform)
- if input_match is None:
- raise AigcPlatformError(f"不支持的平台:{platform}")
- plan = self.get_produce_plan(produce_plan_id)
- group = _match_input_group(plan.get("inputSourceGroups") or [], input_match)
- if group is None:
- raise AigcPlatformError(f"生成计划 {produce_plan_id} 找不到匹配的输入源组")
- group.setdefault("inputSources", []).append(
- {
- **input_match,
- "inputSourceType": 2,
- "inputSourceValue": crawler_plan_id,
- "inputSourceLabel": label,
- }
- )
- self._post(self._paths["produce_plan_save"], plan)
- def _match_input_group(
- groups: list[dict[str, Any]], input_match: dict[str, Any]
- ) -> dict[str, Any] | None:
- """找含匹配输入源(modal/channel/contentType,channel 随平台)的那一组(文档 4.2 step2);
- 没匹配但只有一组时退而用它;否则返回 None(不瞎猜)。"""
- for group in groups:
- for source in group.get("inputSources") or []:
- if all(source.get(key) == value for key, value in input_match.items()):
- return group
- return groups[0] if len(groups) == 1 else None
|