AI 视频生成功能
基于火山引擎豆包 API 的异步视频生成实现指南
AI 视频生成功能
Sistine Starter 集成了火山引擎豆包 API 的视频生成功能,支持图生视频 (Image-to-Video) 和文生视频 (Text-to-Video)。由于视频生成是异步任务,需要通过轮询机制检查任务状态。本文档详细介绍视频生成系统的架构、异步流程和集成方法。
功能概览
核心特性
- ✅ 图生视频 (I2V): 基于静态图像生成动态视频
- ✅ 文生视频 (T2V): 根据文本描述生成视频
- ✅ 异步任务处理: 提交任务后通过轮询获取结果
- ✅ 积分扣费: 每次生成消耗 50 积分,事务性扣费确保数据一致性
- ✅ 结果存储: 生成结果自动上传到 R2 存储,长期保存
- ✅ 超时保护: 5 分钟超时机制防止任务卡死
- ✅ 多参数支持: 分辨率、时长、水印、镜头固定等
技术架构
用户提交生成请求
↓
身份认证 (Better Auth)
↓
检查积分余额 (50 积分)
↓
创建生成记录 (generationHistory)
↓
扣除积分 + 记录账本
↓
调用火山引擎 API (异步任务)
↓
返回 taskId
↓
前端轮询任务状态 (/api/video/status)
↓
任务完成 → 上传到 R2 存储
↓
更新生成记录 (completed)
↓
返回视频 URL
火山引擎配置
模型选择
当前使用的模型是 doubao-seedance-1-0-pro-250528
,这是火山引擎豆包系列的视频生成专用模型。
模型特性:
- 支持图生视频和文生视频
- 输出分辨率: 最高 1080p
- 视频时长: 5-15 秒
- 生成时间: 约 2-5 分钟/个
配置位置: lib/volcano-engine/video.ts:105
const model = volcanoEngineConfig.videoModel || 'doubao-seedance-1-0-pro-250528';
API 参数配置
可用参数:
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
prompt | string | 可选 | 视频生成的文本描述 (文生视频时必需) |
imageUrl | string | 可选 | 参考图像 URL (图生视频时必需) |
resolution | string | - | 输出分辨率 (480p / 720p / 1080p ) |
duration | number | - | 视频时长 (秒),建议 5-15 秒 |
watermark | boolean | - | 是否添加水印 |
cameraFixed | boolean | - | 镜头是否固定 (图生视频时有效) |
调整参数示例:
// 生成高清视频,镜头固定
const result = await volcanoEngine.generateVideoFromImage(
imageUrl,
'人物缓慢微笑',
{
resolution: '1080p',
duration: 10,
watermark: false,
cameraFixed: true,
}
);
分辨率说明
选项 | 说明 | 积分建议 | 生成时间 |
---|---|---|---|
480p | 标清 (约 854x480) | 40 积分 | 约 2 分钟 |
720p | 高清 (约 1280x720) | 50 积分 | 约 3 分钟 |
1080p | 全高清 (约 1920x1080) | 70 积分 | 约 5 分钟 |
数据库架构
生成历史表 (generationHistory)
视频生成使用与图像生成相同的表,通过 type
字段区分。建表语句请参考 数据库
文档中的 generationHistory
表定义。
关键字段说明:
字段 | 类型 | 说明 |
---|---|---|
type | TEXT | 固定为 'video' |
prompt | TEXT | 用户输入的提示词 (图生视频时为动作描述) |
imageUrl | TEXT | 参考图像 URL (图生视频必需,文生视频为 null) |
taskId | TEXT | 火山引擎返回的任务 ID,用于轮询状态 |
resultUrl | TEXT | 视频完成后的 URL (存储在 R2) |
status | TEXT | pending → processing → completed / failed |
creditsUsed | INTEGER | 固定为 50 积分 |
Metadata 示例:
{
"duration": 10,
"resolution": "1080p",
"watermark": false,
"cameraFixed": true
}
异步任务流程
流程图
┌─────────────────────────────────────────────────────────┐
│ 1. 用户提交生成请求 (POST /api/video/generate) │
│ - 扣除 50 积分 │
│ - 创建 generationHistory 记录 (status: pending) │
│ - 调用火山引擎 API │
│ - 返回 taskId 和 historyId │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. 前端开始轮询 (GET /api/video/status?taskId=xxx) │
│ 每 5 秒轮询一次 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────┴─────────┐
│ 任务仍在处理中? │
└─────────┬─────────┘
│
┌──────────┼──────────┐
│ │ │
是 否 │
│ │ │
▼ ▼ │
继续轮询 任务完成 │
│ │
▼ │
┌───────────────────────┐ │
│ 3. 上传视频到 R2 │ │
│ 4. 更新记录为 │ │
│ completed │ │
│ 5. 返回视频 URL │ │
└───────────────────────┘ │
│
▼
┌───────────────┐
│ 任务失败或 │
│ 超时 (5分钟) │
│ 更新为 failed│
└───────────────┘
步骤详解
步骤 1: 提交生成请求
端点: POST /api/video/generate
实现文件: app/api/video/generate/route.ts
请求格式:
// 图生视频 (Image-to-Video)
{
"imageUrl": "https://example.com/photo.jpg",
"prompt": "人物缓慢微笑,背景保持静止",
"duration": 10,
"resolution": "1080p",
"watermark": false
}
// 文生视频 (Text-to-Video)
{
"prompt": "夕阳下的海滩,海浪轻轻拍打岸边",
"duration": 15,
"resolution": "720p",
"watermark": false
}
响应格式:
{
"id": "uuid-history-id",
"taskId": "volcano-task-id-xxx",
"status": "processing",
"remainingCredits": 250
}
关键字段:
id
: 生成记录 ID (用于后续查询历史)taskId
: 火山引擎任务 ID (必须传给状态查询接口)status
: 初始状态为"processing"
步骤 2: 轮询任务状态
端点: GET /api/video/status?taskId=xxx&historyId=xxx
实现文件: app/api/video/status/route.ts
请求参数:
参数 | 类型 | 必需 | 说明 |
---|---|---|---|
taskId | string | 是 | 火山引擎返回的任务 ID |
historyId | string | 是 | 生成记录 ID (用于权限验证) |
响应格式 (处理中):
{
"id": "uuid-history-id",
"taskId": "volcano-task-id-xxx",
"status": "processing",
"taskStatus": "RUNNING",
"statusText": "视频生成中",
"elapsedSeconds": 45
}
响应格式 (完成):
{
"id": "uuid-history-id",
"taskId": "volcano-task-id-xxx",
"status": "completed",
"videoUrl": "https://your-r2-storage.com/videos/user_123/video_xxx.mp4"
}
响应格式 (失败):
{
"id": "uuid-history-id",
"taskId": "volcano-task-id-xxx",
"status": "failed",
"error": "Video generation timeout after 5 minutes"
}
API 实现详解
生成端点 (POST /api/video/generate)
核心实现逻辑
export async function POST(req: NextRequest) {
try {
// 1. 身份认证
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.session?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.session.userId;
// 2. 解析请求参数
const { prompt, imageUrl, duration, resolution, watermark } = await req.json();
// 3. 验证参数 (至少需要 prompt 或 imageUrl)
if (!prompt && !imageUrl) {
return NextResponse.json({
error: "Either prompt or imageUrl is required"
}, { status: 400 });
}
// 4. 检查积分余额 (50 积分)
const creditsNeeded = 50;
const hasCredits = await canUserAfford(userId, creditsNeeded);
if (!hasCredits) {
return NextResponse.json({
error: "Insufficient credits",
creditsNeeded,
remainingCredits: 0
}, { status: 402 });
}
// 5. 创建生成记录
const historyId = randomUUID();
await db.insert(generationHistory).values({
id: historyId,
userId,
type: "video",
prompt: prompt || "Image to video generation",
imageUrl,
status: "pending",
creditsUsed: creditsNeeded,
metadata: JSON.stringify({ duration, resolution, watermark }),
});
// 6. 扣除积分
const deductResult = await deductCredits(
userId,
creditsNeeded,
"video_generation",
historyId
);
if (!deductResult.success) {
// 更新记录为失败
await db.update(generationHistory)
.set({ status: "failed", error: deductResult.error })
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
error: deductResult.error || "Failed to deduct credits",
remainingCredits: deductResult.remainingCredits
}, { status: 402 });
}
try {
// 7. 调用火山引擎 API
let result;
if (imageUrl) {
// 图生视频
result = await volcanoEngine.generateVideoFromImage(
imageUrl,
prompt || "Generate video from image",
{ duration, resolution, watermark }
);
} else if (prompt) {
// 文生视频
result = await volcanoEngine.generateVideoFromText(
prompt,
{ duration, resolution, watermark }
);
}
// 8. 更新记录为 processing 状态,保存 taskId
await db.update(generationHistory)
.set({
status: "processing",
taskId: result.taskId,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
// 9. 返回任务信息
return NextResponse.json({
id: historyId,
taskId: result.taskId,
status: result.status,
remainingCredits: deductResult.remainingCredits,
});
} catch (genError: any) {
// 生成失败: 更新记录
await db.update(generationHistory)
.set({
status: "failed",
error: genError.message,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
throw genError;
}
} catch (error: any) {
console.error("Video generation API error:", error);
return NextResponse.json({
error: error.message || "Failed to generate video"
}, { status: 500 });
}
}
状态查询端点 (GET /api/video/status)
核心实现逻辑
export async function GET(req: NextRequest) {
try {
// 1. 身份认证
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.session?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.session.userId;
// 2. 获取查询参数
const searchParams = req.nextUrl.searchParams;
const taskId = searchParams.get("taskId");
const historyId = searchParams.get("historyId");
if (!taskId || !historyId) {
return NextResponse.json({
error: "taskId and historyId are required"
}, { status: 400 });
}
// 3. 验证记录所有权
const history = await db
.select()
.from(generationHistory)
.where(and(
eq(generationHistory.id, historyId),
eq(generationHistory.userId, userId),
eq(generationHistory.type, "video")
))
.limit(1);
if (!history || history.length === 0) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
const record = history[0];
// 4. 如果已经完成或失败,直接返回缓存结果
if (record.status === "completed" || record.status === "failed") {
return NextResponse.json({
id: historyId,
taskId: record.taskId,
status: record.status,
videoUrl: record.resultUrl,
error: record.error,
});
}
// 5. 查询火山引擎任务状态
try {
const status = await volcanoEngine.getVideoStatus(taskId);
// 解析状态响应
const taskData = status.data || status;
const rawTaskStatus = taskData.task_status || taskData.status || status.status || '';
const taskStatusText = taskData.task_status_text || '';
const normalizedStatus = normalizeStatus(rawTaskStatus, taskStatusText);
// 提取视频 URL (可能存在于多个字段)
const videoUrl = taskData.result?.video_url ||
taskData.result?.url ||
taskData.result?.video?.[0]?.url ||
// ... 其他可能的字段
const isCompleted = normalizedStatus === 'completed';
const isFailed = normalizedStatus === 'failed';
// 6. 任务完成: 上传视频到 R2
if (isCompleted || videoUrl) {
if (videoUrl) {
try {
// 上传到 R2 (失败则使用原始 URL)
const finalUrl = await uploadImageFromUrl(videoUrl, userId, 'video')
.catch(() => videoUrl);
// 更新记录为完成
await db.update(generationHistory)
.set({
status: "completed",
resultUrl: finalUrl,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
id: historyId,
taskId,
status: "completed",
videoUrl: finalUrl,
});
} catch (error) {
console.error('Error processing video URL:', error);
return NextResponse.json({
id: historyId,
taskId,
status: "completed",
videoUrl: videoUrl,
});
}
}
}
// 7. 任务失败
else if (isFailed) {
const errorMessage = taskData.error ||
taskData.result?.error ||
status.error ||
taskStatusText ||
"Video generation failed";
await db.update(generationHistory)
.set({
status: "failed",
error: errorMessage,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
id: historyId,
taskId,
status: "failed",
error: errorMessage,
});
}
// 8. 超时检查 (5 分钟)
const createdAt = new Date(record.createdAt).getTime();
const now = Date.now();
const elapsedMinutes = (now - createdAt) / (1000 * 60);
if (elapsedMinutes > 5) {
await db.update(generationHistory)
.set({
status: "failed",
error: "Video generation timeout",
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
id: historyId,
taskId,
status: "failed",
error: "Video generation timeout after 5 minutes",
});
}
// 9. 仍在处理中: 返回状态
return NextResponse.json({
id: historyId,
taskId,
status: "processing",
taskStatus: rawTaskStatus,
statusText: taskStatusText,
elapsedSeconds: Math.floor((now - createdAt) / 1000),
});
} catch (error: any) {
console.error("Error checking video status:", error);
return NextResponse.json({
id: historyId,
taskId,
status: "processing",
error: "Failed to check status",
});
}
} catch (error: any) {
console.error("Video status API error:", error);
return NextResponse.json({
error: error.message || "Failed to get video status"
}, { status: 500 });
}
}
积分扣费机制
扣费规则
- 消耗标准: 每次视频生成消耗 50 积分
- 扣费时机: 在提交任务之前扣除,即使生成失败也不退款
- 失败退款: 当前不会自动退款 (可根据需求自行添加)
积分消耗定义
定义位置: app/api/video/generate/route.ts:36
const creditsNeeded = 50; // 视频生成消耗 50 积分
动态定价建议:
// 根据分辨率和时长动态计算
const baseCost = 50;
const resolutionMultiplier = {
'480p': 0.8, // 40 积分
'720p': 1.0, // 50 积分
'1080p': 1.4, // 70 积分
};
const durationMultiplier = duration > 10 ? 1.5 : 1.0;
const creditsNeeded = Math.ceil(
baseCost * resolutionMultiplier[resolution] * durationMultiplier
);
客户端集成示例
前端实现 (React)
import { useState, useEffect, useRef } from "react";
export function VideoGeneratorComponent() {
const [prompt, setPrompt] = useState("");
const [imageUrl, setImageUrl] = useState("");
const [videoUrl, setVideoUrl] = useState<string | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [taskId, setTaskId] = useState<string | null>(null);
const [historyId, setHistoryId] = useState<string | null>(null);
const [progress, setProgress] = useState<string>("");
const [credits, setCredits] = useState<number>(0);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// 提交生成请求
const handleGenerate = async () => {
if (!prompt && !imageUrl) {
alert("请输入提示词或参考图像 URL");
return;
}
setIsGenerating(true);
setVideoUrl(null);
setProgress("提交生成请求...");
try {
const response = await fetch("/api/video/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
imageUrl,
duration: 10,
resolution: "720p",
watermark: false,
}),
});
if (!response.ok) {
const error = await response.json();
if (response.status === 402) {
alert(`积分不足!需要 ${error.creditsNeeded} 积分`);
return;
}
throw new Error(error.error || "Failed to start generation");
}
const result = await response.json();
setTaskId(result.taskId);
setHistoryId(result.id);
setCredits(result.remainingCredits);
setProgress("生成中,请稍候...");
// 开始轮询
startPolling(result.taskId, result.id);
} catch (error: any) {
console.error("Video generation error:", error);
alert(error.message);
setIsGenerating(false);
}
};
// 开始轮询任务状态
const startPolling = (taskId: string, historyId: string) => {
// 清除旧的轮询
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
// 每 5 秒轮询一次
pollingIntervalRef.current = setInterval(async () => {
try {
const response = await fetch(
`/api/video/status?taskId=${taskId}&historyId=${historyId}`
);
if (!response.ok) {
throw new Error("Failed to check status");
}
const status = await response.json();
if (status.status === "completed") {
// 完成
setVideoUrl(status.videoUrl);
setProgress("生成完成!");
setIsGenerating(false);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
} else if (status.status === "failed") {
// 失败
alert(`生成失败: ${status.error}`);
setProgress(`失败: ${status.error}`);
setIsGenerating(false);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
} else {
// 仍在处理
const elapsed = status.elapsedSeconds || 0;
setProgress(`生成中... 已用时 ${elapsed} 秒`);
}
} catch (error) {
console.error("Polling error:", error);
}
}, 5000); // 5 秒轮询一次
};
// 组件卸载时清理轮询
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
return (
<div>
{/* 积分显示 */}
<div>剩余积分: {credits}</div>
{/* 输入区域 */}
<div>
<label>参考图像 URL (图生视频):</label>
<input
type="text"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg (可选)"
disabled={isGenerating}
/>
</div>
<div>
<label>提示词:</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="人物缓慢微笑,背景保持静止"
disabled={isGenerating}
/>
</div>
<button
onClick={handleGenerate}
disabled={isGenerating || (!prompt && !imageUrl)}
>
{isGenerating ? "生成中..." : "生成视频 (50 积分)"}
</button>
{/* 进度显示 */}
{progress && <div>{progress}</div>}
{/* 结果显示 */}
{videoUrl && (
<div>
<h3>生成结果:</h3>
<video src={videoUrl} controls style={{ maxWidth: "100%" }} />
<a href={videoUrl} download>下载视频</a>
</div>
)}
</div>
);
}
Fetch API 示例 (纯 JavaScript)
async function generateVideo(prompt, imageUrl, options = {}) {
// 1. 提交生成请求
const generateResponse = await fetch('/api/video/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
imageUrl,
duration: options.duration || 10,
resolution: options.resolution || '720p',
watermark: options.watermark !== undefined ? options.watermark : false,
}),
});
if (!generateResponse.ok) {
const error = await generateResponse.json();
throw new Error(error.error);
}
const { taskId, id: historyId } = await generateResponse.json();
console.log('任务已提交, Task ID:', taskId);
// 2. 轮询任务状态
return new Promise((resolve, reject) => {
const pollInterval = setInterval(async () => {
try {
const statusResponse = await fetch(
`/api/video/status?taskId=${taskId}&historyId=${historyId}`
);
if (!statusResponse.ok) {
throw new Error('Failed to check status');
}
const status = await statusResponse.json();
if (status.status === 'completed') {
clearInterval(pollInterval);
console.log('视频生成完成!');
resolve(status.videoUrl);
} else if (status.status === 'failed') {
clearInterval(pollInterval);
reject(new Error(status.error || 'Generation failed'));
} else {
console.log('生成中...', status.elapsedSeconds, '秒');
}
} catch (error) {
clearInterval(pollInterval);
reject(error);
}
}, 5000); // 5 秒轮询一次
// 设置最大等待时间 (10 分钟)
setTimeout(() => {
clearInterval(pollInterval);
reject(new Error('Timeout: Generation took too long'));
}, 10 * 60 * 1000);
});
}
// 使用示例
generateVideo(
'人物缓慢微笑',
'https://example.com/photo.jpg',
{ resolution: '720p' }
)
.then(videoUrl => {
console.log('视频 URL:', videoUrl);
})
.catch(error => {
console.error('生成失败:', error.message);
});
错误处理与超时
常见错误
错误信息 | 原因 | 解决方案 |
---|---|---|
"Either prompt or imageUrl is required" | 缺少输入参数 | 提供提示词或参考图像 |
"Insufficient credits" | 积分不足 | 提示用户购买积分 |
"Video generation timeout after 5 minutes" | 任务超时 | 重试或联系支持 |
"Failed to upload to R2" | R2 存储配置错误 | 检查环境变量 |
超时保护机制
后端超时检查 (app/api/video/status/route.ts:162-182
):
// 检查任务是否超过 5 分钟
const createdAt = new Date(record.createdAt).getTime();
const now = Date.now();
const elapsedMinutes = (now - createdAt) / (1000 * 60);
if (elapsedMinutes > 5) {
// 标记为失败
await db.update(generationHistory)
.set({
status: "failed",
error: "Video generation timeout",
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
status: "failed",
error: "Video generation timeout after 5 minutes",
});
}
前端超时保护:
// 设置最大轮询时间 (10 分钟)
const maxPollingTime = 10 * 60 * 1000;
const pollingStartTime = Date.now();
const pollInterval = setInterval(() => {
if (Date.now() - pollingStartTime > maxPollingTime) {
clearInterval(pollInterval);
alert('生成超时,请稍后重试');
setIsGenerating(false);
}
// ... 轮询逻辑
}, 5000);
高级功能
断点续传 (恢复轮询)
如果用户刷新页面,可以从 localStorage 恢复轮询:
// 保存任务信息到 localStorage
localStorage.setItem('videoTask', JSON.stringify({
taskId,
historyId,
startTime: Date.now(),
}));
// 页面加载时恢复
useEffect(() => {
const savedTask = localStorage.getItem('videoTask');
if (savedTask) {
const { taskId, historyId, startTime } = JSON.parse(savedTask);
// 检查是否超过 10 分钟
if (Date.now() - startTime < 10 * 60 * 1000) {
setTaskId(taskId);
setHistoryId(historyId);
setIsGenerating(true);
startPolling(taskId, historyId);
} else {
localStorage.removeItem('videoTask');
}
}
}, []);
// 完成或失败时清除
if (status.status === 'completed' || status.status === 'failed') {
localStorage.removeItem('videoTask');
}
批量生成
async function batchGenerateVideos(tasks: Array<{ prompt: string, imageUrl?: string }>) {
const results = [];
for (const task of tasks) {
try {
const videoUrl = await generateVideo(task.prompt, task.imageUrl);
results.push({ success: true, videoUrl });
} catch (error: any) {
results.push({ success: false, error: error.message });
}
// 添加延迟避免频繁调用
await new Promise(resolve => setTimeout(resolve, 10000));
}
return results;
}
进度估算
根据经验数据估算剩余时间:
// 假设平均生成时间为 3 分钟
const averageGenerationTime = 3 * 60; // 秒
const elapsedSeconds = status.elapsedSeconds || 0;
const remainingSeconds = Math.max(0, averageGenerationTime - elapsedSeconds);
const progressPercent = Math.min(100, (elapsedSeconds / averageGenerationTime) * 100);
console.log(`进度: ${progressPercent.toFixed(0)}%`);
console.log(`预计剩余时间: ${remainingSeconds} 秒`);
性能优化建议
1. 轮询频率优化
// 动态调整轮询间隔
let pollInterval = 5000; // 初始 5 秒
if (elapsedSeconds > 60) {
pollInterval = 10000; // 超过 1 分钟后改为 10 秒
}
if (elapsedSeconds > 180) {
pollInterval = 15000; // 超过 3 分钟后改为 15 秒
}
2. WebSocket 替代轮询
对于大量并发任务,可以使用 WebSocket 推送状态:
// 服务端 (伪代码)
wss.on('connection', (ws) => {
ws.on('message', async (taskId) => {
const status = await checkVideoStatus(taskId);
ws.send(JSON.stringify(status));
});
});
// 客户端
const ws = new WebSocket('wss://your-domain.com/video-status');
ws.send(taskId);
ws.onmessage = (event) => {
const status = JSON.parse(event.data);
if (status.status === 'completed') {
setVideoUrl(status.videoUrl);
}
};
3. 缓存任务状态
使用 Redis 缓存任务状态,减少数据库查询:
import { redis } from "@/lib/redis";
// 缓存任务状态
await redis.set(`video:task:${taskId}`, JSON.stringify(status), { ex: 600 });
// 读取缓存
const cached = await redis.get(`video:task:${taskId}`);
if (cached) return JSON.parse(cached);
相关文档
- 积分系统 - 积分扣费和账本管理
- AI 对话功能 - 对话功能文档
- AI 图像生成 - 图像生成功能文档
- 火山引擎视频 API 文档 - 官方 API 文档
总结
AI 视频生成功能通过以下关键设计提供稳定的异步任务处理:
- 异步任务机制: 提交任务后通过轮询获取结果,避免长时间阻塞
- 超时保护: 5 分钟超时机制防止任务卡死
- 断点续传: 支持页面刷新后恢复轮询
- 长期存储: 自动上传到 R2,避免临时 URL 失效
- 事务性扣费: 确保积分扣除和记录的一致性
按照本文档的指导,你可以快速集成视频生成功能并根据业务需求进行优化。