feat(video): 对齐 grok-imagine-video 接口并优化视频生成功能

- 将默认视频模型从 sora-2 更改为 grok-imagine-video
- 新增分辨率选项包括 720p 和 480p,替换原有的质量选项
- 扩展视频尺寸选项,新增方形、宽屏、长图等预设尺寸
- 添加分辨率和尺寸手动输入功能,支持自定义宽高
- 更新视频生成 API 参数为 resolution_name、preset 和 input_reference[]
- 支持最多 7 张参考图片上传
- 优化视频设置浮层定位,避免被工具栏遮挡
- 识别后端错误响应格式,改进视频生成失败处理逻辑
This commit is contained in:
HouYunFei
2026-05-25 17:15:42 +08:00
parent b21f8c3af9
commit 8c506f92f9
5 changed files with 146 additions and 38 deletions
+40 -7
View File
@@ -7,6 +7,7 @@ import { useUserStore } from "@/stores/use-user-store";
import type { ReferenceImage } from "@/types/image";
type VideoResponse = { id: string; status?: string; error?: { message?: string } };
type ApiVideoResponse = VideoResponse | { code?: number; data?: VideoResponse | null; msg?: string };
function aiApiUrl(config: AiConfig, path: string) {
return config.channelMode === "remote" ? `/api/v1${path}` : buildApiUrl(config.baseUrl, path);
@@ -28,16 +29,20 @@ export async function requestVideoGeneration(config: AiConfig, prompt: string, r
body.append("prompt", prompt);
body.append("seconds", normalizeVideoSeconds(config.videoSeconds));
if (normalizeVideoSize(config.size)) body.append("size", normalizeVideoSize(config.size)!);
if (config.vquality) body.append("vquality", config.vquality);
if (references[0]) body.append("input_reference", dataUrlToFile({ ...references[0], dataUrl: await imageToDataUrl(references[0]) }));
const created = await axios.post<VideoResponse>(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) });
body.append("resolution_name", normalizeVideoResolution(config.vquality));
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<ApiVideoResponse>(aiApiUrl(config, "/videos"), body, { headers: aiHeaders(config) })).data);
if (!created.id) throw new Error("视频接口没有返回任务 ID");
for (;;) {
const video = await axios.get<VideoResponse>(aiApiUrl(config, `/videos/${created.data.id}`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined });
if (video.data.status === "completed") break;
if (video.data.status === "failed" || video.data.status === "cancelled") throw new Error(video.data.error?.message || "视频生成失败");
const video = unwrapVideoResponse((await axios.get<ApiVideoResponse>(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<Blob>(aiApiUrl(config, `/videos/${created.data.id}/content`), { headers: aiHeaders(config), params: config.channelMode === "remote" ? { model } : undefined, responseType: "blob" });
const content = await axios.get<Blob>(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;
}
@@ -51,3 +56,31 @@ function normalizeVideoSize(value: string) {
if (/^\d+x\d+$/.test(size)) return size;
return ["9:16", "2:3", "3:4"].includes(size) ? "720x1280" : "1280x720";
}
function normalizeVideoResolution(value: string) {
if (value === "low") return "480p";
if (value === "auto" || value === "high" || value === "medium") return "720p";
const resolution = value.replace(/p$/i, "") || "720";
return `${resolution}p`;
}
function unwrapVideoResponse(payload: ApiVideoResponse) {
if (!payload) throw new Error("接口没有返回视频任务");
if ("code" in payload && typeof payload.code === "number") {
if (payload.code !== 0) throw new Error(payload.msg || "请求失败");
if (!payload.data) throw new Error("接口没有返回视频任务");
return payload.data;
}
return payload;
}
async function assertVideoBlob(blob: Blob) {
if (!blob.type.includes("json")) return;
let payload: { code?: number; msg?: string };
try {
payload = JSON.parse(await blob.text()) as { code?: number; msg?: string };
} catch {
return;
}
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "视频下载失败");
}