""" 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. **不要写慢动作**(除非明确设定为手机慢动作模式)——真实拍摄默认实时速度。" )