157 lines
5.4 KiB
JavaScript
157 lines
5.4 KiB
JavaScript
/**
|
||
* 视频生成路由
|
||
* POST /api/video/generate — 提交生成任务
|
||
* GET /api/video/status/:id — 查询进度
|
||
* GET /api/video/preview/:id — 获取预览URL
|
||
* GET /api/video/download/:id — 下载视频
|
||
*/
|
||
const express = require('express');
|
||
const router = express.Router();
|
||
const { v4: uuidv4 } = require('uuid');
|
||
const videoDispatch = require('../services/video-dispatch');
|
||
|
||
// 任务存储 (P0: 内存Map,P1迁移到PostgreSQL)
|
||
const tasks = new Map();
|
||
|
||
// ── 提交生成任务 ────────────────────────────────────
|
||
router.post('/generate', async (req, res) => {
|
||
try {
|
||
const { prompt, model = 'jimeng', duration = '5', resolution = '1080p', style } = req.body;
|
||
|
||
if (!prompt || !prompt.trim()) {
|
||
return res.status(400).json({ error: '提示词不能为空' });
|
||
}
|
||
if (prompt.length > 3000) {
|
||
return res.status(400).json({ error: '提示词超过1000字限制' });
|
||
}
|
||
|
||
const taskId = uuidv4();
|
||
const task = {
|
||
id: taskId,
|
||
prompt: prompt.trim(),
|
||
model,
|
||
params: { duration, resolution, style },
|
||
status: 'pending',
|
||
progress: 0,
|
||
createdAt: new Date().toISOString(),
|
||
completedAt: null,
|
||
videoUrl: null,
|
||
previewUrl: null,
|
||
apiTaskId: null,
|
||
error: null,
|
||
};
|
||
tasks.set(taskId, task);
|
||
|
||
// 异步启动生成
|
||
processVideoTask(taskId, req.app.get('io'));
|
||
|
||
res.json({ taskId, status: 'pending', message: '视频生成任务已提交' });
|
||
} catch (err) {
|
||
console.error('[POST /api/video/generate]', err);
|
||
res.status(500).json({ error: '服务器内部错误' });
|
||
}
|
||
});
|
||
|
||
// ── 查询进度 ────────────────────────────────────────
|
||
router.get('/status/:taskId', (req, res) => {
|
||
const task = tasks.get(req.params.taskId);
|
||
if (!task) return res.status(404).json({ error: '任务不存在' });
|
||
|
||
res.json({
|
||
taskId: task.id,
|
||
status: task.status,
|
||
progress: task.progress,
|
||
videoUrl: task.previewUrl,
|
||
error: task.error,
|
||
createdAt: task.createdAt,
|
||
completedAt: task.completedAt,
|
||
});
|
||
});
|
||
|
||
// ── 预览URL ─────────────────────────────────────────
|
||
router.get('/preview/:taskId', (req, res) => {
|
||
const task = tasks.get(req.params.taskId);
|
||
if (!task) return res.status(404).json({ error: '任务不存在' });
|
||
if (task.status !== 'completed') {
|
||
return res.status(400).json({ error: '视频尚未生成完成', status: task.status });
|
||
}
|
||
res.json({ previewUrl: task.previewUrl });
|
||
});
|
||
|
||
// ── 下载 ────────────────────────────────────────────
|
||
router.get('/download/:taskId', (req, res) => {
|
||
const task = tasks.get(req.params.taskId);
|
||
if (!task) return res.status(404).json({ error: '任务不存在' });
|
||
if (task.status !== 'completed') {
|
||
return res.status(400).json({ error: '视频尚未生成完成' });
|
||
}
|
||
res.json({ downloadUrl: task.videoUrl || task.previewUrl });
|
||
});
|
||
|
||
// ── 异步处理 ────────────────────────────────────────
|
||
async function processVideoTask(taskId, io) {
|
||
const task = tasks.get(taskId);
|
||
if (!task) return;
|
||
|
||
const emit = (data) => io.emit('video:progress', { taskId, ...data });
|
||
|
||
try {
|
||
// 1) 提交到即梦
|
||
task.status = 'generating';
|
||
task.progress = 10;
|
||
emit({ status: 'generating', progress: 10, message: '正在提交到即梦...' });
|
||
|
||
const submitResult = await videoDispatch.submitTask({
|
||
prompt: task.prompt,
|
||
duration: task.params.duration,
|
||
resolution: task.params.resolution,
|
||
style: task.params.style,
|
||
});
|
||
|
||
task.apiTaskId = submitResult.taskId;
|
||
task.progress = 20;
|
||
emit({ status: 'generating', progress: 20, message: '任务已提交,等待生成...' });
|
||
|
||
// 2) 轮询 (每5秒,最多10分钟)
|
||
const MAX_ATTEMPTS = 120;
|
||
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
||
await sleep(5000);
|
||
|
||
const result = await videoDispatch.queryTask(submitResult.taskId);
|
||
|
||
if (result.status === 'completed') {
|
||
task.status = 'completed';
|
||
task.progress = 100;
|
||
task.videoUrl = result.videoUrl;
|
||
task.previewUrl = result.videoUrl;
|
||
task.completedAt = new Date().toISOString();
|
||
emit({ status: 'completed', progress: 100, message: '✅ 视频生成完成!', videoUrl: result.videoUrl });
|
||
console.log(`[video] ✅ 完成: ${taskId}`);
|
||
return;
|
||
}
|
||
|
||
if (result.status === 'failed') {
|
||
throw new Error(result.error || '即梦返回生成失败');
|
||
}
|
||
|
||
// 进度: 20→90 线性
|
||
const progress = Math.min(90, 20 + Math.floor((i / MAX_ATTEMPTS) * 70));
|
||
task.progress = progress;
|
||
emit({ status: 'generating', progress, message: `生成中... ${progress}%` });
|
||
}
|
||
|
||
throw new Error('生成超时(超过10分钟)');
|
||
} catch (err) {
|
||
task.status = 'failed';
|
||
task.error = err.message;
|
||
emit({ status: 'failed', progress: 0, message: `❌ ${err.message}` });
|
||
console.error(`[video] ❌ 失败: ${taskId}`, err.message);
|
||
}
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
module.exports = router;
|