From f9e4c92ff1a0f34e5b73cfeff729ac42a9043122 Mon Sep 17 00:00:00 2001
From: HouYunFei <1844025705@qq.com>
Date: Mon, 25 May 2026 17:49:12 +0800
Subject: [PATCH] =?UTF-8?q?refactor(video):=20=E9=87=8D=E6=9E=84=E8=A7=86?=
=?UTF-8?q?=E9=A2=91=E9=85=8D=E7=BD=AE=E5=8F=82=E6=95=B0=E6=A0=87=E5=87=86?=
=?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将视频配置参数标准化逻辑提取为独立的 buildVideoConfig 函数
- 添加视频秒数、尺寸、清晰度的标准化验证函数
- 限制视频秒数只允许 6/10/12/16/20 秒的预设值
- 验证视频尺寸格式是否为标准的宽高比格式
- 优化视频生成 API 调用的错误处理机制
- 统一错误消息显示,修复页面残留生图参数影响视频请求的问题
---
docs/pending-test.md | 1 +
web/src/app/(user)/video/page.tsx | 24 ++++++++++++++++++--
web/src/services/api/video.ts | 37 +++++++++++++++++++++----------
3 files changed, 48 insertions(+), 14 deletions(-)
diff --git a/docs/pending-test.md b/docs/pending-test.md
index 7184827..453d3bb 100644
--- a/docs/pending-test.md
+++ b/docs/pending-test.md
@@ -9,3 +9,4 @@
- 视频清晰度输入框改为只输入数字,提交请求时再拼接 `p` 单位。
- 视频生成前端会识别后端 `{ code, msg }` 错误响应,创建失败不再继续轮询 `undefined`。
- 新增 `/video` 视频创作台页面,参考生图工作台布局,支持提示词、参考图、视频参数、生成结果、保存素材、下载和本地生成记录;清晰度和秒数均支持常用值选择与手动输入。
+- 视频创作台生成前会把模型、尺寸、秒数、清晰度归一化为视频接口支持的参数,并展示后端返回的错误信息,避免页面侧残留的生图参数影响视频请求。
diff --git a/web/src/app/(user)/video/page.tsx b/web/src/app/(user)/video/page.tsx
index 71b7675..3ed78ee 100644
--- a/web/src/app/(user)/video/page.tsx
+++ b/web/src/app/(user)/video/page.tsx
@@ -173,7 +173,7 @@ export default function VideoPage() {
openConfigDialog(true);
return null;
}
- return { text, config: { ...effectiveConfig, model }, references: [...references] };
+ return { text, config: buildVideoConfig(effectiveConfig, model), references: [...references] };
};
const retryResult = () => {
@@ -306,7 +306,7 @@ export default function VideoPage() {
- {model} · {effectiveConfig.vquality}p · {effectiveConfig.size} · {effectiveConfig.videoSeconds}s
+ {model} · {normalizeResolution(effectiveConfig.vquality)}p · {normalizeVideoSize(effectiveConfig.size)} · {normalizeVideoSeconds(effectiveConfig.videoSeconds)}s
} onClick={() => setSettingsOpen(true)}>
调整
@@ -569,6 +569,26 @@ function buildLog({ prompt, model, config, durationMs, status, video, error }: {
};
}
+function buildVideoConfig(config: AiConfig, model: string): AiConfig {
+ return {
+ ...config,
+ model,
+ videoModel: model,
+ size: normalizeVideoSize(config.size),
+ videoSeconds: normalizeVideoSeconds(config.videoSeconds),
+ vquality: normalizeResolution(config.vquality),
+ };
+}
+
+function normalizeVideoSeconds(value: string) {
+ const seconds = Math.floor(Number(value) || 6);
+ return String([6, 10, 12, 16, 20].includes(seconds) ? seconds : 6);
+}
+
+function normalizeVideoSize(value: string) {
+ return /^\d+x\d+$/.test(value || "") ? value : "1280x720";
+}
+
function normalizeResolution(value: string) {
return value.replace(/p$/i, "") || "720";
}
diff --git a/web/src/services/api/video.ts b/web/src/services/api/video.ts
index 23e83f6..ef78daa 100644
--- a/web/src/services/api/video.ts
+++ b/web/src/services/api/video.ts
@@ -33,22 +33,27 @@ export async function requestVideoGeneration(config: AiConfig, prompt: string, r
body.append("preset", "normal");
const files = await Promise.all(references.slice(0, 7).map(async (image) => dataUrlToFile({ ...image, dataUrl: await imageToDataUrl(image) })));
files.forEach((file) => body.append("input_reference[]", file));
- const created = unwrapVideoResponse((await axios.post
(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) })).data);
- if (!created.id) throw new Error("视频接口没有返回任务 ID");
- for (;;) {
- const video = unwrapVideoResponse((await axios.get(aiApiUrl(config, `/videos/${created.id}`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined })).data);
- if (video.status === "completed") break;
- if (video.status === "failed" || video.status === "cancelled") throw new Error(video.error?.message || "视频生成失败");
- await new Promise((resolve) => setTimeout(resolve, 2500));
+ try {
+ const created = unwrapVideoResponse((await axios.post(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) })).data);
+ if (!created.id) throw new Error("视频接口没有返回任务 ID");
+ for (;;) {
+ const video = unwrapVideoResponse((await axios.get(aiApiUrl(config, `/videos/${created.id}`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined })).data);
+ if (video.status === "completed") break;
+ if (video.status === "failed" || video.status === "cancelled") throw new Error(video.error?.message || "视频生成失败");
+ await new Promise((resolve) => setTimeout(resolve, 2500));
+ }
+ const content = await axios.get(aiApiUrl(config, `/videos/${created.id}/content`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined, responseType: "blob" });
+ await assertVideoBlob(content.data);
+ refreshRemoteUser(config);
+ return content.data;
+ } catch (error) {
+ throw new Error(readAxiosError(error, "视频生成失败"));
}
- const content = await axios.get(aiApiUrl(config, `/videos/${created.id}/content`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined, responseType: "blob" });
- await assertVideoBlob(content.data);
- refreshRemoteUser(config);
- return content.data;
}
function normalizeVideoSeconds(value: string) {
- return String(Math.max(1, Math.floor(Number(value) || 6)));
+ const seconds = Math.floor(Number(value) || 6);
+ return String([6, 10, 12, 16, 20].includes(seconds) ? seconds : 6);
}
function normalizeVideoSize(value: string) {
@@ -74,6 +79,14 @@ function unwrapVideoResponse(payload: ApiVideoResponse) {
return payload;
}
+function readAxiosError(error: unknown, fallback: string) {
+ if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
+ const responseData = error.response?.data;
+ return responseData?.msg || responseData?.error?.message || (error.response?.status ? `${fallback}:${error.response.status}` : fallback);
+ }
+ return error instanceof Error ? error.message : fallback;
+}
+
async function assertVideoBlob(blob: Blob) {
if (!blob.type.includes("json")) return;
let payload: { code?: number; msg?: string };