feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 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 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,377 @@
"""
ScriptGenerator — 脚本生成桥接模块 v1.0
功能:
- 我(Claw)根据灵感/视频创作剧本 + 分镜列表
- ScriptGenerator 负责格式化为标准脚本 → 注入 PipelineController
- 自动触发 Gate 1 审核流程
工作流:
1. 用户给灵感 → 我创作剧本 + segments 列表
2. 调用 format_and_inject() 一站式注入流水线
3. 系统自动过 Gate 1 → 等待用户审核
用法:
from script_generator import ScriptGenerator
segments = [
{"scene": "...", "description": "...", ...},
...
]
# 方式一:直接注入到已有生产任务
prod = ScriptGenerator.inject_into_pipeline(pc, prod_id, segments)
# 方式二:创建新任务并注入(推荐)
prod = ScriptGenerator.create_and_inject(
pc, "文字灵感", "灵感内容",
"视频标题", "小漫", segments, style="manhua"
)
"""
import os
import copy
from typing import Optional
from pathlib import Path
class ScriptGenerator:
"""脚本生成器桥接模块 — 我创作 → 流水线注入"""
# 必填字段
REQUIRED_SEGMENT_FIELDS = {
"scene": str,
"description": str,
}
# 可选字段及默认值
OPTIONAL_SEGMENT_FIELDS = {
"dialogue": "",
"emotion": "中性",
"duration_sec": 5,
"camera": "medium_shot",
"transition": "cut",
"visual_style": None, # None = 使用脚本 meta 中的值
}
# 有效值列表
VALID_CAMERAS = [
"wide_shot", "medium_shot", "close_up", "over_shoulder",
"aerial", "tracking", "pan", "dolly", "extreme_wide"
]
VALID_TRANSITIONS = [
"cut", "fade_in", "fade_out", "dissolve",
"wipe_left", "wipe_right", "zoom_in", "zoom_out", "none"
]
@classmethod
def validate_segments(cls, segments: list[dict]) -> list[str]:
"""验证分镜列表格式,返回错误列表"""
errors = []
if not segments:
return ["分镜列表为空"]
for i, seg in enumerate(segments):
sid = seg.get("scene", f"#{i+1}")
# 检查必填字段
for field, ftype in cls.REQUIRED_SEGMENT_FIELDS.items():
if field not in seg or not seg.get(field):
errors.append(f"分镜 '{sid}': 缺少必填字段 '{field}'")
# 检查镜头类型
if "camera" in seg and seg["camera"]:
if seg["camera"] not in cls.VALID_CAMERAS:
warnings = ", ".join(cls.VALID_CAMERAS)
errors.append(f"分镜 '{sid}': 无效镜头类型 '{seg['camera']}' (可选: {warnings})")
# 检查转场
if "transition" in seg and seg["transition"]:
if seg["transition"] not in cls.VALID_TRANSITIONS:
warnings = ", ".join(cls.VALID_TRANSITIONS)
errors.append(f"分镜 '{sid}': 无效转场 '{seg['transition']}' (可选: {warnings})")
# 检查时长范围
dur = seg.get("duration_sec", 5)
if dur < 1 or dur > 30:
errors.append(f"分镜 '{sid}': 时长异常 ({dur}s),建议 1-30s")
return errors
@classmethod
def format_segments(cls, title: str, character: str,
segments: list[dict],
style: str = "manhua",
source: str = "手动创作",
bgm_style: str = "治愈") -> dict:
"""
把我创作的 segments 格式化为标准脚本 dict
参数:
title: 视频标题
character: 角色名
segments: 我创作的 segment 列表
style: visual_style (manhua / realistic / mixed)
source: 灵感来源描述
bgm_style: BGM 风格偏好
返回:
标准脚本 dict (兼容 script_schemas 格式)
"""
from script_schemas import ScriptBuilder, validate_script
# 创建空脚本
if not title:
title = "未命名视频"
if not character:
character = "小漫"
script = ScriptBuilder.empty(title, character)
script["meta"]["source"] = source
script["meta"]["visual_style"] = style
script["meta"]["bgm_style"] = bgm_style
# 逐个添加分镜
for seg_data in segments:
# 深拷贝,避免修改原始数据
seg = copy.deepcopy(seg_data)
# 如果未指定 visual_style,使用脚本级默认
if "visual_style" not in seg or seg.get("visual_style") is None:
seg["visual_style"] = style
ScriptBuilder.add_segment(script, seg)
# 验证
errors = validate_script(script)
if errors:
print(" ⚠️ 脚本验证警告:")
for e in errors:
print(f" - {e}")
return script
@classmethod
def inject_into_pipeline(cls, pc, prod_id: str,
title: str, character: str,
segments: list[dict],
style: str = "manhua",
source: str = "手动创作",
bgm_style: str = "治愈") -> dict:
"""
一站式:格式化脚本并注入到已有生产任务
参数:
pc: PipelineController 实例
prod_id: 已有生产任务 ID
title: 视频标题
character: 角色名
segments: segment 列表
style: 视觉风格
source: 灵感来源
返回:
Gate 1 审核信息 (可直接展示)
"""
# 1. 验证 segments
errors = cls.validate_segments(segments)
if errors:
error_msg = "; ".join(errors)
raise ValueError(f"Segments 验证失败: {error_msg}")
# 2. 格式化为标准脚本
script = cls.format_segments(title, character, segments, style, source, bgm_style)
# 3. 注入到生产任务
pc.set_script(prod_id, script)
# 4. 补充 meta 信息
prod = pc.get(prod_id)
if prod:
prod.title = title
prod.character = character
prod.visual_style = style
prod.source_type = source
# 5. 提交 Gate 1 审核
gate_result = pc.submit_gate(prod_id, gate=1)
return gate_result
@classmethod
def create_and_inject(cls, pc,
source_type: str,
source_data: str,
title: str,
character: str,
segments: list[dict],
style: str = "manhua",
bgm_style: str = "治愈") -> dict:
"""
一站式:创建生产任务 + 格式化 + 注入 + 提交 Gate 1
这是最常用的入口方法。
参数:
pc: PipelineController 实例
source_type: 灵感来源类型 (文字灵感 / 参考视频 / 外部剧本)
source_data: 灵感来源内容
title: 视频标题
character: 角色名
segments: segment 列表(我创作的)
style: 视觉风格 (manhua / realistic)
bgm_style: BGM 风格偏好
返回:
{
"production": Production,
"gate_result": dict (Gate 1 审核展示信息)
}
用法:
result = ScriptGenerator.create_and_inject(
pc, "文字灵感", "一段温柔的日常",
"小漫的午后", "小漫", segments
)
prod = result["production"]
gate = result["gate_result"]
"""
from pipeline_controller import PipelineController
# 1. 验证 segments
errors = cls.validate_segments(segments)
if errors:
error_msg = "; ".join(errors)
raise ValueError(f"Segments 验证失败: {error_msg}")
# 2. 创建生产任务
prod = pc.create(source_type, source_data, title, character, style)
# 3. 注入并提交 Gate 1
gate_result = cls.inject_into_pipeline(
pc, prod.id, title, character,
segments, style, source_type, bgm_style
)
return {
"production": prod,
"gate_result": gate_result,
}
@classmethod
def print_gate1_summary(cls, gate_result: dict):
"""打印 Gate 1 审核摘要(给用户看)"""
summary = gate_result.get("summary", {})
print(f"\n{'=' * 60}")
print(f" 🅰️ Gate 1 — 脚本方向审核")
print(f"{'=' * 60}")
print(f" 标题: {summary.get('title', '')}")
print(f" 来源: {summary.get('source', '')}")
print(f" 角色: {summary.get('character', '')}")
print(f" 总时长: {summary.get('total_duration', 0)}s ({summary.get('segment_count', 0)} 个分镜)")
print(f" ─────────────────────────────────")
print(f" 场景结构:")
for sc in summary.get("structure", []):
print(f" #{sc['id']} {sc['scene']} ({sc['duration']}s, {sc['emotion']})")
print(f" ─────────────────────────────────")
print(f" 情绪分布: {summary.get('emotion_distribution', {})}")
print(f" ─────────────────────────────────")
print(f" 📌 等待审核: 脚本方向、风格、节奏")
print(f" ✅ pc.approve_gate('{gate_result.get('production_id', '')}', gate=1)")
print(f" ❌ pc.reject_gate('{gate_result.get('production_id', '')}', gate=1, feedback='...')")
@classmethod
def create_empty_segments(cls, count: int = 4) -> list[dict]:
"""创建空的分镜模板(供手动填写)"""
return [
{
"scene": f"场景 {i+1}",
"description": "请填写视觉描述",
"dialogue": "",
"emotion": "中性",
"duration_sec": 8,
"camera": "medium_shot",
"transition": "cut",
}
for i in range(count)
]
# ═══════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════
def cli():
"""CLI 接口:从 JSON 文件导入并注入流水线"""
import argparse
import json
from pipeline_controller import PipelineController
parser = argparse.ArgumentParser(description="ScriptGenerator CLI — 脚本生成桥接")
parser.add_argument("action", choices=["import", "template", "inspect"],
default="import", nargs="?")
parser.add_argument("--segments", default="", help="分镜 JSON 文件路径")
parser.add_argument("--title", default="未命名", help="视频标题")
parser.add_argument("--character", default="小漫", help="角色名")
parser.add_argument("--style", default="manhua", help="视觉风格 (manhua/realistic)")
parser.add_argument("--source", default="CLI导入", help="灵感来源")
parser.add_argument("--bgm-style", default="治愈", help="BGM 风格")
parser.add_argument("--prod-id", default="", help="注入已有任务(可选,不填则新建)")
args = parser.parse_args()
pc = PipelineController()
if args.action == "template":
# 生成空模板
import json as _json
template = ScriptGenerator.create_empty_segments(4)
print(_json.dumps(template, indent=2, ensure_ascii=False))
return
if args.action == "import":
if not args.segments:
print("❌ 请指定 --segments <json文件路径>")
return
with open(args.segments, "r", encoding="utf-8") as f:
segments = json.load(f)
# 注入
if args.prod_id:
gate_result = ScriptGenerator.inject_into_pipeline(
pc, args.prod_id, args.title, args.character,
segments, args.style, args.source, args.bgm_style
)
print(f"✅ 已注入到生产任务: {args.prod_id}")
else:
result = ScriptGenerator.create_and_inject(
pc, args.source, "", args.title, args.character,
segments, args.style, args.bgm_style
)
prod = result["production"]
gate_result = result["gate_result"]
print(f"✅ 已创建生产任务: {prod.id}")
ScriptGenerator.print_gate1_summary(gate_result)
elif args.action == "inspect":
if not args.prod_id:
print("❌ 请指定 --prod-id")
return
prod = pc.get(args.prod_id)
if not prod:
print(f"❌ 未找到任务: {args.prod_id}")
return
print(f"\n📋 生产任务: {prod.id}")
print(f" 标题: {prod.title}")
print(f" 状态: {prod.state.value}")
print(f" 角色: {prod.character}")
print(f" 风格: {prod.visual_style}")
print(f" 分镜数: {len(prod.script.get('segments', []))}")
total = sum(s.get('duration_sec', 0) for s in prod.script.get('segments', []))
print(f" 总时长: {total}s")
if __name__ == "__main__":
cli()
@@ -0,0 +1,81 @@
# ScriptGenerator — 剧本创作指引 v1.0
> 用途:当我(Claw)根据灵感创作剧本时,按照此格式输出,ScriptGenerator 自动注入流水线
---
## 输出格式(我创作时的标准模板)
创作完剧本后,按以下结构输出 **segments 列表**:
```python
segments = [
{
"scene": "分镜标题/场景名", # 简洁,如"清晨的咖啡馆"
"description": "场景视觉描述", # 15-40字,描述画面内容
"dialogue": "旁白/角色台词", # 格式: "(旁白) ..." 或 "角色名: ..."
"emotion": "情绪标签", # 温暖/专注/愉悦/燃/治愈/悲伤/紧张...
"duration_sec": 8, # 3-12s,通常6-10s
"camera": "镜头类型", # wide_shot / medium_shot / close_up / tracking / aerial / pan / dolly
"transition": "转场效果", # cut / fade_in / dissolve / zoom_in / wipe_left
},
# ...更多分镜
]
```
## 我创作时遵循的准则
### 1. 总时长控制
- 目标视频长度 30-180s
- 每个分镜 5-10s(复杂场景可到 12s,快速节奏 3-5s)
- 分镜数 = 目标时长 / 平均分镜时长
### 2. 叙事结构
```
开场(15-20%): 建立场景/引入角色 → 主要自然过渡
发展(50-60%): 核心内容展开 → 随情绪变化切换
收尾(15-20%): 情绪收束/升华 → 自然淡出
```
### 3. 镜头运用
| 镜头 | 用途 |
|:----|:-----|
| wide_shot | 建立场景、环境展示 |
| medium_shot | 角色互动、日常动作 |
| close_up | 情绪特写、细节展示 |
| tracking | 跟随移动、场景导览 |
| aerial | 全景建立(数字人也可) |
| pan | 场景平移展示 |
| dolly | 推进/拉远,聚焦或展示全景 |
### 4. 情绪节奏
- 同一情绪分段持续 2-4 个分镜,不要频繁跳跃
- 情绪变化需要自然过渡:温暖→专注→愉悦→满足
- 燃系视频保持高能量,治愈系保持舒缓节奏
### 5. 对话 vs 旁白
- 旁白: `"(旁白) 叙述内容"` — 适用于说明性、情感性内容
- 角色台词: `"角色名: 说话内容"` — 适用于有角色互动的场景
- 纯音乐卡点视频:dialogue 可为空字符串
### 6. BGM 风格匹配
- 治愈系: 温和情绪,舒缓节奏
- 燃系: 高能量,短分镜(3-5s),快切
- 搞笑: 轻快节奏,夸张描述
- 情感: 长分镜(8-12s),细腻描述
---
## 使用流程
```
[用户给灵感]
↓
[我创作剧本 + segments 列表]
↓
[调用 ScriptGenerator.format_and_inject()]
↓
[segments 转化为标准脚本 → 注入 PipelineController]
↓
[自动触发 Gate 1 审核]
```