Spaces:
Running
Running
Upload 5 files
Browse files- .gitattributes +3 -0
- app.py +703 -0
- requirement.txt +21 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
demo1.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
demo2.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
demo3.mp4 filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import torch
|
| 3 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 4 |
+
from transformers.models.qwen2_5_omni import Qwen2_5OmniForConditionalGeneration, Qwen2_5OmniProcessor
|
| 5 |
+
import warnings
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
import re
|
| 9 |
+
import base64
|
| 10 |
+
import datetime
|
| 11 |
+
import uuid
|
| 12 |
+
import logging
|
| 13 |
+
from typing import List, Dict, Tuple, Optional
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from huggingface_hub import snapshot_download
|
| 16 |
+
from swift.llm import PtEngine, RequestConfig, InferRequest
|
| 17 |
+
|
| 18 |
+
# --- 依赖库检查 ---
|
| 19 |
+
try:
|
| 20 |
+
import cv2
|
| 21 |
+
from moviepy.editor import VideoFileClip, concatenate_videoclips
|
| 22 |
+
from openai import OpenAI
|
| 23 |
+
from google import genai
|
| 24 |
+
from google.genai import types
|
| 25 |
+
except ImportError as e:
|
| 26 |
+
print(f"❌ 缺少必要库: {e}")
|
| 27 |
+
print("请运行: pip install opencv-python moviepy openai google-genai")
|
| 28 |
+
cv2 = None
|
| 29 |
+
VideoFileClip = None
|
| 30 |
+
OpenAI = None
|
| 31 |
+
genai = None
|
| 32 |
+
|
| 33 |
+
# --- 环境设置 ---
|
| 34 |
+
warnings.filterwarnings("ignore")
|
| 35 |
+
os.environ['PYTHONWARNINGS'] = 'ignore'
|
| 36 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 37 |
+
LOGGER = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
# ==========================================
|
| 40 |
+
# PART 0: 配置常量 & Demo 数据
|
| 41 |
+
# ==========================================
|
| 42 |
+
|
| 43 |
+
# 1. 模型参数配置
|
| 44 |
+
MODEL_CONFIGS = {
|
| 45 |
+
"sora-2": {
|
| 46 |
+
"sizes": ["1792x1024", "1024x1792", "1280x720", "720x1280"],
|
| 47 |
+
"seconds_range": {"minimum": 4, "maximum": 12, "step": 4, "value": 4},
|
| 48 |
+
"seconds_label": "单镜时长 (Sora: 4/8/12秒)"
|
| 49 |
+
},
|
| 50 |
+
"sora-2-pro": {
|
| 51 |
+
"sizes": ["1792x1024", "1024x1792", "1280x720", "720x1280"],
|
| 52 |
+
"seconds_range": {"minimum": 4, "maximum": 12, "step": 4, "value": 4},
|
| 53 |
+
"seconds_label": "单镜时长 (Sora Pro: 4/8/12秒)"
|
| 54 |
+
},
|
| 55 |
+
"veo-3.1": {
|
| 56 |
+
"sizes": ["1080p", "720p"],
|
| 57 |
+
"seconds_range": {"minimum": 4, "maximum": 8, "step": 2, "value": 4},
|
| 58 |
+
"seconds_label": "单镜时长 (Veo: 4/6/8秒)"
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
# 2. 提示词与风格
|
| 63 |
+
CONTINUITY_PROMPT = (
|
| 64 |
+
"保持统一的视觉风格与世界观,场景与光影保持稳定,角色服装、发型、体型与表情连贯,仅根据剧情调整动作;"
|
| 65 |
+
"如果有参考图片,请严格保持人物形象与参考图一致,人物站位不得变化,镜头衔接需流畅自然。"
|
| 66 |
+
"旁白不需要朗读或配音,仅作为剧情提示使用。要求视频生成的最后一帧要展示所有人物的正面形象和此时的站位。"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
STYLE_PROMPTS = {
|
| 70 |
+
"Anime (二次元)": "整体画面要求:高质量二次元动漫渲染风格,角色为手绘动漫人物,肤色与材质为动画质感,背景为虚构的动画场景;禁止出现写实/真人或真实摄影元素。",
|
| 71 |
+
"Realistic (写实)": "整体画面要求:高写实摄影风格,人物与环境光影细节丰富,材质与质感贴近真实世界,禁止出现卡通或夸张笔触,确保色彩与光线符合真实物理规律。",
|
| 72 |
+
"Animated (动画/3D)": "整体画面要求:动画/卡通风格,支持二维或三维渲染,人物线条与轮廓清晰,色彩饱和且富有层次,可适当夸张动作与表情。",
|
| 73 |
+
"Painterly (艺术/绘画)": "整体画面要求:艺术绘画风格,可呈现厚重笔触或水彩晕染质感,允许保留艺术性的纹理与笔法痕迹,整体色彩与构图需统一。",
|
| 74 |
+
"Abstract (抽象/实验)": "整体画面要求:抽象/实验风格,鼓励运用超现实、故障艺术或非传统构图手法,可打破写实规律,突出视觉冲击力与创意表现。"
|
| 75 |
+
}
|
| 76 |
+
STYLE_KEYS = list(STYLE_PROMPTS.keys())
|
| 77 |
+
|
| 78 |
+
# 3. Demo 案例数据(保持不变)
|
| 79 |
+
DEMO_DATA = [
|
| 80 |
+
{
|
| 81 |
+
"file": "demo1.mp4",
|
| 82 |
+
"title": "案例 1",
|
| 83 |
+
"script": "【对话】:1. [0秒-9秒] (运镜:Handheld+远景-中近景-远景) 林间空地,苏洛焦躁踱步,主角静立;手持推进中近景,苏洛抓发踢石:“真不该答应秦飞,这么短哪找齐三个时光盒!”跺脚,切回远景。2. [9-18秒] (浅景深+远景-中景-远景) 主角走近,过肩中景虚焦问:“时光盒是什么?”苏洛回头,专业比划:“云丝控制组倒计时机关盒。”远景二人对站。3. [18-26秒] (Tilt+远景-近景-远景) 远景续讲,切近景苏洛学者状:“指定时间才能开,所以叫时光盒。”镜头下扫手势再回脸,远景收。4. [26-36秒] (Arc+远景-中近景-远景) 远景苏洛夸张,环绕中近景:“秦飞埋三盒说找不到,我当场拍胸口:风物家苏洛一小时全搞定!”下巴扬拍胸,远景仍骄傲。5. [36-44秒] (Shaky+远景-中景-远景) 主角中景揶揄:“看你这么急,没完成吧?”背景苏洛瞬间泄气肩耷,微晃暗示心虚,远景低头。6. [44-53秒] (Pan+远景-近景-远景) 远景垂头,近景苏洛搓衣角小声:“谁料他耍诈盒子远……帮我吗?”抬头平移恳求对视,远景视线交汇。7. [53-62秒] (Crash Zoom+远景-中景-远景) 远景主角点头微笑,���推中景特写苏洛惊喜:“太好了!那两个方向拜托你,余下我搞定!”捶胸指远,远景振作出发。【人设】苏洛:中等轻盈,大眼亚麻棕乱马尾,短夹克工具腰带上阵,情绪秒切藏不住。主角:中高挺拔,柔和深短发深衣简约,内敛冷静微点头。【场景】午后斑驳林间空地,碎石落叶静树,焦躁转轻松合作。【站位】1.苏洛空地中心来回,主角边缘远距。2.主角走近两三步面对面。3-5.保持近距离,苏洛对主角/侧身垂头。6.再对视。7.苏洛侧指远方。"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"file": "demo2.mp4",
|
| 87 |
+
"title": "案例 2",
|
| 88 |
+
"script": "【对话】:1. [0-8秒](运镜类型:Handheld Camera Effect+全景镜头、中景镜头)\n故事情节:远景镜头,幽暗的遗迹室内,蒋风正俯身在一块散发着微光的古代石碑(指引图)前。主角从阴影中走出,站定在他身后几步远。镜头切换为手持拍摄的中景镜头,跟随主角的视线,画面有轻微晃动,聚焦在主角锐利的眼神上。主角双臂环抱,带着审视的口吻质问:“你在做什么?指引图是不能随便篡改的。”声音打破了室内的寂静。结尾回到远景,主角保持质问的姿态,蒋风的背影僵住。\n\n2. [8-16秒](运镜类型:Arc Shot+全景镜头、中景镜头)\n故事情节:远景镜头,蒋风缓缓转过身。镜头以一个平滑的弧度围绕蒋风移动,切换为中景镜头。他看到主角时明显一愣,双手下意识地抬起,掌心向前,做出一个无辜且防御的姿态,眼神慌乱地解释:“篡改指引图?不不不,你误会了。”他的表情诚恳又急切。结尾远景,两人对峙,气氛紧张。\n\n3. [16-24秒](运'运镜类型:Shallow Depth of Field+全景镜头、中近景镜头)\n故事情节:远景镜头,蒋风放下了手,姿态变得谦卑。镜头切换为中近景,焦点落在蒋风身上,他略带窘迫地笑了笑,背景中的主角身影变得模糊。他一边说一边用手比划着自己:“我才加入风物家没多久,哪有这个本事能篡改它。”结尾回到远景,蒋风仍在解释,主角静静地听着,没有打断。\n\n4. [24-33秒](运镜类型:Tilt Shot+全景镜头、特写镜头)\n故事情节:远景镜头,蒋风再次转向指引图。镜头给到蒋风的中近景,他伸出手指,小心翼翼地指向石碑上的一个发光符文,但并未触碰:“我只是想查看指引图上的身份印鉴。”镜头向下倾斜,给到他手指所指之处的符文一个特写,符文复杂而古老。主角的声音从画外传来,带着一丝疑惑:“身份印鉴?”结尾回到远景,主角微微探身,视线也落在了那个符文上。\n\n5. [33-42秒](运镜类型:Panning Shot+全景镜头、近景镜头)\n故事情节:远景镜头,两人都注视着指引图。镜头切换为近景,从蒋风的侧脸开始,他温和地解释着:“嗯,就是一种类似签名的东西。”镜头缓缓横移,扫过石碑上更多类似签名的印鉴,光芒流转。他的声音变得低沉而充满怀念:“在考古界,早期开荒的人员有权在指引图上留下自己的名字,我们称之为身份印鉴。”镜头移回,定格在他充满希冀的眼神上:“我想看看这些指引图上有没有我父亲的名字。”结尾回到远景,整个房间的氛围因这番话而悄然改变。\n\n6. [42-51秒](运镜类型:Lens Flare+全景镜头、中景镜头)\n故事情节:远景镜头,蒋风垂下目光。镜头切换为中景,他背对着石碑,仿佛陷入了久远的回忆,一道柔和的镜头光晕扫过画面,他眼神飘向远方,带着一丝不易察觉的落寞:“我父亲是主攻考古的风物家,但他常年在外勘察……我已经很久很久没见到他了。”结尾回到远景,主角的注意力已经完全从石碑转移到了蒋风身上。\n\n7. [51-59秒](运镜类型:Deep Depth of Field+全景镜头、中近景镜头)\n故事情节:远景镜头,蒋风转过头,重新看向主角。镜头切为中近景,景深拉远,我们能清晰看到前景中蒋风努力挤出一个微笑,眼神却流露着不确定,以及背景里主角严肃倾听的轮廓。蒋风说:“母亲说稷下不少的开荒考古是他完成的,我想看看是不是他真的来过。”结尾回到远景,蒋风的微笑显得有些无力。\n\n8. [59-67秒](运镜类型:Shallow Depth of Field+全景镜头、特写镜头)\n故事情节:远景镜头,室内一片沉寂。镜头推进到蒋风脸部的特写,极浅的景深模糊了周围的一切,只剩下他复杂的表情。他的笑容消失了,嘴唇微微颤抖:“虽然我相信母亲不会骗我,但……”他停顿了一下,低下头,用几不可闻的声音说出心底的委屈,“哪有人经常在外不回家的。”结尾远景,蒋风低着头,肩膀微微垮下。\n\n9. [67-74秒](运镜类型:Shaky Cam+全景镜头、中近景镜头)\n故事情节:远景镜头,主角打破了沉默。镜头切换为中近景,聚焦在主角身上,轻微的镜头晃动暗示着他内心的触动。他原本锐利的眼神已经完全柔和下来,取而代之的是理解与同情。他轻声问道:“那你找到答案了吗?”结尾远景,听到问话,蒋风缓缓抬起头。\n\n10. [74-82秒](运镜类型:Arc Shot+全景镜头、中景镜头)\n故事情节:远景镜头,两人视线交汇。镜头给到蒋风的中景,他轻轻摇头,眼中闪过一丝失望:“目前还没有,我想多找几个地方再下定论。”随即,他深吸一口气,鼓起勇气向前迈了一小步,镜头以一个微小的弧度跟随着他,增加了请求的郑重感。他恳切地问:“那个……可以拜托你帮我深入遗迹内部看看吗?”结尾远景,两人间的距离缩短了。\n\n11. [82-90秒](运镜类型:Shallow Depth of Field+全景镜头、近景镜头)\n故事情节:远景镜头,主角静待下文。镜头切到蒋风的近景,他有些难为情地低下头,看了看自己无力的双手,再抬头望向主角时,眼神充满了坦诚的无助:“以我的实力,里面的机关人我实在无法应付……更别说接近指引图了。”背景中的主角被虚化,突出了蒋风此刻的窘迫与孤立。结尾回到远景,蒋风的姿态显得格外渺小。\n\n12. [90-98秒](运镜类型:Handheld Camera Effect+全景镜头、中近景镜头)\n故事情节:远景镜头,蒋风等待着判决。镜头切为中近景,手持拍摄的画面极度稳定,仿佛连摄影师都屏住了呼吸。蒋风微微躬身,这是一个郑重的请求:“我想最后再确认一下……”他抬起眼,目光灼灼地直视主角,声音里带着颤音,“能请你帮忙完成我这个心愿吗?”结尾回到远景,空气仿佛凝固了,主角一动不动。\n\n13. [98-102秒](运镜类型:Deep Depth of Field+全景镜头、特写镜头)\n故事情节:远景镜头,主角终于有了动作。镜头给到主角面部特写,他沉默地审视着蒋风的眼睛,几秒钟的权衡之后,嘴角无奈地向上一撇,随即发出一声轻不可闻的叹息。景深拉开,我们能看到他身后不远处,蒋风紧张等待的模糊身影。主角终于开口,语气平淡却掷地有声:“行吧。”结尾回到远景,听到回答的蒋风如释重负地松了口气,紧绷的身体瞬间放松下来。\n【人物形象】:主角:身形挺拔,体态匀称有力,面部轮廓分明,眼神锐利如鹰。留着一头便于打理的深色短发,发丝间或夹杂风霜痕迹。身着深色调、材质耐磨的探险服,肩部和肘部有皮革补丁,腰间挂着若干实用工具包。气质沉稳老练,初期动作多为双臂环抱的审视姿态,后期眼神转为柔和,流露同情与无奈,是一位经验丰富、外冷内热的行动派。\n蒋风:身高略低于主角,体态偏瘦,书生气较重,面部线条柔和,眼神清澈但时常流露慌乱与不确定。发型是略显蓬乱的黑色中短发,似乎无暇打理。穿着一身崭新的“风物家”制服,款式简洁但略显宽大,与身形不甚贴合。气质真诚而笨拙,常有抬手、低头、窘迫微笑等下意识动作,在提及父亲时,会从紧张转为充满希冀与感伤的脆弱,是一位涉世未深的年轻后辈。\n【场景描述】:幽暗的古代遗迹室内,唯一的稳定光源来自一块散发着微光的石碑指引图,石壁上刻有古老符文。场景氛围从初始的紧张对峙,随着角色对话的深入,逐渐转变为充满感伤与理解的静谧与私密。\n【站位】:1:主角站在蒋风身后几步远处,蒋风俯身于石碑前。\n2:蒋风完全转过身,与主角正面相对,形成对峙。\n3:两人保持面对面的站位,距离不变。\n4:蒋风转身面向石碑,主角在其侧后方,视线投向石碑。\n5:两人大致并排,共同注视着石碑。\n6:蒋风背对石碑,面向空旷处;主角从侧面注视着蒋风。\n7:蒋风转身,再次与主角面对面站立。\n8:两人位置不变,蒋风低头,避开主角视线。\n9:蒋风抬头,与主角视线交汇,维持原有距离。\n10:蒋风向前迈出一小步,缩短了与主角的距离。\n11:两人位置不变,蒋风抬头直视主角。\n12:蒋风微微躬身,更显谦卑地仰视着主角。\n13:主角与蒋风保持着略近的面对面距离,主角站姿笔直,蒋风躬身等待。\n"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"file": "demo3.mp4",
|
| 92 |
+
"title": "案例 3",
|
| 93 |
+
"script": "【对话】:1. [0-8]秒(Handheld Camera Effect+中近景镜头)\n故事情节:远景中,主角小心翼翼地靠近一具巨大的、半埋在瓦砾中的废弃机器,机器周身缠绕着微光的丝线。镜头推近至主角的中近景,他面带忧虑,试探性地向前伸了伸手,又缩了回来,最终开口问道:“你还好吗?”说完,他沉默地观察着,等待回应。镜头拉回远景,一人一机在废墟中对峙。\n\n2. [8-18]秒(Shallow Depth of Field+特写镜头)\n故事情节:远景中,废弃机器毫无动静。镜头切入机器光学感应器的特写,红光微弱地闪烁了几下,发出断续的电流声。感应器缓缓亮起,聚焦在主角身上,传来一阵沙哑的合成音:“谢……谢,我好多了,”镜头的焦点在它破损的金属外壳和裸露的线缆上游走,“这个躯壳似乎越来越难以承受我的意识了……”镜头回到远景,机器的灯光在昏暗中显得格外醒目。\n\n3. [18-26]秒(Panning Shot+中景镜头)\n故事情节:远景中,机器的姿态未变。镜头缓缓平移,以中景构图框住机器的上半部分,它发出几声清嗓子般的杂音,似乎在整理思绪:“我说到哪了?算了,”它的光学感应器转向主角,“不如你向我提问吧。来,你想了解什么?”镜头回到远景,主角依然保持着倾听的姿态。\n\n4. [26-36]秒(Deep Depth of Field+中远景镜头)\n故事情节:远景中,主角与机器的位置关系不变。镜头切为中远景,将一人一机都清晰地纳入画面,背景是广阔而荒凉的废墟。机器主动开始了话题,语气变得像一位学者:“我的研究对象是云丝,你知道吧?一开始我研究的只是用云丝牵拉傀儡……傀儡的躯干是云根所制,本身就是固化的云丝。”镜头回到远景。\n\n5. [36-44]秒(Arc Shot+近景镜头)\n故事情节:远景中,机器继续讲述。镜头开始围绕机器进行缓慢的弧形运动,切入它的近景,光学感应器闪烁着思索的光芒。“我意识到,自己在做的不过是用云丝拉扯另一条云丝罢了,到此还好……”它的声音里透出一丝自嘲。镜头回到远景,弧光灯在机器表面划过。\n\n6. [44-54]秒(Crash Zoom+特写镜头)\n故事情节:远景中,气氛开始变化。镜头突然快速推向机器的光学感应器,形成一个特写,红光变得明亮而急促。“但我又多想了一点……是什么牵拉着你我这副肉躯呢?肌肉?骨骼?”声音变得尖锐而充满蛊惑,“难道不是另一种云丝和云根吗?”镜头回到远景,主角下意识地后退了半步。\n\n7. [54-63]秒(Shaky Cam+中景镜头)\n故事情节:远景中,主角显得有些不安。镜头切换为手持摇晃效果,对准机器的中景,仿佛在感受它激动的情绪。“既然如此,我们的世界又和云丝编制的一场幻觉有何分别呢?”它的一根机械臂因过载而轻微抽搐了一下。镜头回到远景,那根抽搐的机械臂在静止的废墟中格外显眼。\n\n8. [63-71]秒(Tilt Shot+中近景镜头)\n故事情节:远景中,机器的宣讲仍在继续。镜头从中近景开始,从主角紧锁的眉头的脸上向下倾斜,扫过他紧握的拳头,最后停在地面上。“我们不过是提线木偶……举手投足都是决定好的剧本,”机器的声音回荡着,主角的视线也随之落在自己手上,仿佛在寻找看不见的丝线,“于是我假设了自己的设定。”镜头回到远景。\n\n9. [71-79]秒(Shallow Depth of Field+极端特写镜头)\n故事情节:远景中,机器的音调突然沉重下来。镜头切至极端特写,对准它外壳上一道深刻的裂痕,仿佛一道伤疤,背景完全虚化。“后来改变一切的……”它的声音里充满了悔恨与自负交织的复杂情绪。镜头回到远景,机器陷入短暂的沉默。\n\n10. [79-89]秒(Action Shot+近景镜头)\n故事情节:远景中,机器再次激动起来。镜头切换为模拟剧烈晃动的动作镜头,对准机器的近景,周身的云丝因能量波动而发光、绷紧。“是一次愚蠢的实验,我太急于论证这个世界是一场云丝编制的虚幻了!既然是一场虚幻,就能根据自己的意志改写!”伴随话语,几点电火花从它关节处迸射出来。镜头回到远景。\n\n11. [89-98]秒(Whip Pan+中景镜头)\n故事情节:远景中,机器的能量波动更强了。镜头从机器转向周围缠绕的云丝,再猛地甩回机器身上,形成一次快速摇摄。“一切都是经线和纬线,可以被牵拉、被抽取、被剥离、被绕制……”它用一种近乎癫狂的语气描述着。镜头回到远景,云丝的光芒渐渐暗淡。\n\n12. [98-107]秒(Tilt Shot+中远景镜头)\n故事情节:远景中,机器似乎耗尽了能量。镜头从中远景开始,自上而下缓缓扫过它庞大而残破的躯体,最终停在它被地面卡住的基座上。“用语言解释原理太难了……总之,”它的声音疲惫不堪,“我成功地把自己‘改写’成了这副模样。至于为什么卡在这动弹不得,又是另一个故事了。”镜头回到远景。\n\n13. [107-115]秒(Shallow Depth of Field+中景镜头)\n故事情节:远景中,主角静静地消化着信息。镜头切到机器的中景,它光学感应器里的光芒稳定下来,直视着主角,语气平静但充满挑战:“怎样,现在你愿意相信这个世界是虚假的吗?我说的还是很浅的一部分,这个世界比你想象的更深……”主角的表情在虚化的背景中模糊不清。镜头回到远景。\n\n14. [115-123]秒(Handheld Camera Effect+近景镜头)\n故事情节:远景中,主角没有回应。镜头以手持效果靠近主角的近景,捕捉他脸上怀疑与动摇交织的神情。机器的声音再次响起,带着一丝不耐烦:“还是不信?看来我需要列举更多的证据……”主角的视线从机器移开,望向远方,似乎在思考。镜头回到远景。\n\n15. [123-132]秒(Crane Shot+全景镜头)\n故事情节:远景中,机器的气场陡然增强。镜头如吊臂般向上拉升,俯瞰着以机器为中心、云丝如蛛网般铺开的废墟全景。“通过云丝,如今我可以像一只活在宇宙中心的蜘蛛一样探访世界的所有角落……”声音充满了全知全能的优越感。镜头回到最初的远景视角。\n\n16. [132-142]秒(Panning Shot+特写镜头)\n故事情节:远景中,机器开始列举证据。镜头平移,给到主角面部的特写,他的眼睛因惊讶而微微睁大。机器的声音仿佛从四面八方传来:“我知道在地下世界生活着会说话的小木篱,我听过它们梦中的呓语……我知道善良而勤劳的咕呱,也会在背地里筹划各种坏事……”镜头回到远景。\n\n17. [142-152]秒(Deep Depth of Field+中远景镜头)\n故事情节:远景中,主角的视线被远处的瀑布吸引。镜头切为深景深的中远景,机器在前景,远处的瀑布清晰可见。机器的声音指向那个方向:“我知道在我所处的这个瀑布后面,藏有一份卷轴……上面标识出了一份宝藏……”主角的目光锁定在瀑布上。镜头回到远景。\n\n18. [152-162]秒(Arc Shot+中近景镜头)\n故事情节:远景中,机器继续揭示秘密。镜头围绕主角进行弧形运动,主角的神情从惊讶变为专注。机器的声音继续引导:“而开启它的钥匙,就藏在东南山崖上,某个孤立石刻的底部……那里如今已经被地狼占据了……”主角的视线转向东南方向。镜头回到远景。\n\n19. [162-170]秒(Crash Zoom+近景镜头)\n故事情节:远景中,机器的讲述达到高潮。镜头猛地推向主角的近景,他脸上写满了震惊和决断。机器的声音变得急切而充满煽动性:“你还愣着干什么,快把它们找回来,好印证我的发现啊!”主角深吸一口气,眼神变得坚定。镜头回到远景,主角转身,似乎准备出发。\n【人物形象】:主角:身高约175cm,体态精悍矫健/面容年轻但显风霜,眼神深邃,表情丰富,黑色短发略显凌乱/身着便于行动的暗色系旅行者装束,材质耐磨,配有皮质护具和多功能腰包/气质沉稳内敛,初期动作谨慎试探,善于倾听与观察,后期情绪从忧虑、怀疑转为震惊与决断/他习惯在思考时将视线投向远方,紧握的拳头透露出内心的挣扎与力量。\n废弃机器:体型巨大,如一座小山般半埋于瓦砾中,结构复杂,非人形态/核心是单颗巨大的光学感应器,通过红光的明暗与闪烁频率表达情绪,无传统面部/金属外壳严重锈蚀破损,遍布裂痕与裸露的线缆,周身缠绕着蛛网般发光的“云丝”/声音是沙哑的合成音,语气从虚弱转为学者般的平静,再到癫狂、蛊惑与急切/作为被困的意识体,它的“动作”仅限于感应器灯光变化、电流声、机械臂的轻微抽搐和周身云丝的能量波动。\n【场景描述】:在一片广阔荒凉的废墟之中,光线昏暗,气氛神秘而压抑。视觉中心是一具半埋于地的巨大废弃机器,其周身缠绕的微光丝线是环境中唯一的动态光源。\n【站位】:1:主角在机器前方数米处站定,面朝机器。\n2:两者相对位置不变,机器光学感应器聚焦于主角。\n3:位置不变,主角保持倾听姿态。\n4:位置不变,一人一机远距离对峙。\n5:相对位置不变。\n6:主角向后退半步,与机器距离稍稍拉远。\n7:相对位置不变。\n8:主角低头看向自己的手,视线暂时离开机器。\n9:相对位置不变,主角抬头重新望向机器。\n10:相对位置不变。\n11:相对位置不变。\n12:相对位置不变。\n13:相对位置不变,机器感应器直视主角。\n14:主角身体朝向不变,但视线移向远方。\n15:相对位置不变,俯瞰视角下主角位于机器前方。\n16:主角视线回到前方。\n17:主角身体未动,头部与视线转向远方瀑布。\n18:主角身体与视线转向东南方向。\n19:主角转身,准备离开,背向机器。"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"file": "demo4.mp4",
|
| 97 |
+
"title": "案例 4",
|
| 98 |
+
"script": "【对话】:1. [0-8秒] (Shallow Depth of Field+全景镜头、中近景镜头) 故事情节:远景展示公园小径上,白衫和主角并肩站立,一只胖乎乎的宠物“呱呱”趴在他们脚边的草地上气喘吁吁。镜头随即以浅景深推向白衫的中近景,他低头看着呱呱,脸上交织着无奈与宠溺,接着他抬眼望向主角,问道:“怎么样?呱呱有好好锻炼吗?” 结尾回到远景,主角正准备回答。\n2. [8-17秒] (Handheld Camera Effect+全景镜头、中景镜头) 故事情节:远景中,三人位置不变。镜头切换为手持效果下的中景,画面随着白衫的动作有轻微晃动。他无奈地摊开手,叹了口气:“没办法,之前太惯着它了。” 他的视线落在懒洋洋翻了个身的呱呱身上,语气里满是无可奈何:“现在没有吃的半步也不挪。” 结尾远景,白衫轻轻摇头,主角在一旁安静地听着。\n3. [17-26秒] (Arc Shot+全景镜头、中景镜头) 故事情节:远景展现整个场景。镜头开始围绕白衫进行弧线运动,他蹲下身,温柔地抚摸着呱呱的后背,语气放缓:“不过它今天起码完成了一点运动,晚上允许它多吃一点。” 镜头继续沿弧线转向一旁的主角,他看着这温情的一幕,微笑着点头附和:“今天运动的还不错。” 结尾远景,白衫蹲着,主角站着,形成一高一低的构图。\n4. [26-34秒] (Shallow Depth of Field+全景镜头、近景镜头) 故事情节:远景中,白衫依旧蹲在呱呱身边,主角静立一旁。镜头切入白衫的近景,背景完全虚化。他抚摸呱呱的动作没停,但眼神中浮现出真切的忧虑,声音也低沉下来,充满了担忧:“我知道运动很辛苦,但呱呱真的太胖了,我很怕它胖得生病。” 结尾远景,能看到主角脸上的笑容也收敛了,神情变得严肃。\n5. [34-42秒] (Tilt Shot+全景镜头、中景镜头) 故事情节:远景确认场景站位。镜头从中景开始,从地上心满意足地摇着尾巴的呱呱缓缓向上抬升,最终定格在刚站起身的白衫脸上。他脸上的忧虑一扫而空,转为一种故作爽朗的兴奋,对着呱呱大声宣布:“表现真不错。今晚它可以多吃一点!” 结尾远景,白衫高兴地拍了拍手。\n6. [42-51秒] (Shaky Cam+全景镜头、中近景镜头) 故事情节:远景中,主角看着兴奋的白衫。镜头切换为对准主角的中近景,轻微的摇晃反映出他内心的无语。他看着白衫,嘴角微微抽动,眼神里是哭笑不得的怀疑,几乎是对自己低语:“这么吃还能瘦么……” 随即他像是突然想起了正事,表情一正,视线重新聚焦在白衫身上,问道:“对了,你知道刘叶的情况怎么样了吗?” 结尾远景,主角向前迈了半步,成功转移了话题。\n7. [51-58秒] (Deep Depth of Field+全景镜头、中景镜头) 故事情节:远景中,两人相对而立。镜头切到白衫的中景,听到“刘叶”的名字,他先是愣了一下,随即眼神投向远方,景深变大,背景中的公园路径和行人都变得清晰。他皱眉思索片刻,然后略带歉意地摇了摇头,收回目光:“刘叶?看着是没长高,具体我就不知道了。” 结尾远景,白衫看着主角,摊了摊手表示不知情。\n8. [58-67秒] (Panning Shot+全景镜头、中远景镜头) 故事情节:远景中,三人保持着最后的站位。镜头给到白衫的中远景,他侧过身,抬手指向西边的方向,语气变得热心:“同学要是想知道,不如去找他问问吧。” 镜头随着他的手臂平滑地向西边摇摄,画面中出现一条通往远处开阔广场的小径。“你往西走,他就在那边的广场。” 结尾远景,镜头停下,主角顺着白衫所指的方向望去,若有所思。\n【人物形象】:白衫:身高约180cm,体态匀称修长,略带少年感。面部线条柔和,眉眼清秀,笑起来时眼角有细微纹路。发型是自然的黑色短发,刘海稍长,显得随性。身穿一件干净的白色棉麻衬衫和浅色休闲裤,脚踩白色运动鞋。气质温和亲切,与人交谈时真诚,对待宠物时眼神宠溺,会用摊手、挠头等小动作表达无奈,是个内心细腻的暖男。\n主角:身高与白衫相仿,身形挺拔,站姿稳重。面部轮廓分明,眼神锐利但内敛,表情变化细微,善于观察。发型为深色利落短发,显得干练。穿着深色系的休闲夹克,内搭纯色T恤,下身是工装裤,整体风格偏向实用和低调。气质沉稳,话不多,习惯通过嘴角抽动、眼神聚焦等微表情传递内心活动,行动果断且有目的性。\n呱呱:一只体型极度肥胖的宠物,身躯圆滚滚,四肢短小,趴在地上像个肉球。拥有一双憨态可掬的大眼睛,表情总是懒洋洋的。毛发短而顺滑,脖子上戴着一个简单的项圈。动作迟缓,极度懒散,没有食物的诱惑便不愿动弹,对主人的抚摸会表现出心满意足的样子,是一只被宠坏了��“吃货”。\n【场景描述】:午后阳光明媚的公园草坪,氛围从轻松宠溺的日常,转为对宠物健康的真切担忧,最终变为热心指路的平实交流。主要视觉元素是茵茵绿草、蜿蜒的小径,以及趴在地上一动不动的胖宠物。\n【站位】:1. 白衫与主角并肩站立,呱呱在他们脚边的草地上。\n2. 三人位置不变,白衫面向主角和呱呱。\n3. 白衫蹲在呱呱旁边,主角站在他身侧,形成高低位。\n4. 白衫维持蹲姿,主角站在一旁注视。\n5. 白衫从呱呱身边站起,转身面对呱呱。\n6. 主角面向白衫,两人相对而立,主角向前半步拉近距离。\n7. 两人保持相对站立,白衫短暂望向远方后,目光回到主角身上。\n8. 白衫侧身指向西边,主角随其指向望向同一方向。"
|
| 99 |
+
}
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
# ==========================================
|
| 103 |
+
# PART 1: 剧本生成模型 (ScriptAgent)
|
| 104 |
+
# ==========================================
|
| 105 |
+
from swift.llm import get_model_tokenizer, get_template, inference
|
| 106 |
+
import torch
|
| 107 |
+
|
| 108 |
+
# 全局变量
|
| 109 |
+
MODEL_NAME = "XD-MU/ScriptAgent"
|
| 110 |
+
LOCAL_MODEL_PATH = "./downloaded_models/ScriptAgent"
|
| 111 |
+
OFFLOAD_FOLDER = "./offload"
|
| 112 |
+
model = None # 模型对象
|
| 113 |
+
tokenizer = None # 分词器对象
|
| 114 |
+
template = None # 模板对象
|
| 115 |
+
|
| 116 |
+
# 确保目录存在
|
| 117 |
+
os.makedirs(LOCAL_MODEL_PATH, exist_ok=True)
|
| 118 |
+
os.makedirs(OFFLOAD_FOLDER, exist_ok=True)
|
| 119 |
+
|
| 120 |
+
def load_llm_model():
|
| 121 |
+
"""使用 SWIFT 加载 ScriptAgent 模型 - CPU优化版本"""
|
| 122 |
+
global model, tokenizer, template
|
| 123 |
+
if model is not None:
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
# 1. 检查本地是否已下载模型
|
| 128 |
+
if not os.path.exists(LOCAL_MODEL_PATH):
|
| 129 |
+
print(f"正在从 HuggingFace 下载模型到 {LOCAL_MODEL_PATH}...")
|
| 130 |
+
snapshot_download(
|
| 131 |
+
repo_id=MODEL_NAME,
|
| 132 |
+
local_dir=LOCAL_MODEL_PATH,
|
| 133 |
+
local_dir_use_symlinks=False,
|
| 134 |
+
resume_download=True
|
| 135 |
+
)
|
| 136 |
+
print(f"✅ 模型已下载到: {LOCAL_MODEL_PATH}")
|
| 137 |
+
else:
|
| 138 |
+
print(f"✅ 模型已存在: {LOCAL_MODEL_PATH}")
|
| 139 |
+
|
| 140 |
+
# 2. 使用 SWIFT 正确加载模型
|
| 141 |
+
print("正在使用 SWIFT 加载模型(CPU + 半精度优化)...")
|
| 142 |
+
|
| 143 |
+
# 🔥 关键修改:使用 get_model_tokenizer
|
| 144 |
+
model, tokenizer = get_model_tokenizer(
|
| 145 |
+
model_id_or_path=LOCAL_MODEL_PATH,
|
| 146 |
+
torch_dtype=torch.float16, # 半精度
|
| 147 |
+
model_kwargs={
|
| 148 |
+
'device_map': 'cpu', # CPU设备
|
| 149 |
+
'low_cpu_mem_usage': True, # 低内存模式
|
| 150 |
+
'offload_folder': OFFLOAD_FOLDER, # 内存溢出卸载到磁盘
|
| 151 |
+
},
|
| 152 |
+
max_model_len=4096, # 限制上下文长度
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# 设置为评估模式
|
| 156 |
+
model.eval()
|
| 157 |
+
|
| 158 |
+
# 获取模板
|
| 159 |
+
template = get_template(tokenizer=tokenizer, model=model)
|
| 160 |
+
|
| 161 |
+
print("✅ SWIFT 模型加载完成(已启用内存优化)")
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
print(f"❌ 模型加载失败: {e}")
|
| 165 |
+
import traceback
|
| 166 |
+
traceback.print_exc()
|
| 167 |
+
|
| 168 |
+
def chat_with_scriptagent(user_input: str):
|
| 169 |
+
"""使用 SWIFT 与 ScriptAgent 对话生成剧本"""
|
| 170 |
+
global model, tokenizer, template
|
| 171 |
+
|
| 172 |
+
if model is None:
|
| 173 |
+
load_llm_model()
|
| 174 |
+
if model is None:
|
| 175 |
+
return "❌ 模型加载失败,请检查后台日志。"
|
| 176 |
+
|
| 177 |
+
user_input = user_input.strip()
|
| 178 |
+
if not user_input:
|
| 179 |
+
return "请输入内容"
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
print("🤖 正在使用 SWIFT 推理剧本...")
|
| 183 |
+
|
| 184 |
+
# 🔥 使用 SWIFT 的 inference 函数
|
| 185 |
+
response, _ = inference(
|
| 186 |
+
model=model,
|
| 187 |
+
tokenizer=tokenizer,
|
| 188 |
+
template=template,
|
| 189 |
+
query=user_input,
|
| 190 |
+
max_new_tokens=4096, # 从8192降低到4096
|
| 191 |
+
temperature=0.7,
|
| 192 |
+
top_p=0.9,
|
| 193 |
+
repetition_penalty=1.1,
|
| 194 |
+
do_sample=True,
|
| 195 |
+
num_beams=1, # 贪婪解码
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
print(f"✅ 生成结果长度: {len(response)} 字符")
|
| 199 |
+
return response if response else "⚠️ 生成为空,请重试"
|
| 200 |
+
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"❌ 生成出错: {e}")
|
| 203 |
+
import traceback
|
| 204 |
+
traceback.print_exc()
|
| 205 |
+
return f"生成失败: {str(e)}"
|
| 206 |
+
# ==========================================
|
| 207 |
+
# PART 2: 视频生成 API 封装
|
| 208 |
+
# ==========================================
|
| 209 |
+
|
| 210 |
+
class OpenAISoraAPI:
|
| 211 |
+
"""OpenAI Sora API 封装"""
|
| 212 |
+
|
| 213 |
+
def __init__(self, api_key: str):
|
| 214 |
+
if OpenAI is None:
|
| 215 |
+
raise RuntimeError("未安装 openai 库,请运行: pip install openai")
|
| 216 |
+
self.client = OpenAI(api_key=api_key)
|
| 217 |
+
|
| 218 |
+
def generate_video(
|
| 219 |
+
self,
|
| 220 |
+
prompt: str,
|
| 221 |
+
output_path: str,
|
| 222 |
+
model: str,
|
| 223 |
+
size: str,
|
| 224 |
+
seconds: int,
|
| 225 |
+
ref_img_path: str = None
|
| 226 |
+
) -> Optional[str]:
|
| 227 |
+
"""
|
| 228 |
+
生成视频
|
| 229 |
+
返回: None (成功) 或 错误信息字符串
|
| 230 |
+
"""
|
| 231 |
+
try:
|
| 232 |
+
LOGGER.info(f"🎬 Sora API 调用: {model} | {size} | {seconds}秒")
|
| 233 |
+
|
| 234 |
+
# 构建请求参数
|
| 235 |
+
kwargs = {
|
| 236 |
+
"model": model,
|
| 237 |
+
"prompt": prompt,
|
| 238 |
+
"size": size,
|
| 239 |
+
"seconds": str(seconds),
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
# 添加参考图片(如果有)
|
| 243 |
+
if ref_img_path and os.path.exists(ref_img_path):
|
| 244 |
+
with open(ref_img_path, 'rb') as f:
|
| 245 |
+
kwargs["input_reference"] = f
|
| 246 |
+
|
| 247 |
+
# 创建视频任务
|
| 248 |
+
video_job = self.client.videos.create(**kwargs)
|
| 249 |
+
|
| 250 |
+
# 轮询任务状态
|
| 251 |
+
while video_job.status in ["queued", "processing"]:
|
| 252 |
+
LOGGER.info(f"⏳ 视频生成中... 进度: {video_job.progress}%")
|
| 253 |
+
time.sleep(10)
|
| 254 |
+
video_job = self.client.videos.retrieve(video_job.id)
|
| 255 |
+
|
| 256 |
+
# 检查任务状态
|
| 257 |
+
if video_job.status == "completed":
|
| 258 |
+
# 下载视频
|
| 259 |
+
video_url = video_job.url
|
| 260 |
+
import requests
|
| 261 |
+
video_data = requests.get(video_url).content
|
| 262 |
+
with open(output_path, 'wb') as f:
|
| 263 |
+
f.write(video_data)
|
| 264 |
+
LOGGER.info(f"✅ 视频已保存: {output_path}")
|
| 265 |
+
return None
|
| 266 |
+
else:
|
| 267 |
+
error_msg = f"视频生成失败,状态: {video_job.status}"
|
| 268 |
+
LOGGER.error(error_msg)
|
| 269 |
+
return error_msg
|
| 270 |
+
|
| 271 |
+
except Exception as e:
|
| 272 |
+
error_msg = f"Sora API 错误: {str(e)}"
|
| 273 |
+
LOGGER.error(error_msg)
|
| 274 |
+
import traceback
|
| 275 |
+
traceback.print_exc()
|
| 276 |
+
return error_msg
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class GoogleVeoAPI:
|
| 280 |
+
"""Google Veo 3.1 API 封装"""
|
| 281 |
+
|
| 282 |
+
def __init__(self, api_key: str):
|
| 283 |
+
if genai is None:
|
| 284 |
+
raise RuntimeError("未安装 google-genai 库,请运行: pip install google-genai")
|
| 285 |
+
self.client = genai.Client(api_key=api_key)
|
| 286 |
+
|
| 287 |
+
def generate_video(
|
| 288 |
+
self,
|
| 289 |
+
prompt: str,
|
| 290 |
+
output_path: str,
|
| 291 |
+
size: str,
|
| 292 |
+
seconds: int,
|
| 293 |
+
ref_img_path: str = None
|
| 294 |
+
) -> Optional[str]:
|
| 295 |
+
"""
|
| 296 |
+
生成视频
|
| 297 |
+
返回: None (成功) 或 错误信息字符串
|
| 298 |
+
"""
|
| 299 |
+
try:
|
| 300 |
+
LOGGER.info(f"🎬 Veo API 调用: {size} | {seconds}秒")
|
| 301 |
+
|
| 302 |
+
# 构建配置
|
| 303 |
+
config_kwargs = {}
|
| 304 |
+
|
| 305 |
+
# 添加参考图片(如果有)
|
| 306 |
+
if ref_img_path and os.path.exists(ref_img_path):
|
| 307 |
+
ref_image = Image.open(ref_img_path)
|
| 308 |
+
reference = types.VideoGenerationReferenceImage(
|
| 309 |
+
image=ref_image,
|
| 310 |
+
reference_type="asset"
|
| 311 |
+
)
|
| 312 |
+
config_kwargs["reference_images"] = [reference]
|
| 313 |
+
|
| 314 |
+
# 映射分辨率
|
| 315 |
+
resolution_map = {"1080p": "1080p", "720p": "720p"}
|
| 316 |
+
resolution = resolution_map.get(size, "720p")
|
| 317 |
+
|
| 318 |
+
# 创建视频生成任务
|
| 319 |
+
operation = self.client.models.generate_videos(
|
| 320 |
+
model="veo-3.1-generate-preview",
|
| 321 |
+
prompt=prompt,
|
| 322 |
+
config=types.GenerateVideosConfig(
|
| 323 |
+
duration_seconds=seconds,
|
| 324 |
+
resolution=resolution,
|
| 325 |
+
aspect_ratio="16:9",
|
| 326 |
+
**config_kwargs
|
| 327 |
+
),
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# 轮询任务状态
|
| 331 |
+
while not operation.done:
|
| 332 |
+
LOGGER.info("⏳ 视频生成中...")
|
| 333 |
+
time.sleep(10)
|
| 334 |
+
operation = self.client.operations.get(operation)
|
| 335 |
+
|
| 336 |
+
# 下载视频
|
| 337 |
+
video = operation.response.generated_videos[0]
|
| 338 |
+
self.client.files.download(file=video.video, output_path=output_path)
|
| 339 |
+
|
| 340 |
+
LOGGER.info(f"✅ 视频已保存: {output_path}")
|
| 341 |
+
return None
|
| 342 |
+
|
| 343 |
+
except Exception as e:
|
| 344 |
+
error_msg = f"Veo API 错误: {str(e)}"
|
| 345 |
+
LOGGER.error(error_msg)
|
| 346 |
+
import traceback
|
| 347 |
+
traceback.print_exc()
|
| 348 |
+
return error_msg
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# ==========================================
|
| 352 |
+
# PART 3: 视频处理工具函数
|
| 353 |
+
# ==========================================
|
| 354 |
+
|
| 355 |
+
def parse_script_nodes(script_text: str) -> List[str]:
|
| 356 |
+
"""解析剧本为分镜列表"""
|
| 357 |
+
cleaned = script_text.replace("\r\n", "\n").strip()
|
| 358 |
+
pattern = re.compile(r"\s*(\d+)\.\s*")
|
| 359 |
+
matches = list(pattern.finditer(cleaned))
|
| 360 |
+
if not matches:
|
| 361 |
+
return [line.strip() for line in cleaned.split('\n') if line.strip()]
|
| 362 |
+
nodes = []
|
| 363 |
+
for index, match in enumerate(matches):
|
| 364 |
+
start = match.end()
|
| 365 |
+
end = matches[index + 1].start() if index + 1 < len(matches) else len(cleaned)
|
| 366 |
+
content = cleaned[start:end].strip()
|
| 367 |
+
if content:
|
| 368 |
+
nodes.append(content)
|
| 369 |
+
return nodes
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def extract_last_frame(video_path: str, output_path: str) -> Optional[str]:
|
| 373 |
+
"""提取视频最后一帧作为参考图"""
|
| 374 |
+
if cv2 is None:
|
| 375 |
+
return None
|
| 376 |
+
|
| 377 |
+
cap = cv2.VideoCapture(video_path)
|
| 378 |
+
if not cap.isOpened():
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 382 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, max(total_frames - 1, 0))
|
| 383 |
+
ret, frame = cap.read()
|
| 384 |
+
cap.release()
|
| 385 |
+
|
| 386 |
+
if ret:
|
| 387 |
+
cv2.imwrite(output_path, frame)
|
| 388 |
+
return output_path
|
| 389 |
+
return None
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def stitch_videos(video_paths: List[str], output_path: str):
|
| 393 |
+
"""拼接多个视频为最终成片"""
|
| 394 |
+
if not video_paths:
|
| 395 |
+
raise ValueError("未提供可拼接的视频文件。")
|
| 396 |
+
|
| 397 |
+
if VideoFileClip is None or concatenate_videoclips is None:
|
| 398 |
+
raise RuntimeError("未找到 moviepy,请安装依赖。")
|
| 399 |
+
|
| 400 |
+
clips = []
|
| 401 |
+
try:
|
| 402 |
+
for path in video_paths:
|
| 403 |
+
if not os.path.exists(path):
|
| 404 |
+
continue
|
| 405 |
+
clips.append(VideoFileClip(path))
|
| 406 |
+
|
| 407 |
+
if not clips:
|
| 408 |
+
raise ValueError("没有有效的视频片段")
|
| 409 |
+
|
| 410 |
+
final_clip = concatenate_videoclips(clips, method="compose")
|
| 411 |
+
final_clip.write_videofile(
|
| 412 |
+
output_path,
|
| 413 |
+
codec="libx264",
|
| 414 |
+
audio_codec="aac",
|
| 415 |
+
verbose=False,
|
| 416 |
+
logger=None,
|
| 417 |
+
remove_temp=True
|
| 418 |
+
)
|
| 419 |
+
finally:
|
| 420 |
+
for clip in clips:
|
| 421 |
+
clip.close()
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
# ==========================================
|
| 425 |
+
# PART 4: 视频生成流水线
|
| 426 |
+
# ==========================================
|
| 427 |
+
|
| 428 |
+
def run_video_generation_pipeline(
|
| 429 |
+
script_text: str,
|
| 430 |
+
api_key: str,
|
| 431 |
+
model_name: str,
|
| 432 |
+
style_choice: str,
|
| 433 |
+
size: str,
|
| 434 |
+
seconds: int
|
| 435 |
+
):
|
| 436 |
+
"""
|
| 437 |
+
视频生成流水线
|
| 438 |
+
|
| 439 |
+
Yields: (分镜列表, 最终视频路径, 日志信息)
|
| 440 |
+
"""
|
| 441 |
+
# 验证输入
|
| 442 |
+
if not script_text:
|
| 443 |
+
yield [], None, "❌ 请输入剧本!"
|
| 444 |
+
return
|
| 445 |
+
|
| 446 |
+
if not api_key or api_key == "Your API Key":
|
| 447 |
+
yield [], None, "❌ 请输入有效的 API Key!"
|
| 448 |
+
return
|
| 449 |
+
|
| 450 |
+
# 解析剧本
|
| 451 |
+
nodes = parse_script_nodes(script_text)
|
| 452 |
+
run_id = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 453 |
+
output_dir = os.path.join("output_videos", run_id)
|
| 454 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 455 |
+
|
| 456 |
+
# 初始化 API 客户端
|
| 457 |
+
try:
|
| 458 |
+
if model_name.startswith("sora"):
|
| 459 |
+
api_client = OpenAISoraAPI(api_key)
|
| 460 |
+
elif model_name.startswith("veo"):
|
| 461 |
+
api_client = GoogleVeoAPI(api_key)
|
| 462 |
+
else:
|
| 463 |
+
yield [], None, f"❌ 不支持的模型: {model_name}"
|
| 464 |
+
return
|
| 465 |
+
except Exception as e:
|
| 466 |
+
yield [], None, f"❌ API 初始化失败: {str(e)}"
|
| 467 |
+
return
|
| 468 |
+
|
| 469 |
+
generated_videos = []
|
| 470 |
+
last_frame_path = None
|
| 471 |
+
style_prompt = STYLE_PROMPTS.get(style_choice, "")
|
| 472 |
+
|
| 473 |
+
yield [], None, f"🚀 开始任务,共 {len(nodes)} 个分镜。模型: {model_name}"
|
| 474 |
+
|
| 475 |
+
# 逐个生成分镜
|
| 476 |
+
for i, node_text in enumerate(nodes):
|
| 477 |
+
idx = i + 1
|
| 478 |
+
video_filename = os.path.join(output_dir, f"segment_{idx:02d}.mp4")
|
| 479 |
+
full_prompt = f"{CONTINUITY_PROMPT}\n{style_prompt}\n镜头编号:{idx}/{len(nodes)}。\n镜头脚本:{node_text}"
|
| 480 |
+
|
| 481 |
+
yield generated_videos, None, f"🎥 生成中: 分镜 {idx}/{len(nodes)}..."
|
| 482 |
+
|
| 483 |
+
# 调用 API 生成视频
|
| 484 |
+
if model_name.startswith("sora"):
|
| 485 |
+
err = api_client.generate_video(
|
| 486 |
+
prompt=full_prompt,
|
| 487 |
+
output_path=video_filename,
|
| 488 |
+
model=model_name,
|
| 489 |
+
size=size,
|
| 490 |
+
seconds=seconds,
|
| 491 |
+
ref_img_path=last_frame_path
|
| 492 |
+
)
|
| 493 |
+
else: # veo
|
| 494 |
+
err = api_client.generate_video(
|
| 495 |
+
prompt=full_prompt,
|
| 496 |
+
output_path=video_filename,
|
| 497 |
+
size=size,
|
| 498 |
+
seconds=seconds,
|
| 499 |
+
ref_img_path=last_frame_path
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
if err:
|
| 503 |
+
yield generated_videos, None, f"❌ 分镜 {idx} 失败: {err}"
|
| 504 |
+
return
|
| 505 |
+
|
| 506 |
+
generated_videos.append(video_filename)
|
| 507 |
+
|
| 508 |
+
# 提取最后一帧作为下一个分镜的参考
|
| 509 |
+
if i < len(nodes) - 1:
|
| 510 |
+
frame_path = os.path.join(output_dir, f"ref_{idx:02d}.png")
|
| 511 |
+
last_frame_path = extract_last_frame(video_filename, frame_path)
|
| 512 |
+
|
| 513 |
+
yield generated_videos, None, f"✅ 分镜 {idx} 完成"
|
| 514 |
+
|
| 515 |
+
# 拼接视频
|
| 516 |
+
yield generated_videos, None, "🎬 正在拼接..."
|
| 517 |
+
final_video_path = os.path.join(output_dir, "final_movie.mp4")
|
| 518 |
+
|
| 519 |
+
try:
|
| 520 |
+
stitch_videos(generated_videos, final_video_path)
|
| 521 |
+
yield generated_videos, final_video_path, "🎉 任务完成!"
|
| 522 |
+
except Exception as e:
|
| 523 |
+
yield generated_videos, None, f"❌ 拼接失败: {str(e)}"
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
# ==========================================
|
| 527 |
+
# PART 5: Gradio 界面
|
| 528 |
+
# ==========================================
|
| 529 |
+
|
| 530 |
+
def update_model_params(model_name):
|
| 531 |
+
"""根据模型更新界面参数"""
|
| 532 |
+
config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["sora-2"])
|
| 533 |
+
return (
|
| 534 |
+
gr.Dropdown(
|
| 535 |
+
choices=config["sizes"],
|
| 536 |
+
value=config["sizes"][0],
|
| 537 |
+
label=f"分辨率 ({model_name})"
|
| 538 |
+
),
|
| 539 |
+
gr.Slider(
|
| 540 |
+
minimum=config["seconds_range"]["minimum"],
|
| 541 |
+
maximum=config["seconds_range"]["maximum"],
|
| 542 |
+
step=config["seconds_range"]["step"],
|
| 543 |
+
value=config["seconds_range"]["value"],
|
| 544 |
+
label=config["seconds_label"]
|
| 545 |
+
)
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def get_demo_path(filename):
|
| 550 |
+
"""获取 Demo 文件路径"""
|
| 551 |
+
return filename if os.path.exists(filename) else None
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
# 构建 Gradio 界面
|
| 555 |
+
with gr.Blocks(title="AI 剧本视频工厂") as demo:
|
| 556 |
+
gr.Markdown("# 🎬 ScriptAgent & Sora/Veo 视频生成工坊")
|
| 557 |
+
|
| 558 |
+
with gr.Tabs():
|
| 559 |
+
# --- TAB 1: 剧本创作 ---
|
| 560 |
+
with gr.Tab("📝 第一步:剧本创作"):
|
| 561 |
+
with gr.Row():
|
| 562 |
+
with gr.Column():
|
| 563 |
+
llm_input = gr.Textbox(
|
| 564 |
+
label="剧情输入",
|
| 565 |
+
placeholder="主角:你在做什么?...",
|
| 566 |
+
lines=6
|
| 567 |
+
)
|
| 568 |
+
llm_btn = gr.Button("生成/续写剧本", variant="primary")
|
| 569 |
+
|
| 570 |
+
with gr.Column():
|
| 571 |
+
llm_output = gr.Textbox(
|
| 572 |
+
label="生成的剧本",
|
| 573 |
+
lines=10,
|
| 574 |
+
interactive=True
|
| 575 |
+
)
|
| 576 |
+
to_video_btn = gr.Button("⬇️ 发送到视频生成", variant="secondary")
|
| 577 |
+
|
| 578 |
+
gr.Examples(
|
| 579 |
+
[[
|
| 580 |
+
"主角:你在做什么?指引图是不能随便篡改的。\n"
|
| 581 |
+
"蒋前:篡改指引图?不不不,你误会了。\n"
|
| 582 |
+
"蒋前:我才加入风物家没多久,哪有这个本事能篡改它..."
|
| 583 |
+
]],
|
| 584 |
+
inputs=llm_input
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
# --- TAB 2: 视频生成 ---
|
| 588 |
+
with gr.Tab("🎥 第二步:视频生成"):
|
| 589 |
+
with gr.Row():
|
| 590 |
+
# 左侧配置区
|
| 591 |
+
with gr.Column(scale=1):
|
| 592 |
+
with gr.Accordion("⚙️ API 设置", open=True):
|
| 593 |
+
api_key_input = gr.Textbox(
|
| 594 |
+
label="API Key",
|
| 595 |
+
type="password",
|
| 596 |
+
value="Your API Key",
|
| 597 |
+
info="根据选择的模型输入 OpenAI 或 Google API Key"
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
gr.Markdown("### 🎨 风格与模型配置")
|
| 601 |
+
style_radio = gr.Radio(
|
| 602 |
+
choices=STYLE_KEYS,
|
| 603 |
+
value=STYLE_KEYS[0],
|
| 604 |
+
label="画风"
|
| 605 |
+
)
|
| 606 |
+
model_sel = gr.Dropdown(
|
| 607 |
+
choices=["sora-2", "sora-2-pro", "veo-3.1"],
|
| 608 |
+
value="sora-2",
|
| 609 |
+
label="选择模型",
|
| 610 |
+
info="Sora 使用 OpenAI Key,Veo 使用 Google Key"
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
with gr.Row():
|
| 614 |
+
size_sel = gr.Dropdown(
|
| 615 |
+
choices=MODEL_CONFIGS["sora-2"]["sizes"],
|
| 616 |
+
value=MODEL_CONFIGS["sora-2"]["sizes"][0],
|
| 617 |
+
label="分辨率"
|
| 618 |
+
)
|
| 619 |
+
sec_slider = gr.Slider(
|
| 620 |
+
minimum=4,
|
| 621 |
+
maximum=12,
|
| 622 |
+
step=4,
|
| 623 |
+
value=4,
|
| 624 |
+
label="单镜时长"
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
video_script_input = gr.TextArea(
|
| 628 |
+
label="分镜脚本",
|
| 629 |
+
lines=8,
|
| 630 |
+
placeholder="1. [0-8秒] ..."
|
| 631 |
+
)
|
| 632 |
+
gen_btn = gr.Button("🚀 开始生成", variant="primary")
|
| 633 |
+
status_log = gr.Textbox(label="日志", interactive=False)
|
| 634 |
+
|
| 635 |
+
# 右侧展示区
|
| 636 |
+
with gr.Column(scale=2):
|
| 637 |
+
gr.Markdown("### 🎞️ 分镜预览")
|
| 638 |
+
gallery = gr.Gallery(
|
| 639 |
+
label="分镜序列",
|
| 640 |
+
columns=3,
|
| 641 |
+
height="auto"
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
gr.Markdown("### 🎬 最终成片")
|
| 645 |
+
final_video = gr.Video(label="成片输出")
|
| 646 |
+
|
| 647 |
+
# Demo 展示区(保持不变)
|
| 648 |
+
gr.Markdown("---")
|
| 649 |
+
gr.Markdown("### 🌟 精选成片案例 (Demo Showcase)")
|
| 650 |
+
|
| 651 |
+
# 使用 2x2 布局
|
| 652 |
+
for i in range(0, 4, 2):
|
| 653 |
+
with gr.Row():
|
| 654 |
+
for j in range(2):
|
| 655 |
+
idx = i + j
|
| 656 |
+
if idx < len(DEMO_DATA):
|
| 657 |
+
item = DEMO_DATA[idx]
|
| 658 |
+
with gr.Column():
|
| 659 |
+
# 使用 Group 制造卡片效果
|
| 660 |
+
with gr.Group():
|
| 661 |
+
gr.Video(value=get_demo_path(item["file"]), label=item["title"], interactive=False)
|
| 662 |
+
# 使用 Accordion 折叠剧本
|
| 663 |
+
with gr.Accordion(f"📄 查看剧本: {item['title']}", open=False):
|
| 664 |
+
gr.Textbox(
|
| 665 |
+
value=item["script"],
|
| 666 |
+
show_label=False,
|
| 667 |
+
lines=6,
|
| 668 |
+
max_lines=6,
|
| 669 |
+
interactive=False
|
| 670 |
+
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
# --- 逻辑绑定 ---
|
| 674 |
+
llm_btn.click(chat_with_scriptagent, llm_input, llm_output)
|
| 675 |
+
|
| 676 |
+
to_video_btn.click(
|
| 677 |
+
lambda x: (x, gr.Tabs(selected="🎥 第二步:视频生成")),
|
| 678 |
+
inputs=llm_output,
|
| 679 |
+
outputs=[video_script_input, demo]
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
model_sel.change(
|
| 683 |
+
fn=update_model_params,
|
| 684 |
+
inputs=model_sel,
|
| 685 |
+
outputs=[size_sel, sec_slider]
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
gen_btn.click(
|
| 689 |
+
fn=run_video_generation_pipeline,
|
| 690 |
+
inputs=[
|
| 691 |
+
video_script_input,
|
| 692 |
+
api_key_input,
|
| 693 |
+
model_sel,
|
| 694 |
+
style_radio,
|
| 695 |
+
size_sel,
|
| 696 |
+
sec_slider
|
| 697 |
+
],
|
| 698 |
+
outputs=[gallery, final_video, status_log]
|
| 699 |
+
)
|
| 700 |
+
|
| 701 |
+
if __name__ == "__main__":
|
| 702 |
+
demo.queue()
|
| 703 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
requirement.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
torch
|
| 3 |
+
transformers==4.52.3
|
| 4 |
+
accelerate
|
| 5 |
+
bitsandbytes
|
| 6 |
+
sentencepiece
|
| 7 |
+
protobuf
|
| 8 |
+
scipy
|
| 9 |
+
opencv-python
|
| 10 |
+
moviepy==1.0.3
|
| 11 |
+
numpy
|
| 12 |
+
imageio
|
| 13 |
+
imageio-ffmpeg
|
| 14 |
+
requests
|
| 15 |
+
torchvision
|
| 16 |
+
google
|
| 17 |
+
openai
|
| 18 |
+
google-genai
|
| 19 |
+
ms-swift
|
| 20 |
+
qwen-omni-utils
|
| 21 |
+
soundfile
|