目录
第一部分:基石搭建 - 环境配置与模型API调用
步骤 1.1:硬件与软件准备
步骤 1.2:在LM Studio中下载并配置模型
步骤 1.3:编写Python脚本测试API调用
第二部分:核心引擎 - 视频序列的生成与拼接
步骤 2.1:设计并创建项目结构
步骤 2.2:封装模型接口 (src/model_interface.py)
步骤 2.3:开发批量生成逻辑 (src/generator.py)
步骤 2.4:实现基础视频拼接 (src/editor.py)
步骤 2.5:整合所有部分 (main.py)
第三部分:智能升华 - 增强视频的连贯性
步骤 3.1:视觉连贯性 - 添加平滑的交叉溶解过渡
步骤 3.2:语义连贯性 - 集成本地LLM智能拆解提示词
第四部分:成品封装 - 用户界面、稳健性与未来展望
步骤 4.1:构建图形用户界面 (GUI) - 使用 Gradio
步骤 4.2:增强稳健性与日志记录
步骤 4.3:创建项目文档
步骤 4.4:总结与未来展望
这是整个项目的基础,如果这一步不稳固,后续的一切都无法进行。请务必仔细操作。
Wan2.2。你会看到几个结果。我们需要下载以下两个模型(至少一个):
Wan2.2-T2V-A14B-GGUF (Text-to-Video)
Wan2.2-I2V-A14B-GGUF (Image-to-Video) 点击其中一个模型,进入下载页面。你会看到不同量化版本的文件。
推荐: 选择
Q4_K_M 版本。它在质量和性能之间取得了很好的平衡。如果你的显存充足(>24GB): 可以选择
Q5_K_M 或
Q8_0 以获得更好质量。如果你的显存紧张(<12GB): 可以尝试
Q3_K_M,但质量会下降。 点击 Download 按钮,等待下载完成。这可能需要一些时间。 加载模型并启动服务器:
在LM Studio主界面顶部,点击 “Chat” (💬) 标签页。在模型选择下拉菜单中,选择你刚刚下载的模型,例如
Wan2.2-T2V-A14B-GGUF。在界面右侧,找到 “Server” 选项卡。确保 “Enable Server” 开关是打开的。记录下 “Host” 和 “Port”。默认通常是
http://localhost:1234。关键一步: 在 “CORS” 设置下方,找到 “Custom Server Preset” 或类似选项。Wan2.2这类多模态模型通常不使用标准的
/v1/chat/completions端点。你需要查看模型信息或尝试不同的端点。通常,它可能是
/v1/images/generations 或一个自定义路径。如果不确定,先保持默认,我们将在Python脚本中进行调试。
这是验证环境是否成功的“Hello, World!”时刻。
创建项目文件夹: 在你的电脑上创建一个新文件夹,例如
video_agent。安装Python库: 打开你的终端(Windows上是CMD或PowerShell,macOS/Linux上是Terminal),进入项目文件夹,然后运行:
pip install requests
创建测试脚本: 在
video_agent 文件夹中,创建一个名为
test_api.py 的文件,并粘贴以下代码:
import requests
import json
import time
# --- 配置区 ---
# LM Studio本地服务器地址,请根据你的实际情况修改
API_URL = "http://localhost:1234/v1/images/generations"
# 这是一个常见的端点,如果不行,请查看LM Studio中模型页面的"Developer"标签页获取正确端点
# 请求头
HEADERS = {
"Content-Type": "application/json",
}
def test_t2v_generation(prompt: str, output_filename: str = "test_output.mp4"):
"""
测试调用Wan2.2 T2V模型生成视频
"""
print(f"🚀 正在向 '{API_URL}' 发送请求...")
print(f"📝 提示词: '{prompt}'")
# --- 请求体 ---
# !!! 重要:这个结构是基于常见API的猜测,可能需要根据LM Studio的实际API进行调整 !!!
# 请在LM Studio的模型页面 -> "Developer"标签页查看示例请求体
payload = {
"model": "Wan2.2-T2V-A14B-GGUF", # 模型标识符,通常是你加载的模型名
"prompt": prompt,
# 以下参数是可选的,你可以根据需要添加或修改
# "n": 1, # 生成视频的数量
# "size": "832x480", # 视频分辨率
# "steps": 30, # 推理步数,越多越慢但质量可能越好
# "cfg_scale": 7.5, # 引导系数
}
try:
# 发送POST请求
response = requests.post(API_URL, headers=HEADERS, json=payload, timeout=300) # 设置5分钟超时
# 检查响应状态码
if response.status_code == 200:
print("✅ 请求成功!正在处理响应...")
result = response.json()
# --- 响应处理 ---
# !!! 这是最需要根据实际情况修改的部分 !!!
# API可能返回视频的base64编码,或者一个下载链接,或者直接是二进制流
# 这里我们假设它返回一个包含URL的JSON对象
if 'data' in result and len(result['data']) > 0 and 'url' in result['data'][0]:
video_url = result['data'][0]['url']
print(f"🔗 视频生成URL: {video_url}")
# 下载视频文件
print("⬇️ 正在下载视频...")
video_response = requests.get(video_url, stream=True)
if video_response.status_code == 200:
with open(output_filename, 'wb') as f:
for chunk in video_response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"🎉 视频已成功保存为: {output_filename}")
else:
print(f"❌ 下载视频失败: {video_response.status_code}")
else:
print("❌ 响应格式不符合预期,请检查API返回的JSON结构:")
print(json.dumps(result, indent=2))
else:
print(f"❌ API请求失败,状态码: {response.status_code}")
print("错误信息:", response.text)
except requests.exceptions.RequestException as e:
print(f"❌ 网络请求出错: {e}")
except json.JSONDecodeError:
print("❌ 解析响应JSON失败,服务器可能返回了非JSON格式的错误信息。")
print("原始响应:", response.text)
if __name__ == '__main__':
# 确保LM Studio已加载模型并启动了服务器
input("请确保LM Studio已准备就绪,然后按Enter键开始测试...")
test_prompt = "A majestic lion walking slowly across the African savanna at sunset."
test_t2v_generation(test_prompt, "lion_savanna.mp4")
运行与调试:
在终端中,确保你仍在
video_agent 文件夹内。运行脚本:
python test_api.py如果成功: 你会看到一系列打印信息,最终在文件夹中出现一个名为
lion_savanna.mp4 的视频文件。恭喜你,基础环境搭建成功!如果失败(最常见的情况):
404 Not Found: 说明API端点
API_URL 错误。请回到LM Studio,仔细检查模型页面的 “Developer” 标签页,找到正确的Endpoint URL,并更新
test_api.py 中的
API_URL。
500 Internal Server Error: 可能是模型未完全加载,或请求体格式
payload 不正确。检查LM Studio主界面,确保模型加载完成。然后,尝试简化
payload,只保留
prompt 和
model 字段。连接被拒绝: 确认LM Studio的服务器已启动,并且端口号
1234 正确。第一部分到此结束。当你成功运行
test_api.py并得到视频文件后,就可以继续前往第二部分了。
在这一部分,我们将把第一部分的单次调用代码,重构为一个结构化、可复用的系统。我们将实现从一系列文本/图片输入,到生成一系列视频片段,再到将它们拼接成一个完整视频的完整流程。
一个好的项目结构能让代码更清晰、更易于维护。请在你的
video_agent 文件夹中,创建以下目录和文件:
video_agent/
├── src/ # 存放我们的源代码
│ ├── __init__.py # 使src成为一个Python包
│ ├── model_interface.py # 封装与LM Studio的API交互
│ ├── generator.py # 负责批量生成视频片段
│ └── editor.py # 负责视频的拼接与编辑
├── temp/ # 存放临时生成的视频片段
├── output/ # 存放最终的成品视频
├── config.py # 存放配置信息(如API地址)
└── main.py # 主程序入口,用于测试和整合
创建方法:
Windows: 在文件资源管理器中手动创建文件夹,并创建空白的
.py文件。macOS/Linux: 在终端中运行以下命令:
cd video_agent
mkdir -p src temp output
touch src/__init__.py src/model_interface.py src/generator.py src/editor.py config.py main.py
src/model_interface.py)我们将把第一部分
test_api.py 的核心逻辑抽象成一个类,方便后续调用。
pip install moviepy
编写
config.py: 将配置项分离出来,便于管理。
# config.py
# LM Studio API配置
LM_STUDIO_HOST = "http://localhost"
LM_STUDIO_PORT = "1234"
# 根据你在LM Studio中确认的端点进行修改
# T2V (Text-to-Video) 端点
T2V_ENDPOINT = f"{LM_STUDIO_HOST}:{LM_STUDIO_PORT}/v1/images/generations"
# I2V (Image-to-Video) 端点 (可能和T2V相同,也可能不同,需确认)
I2V_ENDPOINT = f"{LM_STUDIO_HOST}:{LM_STUDIO_PORT}/v1/images/generations"
# 模型名称
T2V_MODEL_NAME = "Wan2.2-T2V-A14B-GGUF"
I2V_MODEL_NAME = "Wan2.2-I2V-A14B-GGUF"
编写
src/model_interface.py:
# src/model_interface.py
import requests
import json
import os
from typing import Optional
# 导入配置
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import T2V_ENDPOINT, I2V_ENDPOINT, T2V_MODEL_NAME, I2V_MODEL_NAME
class ModelInterface:
"""封装与LM Studio中Wan2.2模型的API交互"""
def __init__(self):
self.headers = {"Content-Type": "application/json"}
# 注意:Wan2.2的API可能需要特定的请求体格式,这里的结构是基于常见API的推断
# 如果后续调用失败,请首先检查这里的payload结构是否与LM Studio开发者文档一致
self.default_payload = {
"steps": 30, # 推理步数,可根据性能调整
"cfg_scale": 7.5, # 引导系数
}
def _make_request(self, endpoint: str, payload: dict) -> Optional[dict]:
"""发送POST请求并处理通用响应"""
try:
response = requests.post(endpoint, headers=self.headers, json=payload, timeout=300)
response.raise_for_status() # 如果状态码不是2xx,则抛出HTTPError
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ API请求失败: {e}")
return None
except json.JSONDecodeError:
print("❌ 解析响应JSON失败。")
print("原始响应:", response.text)
return None
def generate_video_from_text(self, prompt: str, output_path: str) -> bool:
"""
使用T2V模型根据文本生成视频
:param prompt: 文本提示词
:param output_path: 视频保存的完整路径
:return: 是否成功
"""
print(f"🎬 [T2V] 生成中: '{prompt}'")
payload = {
"model": T2V_MODEL_NAME,
"prompt": prompt,
**self.default_payload
}
result = self._make_request(T2V_ENDPOINT, payload)
if result and 'data' in result and len(result['data']) > 0:
# 假设API返回一个包含视频URL的JSON对象
video_url = result['data'][0].get('url')
if video_url:
try:
video_response = requests.get(video_url, stream=True)
video_response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in video_response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"✅ [T2V] 成功保存到: {output_path}")
return True
except Exception as e:
print(f"❌ [T2V] 下载或保存视频失败: {e}")
else:
print("❌ [T2V] 响应中未找到视频URL。")
print("响应内容:", json.dumps(result, indent=2))
return False
def generate_video_from_image(self, image_path: str, prompt: str, output_path: str) -> bool:
"""
使用I2V模型根据图片和文本生成视频
:param image_path: 输入图片的路径
:param prompt: 文本提示词 (用于指导运动)
:param output_path: 视频保存的完整路径
:return: 是否成功
"""
# !!! 注意:I2V的API调用方式可能与T2V不同,特别是如何传递图片数据 !!!
# 以下是两种可能的方式,你需要根据LM Studio的实际API进行调整
print(f"🎬 [I2V] 生成中: 图片 '{os.path.basename(image_path)}' + 提示词 '{prompt}'")
# 方式一:如果API接受本地图片路径 (可能性较小)
# payload = {
# "model": I2V_MODEL_NAME,
# "image_path": image_path,
# "prompt": prompt,
# **self.default_payload
# }
# 方式二:如果API接受base64编码的图片数据 (可能性较大)
import base64
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
payload = {
"model": I2V_MODEL_NAME,
"image": base64_image, # 字段名可能是 "image" 或 "image_data"
"prompt": prompt,
**self.default_payload
}
result = self._make_request(I2V_ENDPOINT, payload)
if result and 'data' in result and len(result['data']) > 0:
video_url = result['data'][0].get('url')
if video_url:
try:
video_response = requests.get(video_url, stream=True)
video_response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in video_response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"✅ [I2V] 成功保存到: {output_path}")
return True
except Exception as e:
print(f"❌ [I2V] 下载或保存视频失败: {e}")
else:
print("❌ [I2V] 响应中未找到视频URL。")
print("响应内容:", json.dumps(result, indent=2))
return False
src/generator.py)这个模块负责接收一个输入列表,并调用
ModelInterface 来生成一系列视频片段。
# src/generator.py
import os
from typing import List, Union
from .model_interface import ModelInterface
class VideoSequenceGenerator:
"""负责批量生成视频序列"""
def __init__(self, model_interface: ModelInterface, temp_dir: str = "temp"):
self.interface = model_interface
self.temp_dir = temp_dir
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
print(f"📁 创建临时目录: {self.temp_dir}")
def generate_from_prompts(self, prompts: List[str]) -> List[str]:
"""
根据文本提示词列表生成视频片段
:param prompts: 文本提示词列表
:return: 成功生成的视频片段路径列表
"""
generated_paths = []
print(f"
--- 开始批量生成 {len(prompts)} 个视频片段 ---")
for i, prompt in enumerate(prompts):
# 生成带序号的文件名,如 clip_001.mp4, clip_002.mp4
filename = f"clip_{i+1:03d}.mp4"
output_path = os.path.join(self.temp_dir, filename)
success = self.interface.generate_video_from_text(prompt, output_path)
if success:
generated_paths.append(output_path)
else:
print(f"⚠️ 跳过片段 {i+1},因为生成失败。")
print(f"--- 批量生成完成,共成功 {len(generated_paths)} 个片段 ---
")
return generated_paths
def generate_from_images(self, image_paths: List[str], prompts: List[str]) -> List[str]:
"""
根据图片路径和提示词列表生成视频片段
:param image_paths: 图片路径列表
:param prompts: 对应的文本提示词列表
:return: 成功生成的视频片段路径列表
"""
if len(image_paths) != len(prompts):
print("❌ 错误:图片列表和提示词列表的长度必须相同。")
return []
generated_paths = []
print(f"
--- 开始批量生成 {len(image_paths)} 个I2V视频片段 ---")
for i, (img_path, prompt) in enumerate(zip(image_paths, prompts)):
filename = f"clip_{i+1:03d}.mp4"
output_path = os.path.join(self.temp_dir, filename)
success = self.interface.generate_video_from_image(img_path, prompt, output_path)
if success:
generated_paths.append(output_path)
else:
print(f"⚠️ 跳过片段 {i+1},因为生成失败。")
print(f"--- I2V批量生成完成,共成功 {len(generated_paths)} 个片段 ---
")
return generated_paths
src/editor.py)现在我们有了视频片段,让我们用
MoviePy 把它们串起来。
# src/editor.py
import os
from moviepy.editor import VideoFileClip, concatenate_videoclips
from typing import List
class VideoEditor:
"""负责视频的拼接与后期处理"""
def __init__(self, output_dir: str = "output"):
self.output_dir = output_dir
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
print(f"📁 创建输出目录: {self.output_dir}")
def concatenate_clips(self, clip_paths: List[str], final_filename: str, method: str = "compose"):
"""
将视频片段列表拼接成一个视频
:param clip_paths: 视频片段路径列表
:param final_filename: 最终输出文件名 (e.g., "my_story.mp4")
:param method: "compose" (兼容不同尺寸) 或 "chain" (更快,要求尺寸相同)
:return: 最终视频的完整路径
"""
if not clip_paths:
print("❌ 没有可拼接的视频片段。")
return None
print(f"
--- 开始拼接 {len(clip_paths)} 个视频片段 ---")
try:
clips = [VideoFileClip(path) for path in clip_paths]
# 使用 "compose" 方法,它能处理不同分辨率的片段
final_clip = concatenate_videoclips(clips, method=method)
output_path = os.path.join(self.output_dir, final_filename)
final_clip.write_videofile(
output_path,
codec="libx264", # 常用且兼容性好的视频编码
audio_codec="aac", # 常用且兼容性好的音频编码
fps=24 # 帧率,可根据需要调整
)
# 释放内存
for clip in clips:
clip.close()
final_clip.close()
print(f"✅ 视频拼接成功!最终文件: {output_path}")
return output_path
except Exception as e:
print(f"❌ 视频拼接失败: {e}")
return None
def cleanup_temp_clips(self, clip_paths: List[str]):
"""清理临时视频片段以释放磁盘空间"""
print("
--- 清理临时文件 ---")
for path in clip_paths:
try:
os.remove(path)
print(f"🗑️ 已删除: {path}")
except OSError as e:
print(f"⚠️ 删除文件 {path} 失败: {e}")
main.py)现在,我们来编写一个主程序,测试我们刚刚构建的整个流程。
# main.py
import os
import sys
# 将src目录添加到Python路径,以便导入模块
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
from model_interface import ModelInterface
from generator import VideoSequenceGenerator
from editor import VideoEditor
def main():
"""主函数,整合所有功能"""
print("--- 视频故事编织者 v0.1 - 核心引擎测试 ---")
# 1. 初始化组件
model_interface = ModelInterface()
generator = VideoSequenceGenerator(model_interface)
editor = VideoEditor()
# 2. 定义输入:一个包含多个场景的提示词列表
story_prompts = [
"A close-up shot of a single, tiny seed on dark, fertile soil.",
"A time-lapse of the seed sprouting, a small green shoot emerging.",
"The sprout grows into a healthy young plant with a few leaves, under gentle sunlight.",
"The plant matures into a large, strong tree with sprawling branches, in a field.",
"The seasons change rapidly: leaves turn red and fall, snow covers the branches, then new green leaves appear.",
"Finally, a small bird flies in and builds a nest in the tree's branches."
]
# 3. 生成视频片段
generated_clip_paths = generator.generate_from_prompts(story_prompts)
# 4. 拼接视频
if generated_clip_paths:
final_video_path = editor.concatenate_clips(
generated_clip_paths,
final_filename="a_tree_life_story.mp4"
)
# 5. (可选) 清理临时文件
if final_video_path:
editor.cleanup_temp_clips(generated_clip_paths)
print(f"
🎉 全部流程完成!请查看 {final_video_path}")
else:
print("
💔 由于没有生成任何视频片段,流程终止。")
if __name__ == '__main__':
# 确保LM Studio已加载T2V模型并启动服务器
input("请确认LM Studio已准备就绪,然后按Enter键开始...")
main()
运行与验证
确保LM Studio运行: 回到LM Studio,确保
Wan2.2-T2V-A14B-GGUF 模型已加载,服务器已启动。运行主程序: 在你的终端(位于
video_agent 根目录)中,运行:
python main.py
观察过程: 你将看到程序逐一调用模型生成6个视频片段,并将它们保存在
temp 文件夹中。生成完成后,它会自动将这些片段拼接成一个完整的视频,保存在
output 文件夹中,并删除
temp 文件夹中的临时文件。这一部分是项目的“灵魂”,我们将从两个维度提升视频质量:
视觉连贯性: 解决片段之间生硬的跳切问题,添加平滑的过渡效果。语义连贯性: 引入另一个本地LLM,将用户的简单故事大纲自动“翻译”成适合AI视频生成的、连续的详细提示词。硬切虽然直接,但会显得突兀。交叉溶解是最常用且有效的平滑过渡方式。
修改
src/editor.py: 我们将在
VideoEditor 类中添加一个新方法,专门用于处理带过渡效果的拼接。
src/editor.py 文件,在
concatenate_clips 方法后面添加以下代码:
# src/editor.py (在现有代码后添加)
from moviepy.editor import vfx # 导入视频效果模块
# ... (保留 VideoEditor 类和 __init__, concatenate_clips, cleanup_temp_clips 方法) ...
def concatenate_clips_with_crossfade(self, clip_paths: List[str], final_filename: str, fade_duration: float = 1.0):
"""
将视频片段使用交叉溶解效果拼接
:param clip_paths: 视频片段路径列表
:param final_filename: 最终输出文件名
:param fade_duration: 过渡效果的持续时间(秒)
:return: 最终视频的完整路径
"""
if not clip_paths:
print("❌ 没有可拼接的视频片段。")
return None
if len(clip_paths) < 2:
print("⚠️ 只有一个片段,无需添加过渡,将使用普通拼接。")
return self.concatenate_clips(clip_paths, final_filename)
print(f"
--- 开始使用交叉溶解拼接 {len(clip_paths)} 个片段 (过渡时长: {fade_duration}s) ---")
try:
clips = [VideoFileClip(path) for path in clip_paths]
# --- 核心逻辑:为每个片段添加淡入淡出效果 ---
final_clips = []
for i, clip in enumerate(clips):
# 第一个片段不需要淡入
if i > 0:
clip = clip.crossfadein(fade_duration)
# 最后一个片段不需要淡出
if i < len(clips) - 1:
clip = clip.crossfadeout(fade_duration)
final_clips.append(clip)
# --- 关键参数:padding ---
# concatenate_videoclips 会将片段按顺序放置。
# 设置 padding 为负数,意味着下一个片段会提前开始播放,与当前片段的结尾重叠。
# 重叠的时间正好是 fade_duration,这样就实现了平滑的交叉溶解。
final_clip = concatenate_videoclips(final_clips, padding=-fade_duration, method="compose")
output_path = os.path.join(self.output_dir, final_filename)
final_clip.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
fps=24
)
# 释放内存
for clip in clips:
clip.close()
final_clip.close()
print(f"✅ 带过渡效果的视频拼接成功!最终文件: {output_path}")
return output_path
except Exception as e:
print(f"❌ 视频拼接失败: {e}")
return None
更新
main.py 进行测试: 修改
main.py,调用我们新创建的
concatenate_clips_with_crossfade 方法。
# main.py (修改 main 函数)
# ... (保留所有 import 语句) ...
def main():
"""主函数,整合所有功能"""
print("--- 视频故事编织者 v0.2 - 智能过渡测试 ---")
# 1. 初始化组件
model_interface = ModelInterface()
generator = VideoSequenceGenerator(model_interface)
editor = VideoEditor()
# 2. 定义输入
story_prompts = [
"A close-up shot of a single, tiny seed on dark, fertile soil.",
"A time-lapse of the seed sprouting, a small green shoot emerging.",
"The sprout grows into a healthy young plant with a few leaves, under gentle sunlight.",
"The plant matures into a large, strong tree with sprawling branches, in a field.",
"The seasons change rapidly: leaves turn red and fall, snow covers the branches, then new green leaves appear.",
"Finally, a small bird flies in and builds a nest in the tree's branches."
]
# 3. 生成视频片段
generated_clip_paths = generator.generate_from_prompts(story_prompts)
# 4. 拼接视频 (使用新的交叉溶解方法)
if generated_clip_paths:
final_video_path = editor.concatenate_clips_with_crossfade(
generated_clip_paths,
final_filename="a_tree_life_story_fade.mp4",
fade_duration=1.5 # 设置1.5秒的过渡时间
)
# 5. (可选) 清理临时文件
if final_video_path:
editor.cleanup_temp_clips(generated_clip_paths)
print(f"
🎉 全部流程完成!请查看 {final_video_path}")
else:
print("
💔 由于没有生成任何视频片段,流程终止。")
# ... (保留 if __name__ == '__main__': 部分) ...
运行并观察效果:
确保LM Studio的T2V模型服务器仍在运行。在终端运行
python main.py。当视频生成完毕后,对比
output 文件夹中的
a_tree_life_story.mp4(第二部分生成的)和
a_tree_life_story_fade.mp4(刚刚生成的)。你会明显感觉到新版本的视频片段之间的切换变得非常平滑。让用户写6个详细的提示词还是有些麻烦。我们希望用户只需提供一个故事大纲,AI就能自动生成这些提示词。
准备第二个模型和服务器: 下载文本模型: 打开LM Studio,搜索并下载一个轻量级但能力强的文本模型,例如
Llama-3-8B-Instruct-GGUF。选择
Q4_K_M 版本即可。启动第二个服务器: 为了避免与Wan2.2模型冲突,我们需要为这个文本模型使用一个不同的端口。
在LM Studio中,加载
Llama-3-8B-Instruct-GGUF 模型。进入右侧的 “Server” 选项卡。将 “Port” 从
1234 修改为
1235 (或其他未被占用的端口)。确保 “Enable Server” 是开启的。重要: 记录下这个文本模型的服务器地址:
http://localhost:1235。这个模型使用标准的OpenAI兼容API,端点是
/v1/chat/completions。 创建LLM客户端 (
src/llm_client.py): 为了代码整洁,我们创建一个新模块专门负责与文本LLM交互。
src 文件夹中创建新文件
llm_client.py,并添加以下代码:
# src/llm_client.py
import requests
import json
import sys
import os
# 导入配置,并添加文本LLM的配置
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import LM_STUDIO_HOST, LM_STUDIO_PORT
# 文本LLM的配置
TEXT_LLM_ENDPOINT = f"{LM_STUDIO_HOST}:1235/v1/chat/completions" # 使用我们设置的端口1235
TEXT_LLM_MODEL_NAME = "Llama-3-8B-Instruct-GGUF" # 确保名称与LM Studio中一致
class LLMClient:
"""封装与本地文本LLM的交互"""
def __init__(self):
self.headers = {"Content-Type": "application/json"}
def generate_prompts_from_outline(self, story_outline: str, num_scenes: int = 6) -> list[str]:
"""
根据故事大纲,使用LLM生成一系列详细的视频提示词
:param story_outline: 用户输入的故事大纲
:param num_scenes: 希望生成的场景数量
:return: 生成的提示词列表
"""
print(f"🤖 [LLM] 正在根据大纲生成 {num_scenes} 个场景的提示词...")
# --- 精心设计的提示词 ---
# 这是保证生成质量的关键
system_prompt = f"""
You are an expert AI video prompt engineer. Your task is to break down a user's story outline into {num_scenes} distinct, visually rich, and sequential prompts for a text-to-video AI model.
Rules to follow:
1. Each prompt must describe a single, clear scene.
2. Focus on visual details: camera angles (e.g., close-up, wide shot), lighting, colors, and character actions.
3. Ensure the scenes are logically connected and form a coherent narrative.
4. The output must be a JSON list of strings. Each string is one prompt.
5. Do not add any explanations or text outside the JSON list.
Example Input Outline: "A robot wakes up in a factory, looks at the sunset, and then escapes."
Example Output JSON: ["A close-up of a robot's eye flickering to life in a dim, futuristic factory.", "The robot's point of view, looking out a large window at a beautiful, orange sunset.", "A wide shot of the robot smashing through the factory door and running into the outside world."]
"""
user_prompt = f"Story Outline: {story_outline}"
payload = {
"model": TEXT_LLM_MODEL_NAME,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"temperature": 0.7, # 增加一点创造性
"max_tokens": 1024
}
try:
response = requests.post(TEXT_LLM_ENDPOINT, headers=self.headers, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content']
# 尝试解析JSON格式的输出
prompts = json.loads(content)
if isinstance(prompts, list):
print("✅ [LLM] 成功生成提示词列表。")
return prompts
else:
print("❌ [LLM] 返回的内容不是有效的列表格式。")
print("返回内容:", content)
return []
except requests.exceptions.RequestException as e:
print(f"❌ [LLM] API请求失败: {e}")
print("请确保文本LLM服务器已在端口1235上运行。")
return []
except json.JSONDecodeError:
print("❌ [LLM] 无法解析返回的JSON。")
print("原始返回内容:", content)
return []
except KeyError:
print("❌ [LLM] API响应格式不正确。")
print("原始响应:", json.dumps(result, indent=2))
return []
最终整合
main.py: 现在,我们将所有功能整合到
main.py 中,并使用
argparse 来让用户选择是直接提供提示词,还是提供大纲。
main.py 的内容如下:
# main.py
import os
import sys
import argparse # 导入argparse
# 将src目录添加到Python路径
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
from model_interface import ModelInterface
from generator import VideoSequenceGenerator
from editor import VideoEditor
from llm_client import LLMClient # 导入LLM客户端
def main():
"""主函数,整合所有功能"""
parser = argparse.ArgumentParser(description="视频故事编织者 v0.3 - 智能体完整版")
group = parser.add_mutually_exclusive_group(required=True)
# 方式一:直接提供提示词列表
group.add_argument(
'--prompts',
nargs='+',
help='直接指定一个或多个文本提示词。用空格分隔。如果提示词包含空格,请用引号括起来。'
)
# 方式二:提供故事大纲,由LLM生成提示词
group.add_argument(
'--outline',
type=str,
help='提供一个故事大纲,由AI自动拆解成详细的提示词。'
)
# 其他参数
parser.add_argument(
'--scenes',
type=int,
default=6,
help='当使用 --outline 时,指定要生成的场景数量。默认为6。'
)
parser.add_argument(
'--fade',
type=float,
default=1.5,
help='视频片段之间的过渡时长(秒)。设置为0则无过渡。默认为1.5。'
)
parser.add_argument(
'--output',
type=str,
default="final_story.mp4",
help='最终输出视频的文件名。'
)
args = parser.parse_args()
print("--- 视频故事编织者 v0.3 - 智能体完整版 ---")
# 1. 初始化组件
model_interface = ModelInterface()
generator = VideoSequenceGenerator(model_interface)
editor = VideoEditor()
llm_client = LLMClient()
# 2. 准备提示词
final_prompts = []
if args.outline:
print(f"
📝 用户输入大纲: '{args.outline}'")
final_prompts = llm_client.generate_prompts_from_outline(args.outline, args.scenes)
if not final_prompts:
print("💔 无法从大纲生成提示词,程序退出。")
return
else: # args.prompts
final_prompts = args.prompts
print("
🎬 即将根据以下提示词生成视频:")
for i, p in enumerate(final_prompts):
print(f" {i+1}. {p}")
# 3. 生成视频片段
generated_clip_paths = generator.generate_from_prompts(final_prompts)
# 4. 拼接视频
if generated_clip_paths:
# 根据用户是否设置过渡时长来选择拼接方法
if args.fade > 0:
final_video_path = editor.concatenate_clips_with_crossfade(
generated_clip_paths,
final_filename=args.output,
fade_duration=args.fade
)
else:
final_video_path = editor.concatenate_clips(
generated_clip_paths,
final_filename=args.output
)
# 5. 清理临时文件
if final_video_path:
editor.cleanup_temp_clips(generated_clip_paths)
print(f"
🎉 全部流程完成!请查看 {final_video_path}")
else:
print("
💔 由于没有生成任何视频片段,流程终止。")
if __name__ == '__main__':
# 运行前提醒用户检查服务器
print("请确保以下服务已准备就绪:")
print("1. LM Studio: Wan2.2-T2V 模型在端口 1234 上运行。")
print("2. LM Studio: Llama-3-8B-Instruct 模型在端口 1235 上运行。")
input("
按Enter键开始...")
main()
运行与验证智能体
现在,你的智能体已经具备了完整的智能。你可以通过两种方式使用它:
使用大纲(推荐):
python main.py --outline "A lonely astronaut on Mars discovers a strange, glowing plant that grows rapidly when he waters it." --scenes 4 --fade 2.0 --output astronaut_plant.mp4
你会看到LLM首先将大纲拆解成4个场景的提示词,然后依次生成视频,最后用2秒的过渡效果拼接起来。直接使用提示词:
python main.py --prompts "A golden retriever puppy sleeping in a basket." "The puppy wakes up and yawns." "The puppy stumbles out of the basket and plays with a ball." --fade 1.0 --output puppy_story.mp4
第三部分完成! 恭喜你,你已经成功构建了一个具备智能流程编排、内容理解和后期处理能力的AI视频生成智能体。它已经远远超出了一个简单的模型调用脚本。
在这一部分,我们将完成以下工作:
构建图形用户界面 (GUI): 使用 Gradio 创建一个简单的Web界面,让非技术用户也能轻松使用。增强稳健性与日志记录: 让程序更健壮,错误信息更清晰。创建项目文档: 编写
README.md 和
requirements.txt,使其成为一个标准化的项目。总结与展望: 回顾整个项目,并提出未来的优化方向。
命令行虽然强大,但图形界面更直观。Gradio 是一个用于快速创建机器学习UI的Python库,非常适合我们的项目。
安装 Gradio:
pip install gradio
创建
src/gui.py: 我们将把所有GUI相关的逻辑放在这个新文件中。这个文件将整合我们之前创建的所有类。
# src/gui.py
import gradio as gr
import os
import sys
import time
import threading
# 导入我们自己的模块
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.model_interface import ModelInterface
from src.generator import VideoSequenceGenerator
from src.editor import VideoEditor
from src.llm_client import LLMClient
# --- 全局变量,用于在Gradio的会话中保持状态 ---
# 注意:对于更复杂的应用,应使用gr.State(),但这里为了简化,我们先用全局变量
model_interface = ModelInterface()
generator = VideoSequenceGenerator(model_interface)
editor = VideoEditor()
llm_client = LLMClient()
def generate_video_wrapper(
mode: str,
outline: str,
prompts: str,
scenes: int,
fade_duration: float,
output_filename: str,
progress=gr.Progress()
):
"""
Gradio调用的核心生成函数,包含进度跟踪
"""
try:
# 1. 准备提示词
final_prompts = []
if mode == "从大纲生成":
progress(0.1, desc="正在由LLM生成详细提示词...")
final_prompts = llm_client.generate_prompts_from_outline(outline, scenes)
if not final_prompts:
return "错误:无法从大纲生成提示词,请检查LLM服务器。", None
else: # "从提示词生成"
# 将用户输入的多行文本分割成列表
final_prompts = [p.strip() for p in prompts.split('
') if p.strip()]
if not final_prompts:
return "错误:请至少输入一个有效的提示词。", None
# 2. 生成视频片段
progress(0.2, desc="开始生成视频片段...")
generated_clip_paths = generator.generate_from_prompts(final_prompts)
if not generated_clip_paths:
return "错误:未能生成任何视频片段,请检查T2V模型服务器。", None
# 3. 拼接视频
progress(0.8, desc="正在拼接视频并添加过渡效果...")
if fade_duration > 0:
final_video_path = editor.concatenate_clips_with_crossfade(
generated_clip_paths,
final_filename=output_filename,
fade_duration=fade_duration
)
else:
final_video_path = editor.concatenate_clips(
generated_clip_paths,
final_filename=output_filename
)
# 4. 清理临时文件
if final_video_path:
progress(0.95, desc="清理临时文件...")
editor.cleanup_temp_clips(generated_clip_paths)
progress(1.0, desc="完成!")
return f"✅ 视频生成成功!文件已保存为: {final_video_path}", final_video_path
else:
return "错误:视频拼接失败。", None
except Exception as e:
# 捕获所有未预见的错误
return f"发生未知错误: {e}", None
# --- Gradio 界面定义 ---
with gr.Blocks(title="视频故事编织者", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎬 视频故事编织者")
gr.Markdown("一个基于本地开源模型的AI视频生成智能体。请确保LM Studio中的Wan2.2-T2V和Llama-3模型已分别在端口1234和1235上运行。")
with gr.Tabs():
with gr.TabItem("📝 从大纲生成"):
with gr.Row():
with gr.Column():
outline_input = gr.Textbox(
label="故事大纲",
placeholder="例如:一只小猫在雨天迷路了,最后被一个善良的小女孩收养。",
lines=3
)
scenes_input = gr.Slider(
minimum=3, maximum=10, value=6, step=1,
label="生成场景数量"
)
with gr.Column():
# 这里可以放一个示例大纲的按钮
pass
with gr.TabItem("🎭 从提示词生成"):
prompts_input = gr.Textbox(
label="视频提示词 (每行一个)",
placeholder="A cat walking in the rain.
A girl finds the cat under a bench.
The girl holds an umbrella for the cat.",
lines=5
)
# --- 通用控制面板 ---
with gr.Row():
with gr.Column(scale=2):
fade_input = gr.Slider(
minimum=0, maximum=3, value=1.5, step=0.1,
label="过渡时长 (秒, 0为无过渡)"
)
output_name_input = gr.Textbox(
label="输出文件名",
value="my_ai_story.mp4"
)
with gr.Column(scale=1):
generate_btn = gr.Button("🚀 生成视频", variant="primary", size="lg")
# --- 输出区域 ---
with gr.Row():
status_output = gr.Textbox(label="状态", interactive=False)
with gr.Row():
video_output = gr.Video(label="生成的视频")
# --- 绑定事件 ---
generate_btn.click(
fn=generate_video_wrapper,
inputs=[
gr.Textbox(visible=False), # 用于传递mode的隐藏组件
outline_input,
prompts_input,
scenes_input,
fade_input,
output_name_input
],
outputs=[status_output, video_output],
# 根据选中的Tab来决定传递哪个mode
_js="(outline, prompts) => { var tabs = document.querySelectorAll('button[role="tab"]'); for(var i = 0; i < tabs.length; i++){ if(tabs[i].classList.contains('border-b-2')){ return i == 0 ? 'outline' : 'prompts'; } } }"
)
# 修正事件绑定,使其更简单可靠
# 我们需要知道用户当前在哪个Tab
def get_active_tab():
# 这个JS函数有点复杂,我们换一种更简单的方式
# 我们让两个Tab各有一个生成按钮
pass
# 重新设计UI,让每个Tab有自己的按钮,逻辑更清晰
# (为了教程的清晰性,我将重写上面的UI部分)
# ... (省略上面的UI代码,直接写最终版本) ...
# --- 最终版 Gradio 界面定义 ---
with gr.Blocks(title="视频故事编织者", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎬 视频故事编织者")
gr.Markdown("一个基于本地开源模型的AI视频生成智能体。请确保LM Studio中的Wan2.2-T2V和Llama-3模型已分别在端口1234和1235上运行。")
with gr.Tabs():
with gr.TabItem("📝 从大纲生成"):
outline_input = gr.Textbox(label="故事大纲", placeholder="一只小猫在雨天迷路了,最后被一个善良的小女孩收养。", lines=3)
scenes_input = gr.Slider(minimum=3, maximum=10, value=6, step=1, label="生成场景数量")
generate_btn_outline = gr.Button("🚀 从大纲生成", variant="primary")
with gr.TabItem("🎭 从提示词生成"):
prompts_input = gr.Textbox(label="视频提示词 (每行一个)", placeholder="A cat walking in the rain.
A girl finds the cat under a bench.", lines=5)
generate_btn_prompts = gr.Button("🚀 从提示词生成", variant="primary")
# 通用控制面板
with gr.Accordion("⚙️ 高级设置", open=False):
fade_input = gr.Slider(minimum=0, maximum=3, value=1.5, step=0.1, label="过渡时长 (秒, 0为无过渡)")
output_name_input = gr.Textbox(label="输出文件名", value="my_ai_story.mp4")
# 输出区域
status_output = gr.Textbox(label="状态", interactive=False)
video_output = gr.Video(label="生成的视频")
# 绑定事件
generate_btn_outline.click(
fn=generate_video_wrapper,
inputs=[
gr.Textbox(value="outline", visible=False), # mode
outline_input,
gr.Textbox(visible=False), # prompts (not used)
scenes_input,
fade_input,
output_name_input
],
outputs=[status_output, video_output]
)
generate_btn_prompts.click(
fn=generate_video_wrapper,
inputs=[
gr.Textbox(value="prompts", visible=False), # mode
gr.Textbox(visible=False), # outline (not used)
prompts_input,
gr.Slider(visible=False), # scenes (not used)
fade_input,
output_name_input
],
outputs=[status_output, video_output]
)
if __name__ == '__main__':
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
运行GUI:
确保你的两个LM Studio服务器都在运行。在终端中,运行:
python src/gui.py
终端会显示一个本地URL(通常是
http://localhost:7860)。在浏览器中打开它,你就能看到并使用你亲手创建的图形界面了!
一个成品应用需要能优雅地处理错误,并记录运行信息。
创建
src/logger.py:
# src/logger.py
import logging
import os
from datetime import datetime
def setup_logger(log_file="app.log", log_level=logging.INFO):
"""配置日志记录器"""
# 确保日志目录存在
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_path = os.path.join(log_dir, log_file)
# 创建logger
logger = logging.getLogger("VideoAgent")
logger.setLevel(log_level)
# 防止重复添加handler
if not logger.handlers:
# 创建文件handler
file_handler = logging.FileHandler(log_path, encoding='utf-8')
file_handler.setLevel(log_level)
# 创建控制台handler
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
# 创建formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加handler到logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# 在模块加载时就设置好logger
logger = setup_logger()
在现有代码中使用Logger:
在
model_interface.py,
generator.py,
editor.py,
llm_client.py 的开头,添加:
from .logger import logger
将所有的
print(...) 语句替换为
logger.info(...),
logger.warning(...),
logger.error(...)。示例 (
model_interface.py):
# ...
# print(f"🎬 [T2V] 生成中: '{prompt}'")
logger.info(f"[T2V] Generating for prompt: '{prompt}'")
# ...
# print(f"❌ API请求失败: {e}")
logger.error(f"[T2V] API request failed: {e}")
# ...
这样做之后,所有运行信息都会被记录到
logs/app.log 文件中,方便调试和问题排查。
一个标准的项目需要清晰的文档。
创建
requirements.txt:
video_agent/) 下,创建
requirements.txt 文件,并列出所有依赖:
requests
moviepy
gradio
用户可以通过
pip install -r requirements.txt 一键安装所有依赖。创建
README.md:
README.md 文件。这是项目的“门面”。
# 视频故事编织者
一个基于本地开源模型(Wan2.2)的AI视频生成智能体,能够将文本大纲或提示词序列转化为连贯的短视频故事。
## ✨ 主要功能
- 🧠 **智能提示词生成**: 输入简单故事大纲,由本地LLM自动拆解为详细的视频生成提示词。
- 🎬 **高质量视频生成**: 调用高性能的Wan2.2 T2V模型生成视频片段。
- 🎞️ **智能视频拼接**: 自动将视频片段用平滑的过渡效果(交叉溶解)串联成完整视频。
- 🖥️ **友好的图形界面**: 提供基于Gradio的Web UI,无需编写代码即可使用。
- 🔒 **完全本地化**: 所有数据处理均在本地完成,保护隐私,无需联网。
## 🛠️ 安装与配置
### 1. 系统要求
- Python 3.9+
- 至少 32GB RAM
- 至少 12GB VRAM (推荐)
- 至少 50GB 可用硬盘空间
### 2. 安装Python依赖
```bash
git clone <your-repo-url> # 或下载项目zip包
cd video_agent
pip install -r requirements.txt
3. 配置LM Studio
下载并安装 LM Studio。在LM Studio中搜索并下载以下模型:
Wan2.2-T2V-A14B-GGUF (推荐Q4_K_M版本)
Llama-3-8B-Instruct-GGUF (推荐Q4_K_M版本) 启动T2V模型服务器:
加载
Wan2.2-T2V-A14B-GGUF 模型。进入 “Server” 标签页,确保端口为
1234,并启动服务器。 启动LLM服务器:
加载
Llama-3-8B-Instruct-GGUF 模型。进入 “Server” 标签页,将端口修改为
1235,并启动服务器。 🚀 使用方法
方法一:使用图形界面 (推荐)
确保两个LM Studio服务器都已启动后,在终端运行:
python src/gui.py
然后在浏览器中打开
http://localhost:7860,根据界面提示操作即可。 方法二:使用命令行
从大纲生成视频:
python main.py --outline "一个宇航员在火星上发现了一株会发光的植物。" --scenes 4 --fade 2.0 --output astronaut_story.mp4
从提示词列表生成视频:
python main.py --prompts "一只金毛幼犬在篮子里睡觉。" "幼犬醒来打了个哈欠。" --fade 1.0 --output puppy.mp4
📁 项目结构
video_agent/
├── src/ # 源代码
├── temp/ # 临时视频片段
├── output/ # 最终成品视频
├── logs/ # 日志文件
├── config.py # 配置文件
├── main.py # CLI入口
├── requirements.txt # Python依赖
└── README.md # 项目说明
⚠️ 注意事项
视频生成非常消耗计算资源,请确保你的硬件满足要求。生成过程可能需要较长时间,请耐心等待。如果遇到API错误,请检查LM Studio中的模型是否正确加载,服务器是否正常运行。🔮 未来展望
集成音频生成(TTS旁白、背景音乐) 实现更高级的视频过渡效果(如基于光流的智能过渡) 完善图生视频(I2V)工作流 性能优化与并行处理我们完成了什么?
我们从零开始,一步步构建了一个功能完整的AI视频生成智能体。它不仅仅是一个模型的调用脚本,而是一个集成了**流程编排(任务调度)、内容理解(LLM拆解大纲)和后期处理(视频拼接与过渡)**的自动化系统。我们实现了从命令行到图形界面的跨越,并为其添加了日志和文档,使其成为一个结构化、可维护的“成品”。
项目的局限性:
edge-tts 或本地 TTS 模型。背景音乐: 使用
MusicGen 等开源模型生成背景音乐,然后用
MoviePy 将音频与视频合成。 高级过渡技术:
光流: 使用
OpenCV 的光流算法(如
calcOpticalFlowPyrLK)分析两个片段首尾帧的运动,并生成中间帧,实现运动模糊和动态过渡,效果远超交叉溶解。 图生视频(I2V)工作流完善:
在GUI中增加一个“故事板模式”,用户可以上传多张图片,并为每张图片填写运动提示词,智能体则调用I2V模型生成视频。 性能优化:
模型量化: 尝试更激进的量化等级(如Q2)。并行生成: 如果硬件允许(特别是多GPU),可以修改
generator.py,使用
threading 或
multiprocessing 并行生成多个独立的视频片段,大幅缩短总时间。
至此,整个教程已全部完成。 你不仅拥有了一个强大的AI视频生成工具,更重要的是,你完整地经历了一个复杂AI项目从概念、设计、开发到封装的全过程。希望这个旅程对你有所启发,并鼓励你继续在AI的广阔世界里探索和创造。