Files
MediaCrawler/mac-agent-os-main/05_tools/09_ave/scripts/directors/base.py
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

305 lines
14 KiB
Python
Raw 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.
"""
directors.base — 导演系统基类(替换点①)
==========================================
任何导演系统只需实现 `generate(story, anchors, options) -> Storyboard`,
即可接入视频工厂,下游(编排/能力/执行)完全不感知用的是哪个导演。
接入新导演(3 步):
1. 在 directors/ 加一个文件,实现 BaseDirector
2. 在 directors/__init__.py 的 DIRECTORS 注册
3. (可选)填 options_schema,前端自动生成该导演的选项面板
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
_SCRIPTS = Path(__file__).resolve().parent.parent
if str(_SCRIPTS) not in sys.path:
sys.path.insert(0, str(_SCRIPTS))
from schemas import GlobalAnchors, Storyboard # noqa: E402
class BaseDirector:
"""导演系统基类"""
id: str = ""
name: str = ""
kind: str = "skill" # skill(提示词包)| service(HTTP 服务)| legacy(本地既有)
description: str = ""
features: list[str] = [] # ["visual_concept", "two_pass", "consistency", "auto_pipeline"]
status: str = "ready" # ready | partial | planned
source: str = "" # 来源说明(如 ai-film-skills v5.2 / 本地既有)
options_schema: dict = {} # 该导演的专属选项(自动生成 UI 表单)
def generate(
self,
story: str,
anchors: GlobalAnchors | None = None,
options: dict | None = None,
) -> Storyboard:
"""故事 → 统一分镜 Schema(**唯一契约**)"""
raise NotImplementedError
def revise_shot(self, storyboard: Storyboard, shot_id: str,
instruction: str = "") -> Storyboard:
"""单镜头重生成(默认不支持,子类可覆写)"""
raise NotImplementedError(f"{self.name} 不支持单镜头重生成")
def info(self) -> dict:
"""给前端的能力描述"""
return {
"id": self.id, "name": self.name, "kind": self.kind,
"description": self.description, "features": self.features,
"status": self.status, "source": self.source,
"options_schema": self.options_schema,
}
# 通用选项(各导演共享的基础参数)
COMMON_OPTIONS = {
# ★ 推荐用法:给「目标总时长」,镜头数与每镜时长交给导演自行决定
"total_duration": {"type": "number", "min": 0, "max": 1800, "default": 0,
"label": "目标总时长(秒)",
"hint": "推荐用法:60 = 一分钟短片 · 180 = 三分钟 · 300 = 五分钟。"
"导演据此自由决定镜头数与每镜时长(充分解放创作)。填 0 = 不限制"},
# 以下两项**仅作参考**(导演可采纳可不采纳;填 0 = 不干涉)
"shot_count": {"type": "number", "min": 0, "max": 30, "default": 0,
"label": "镜头数(仅参考)",
"hint": "仅作参考:导演可采纳也可不采纳。填 0 = 让导演按内容自行决定(推荐)"},
"shot_duration": {"type": "number", "min": 0, "max": 15, "default": 0,
"label": "单镜时长(仅参考)",
"hint": "仅作参考:导演按内容与节奏自行决定。填 0 = 不限制"},
"shooting_style": {
"type": "enum",
"options": ["documentary_realism", "cinematic", "vlog_handheld",
"studio_clean", "selfie_pov", "interview_doc",
"anime_2d", "anime_3d", "product_ad"],
# 中文标签(前端显示,值保持英文以兼容后端)
"option_labels": {
"documentary_realism": "🎥 伪纪录片 · 真实自然(像随手拍的生活记录)",
"cinematic": "🎞️ 电影感 · 考究构图(有设计感的画面)",
"vlog_handheld": "🚶 手持 Vlog · 第一视角(边走边拍)",
"studio_clean": "💡 商业棚拍 · 高清干净(打光讲究、背景简洁)",
"selfie_pov": "📱 手机自拍 · 第一人称(举着手机自己拍自己)",
"interview_doc": "📺 访谈纪实 · 半身对镜(像接受采访)",
"anime_2d": "🎨 2D 动画 · 漫剧(手绘/动漫风格)",
"anime_3d": "🧸 3D 动画 · 卡通渲染(三维风格化)",
"product_ad": "🧊 产品广告 · 精致特写(微距/质感)",
},
"default": "documentary_realism", "label": "拍摄风格",
},
"realism_boost": {
"type": "enum",
"options": ["off", "mild", "strong"],
"option_labels": {
"off": "关闭(不做额外处理)",
"mild": "轻度(追加真实质感:噪点/毛孔/不匀光)",
"strong": "强力(↑ 并自动剔除「4K/超清/无瑕/大师作品」等反真实词)",
},
"default": "mild", "label": "反 AI 感(真实感增强)",
},
"director_style": {
"type": "enum",
"options": ["", "spielberg", "hitchcock", "kubrick", "kurosawa", "scorsese",
"fellini", "bergman", "tarkovsky", "wong_kar_wai", "nolan",
"villeneuve", "fincher", "refn", "bi_gan", "zhang_yimou",
"hou_hsiao_hsien", "park_chan_wook", "malick",
"michael_mann", "coen_brothers"],
"option_labels": {
"": "(不使用导演风格)",
"spielberg": "斯皮尔伯格 · 奇观与亲密", "hitchcock": "希区柯克 · 悬念与信息差",
"kubrick": "库布里克 · 几何与秩序", "kurosawa": "黑泽明 · 群像与天气",
"scorsese": "斯科塞斯 · 都市与旁白", "fellini": "费里尼 · 狂欢与记忆",
"bergman": "伯格曼 · 凝视与沉默", "tarkovsky": "塔可夫斯基 · 雕刻时间",
"wong_kar_wai": "王家卫 · 都市孤独", "nolan": "诺兰 · 高概念谜题",
"villeneuve": "维伦纽瓦 · 宏大克制", "fincher": "芬奇 · 外科精确",
"refn": "雷弗恩 · 霓虹停滞", "bi_gan": "毕赣 · 梦境长镜",
"zhang_yimou": "张艺谋 · 单色仪式", "hou_hsiao_hsien": "侯孝贤 · 固定远观",
"park_chan_wook": "朴赞郁 · 巴洛克反讽", "malick": "马力克 · 自然光诗意",
"michael_mann": "迈克尔·曼 · 夜色与专业", "coen_brothers": "科恩兄弟 · 冷峻荒诞",
},
"default": "", "label": "🎥 导演风格(镜头语言与叙事逻辑)",
},
"lang": {"type": "enum", "options": ["zh", "en"], "default": "zh", "label": "提示词语言"},
}
# ═══════════════════════════════════════════════════════════
# 公共工具:时长归一化(各导演共用,保证内容驱动语义一致)
# ═══════════════════════════════════════════════════════════
def shot_brief(opts: dict) -> str:
"""把「目标总时长 / 镜头数 / 单镜时长」组装成一句给导演的说明
设计原则(用户 2026-10-04 明确):
· **总时长 = 主驱动**("一分钟短片"/"五分钟短片")
· **镜头数与单镜时长仅作参考** —— 导演可采纳可不采纳
· 硬约束会削弱创作有效性 → 一律用"参考"语气,不写"必须"
让导演按题材/内容/目的自由裁镜头数,短片长篇皆可。
"""
def _int(v):
try:
return int(float(v or 0))
except Exception:
return 0
total = _int((opts or {}).get("total_duration"))
n = _int((opts or {}).get("shot_count"))
d = _int((opts or {}).get("shot_duration"))
out = ""
if total:
out = f"目标总时长:约 {total} 秒({total / 60:.1f} 分钟)。"
refs = []
if n:
refs.append(f"约 {n} 个镜头")
if d:
refs.append(f"每镜约 {d} 秒")
if refs:
out += (f"参考:{'、'.join(refs)} —— 可采纳可不采纳,"
f"请按内容与叙事节奏自行决定要分多少个镜头、每个镜头多长。")
return out or "镜头数量与每镜时长由你按内容与叙事节奏自由决定。"
def norm_duration(d, text: str = "") -> float:
"""时长归一化:内容驱动 + 台词兜底 + 上下限(2~10 秒)
· 台词兜底:中文口播约 4.5 字/秒 → 时长至少能容纳台词
· 上限 10 秒(生成模型单镜约束;超长需接续能力,由技术优选层识别)
"""
try:
v = float(d)
except Exception:
v = 5.0
if text:
need = len(str(text).strip()) / 4.5
if need > v:
v = need
return round(max(2.0, min(10.0, v)), 1)
def cast_or_character(anchors) -> str:
"""角色/演员表 → 提示词文本(**演员表优先**,含出场标注要求)
用户 2026-10-03 要求:短片常有多人物(主角/配角/人物一),
演员表(cast)比单角色字段更贴合;逐镜头出场由 cast_in_shot 标注。
"""
cc = getattr(anchors, "cast_context", "") or ""
if cc:
return (cc + '\n(多人物戏:请在每个镜头的 JSON 里用 "cast_in_shot": ["主角"] '
'标注谁出场;代号只能取自上面【】里的)')
return (getattr(anchors, "character_context", "") or getattr(anchors, "character_id", "")
or "(未指定)")
def style_pack_block(anchors) -> str:
"""拍摄风格包 → 导演可执行的「拍摄说明」块
作用:让导演在写分镜时就知道"这戏按什么风格拍"(设备/光/机位/质感/情绪),
从而产出风格一致的镜头语言,而不是每部片都写一样的通用描述。
"""
try:
from composer.realism import STYLE_PACKS
except Exception:
return ""
style = (getattr(anchors, "shooting_style", "") or "").strip()
pack = STYLE_PACKS.get(style) or {}
if not pack:
return ""
lines = [
f"【本次拍摄风格:{pack.get('label', style)}】",
f"· 设备:{pack.get('device', '')}",
f"· 光线:{pack.get('lighting', '')}",
f"· 机位与运镜:{pack.get('camera', '')}",
f"· 质感与瑕疵:{pack.get('flaws', '')}",
f"· 情绪表达:{pack.get('emotion', '')}",
"→ 每个镜头的画面描述都要符合以上设定;不要写与风格冲突的元素。",
]
boost = (getattr(anchors, "realism_boost", "") or "").strip()
if boost == "strong":
lines.append("→ 真实感要求高:避免「完美/无瑕/超清」这类修饰,多写生活化的不完美细节。")
return "\n".join(lines)
def director_style_hint(anchors) -> str:
"""导演风格 → 提示词块(空 key 返回空串)
与 style_pack_block(拍摄风格)**互补**:
· 拍摄风格 = 用什么设备/什么质感拍(自拍/手持/电影感…)
· 导演风格 = 用什么镜头语言/叙事逻辑(斯皮尔伯格/王家卫…)
"""
key = (getattr(anchors, "director_style", "") or "").strip()
if not key:
return ""
try:
from composer.director_styles import director_style_block
return director_style_block(key)
except Exception:
return ""
def realism_block() -> str:
"""「真实感纪律」提示块(注入导演提示词)
来源:anti-ai-checklist.md(外部开源研究,原文存 docs/realism/)
内容:非完美事件分配 + 2 秒节奏 + 禁止慢动作 + 抽象词黑名单
"""
try:
from composer.realism import imperfect_events_prompt
return imperfect_events_prompt()
except Exception:
return ""
# ── 拍摄风格 → 设备缺陷包映射(让导演按真实设备的物理缺陷描述画面)──
_DEVICE_MAP = {
"selfie_pov": "smartphone_vertical",
"vlog_handheld": "smartphone_vertical",
"documentary_realism": "dv_2000s",
"interview_doc": "doc_handheld",
# cinematic / studio_clean:刻意干净,不套缺陷包
}
def device_pack_hint(anchors) -> str:
"""拍摄风格 → 设备缺陷参数包(研究落地:缺陷即真实)
来源:camera-aesthetics.md(9 个设备包,原文存 docs/realism/)
"""
style = (getattr(anchors, "shooting_style", "") or "").strip()
key = _DEVICE_MAP.get(style, "")
if not key:
return ""
try:
from composer.realism import device_pack_block
return device_pack_block(key)
except Exception:
return ""
def realism_discipline() -> str:
"""去 AI 感的**提示词纪律**(供导演写分镜时遵守)
来源:anti-ai-checklist.md + atmosphere-dictionary.md(研究落地)
"""
return (
"【去 AI 感纪律(★重要,直接决定像不像真拍的)】\n"
"1. **禁用抽象氛围词**:不要写「温馨」「紧张」「孤独」这类词 —— "
"把它们翻译成**具体可拍的现象**(例:不写「温馨」,写「午后阳光透过树叶在地面"
"投下斑驳光影、杯口冒着细微热气」)。\n"
"2. **每镜头一个「非完美事件」**(全片不重复,缺陷类型也要换):\n"
" · 摄影师失误:自动对焦在主体与背景间搜索 / 构图偏离中心 / 慢了半拍才跟上\n"
" · 环境介入:微风吹动发丝 / 云影掠过导致曝光波动 / 场外有人打招呼\n"
" · 主体自然行为:注意到镜头露出真诚微笑后移开视线 / 说话被环境音打断笑场\n"
"3. **对焦要写路径**(不要笼统写「自动对焦搜索」):\n"
" ❌ 自动对焦反复搜索\n"
" ✅ 自动对焦在她脸部和她手中的杯子之间来回切换\n"
"4. **只用现场真实光源**(窗光/顶灯/手机屏幕光),禁影棚词(柔光箱/主光/轮廓光)。\n"
"5. **不要写慢动作**(除非明确设定为手机慢动作模式)——真实拍摄默认实时速度。"
)