Files
MediaCrawler/mac-agent-os-main/05_tools/09_ave/scripts/composer/realism.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

802 lines
45 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""
composer/realism.py — 写实感增强(去 AI 感)
来源:外部方法论学习(详见 PLANS/REALISM_GUIDE.md + 09_ave/docs/realism/)
作用:为提示词编译提供三件东西——
① EMOTION_ACTIONS 情绪外化表(抽象情绪 → 具体身体动作)
② REALISM_ANCHORS 写实锚点包(按拍摄风格给"反 AI 感"的具体锚点)
③ check_conflicts 矛盾约束自检(写了互相抵消的组合)
"""
import re
# ════════════════════════════════════════════════════════════
# ① 情绪外化表
# 原则:视频模型看不懂抽象形容词,必须外化为身体细节;
# 每个镜头只写 1-2 个关键动作(多了稀释焦点)
# ════════════════════════════════════════════════════════════
EMOTION_ACTIONS: dict = {
"悲伤": "低头、肩膀微微颤抖、眼眶泛红,手指无意识地攥紧衣角",
"难过": "低头、肩膀微微颤抖、眼眶泛红,手指无意识地攥紧衣角",
"喜悦": "嘴角抑制不住地上扬、眉眼舒展,说话时眼睛弯起来",
"开心": "嘴角抑制不住地上扬、眉眼舒展,说话时眼睛弯起来",
"高兴": "嘴角抑制不住地上扬、眉眼舒展,说话时眼睛弯起来",
"紧张": "手指无意识地攥紧衣角、眼神闪躲、呼吸略急",
"焦虑": "频繁看表、手指轻敲、眼神游移、下意识抿嘴",
"愤怒": "双拳紧握、下颌线绷紧、胸口起伏,从牙缝里挤话",
"生气": "双拳紧握、下颌线绷紧、胸口起伏,从牙缝里挤话",
"释然": "长舒一口气、紧绷的肩膀放松下来,浮起淡淡的微笑",
"放松": "长舒一口气、紧绷的肩膀放松下来,浮起淡淡的微笑",
"恐惧": "瞳孔放大、后退半步、手不自觉抓住身边物体、嘴唇发抖",
"害怕": "瞳孔放大、后退半步、手不自觉抓住身边物体、嘴唇发抖",
"疲惫": "揉太阳穴、眼皮下沉、长呼气、动作迟缓",
"疲惫不堪": "揉太阳穴、眼皮下沉、长呼气、动作迟缓",
"专注": "眉头微皱、眼神锁定一点、嘴唇抿紧、身体前倾",
"认真": "眉头微皱、眼神锁定一点、嘴唇抿紧、身体前倾",
"犹豫": "张嘴又闭上、视线游移、手悬在半空、欲言又止",
"纠结": "张嘴又闭上、视线游移、手悬在半空、欲言又止",
"惊喜": "眼睛瞬间睁大、嘴巴微张、身体向前倾,笑容缓慢绽放",
"期待": "身体微微前倾、目光停在一点、呼吸放轻",
"尴尬": "视线移向别处、嘴角抽动一下、手指摸了摸鼻尖",
"无奈": "轻轻摇头、嘴角扯出一个笑、肩膀垮下来",
"委屈": "嘴唇抿成一条线、眼眶发热、目光低垂看向地面",
"好奇": "微微歪头、眼睛亮起来、视线跟着对象移动",
"警觉": "下颌收紧、眼神快速扫向声源、身体下意识绷住",
"疏离": "目光越过对方看向远处、表情平静没有起伏、动作缓慢",
"冷漠": "目光越过对方看向远处、表情平静没有起伏、动作缓慢",
}
def externalize_emotion(sentiment: str) -> str:
"""抽象情绪 → 具体身体动作;未命中则原样返回(调用方可自行处理)"""
s = (sentiment or "").strip()
if not s:
return ""
# 直接命中
if s in EMOTION_ACTIONS:
return EMOTION_ACTIONS[s]
# 含"紧张/好奇"这类复合写法(用 / 、 分隔)→ 取第一个能命中的
for sep in ("/", "、", ",", ",", " "):
if sep in s:
for piece in s.split(sep):
piece = piece.strip()
if piece in EMOTION_ACTIONS:
return EMOTION_ACTIONS[piece]
# 部分包含
for k, v in EMOTION_ACTIONS.items():
if k in s:
return v
return s
# ════════════════════════════════════════════════════════════
# ② 写实锚点包(按拍摄风格)
# 核心:用"设备型号 / 真实光源 / 微瑕疵 / 皮肤细节"替换抽象形容词
# ════════════════════════════════════════════════════════════
REALISM_ANCHORS: dict = {
# 📱 自拍 Vlog(用户核心诉求)
"selfie_pov": {
"name": "手机自拍 · 第一人称",
"device": "iPhone 前置摄像头拍摄,竖屏 9:16,一臂距离(鼻梁与脸颊有轻微广角透视变形)",
"hand": "手持自拍,画面随呼吸与手臂疲劳轻微起伏;偶尔抬手微调构图",
"light": "窗光从侧面打在脸上、室内顶灯偏暖;背景比人脸暗一档",
"skin": "真实皮肤纹理、毛孔与细小绒毛可见、不过度磨皮、没有塑料质感",
"flaws": ("前置镜头为**定焦**(不会有对焦搜索);近距离透视变形;"
"动态范围有限——窗外或灯下的高光容易过曝、暗部有可见噪点;"
"头发有几缕没梳顺"),
"motion": "说话时自然眨眼、吞咽、鼻翼微微起伏,视线偶尔离开镜头再回来",
"sound": "房间轻微混响、远处环境声、衣服摩擦声(手机麦克风拾音,人声比正常对话稍远)",
"_no": ("不要三脚架式的绝对稳定(自然的手持呼吸感要保留)、不要影棚布光、"
"不要滤镜磨皮、不要对焦搜索(手机前置是定焦)"),
},
# 伪纪录片 · 真实自然(现有默认)
"documentary_realism": {
"device": "手持摄影机质感,轻微呼吸起伏",
"hand": "镜头随拍摄者呼吸轻微移动,构图不是完美的",
"light": "现场实际光源(窗光、路灯、顶灯),明暗过渡自然",
"skin": "真实皮肤纹理、毛孔可见、不过度磨皮",
"flaws": "轻微噪点、暗部细节不完全干净、对焦有呼吸感",
"motion": "被拍摄者的动作不追求完美,有停顿与迟疑",
"_no": "不要运镜特效痕迹、不要 AI 塑料质感、不要字幕水印",
},
# 电影感
"cinematic": {
"device": "35mm 电影镜头,浅景深",
"hand": "三脚架或稳定器,运动平滑",
"light": "动机光(画面内有光源依据)+ 逆光轮廓",
"skin": "电影级肤色还原、保留皮肤质感",
"flaws": "轻微的镜头光晕、真实的光斑",
"motion": "调度从容,动作有起势与收势",
"_no": "不要手持晃动、不要纪实粗糙感",
},
# 棚拍干净
"studio_clean": {
"device": "专业摄影机,稳定机位",
"hand": "固定机位 / 滑轨",
"light": "柔光箱主光 + 轮廓光",
"skin": "干净但保留皮肤纹理(不要过度磨皮)",
"flaws": "极少的噪点,干净背景",
"motion": "表演可预期、动作清晰",
"_no": "不要手持晃动、不要环境杂光",
},
# 手持 Vlog · 第一视角(边走边拍,镜头=拍摄者的眼睛)
"vlog_handheld": {
"device": "手机或轻便相机手持拍摄,竖屏 9:16",
"hand": "第一视角手持,画面随走路上下起伏,转向时略有回摆",
"light": "自然光为主( daylight / 室内灯),明暗随走动变化",
"skin": "真实皮肤纹理、毛孔可见、不过度磨皮",
"flaws": "走动时的轻微抖动、暗部噪点、偶尔的手部入画",
"motion": "被拍对象自然走动、走动时说话有些喘",
"sound": "环境音随移动变化、脚步声、风噪",
"_no": "不要稳定器般平滑、不要影棚布光",
},
# 访谈纪实 · 半身对镜
"interview_doc": {
"device": "中焦镜头,半身构图,略浅景深",
"hand": "机位基本固定,允许极轻微的手持呼吸感",
"light": "柔和侧光或窗户光,面部立体但不过曝",
"skin": "真实皮肤纹理、皱纹与毛孔保留、不过度磨皮",
"flaws": "背景略暗、轻微噪点、被访者偶尔的停顿与眼神移动",
"motion": "被访者自然对镜说话,有停顿、思考、手势",
"sound": "安静室内混响、衣料摩擦声",
"_no": "不要花哨运镜、不要强风格化调色",
},
}
def get_anchors(shooting_style: str) -> dict:
"""按拍摄风格取锚点包;未知名回退 documentary_realism"""
s = (shooting_style or "").strip().lower()
if s in REALISM_ANCHORS:
return REALISM_ANCHORS[s]
# 常见别名
alias = {
"selfie": "selfie_pov", "vlog": "selfie_pov", "自拍": "selfie_pov",
"手机自拍": "selfie_pov", "第一人称": "selfie_pov",
"documentary": "documentary_realism", "纪实": "documentary_realism",
"film": "cinematic", "电影感": "cinematic", "movie": "cinematic",
"studio": "studio_clean", "棚拍": "studio_clean",
}
for k, v in alias.items():
if k in s:
return REALISM_ANCHORS[v]
return REALISM_ANCHORS["documentary_realism"]
# ════════════════════════════════════════════════════════════
# ③ 矛盾约束自检(写了等于没写)
# ════════════════════════════════════════════════════════════
CONFLICT_PAIRS: list = [
(("画面稳定", "稳定机位", "三脚架"), ("手持", "晃动", "呼吸起伏"),
"「画面稳定」与「手持晃动」冲突(稳定=三脚架语言)"),
(("电影感", "cinematic"), ("纪实", "documentary", "手持"),
"「电影感」与「纪实手持」冲突(风格相互拉扯)"),
(("胶片", "film grain", "颗粒感"), ("4K", "超清", "ultra sharp", "高清锐利"),
"「胶片感」与「超清」冲突(胶片感本身是模糊+颗粒)"),
(("慢动作", "slow motion"), ("高速", "快切", "high speed"),
"「慢动作」与「高速动作」冲突"),
]
def check_conflicts(text: str) -> list:
"""扫描提示词里的矛盾组合,返回告警列表(空 = 无冲突)
⚠️ 先剔除负向语境:负向约束里写「不要画面稳定」是在**否定**该项,
不能被当成"要求画面稳定"(否则与"手持晃动"误报冲突)。
"""
t = (text or "")
# 剔除"不要X / 避免X / 禁止X / 无X"片段(X 最多 12 字),再做正向检测
t_positive = re.sub(r"(不要|避免|禁止|无)[^、;。\n]{0,12}", " ", t)
warns = []
for group_a, group_b, msg in CONFLICT_PAIRS:
hit_a = next((a for a in group_a if a.lower() in t_positive.lower()), "")
hit_b = next((b for b in group_b if b.lower() in t_positive.lower()), "")
if hit_a and hit_b:
warns.append(f"{msg}(同时出现「{hit_a}」与「{hit_b}」)")
return warns
# ════════════════════════════════════════════════════════════
# 必加约束(护栏)
# ════════════════════════════════════════════════════════════
BASE_CONSTRAINTS = "不要水印、不要 Logo、不要字幕"
FACE_CONSTRAINTS = "人物面部稳定不变形、动作连贯自然不僵硬、无穿模无卡顿无闪烁"
ANTI_TWIN = "同一画面中不出现外形着装完全一致的人物(防止分身/双胞胎)"
def compose_constraints(extra_no: str = "", with_anti_twin: bool = True) -> str:
"""组装约束串(负向 + 正向要求,自动去重)
注意:两类语义不同——
· 负向约束("不要 X")用「负向约束:」引导
· 正向要求("人物面部稳定不变形"这类陈述句)用「画面要求:」引导
⚠️ 不能给正向要求硬加"不要"前缀(会把要求变成禁止,语义反转)
"""
negatives: list = [] # 形如 "不要水印"
positives: list = [] # 形如 "人物面部稳定不变形"
for x in BASE_CONSTRAINTS.split("、"):
x = x.strip()
if x:
negatives.append(x if x.startswith("不要") else "不要" + x)
for x in FACE_CONSTRAINTS.split("、"):
x = x.strip()
if x:
positives.append(x)
if with_anti_twin:
positives.append(ANTI_TWIN)
if extra_no:
e = extra_no.strip()
for x in e.split("、"):
x = x.strip()
# ⚠️ 每项单独去前缀(锚点包每项通常自带"不要",若只去整体前缀会出现"不要不要X")
while x.startswith("不要"):
x = x[len("不要"):].strip()
if x:
negatives.append("不要" + x)
def _dedup(seq):
"""**完全去重**(相同项只留一条)
⚠️ 我曾经想在这里做"子串合并"("不要字幕水印" 吞掉 "不要字幕"),
但实测证明那是**错的**:
· `不要字幕` 与 `不要字幕水印` 是两条**不同**的约束(后者更具体),都该保留
· 而且按子串匹配会把 `不要水印` 误判成 `字幕水印` 的子集 → 错误替换
结论:**只做完全去重,不做子串合并。**
"""
seen, out = set(), []
for it in seq:
k = it.strip()
if not k or k in seen:
continue
seen.add(k)
out.append(it)
return out
parts = []
if negatives:
parts.append("负向约束:" + "、".join(_dedup(negatives)))
if positives:
parts.append("画面要求:" + "、".join(_dedup(positives)))
return ";".join(parts)
# ════════════════════════════════════════════════════════════
# ④ 风格包(Style Pack)—— 把"导演提示词知识"变成可选参数
# 用户 2026-10-03 要求:把学到的提示词技巧整理成参数选项,
# 选一个风格 = 自动带上一整套「设备/光/机位/瑕疵/情绪/负面词」设定。
# ════════════════════════════════════════════════════════════
STYLE_PACKS: dict = {
"selfie_pov": {
"label": "📱 手机自拍 · 第一人称",
"device": "iPhone 前置摄像头,手持自拍,手臂伸出一臂距离",
"lighting": "室内自然光或窗光为主,光源不均匀,脸上有明显明暗过渡",
"camera": "轻微手持晃动,构图略微歪斜不居中,对焦偶尔漂移",
"flaws": "轻微手机噪点、真实肤质(可见毛孔与细纹)、有一缕碎发没整理好",
"emotion": "外化", "realism_default": "mild",
"negatives": ["影棚均匀布光", "三脚架固定机位", "过度磨皮", "商业修图感"],
},
"vlog_handheld": {
"label": "🚶 手持 Vlog · 第一视角",
"device": "手持相机或手机,走动跟拍,第一视角",
"lighting": "户外环境光/店内混合光源,色温不统一",
"camera": "明显手持晃动(模拟步行节奏),偶有快速转向,构图随意",
"flaws": "轻微运动模糊、曝光偶尔跳动、背景杂乱真实",
"emotion": "外化", "realism_default": "mild",
"negatives": ["稳定器丝滑运镜", "影棚打光", "摆拍感"],
},
"documentary_realism": {
"label": "🎥 伪纪录片 · 真实自然",
"device": "手持摄像机或长焦,像随手记录",
"lighting": "以现场自然光为主,不补光,允许暗部欠曝",
"camera": "长镜头、轻微呼吸感晃动、不稳定构图",
"flaws": "轻微胶片颗粒、真实环境杂乱、画面有生活痕迹",
"emotion": "外化", "realism_default": "mild",
"negatives": ["影棚布光", "摆拍摆姿", "商业感"],
},
"interview_doc": {
"label": "📺 访谈纪实 · 半身对镜",
"device": "固定机位,中焦镜头,半身构图",
"lighting": "柔和主光 + 环境光,背景稍暗",
"camera": "机位固定,极轻微晃动,人物可略偏离画面中心",
"flaws": "背景真实但不抢眼、肤色自然不修饰",
"emotion": "外化", "realism_default": "off",
"negatives": ["夸张运镜", "过度磨皮"],
},
"cinematic": {
"label": "🎞️ 电影感 · 考究构图",
"device": "专业电影机,变焦或定焦,景深控制",
"lighting": "有设计感的光比,侧逆光/轮廓光,暗部有层次",
"camera": "稳定运镜或精心设计的推拉摇移,构图讲究",
"flaws": "轻微胶片颗粒(不要数码锐化的塑料感)",
"emotion": "外化", "realism_default": "off",
"negatives": ["手机感", "随手拍", "画面杂乱"],
},
"studio_clean": {
"label": "💡 商业棚拍 · 高清干净",
"device": "专业摄影机,三脚架固定,中长焦",
"lighting": "专业布光:主光 + 辅光 + 轮廓光,光比均匀",
"camera": "稳定固定机位,构图工整居中",
"flaws": "(此风格刻意干净 —— 但保留真实肤质,避免塑料感)",
"emotion": "外化", "realism_default": "off",
"negatives": ["画面杂乱", "手持晃动", "环境噪音"],
},
"anime_2d": {
"label": "🎨 2D 动画 · 漫剧",
"device": "动画分镜,2D 手绘或动漫渲染",
"lighting": "动画式平光或风格化光影(非物理真实)",
"camera": "动画运镜:推拉摇移、定格、分格",
"flaws": "(动画无需真实瑕疵;改为保持线条稳定、色块不跳变)",
"emotion": "夸张外化(动漫式表情与动作幅度)", "realism_default": "off",
"negatives": ["写实人脸照片感", "真实摄影质感", "胶片颗粒", "真人实拍"],
},
"anime_3d": {
"label": "🧸 3D 动画 · 卡通渲染",
"device": "3D 渲染,卡通/风格化着色",
"lighting": "风格化打光,柔和阴影或卡通描边",
"camera": "3D 运镜(环绕/位移/景深)",
"flaws": "(保持模型一致,避免穿模与闪烁)",
"emotion": "夸张外化", "realism_default": "off",
"negatives": ["写实照片", "真人实拍", "皮肤毛孔", "胶片噪点"],
},
"product_ad": {
"label": "🧊 产品广告 · 精致特写",
"device": "微距或长焦,产品特写,浅景深",
"lighting": "精细打光:轮廓光 + 反光板,突出材质",
"camera": "稳定运镜(滑轨/环绕),构图讲究",
"flaws": "材质细节清晰(不是缩小版瑕疵)",
"emotion": "(产品无情绪;如有模特出镜则外化)", "realism_default": "off",
"negatives": ["画面杂乱", "廉价感", "低清晰度"],
},
}
# ════════════════════════════════════════════════════════════
# ⑤ 反真实词库(★ 研究核心发现)
# "AI 感"的头号来源:提示词里堆砌「4K/8K/超清/无瑕/大师作品」——
# 模型会理解成"要无瑕",于是过度磨皮、抹平纹理、统一光影 → 塑料感。
# 真实感增强开启时,自动剔除这些词。
# ════════════════════════════════════════════════════════════
ANTI_REALISM_WORDS: list = [
"4k", "8k", "4K", "8K", "超清", "超高清", "高清", "极致画质", "顶级画质",
"电影级", "电影感构图", "大片感", "杂志级",
"无瑕", "无瑕疵", "完美", "大师作品", "大师级", "杰作",
"masterpiece", "best quality", "ultra detailed", "ultra realistic",
"hyperrealistic", "photorealistic", "8k uhd", "highly detailed",
"过度锐化", "塑料感", "CG感", "3D渲染感",
]
# 反真实词在"写实类"风格下才剔除(动画类风格里这些词无害甚至有用)
_REALISM_STYLES = {"selfie_pov", "vlog_handheld", "documentary_realism",
"interview_doc", "cinematic", "studio_clean"}
def strip_anti_realism(text: str) -> tuple:
"""剔除会导致「塑料感」的反真实词(写实风格下)
返回 (清洗后文本, 被剔除的词列表)
"""
if not text:
return "", []
out = text
removed = []
for w in ANTI_REALISM_WORDS:
if w in out:
out = out.replace(w, "")
removed.append(w)
# 清理残留下的多余标点/空格
out = re.sub(r"[、,,]\s*(?=[、,,])", "", out)
out = re.sub(r"\s{2,}", " ", out).strip(" 、,,")
return out, removed
def apply_style_pack(prompt: str, shooting_style: str,
realism_boost: str = "mild") -> tuple:
"""把「风格包」应用到提示词上(去 AI 感的核心动作)
realism_boost: off(不动)/ mild(加瑕疵锚点)/ strong(瑕疵 + 剔除反真实词)
返回 (增强后的提示词, 说明列表)
"""
notes: list = []
if not prompt:
return prompt, notes
pack = STYLE_PACKS.get(shooting_style) or {}
boost = (realism_boost or "off").lower()
out = prompt
if boost == "off" or not pack:
return out, notes
# ① strong:剔除反真实词(研究里最大的坑)
if boost == "strong" and shooting_style in _REALISM_STYLES:
out, removed = strip_anti_realism(out)
if removed:
notes.append(f"已剔除反真实词:{'、'.join(removed[:6])}")
# ② 追加瑕疵锚点(如果提示词里还没有类似描述)
flaws = pack.get("flaws") or ""
if flaws and not any(k in out for k in ("噪点", "颗粒", "毛孔", "瑕疵", "纹理")):
out = out.rstrip("。. ") + f"。{flaws}。"
notes.append("已追加真实质感锚点")
# ③ strong:额外补一句"反完美"总纲
if boost == "strong":
tail = "真实拍摄的不完美感:光线不均匀、构图不刻意、画面有生活痕迹"
if "不完美" not in out and "不均匀" not in out:
out = out.rstrip("。. ") + f"。{tail}。"
notes.append("已追加反完美总纲")
return out, notes
def style_pack_options() -> dict:
"""供前端生成「拍摄风格」下拉(含中文标签)"""
return {k: v.get("label", k) for k, v in STYLE_PACKS.items()}
# ════════════════════════════════════════════════════════════
# ★ 去 AI 感扩展库(2026-10-04 研究落地)
# 来源:zhouwei713/seedance-prompt 的 anti-ai-checklist.md
# (原文存 09_ave/docs/realism/anti-ai-checklist.md)
#
# 核心洞察:**"AI 感"主要来自模型的默认审美**(电影化运镜/稳定画面/
# 商业调色/完美构图/磨皮皮肤)—— 必须用负面约束显式关闭,
# 并主动注入「非完美事件」才像真实拍摄。
# ════════════════════════════════════════════════════════════
# ① AI 默认行为 → 负面约束句(10 项,含"造成的问题")
AI_DEFAULT_NEGATIVES = [
("电影化平滑运镜(推轨/摇臂感)", "没有电影化的摄像机移动"),
("画面稳定如三脚架", "不要三脚架式的绝对稳定(自然手持呼吸感是允许的)"),
("现代商业调色(青橙色调、高对比)", "没有现代色彩分级"),
("完美居中构图", "不完美的构图,构图偏离中心"),
("场景自动加商店/咖啡馆/人群/广告牌", "没有商店、广告、咖啡馆、人群或商业活动"),
("自动配背景音乐", "没有音乐。没有音效设计。没有旁白"),
("表演感强、直视镜头摆姿势", "真实的即兴行为,无剧本的日常生活片段感"),
("皮肤过度平滑/磨皮感", "逼真的皮肤纹理"),
("角色跨镜头漂移", "在整个视频中保持一致的身份、服装、发型和外貌"),
("灯光过于均匀完美", "自然光,在阳光和阴影间移动时的曝光波动"),
]
# ② 非完美事件库(21 个 · 四类)—— 每镜头选一个,全片不重复
IMPERFECT_EVENTS = {
"photographer": [
"自动对焦在主体和前景/背景之间反复搜索",
"构图偏离中心(操作者试图跟上主体)",
"摄像机稍晚才捕捉到关键瞬间(人已转身才对准)",
"变焦生硬,推过了又拉回来",
"走动跟拍时的强烈晃动和运动模糊",
"手指或背带短暂遮挡镜头一角",
"镜头努力对焦但短暂失败,画面呼吸",
],
"environment": [
"一阵微风吹动散落的发丝或衣角",
"云影掠过,曝光随之波动",
"小动物(猫/狗/鸽子)闯入画面,主体自然回应",
"场外有人打招呼,主体转头回应",
"阳光透过树叶在脸上闪烁明灭",
"路人从镜头前无意穿过",
],
"subject": [
"注意到镜头,露出小而真诚的微笑后移开视线",
"说话被环境音打断,笑场",
"整理头发、拨发到耳后等无意识小动作",
"动作做到一半改变主意(伸手拿杯子又先擦了下桌子)",
],
"ending": [
"录制在中途突然切到黑屏,仿佛摄像机关闭",
"主体走出画面,镜头停留在空景两秒",
"镜头垂下拍到地面/脚,然后切断",
"有人喊拍摄者的名字,镜头晃动着转向声源时切断",
],
}
# ③ 特殊字符规范(信息类型标注)—— 来自 07-特殊字符规范.md
SYMBOL_SPEC = {
"台词": "{你好,世界}(外语需标语种:用日语说道{こんにちは})",
"音效": "<远处传来狗叫声>",
"音乐": "(背景中播放着快节奏的摇滚乐)",
"字幕": "【第一章:启程】",
}
def ai_negative_block(hard: bool = True, extra: list = ()) -> str:
"""组装「关闭 AI 默认审美」负面约束段
hard=False 时只取前 6 条(精简版,用于短提示词)
"""
items = [neg for _, neg in (AI_DEFAULT_NEGATIVES if hard else AI_DEFAULT_NEGATIVES[:6])]
items.extend([str(x) for x in (extra or []) if x])
return "【负面约束(关闭 AI 默认审美)】" + ";".join(items) + "。"
def pick_imperfect_events(n_shots: int, categories: tuple = None) -> list:
"""为 N 个镜头分配「非完美事件」(不重复;最后一条自动用收束方式)"""
import random
cats = list(categories or ("photographer", "environment", "subject"))
pool = []
for c in cats:
pool.extend(IMPERFECT_EVENTS.get(c, []))
random.shuffle(pool)
out = []
for i in range(max(0, n_shots)):
if i == n_shots - 1 and n_shots > 1:
out.append(random.choice(IMPERFECT_EVENTS["ending"]))
else:
out.append(pool[i % len(pool)] if pool else "")
return out
def imperfect_events_prompt() -> str:
"""给导演的「非完美事件」纪律(逐镜头分配、不重复、结尾收束、2 秒节奏)"""
return (
"【真实感纪律:非完美事件(每个镜头分配一个,全片不重复)】\n"
"真实拍摄必然有瑕疵。请为每个镜头挑 1 个(**一镜一个,不要堆**):\n"
"· 摄影师失误:" + ";".join(IMPERFECT_EVENTS["photographer"][:4]) + "\n"
"· 环境介入:" + ";".join(IMPERFECT_EVENTS["environment"][:4]) + "\n"
"· 主体自然行为:" + ";".join(IMPERFECT_EVENTS["subject"][:3]) + "\n"
"· **结尾必须用一种收束方式**:" + ";".join(IMPERFECT_EVENTS["ending"]) + "\n"
"另外:镜头约 2 秒一个(超过 3 秒会滑向「精心拍摄」的假感);"
"除非明确设定为手机慢动作,否则不要写慢动作。"
)
def check_abstract_words(text: str) -> list:
"""自检:扫出会引发 AI 感的抽象词(返回命中列表)"""
BAD = ["美丽", "唯美", "高级感", "大片感", "史诗", "震撼", "精致", "梦幻", "氛围感"]
return [w for w in BAD if w in str(text or "")]
# ════════════════════════════════════════════════════════════
# ★ 设备美学缺陷参数包(9 个)—— 2026-10-04 研究落地
# 来源:camera-aesthetics.md(原文存 docs/realism/)
#
# 核心原则:**想要某种年代质感,就完整描述那个设备的物理缺陷,缺陷即真实。**
# 与 STYLE_PACKS 的关系:
# · STYLE_PACKS = 抽象风格取向(自拍/纪录片/电影感…)
# · DEVICE_PACKS = **具体设备的物理缺陷**(DV/VHS/胶片/手机/GoPro…)
# 两者互补,可叠加。
# ════════════════════════════════════════════════════════════
DEVICE_PACKS: dict = {
"dv_2000s": {
"name": "2000年代初消费级DV摄像机",
"fit": "家庭录像、朋友随拍、千禧年怀旧、韩日老社区题材",
"visual": ["中等数字压缩伪影", "褪色的色彩,柔和的对比度", "轻微的传感器噪点",
"轻微的滚动快门", "480p 到轻微高清之间的分辨率感", "高光容易过曝,暗部发灰"],
"operation": ["强烈的手持抖动,不完美的构图", "频繁的自动对焦搜索",
"镜头呼吸(对焦时视角轻微变化)", "在阳光和阴影间移动时的曝光波动",
"偶尔的运动模糊", "变焦生硬,速度不均匀", "录制中途突然开始或结束"],
"negative": "不要三脚架式的绝对稳定。不要电影化的摄像机移动。不要现代色彩分级。",
},
"vhs_8090s": {
"name": "80至90年代VHS家用摄像机",
"fit": "童年录像、生日聚会、更旧的怀旧感、复古恐怖",
"visual": ["明显的磁带噪点和扫描线", "色彩溢出(红色尤其容易晕开)",
"底部时间码或日期戳", "画面边缘轻微的几何畸变",
"低分辨率,细节模糊", "磁带损伤造成的偶尔画面撕裂或跳帧"],
"operation": ["沉重机身带来的缓慢笨重移动", "手动变焦的顿挫感",
"白平衡偏移(室内偏橙,室外偏蓝)", "逆光时人物完全剪影化"],
"negative": "不要数字锐利感。不要三脚架式的绝对稳定。不要高动态范围。",
},
"film_8mm": {
"name": "8mm/Super 8 胶片",
"fit": "更久远的家庭记忆、梦境感、婚礼老片、艺术短片",
"visual": ["明显的胶片颗粒", "温暖偏黄或偏品红的色彩偏移", "画面边缘的暗角",
"灰尘、毛发、划痕偶尔闪过", "18fps 带来的轻微卡顿感",
"过曝的高光呈现柔和的光晕"],
"operation": ["每段镜头很短(胶片昂贵,3到5秒就切)", "硬切,没有转场",
"曝光靠估计,时对时错", "无声(胶片本身不录音)或后期配的环境音"],
"negative": "没有同期声对白。没有平滑运镜。没有数字清晰度。",
},
"smartphone_vertical": {
"name": "智能手机竖屏(2015年后)",
"fit": "Vlog、社交媒体感、街头偶遇、当代日常",
"visual": ["9:16 竖构图", "过度锐化和 HDR 处理感",
"电子防抖造成的果冻效应", "暗光下的涂抹感降噪",
"前置镜头的轻微广角畸变"],
"operation": ["单手持机的轻微倾斜", "走路时的上下浮动(电子防抖压制后仍残留)",
"对着镜头说话的自拍视角切换", "手指偶尔擦过镜头边缘",
"突然抬起或放下手机的开始与结束"],
"negative": "没有横屏。没有三脚架稳定。没有专业布光。",
},
"gopro": {
"name": "GoPro / 运动相机",
"fit": "第一人称视角、运动、旅行、冒险感",
"visual": ["超广角鱼眼畸变,边缘拉伸", "高饱和的默认色彩",
"强光下的镜头眩光", "水滴或灰尘沾在镜头上"],
"operation": ["胸挂或头戴的第一人称视角", "剧烈但被电子增稳压制的震动",
"呼吸和身体晃动传导到画面", "视角随头部转动突然甩动"],
"negative": "没有第三人称跟拍。没有电影构图。没有慢动作(除非明确要求)。",
},
"cctv": {
"name": "监控摄像头 / CCTV",
"fit": "悬疑、都市传说、客观记录感、伪纪录片",
"visual": ["固定高位俯视角", "低帧率(每秒约10到15帧的顿挫感)",
"单色或低饱和色彩,夜间红外模式呈灰白", "角落的时间戳持续跳动",
"低分辨率,人脸模糊难辨"],
"operation": ["完全静止,或极缓慢的预设巡航转动", "无声",
"人物不知道被拍摄,行为完全自然"],
"negative": "没有摄影师存在。没有构图意识。没有声音。没有变焦跟随主体。",
},
"doc_handheld": {
"name": "专业纪录片(手持电影机)",
"fit": "高质感纪实、人物特稿、社会观察",
"visual": ["浅景深,主体清晰背景柔和", "自然光为主,宽容度高",
"克制的调色,接近肉眼所见"],
"operation": ["有经验的手持,微晃但不失控", "跟焦偶尔慢半拍(保留这个不完美)",
"长镜头耐心观察,不急于切换", "被摄者偶尔瞥向镜头又移开"],
"negative": "没有摆拍感。没有采访灯光。没有戏剧化运镜。没有配乐驱动的剪辑节奏。",
},
"webcam": {
"name": "网络摄像头 / 视频通话",
"fit": "疫情记忆、远程对话、屏幕录制感、亲密而低保真的交流",
"visual": ["720p 以下的低清晰度", "压缩造成的块状伪影,网络波动时画面糊成马赛克",
"顶光或屏幕光打在脸上的青白色", "固定机位,仰视或平视的笔记本视角"],
"operation": ["音画偶尔不同步", "人物偶尔卡顿定格再恢复", "背景是真实的房间一角"],
"negative": "没有专业收音。没有补光灯质感。没有运镜。",
},
"flip_phone_2003": {
"name": "2003年翻盖手机摄像(CMOS低保真)",
"fit": "极度怀旧、千禧年初学生时代、赛博朋克低保真、暗角情绪短片",
"visual": ["极低分辨率(176×144 或 320×240)", "严重 JPEG 压缩块状伪影,色块化严重",
"色彩饱和度极端且偏色严重(通常偏蓝绿或偏品红)", "暗部几乎完全噪点糊死",
"画面中央清晰度尚可,边缘急剧劣化", "固定数字变焦(裁切放大)",
"超窄动态范围,高光纯白一片"],
"operation": ["单手持机,手臂伸直自拍或随意抓拍", "翻盖打开即拍,几乎没有延迟补偿",
"快门时滞极长(按下到实际拍摄有0.5到1秒延迟)",
"录像按一下开始,再按一下结束,没有预览确认",
"对焦基本靠固定焦段,前后移动自身来「对焦」",
"录制过程中镜头容易被手指或翻盖边缘遮挡"],
"negative": "没有清晰度。没有平滑运动。没有色彩准确度。没有变焦操作。",
},
}
# ════════════════════════════════════════════════════════════
# ★ 氛围词典(抽象氛围 → 具体可拍现象)
# 来源:atmosphere-dictionary.md(原文存 docs/realism/)
#
# 原则:**禁止直接使用抽象氛围词**,必须翻译成具体视觉/听觉现象,
# 每种氛围至少选 3~5 个现象组合使用。
# ════════════════════════════════════════════════════════════
ATMOSPHERE_DICT: dict = {
"温暖": {
"visual": ["午后阳光透过树叶在地面投下移动的斑驳光影", "晾衣绳上的织物在微风中缓慢摇曳",
"陶瓷杯口冒出的细微热气", "猫或狗慵懒地靠近人",
"盆栽植物摆在窗台和门口台阶上", "人物把头发拨到耳后的小动作",
"不经意的真诚微笑,随即移开视线"],
"audio": ["晨间鸟鸣", "远处小孩嬉闹声", "织物摩擦声", "猫叫", "勺子碰杯壁"],
},
"怀旧": {
"visual": ["褪色的色彩与柔和的对比度", "老式电线杆和架空电线切割天空",
"墙面斑驳的低层住宅", "停在巷口的旧自行车", "画面上的日期戳",
"人物穿着宽松水洗牛仔裤等年代服饰"],
"audio": ["远处摩托车驶过", "老风扇转动声", "模拟信号的底噪"],
},
"孤独": {
"visual": ["大面积负空间构图,人物偏居画面一角", "空旷街道上唯一移动的身影",
"荧光灯的青白色调", "便利店或地铁站的深夜灯光",
"人物长时间静止,只有环境在动(车流、雨、霓虹闪烁)", "玻璃反光中模糊的人影"],
"audio": ["空调外机嗡鸣", "脚步回声", "远处地铁进站声", "雨点打在遮阳棚上"],
},
"紧张": {
"visual": ["画面边缘偶尔闯入不明物体或人影", "光源不稳定,灯管闪烁",
"门缝、走廊尽头等纵深阴影", "手持镜头呼吸感加重,微微颤抖",
"人物频繁回头或停顿倾听"],
"audio": ["突然中断的环境音", "远处不明的金属碰撞声", "自己的呼吸声被收进麦克风"],
},
"平静": {
"visual": ["极缓慢的光线变化(云影掠过整个画面)", "水面或窗帘的规律性微动",
"长镜头,机位几乎不动", "人物做重复性动作(浇水、叠衣、剥豆)"],
"audio": ["风穿过树叶的持续沙沙声", "水滴声", "远处若有若无的生活声"],
},
"热闹": {
"visual": ["前景不断有人或车穿过镜头", "摊贩的动作细节(翻炒、找零、擦桌子)",
"蒸汽、油烟、烤炉的热浪扭曲空气",
"多层次的动态:近景手部动作、中景人流、远景车流"],
"audio": ["叫卖声", "锅铲碰撞", "多人交谈混叠", "自行车铃"],
},
"梦境": {
"visual": ["过曝的窗户白成一片", "胶片颗粒与光晕", "镜头短,硬切,时间线跳跃",
"人物对镜头挥手或大笑但无声", "逆光轮廓,面部细节丢失"],
"audio": ["无声,或只有一层模糊的环境底噪"],
},
"田园": {
"visual": ["大片稻田或麦田在风中如波浪起伏", "人物赤脚踩在湿润泥土或田埂上",
"荷叶、野花、藤蔓等植物特写", "阳光透过树叶在人物脸上流动的光斑",
"远处农舍的炊烟和黑瓦白墙", "蜻蜓点水、蝴蝶停在花上",
"人物背影渐渐走远,融入田野尽头"],
"audio": ["连续的蛙鸣和虫鸣", "风吹稻田的沙沙声", "远处公鸡打鸣", "溪流声",
"脚踏湿泥的柔软声音", "竹叶碰撞声"],
},
"夏日": {
"visual": ["树影在地面剧烈晃动,光斑密集而明亮", "人物额头和鬓角有细微汗珠",
"手中冰饮或西瓜表面凝结水珠", "风扇叶片或蒲扇的转动",
"强烈阳光造成的高光过曝和镜头眩光", "热空气扭曲远处景物",
"牵牛花、向日葵、荷叶等盛夏植物", "偶尔的云影掠过造成短暂阴凉"],
"audio": ["比任何季节都响亮的蝉鸣", "空调外机或风扇的低沉嗡鸣",
"冰块碰杯的清脆声", "远处孩童的嬉笑和泼水声"],
},
"通勤": {
"visual": ["密集的人流与单个主体形成对比", "从室内到室外/地上到地下的曝光剧烈变化",
"呼出的白气(秋冬)或额头的汗(盛夏)", "低头看手机、翻包找卡等碎片化动作",
"车窗玻璃上反射的模糊面容", "通勤者独特的身体语言(缩脖子、拉围巾)",
"地铁隧道中快速掠过的灯光管"],
"audio": ["广播模糊的播报声", "列车进站的轰鸣和刹车尖叫", "闸机蜂鸣声",
"脚步匆忙的节奏", "车门气动声", "隧道风噪声"],
},
"青春": {
"visual": ["漫无目的的闲逛或长时间坐着发呆", "手部小动作(拨弄衣角、卷头发、转笔)",
"靠在墙边、栏杆上,重心随意切换", "低头笑、抬头看天、把脸埋进手臂",
"旧电扇转头吹动刘海和衣领", "地面上的影子被拉长,时间缓慢推移",
"零星对话,说话间隙被环境填满"],
"audio": ["远处收音机或电视的模糊声音", "老旧风扇的转动声", "冰棍棒折断声",
"偶尔的笑声然后继续安静"],
},
}
def device_pack_block(key: str, include_negative: bool = True) -> str:
"""设备缺陷参数包 → 提示词片段(可直接粘进「摄像风格」段)"""
p = DEVICE_PACKS.get((key or "").strip())
if not p:
return ""
out = [f"【设备美学:{p['name']}】"]
out.append("画面特征:" + ";".join(p["visual"]))
out.append("操作特征:" + ";".join(p["operation"]))
if include_negative and p.get("negative"):
out.append("负面约束:" + p["negative"])
return "\n".join(out)
def atmosphere_block(keys, n_each: int = 4) -> str:
"""氛围 → 具体现象片段(每种氛围取 n 个视觉 + 全部听觉的前若干个)
keys 可以是 str 或 list;未命中则原样返回空串(调用方可自行翻译)。
用法原则:**不要直接写抽象氛围词,用这些具体现象替代**。
"""
if isinstance(keys, str):
keys = [keys]
out = []
for k in (keys or []):
a = ATMOSPHERE_DICT.get(str(k).strip())
if not a:
continue
vis = a["visual"][:max(1, n_each)]
aud = a["audio"][:max(1, n_each // 2)]
out.append(f"【{k}(用具体现象替代)】视觉:" + ";".join(vis)
+ ("。听觉:" + ";".join(aud) if aud else ""))
return "\n".join(out)
def assign_device_defects(n_shots: int) -> list:
"""按「缺陷不重复原则」给每个镜头分配**不同类型**的摄像机缺陷
来源:camera-aesthetics.md 进阶用法 —— 同一提示词里每镜的缺陷类型不重复:
对焦 → 构图 → 抖动 → 曝光 → 反应延迟 → 呼吸感 → 录制中断
"""
seq = [
"自动对焦在前景和背景之间搜索", # 对焦缺陷
"构图偏离中心,操作者跟不上主体", # 构图缺陷
"跟拍晃动 + 运动模糊", # 抖动缺陷
"进入阴影时曝光波动", # 曝光缺陷
"镜头慢了半拍才跟上动作", # 反应延迟
"自然的镜头呼吸和焦距漂移", # 呼吸感
]
out = []
for i in range(max(0, n_shots)):
if i == n_shots - 1 and n_shots > 1:
out.append("画面突然切到黑屏(录制中断)") # 结尾固定收束
else:
out.append(seq[i % len(seq)])
return out
def focus_path_hint() -> str:
"""「自动对焦路径指定法」提示(效果翻倍的关键技巧)
不要笼统写"自动对焦搜索",要给出**对焦起点和终点**。
"""
return ("【对焦路径指定(关键技巧)】不要笼统写「自动对焦搜索」——"
"要明确写出对焦的起点和终点,模型才知道对焦该在哪两个物体之间走:\n"
" ❌ 自动对焦反复搜索\n"
" ✅ 自动对焦在她脸部和她手中的西瓜之间来回切换\n"
" ✅ 自动对焦从远处山影漂移到她后背再拉回远处")