feat(video): 对齐 grok-imagine-video 接口并优化视频生成功能
- 将默认视频模型从 sora-2 更改为 grok-imagine-video - 新增分辨率选项包括 720p 和 480p,替换原有的质量选项 - 扩展视频尺寸选项,新增方形、宽屏、长图等预设尺寸 - 添加分辨率和尺寸手动输入功能,支持自定义宽高 - 更新视频生成 API 参数为 resolution_name、preset 和 input_reference[] - 支持最多 7 张参考图片上传 - 优化视频设置浮层定位,避免被工具栏遮挡 - 识别后端错误响应格式,改进视频生成失败处理逻辑
This commit is contained in:
@@ -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 || "视频下载失败");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user