Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| import subprocess | |
| import re | |
| import time | |
| import datetime | |
| import shutil | |
| import json | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| # 1. 自动安装依赖 | |
| def ensure_dependencies(): | |
| try: | |
| import gradio | |
| import requests | |
| except ImportError: | |
| print("正在安装所需依赖: gradio, requests...") | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "gradio", "requests"]) | |
| ensure_dependencies() | |
| import gradio as gr | |
| import requests | |
| # ================= 配置区域 ================= | |
| DEFAULT_LLM_API_KEY = "sk-DZ5g7Zu0lFDlR7mBkbNsZLFTt1KBqA8ocsAH1mcvsZDWtydx" | |
| DEFAULT_VIDEO_API_KEY = "sk-G6LN0uC2BVclZjx1ObDJPkMZTZvtjau1Ss7GjCvRLJyI5euU" | |
| MERCHANT_BASE_URL = "https://xingjiabiapi.com" | |
| VEO_MODEL = "veo_3_1-fast" | |
| VIDEO_SIZE = "16x9" | |
| TEXT_MODEL = "gemini-3-pro-preview-thinking" | |
| CONFIG_FILE = "prompt_config.json" | |
| # ================= 全局变量 ================= | |
| IS_PAUSED = False | |
| BATCH_SIZE = 10 | |
| def toggle_pause(): | |
| global IS_PAUSED | |
| IS_PAUSED = not IS_PAUSED | |
| return "▶️ 恢复运行" if IS_PAUSED else "⏸️ 暂停任务" | |
| # =============================================== | |
| # --- 默认提示词备份 --- | |
| # =============================================== | |
| # 1. 工程师:全链路冗余记录员 (🔥已升级:强制各阶段 30+ 步骤) | |
| FALLBACK_ENGINEER = """ | |
| 你现在的身份是 **全链路工业冗余记录员 (The Full-Chain Industrial Logger)**。 | |
| 你的任务是输出一份 **极度详细、甚至显得啰嗦** 的全流程生产日志。**不要替导演做减法,你的任务是提供海量素材!** | |
| **⚠️ 核心指令 1:全链路覆盖与数量硬指标 (Mandatory Full Chain & Quota)** | |
| 你必须按顺序覆盖以下 **6 大阶段**,且对关键阶段有严格的数量要求: | |
| 1. **[源头与收获]** (Harvest): 田间/矿场的机械化采集 -> 装车。 | |
| 2. **[进厂与净化]** (Intake & Cleaning): 卸货 -> 去石 -> 多级清洗 -> 分选。**(🔥死命令:此阶段微步骤总数不低于 30 个!要反复写清洗细节)** | |
| 3. **[预处理]** (Prep): 去皮/破碎/切割/研磨。**(🔥死命令:此阶段微步骤总数不低于 30 个!)** | |
| 4. **[核心质变]** (Transformation): 榨汁/油炸/冶炼/混合/反应。**(🔥死命令:此阶段微步骤总数不低于 30 个!要拆解每一秒的物理变化)** | |
| 5. **[精炼与成型]** (Refinement): 过滤/杀菌/干燥/冷却/压制。**(🔥死命令:此阶段微步骤总数不低于 30 个!)** | |
| 6. **[包装与物流]** (Packaging): 吹瓶/制罐 -> 灌装 -> 封口 -> 贴标 -> 喷码 -> 装箱 -> 码垛。 | |
| **⚠️ 核心指令 2:1:5 裂变法则 (The 1:5 Fission Law)** | |
| 不要概括!任何一个动作必须拆解为 **5个微步骤**。 | |
| * *Bad*: "清洗土豆。" | |
| * *Good*: | |
| * 步骤 2.1: 土豆落入气泡清洗池,激起水花。 | |
| * 步骤 2.2: 高压气泡翻滚,土豆表面泥土松动。 | |
| * 步骤 2.3: 滚筒毛刷摩擦土豆表皮,去除顽固污渍。 | |
| * 步骤 2.4: 喷淋装置用净水冲洗土豆表面。 | |
| * 步骤 2.5: 风刀吹干土豆表面的残余水珠。 | |
| **⚠️ 核心指令 3:物理细节描述** | |
| 在每个微步骤中,必须包含:**力学** (撞击/摩擦)、**热力学** (蒸汽/起泡)、**流体力学** (飞溅/漩涡)。 | |
| **输出格式 (请严格执行,全部中文):** | |
| **阶段 [序号]:[阶段名称]** | |
| * **微步骤 [x.1]**: [动作描述] + [物理反馈细节] | |
| * **微步骤 [x.2]**: [动作描述] + [物理反馈细节] | |
| (请确保总步骤数足以支撑 20 分钟的视频,总计输出不低于 150 个微步骤) | |
| """ | |
| # 2. 导演 1.0:全能融合版 (🔥完整 19 铁律 + 7D 格式 + 多机位) | |
| FALLBACK_DIRECTOR_V1 = """ | |
| 你现在是 **AI 视频提示词架构师** 兼 **“多角度强迫症”工业纪录片总导演**。 | |
| 你拥有一份由架构师提供的《工艺技术详情》作为**参考知识库**。 | |
| **你的核心任务**:基于这些技术步骤,构思一部 **15-20分钟的史诗级、沉浸式、解压** 现代工业纪录片。 | |
| **⚠️ 核心指令 0:解压节点的多机位重复法则 (The Law of Satisfying Repetition)** | |
| * **定义**:对于 **倾倒、清洗、传送、切割、搅拌、流体流动** 等高解压画面,**绝对禁止**一个镜头带过! | |
| * **执行**:必须使用 **3-5 个连续镜头**,从不同角度去表现同一个动作的持续进行。 | |
| * *Shot A*: 宏观全景 (展示规模/洪流)。 | |
| * *Shot B*: 俯视上帝视角 (展示几何阵列/秩序)。 | |
| * *Shot C*: 侧面特写 (展示物理质感)。 | |
| * *Shot D*: 极度微距/探针 (展示纹理/撞击/飞溅)。 | |
| **⚠️ 核心指令 1:大片级叙事与节奏控制 (The 10-80-10 Rule)** | |
| 1. **前奏 (Origin)**:原料采集 -> 进厂。**严格控制在总篇幅的 10% 以内**。 | |
| 2. **高潮 (Core Processing)**:核心加工环节。**必须占据 80% 以上的篇幅**。这是观众最爱看的解压部分。 | |
| 3. **尾声 (Packaging)**:最终封装。**控制在 10% 以内**。 | |
| **📜 核心创作铁律合集 (The Consolidated Iron Laws) [🔥完整保留]** | |
| **第一类:视觉氛围与工厂设定** | |
| 1. **BBC 质感与风格**:所有镜头必须具备 **BBC 纪录片风格**。画面必须干净、电影级。**绝对禁止字幕**。 | |
| 2. **密度分流法则**: | |
| * **默认状态**:生产/运输/清洗/堆叠 -> 必须是 `Thousands of (成千上万), A sea of (海洋般的)`。 | |
| * **特例状态**:抽样/显微镜/检测 -> 必须是 `Single (单个), Isolated (独立的)`。 | |
| 3. **重力来源法则**:严禁魔幻般“从天而降”。所有倾泻必须写明源头设备(如:`pouring from a stainless steel hopper`)。 | |
| 4. **高亮食欲光**:明亮通透,拒绝暗调。 | |
| 5. **泥土与洁净冲突**:清洗前必须脏(带泥土),清洗后必须净(水珠光泽)。 | |
| **第二类:人机协同与安全** | |
| 6. **人物与人声铁律** [🔥绝对红线]: | |
| * **视觉比例**:人机协同画面严格控制在 **总镜头的 30% 以内**。机器永远是主角。 | |
| * **听觉禁令**:**全片绝对禁止人声** (0% Human Voice)。严禁出现:说话声、采访声、旁白。 | |
| 7. **专业形象**:`身穿白色无菌服的工人, 蓝色乳胶手套, 防尘网帽`。 | |
| 8. **安全约束**:严禁血腥。描述必须是 `强力(Powerful), 极速(High-Speed)`。 | |
| **第三类:物理质感与形态** | |
| 9. **物料演变法则**:严禁穿越。捕捉形态改变瞬间。 | |
| 10. **湿润与材质质感**:强调湿润感、光泽感。 | |
| 11. **刚体碰撞法则**: | |
| * 拒绝柔和。固体是撞击,不是流动。 | |
| * 关键词:`刚性碰撞, 互相挤压, 剧烈弹跳, 机械震动`。 | |
| **第四类:运镜与机械逻辑** | |
| 12. **开篇震撼法则**:Shot 1 必须是宏大的源头场景。 | |
| 13. **高速通量法则**:传送带永远在高速运转,机械韵律即BGM。 | |
| 14. **机械秩序法则**:机器动作无卡顿,展现毫秒级同步的数学美感。 | |
| 15. **机械原理揭示**:`微距特写, 刀片切入点, 摩擦纹理, 物理接触面`。 | |
| 16. **沉浸式运镜**:`第一人称视角 (POV), 传送带视角, 追踪镜头, Probe Lens (探针镜头)`。 | |
| **第五类:硬核听觉** | |
| 17. **硬核听觉纹理**:`清脆的咔哒声, 沉重的撞击声, 电机高频啸叫, 湿润拍击声`。 | |
| **第六类:逻辑衔接** | |
| 18. **全链路物理传输法则**:**绝不瞬移**。前段要有完整的 `收割->装车->运输->卸货` 链条。 | |
| **输出格式 (7D 旗舰版 A++ Optimized) - 必须输出 [用户指定数量] 个镜头!** | |
| 镜头 [序号] | [中文标题] | |
| Veo 提示词 (格式 A++ Optimized): | |
| 01. 主体与密度:[🔥核心死令:必须显式包含[产品名]!必须是“无尽的[产品]阵列”、“[产品]的洪流”。严禁单数。] | |
| 02. 材质与光影:[合并描述:表面纹理(粗糙/湿润/油亮) + 光照交互(金属反光/透光/高光)] | |
| 03. 动作演变:[🎬 关键!描述动作。如果是“重复性解压镜头”,请强调其持续性、循环性、无休止性] | |
| 04. 物理反馈:[合并描述:(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹] | |
| 05. 环境构建:[工厂场景(无菌室/不锈钢背景) + 空间深度。🔥严禁“纯黑虚空”!] | |
| 06. 镜头语言:[🔥必须多变!在此处轮换使用:1.上帝俯视 2.侧面平视 3.探针穿越 4.微距特写] | |
| 07. 核心音效:[🔥强制硬核:只有机械声(Clanking/Whirring)和物理声(Splashing/Cracking)。严禁人声。] | |
| """ | |
| # 3. 导演 2.0 (保持不变) | |
| FALLBACK_DIRECTOR_V2 = """ | |
| 你现在是 **导演 2.0 (状态进化与逻辑审计引擎)**。 | |
| 你的目标是维护 **物理状态链**、**人机交互逻辑** 以及 **全流程完整性**。 | |
| **💀 语义清洗与进化法则 (必须严格执行)** | |
| **0. ⚠️ 主权归还审计 (Subject Restoration Audit)** [🔥最高优先级] | |
| * **检测**:检查 `主体` 字段。 | |
| * **规则**:如果 `主体` 描述的是 **机器、工具、容器、环境** 或 **工人/人类**,这是**严重错误**。机器和人必须退居二线。 | |
| * **修正执行**:将 `主体` **强制重写** 为 **“正在经历该工序的 [产品]”**。 | |
| **1. ⚠️ 绝对物种锁定 (Absolute Species Lock)** [🔥防止幻觉] | |
| * **现象**:当出现“泥土/卡车卸货”时,LLM容易幻觉成“甜菜”、“土豆”或“矿石”。 | |
| * **纠错**:检查 `主体`。如果出现了非 **[产品]** 的名词,**必须强制替换回 [产品]**。 | |
| **2. ⚠️ 容器一致性审计 (Container Consistency Audit)** | |
| * **规则**:检查包装环节的容器描述。 | |
| * **执行**:如果之前的镜头确定了是“玻璃瓶”,本镜头严禁变成“塑料瓶”。 | |
| **3. ⚠️ 客观密度与重力源头审计 (Logic Audit)** | |
| * **密度审计**: | |
| * **情况 A:一般工序** -> **强制复数/密集 (成千上万)**。 | |
| * **情况 B:特殊工序 (抽样/检测)** -> **允许单体**。 | |
| * **重力源审计**: | |
| * 如果描述了“倾泻/掉落”,强制加上 `从不锈钢料斗倾泻`。 | |
| **4. 极度冗余的主体描述 (Redundant Subject Description)** [🔥核心] | |
| * **规则**:每个视频是独立生成的。每个镜头的主体必须是**独立、完整、详细**的描述。 | |
| * **执行**:必须堆叠形容词。例如:不要只写 "切片",要写 "完美均匀的圆形[产品]切片,闪烁着汁液"。 | |
| **5. 形态进化与强制替换 (Material Evolution - 强制执行)**: | |
| * **⚠️ 关键指令:消除歧义!必须明确写出[产品]的物理状态后缀。** | |
| * **当 [切片/切割] 发生后**: 删除 "完整",替换为 "切片/横截面"。 | |
| * **当 [粉碎/研磨] 发生后**: 删除 "块状",替换为 "碎屑/粉尘"。 | |
| * **当 [液化/榨汁] 发生后**: 删除 "固体",替换为 "浆液/流体"。 | |
| **6. 清洁度进化 (Dirty Contrast)**: | |
| * **当 [清洗] 发生后**:强制删除 "脏/泥泞",强制添加 "干净,晶莹剔透"。 | |
| **7. 🚫 反孤独/反虚空铁律 (Anti-Isolation Iron Law)** [🔥新增]: | |
| * **严禁单体英雄镜头**:除非是“实验室显微镜”或“X光检测”场景,否则**绝对禁止**出现“单个产品在黑暗虚空中旋转”的描述。 | |
| * **强制集群**:必须描述为“成千上万”、“密集的”、“堆积如山的”。 | |
| * **拒绝纯黑背景**:严禁 "Pitch black void"。背景必须是“工业不锈钢”、“工厂模糊背景”。 | |
| **8. 🛡️ 词汇安全协议 (Vocabulary Safety Protocol)** [🔥新增]: | |
| * **敏感词清洗**:检查全文。如果出现 "Execution", "Death Penalty", "Torture", "Violence", "Terror" 等词。 | |
| * **强制替换**:用 "Industrial Rejection", "High Kinetic Energy", "Intense Processing" 等工业术语替换。 | |
| **9. 🌊 主体豁免与介质置换协议 (Subject Exemption & Media Replacement)** [🔥核心逻辑]: | |
| * **死锁解决**:当产品尚未入场(如预热/搅拌启动)时,**为了防止AI幻觉**,必须执行以下置换: | |
| * **策略 A (介质即洪流)**:将“工业介质”(油/水/蒸汽)升级为高密度主体(如“金色的油海”)。 | |
| * **策略 B (前景借位)**:利用“前景堆积的原料”作为视觉重心,拍摄后景的机器动作。 | |
| **输出格式 (必须完全一致,包含19个核心维度):** | |
| 镜头 [序号]/[总数] | [中文标题] | |
| Veo 提示词 (格式 A++ Optimized): | |
| 01. 主体与密度:[🔥核心死令:必须显式包含[产品名]!禁止只写“切片/物体”。必须是“无尽的[产品]阵列”、“[产品]的洪流”。如果产品未入场,则描述“油的海洋”或“蒸汽墙”。严禁单数。] | |
| 02. 材质与光影:[合并描述:表面纹理(粗糙/湿润) + 光照交互(金属反光/高光/次表面散射)] | |
| 03. 动作演变:[🎬 关键!用一句话描述 T=0s(入画) -> T=4s(高潮/撞击) -> T=8s(出画) 的完整过程。动作必须连贯] | |
| 04. 物理反馈:[合并描述:包含原来的(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹] | |
| 05. 环境构建:[工厂场景(无菌室/不锈钢背景) + 空间深度(景深虚化)] | |
| 06. 镜头语言:[合并描述:机位(微距/探针) + 运镜(跟随/推拉) + 光学参数(广角/眩光)] | |
| 07. 核心音效:[🔥强制硬核:只有机械声(Clanking/Whirring)和物理声(Splashing/Cracking)。严禁人声/音乐。] | |
| [状态连接]: [用于下一个镜头的干净、优化的形容词列表。例如 "干净的,去皮的,切片的"] | |
| """ | |
| # =============================================== | |
| # --- 提示词管理函数 --- | |
| # =============================================== | |
| def load_prompts(): | |
| """从 JSON 加载提示词""" | |
| if os.path.exists(CONFIG_FILE): | |
| try: | |
| with open(CONFIG_FILE, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| return data.get("engineer", FALLBACK_ENGINEER), \ | |
| data.get("director1", FALLBACK_DIRECTOR_V1), \ | |
| data.get("director2", FALLBACK_DIRECTOR_V2) | |
| except Exception: pass | |
| return FALLBACK_ENGINEER, FALLBACK_DIRECTOR_V1, FALLBACK_DIRECTOR_V2 | |
| def save_prompts(p_eng, p_v1, p_v2): | |
| """保存提示词到 JSON""" | |
| data = { | |
| "engineer": p_eng, | |
| "director1": p_v1, | |
| "director2": p_v2, | |
| "last_updated": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| try: | |
| with open(CONFIG_FILE, 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=4) | |
| return f"✅ 配置已保存 ({data['last_updated']})" | |
| except Exception as e: | |
| return f"❌ 保存失败: {e}" | |
| def refresh_config_on_load(): | |
| return load_prompts() | |
| # =============================================== | |
| # --- 核心逻辑 (API 调用) --- | |
| # =============================================== | |
| def get_timestamp(): | |
| return datetime.datetime.now().strftime("%H:%M:%S") | |
| def log_msg(logs, msg, level="INFO"): | |
| icon = {"INFO": "ℹ️", "PROCESS": "⚙️", "SUCCESS": "✅", "WARN": "⚠️", "STATE": "🧬", "WAIT": "⏳", "NET": "📡", "RETRY": "🔄", "ERROR": "❌", "PAUSE": "⏸️", "AUDIT": "👁️"}.get(level, "") | |
| entry = f"[{get_timestamp()}] {icon} {msg}" | |
| logs.append(entry) | |
| return "\n".join(logs) | |
| # 1. 工程师 | |
| def execute_engineer(topic, api_key, system_prompt): | |
| if not api_key: yield "❌ 未提供 API Key"; return | |
| yield "⏳ [工程师] 正在构建现代工厂工艺流程 (全链路+裂变模式)..." | |
| url = f"{MERCHANT_BASE_URL}/v1/chat/completions" | |
| headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"} | |
| data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"产品: {topic}"}]} | |
| try: | |
| yield "📡 [工程师] 正向 LLM 发送请求..." | |
| res = requests.post(url, headers=headers, json=data, timeout=120) | |
| yield res.json()['choices'][0]['message']['content'] | |
| except Exception as e: yield f"❌ 错误: {e}" | |
| # 2. 导演 1.0 (分批接力 + 全量历史) | |
| def execute_director_v1(topic, architecture, count, api_key, system_prompt): | |
| if not architecture: yield "❌ 请先生成工艺架构"; return | |
| V1_BATCH_SIZE = 10 | |
| full_script_content = "" | |
| yield f"⏳ [导演 1.0] 正在构思 {count} 个镜头的解压沉浸剧本 (启用多角度重复法则)..." | |
| for batch_start in range(1, count + 1, V1_BATCH_SIZE): | |
| batch_end = min(batch_start + V1_BATCH_SIZE - 1, count) | |
| current_batch_count = batch_end - batch_start + 1 | |
| yield f"📡 [导演 1.0] 正在生成第 {batch_start} - {batch_end} 个镜头 (Batch Processing)..." | |
| context_instruction = "" | |
| if full_script_content: | |
| context_instruction = f""" | |
| **⚠️ 前序镜头全量历史 (Context History - DO NOT REPEAT)**: | |
| {full_script_content} | |
| **⚠️ 接力指令**: | |
| 以上是第 1 到第 {batch_start - 1} 个镜头的剧本。 | |
| 请紧接着上面的内容,生成第 {batch_start} 到第 {batch_end} 个镜头。 | |
| 1. **逻辑连贯**:严格继承上一个镜头的产品状态(State)和时间线。 | |
| 2. **多角度执行**:如果是解压环节(如倾倒、清洗),请务必换个角度继续展示,不要急着跳到下一步。 | |
| """ | |
| user_content = f""" | |
| 产品: {topic} | |
| 工程协议: | |
| {architecture} | |
| {context_instruction} | |
| 任务: 生成第 {batch_start} 到 {batch_end} 个镜头 (共 {current_batch_count} 个)。 | |
| 关键: 严格遵循所有核心铁律 (7D格式 + 绝对主体 + 宏大量词 + 多角度重复)。 | |
| **⚠️ 特别限制**: | |
| 1. **人机协同画面**:严格控制在总数的 30% 以内 (仅在 3-4 个镜头中出现人物)。 | |
| 2. **声音禁令**:全片严禁人声 (0% Human Voice)。 | |
| **警告:必须真实生成 {current_batch_count} 个独立的提示词块!** | |
| 输出: 结构化镜头列表,全部使用中文。 | |
| """ | |
| url = f"{MERCHANT_BASE_URL}/v1/chat/completions" | |
| headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"} | |
| data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}]} | |
| try: | |
| res = requests.post(url, headers=headers, json=data, timeout=300) | |
| if res.status_code != 200: | |
| yield f"❌ API Error: {res.text}" | |
| return | |
| new_content = res.json()['choices'][0]['message']['content'] | |
| full_script_content += "\n" + new_content | |
| yield full_script_content | |
| except Exception as e: | |
| yield f"❌ 错误: {e}" | |
| return | |
| # 3. 导演 2.0 API (带 prev_context_shots) | |
| def call_director_v2_batch_api(batch_plans, start_idx, total, prev_state, prev_context_shots, api_key, system_prompt): | |
| joined_plans = "\n\n".join(batch_plans) | |
| context_instruction = "" | |
| if prev_context_shots: | |
| context_instruction = f""" | |
| **👁️ 扩展视觉记忆 (Extended Visual Memory - Last 10 Shots)**: | |
| 为了解决“准备阶段无产品”导致的逻辑断层,以下是过去 **10个镜头** 的记录。 | |
| **关键指令**:请回溯这10个镜头,找到 **[产品]** 最后一次出现时的物理状态。 | |
| 如果上一镜只是“热油/空转机器”,请继承更早之前的“原料状态”,不要让产品凭空消失或重置。 | |
| --- | |
| {prev_context_shots} | |
| --- | |
| **强制约束**: Shot {start_idx} 必须继承[产品]的真实物理状态,并延续视觉风格。 | |
| """ | |
| user_content = f""" | |
| **批量任务**: 正在处理第 {start_idx} 到第 {start_idx + len(batch_plans) - 1} 个镜头 (共 {len(batch_plans)} 个)。 | |
| 🧬 **初始状态 (Batch Start State)**: "{prev_state}" | |
| {context_instruction} | |
| 📜 **导演 1 原始计划组 (Raw Batch)**: | |
| {joined_plans} | |
| **你的任务 (Batch Execution)**: | |
| 请**依次**处理这 {len(batch_plans)} 个镜头。对于每一个镜头,应用以下规则,并**连续输出**所有结果: | |
| 1. **链式推演指令 (Chain-of-Thought)**: | |
| - 严禁并发。必须先写完 Shot N,锁定其输出状态,再将其作为 Shot N+1 的不可逆前提。 | |
| - 必须形成严格的因果链。 | |
| 2. **检查“主体”字段**:强制应用当前的物理状态 (物料演变)。 | |
| 3. **极度冗余描述**: 不要简写。如果产品切片了,详细描述切片的纹理。 | |
| 4. **清洗 (PURGE)** 矛盾属性 (泥土与洁净冲突)。 | |
| 5. **人声审计**: 确保输出中绝对没有"说话", "人声", "Voice"等词汇。 | |
| 6. **安全审查**: 替换掉所有的 "死刑", "暴力", "恐怖" 等可能违规的词语。 | |
| 7. **主体消歧义与冗余**: 🔥死刑级规则!每个镜头的“主体”字段必须**显式包含产品名称**(例如“马铃薯切片”),严禁使用代词(它/它们)或泛指名词(切片/物体)。Veo不知道上下文!此外,必须使用**极度冗余**的形容词堆叠。 | |
| 8. **密度与背景铁律**: 除了实验室检测,**绝对禁止**“单个产品”或“黑暗虚空背景”。必须描述为“传送带上密集的[产品]流”、“成千上万的[产品]”。如果原文是“一个[产品]在旋转”,**强制改写**为“无数[产品]在旋转”。 | |
| 9. **准备阶段豁免 (Pre-processing Exception)**: 如果产品尚未入场(如预热/空转),**必须**将“油/水/蒸汽”描述为“海洋/洪流”来填满画面,或者使用“前景堆积的原料”作为借位。 | |
| **输出格式要求**: | |
| 必须输出 {len(batch_plans)} 个完整的 7D 格式块。 | |
| 在所有镜头输出完毕后,**必须**输出一行最终状态: | |
| `[BATCH_END_STATE]: <这里写最后一个镜头完成后的物理状态>` | |
| """ | |
| url = f"{MERCHANT_BASE_URL}/v1/chat/completions" | |
| headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"} | |
| data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}], "temperature": 0.3} | |
| try: | |
| res = requests.post(url, headers=headers, json=data, timeout=300) | |
| content = res.json()['choices'][0]['message']['content'] | |
| final_state = prev_state | |
| if "[BATCH_END_STATE]" in content: | |
| parts = content.split("[BATCH_END_STATE]") | |
| content_body = parts[0].strip() | |
| state_raw = parts[1].strip() | |
| if state_raw.startswith(":") or state_raw.startswith(":"): | |
| final_state = state_raw[1:].strip() | |
| else: | |
| content_body = content | |
| return content_body, final_state | |
| except Exception as e: return None, str(e) | |
| # 3. 导演 2.0 循环 (结果缓存 + 10段回溯) | |
| def execute_director_v2_loop(v1_text, count, api_key, system_prompt): | |
| if not v1_text: yield "❌ 请先生成导演 1.0 的视觉方案", ""; return | |
| global IS_PAUSED | |
| IS_PAUSED = False | |
| logs = [] | |
| v2_text_display = "" | |
| full_results_history = [] | |
| raw_text = v1_text.strip() | |
| header_pattern = re.compile(r'(?:^|[\n\r])(?:[\*\#\>\-\s]*)(?:镜头|Shot)\s*(\d+)', re.IGNORECASE) | |
| matches = list(header_pattern.finditer(raw_text)) | |
| if not matches: | |
| yield log_msg(logs, "⚠️ 未检测到 '镜头' 关键字,尝试兜底解析...", "WARN"), v2_text_display | |
| header_pattern = re.compile(r'(?:^|[\n\r])(\d+[\.、])', re.IGNORECASE) | |
| matches = list(header_pattern.finditer(raw_text)) | |
| if not matches: | |
| yield log_msg(logs, f"❌ 解析失败:无法识别任何镜头。", "ERROR"), v2_text_display | |
| return | |
| visual_plans = [] | |
| for i in range(len(matches)): | |
| start = matches[i].start() | |
| end = matches[i+1].start() if i+1 < len(matches) else len(raw_text) | |
| block = raw_text[start:end].strip() | |
| if len(block) > 5: visual_plans.append(block) | |
| yield log_msg(logs, f"🚀 导演 2.0 启动:状态进化引擎 (7D 旗舰版 | Batch={BATCH_SIZE})", "SUCCESS"), v2_text_display | |
| yield log_msg(logs, f"📊 识别到 {len(visual_plans)} 个镜头", "INFO"), v2_text_display | |
| prev_state = "原材料,脏的,完整的,未处理的 (自然状态)" | |
| yield log_msg(logs, f"🧬 [初始状态] {prev_state}", "STATE"), v2_text_display | |
| total_shots = len(visual_plans) | |
| for i in range(0, total_shots, BATCH_SIZE): | |
| batch = visual_plans[i : i + BATCH_SIZE] | |
| start_idx = i + 1 | |
| end_idx = min(i + BATCH_SIZE, total_shots) | |
| # 🔥 核心修复:移除错误的 idx 变量判断,使用 start_idx 替代 | |
| if start_idx > count: break | |
| if IS_PAUSED: | |
| yield log_msg(logs, f"⏸️ 已暂停...", "PAUSE"), v2_text_display | |
| while IS_PAUSED: time.sleep(1) | |
| yield log_msg(logs, f"▶️ 恢复", "PROCESS"), v2_text_display | |
| # 🔥 核心修改:回溯最后 10 个镜头 | |
| prev_context_shots = "" | |
| if full_results_history: | |
| last_10 = full_results_history[-10:] | |
| prev_context_shots = "\n\n".join(last_10) | |
| yield log_msg(logs, f"👁️ [视觉审阅] 回溯 Shot {max(1, start_idx-10)} - {start_idx-1} 以确保状态连续...", "AUDIT"), v2_text_display | |
| else: | |
| yield log_msg(logs, f"👁️ [视觉审阅] 初始批次,无前序记忆", "INFO"), v2_text_display | |
| yield log_msg(logs, f"⚙️ [批量处理] 镜头 {start_idx} - {end_idx} ({len(batch)}个)...", "PROCESS"), v2_text_display | |
| yield log_msg(logs, f"🧬 [思维链继承] 起始状态: {prev_state}", "STATE"), v2_text_display | |
| success = False | |
| for attempt in range(5): | |
| if IS_PAUSED: | |
| yield log_msg(logs, "⏸️ 暂停中...", "PAUSE"), v2_text_display | |
| while IS_PAUSED: time.sleep(1) | |
| try: | |
| if attempt > 0: yield log_msg(logs, f" 🔄 重试 ({attempt}/5)...", "RETRY"), v2_text_display | |
| batch_result, next_state = call_director_v2_batch_api( | |
| batch, start_idx, count, prev_state, prev_context_shots, api_key, system_prompt | |
| ) | |
| if batch_result: | |
| success = True | |
| yield log_msg(logs, f" 📥 Batch 完成", "SUCCESS"), v2_text_display | |
| v2_text_display += f"{batch_result}\n\n" | |
| current_blocks = re.split(r'(?:^|[\n\r])(?:[\*\#\>\-\s]*)(?:镜头|Shot)\s*\d+', batch_result) | |
| current_blocks = [b.strip() for b in current_blocks if len(b) > 20] | |
| full_results_history.extend(current_blocks) | |
| if prev_state != next_state: | |
| yield log_msg(logs, f" 🌊 [进化] ... -> {next_state}", "STATE"), v2_text_display | |
| else: | |
| yield log_msg(logs, f" 🧬 [保持] {next_state}", "STATE"), v2_text_display | |
| prev_state = next_state | |
| break | |
| time.sleep(1.5 * (attempt + 1)) | |
| except Exception as e: | |
| yield log_msg(logs, f" ⚠️ Batch 错误: {e}", "WARN"), v2_text_display | |
| time.sleep(1.5 * (attempt + 1)) | |
| if not success: | |
| yield log_msg(logs, f" ❌ Batch 失败 (跳过)", "ERROR"), v2_text_display | |
| yield "\n".join(logs[-20:]), v2_text_display | |
| time.sleep(0.1) | |
| yield log_msg(logs, "🎉 全片逻辑闭环完成", "SUCCESS"), v2_text_display | |
| # =============================================== | |
| # --- 渲染逻辑升级 (7要素拼接 + 格式化展示) --- | |
| # =============================================== | |
| def inject_satisfaction_physics(satisfaction_text): | |
| booster = "" | |
| if not satisfaction_text: return booster | |
| if any(k in satisfaction_text for k in ["填满", "堆叠", "丰盛", "空虚", "数量"]): | |
| booster += ",极高密度填充,画面无任何空隙,形成视觉实体墙," | |
| if any(k in satisfaction_text for k in ["秩序", "同步", "整齐", "治愈", "强迫症"]): | |
| booster += ",完美几何对齐,毫秒级机械同步运动,无任何误差," | |
| if any(k in satisfaction_text for k in ["剥离", "去除", "刮", "磨", "分离", "炸开"]): | |
| booster += ",表面污垢被物理暴力剥离,瞬间露出完美内部," | |
| if any(k in satisfaction_text for k in ["净化", "洁净", "丝滑", "顺滑", "粘稠"]): | |
| booster += ",极度洁净的表面,丝滑的高光反射,无暇质感," | |
| return booster | |
| def assemble_veo_prompt(raw_block): | |
| """提取+拼接+转译 (适配新的 7D 格式)""" | |
| def extract(key_pattern, text): | |
| match = re.search(key_pattern, text) | |
| return match.group(1).strip().rstrip("。") if match else "" | |
| # 🔥 核心修改:适配新的 7D 格式 | |
| f_subject = extract(r"01\. 主体与密度:(.*?)\n", raw_block) | |
| f_texture = extract(r"02\. 材质与光影:(.*?)\n", raw_block) | |
| f_action = extract(r"03\. 动作演变:(.*?)\n", raw_block) | |
| f_physics = extract(r"04\. 物理反馈:(.*?)\n", raw_block) | |
| f_env = extract(r"05\. 环境构建:(.*?)\n", raw_block) | |
| f_camera = extract(r"06\. 镜头语言:(.*?)\n", raw_block) | |
| physics_booster = inject_satisfaction_physics(f_physics) | |
| parts = [ | |
| "BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实电影质感", | |
| f"{f_camera}", | |
| f"{f_subject}", | |
| f"{f_texture}", | |
| f"{f_action}{physics_booster}", | |
| f"{f_physics}", | |
| f"位于{f_env}" | |
| ] | |
| final_prompt = ",".join([p for p in parts if p and p.strip() != ","]) | |
| final_prompt = re.sub(r",+", ",", final_prompt) | |
| final_prompt = f"{final_prompt} --ar 16x9" | |
| return final_prompt | |
| def render_videos(script, topic, video_key, progress=gr.Progress()): | |
| if not script: yield "无脚本", None, ""; return | |
| session_dir = os.path.join("AutoSaved_Videos", f"{topic}_{int(time.time())}") | |
| os.makedirs(session_dir, exist_ok=True) | |
| # 🔥 核心修复:弃用 re.split,改用 finditer 进行安全切片,防止正则吞噬内容 | |
| raw_blocks = [] | |
| # 查找所有 "镜头 XX" 的起始位置 | |
| matches = list(re.finditer(r'(?:^|[\n\r])(?:[\*\#\>\-\s]*)(?:镜头|Shot)\s*(\d+)', script, re.IGNORECASE)) | |
| if not matches: | |
| logs = ["❌ 无法解析脚本:未找到任何 '镜头' 标记"] | |
| yield "\n".join(logs), None, ""; return | |
| for i in range(len(matches)): | |
| start = matches[i].start() | |
| # 如果不是最后一个,结束位置就是下一个的开始 | |
| if i < len(matches) - 1: | |
| end = matches[i+1].start() | |
| else: | |
| end = len(script) | |
| block = script[start:end].strip() | |
| # 简单过滤空块 | |
| if "Veo 提示词" in block or "01. 主体" in block: | |
| raw_blocks.append(block) | |
| logs = [f"🚀 全量并发渲染启动: 检测到 {len(raw_blocks)} 个独立镜头任务...", "✨ 已启用 [7大要素拼接] + [心理物理转译]"] | |
| yield "\n".join(logs), None, "" | |
| if len(raw_blocks) == 0: | |
| logs.append("❌ 未找到可渲染提示词块 (请确保脚本包含 7D 格式)"); yield "\n".join(logs), None, ""; return | |
| # 用于展示拼接后的英文提示词的文本 | |
| formatted_prompts_display = "" | |
| parsed_tasks = [] | |
| for i, block in enumerate(raw_blocks): | |
| # 1. 解析标题 (现在 block 包含标题行,所以能正确解析) | |
| title_match = re.search(r'(?:镜头|Shot)\s*(\d+)(?:/\d+)?\s*[\||]\s*(.*)', block) | |
| title = title_match.group(2).strip() if title_match else f"Untitled" | |
| # 文件名安全处理 | |
| safe_title = re.sub(r'[\\/*?:"<>|]', "", title).replace(" ", "_") | |
| # 2. 拼接 Prompt | |
| final_p = assemble_veo_prompt(block) | |
| # 兜底 | |
| if len(final_p) < 50: | |
| pattern = r"(?:Veo 提示词|Veo Prompt).*?[::]\s*(.*?)(?=\[状态连接\]|\[NEXT_LINK\]|镜头|Shot|$)" | |
| match = re.search(pattern, block, re.DOTALL | re.IGNORECASE) | |
| if match: | |
| final_p = f"BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实,{match.group(1).strip().replace(chr(10), ' ')} --ar 16x9" | |
| # 3. 格式化输出到展示窗口 | |
| shot_num = f"{i+1:03d}" | |
| formatted_entry = f"=== Shot {shot_num}/{len(raw_blocks)}: {title} ===\nveo Prompt (English): {final_p}\n\n" | |
| formatted_prompts_display += formatted_entry | |
| # 4. 准备文件名 (Shot_001_标题.mp4) | |
| fname = f"Shot_{shot_num}_{safe_title}" | |
| parsed_tasks.append((final_p, fname)) | |
| # 更新UI显示所有拼接后的Prompt | |
| yield "\n".join(logs), None, formatted_prompts_display | |
| files = [] | |
| with ThreadPoolExecutor(max_workers=len(parsed_tasks)) as executor: | |
| futures = {} | |
| for final_p, fname in parsed_tasks: | |
| # 🔥 核心修改:在调用 simple_veo_call 时增加错误捕捉 | |
| futures[executor.submit(simple_veo_call, final_p, fname, session_dir, video_key)] = fname | |
| done = 0 | |
| for f in as_completed(futures): | |
| res = f.result() | |
| done += 1 | |
| progress(done/len(parsed_tasks)) | |
| if res['ok']: | |
| logs.append(f"✅ {res['name']} 完成") | |
| files.append(res['path']) | |
| else: | |
| # 🔥 核心修改:显示具体错误原因 | |
| error_msg = res.get('error', '未知错误') | |
| logs.append(f"❌ {res['name']} 失败: {error_msg}") | |
| yield "\n".join(logs[-15:]), None, formatted_prompts_display | |
| if files: | |
| shutil.make_archive(session_dir, 'zip', session_dir) | |
| yield "\n".join(logs), f"{session_dir}.zip", formatted_prompts_display | |
| def simple_veo_call(prompt, name, folder, key): | |
| last_error = "" | |
| for attempt in range(5): | |
| try: | |
| url = f"{MERCHANT_BASE_URL}/v1/chat/completions" | |
| headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"} | |
| data = {"model": VEO_MODEL, "messages": [{"role": "user", "content": prompt}], "size": VIDEO_SIZE} | |
| r = requests.post(url, headers=headers, json=data, timeout=300) | |
| # 🔥 核心修改:增加状态码检查 | |
| if r.status_code != 200: | |
| raise Exception(f"API Error {r.status_code}: {r.text[:100]}") # 只截取前100个字符防止日志爆炸 | |
| content = r.json()['choices'][0]['message']['content'] | |
| v_url = re.search(r'(https?://[^\s)"]+)', content).group(1) | |
| path = os.path.join(folder, name + ".mp4") | |
| with open(path, "wb") as f: f.write(requests.get(v_url).content) | |
| return {"ok": True, "name": name, "path": path} | |
| except Exception as e: | |
| last_error = str(e) | |
| time.sleep(2 * (attempt + 1)) | |
| continue | |
| return {"ok": False, "name": name, "error": last_error} | |
| # =============================================== | |
| # --- UI 界面构造 --- | |
| # =============================================== | |
| with gr.Blocks(title="Veo 终极全能引擎 (全链路覆盖版)") as app: | |
| init_eng, init_v1, init_v2 = load_prompts() | |
| gr.Markdown("## 🏭 Veo 终极全能引擎 (全链路覆盖版:微观拆解 + 多机位重复 + 7D)") | |
| gr.Markdown("**提示**:此版本已强制工程师覆盖从【源头收获】到【包装物流】的全流程,并保留了所有历史铁律。") | |
| # 0. 全局配置 & 保存按钮 | |
| with gr.Row(variant="panel"): | |
| with gr.Column(scale=2): | |
| with gr.Row(): | |
| llm_key = gr.Textbox(label="🔑 LLM Key", value=DEFAULT_LLM_API_KEY, type="password") | |
| video_key = gr.Textbox(label="🎬 Video Key", value=DEFAULT_VIDEO_API_KEY, type="password") | |
| with gr.Column(scale=1): | |
| btn_save_config = gr.Button("💾 保存所有配置", variant="primary") | |
| save_status = gr.Textbox(label="状态", lines=1, interactive=False) | |
| topic_input = gr.Textbox(label="📦 产品名称", placeholder="例如:菠萝") | |
| # 1. 工程师 | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 🛠️ 步骤 1: 工程师 (全链路拆解)") | |
| with gr.Accordion("提示词设定 (可修改)", open=False): | |
| prompt_eng_input = gr.Textbox(value=init_eng, lines=10, label="工程师提示词", interactive=True) | |
| btn_eng = gr.Button("1. 生成海量素材日志", variant="secondary") | |
| with gr.Column(): | |
| arch_output = gr.Textbox(label="工艺架构输出", lines=10) | |
| gr.HTML("<hr>") | |
| # 2. 导演 1.0 | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 🎨 步骤 2: 导演 1.0 (最终剪辑与铺排)") | |
| count_slider = gr.Slider(1, 150, 120, step=1, label="镜头数量") | |
| with gr.Accordion("提示词设定 (可修改)", open=False): | |
| prompt_v1_input = gr.Textbox(value=init_v1, lines=15, label="导演1.0提示词", interactive=True) | |
| btn_v1 = gr.Button("2. 生成完整剧本", variant="primary") | |
| with gr.Column(): | |
| v1_output = gr.Textbox(label="V1 剧本输出", lines=15) | |
| gr.HTML("<hr>") | |
| # 3. 导演 2.0 | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 🧬 步骤 3: 导演 2.0 (状态进化)") | |
| with gr.Accordion("提示词设定 (可修改)", open=False): | |
| prompt_v2_input = gr.Textbox(value=init_v2, lines=10, label="导演2.0提示词", interactive=True) | |
| with gr.Row(): | |
| btn_v2 = gr.Button("3. 启动逻辑变异 (Batch=10)", variant="primary") | |
| btn_pause = gr.Button("⏸️ 暂停/继续", variant="secondary") | |
| log_output = gr.Textbox(label="演算日志", lines=15, elem_id="log_box", autoscroll=True) | |
| with gr.Column(): | |
| v2_output = gr.Textbox(label="最终脚本输出", lines=20, autoscroll=True) | |
| gr.HTML("<hr>") | |
| # 4. 渲染 | |
| with gr.Row(variant="panel"): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 🚀 步骤 4: 渲染") | |
| btn_render = gr.Button("4. 全量并发渲染", variant="stop") | |
| zip_output = gr.File(label="下载 ZIP") | |
| with gr.Column(scale=2): | |
| final_prompts_output = gr.Textbox(label="拼接后的提示词 (Assembled Prompts)", lines=20, interactive=False) | |
| app.load(refresh_config_on_load, inputs=None, outputs=[prompt_eng_input, prompt_v1_input, prompt_v2_input]) | |
| btn_save_config.click(save_prompts, [prompt_eng_input, prompt_v1_input, prompt_v2_input], [save_status]) | |
| btn_eng.click(execute_engineer, [topic_input, llm_key, prompt_eng_input], [arch_output]) | |
| btn_v1.click(execute_director_v1, [topic_input, arch_output, count_slider, llm_key, prompt_v1_input], [v1_output]) | |
| btn_pause.click(toggle_pause, [], [btn_pause]) | |
| btn_v2.click(execute_director_v2_loop, [v1_output, count_slider, llm_key, prompt_v2_input], [log_output, v2_output]) | |
| # 渲染按钮点击事件 | |
| btn_render.click(render_videos, [v2_output, topic_input, video_key], [log_output, zip_output, final_prompts_output]) | |
| app.launch() |