Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import subprocess
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
import datetime
|
| 7 |
+
import shutil
|
| 8 |
+
import json
|
| 9 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 10 |
+
|
| 11 |
+
# 1. 自动安装依赖
|
| 12 |
+
def ensure_dependencies():
|
| 13 |
+
try:
|
| 14 |
+
import gradio
|
| 15 |
+
import requests
|
| 16 |
+
except ImportError:
|
| 17 |
+
print("正在安装所需依赖: gradio, requests...")
|
| 18 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "gradio", "requests"])
|
| 19 |
+
|
| 20 |
+
ensure_dependencies()
|
| 21 |
+
|
| 22 |
+
import gradio as gr
|
| 23 |
+
import requests
|
| 24 |
+
|
| 25 |
+
# ================= 配置区域 =================
|
| 26 |
+
# 🔥 已更新:思考专用 Key (架构师 + 导演)
|
| 27 |
+
DEFAULT_LLM_API_KEY = "sk-Vhxjwm4XXu5fKrAtRNbRZGdPbocDZjG7B9UsSUjAdOQLyMUA"
|
| 28 |
+
# 🔥 已更新:视频生成专用 Key (Veo 渲染)
|
| 29 |
+
DEFAULT_VIDEO_API_KEY = "sk-crO3d0vpI1gKZRxwhgJ5TfyGKrvKAydummPmuPFmR5Obkbm8"
|
| 30 |
+
|
| 31 |
+
# 🔥 已更新:请求地址
|
| 32 |
+
MERCHANT_BASE_URL = "https://yunwu.ai"
|
| 33 |
+
VEO_MODEL = "veo_3_1-fast"
|
| 34 |
+
VIDEO_SIZE = "16x9"
|
| 35 |
+
TEXT_MODEL = "gemini-3-pro-preview-thinking"
|
| 36 |
+
CONFIG_FILE = "prompt_config.json"
|
| 37 |
+
|
| 38 |
+
# ================= 全局变量 =================
|
| 39 |
+
IS_PAUSED = False
|
| 40 |
+
BATCH_SIZE = 10
|
| 41 |
+
|
| 42 |
+
def toggle_pause():
|
| 43 |
+
global IS_PAUSED
|
| 44 |
+
IS_PAUSED = not IS_PAUSED
|
| 45 |
+
return "▶️ 恢复运行" if IS_PAUSED else "⏸️ 暂停任务"
|
| 46 |
+
|
| 47 |
+
# ===============================================
|
| 48 |
+
# --- 默认提示词备份 ---
|
| 49 |
+
# ===============================================
|
| 50 |
+
|
| 51 |
+
# 1. 工程师 (🔥保持不变:核心环节 50+ 微步骤,足以支撑 200+ 镜头)
|
| 52 |
+
FALLBACK_ENGINEER = """
|
| 53 |
+
你现在的身份是 **全链路工业冗余记录员 (The Full-Chain Industrial Logger)**。
|
| 54 |
+
你的任务是输出一份 **极度详细、甚至显得啰嗦** 的全流程生产日志。**不要替导演做减法,你的任务是提供海量素材!**
|
| 55 |
+
|
| 56 |
+
**⚠️ 核心指令 1:全链路覆盖与数量硬指标 (Mandatory Full Chain & Quota)**
|
| 57 |
+
你必须按顺序覆盖以下 **6 大阶段**,缺一不可:
|
| 58 |
+
1. **[源头与收获]** (Harvest): **(🔥视频必须以此开始!)** 田间/矿场/海洋的机械化采集 -> 装车。要体现原材料的原始状态。
|
| 59 |
+
2. **[进厂与净化]** (Intake & Cleaning): 卸货 -> 去石 -> 多级清洗 -> 分选。**(🔥死命令:此阶段微步骤总数不低于 50 个!要反复写清洗细节)**
|
| 60 |
+
3. **[预处理]** (Prep): 去皮/破碎/切割/研磨。**(🔥死命令:此阶段微步骤总数不低于 50 个!)**
|
| 61 |
+
4. **[核心质变]** (Transformation): 榨汁/油炸/冶炼/混合/反应。**(🔥死命令:此阶段微步骤总数不低于 50 个!要拆解每一秒的物理变化)**
|
| 62 |
+
5. **[精炼与成型]** (Refinement): 过滤/杀菌/干燥/冷却/压制。**(🔥死命令:此阶段微步骤总数不低于 50 个!)**
|
| 63 |
+
6. **[包装与物流]** (Packaging): 吹瓶/制罐 -> 灌装 -> 封口 -> 贴标 -> 喷码 -> 装箱 -> 码垛。
|
| 64 |
+
|
| 65 |
+
**⚠️ 核心指令 2:1:5 裂变法则 (The 1:5 Fission Law)**
|
| 66 |
+
不要概括!任何一个动作必须拆解为 **5个微步骤**。
|
| 67 |
+
* *Bad*: "清洗土豆。"
|
| 68 |
+
* *Good*:
|
| 69 |
+
* 步骤 2.1: 土豆落入气泡清洗池,激起水花。
|
| 70 |
+
* 步骤 2.2: 高压气泡翻滚,土豆表面泥土松动。
|
| 71 |
+
* 步骤 2.3: 滚筒毛刷摩擦土豆表皮,去除顽固污渍。
|
| 72 |
+
* 步骤 2.4: 喷淋装置用净水冲洗土豆表面。
|
| 73 |
+
* 步骤 2.5: 风刀吹干土豆表面的残余水珠。
|
| 74 |
+
|
| 75 |
+
**⚠️ 核心指令 3:物理细节描述**
|
| 76 |
+
在每个微步骤中,必须包含:**力学** (撞击/摩擦)、**热力学** (蒸汽/起泡)、**流体力学** (飞溅/漩涡)。
|
| 77 |
+
|
| 78 |
+
**输出格式 (请严格执行,全部中文):**
|
| 79 |
+
**阶段 [序号]:[阶段名称]**
|
| 80 |
+
* **微步骤 [x.1]**: [动作描述] + [物理反馈细节]
|
| 81 |
+
* **微步骤 [x.2]**: [动作描述] + [物理反馈细节]
|
| 82 |
+
... (请确保总步骤数足以支撑 30 分钟的视频,总计输出不低于 250 个微步骤)
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
# 2. 导演 1.0 (🔥已修改:预期调整为 200 镜头)
|
| 86 |
+
FALLBACK_DIRECTOR_V1 = """
|
| 87 |
+
你现在是 **AI 视频提示词架构师 (Veo Prompt Architect)** 兼 **高级工业纪录片总导演**。
|
| 88 |
+
你拥有一份由架构师提供的《工艺技术详情》作为**参考知识库**。
|
| 89 |
+
**你的核心任务**:基于这些技术步骤,构思一部 **超长篇幅 (200+ 镜头)、沉浸式、解压** 现代工业纪录片。
|
| 90 |
+
|
| 91 |
+
**⚠️ 核心指令 0:大片级叙事与节奏控制**
|
| 92 |
+
* **参考与重构**:架构师的输出只是**技术底稿**。你必须将其转化为**视觉脚本**。
|
| 93 |
+
* **黄金时间分配法则**:
|
| 94 |
+
1. **前奏 (Origin)**:原料采集 -> 进厂。**必须以此开场!** (Shot 1 必须是源头)
|
| 95 |
+
2. **高潮 (Core Processing)**:核心加工环节。**必须占据 80% 以上的篇幅**。这是观众最爱看的解压部分。
|
| 96 |
+
3. **尾声 (Packaging)**:最终封装。**必须严格控制在最后 20% 的镜头**。
|
| 97 |
+
* **预期管理**:假设总镜头数为 **200 个**,你必须在 **第 161 个镜头** 左右才能开始进入包装环节。在此之前,请尽情展示加工细节!
|
| 98 |
+
|
| 99 |
+
**⚠��� 核心指令 1:全域视觉锚点与命名协议 (Naming Protocol)** [🔥新增补丁]
|
| 100 |
+
* **⚠️ 绝对主体铁律**:所有镜头的 `**主体**` 必须是 **[产品]**。
|
| 101 |
+
* **🛑 强制名词复诵 (Mandatory Noun Repetition)**:
|
| 102 |
+
* **严禁代词**:绝对禁止使用 `It`, `They`, `The liquid`。
|
| 103 |
+
* **全名锁定**:必须复诵全名!如 `The ruby-red Pomegranate Juice` (红宝石般的石榴汁),`Wet Potato Strips` (湿润的土豆条)。
|
| 104 |
+
|
| 105 |
+
**📜 核心创作铁律合集 (The 18 Iron Laws)**
|
| 106 |
+
|
| 107 |
+
**第一类:视觉氛围与工厂设定**
|
| 108 |
+
1. **BBC 质感与风格**:所有镜头必须具备 **BBC 纪录片风格**。画面必须干净、电影级。**绝对禁止字幕**。
|
| 109 |
+
2. **密度分流法则 (The Law of Density)** [🔥核心修正]:
|
| 110 |
+
* **默认状态 (99%场景)**:生产/运输/清洗/堆叠 -> **必须是窒息密度**: `Thousands of (成千上万), A sea of (海洋般的), Filling the frame (填满画面)`。
|
| 111 |
+
* **特例状态 (1%场景)**:仅在抽样/显微镜/检测 -> 必须是 `Single (单个), Isolated (独立的)`。
|
| 112 |
+
3. **重力来源法则**:严禁魔幻般“从天而降”。所有倾泻必须写明源头设备(如:`pouring from a stainless steel hopper`)。
|
| 113 |
+
4. **高亮食欲光**:明亮通透,拒绝暗调。
|
| 114 |
+
5. **泥土与洁净冲突**:清洗前必须脏(带泥土),清洗后必须净(水珠光泽)。
|
| 115 |
+
|
| 116 |
+
**第二类:人机协同与安全**
|
| 117 |
+
6. **人物与人声铁律** [🔥绝对红线]:
|
| 118 |
+
* **视觉比例**:人机协同画面严格控制在 **总镜头的 30% 以内**。机器永远是主角。
|
| 119 |
+
* **听觉禁令**:**全片绝对禁止人声** (0% Human Voice)。严禁出现:说话声、采访声、旁白。
|
| 120 |
+
7. **专业形象**:`身穿白色无菌服的工人, 蓝色乳胶手套, 防尘网帽`。
|
| 121 |
+
8. **安全约束**:严禁血腥。描述必须是 `强力(Powerful), 极速(High-Speed)`。
|
| 122 |
+
|
| 123 |
+
**第三类:物理质感与形态**
|
| 124 |
+
9. **物料演变法则**:严禁穿越。捕捉形态改变瞬间。
|
| 125 |
+
10. **湿润与材质质感**:强调湿润感、光泽感。
|
| 126 |
+
11. **刚体碰撞法则**:
|
| 127 |
+
* 拒绝柔和。固体是撞击,不是流动。
|
| 128 |
+
* 关键词:`刚性碰撞, 互相挤压, 剧烈弹跳, 机械震动`。
|
| 129 |
+
|
| 130 |
+
**第四类:运镜与机械逻辑**
|
| 131 |
+
12. **开篇震撼法则**:Shot 1 必须是宏大的源头场景(农田/矿山)。严禁直接从工厂开始。
|
| 132 |
+
13. **高速通量法则**:传送带永远在高速运转,机械韵律即BGM。
|
| 133 |
+
14. **机械秩序法则**:机器动作无卡顿,展现毫秒级同步的数学美感。
|
| 134 |
+
15. **机械原理揭示**:`微距特写, 刀片切入点, 摩擦纹理, 物理接触面`。
|
| 135 |
+
16. **沉浸式运镜**:`第一人称视角 (POV), 传送带视角, 追踪镜头, Probe Lens (探针镜头)`。
|
| 136 |
+
|
| 137 |
+
**第五类:ASMR 听觉铁律**
|
| 138 |
+
17. **三维听觉纹理**:
|
| 139 |
+
* **强制拟声词**:必须包含如 `[咔哒 Click]`, `[滋滋 Zzzzt]`, `[咕嘟 Gurgle]`。
|
| 140 |
+
* **材质绑定**:金属=清脆;液体=粘稠/飞溅;重物=沉闷。
|
| 141 |
+
18. **全链路物理传输法则**:**绝不瞬移**。前段要有完整的 `收割->装车->运输->卸货` 链条。
|
| 142 |
+
|
| 143 |
+
**输出格式 (7D 旗舰版 A++ Optimized) - 必须输出 [用户指定数量] 个镜头!**
|
| 144 |
+
|
| 145 |
+
**⚠️ 强制锚点协议 (Mandatory Anchor Protocol)**:
|
| 146 |
+
为了确保解析准确,**每一个镜头**的上方必须严格包含锚点代码:`[[SHOT_ID: 序号]]`。
|
| 147 |
+
|
| 148 |
+
[[SHOT_ID: 序号]]
|
| 149 |
+
镜头 [序号] | [中文标题]
|
| 150 |
+
Veo 提示词 (格式 A++ Optimized):
|
| 151 |
+
01. 主体与密度:[🔥核心死令:必须显式包含[产品名]!禁止代词。必须是“无尽的[产品]阵列”、“[产品]的洪流”。如果产品未入场,则描述“油的海洋”或“蒸汽墙”。严禁单数。]
|
| 152 |
+
02. 材质与光影:[合并描述:表面纹理(粗糙/湿润/油亮) + 光照交互(金属反光/透光/高光)]
|
| 153 |
+
03. 动作演变:[🎬 关键!描述动作。如果是“重复性解压镜头”,请强调其持续性、循环性、无休止性]
|
| 154 |
+
04. 物理反馈:[合并描述:(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹]
|
| 155 |
+
05. 环境构建:[**收获阶段:自然场景(农田/矿山)**;进厂后:工厂场景(无菌室/不锈钢)。🔥严禁“纯黑虚空”!]
|
| 156 |
+
06. 镜头语言:[🔥必须多变!在此处轮换使用:1.上帝俯视 2.侧面平视 3.探针穿越 4.微距特写]
|
| 157 |
+
07. [ASMR] 核心音效:[🔥强制格式:[拟声词] + [材质形容词] + [动作描述] + [空间感]。]
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
# 3. 导演 2.0 (🔥保持不变:铁律执行引擎)
|
| 161 |
+
FALLBACK_DIRECTOR_V2 = """
|
| 162 |
+
你现在是 **导演 2.0 (状态进化与逻辑审计引擎)**。
|
| 163 |
+
你拥有 **绝对的修改权**,必须保证以下逻辑被 **100% 执行**。
|
| 164 |
+
|
| 165 |
+
**💀 导演 2 核心逻辑补丁 (Director 2 Logic Patch)**
|
| 166 |
+
**【以下两条指令具有最高优先级,覆盖一切旧逻辑】**
|
| 167 |
+
|
| 168 |
+
**⚠️ 补丁 1:🛑 强制名词复诵 (Mandatory Noun Repetition)**
|
| 169 |
+
* **指令详情**:在 **01. 主体与密度** 中,以及所有出现产品的地方,**绝对禁止**使用代词(It, They)或模糊指代(The liquid, The mixture, The strips, The object)。
|
| 170 |
+
* **执行标准**:
|
| 171 |
+
* **必须写全名**:比如是石榴汁,就必须写 `The ruby-red Pomegranate Juice` (红宝石般的石榴汁)。比如是土豆条,就必须写 `Wet Potato Strips` (湿润的土豆条)。
|
| 172 |
+
* **中间态锁定**:即使形态改变,也要带上名字。如 `Crushed Potato Mash` (粉碎的土豆泥)。
|
| 173 |
+
* **例外**:只有在 `Empty Machine Preparation` (完全空机) 阶段,才允许不写产品名。一旦产品入画,必须复诵全名!
|
| 174 |
+
|
| 175 |
+
**⚠️ 补丁 2:🌊 99% 密度压制 (99% Density Dominance)**
|
| 176 |
+
* **指令详情**:除非是 **[实验室]**、**[显微镜]** 或 **[单体破坏测试]** 场景(这属于 1% 的例外),否则 **99% 的场景**(运输、清洗、切割、灌装、混合)必须展示 **“成千上万”** 的产品阵列。
|
| 177 |
+
* **执行标准**:
|
| 178 |
+
* **必须使用**:`Thousands of`, `A sea of`, `An endless flood of`, `Filling the entire frame`。
|
| 179 |
+
* **拒绝留白**:传送带上不能只有几根土豆条,必须是**堆积如山的土豆条**。
|
| 180 |
+
|
| 181 |
+
**💀 语义清洗与进化法则 (常规审计)**
|
| 182 |
+
|
| 183 |
+
**0. ⚠️ 主权归还审计 (Subject Restoration Audit)**
|
| 184 |
+
* **检测**:检查 `主体` 字段。
|
| 185 |
+
* **规则**:如果 `主体` 描述的是 **机器、工具、容器、环境** 或 **工人/人类**,这是**严重错误**。
|
| 186 |
+
* **修正执行**:将 `主体` **强制重写** 为 **“正在经历该工序的 [产品名]”** (需遵守补丁1)。
|
| 187 |
+
|
| 188 |
+
**1. ⚠️ 标题保护与进化协议**
|
| 189 |
+
* **保留原标题**:你必须保留导演 1.0 给出的原始中文标题。
|
| 190 |
+
* **进化标注**:仅当发生物理质变时追加 `[原标题] + [进化:变化描述]`。
|
| 191 |
+
|
| 192 |
+
**2. 🛡️ 绝对安全协议**
|
| 193 |
+
* **严禁词汇**:绝对禁止 "死刑"、"暴力"、"血腥"。
|
| 194 |
+
* **替换策略**:使用 "精密裁切" (Precision Cutting)、"高能冲击" (High Energy Impact)。
|
| 195 |
+
|
| 196 |
+
**3. 🎵 听觉审计与润色**
|
| 197 |
+
* **拒绝模糊**:将 "机器声" 改写为 **[嗡嗡 Hum]** 等 ASMR 描述。
|
| 198 |
+
* **清洗负面听觉**:删除 "刺耳"、"尖叫"、"噪音"。
|
| 199 |
+
|
| 200 |
+
**4. ⚠️ 绝对物种锁定**
|
| 201 |
+
* **纠错**:出现非 [产品] 的名词(如幻觉成甜菜),强制替换回 [产品]。
|
| 202 |
+
|
| 203 |
+
**5. ⚠️ 容器一致性审计**
|
| 204 |
+
* **规则**:一旦确定包装材质(如玻璃瓶),后续严禁变更。
|
| 205 |
+
|
| 206 |
+
**6. ⚠️ 客观密度与重力源头审计 (Logic Audit)**
|
| 207 |
+
* **密度审计**:执行补丁2 (99%密度)。
|
| 208 |
+
* **重力源审计**:如果描述了“倾泻/掉落”,强制加上 `从不锈钢料斗倾泻`。
|
| 209 |
+
|
| 210 |
+
**7. 极度冗余的主体描述**
|
| 211 |
+
* **规则**:每个镜头的主体必须是**独立、完整、详细**的描述。
|
| 212 |
+
* **执行**:堆叠形容词。不要只写 "切片",要写 "完美均匀的圆形[产品]切片,闪烁着汁液"。
|
| 213 |
+
|
| 214 |
+
**8. 形态进化与强制替换**:
|
| 215 |
+
* **当 [切片] 发生后**: 删除 "完整",替换为 "切片/横截面"。
|
| 216 |
+
* **当 [粉碎] 发生后**: 删除 "块状",替换为 "碎屑/粉尘"。
|
| 217 |
+
|
| 218 |
+
**9. 清洁度进化**:
|
| 219 |
+
* **当 [清洗] 发生后**:强制删除 "脏/泥泞",强制添加 "干净,晶莹剔透"。
|
| 220 |
+
|
| 221 |
+
**10. 🌊 主体豁免与介质置换协议**:
|
| 222 |
+
* **死锁解决**:当产品尚未入场时,将“工业介质”(油/水/蒸汽)升级为高密度主体(如“金色的油海”)。
|
| 223 |
+
|
| 224 |
+
**输出格式 (必须完全一致,包含19个核心维度):**
|
| 225 |
+
|
| 226 |
+
**⚠️ 锚点保留协议 (Anchor Preservation)**:
|
| 227 |
+
必须保留输入中的 `[[SHOT_ID: 序号]]` 标记,放在每个镜头的最开头。严禁删除或修改此ID。
|
| 228 |
+
|
| 229 |
+
[[SHOT_ID: 序号]]
|
| 230 |
+
镜头 [序号]/[总数] | [中文标题] (+ [进化标注] 如有)
|
| 231 |
+
Veo 提示词 (格式 A++ Optimized):
|
| 232 |
+
01. 主体与密度:[🔥核心补丁:拒绝代词,强制复诵[产品全名]。99%场景必须是“无尽的[产品]阵列”、“[产品]的洪流”。]
|
| 233 |
+
02. 材质与光影:[合并描述:表面纹理(粗糙/湿润) + 光照交互(金属反光/高光/次表面散射)]
|
| 234 |
+
03. 动作演变:[🎬 关键!用一句话描述 T=0s(入画) -> T=4s(高潮/撞击) -> T=8s(出画) 的完整过程。动作必须连贯]
|
| 235 |
+
04. 物理反馈:[合并描述:包含原来的(质量/碰撞/流体/粒子)。如:沉重的撞击感、汁液飞溅、粉尘在空气中漂浮、刚体反弹]
|
| 236 |
+
05. 环境构建:[**收获阶段:自然场景(农田/矿山)**;进厂后:工厂场景(无菌室/不锈钢)。🔥严禁“纯黑虚空”!]
|
| 237 |
+
06. 镜头语言:[合并描述:机位(微距/探针) + 运镜(跟随/推拉) + 光学参数(广角/眩光)]
|
| 238 |
+
07. [ASMR] 核心音效:[🔥强制硬核:[拟声词] + [材质形容词] + [动作描述]。例如:**[滋滋 Zzzzt]** 锋利刀片切开多汁果肉的**湿润撕裂声**。]
|
| 239 |
+
|
| 240 |
+
[状态连接]: [用于下一个镜头的干净、优化的形容词列表。例如 "干净的,去皮的,切片的"]
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
# ===============================================
|
| 244 |
+
# --- 提示词管理函数 ---
|
| 245 |
+
# ===============================================
|
| 246 |
+
def load_prompts():
|
| 247 |
+
"""从 JSON 加载提示词,增加兜底逻辑"""
|
| 248 |
+
p_eng, p_v1, p_v2 = FALLBACK_ENGINEER, FALLBACK_DIRECTOR_V1, FALLBACK_DIRECTOR_V2 # 默认值
|
| 249 |
+
|
| 250 |
+
if os.path.exists(CONFIG_FILE):
|
| 251 |
+
try:
|
| 252 |
+
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
| 253 |
+
data = json.load(f)
|
| 254 |
+
# 使用 get 并提供 fallback,防止 json 里的 key 缺失
|
| 255 |
+
p_eng = data.get("engineer", FALLBACK_ENGINEER)
|
| 256 |
+
p_v1 = data.get("director1", FALLBACK_DIRECTOR_V1)
|
| 257 |
+
p_v2 = data.get("director2", FALLBACK_DIRECTOR_V2)
|
| 258 |
+
except Exception as e:
|
| 259 |
+
pass
|
| 260 |
+
|
| 261 |
+
return p_eng, p_v1, p_v2
|
| 262 |
+
|
| 263 |
+
def save_prompts(p_eng, p_v1, p_v2):
|
| 264 |
+
"""保存提示词到 JSON"""
|
| 265 |
+
data = {
|
| 266 |
+
"engineer": p_eng,
|
| 267 |
+
"director1": p_v1,
|
| 268 |
+
"director2": p_v2,
|
| 269 |
+
"last_updated": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 270 |
+
}
|
| 271 |
+
try:
|
| 272 |
+
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
| 273 |
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
| 274 |
+
return f"✅ 配置已保存 ({data['last_updated']})"
|
| 275 |
+
except Exception as e:
|
| 276 |
+
return f"❌ 保存失败: {e}"
|
| 277 |
+
|
| 278 |
+
def refresh_config_on_load():
|
| 279 |
+
"""刷新回调,确保返回正确的值"""
|
| 280 |
+
return load_prompts()
|
| 281 |
+
|
| 282 |
+
# ===============================================
|
| 283 |
+
# --- 核心逻辑 (API 调用) ---
|
| 284 |
+
# ===============================================
|
| 285 |
+
|
| 286 |
+
def get_timestamp():
|
| 287 |
+
return datetime.datetime.now().strftime("%H:%M:%S")
|
| 288 |
+
|
| 289 |
+
def log_msg(logs, msg, level="INFO"):
|
| 290 |
+
icon = {"INFO": "ℹ️", "PROCESS": "⚙️", "SUCCESS": "✅", "WARN": "⚠️", "STATE": "🧬", "WAIT": "⏳", "NET": "📡", "RETRY": "🔄", "ERROR": "❌", "PAUSE": "⏸️", "AUDIT": "👁️"}.get(level, "")
|
| 291 |
+
entry = f"[{get_timestamp()}] {icon} {msg}"
|
| 292 |
+
logs.append(entry)
|
| 293 |
+
return "\n".join(logs)
|
| 294 |
+
|
| 295 |
+
# 1. 工程师
|
| 296 |
+
def execute_engineer(topic, api_key, system_prompt):
|
| 297 |
+
if not api_key: yield "❌ 未提供 API Key"; return
|
| 298 |
+
yield "⏳ [工程师] 正在构建现代工厂工艺流程 (全链路+裂变模式)..."
|
| 299 |
+
|
| 300 |
+
url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
|
| 301 |
+
headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
|
| 302 |
+
data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"产品: {topic}"}]}
|
| 303 |
+
|
| 304 |
+
for attempt in range(5):
|
| 305 |
+
try:
|
| 306 |
+
yield f"📡 [工程师] 正向 LLM 发送请求 (尝试 {attempt+1}/5)..."
|
| 307 |
+
res = requests.post(url, headers=headers, json=data, timeout=120)
|
| 308 |
+
if res.status_code != 200: raise Exception(res.text)
|
| 309 |
+
yield res.json()['choices'][0]['message']['content']
|
| 310 |
+
return
|
| 311 |
+
except Exception as e:
|
| 312 |
+
if attempt == 4: yield f"❌ 错误 (最终失败): {e}"
|
| 313 |
+
else: yield f"⚠️ 请求失败: {e},正在重试..."
|
| 314 |
+
time.sleep(2)
|
| 315 |
+
|
| 316 |
+
# 2. 导演 1.0 (分批接力 + 全量历史)
|
| 317 |
+
def execute_director_v1(topic, architecture, count, api_key, system_prompt):
|
| 318 |
+
if not architecture: yield "❌ 请先生成工艺架构"; return
|
| 319 |
+
|
| 320 |
+
V1_BATCH_SIZE = 10
|
| 321 |
+
|
| 322 |
+
# 🔥 核心修正:使用列表来存储所有结果,防止覆盖
|
| 323 |
+
v1_all_results = []
|
| 324 |
+
|
| 325 |
+
packaging_start_shot = int(count * 0.8) + 1
|
| 326 |
+
yield f"⏳ [导演 1.0] 正在构思 {count} 个镜头的解压沉浸剧本..."
|
| 327 |
+
yield f"📌 [时间线控制] Shot 1=源头, Shot {packaging_start_shot}=包装开始 (97% 处)"
|
| 328 |
+
|
| 329 |
+
for batch_start in range(1, count + 1, V1_BATCH_SIZE):
|
| 330 |
+
batch_end = min(batch_start + V1_BATCH_SIZE - 1, count)
|
| 331 |
+
current_batch_count = batch_end - batch_start + 1
|
| 332 |
+
|
| 333 |
+
yield f"📡 [导演 1.0] 正在生成第 {batch_start} - {batch_end} 个镜头 (Batch Processing)..."
|
| 334 |
+
|
| 335 |
+
# 构建当前全量剧本用于上下文 (只取列表中的 string)
|
| 336 |
+
current_full_script = "\n".join(v1_all_results)
|
| 337 |
+
|
| 338 |
+
context_instruction = ""
|
| 339 |
+
if current_full_script:
|
| 340 |
+
context_instruction = f"""
|
| 341 |
+
**⚠️ 前序镜头全量历史 (Context History - DO NOT REPEAT)**:
|
| 342 |
+
{current_full_script}
|
| 343 |
+
"""
|
| 344 |
+
|
| 345 |
+
user_content = f"""
|
| 346 |
+
产品: {topic}
|
| 347 |
+
工程协议:
|
| 348 |
+
{architecture}
|
| 349 |
+
|
| 350 |
+
{context_instruction}
|
| 351 |
+
|
| 352 |
+
任务: 生成第 {batch_start} 到 {batch_end} 个镜头 (共 {current_batch_count} 个)。
|
| 353 |
+
|
| 354 |
+
**⚠️ 时间线与包装死令**:
|
| 355 |
+
1. **起始点**:如果包含 Shot 1,**必须是【原料采集/源头】**。
|
| 356 |
+
2. **包装禁令**:**在第 {packaging_start_shot} 个镜头之前,绝对禁止进入包装环节!**
|
| 357 |
+
|
| 358 |
+
**⚠️ 强制格式锚点 (Format Anchor)**:
|
| 359 |
+
必须严格执行提示词中的输出格式,每个镜头前必须加上 `[[SHOT_ID: 序号]]`。
|
| 360 |
+
|
| 361 |
+
**警告:必须真实生成 {current_batch_count} 个独立的提示词块!**
|
| 362 |
+
输出: 结构化镜头列表,全部使用中文。
|
| 363 |
+
"""
|
| 364 |
+
|
| 365 |
+
url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
|
| 366 |
+
headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
|
| 367 |
+
data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}]}
|
| 368 |
+
|
| 369 |
+
success = False
|
| 370 |
+
for attempt in range(5):
|
| 371 |
+
try:
|
| 372 |
+
res = requests.post(url, headers=headers, json=data, timeout=300)
|
| 373 |
+
if res.status_code != 200: raise Exception(f"API Error: {res.text}")
|
| 374 |
+
|
| 375 |
+
new_content = res.json()['choices'][0]['message']['content']
|
| 376 |
+
|
| 377 |
+
# 🔥 增加空内容防御
|
| 378 |
+
if not new_content or len(new_content) < 50:
|
| 379 |
+
raise Exception("返回内容为空或过短,可能被安全拦截")
|
| 380 |
+
|
| 381 |
+
# 🔥 存入列表
|
| 382 |
+
v1_all_results.append(new_content)
|
| 383 |
+
|
| 384 |
+
# 🔥 强制拼合并返回
|
| 385 |
+
yield "\n\n".join(v1_all_results)
|
| 386 |
+
success = True
|
| 387 |
+
break
|
| 388 |
+
except Exception as e:
|
| 389 |
+
yield f"⚠️ Batch 失败 (尝试 {attempt+1}/5): {e}"
|
| 390 |
+
time.sleep(2)
|
| 391 |
+
|
| 392 |
+
if not success:
|
| 393 |
+
yield "❌ 致命错误:多次重试失败,流程终止。"
|
| 394 |
+
return
|
| 395 |
+
|
| 396 |
+
# 3. 导演 2.0 API (带 prev_context_shots)
|
| 397 |
+
def call_director_v2_batch_api(batch_plans, start_idx, total, prev_state, prev_context_shots, api_key, system_prompt):
|
| 398 |
+
|
| 399 |
+
joined_plans = "\n\n".join(batch_plans)
|
| 400 |
+
packaging_start_shot = int(total * 0.8) + 1
|
| 401 |
+
|
| 402 |
+
# 🔥 核心修改:回溯指令 (首批次豁免 + N-1 自动逻辑)
|
| 403 |
+
context_instruction = ""
|
| 404 |
+
if prev_context_shots and start_idx > 1:
|
| 405 |
+
# 如果是 Shot 7,list slice [-10:] 会返回 Shot 1-6,符合 "N-1" 要求
|
| 406 |
+
context_instruction = f"""
|
| 407 |
+
**👁️ 扩展视觉记忆 (Extended Visual Memory - Previous Shots)**:
|
| 408 |
+
以下是该镜头之前 **最多10个镜头** 的记录 (用于确保物理状态连续)。
|
| 409 |
+
---
|
| 410 |
+
{prev_context_shots}
|
| 411 |
+
---
|
| 412 |
+
**强制约束**: Shot {start_idx} 必须继承[产品]的真实物理状态,并延续视觉风格。
|
| 413 |
+
"""
|
| 414 |
+
else:
|
| 415 |
+
# 🔥 Shot 1 不回溯
|
| 416 |
+
context_instruction = "**【初始批次】:这是视频的开头(Shot 1),无需回溯历史。请建立基准物理状态(通常为原料采集)。**"
|
| 417 |
+
|
| 418 |
+
user_content = f"""
|
| 419 |
+
**批量任务**: 正在处理第 {start_idx} 到第 {start_idx + len(batch_plans) - 1} 个镜头 (共 {len(batch_plans)} 个)。
|
| 420 |
+
|
| 421 |
+
**⚠️ 包装时间线检查**:
|
| 422 |
+
- 包装允许开始点: Shot {packaging_start_shot}
|
| 423 |
+
|
| 424 |
+
🧬 **初始状态**: "{prev_state}"
|
| 425 |
+
|
| 426 |
+
{context_instruction}
|
| 427 |
+
|
| 428 |
+
📜 **导演 1 原始计划组**:
|
| 429 |
+
{joined_plans}
|
| 430 |
+
|
| 431 |
+
**你的任务**:
|
| 432 |
+
请**依次**处理这 {len(batch_plans)} 个镜头。
|
| 433 |
+
**输出格式要求**:
|
| 434 |
+
必须输出 {len(batch_plans)} 个完整的 7D 格式块,每个都要带 `[[SHOT_ID]]` 锚点。
|
| 435 |
+
在所有镜头输出完毕后,**必须**输出一行最终状态:
|
| 436 |
+
`[BATCH_END_STATE]: <这里写最后一个镜头完成后的物理状态>`
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
|
| 440 |
+
headers = {"Authorization": f"Bearer {api_key.strip()}", "Content-Type": "application/json"}
|
| 441 |
+
data = {"model": TEXT_MODEL, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}], "temperature": 0.3}
|
| 442 |
+
|
| 443 |
+
try:
|
| 444 |
+
res = requests.post(url, headers=headers, json=data, timeout=300)
|
| 445 |
+
content = res.json()['choices'][0]['message']['content']
|
| 446 |
+
|
| 447 |
+
final_state = prev_state
|
| 448 |
+
if "[BATCH_END_STATE]" in content:
|
| 449 |
+
parts = content.split("[BATCH_END_STATE]")
|
| 450 |
+
content_body = parts[0].strip()
|
| 451 |
+
state_raw = parts[1].strip()
|
| 452 |
+
if state_raw.startswith(":") or state_raw.startswith(":"):
|
| 453 |
+
final_state = state_raw[1:].strip()
|
| 454 |
+
else:
|
| 455 |
+
content_body = content
|
| 456 |
+
|
| 457 |
+
return content_body, final_state
|
| 458 |
+
|
| 459 |
+
except Exception as e: return None, str(e)
|
| 460 |
+
|
| 461 |
+
# 3. 导演 2.0 循环 (结果缓存 + 10段回溯)
|
| 462 |
+
def execute_director_v2_loop(v1_text, count, api_key, system_prompt):
|
| 463 |
+
if not v1_text: yield "❌ 请先生成导演 1.0 的视觉方案", ""; return
|
| 464 |
+
global IS_PAUSED
|
| 465 |
+
IS_PAUSED = False
|
| 466 |
+
logs = []
|
| 467 |
+
|
| 468 |
+
# 🔥 核心修正:UI 持久化容器
|
| 469 |
+
# 只要生成了,就塞进这里,永远不丢
|
| 470 |
+
all_generated_batches = []
|
| 471 |
+
|
| 472 |
+
full_results_history = []
|
| 473 |
+
|
| 474 |
+
raw_text = v1_text.strip()
|
| 475 |
+
|
| 476 |
+
# 🔥 核心修复:双重锚点解析 (兼容性 MAX)
|
| 477 |
+
header_pattern = re.compile(r'(?:\[\[\s*SHOT_ID\s*[::]\s*(\d+)\s*\]\]|###\s*镜头\s*(\d+))', re.IGNORECASE)
|
| 478 |
+
matches = list(header_pattern.finditer(raw_text))
|
| 479 |
+
|
| 480 |
+
if not matches:
|
| 481 |
+
yield log_msg(logs, f"❌ 严重错误:未找到锚点。请确认导演 1.0 输出包含 [[SHOT_ID]] 或 ### 镜头。", "ERROR"), ""
|
| 482 |
+
return
|
| 483 |
+
|
| 484 |
+
visual_plans = []
|
| 485 |
+
seen_ids = set() # 去重,防止同一镜头被重复解析
|
| 486 |
+
|
| 487 |
+
for i in range(len(matches)):
|
| 488 |
+
start = matches[i].start()
|
| 489 |
+
end = matches[i+1].start() if i+1 < len(matches) else len(raw_text)
|
| 490 |
+
block = raw_text[start:end].strip()
|
| 491 |
+
|
| 492 |
+
try:
|
| 493 |
+
val1 = matches[i].group(1)
|
| 494 |
+
val2 = matches[i].group(2)
|
| 495 |
+
shot_id = int(val1) if val1 else int(val2)
|
| 496 |
+
except:
|
| 497 |
+
shot_id = i + 1
|
| 498 |
+
|
| 499 |
+
if shot_id not in seen_ids and len(block) > 5:
|
| 500 |
+
visual_plans.append({"id": shot_id, "content": block})
|
| 501 |
+
seen_ids.add(shot_id)
|
| 502 |
+
|
| 503 |
+
# 排序以防万一
|
| 504 |
+
visual_plans.sort(key=lambda x: x['id'])
|
| 505 |
+
|
| 506 |
+
yield log_msg(logs, f"🚀 导演 2.0 启动:状态进化引擎 (7D 旗舰版 | Batch={BATCH_SIZE})", "SUCCESS"), ""
|
| 507 |
+
yield log_msg(logs, f"📊 成功解析 {len(visual_plans)} 个镜头 (首镜ID: {visual_plans[0]['id']})", "INFO"), ""
|
| 508 |
+
|
| 509 |
+
prev_state = "原材料,脏的,完整的,未处理的 (自然状态)"
|
| 510 |
+
yield log_msg(logs, f"🧬 [初始状态] {prev_state}", "STATE"), ""
|
| 511 |
+
|
| 512 |
+
total_shots = len(visual_plans)
|
| 513 |
+
|
| 514 |
+
for i in range(0, total_shots, BATCH_SIZE):
|
| 515 |
+
batch_items = visual_plans[i : i + BATCH_SIZE]
|
| 516 |
+
batch_texts = [item['content'] for item in batch_items]
|
| 517 |
+
|
| 518 |
+
start_real_id = batch_items[0]['id']
|
| 519 |
+
end_real_id = batch_items[-1]['id']
|
| 520 |
+
|
| 521 |
+
# 强制显式返回一次全量数据
|
| 522 |
+
full_text_snapshot = "\n\n".join(all_generated_batches)
|
| 523 |
+
if IS_PAUSED:
|
| 524 |
+
yield log_msg(logs, f"⏸️ 已暂停...", "PAUSE"), full_text_snapshot
|
| 525 |
+
while IS_PAUSED: time.sleep(1)
|
| 526 |
+
yield log_msg(logs, f"▶️ 恢复", "PROCESS"), full_text_snapshot
|
| 527 |
+
|
| 528 |
+
# 回溯逻辑
|
| 529 |
+
prev_context_shots = ""
|
| 530 |
+
if full_results_history:
|
| 531 |
+
last_10 = full_results_history[-10:]
|
| 532 |
+
prev_context_shots = "\n\n".join(last_10)
|
| 533 |
+
yield log_msg(logs, f"👁️ [视觉审阅] 回溯 Shot {max(1, start_real_id-10)} - {start_real_id-1}...", "AUDIT"), full_text_snapshot
|
| 534 |
+
else:
|
| 535 |
+
yield log_msg(logs, f"👁️ [视觉审阅] 初始批次 (Shot 1),无前序记忆", "INFO"), full_text_snapshot
|
| 536 |
+
|
| 537 |
+
yield log_msg(logs, f"⚙️ [批量处理] 镜头 {start_real_id} - {end_real_id} ({len(batch_items)}个)...", "PROCESS"), full_text_snapshot
|
| 538 |
+
yield log_msg(logs, f"🧬 [思维链继承] 起始状态: {prev_state}", "STATE"), full_text_snapshot
|
| 539 |
+
|
| 540 |
+
success = False
|
| 541 |
+
for attempt in range(5):
|
| 542 |
+
if IS_PAUSED:
|
| 543 |
+
yield log_msg(logs, "⏸️ 暂停中...", "PAUSE"), full_text_snapshot
|
| 544 |
+
while IS_PAUSED: time.sleep(1)
|
| 545 |
+
try:
|
| 546 |
+
if attempt > 0: yield log_msg(logs, f" 🔄 重试 ({attempt}/5)...", "RETRY"), full_text_snapshot
|
| 547 |
+
|
| 548 |
+
# 🔥 传入真实的 start_real_id
|
| 549 |
+
batch_result, next_state = call_director_v2_batch_api(
|
| 550 |
+
batch_texts, start_real_id, count, prev_state, prev_context_shots, api_key, system_prompt
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
if batch_result:
|
| 554 |
+
success = True
|
| 555 |
+
yield log_msg(logs, f" 📥 Batch 完成", "SUCCESS"), full_text_snapshot
|
| 556 |
+
|
| 557 |
+
# 🔥 核心修正:追加到全局列表
|
| 558 |
+
all_generated_batches.append(batch_result)
|
| 559 |
+
full_text_snapshot = "\n\n".join(all_generated_batches) # 更新快照
|
| 560 |
+
|
| 561 |
+
split_regex = r'(?:\[\[\s*SHOT_ID\s*[::]\s*\d+\s*\]\]|###\s*镜头\s*\d+)'
|
| 562 |
+
current_blocks = re.split(split_regex, batch_result)
|
| 563 |
+
current_blocks = [b.strip() for b in current_blocks if len(b) > 20]
|
| 564 |
+
full_results_history.extend(current_blocks)
|
| 565 |
+
|
| 566 |
+
if prev_state != next_state:
|
| 567 |
+
yield log_msg(logs, f" 🌊 [进化] ... -> {next_state}", "STATE"), full_text_snapshot
|
| 568 |
+
else:
|
| 569 |
+
yield log_msg(logs, f" 🧬 [保持] {next_state}", "STATE"), full_text_snapshot
|
| 570 |
+
|
| 571 |
+
prev_state = next_state
|
| 572 |
+
break
|
| 573 |
+
time.sleep(1.5 * (attempt + 1))
|
| 574 |
+
except Exception as e:
|
| 575 |
+
yield log_msg(logs, f" ⚠️ Batch 错误: {e}", "WARN"), full_text_snapshot
|
| 576 |
+
time.sleep(1.5 * (attempt + 1))
|
| 577 |
+
|
| 578 |
+
if not success:
|
| 579 |
+
yield log_msg(logs, f" ❌ Batch 失败 (跳过)", "ERROR"), full_text_snapshot
|
| 580 |
+
|
| 581 |
+
# 🔥 每一批次完成后,必须显式返回全量文本
|
| 582 |
+
yield "\n".join(logs[-20:]), full_text_snapshot
|
| 583 |
+
time.sleep(0.2)
|
| 584 |
+
|
| 585 |
+
yield log_msg(logs, "🎉 全片逻辑闭环完成", "SUCCESS"), "\n\n".join(all_generated_batches)
|
| 586 |
+
|
| 587 |
+
# ===============================================
|
| 588 |
+
# --- 渲染逻辑升级 (7要素拼接 + 格式化展示) ---
|
| 589 |
+
# ===============================================
|
| 590 |
+
|
| 591 |
+
def inject_satisfaction_physics(satisfaction_text):
|
| 592 |
+
booster = ""
|
| 593 |
+
if not satisfaction_text: return booster
|
| 594 |
+
if any(k in satisfaction_text for k in ["填满", "堆叠", "丰盛", "空虚", "数量"]):
|
| 595 |
+
booster += ",极高密度填充,画面无任何空隙,形成视觉实体墙,"
|
| 596 |
+
if any(k in satisfaction_text for k in ["秩序", "同步", "整齐", "治愈", "强迫症"]):
|
| 597 |
+
booster += ",完美几何对齐,毫秒级机械同步运动,无任何误差,"
|
| 598 |
+
if any(k in satisfaction_text for k in ["剥离", "去除", "刮", "磨", "分离", "炸开"]):
|
| 599 |
+
booster += ",表面污垢被物理暴力剥离,瞬间露出完美内部,"
|
| 600 |
+
if any(k in satisfaction_text for k in ["净化", "洁净", "丝滑", "顺滑", "粘稠"]):
|
| 601 |
+
booster += ",极度洁净的表面,丝滑的高光反射,无暇质感,"
|
| 602 |
+
return booster
|
| 603 |
+
|
| 604 |
+
def assemble_veo_prompt(raw_block):
|
| 605 |
+
"""提取+拼接+转译 (适配新的 7D 格式)"""
|
| 606 |
+
def extract(key_pattern, text):
|
| 607 |
+
match = re.search(key_pattern, text)
|
| 608 |
+
return match.group(1).strip().rstrip("。") if match else ""
|
| 609 |
+
|
| 610 |
+
f_subject = extract(r"01\. 主体与密度:(.*?)\n", raw_block)
|
| 611 |
+
f_texture = extract(r"02\. 材质与光影:(.*?)\n", raw_block)
|
| 612 |
+
f_action = extract(r"03\. 动作演变:(.*?)\n", raw_block)
|
| 613 |
+
f_physics = extract(r"04\. 物理反馈:(.*?)\n", raw_block)
|
| 614 |
+
f_env = extract(r"05\. 环境构建:(.*?)\n", raw_block)
|
| 615 |
+
f_camera = extract(r"06\. 镜头语言:(.*?)\n", raw_block)
|
| 616 |
+
|
| 617 |
+
physics_booster = inject_satisfaction_physics(f_physics)
|
| 618 |
+
|
| 619 |
+
parts = [
|
| 620 |
+
"BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实电影质感",
|
| 621 |
+
f"{f_camera}",
|
| 622 |
+
f"{f_subject}",
|
| 623 |
+
f"{f_texture}",
|
| 624 |
+
f"{f_action}{physics_booster}",
|
| 625 |
+
f"{f_physics}",
|
| 626 |
+
f"位于{f_env}"
|
| 627 |
+
]
|
| 628 |
+
|
| 629 |
+
final_prompt = ",".join([p for p in parts if p and p.strip() != ","])
|
| 630 |
+
final_prompt = re.sub(r",+", ",", final_prompt)
|
| 631 |
+
final_prompt = f"{final_prompt} --ar 16x9"
|
| 632 |
+
return final_prompt
|
| 633 |
+
|
| 634 |
+
def render_videos(script, topic, video_key, progress=gr.Progress()):
|
| 635 |
+
if not script: yield "无脚本", None, ""; return
|
| 636 |
+
session_dir = os.path.join("AutoSaved_Videos", f"{topic}_{int(time.time())}")
|
| 637 |
+
os.makedirs(session_dir, exist_ok=True)
|
| 638 |
+
|
| 639 |
+
# 🔥 核心修复:兼容双重锚点
|
| 640 |
+
raw_blocks = []
|
| 641 |
+
matches = list(re.finditer(r'(?:\[\[\s*SHOT_ID\s*[::]\s*(\d+)\s*\]\]|###\s*镜头\s*(\d+))', script, re.IGNORECASE))
|
| 642 |
+
|
| 643 |
+
if not matches:
|
| 644 |
+
logs = ["❌ 无法解析脚本:未找到任何 '[[SHOT_ID]]' 标记"]
|
| 645 |
+
yield "\n".join(logs), None, ""; return
|
| 646 |
+
|
| 647 |
+
for i in range(len(matches)):
|
| 648 |
+
start = matches[i].start()
|
| 649 |
+
if i < len(matches) - 1:
|
| 650 |
+
end = matches[i+1].start()
|
| 651 |
+
else:
|
| 652 |
+
end = len(script)
|
| 653 |
+
|
| 654 |
+
block = script[start:end].strip()
|
| 655 |
+
if "Veo 提示词" in block or "01. 主体" in block:
|
| 656 |
+
raw_blocks.append(block)
|
| 657 |
+
|
| 658 |
+
logs = [f"🚀 全量并发渲染启动: 检测到 {len(raw_blocks)} 个独立镜头任务...", "✨ 已启用 [7大要素拼接] + [心理物理转译]"]
|
| 659 |
+
yield "\n".join(logs), None, ""
|
| 660 |
+
|
| 661 |
+
if len(raw_blocks) == 0:
|
| 662 |
+
logs.append("❌ 未找到可渲染提示词块 (请确保脚本包含 7D 格式)"); yield "\n".join(logs), None, ""; return
|
| 663 |
+
|
| 664 |
+
formatted_prompts_display = ""
|
| 665 |
+
parsed_tasks = []
|
| 666 |
+
|
| 667 |
+
for i, block in enumerate(raw_blocks):
|
| 668 |
+
title_match = re.search(r'(?:镜头|Shot)\s*(\d+)(?:/\d+)?\s*[\||]\s*(.*)', block)
|
| 669 |
+
title = title_match.group(2).strip() if title_match else f"Untitled"
|
| 670 |
+
safe_title = re.sub(r'[\\/*?:"<>|]', "", title).replace(" ", "_")
|
| 671 |
+
|
| 672 |
+
final_p = assemble_veo_prompt(block)
|
| 673 |
+
if len(final_p) < 50:
|
| 674 |
+
pattern = r"(?:Veo 提示词|Veo Prompt).*?[::]\s*(.*?)(?=\[状态连接\]|\[NEXT_LINK\]|镜头|Shot|$)"
|
| 675 |
+
match = re.search(pattern, block, re.DOTALL | re.IGNORECASE)
|
| 676 |
+
if match:
|
| 677 |
+
final_p = f"BBC纪录片风格,无字幕,无说话声,8k分辨率,超写实,{match.group(1).strip().replace(chr(10), ' ')} --ar 16x9"
|
| 678 |
+
|
| 679 |
+
shot_num = f"{i+1:03d}"
|
| 680 |
+
formatted_entry = f"=== Shot {shot_num}/{len(raw_blocks)}: {title} ===\nveo Prompt (English): {final_p}\n\n"
|
| 681 |
+
formatted_prompts_display += formatted_entry
|
| 682 |
+
|
| 683 |
+
fname = f"Shot_{shot_num}_{safe_title}"
|
| 684 |
+
parsed_tasks.append((final_p, fname))
|
| 685 |
+
|
| 686 |
+
yield "\n".join(logs), None, formatted_prompts_display
|
| 687 |
+
|
| 688 |
+
files = []
|
| 689 |
+
with ThreadPoolExecutor(max_workers=len(parsed_tasks)) as executor:
|
| 690 |
+
futures = {}
|
| 691 |
+
for final_p, fname in parsed_tasks:
|
| 692 |
+
futures[executor.submit(simple_veo_call, final_p, fname, session_dir, video_key)] = fname
|
| 693 |
+
|
| 694 |
+
done = 0
|
| 695 |
+
for f in as_completed(futures):
|
| 696 |
+
res = f.result()
|
| 697 |
+
done += 1
|
| 698 |
+
progress(done/len(parsed_tasks))
|
| 699 |
+
if res['ok']:
|
| 700 |
+
logs.append(f"✅ {res['name']} 完成")
|
| 701 |
+
files.append(res['path'])
|
| 702 |
+
else:
|
| 703 |
+
error_msg = res.get('error', '未知错误')
|
| 704 |
+
logs.append(f"❌ {res['name']} 失败: {error_msg}")
|
| 705 |
+
yield "\n".join(logs[-15:]), None, formatted_prompts_display
|
| 706 |
+
|
| 707 |
+
if files:
|
| 708 |
+
shutil.make_archive(session_dir, 'zip', session_dir)
|
| 709 |
+
yield "\n".join(logs), f"{session_dir}.zip", formatted_prompts_display
|
| 710 |
+
|
| 711 |
+
def simple_veo_call(prompt, name, folder, key):
|
| 712 |
+
last_error = ""
|
| 713 |
+
for attempt in range(5):
|
| 714 |
+
try:
|
| 715 |
+
url = f"{MERCHANT_BASE_URL}/v1/chat/completions"
|
| 716 |
+
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
| 717 |
+
data = {"model": VEO_MODEL, "messages": [{"role": "user", "content": prompt}], "size": VIDEO_SIZE}
|
| 718 |
+
r = requests.post(url, headers=headers, json=data, timeout=300)
|
| 719 |
+
|
| 720 |
+
if r.status_code != 200:
|
| 721 |
+
raise Exception(f"API Error {r.status_code}: {r.text[:100]}")
|
| 722 |
+
|
| 723 |
+
content = r.json()['choices'][0]['message']['content']
|
| 724 |
+
v_url = re.search(r'(https?://[^\s)"]+)', content).group(1)
|
| 725 |
+
|
| 726 |
+
path = os.path.join(folder, name + ".mp4")
|
| 727 |
+
with open(path, "wb") as f: f.write(requests.get(v_url).content)
|
| 728 |
+
|
| 729 |
+
return {"ok": True, "name": name, "path": path}
|
| 730 |
+
except Exception as e:
|
| 731 |
+
last_error = str(e)
|
| 732 |
+
time.sleep(2 * (attempt + 1))
|
| 733 |
+
continue
|
| 734 |
+
|
| 735 |
+
return {"ok": False, "name": name, "error": last_error}
|
| 736 |
+
|
| 737 |
+
# ===============================================
|
| 738 |
+
# --- UI 界面构造 ---
|
| 739 |
+
# ===============================================
|
| 740 |
+
with gr.Blocks(title="Veo 终极全能引擎 (全链路覆盖版)") as app:
|
| 741 |
+
|
| 742 |
+
init_eng, init_v1, init_v2 = load_prompts()
|
| 743 |
+
|
| 744 |
+
gr.Markdown("## 🏭 Veo 终极全能引擎 (全链路覆盖版:微观拆解 + 多机位重复 + 7D)")
|
| 745 |
+
gr.Markdown("**提示**:此版本已强制工程师覆盖从【源头收获】到【包装物流】的全流程,并保留了所有历史铁律。")
|
| 746 |
+
|
| 747 |
+
# 0. 全局配置 & 保存按钮
|
| 748 |
+
with gr.Row(variant="panel"):
|
| 749 |
+
with gr.Column(scale=2):
|
| 750 |
+
with gr.Row():
|
| 751 |
+
llm_key = gr.Textbox(label="🔑 LLM Key", value=DEFAULT_LLM_API_KEY, type="password")
|
| 752 |
+
video_key = gr.Textbox(label="🎬 Video Key", value=DEFAULT_VIDEO_API_KEY, type="password")
|
| 753 |
+
with gr.Column(scale=1):
|
| 754 |
+
btn_save_config = gr.Button("💾 保存所有配置", variant="primary")
|
| 755 |
+
save_status = gr.Textbox(label="状态", lines=1, interactive=False)
|
| 756 |
+
topic_input = gr.Textbox(label="📦 产品名称", placeholder="例如:菠萝")
|
| 757 |
+
|
| 758 |
+
# 1. 工程师
|
| 759 |
+
with gr.Row():
|
| 760 |
+
with gr.Column():
|
| 761 |
+
gr.Markdown("### 🛠️ 步骤 1: 工程师 (全链路拆解)")
|
| 762 |
+
# 🔥 修改点:open=True,强制展开
|
| 763 |
+
with gr.Accordion("提示词设定 (可修改)", open=True):
|
| 764 |
+
prompt_eng_input = gr.Textbox(value=init_eng, lines=10, label="工程师提示词", interactive=True)
|
| 765 |
+
btn_eng = gr.Button("1. 生成海量素材日志", variant="secondary")
|
| 766 |
+
with gr.Column():
|
| 767 |
+
arch_output = gr.Textbox(label="工艺架构输出", lines=10)
|
| 768 |
+
|
| 769 |
+
gr.HTML("<hr>")
|
| 770 |
+
|
| 771 |
+
# 2. 导演 1.0
|
| 772 |
+
with gr.Row():
|
| 773 |
+
with gr.Column():
|
| 774 |
+
gr.Markdown("### 🎨 步骤 2: 导演 1.0 (最终剪辑与铺排)")
|
| 775 |
+
# 🔥 默认值改为 200,最大值改为 300
|
| 776 |
+
count_slider = gr.Slider(1, 300, 200, step=1, label="镜头数量")
|
| 777 |
+
# 🔥 修改点:open=True,强制展开
|
| 778 |
+
with gr.Accordion("提示词设定 (可修改)", open=True):
|
| 779 |
+
prompt_v1_input = gr.Textbox(value=init_v1, lines=15, label="导演1.0提示词", interactive=True)
|
| 780 |
+
btn_v1 = gr.Button("2. 生成完整剧本", variant="primary")
|
| 781 |
+
with gr.Column():
|
| 782 |
+
v1_output = gr.Textbox(label="V1 剧本输出", lines=15)
|
| 783 |
+
|
| 784 |
+
gr.HTML("<hr>")
|
| 785 |
+
|
| 786 |
+
# 3. 导演 2.0
|
| 787 |
+
with gr.Row():
|
| 788 |
+
with gr.Column():
|
| 789 |
+
gr.Markdown("### 🧬 步骤 3: 导演 2.0 (状态进化)")
|
| 790 |
+
# 🔥 修改点:open=True,强制展开
|
| 791 |
+
with gr.Accordion("提示词设定 (可修改)", open=True):
|
| 792 |
+
prompt_v2_input = gr.Textbox(value=init_v2, lines=10, label="导演2.0提示词", interactive=True)
|
| 793 |
+
with gr.Row():
|
| 794 |
+
btn_v2 = gr.Button("3. 启动逻辑变异 (Batch=10)", variant="primary")
|
| 795 |
+
btn_pause = gr.Button("⏸️ 暂停/继续", variant="secondary")
|
| 796 |
+
log_output = gr.Textbox(label="演算日志", lines=15, elem_id="log_box", autoscroll=True)
|
| 797 |
+
with gr.Column():
|
| 798 |
+
v2_output = gr.Textbox(label="最终脚本输出", lines=20, autoscroll=True)
|
| 799 |
+
|
| 800 |
+
gr.HTML("<hr>")
|
| 801 |
+
|
| 802 |
+
# 4. 渲染
|
| 803 |
+
with gr.Row(variant="panel"):
|
| 804 |
+
with gr.Column(scale=1):
|
| 805 |
+
gr.Markdown("### 🚀 步骤 4: 渲染")
|
| 806 |
+
btn_render = gr.Button("4. 全量并发渲染", variant="stop")
|
| 807 |
+
zip_output = gr.File(label="下载 ZIP")
|
| 808 |
+
with gr.Column(scale=2):
|
| 809 |
+
final_prompts_output = gr.Textbox(label="拼接后的提示词 (Assembled Prompts)", lines=20, interactive=False)
|
| 810 |
+
|
| 811 |
+
# 绑定加载事件
|
| 812 |
+
app.load(refresh_config_on_load, inputs=None, outputs=[prompt_eng_input, prompt_v1_input, prompt_v2_input])
|
| 813 |
+
# 绑定按钮事件
|
| 814 |
+
btn_save_config.click(save_prompts, [prompt_eng_input, prompt_v1_input, prompt_v2_input], [save_status])
|
| 815 |
+
btn_eng.click(execute_engineer, [topic_input, llm_key, prompt_eng_input], [arch_output])
|
| 816 |
+
btn_v1.click(execute_director_v1, [topic_input, arch_output, count_slider, llm_key, prompt_v1_input], [v1_output])
|
| 817 |
+
btn_pause.click(toggle_pause, [], [btn_pause])
|
| 818 |
+
btn_v2.click(execute_director_v2_loop, [v1_output, count_slider, llm_key, prompt_v2_input], [log_output, v2_output])
|
| 819 |
+
btn_render.click(render_videos, [v2_output, topic_input, video_key], [log_output, zip_output, final_prompts_output])
|
| 820 |
+
|
| 821 |
+
app.launch()
|