Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

157 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 视频生成路由
* 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: 内存MapP1迁移到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;