重构 Pixelle-Video:公有 AI 视频生成方案
用公有云 API(MiniMax / 通义千问 / 豆包 / MiniMax-Music)替代本地文生图/视频模型,降低硬件门槛,实现「输入标题→全自动生成视频」的多 Agent 协作流水线。
一、项目背景与目标
1.1 现状痛点
- 现有 Pixelle-Video 依赖本地 ComfyUI + 文生图/视频模型,需要 16-24GB 显存 GPU 显卡
- 硬件投入成本高(五金门店/小团队难以承担)
- 模型更新维护复杂,需要持续跟进开源社区
- 本地部署对网络要求低,但运维成本高
1.2 重构目标
- 去除所有本地 AI 模型(文生图、视频生成),改用公有云 API
- 保留并强化:文案生成编排、多 Agent 协作调度、本地 ffmpeg 合成
- 硬件要求降至:CPU(无需 GPU)+ 8GB RAM + 50GB 磁盘
- 保持「输入标题→全自动出片」的核心体验
- 支持私有化部署,API Key 可配置
1.3 预期收益
- 硬件成本降低 90%(无需 GPU 服务器)
- 上线周期从「月」缩短到「天」
- 可对接任意公有 AI 服务商(MiniMax / 通义千问 / 豆包 / 混元)
- 适合 SaaS 化输出,多商户共用一套流水线
二、整体架构设计
2.1 系统分层
采用五层架构,将 AI 推理全部委托给公有云,本地只负责编排和合成:
┌─────────────────────────────────────────────────────┐
│ 接入层:用户输入(标题/关键词/主题) │
└────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 编排层:多 Agent 协作调度(Hermes / LangGraph) │
│ ├── Agent-文案 → MiniMax LLM / 通义千问 │
│ ├── Agent-配图 → 豆包视觉 / 通义万相 / 混元 │
│ ├── Agent-视频 → MiniMax Video / 通义万相 │
│ ├── Agent-音乐 → MiniMax-Music │
│ ├── Agent-配音 → edge-tts / MiniMax TTS │
│ └── Agent-合成 → 本地 ffmpeg │
└────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 合成层:ffmpeg 拼接 + 字幕 + 背景音乐 → MP4 │
└────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 输出层:成品视频 + 预览链接 / 微信/钉钉推送 │
└─────────────────────────────────────────────────────┘
2.2 技术选型
- 编排框架:MiniMax 原生 + Hermes 多租户(复用现有基础设施)
- 公有 API:MiniMax 全家桶(文案/视频/音乐/TTS)+ 豆包/通义千问(图片)
- 本地合成:ffmpeg(字幕压制 + 音频混音 + 拼接)
- 配置管理:config.yaml(API Key / 模型选择 / 尺寸参数)
- 消息队列:Redis(可选,批量生成时解耦各 Agent)
2.3 与现有 Pixelle-Video 的区别
| 维度 | 原版 Pixelle-Video | 重构版 |
|---|---|---|
| 文生图 | 本地 ComfyUI | 公有云 API |
| 图生视频 | 本地 VideoGen 模型 | 公有云 API |
| 显存要求 | 16-24GB | 0(纯 CPU) |
| 部署难度 | 高(依赖模型下载) | 低(pip install 即可) |
| 运维成本 | 高(模型更新/显存管理) | 低(API 版本由厂商维护) |
三、模块拆解:公有 API 集成
3.1 文案生成模块
选用方案:MiniMax LLM 或 通义千问
核心流程
- 接收用户输入的主题(1-2句话)
- 调用 LLM API 生成:标题 + 5-10 个分镜脚本 + BGM 描述
- 输出结构化 JSON,供后续 Agent 消费
Prompt 设计模板
你是一个专业的短视频文案师。请根据以下主题,生成短视频脚本:
主题:{user_topic}
请以 JSON 格式输出:
{
"title": "视频标题(10字以内)",
"hook": "开场抓人句子(5字)",
"scenes": [
{
"scene_id": 1,
"duration": 5,
"narration": "旁白文案(20字以内)",
"visual_prompt": "画面描述词(供 AI 生成图片/视频用,15字以内)",
"bgm_mood": "欢快/紧张/温柔/史诗"
}
],
"ending": "结尾行动号召"
}
API 调用示例(MiniMax)
import requests
def generate_script(topic: str) -> dict:
response = requests.post(
"https://api.minimaxi.com/v1/text/chatcompletion_v2",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
json={
"model": "MiniMax-Text-01",
"messages": [{"role": "user", "content": prompt_template.format(user_topic=topic)}]
}
)
return response.json()["choices"][0]["message"]["content"]
视频时长估算
- 每个分镜:5-8 秒
- 总分镜数:根据总时长动态分配(30s视频≈5分镜,60s≈8分镜)
3.2 图片生成模块
推荐方案:豆包视觉(dart-vision)或 通义万相(Wan2.1-I2V)或 混元
选型对比
| 服务 | 模型 | 优势 | 劣势 |
|---|---|---|---|
| 豆包 | dart-vision | 字节自研,价格低,响应快 | 国内需要 API 代理 |
| 通义万相 | Wan2.1-I2V | 阿里自研,中文理解强 | 需开通阿里云百炼 |
| 混元 | Hunyuan-DiT | 腾讯自研,与微信生态打通 | 单独接入流程较复杂 |
核心流程
- 接收文案 Agent 输出的 scenes[].visual_prompt
- 每个分镜调用图片生成 API(可并发)
- 下载图片到本地临时目录
- 检查图片尺寸,不合格则重试(最多 3 次)
API 调用示例(豆包 dart-vision)
import requests
def generate_image(prompt: str, output_path: str) -> str:
response = requests.post(
"https://ark.cn-beijing.volces.com/api/v3/images/generations",
headers={"Authorization": f"Bearer {DOUBAN_API_KEY}"},
json={
"model": "doubao-vision-01",
"prompt": prompt,
"size": "1080x1920",
"response_format": "url"
}
)
image_url = response.json()["data"][0]["url"]
img_data = requests.get(image_url).content
with open(output_path, "wb") as f:
f.write(img_data)
return output_path
图片规格
- 分辨率:1080x1920(竖屏 9:16,适配抖音/视频号)
- 也支持 1920x1080(横屏 16:9,适配B站/YouTube)
- 格式:PNG → ffmpeg 前转 JPG 降低体积
失败处理
- 重试机制:指数退避,3次失败则用 fallback 纯色图
- 降级策略:图片生成连续失败时,改用纯色+文字字幕作为替代
3.3 视频生成模块
推荐方案:MiniMax Video + 通义万相 Wan2.1(双保险)
选型对比
| 服务 | 模型 | 生成长度 | 优势 | 劣势 |
|---|---|---|---|---|
| MiniMax | minimax-video-01 | 5-15秒 | 速度快,支持中文 prompt | 国内访问需代理 |
| 通义万相 | Wan2.1-VACE | 5秒/10秒/15秒 | 阿里官方,稳定性高 | 等待较长 |
核心流程
- 接收图片(3.2 生成的本地图片路径)
- 将图片上传到图生视频 API
- 轮询任务状态(每3秒一次)
- 视频就绪后下载到本地
MiniMax Video API 示例
import requests, time
def generate_video(image_path: str) -> str:
# 1. 上传图片获取 asset_id
with open(image_path, "rb") as f:
upload_resp = requests.post(
"https://api.minimaxi.com/v1/files/upload",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
files={"file": f}
)
asset_id = upload_resp.json()["file"]["file_id"]
# 2. 发起视频生成任务
task_resp = requests.post(
"https://api.minimaxi.com/v1/video_generation",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
json={"model": "minimax-video-01", "asset_id": asset_id, "duration": 5}
)
task_id = task_resp.json()["task_id"]
# 3. 轮询直到完成
while True:
status = requests.get(
f"https://api.minimaxi.com/v1/video_generation/{task_id}",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"}
).json()
if status["status"] == "completed":
return status["video"]["url"]
elif status["status"] == "failed":
raise Exception("视频生成失败")
time.sleep(3)
视频规格
- 分辨率:720p 或 1080p(由 API 决定)
- 竖屏优先(适配短视频平台)
- 格式:MP4(H.264)
降级策略
- MiniMax Video 失败时 → 切换通义万相 Wan2.1
- 都失败时 → 使用静态图片 + 字幕 + 配音 代替视频片段
3.4 音乐生成模块
推荐方案:MiniMax-Music API
选型对比
| 服务 | 模型 | 生成长度 | 优势 | 劣势 |
|---|---|---|---|---|
| MiniMax | minimax-music-01 | 15-60秒 | 字节自研,质量高,响应快 | 国内需代理 |
| 豆包 | (暂无音乐生成) | - | - | - |
| 通义万相 | Wan2.1 (图像/视频) | - | 阿里官方 | 无音乐模块 |
核心流程
- 接收文案 Agent 输出的 scenes[].bgm_mood(欢快/紧张/温柔/史诗等)
- 根据 mood 调用音乐生成 API
- 下载 MP3 到本地临时目录
- 可选:生成多段不同 mood 的音乐,最终 ffmpeg 拼接
MiniMax-Music API 示例
import requests, time
def generate_music(mood: str, duration: int = 30) -> str:
response = requests.post(
"https://api.minimaxi.com/v1/music_generation",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
json={
"model": "minimax-music-01",
"prompt": f"生成一段{mood}风格的背景音乐",
"duration": duration,
"format": "mp3"
}
)
task_id = response.json()["task_id"]
# 轮询直到完成
while True:
status = requests.get(
f"https://api.minimaxi.com/v1/music_generation/{task_id}",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"}
).json()
if status["status"] == "completed":
music_url = status["music"]["audio_url"]
mp3_data = requests.get(music_url).content
output_path = f"/tmp/bgm_{mood}_{int(time.time())}.mp3"
with open(output_path, "wb") as f:
f.write(mp3_data)
return output_path
elif status["status"] == "failed":
raise Exception("音乐生成失败")
time.sleep(3)
音乐时长策略
- 总时长 = 视频总时长
- 音乐 < 视频时长:循环 + 淡出
- 音乐 > 视频时长:截断 + 淡出
- 音量:背景音乐降低 30%,确保配音清晰
情感标签体系
| mood 标签 | 适用场景 |
|---|---|
| 欢快 | 五金产品展示、促销活动 |
| 史诗/震撼 | 品牌宣传片 |
| 温柔 | 产品讲解、使用教程 |
| 紧张 | 限时促销、倒计时 |
| 科技感 | 新品发布、技术介绍 |
3.5 语音配音模块
推荐方案:edge-tts(免费)或 MiniMax TTS
选型对比
| 服务 | 模型 | 费用 | 质量 | 中文支持 |
|---|---|---|---|---|
| edge-tts | 微软 Edge 在线 | 免费 | 高 | 优(晓晓、云扬等) |
| MiniMax TTS | minimax-tts-01 | 按 token 计费 | 很高 | 优(多种音色) |
| 通义千问 | qwen-tts | 按调用计费 | 高 | 优 |
核心流程
- 接收文案 Agent 输出的 scenes[].narration
- 调用 TTS API 生成音频
- 下载 WAV/MP3 到本地
- 检查音频时长是否与预期分镜时长匹配
edge-tts 示例(免费方案)
import asyncio, edge_tts
async def generate_voice(narration: str, output_path: str, voice: str = "zh-CN-XiaoxiaoNeural") -> str:
communicate = edge_tts.Communicate(narration, voice)
await communicate.save(output_path)
return output_path
# 常用中文音色
VOICES = {
"女-晓晓": "zh-CN-XiaoxiaoNeural",
"男-云扬": "zh-CN-YunyanNeural",
"女-云希": "zh-CN-YunxiNeural",
}
分镜时长对齐
- 每个分镜预期时长 = scenes[].duration(来自文案 Agent)
- TTS 生成后用 pydub 检测实际时长
- 如果实际时长 > 预期 20%:加速播放(speed up)
- 如果实际时长 < 预期 20%:添加静音 padding
音效增强
- 添加轻微混响(reverb)让声音更有空间感
- 开头和结尾 0.3s 淡入淡出
3.6 品牌素材库模块
功能定位:为每个租户建立私有品牌素材库,存储 Logo、产品图/视频、门店照片、员工照片,在视频生成时将品牌素材作为场景背景或水印嵌入成品。
3.6.1 素材分类体系
| 类型 | 目录 | 文件格式 | 典型用途 |
|---|---|---|---|
| 品牌 Logo | logo/ |
PNG(透明通道)/SVG/AI | 固定水印嵌入右下角 |
| 产品图片 | products/ |
JPG/PNG/WebP | 场景背景、分镜主体 |
| 产品视频 | products/ |
MP4/MOV(≤30秒) | 直接作为分镜片段 |
| 门店照片 | stores/ |
JPG/PNG | 到店场景、门头展示 |
| 员工照片 | employees/ |
JPG/PNG | 员工推荐分镜、团队展示 |
3.6.2 本地存储结构
/data/brand_assets/{tenant_id}/
├── logo/
│ ├── primary.png # 主 Logo(透明背景)
│ └── icon.png # 方形图标(App/头像用)
├── products/
│ ├── {product_id}/
│ │ ├── main.jpg # 产品主图(白底)
│ │ ├── scene.jpg # 场景图(使用中)
│ │ └── video.mp4 # 产品视频(可选)
├── stores/
│ ├── {store_id}/
│ │ ├── facade.jpg # 门头照
│ │ └── interior.jpg # 店内照
└── employees/
├── {employee_id}/
│ ├── avatar.jpg # 个人头像
│ └── team.jpg # 团队合照(可选)
3.6.3 OSS 云端备份
本地存储使用 SSD,OSS(阿里云/兼容S3协议)作为冷备:
import oss2, hashlib
from pathlib import Path
def upload_to_oss(local_path: str, tenant_id: str, category: str) -> str:
"""上传本地素材到OSS,返回CDN URL"""
oss_config = get_oss_config(tenant_id)
bucket = oss2.Bucket(oss2.Auth(oss_config['key'], oss_config['secret']),
oss_config['endpoint'], oss_config['bucket'])
# 计算文件MD5去重
with open(local_path, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()
oss_key = f"brand_assets/{tenant_id}/{category}/{file_hash}_{Path(local_path).name}"
# 已存在则跳过
if bucket.object_exists(oss_key):
return f"https://{oss_config['cdn_domain']}/{oss_key}"
bucket.put_object_from_file(oss_key, local_path)
return f"https://{oss_config['cdn_domain']}/{oss_key}"
3.6.4 视频生成融合方式
Logo 水印:ffmpeg overlay 固定在右下角(见 4.7 节)
产品图片:作为分镜背景,visual_prompt 中引用 {product_id},系统自动从素材库加载对应图片路径替换 AI 生成:
def resolve_product_image(product_id: str, scene_prompt: str) -> str:
"""将 prompt 中的 {product_id} 替换为实际素材路径"""
if product_id in scene_prompt:
local_path = get_product_image(product_id) # 从素材库获取
return scene_prompt.replace(f"{{{product_id}}}", local_path)
return scene_prompt # 无 product_id,使用 AI 生成
门店/员工照片:用于特定分镜类型(如"员工推荐"、"门店环境"),分镜类型映射到对应素材目录。
3.6.5 多租户隔离
每个租户独立目录,API 请求必须携带 tenant_id,跨租户访问抛出 403。
3.7 克隆音色模块
功能定位:使用 MiniMax 声音克隆 API,将店主/员工的声音克隆后用于配音,替代标准 TTS 音色,增加品牌真实感。
3.7.1 克隆流程总览
Step 1: 采集样本音频(3-5分钟纯净人声)
Step 2: 上传样本 → 调用克隆API → 获得 voice_id
Step 3: 生成配音时用 voice_id 替代标准 TTS
Step 4: voice_id 存入租户音色库(最多10个)
3.7.2 样本音频要求
| 参数 | 要求 |
|---|---|
| 时长 | 3-5 分钟(越多越好,最少 1 分钟) |
| 格式 | WAV/MP3,16bit/44.1kHz |
| 环境 | 安静无噪声、无背景音乐、无混响 |
| 内容 | 纯人声朗读,语速中等,情感稳定 |
| 格式校验 | 预处理时用 pydub 检测峰值电平(>-30dBFS) |
3.7.3 MiniMax 声音克隆 API
import requests, time, os
MINIMAX_API_KEY = os.environ["MINIMAX_API_KEY"]
def clone_voice(audio_path: str, voice_name: str, tenant_id: str) -> str:
"""
上传样本音频,发起克隆任务,返回 voice_id
"""
# Step 1: 上传音频文件
with open(audio_path, "rb") as f:
upload_resp = requests.post(
"https://api.minimaxi.com/v1/files/upload",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
files={"file": f}
)
file_id = upload_resp.json()["file"]["file_id"]
# Step 2: 发起克隆任务
clone_resp = requests.post(
"https://api.minimaxi.com/v1/voice_clone",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
json={
"file_id": file_id,
"name": voice_name,
"model": "minimax-voice-clone-01"
}
)
task_id = clone_resp.json()["task_id"]
# Step 3: 轮询直到完成(每5秒一次,超时30分钟)
for _ in range(360):
status = requests.get(
f"https://api.minimaxi.com/v1/voice_clone/{task_id}",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"}
).json()
if status["status"] == "completed":
voice_id = status["voice_id"]
save_voice_to_library(tenant_id, voice_id, voice_name)
return voice_id
elif status["status"] == "failed":
raise Exception(f"音色克隆失败: {status.get('error', 'unknown')}")
time.sleep(5)
raise Exception("音色克隆超时")
def generate_voice_with_clone(narration: str, voice_id: str, output_path: str) -> str:
"""使用克隆音色生成配音"""
response = requests.post(
"https://api.minimaxi.com/v1/tts_with_voice_id",
headers={"Authorization": f"Bearer {MINIMAX_API_KEY}"},
json={
"text": narration,
"voice_id": voice_id,
"model": "minimax-tts-01",
"response_format": "mp3"
}
)
audio_url = response.json()["audio_url"]
audio_data = requests.get(audio_url).content
with open(output_path, "wb") as f:
f.write(audio_data)
return output_path
3.7.4 音色库管理
# 音色库表结构
"""
CREATE TABLE voice_library (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
voice_id TEXT NOT NULL,
voice_name TEXT NOT NULL,
sample_duration INT,
created_at TIMESTAMP DEFAULT NOW(),
status TEXT DEFAULT 'active',
usage_count INT DEFAULT 0
);
"""
def list_voices(tenant_id: str) -> list:
return db.query(
"SELECT * FROM voice_library WHERE tenant_id = ? AND status = 'active'",
tenant_id
)
def delete_voice(tenant_id: str, voice_id: str) -> bool:
affected = db.execute(
"DELETE FROM voice_library WHERE tenant_id = ? AND voice_id = ?",
tenant_id, voice_id
)
return affected > 0
3.7.5 Fallback 机制
克隆音色生成失败
→ 回退到 edge-tts 标准音色(zh-CN-XiaoxiaoNeural)
→ 记录失败日志,标记 voice_id 为 unavailable
3.7.6 费用参考
| 项目 | 单价 |
|---|---|
| 样本音频克隆(训练) | ~¥0.5/分钟音频 |
| 克隆音色生成配音 | 按 MiniMax TTS 标准计费 |
| edge-tts(fallback) | 免费 |
3.7.7 隐私与合规
- 克隆音色仅限本租户使用,不可跨租户共享
- 样本音频加密存储(OSS SSE-KMS),不留存克隆后删除
- 克隆前需用户确认《声音授权书》(电子签)
四、本地合成层:ffmpeg 流水线
4.1 合成流程总览
输入阶段
├── 图片:分镜图 N 张(JPG)
├── 音频:配音 N 段(WAV/MP3)
├── 音乐:背景音乐 1 段(MP3)
└── 字幕:SRT 格式
第一步:图片 → 视频片段(ffmpeg -loop)
第二步:配音 + 音乐混音(ffmpeg amix)
第三步:视频片段拼接(ffmpeg concat)
第四步:字幕烧录(ffmpeg subtitles)
第五步:音频混音绑定 → final_output.mp4
4.2 分镜图转视频片段
ffmpeg -loop 1 \
-i scene_01.jpg -t 5 \
-vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920" \
-r 30 -c:v libx264 -preset fast -crf 23 \
-pix_fmt yuv420p -movflags +faststart \
scene_01.mp4
4.3 音频混音
ffmpeg -i voice.wav -i bgm.mp3 \
-filter_complex "[0:a]volume=0.8[voice];[1:a]volume=0.25[bgm];[voice][bgm]amix=inputs=2:duration=first[mixed]" \
-map "[mixed]" -t {total_duration} audio_mixed.mp3
4.4 字幕压制(SRT)
def generate_srt(scenes, output_path):
def ms_to_srt(ms):
h, rem = divmod(int(ms), 3600000)
m, rem = divmod(rem, 60000)
s, ms = divmod(rem, 1000)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
with open(output_path, "w", encoding="utf-8") as f:
for i, scene in enumerate(scenes, 1):
start = sum(s["duration"] for s in scenes[:i-1])
end = start + scene["duration"]
f.write(f"{i}\n{ms_to_srt(start*1000)} --> {ms_to_srt(end*1000)}\n{scene['narration']}\n\n")
4.5 完整合成脚本
import subprocess
from pathlib import Path
def synthesize(video_or_images, voices, music_path, scenes, output_path):
# Step 1: 图片/视频转片段
for i, src in enumerate(video_or_images):
ext = Path(src).suffix
if ext in [".jpg", ".jpeg", ".png"]:
subprocess.run(["ffmpeg", "-y", "-loop", "1", "-i", src,
"-t", str(scenes[i]["duration"]),
"-vf", "scale=1080:1920,crop=1080:1920",
"-r", "30", "-c:v", "libx264", "-preset", "fast",
f"/tmp/scene_{i}.mp4"])
else:
subprocess.run(["cp", src, f"/tmp/scene_{i}.mp4"])
# Step 2: 拼接
with open("/tmp/filelist.txt", "w") as f:
for i in range(len(video_or_images)):
f.write(f"file '/tmp/scene_{i}.mp4'\n")
subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", "/tmp/filelist.txt", "-c", "copy", "/tmp/video_only.mp4"])
# Step 3: 字幕
generate_srt(scenes, "/tmp/subtitle.srt")
subprocess.run(["ffmpeg", "-y", "-i", "/tmp/video_only.mp4",
"-vf", "subtitles=subtitle.srt", "/tmp/video_with_subs.mp4"])
# Step 4: 混音绑定
total_dur = sum(s["duration"] for s in scenes)
subprocess.run(["ffmpeg", "-y", "-i", "/tmp/video_with_subs.mp4",
"-i", music_path, "-c:v", "copy", "-c:a", "aac",
"-t", str(total_dur), output_path])
return output_path
4.6 ffmpeg 依赖
# Ubuntu
apt install ffmpeg
# macOS
brew install ffmpeg
4.7 品牌 Logo 水印叠加
4.7.1 叠加位置(9点布局)
| 位置 | ffmpeg 参数 |
|---|---|
| 左上 | overlay=20:20 |
| 右上 | overlay=W-w-20:20 |
| 左下 | overlay=20:H-h-20 |
| 右下(默认) | overlay=W-w-20:H-h-20 |
| 居中 | overlay=(W-w)/2:(H-h)/2 |
4.7.2 透明度处理
PNG Alpha 通道(推荐):透明区域自动保留,无需额外滤镜。
透明度滤镜:
ffmpeg -i input.mp4 -i logo.png \
-filter_complex "[1:v]format=rgba,colorchannelmixer=aa=0.35[logo];[0:v][logo]overlay=W-w-20:H-h-20[out]" \
-map "[out]" output.mp4
4.7.3 固定水印命令模板
LOGO="logo.png"
ffmpeg -i input_video.mp4 -i $LOGO \
-filter_complex "[1:v]scale=${LOGO_W}:${LOGO_H}[logo];\
[0:v][logo]overlay=W-w-20:H-h-20:format=auto[out]" \
-map "[out]" -c:v libx264 -preset fast -crf 23 \
output_with_logo.mp4
4.7.4 动态水印(渐入渐出)
# 前3秒淡入,第58秒开始淡出
ffmpeg -i input_video.mp4 -i logo.png \
-filter_complex "[1:v]format=rgba,fade=t=in:st=0:d=1:alpha=1[logo];\
[0:v][logo]overlay=W-w-20:H-h-20:enable='between(t,1,57)'[out]" \
-map "[out]" output.mp4
4.7.5 水印配置持久化
# config.yaml
watermark:
enabled: true
logo_path: /data/brand_assets/{tenant_id}/logo/primary.png
position: 右下 # 左上/右上/左下/右下/居中
offset_x: 20
offset_y: 20
opacity: 0.35 # 0.0~1.0
fade_in: 1.0 # 秒
fade_out: 2.0 # 秒
scale: 0.12 # 相对视频宽度的比例
4.8 品牌素材融合合成
4.8.1 素材融合场景
| 场景 | 素材类型 | 合成方式 |
|---|---|---|
| 产品特写 | products/{id}/main.jpg |
背景叠加 + 主体居中 |
| 门店环境 | stores/{id}/interior.jpg |
虚化背景 + 分镜字幕 |
| 员工推荐 | employees/{id}/avatar.jpg |
画中画 PiP + 字幕说明 |
| 绿幕视频 | 自带 Alpha 通道 | 色键抠像 + 背景叠加 |
4.8.2 产品图作为分镜背景
def blend_product_image(product_img: str, duration: int, output: str):
"""将产品图作为固定背景,持续 duration 秒"""
subprocess.run([
"ffmpeg", "-y",
"-loop", "1", "-i", product_img,
"-i", f"/tmp/voice_{idx}.wav",
"-i", f"/tmp/bgm.mp3",
"-t", str(duration),
"-vf", (
"scale=1080:1920:force_original_aspect_ratio=increase,"
"crop=1080:1920,"
"boxblur=3"
),
"-i", "logo.png",
"-filter_complex",
"[0:v][3:v]overlay=W-w-20:H-h-20[bg];"
"[bg]ass=subtitle.ass[out]",
"-map", "[out]", "-map", "1:a", "-map", "2:a",
"-c:v", "libx264", "-preset", "fast",
"-shortest", output
], check=True)
4.8.3 画中画模式(员工/门店素材)
# 员工头像 PIP:左下角小窗(占画面 15%)
ffmpeg -i store_bg.mp4 -i employee_avatar.jpg \
-filter_complex "[1:v]scale=160:160[avatar];\
[0:v][avatar]overlay=20:H-h-180[out]" \
-map "[out]" -c:v libx264 -preset fast \
pip_with_employee.mp4
4.8.4 绿幕抠像融合
# 色键抠像 + 品牌背景叠加
ffmpeg -i green_screen.mp4 -i brand_bg.jpg \
-filter_complex "[0:v]chromakey=0x00ff00:0.15:0.02[fg];\
[1:v][fg]overlay=0:0[out]" \
-map "[out]" -c:v libx264 -preset fast \
keying_output.mp4
4.8.5 批量素材合成脚本
#!/bin/bash
# synthesize_with_brand.sh
TENANT_ID=$1
STORE_ID=$2
PRODUCT_ID=$3
LOGO="/data/brand_assets/${TENANT_ID}/logo/primary.png"
STORE_BG="/data/brand_assets/${TENANT_ID}/stores/${STORE_ID}/interior.jpg"
PRODUCT_IMG="/data/brand_assets/${TENANT_ID}/products/${PRODUCT_ID}/main.jpg"
VOICE="/tmp/voice.wav"
BGM="/tmp/bgm.mp3"
SUBS="/tmp/subtitle.ass"
OUTPUT="/data/videos/${TENANT_ID}/${STORE_ID}_${PRODUCT_ID}_$(date +%Y%m%d%H%M%S).mp4"
mkdir -p "$(dirname $OUTPUT)"
ffmpeg -y \
-loop 1 -i "$PRODUCT_IMG" \
-i "$VOICE" \
-i "$BGM" \
-i "$LOGO" \
-filter_complex "
[0:v]scale=1080:1920,crop=1080:1920,boxblur=2[bg];
[bg][3:v]overlay=W-w-20:H-h-20[bg_logo];
[bg_logo]ass=${SUBS}[out]
" \
-map "[out]" -map 1:a -map 2:a \
-c:v libx264 -preset fast -crf 23 \
-t 30 -shortest \
"$OUTPUT"
echo "输出: $OUTPUT"
4.8.6 合成配置字段
# config.yaml - 合成配置
synthesis:
brand_integration:
logo:
enabled: true
path: /data/brand_assets/{tenant_id}/logo/primary.png
position: 右下
opacity: 0.35
product_as_bg: true # 产品图作为分镜背景
store_photo_as_bg: true # 门店照作为背景
employee_pip: false # 员工头像画中画
subtitle_ass: true # 烧录 ASS 字幕
五、多 Agent 编排层设计
5.1 编排框架选型
| 方案 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|
| MiniMax 原生 + Hermes | 复用现有架构,多租户支持 | 需要熟悉 Hermes 配置 | 推荐:五金门店 SaaS |
| LangGraph | 细粒度状态机,可视化好 | 学习曲线较陡 | 复杂条件分支 |
| AutoGen | 微软开源,生态成熟 | 资源占用较高 | 企业内部知识库 |
5.2 Hermes Profile 设计
platforms:
api_server:
enabled: true
port: 8646
redis:
enabled: true
host: localhost
port: 6379
agent:
model: MiniMax-Text-01
skills:
- name: video_pipeline
config:
image_api: doubao
video_api: minimax
music_api: minimax
tts_api: edge-tts
5.3 任务状态机
IDLE → 收到主题 → SCRIPTING(文案生成)
↓ 文案完成
ASSET_GEN(图片/音乐/配音并发生成)
↓ 素材就绪
SYNTHESIZING(ffmpeg 合成)
↓ 完成
DELIVERING(推送微信/存储)
↓ 完成 → DONE
5.4 Agent 职责
| Agent | 输入 | 输出 | 调用工具 |
|---|---|---|---|
| 文案 Agent | 用户主题 | 结构化脚本 JSON | MiniMax LLM |
| 配图 Agent | visual_prompt | 本地图片路径 | 豆包/通义万相 API |
| 视频 Agent | 本地图片路径 | 本地视频路径 | MiniMax Video API |
| 音乐 Agent | mood 标签 | 本地 MP3 路径 | MiniMax-Music API |
| 配音 Agent | narration | 本地音频路径 | edge-tts / MiniMax TTS |
| 合成 Agent | 所有素材路径 | 最终 MP4 | 本地 ffmpeg |
5.5 并发控制
import asyncio
async def generate_assets_concurrently(scenes, music_mood):
image_tasks = [generate_image(s["visual_prompt"]) for s in scenes]
voice_tasks = [generate_voice(s["narration"]) for s in scenes]
music_task = generate_music(music_mood)
images, voices, music = await asyncio.gather(
asyncio.gather(*image_tasks),
asyncio.gather(*voice_tasks),
music_task
)
return list(images), list(voices), music
5.6 错误恢复与重试
MAX_RETRIES = 3
def generate_with_retry(func, *args, **kwargs):
for attempt in range(MAX_RETRIES):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == MAX_RETRIES - 1:
raise
time.sleep(5 * (2 ** attempt))
六、用户界面与 API 接口
6.1 三种接入方式
| 方式 | 受众 | 体验 |
|---|---|---|
| Web 管理后台 | 运营人员 | 点选主题,一键生成,预览下载 |
| 企业微信机器人 | 销售/店员 | 发标题给机器人,自动出片推送 |
| REST API | 第三方系统 | 接口调用,批量自动化 |
6.2 Web 管理后台
技术栈:Streamlit(快速原型)或 Next.js + Tailwind(正式版)
核心页面
- 首页:输入标题,选择视频类型(产品展示/促销/教程)
- 生成页:显示进度(文案→配图→配音→音乐→合成)
- 预览页:内置视频播放器,支持下载 MP4
- 历史页:查看历史生成记录
页面示例(Streamlit 简化版)
import streamlit as st
st.title("🎬 AI 全自动视频生成")
topic = st.text_input("输入视频主题", placeholder="例如:五金工具促销,冲击钻特价活动")
video_type = st.selectbox("视频类型", ["产品展示", "促销活动", "使用教程"])
if st.button("🚀 一键生成"):
with st.spinner("文案生成中..."):
script = generate_script(topic)
col1, col2, col3 = st.columns(3)
with col1:
st.info("✅ 文案完成")
with col2:
with st.spinner("配图+配音+音乐生成中..."):
assets = generate_assets(script)
with col3:
with st.spinner("视频合成中..."):
output = synthesize_video(script, assets)
st.success("生成完成!")
st.video(output)
st.download_button("📥 下载视频", open(output, "rb"), f"{topic}.mp4")
6.3 企业微信机器人接入
@hermes.on_message(channel="wecom")
async def handle_video_request(message: str, user_id: str):
topic = message.strip()
await hermes.send_text(
f"收到!正在为您生成视频:{topic}\n预计 3-5 分钟完成,请稍候...",
to=user_id
)
loop = asyncio.get_event_loop()
loop.run_in_executor(None, generate_video_task, topic, user_id)
def generate_video_task(topic: str, user_id: str):
try:
output_path = main_pipeline(topic)
hermes.send_file(output_path, to=user_id)
except Exception as e:
hermes.send_text(f"生成失败:{str(e)}", to=user_id)
6.4 REST API
POST /api/v1/video/generate
Body: { "topic": "冲击钻促销", "type": "product_showcase" }
Response: { "task_id": "vid_abc123", "status": "queued" }
GET /api/v1/video/{task_id}
Response: {
"task_id": "vid_abc123",
"status": "synthesizing",
"progress": 75,
"video_url": null
}
GET /api/v1/video/{task_id}/download
Response: MP4 文件流
6.5 Webhook 回调
def on_video_completed(task_id: str, video_path: str):
requests.post(
"https://your-crm.com/webhook/video",
json={
"task_id": task_id,
"video_path": video_path,
"completed_at": datetime.now().isoformat()
}
)
10.6 品牌素材管理后台
10.6.1 功能概览
素材管理后台提供 Web 界面,支持品牌 Logo、产品图/视频、门店照片、员工照片的上传、查看、删除与标签管理。
10.6.2 素材上传
上传限制:
| 类型 | 格式 | 单文件上限 | 说明 |
|---|---|---|---|
| Logo | PNG/SVG/JPG | 10MB | PNG 必须有透明通道 |
| 产品图 | JPG/PNG/WebP | 20MB | 建议 1080×1920 |
| 产品视频 | MP4/MOV | 100MB | ≤30秒 |
| 门店照 | JPG/PNG | 30MB | 建议横版 |
| 员工头像 | JPG/PNG | 5MB | 建议 1:1 正方形 |
上传流程:选择分类 → 拖拽文件 → 自动校验格式/尺寸/MD5去重 → 缩略图生成 → 写入本地+OSS备份 → 返回 CDN URL
上传接口:
# POST /api/v1/materials/upload
from fastapi import UploadFile, File
import hashlib, uuid, asyncio
async def upload_material(tenant_id: str, category: str, file: UploadFile = File(...)):
ext = file.filename.split('.')[-1].lower()
content = await file.read()
file_hash = hashlib.md5(content).hexdigest()
# MD5去重
existing = db.query(
"SELECT id FROM materials WHERE tenant_id=? AND hash=?", tenant_id, file_hash
)
if existing:
return {"id": existing[0]['id'], "duplicate": True}
material_id = str(uuid.uuid4())
path = f"/data/brand_assets/{tenant_id}/{category}/{material_id}.{ext}"
Path(path).parent.mkdir(parents=True, exist_ok=True)
with open(path, 'wb') as f:
f.write(content)
db.execute(
"INSERT INTO materials(id,tenant_id,category,hash,path,filename,size) VALUES(?,?,?,?,?,?,?)",
material_id, tenant_id, category, file_hash, path, file.filename, len(content)
)
# OSS备份(异步)
asyncio.create_task(upload_to_oss(path, tenant_id, category))
return {"id": material_id, "url": f"/materials/{material_id}"}
10.6.3 素材查看与检索
# GET /api/v1/materials?category=product&page=1&page_size=20
def list_materials(tenant_id: str, category: str = None,
tag: str = None, page: int = 1, page_size: int = 20):
query = "SELECT * FROM materials WHERE tenant_id=? AND deleted_at IS NULL"
params = [tenant_id]
if category:
query += " AND category=?"
params.append(category)
if tag:
query += " AND tags LIKE ?"
params.append(f"%{tag}%")
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
offset = (page - 1) * page_size
params.extend([page_size, offset])
items = db.query(query, *params)
total = db.query(
"SELECT COUNT(*) FROM materials WHERE tenant_id=? AND deleted_at IS NULL"
+ (" AND category=?" if category else ""),
tenant_id, *([category] if category else [])
)[0]['count']
return {"items": items, "total": total, "page": page, "page_size": page_size}
10.6.4 素材删除
# DELETE /api/v1/materials/{id}
def delete_material(tenant_id: str, material_id: str):
# 软删除(保留30天)
db.execute(
"UPDATE materials SET deleted_at=NOW() WHERE id=? AND tenant_id=?",
material_id, tenant_id
)
ref_count = db.query(
"SELECT COUNT(*) FROM video_tasks WHERE tenant_id=? AND materials LIKE ?",
tenant_id, f"%{material_id}%"
)[0]['count']
if ref_count > 0:
return {"warning": f"该素材被{ref_count}个任务引用,30天内可恢复"}
return {"status": "deleted"}
10.7 克隆音色管理
10.7.1 音色库界面
Web 端提供音色管理页面,展示所有克隆音色列表:音色名称、创建时间、状态(训练中/可用)、使用次数、操作(预览/删除)。
10.7.2 上传音频样本
格式要求:WAV/MP3,1-5分钟,≥16kHz采样,安静无噪声。
// POST /api/v1/voices/upload-sample
const formData = new FormData();
formData.append('audio', audioFile);
formData.append('voice_name', '老板张总');
formData.append('tenant_id', 'tenant_001');
const res = await fetch('/api/v1/voices/upload-sample', {
method: 'POST',
body: formData
});
const { sample_id, quality_score, status } = await res.json();
// status: 'processing' → 后台自动分析音质
10.7.3 创建克隆音色
# POST /api/v1/voices/clone
def create_voice_clone(tenant_id: str, sample_id: str, voice_name: str):
sample = db.query("SELECT * FROM voice_samples WHERE id=?", sample_id)[0]
voice_id = clone_voice(sample['file_path'], voice_name, tenant_id)
voice_record = {
'id': str(uuid.uuid4()),
'tenant_id': tenant_id,
'voice_id': voice_id,
'voice_name': voice_name,
'sample_duration': sample['duration'],
'status': 'active',
'created_at': datetime.now()
}
db.execute("INSERT INTO voice_library(...) VALUES(...)", *voice_record.values())
return voice_record
10.7.4 音色预览与使用
# GET /api/v1/voices/{voice_id}/preview
def preview_voice(voice_id: str, text: str = "欢迎使用品牌配音"):
return generate_voice_with_clone(
narration=text[:50],
voice_id=voice_id,
output_path=f"/tmp/preview_{voice_id}.mp3"
)
10.7.5 音色使用限制
每个租户最多 10 个克隆音色,超限需删除旧音色后才能新建。
10.8 多租户权限隔离
10.8.1 隔离策略总览
| 层级 | 隔离方式 |
|---|---|
| 存储路径 | /data/brand_assets/{tenant_id}/ |
| 数据库 | PostgreSQL RLS(行级安全策略) |
| Redis | Key 前缀 tenant:{tenant_id}: |
| OSS | Bucket 内路径前缀隔离 |
10.8.2 数据库行级隔离
-- PostgreSQL RLS
ALTER TABLE materials ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_materials ON materials
FOR ALL USING (tenant_id = current_setting('app.tenant_id', true));
ALTER TABLE voice_library ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_voices ON voice_library
FOR ALL USING (tenant_id = current_setting('app.tenant_id', true));
10.8.3 中间件注入
# FastAPI 中间件:自动注入 tenant_id
@app.middleware("http")
async def inject_tenant(request: Request, call_next):
tenant_id = request.headers.get("X-Tenant-ID")
if not tenant_id:
raise HTTPException(401, "Missing X-Tenant-ID")
with get_db_connection() as conn:
conn.execute(f"SET LOCAL app.tenant_id = '{tenant_id}'")
response = await call_next(request)
return response
10.8.4 审计日志
def audit_log(tenant_id: str, user_id: str, action: str, resource: str, result: str):
db.execute(
"INSERT INTO audit_logs(tenant_id,user_id,action,resource,result,ip,created_at) VALUES(?,?,?,?,?,?,NOW())",
tenant_id, user_id, action, resource, result,
request.client.host if request else 'system'
)
10.8.5 跨租户访问检测
def check_cross_tenant(tenant_id: str, resource_tenant_id: str):
if tenant_id != resource_tenant_id:
audit_log(tenant_id, "system", "CROSS_TENANT_ACCESS_DENIED",
f"attempted access to {resource_tenant_id}", "DENIED")
raise HTTPException(403, "Access denied")
七、部署架构与运维
7.1 推荐部署架构
用户层
├── Web 管理后台(浏览器)
├── 企业微信机器人
└── REST API
网关层(Nginx)
├── 静态文件服务(视频)
├── 反向代理(API)
└── HTTPS
调度层(Hermes + Redis)
├── Hermes Gateway(任务编排)
└── Redis(队列 + 缓存)
公有 API 层
├── 图片(豆包/通义万相)
├── 视频(MiniMax Video)
└── 音乐(MiniMax Music)
合成层
├── ffmpeg
├── 临时存储(SSD)
└── 成品存储(NFS/OSS)
7.2 硬件配置
方案 A:小规模(< 100 并发)
| 组件 | 配置 | 成本 |
|------|------|------|
| CPU | 4 核 | |
| 内存 | 8 GB | ~300元/月 |
| 磁盘 | 100GB SSD + 500GB NAS | |
| 带宽 | 10 Mbps | |
方案 B:中规模(100-500 并发)
| 组件 | 配置 | 成本 |
|------|------|------|
| CPU | 8 核 | |
| 内存 | 16 GB | ~800元/月 |
| 磁盘 | 200GB SSD + 1TB NAS | |
| 带宽 | 50 Mbps | |
7.3 环境变量配置
MINIMAX_API_KEY=your_key
DOUBAN_API_KEY=your_key
WANXIANG_API_KEY=your_key
FFMPEG_PATH=/usr/bin/ffmpeg
TEMP_DIR=/tmp/video_generation
OUTPUT_DIR=/data/videos
REDIS_URL=redis://localhost:6379/0
7.4 监控与告警
@app.get("/health")
def health_check():
return {
"status": "healthy",
"redis": check_redis(),
"ffmpeg": check_ffmpeg(),
}
7.5 日志与备份
- 访问/生成/错误日志,接入 SLS 或 ELK
- 视频成品每日增量备份到 OSS,保留 30 天
- 配置文件 Git 管理
八、实施计划与里程碑
8.1 整体路线图
| 阶段 | 时间 | 目标 |
|---|---|---|
| 第一阶段 | Week 1-2 | 核心框架搭建(环境 + 文案 + 图片 + ffmpeg) |
| 第二阶段 | Week 3-4 | 完整流水线(视频 + 音乐 + 配音 + Hermes 编排) |
| 第三阶段 | Week 5-6 | 用户界面(Web 后台 + 企业微信 + REST API) |
| 第四阶段 | Week 7-8 | 生产优化(错误处理 + 限流 + 监控) |
| 第五阶段 | Week 9-10 | 上线与迭代(内部测试 + 试点 + 正式上线) |
8.2 详细任务分解
Week 1-2(核心框架)
| 任务 | 产出 |
|------|------|
| Python 环境 + ffmpeg 安装 | 可执行环境 |
| 文案生成模块(MiniMax LLM) | generate_script() |
| 图片生成模块(豆包 API) | generate_image() |
| ffmpeg 合成流程 | synthesize() |
Week 3-4(完整流水线)
| 任务 | 产出 |
|------|------|
| 视频生成模块(MiniMax Video) | generate_video() |
| 音乐生成模块(MiniMax-Music) | generate_music() |
| 语音配音模块(edge-tts) | generate_voice() |
| 多 Agent 并发编排 | asyncio 流水线 |
Week 5-6(用户界面)
| 任务 | 产出 |
|------|------|
| Streamlit 管理后台 | web 页面 |
| 企业微信机器人 | 消息接收/推送 |
| REST API | Swagger 文档 |
Week 7-8(生产优化)
| 任务 | 产出 |
|------|------|
| 错误处理与降级策略 | 熔断机制 |
| Redis 限流 | 滑动窗口限流 |
| Prometheus 监控 | 监控面板 |
| 压力测试 | TPS 报告 |
Week 9-10(上线)
| 任务 | 产出 |
|------|------|
| 内部测试验收 | UAT 报告 |
| 试点门店部署 | 运行日志 |
| 培训文档 | 用户手册 |
8.3 风险评估
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 公有 API 限流/宕机 | 高 | 多 API 备援 + 降级策略 |
| ffmpeg 合成长时间 | 中 | 异步队列 + 超时中断 |
| API Key 泄露 | 高 | 环境变量 + Vault |
| 视频版权问题 | 中 | 无版权素材 + 水印 |
8.4 成本估算(月度)
| 项目 | 单价 | 预估量 | 月成本 |
|---|---|---|---|
| 云服务器(方案 A) | 300 元/月 | 1 台 | 300 元 |
| MiniMax 文案(LLM) | ~0.1 元/千 Token | 10 万次生成 | 100 元 |
| 豆包图片生成 | ~0.1 元/张 | 1 万张 | 1000 元 |
| MiniMax 视频 | ~1 元/个 | 500 个 | 500 元 |
| MiniMax 音乐 | ~0.5 元/段 | 200 段 | 100 元 |
| edge-tts | 免费 | - | 0 元 |
| 合计 | ~2000 元/月 |
九、代码示例与 Prompt 模板
9.1 核心入口脚本(main_pipeline.py)
#!/usr/bin/env python3
"""
Pixelle-Refactor:公有 API 全自动视频生成流水线
输入:标题 / 关键词
输出:完整 MP4 视频
"""
import os, asyncio
from pathlib import Path
from datetime import datetime
from modules.script import generate_script
from modules.image import generate_image_batch
from modules.video import generate_video_batch
from modules.music import generate_music
from modules.voice import generate_voice_batch
from modules.synthesize import synthesize
class VideoPipeline:
def __init__(self, config: dict):
self.config = config
self.temp_dir = Path(config["TEMP_DIR"])
self.output_dir = Path(config["OUTPUT_DIR"])
self.temp_dir.mkdir(parents=True, exist_ok=True)
self.output_dir.mkdir(parents=True, exist_ok=True)
async def run(self, topic: str) -> str:
task_id = datetime.now().strftime("%Y%m%d_%H%M%S")
print(f"[{task_id}] 开始生成视频,主题:{topic}")
# Step 1: 文案生成
print(f"[{task_id}] Step 1/4 文案生成中...")
script_data = await generate_script(topic)
scenes = script_data["scenes"]
total_duration = sum(s["duration"] for s in scenes)
# Step 2: 素材并发生成
print(f"[{task_id}] Step 2/4 素材生成中(图片+配音+音乐)...")
image_tasks = [generate_image(s["visual_prompt"]) for s in scenes]
voice_tasks = [generate_voice(s["narration"]) for s in scenes]
music_task = generate_music(script_data.get("bgm_mood", "欢快"), duration=total_duration)
images, voices, music_path = await asyncio.gather(
asyncio.gather(*image_tasks),
asyncio.gather(*voice_tasks),
music_task
)
# Step 3: 视频生成(降级:使用静态图)
print(f"[{task_id}] Step 3/4 视频生成中...")
video_paths = []
for i, img_path in enumerate(images):
try:
video_path = await generate_video(img_path)
video_paths.append(video_path)
except Exception as e:
print(f"[{task_id}] 视频生成失败,使用静态图片:{e}")
video_paths.append(img_path)
# Step 4: ffmpeg 合成
print(f"[{task_id}] Step 4/4 视频合成中...")
output_path = self.output_dir / f"{task_id}_{topic[:20]}.mp4"
await synthesize(video_paths, voices, music_path, scenes, output_path)
print(f"[{task_id}] ✅ 生成完成:{output_path}")
return str(output_path)
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--topic", "-t", required=True)
parser.add_argument("--output", "-o", default="/data/videos")
args = parser.parse_args()
config = {
"TEMP_DIR": os.environ.get("TEMP_DIR", "/tmp/video_generation"),
"OUTPUT_DIR": args.output,
"MINIMAX_API_KEY": os.environ.get("MINIMAX_API_KEY", ""),
}
pipeline = VideoPipeline(config)
result = asyncio.run(pipeline.run(args.topic))
print(f"视频路径:{result}")
if __name__ == "__main__":
main()
9.2 Prompt 模板库
文案生成 Prompt
你是一个专业的抖音/快手短视频文案师。
请根据以下主题,生成一个完整的短视频脚本。
主题:{user_topic}
视频类型:{video_type}(产品展示/促销活动/使用教程)
目标时长:约 60 秒
要求:
1. 开场前 3 秒必须有强吸引力(悬念/数字/冲突)
2. 每个分镜旁白不超过 20 字
3. 画面描述词要具体,适合 AI 生成
4. BGM 风格选择要贴合内容情绪
输出格式(严格 JSON):
{
"title": "视频标题(10字内)",
"hook": "开场钩子(5字)",
"scenes": [{"scene_id": 1, "duration": 5, "narration": "旁白", "visual_prompt": "画面", "bgm_mood": "欢快"}],
"ending": "结尾行动号召"
}
五金产品专用 Prompt
你是一个五金行业资深销售短视频文案师。
主题:{user_topic}
产品类型:{product_category}(电动工具/手动工具/劳保用品等)
请生成包含以下要素的脚本:
1. 痛点切入(使用场景描述)
2. 产品亮点(核心卖点,3 个以内)
3. 促销信息(如有折扣/活动)
4. 行动号召(点击购买/咨询客服)
开场方式:{hook_type}(提问式/对比式/场景式/数字式)
9.3 配置示例(config.yaml)
pipeline:
temp_dir: /tmp/video_generation
output_dir: /data/videos
apis:
minimax:
api_key: ${MINIMAX_API_KEY}
base_url: https://api.minimaxi.com/v1
models:
text: MiniMax-Text-01
video: minimax-video-01
music: minimax-music-01
doubao:
api_key: ${DOUBAN_API_KEY}
model: doubao-vision-01
video:
width: 1080
height: 1920
fps: 30
audio:
voice: zh-CN-XiaoxiaoNeural
voice_volume: 0.8
bgm_volume: 0.25
rate_limit:
max_concurrent_generations: 10
api_retry: 3
9.4 Docker 部署
FROM python:3.11-slim
RUN apt-get update && apt-get install -y ffmpeg curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install -r requirements.txt --no-cache-dir
COPY . /app
WORKDIR /app
ENV TEMP_DIR=/tmp/video_generation
ENV OUTPUT_DIR=/data/videos
EXPOSE 8501
CMD ["streamlit", "run", "web/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
# docker-compose.yml
version: "3.8"
services:
pipeline:
build: .
container_name: pixelle-pipeline
restart: unless-stopped
environment:
- MINIMAX_API_KEY=${MINIMAX_API_KEY}
- DOUBAN_API_KEY=${DOUBAN_API_KEY}
volumes:
- /tmp/video_generation:/tmp/video_generation
- /data/videos:/data/videos
depends_on:
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8501/health"]
interval: 30s
redis:
image: redis:7-alpine
container_name: pixelle-redis
restart: unless-stopped
volumes:
- redis_data:/data
volumes:
redis_data: