fix(video): 创作台视频 - 修正同步音频参数与万有引力结果下载地址 (#554)
Original-Commit: e85e78fb898be7ef1fcd2027b62ab43c00dfaa2c fix(video): 创作台视频 - 修正同步音频参数与万有引力结果下载地址
This commit is contained in:
51
backend/internal/protocol/lxmone_video_test.go
Normal file
51
backend/internal/protocol/lxmone_video_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOfficialLxmoneVideoResultURLs(t *testing.T) {
|
||||||
|
providers := []string{
|
||||||
|
"lxmone-wan-videos", "lxmone-wan-channel-s", "lxmone-seedance-videos", "lxmone-h3-max-videos",
|
||||||
|
"lxmone-sd-videos", "lxmone-sd-mini-videos", "lxmone-grok-videos", "lxmone-h3-workflow",
|
||||||
|
}
|
||||||
|
const directURL = "https://cdn.example.com/generated.mp4?signature=test"
|
||||||
|
for _, provider := range providers {
|
||||||
|
t.Run(provider, func(t *testing.T) {
|
||||||
|
adapter := officialPackageAdapter(t, "lxmone-video-suite.yingce-plugin", provider)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
payload map[string]any
|
||||||
|
}{
|
||||||
|
{"direct_over_relative", map[string]any{"id": "video-test", "status": "completed", "metadata": map[string]any{"url": "/v1/videos/video-test/content", "direct_url": directURL}}},
|
||||||
|
{"nested_direct_over_relative", map[string]any{"metadata": map[string]any{"url": "/v1/videos/video-test/content"}, "data": map[string]any{"id": "video-test", "status": "completed", "metadata": map[string]any{"url": "/v1/videos/video-test/content", "direct_url": directURL}}}},
|
||||||
|
{"absolute_url_without_direct", map[string]any{"id": "video-test", "status": "completed", "metadata": map[string]any{"url": directURL}}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
body, err := json.Marshal(tc.payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
created, err := adapter.ParseCreate(context.Background(), body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
polled, err := adapter.ParsePoll(context.Background(), PollContext{TaskID: "video-test"}, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if created.Status != StatusSucceeded || polled.Status != StatusSucceeded || created.TaskID != "video-test" || polled.TaskID != "video-test" {
|
||||||
|
t.Fatalf("unexpected completion: create=%#v poll=%#v", created, polled)
|
||||||
|
}
|
||||||
|
for _, result := range []*Result{created.Result, polled.Result} {
|
||||||
|
if result == nil || len(result.Videos) != 1 || result.Videos[0].URL != directURL {
|
||||||
|
t.Fatalf("expected direct download URL, got %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,20 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
|||||||
- [x] 修复已在部署预览版验证:从设置页返回画布列表后,菜单背景和圆角保持正常;深浅主题的卡片操作菜单及浅色排序菜单验证通过。
|
- [x] 修复已在部署预览版验证:从设置页返回画布列表后,菜单背景和圆角保持正常;深浅主题的卡片操作菜单及浅色排序菜单验证通过。
|
||||||
- [ ] 上游集成后复核设置页与其他工作台页面之间的导航。
|
- [ ] 上游集成后复核设置页与其他工作台页面之间的导航。
|
||||||
|
|
||||||
|
## 万有引力视频结果地址
|
||||||
|
|
||||||
|
- 已修复:万有引力视频套件更新为 `1.0.1`,8 个协议优先读取 `metadata.direct_url` / `data.metadata.direct_url`,避免完成响应中的相对 `metadata.url` 遮蔽完整视频地址。保留已有完整 URL 回退,宿主下载与 SSRF 校验不变;源码、内嵌接口文档和插件归档同步更新。
|
||||||
|
- [x] 真实插件归档的 24 组响应回归通过,覆盖创建 / 轮询完成、顶层 / data 内直链优先以及完整 URL 回退;官方插件归档完整性检查通过。
|
||||||
|
- [x] 修复插件已部署并验证运行时注册版本与归档一致,后端就绪及 API 代理检查通过。从失败任务已记录的完整地址取回 MP4,未发起新的上游生成,也未修改原失败任务与账务状态。
|
||||||
|
- [ ] 真实创作台新视频任务验证下载、资源转存与结果展示。原失败任务取回的视频保存在受限本地缓存,未自动回填到会话。
|
||||||
|
|
||||||
|
## 创作台视频同步音频参数
|
||||||
|
|
||||||
|
- 已修复:创作台视频报价、提交和重试共用按所选模型能力整理的配置。不支持同步音频或水印时使用 `false`,支持时遵循模型默认值,不再把全局 `videoGenerateAudio=true` 当成用户请求;模型筛选只使用创作台实际可选的参数。
|
||||||
|
- [x] 创作台请求配置、模型能力与报价相关的 32 项专项测试通过,前端类型检查通过;覆盖无声视频的报价 / 任务参数、重试元数据以及支持音频时的模型默认值。
|
||||||
|
- [x] 前端生产构建通过,修复镜像已部署;首页、创作台路由、入口资源和创作台脚本与本次构建逐字节一致,容器健康与 API 就绪检查通过。构建仍提示部分 chunk 超过 500 kB。
|
||||||
|
- [ ] 在真实创作台使用万有引力渠道的 `minimax-h3-a`,输入“小猫在流泪”,选择 `16:9`、`480P`,检查报价、首次生成和失败后重试;同时确认支持同步音频的模型遵循各自默认值。本次不调用真实上游,生成结果仍待验收。
|
||||||
|
|
||||||
## 多模态列表与分镜表合并回归
|
## 多模态列表与分镜表合并回归
|
||||||
|
|
||||||
- [x] 合入 PR #549 的图片 / 视频列表分析、动态表格、视频脚本转换和批量生成设置;参考图上限前后端统一为 10。保留本地主分支表格布局、参考图提及、删除列、结果定位、手工行增量同步及单行重新生成。
|
- [x] 合入 PR #549 的图片 / 视频列表分析、动态表格、视频脚本转换和批量生成设置;参考图上限前后端统一为 10。保留本地主分支表格布局、参考图提及、删除列、结果定位、手工行增量同步及单行重新生成。
|
||||||
|
|||||||
Binary file not shown.
@@ -85,10 +85,15 @@
|
|||||||
轮询时按 `status`、`state`、`data.status` 取状态,`completed`/`succeeded` 归一为 `succeeded`,
|
轮询时按 `status`、`state`、`data.status` 取状态,`completed`/`succeeded` 归一为 `succeeded`,
|
||||||
`failed`/`expired`/`error` 归一为 `failed`,其余按进行中处理。
|
`failed`/`expired`/`error` 归一为 `failed`,其余按进行中处理。
|
||||||
|
|
||||||
视频地址按 `metadata.url`、`metadata.video_url`、`video_url`、`videoUrl`、`result_url`、`url`、
|
视频地址优先取 `metadata.direct_url`、`data.metadata.direct_url`,再按
|
||||||
|
`metadata.url`、`metadata.video_url`、`video_url`、`videoUrl`、`result_url`、`url`、
|
||||||
`data.metadata.url`、`data.video_url`、`output.url` 依次取值;上游返回的是临时地址
|
`data.metadata.url`、`data.video_url`、`output.url` 依次取值;上游返回的是临时地址
|
||||||
(`resultEphemeral`),由宿主立即下载并转存为项目资源。
|
(`resultEphemeral`),由宿主立即下载并转存为项目资源。
|
||||||
|
|
||||||
|
H3 工作流完成响应可能同时包含相对路径 `metadata.url` 和完整 HTTPS 地址
|
||||||
|
`metadata.direct_url`,必须优先选择后者,避免把缺少主机的路径交给外部资源下载器。
|
||||||
|
下载仍执行宿主的地址与 SSRF 校验。
|
||||||
|
|
||||||
错误信息取 `error.message`、`message`、`fail_reason`,错误码路径为 `error.message` 与 `error.code`。
|
错误信息取 `error.message`、`message`、`fail_reason`,错误码路径为 `error.message` 与 `error.code`。
|
||||||
HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结束并把上游原文回传给用户。
|
HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结束并把上游原文回传给用户。
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
"apiVersion": "yingce.plugin/v2",
|
"apiVersion": "yingce.plugin/v2",
|
||||||
"id": "lxmone-video-suite",
|
"id": "lxmone-video-suite",
|
||||||
"name": "万有引力视频套件",
|
"name": "万有引力视频套件",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"author": "Yingce / 万有引力",
|
"author": "Yingce / 万有引力",
|
||||||
"description": "万有引力(lxmone.xyz)视频协议套件:Wan 3.0(现有渠道与 S 渠道)、Seedance 2 / 2.5、SD 2.0 / 2.5 / Mini、Grok Imagine、MiniMax H3(Max 与 A-E 独立工作流)。",
|
"description": "万有引力(lxmone.xyz)视频协议套件:Wan 3.0(现有渠道与 S 渠道)、Seedance 2 / 2.5、SD 2.0 / 2.5 / Mini、Grok Imagine、MiniMax H3(Max 与 A-E 独立工作流)。",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -360,6 +365,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -648,6 +659,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -912,6 +929,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -1176,6 +1199,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -1440,6 +1469,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -1721,6 +1756,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -2013,6 +2054,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
@@ -2347,6 +2394,12 @@ HTTP 失败、业务错误码或状态进入 `failed` 时,任务以失败结
|
|||||||
},
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"$coalesce": [
|
"$coalesce": [
|
||||||
|
{
|
||||||
|
"$ref": "response.metadata.direct_url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "response.data.metadata.direct_url"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "response.metadata.url"
|
"$ref": "response.metadata.url"
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
26
web/src/pages/create/creation-generation-config.ts
Normal file
26
web/src/pages/create/creation-generation-config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { modelCapabilityConfigFor, normalizeVideoValue } from "@/lib/model-capabilities";
|
||||||
|
import { resolveModelVideoBooleanOptions } from "@/lib/model-selection";
|
||||||
|
import type { AiConfig } from "@/stores/use-config-store";
|
||||||
|
import type { CreationSettings } from "./creation-types";
|
||||||
|
|
||||||
|
// 创作台没有音频和水印开关;报价与提交共用所选模型的默认值和能力限制。
|
||||||
|
export function creationVideoConfig(config: AiConfig, model: string, settings: Pick<CreationSettings, "ratio" | "seconds" | "videoQuality">): AiConfig {
|
||||||
|
const profile = modelCapabilityConfigFor(config, model).video!;
|
||||||
|
const normalized = normalizeVideoValue(profile, {
|
||||||
|
ratio: settings.ratio,
|
||||||
|
seconds: settings.seconds,
|
||||||
|
resolution: settings.videoQuality,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
model,
|
||||||
|
videoModel: model,
|
||||||
|
size: normalized.ratio,
|
||||||
|
videoSeconds: normalized.seconds,
|
||||||
|
vquality: normalized.resolution.replace(/p$/i, ""),
|
||||||
|
...resolveModelVideoBooleanOptions(config, model, {}, {
|
||||||
|
videoGenerateAudio: config.videoGenerateAudio,
|
||||||
|
videoWatermark: config.videoWatermark,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import { attachCreationTaskContexts, completedCreationGenerationTask, conversati
|
|||||||
import { CreationComposer, CreationEmptySuggest, CreationFeaturedWorks, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
|
import { CreationComposer, CreationEmptySuggest, CreationFeaturedWorks, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
|
||||||
import { CreationAgentEntry } from "./creation-agent-entry";
|
import { CreationAgentEntry } from "./creation-agent-entry";
|
||||||
import { createCreationSubmitGate } from "./creation-submit-gate";
|
import { createCreationSubmitGate } from "./creation-submit-gate";
|
||||||
|
import { creationVideoConfig } from "./creation-generation-config";
|
||||||
|
|
||||||
const AssetLibraryPickerModal = lazy(() => import("@/components/assets/asset-library-picker-modal").then((module) => ({ default: module.AssetLibraryPickerModal })));
|
const AssetLibraryPickerModal = lazy(() => import("@/components/assets/asset-library-picker-modal").then((module) => ({ default: module.AssetLibraryPickerModal })));
|
||||||
const loadCreationRuntime = () => import("./creation-runtime");
|
const loadCreationRuntime = () => import("./creation-runtime");
|
||||||
@@ -142,10 +143,13 @@ export default function CreatePage() {
|
|||||||
options: mode === "image"
|
options: mode === "image"
|
||||||
? { size: ratio, quality, count: Number(count), transparentBackground: config.transparentBackground === "true" }
|
? { size: ratio, quality, count: Number(count), transparentBackground: config.transparentBackground === "true" }
|
||||||
: mode === "video"
|
: mode === "video"
|
||||||
? { size: ratio, videoSeconds: Number(seconds), vquality: videoQuality, videoGenerateAudio: config.videoGenerateAudio === "true", videoWatermark: config.videoWatermark === "true" }
|
? { size: ratio, videoSeconds: Number(seconds), vquality: videoQuality }
|
||||||
: {},
|
: {},
|
||||||
}), [attachments, config.transparentBackground, config.videoGenerateAudio, config.videoWatermark, count, hasPrompt, mode, quality, ratio, seconds, videoQuality]);
|
}), [attachments, config.transparentBackground, count, hasPrompt, mode, quality, ratio, seconds, videoQuality]);
|
||||||
const selectedModel = resolveCompatibleModel(config, preferredModel, modelRequirements) || preferredModel;
|
const selectedModel = resolveCompatibleModel(config, preferredModel, modelRequirements) || preferredModel;
|
||||||
|
const generationConfig = useMemo(() => mode === "video"
|
||||||
|
? creationVideoConfig(config, selectedModel, { ratio, seconds, videoQuality })
|
||||||
|
: config, [config, mode, ratio, seconds, selectedModel, videoQuality]);
|
||||||
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
const imageProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).image!, [config, selectedModel]);
|
||||||
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
const videoProfile = useMemo(() => modelCapabilityConfigFor(config, selectedModel).video!, [config, selectedModel]);
|
||||||
const maxReferences = mode === "video" ? videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0 : mode === "image" ? imageProfile.references.maxImages : 6;
|
const maxReferences = mode === "video" ? videoProfile.operations.includes("image_to_video") ? videoProfile.references.maxImages : 0 : mode === "image" ? imageProfile.references.maxImages : 6;
|
||||||
@@ -628,18 +632,15 @@ export default function CreatePage() {
|
|||||||
const requestLifecycle = runtime.beginGenerationConsumer(controller.signal);
|
const requestLifecycle = runtime.beginGenerationConsumer(controller.signal);
|
||||||
abortRef.current = controller;
|
abortRef.current = controller;
|
||||||
const normalizedImage = mode === "image" ? normalizeImageValue(imageProfile, { size: ratio, quality, count }) : undefined;
|
const normalizedImage = mode === "image" ? normalizeImageValue(imageProfile, { size: ratio, quality, count }) : undefined;
|
||||||
const normalizedVideo = mode === "video" ? normalizeVideoValue(videoProfile, { seconds, ratio, resolution: videoQuality }) : undefined;
|
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
...config,
|
...generationConfig,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
imageModel: selectedModel,
|
imageModel: selectedModel,
|
||||||
videoModel: selectedModel,
|
videoModel: selectedModel,
|
||||||
textModel: selectedModel,
|
textModel: selectedModel,
|
||||||
...(mode === "image"
|
...(mode === "image"
|
||||||
? { size: normalizedImage?.size || ratio, quality: normalizedImage?.quality || quality, count: normalizedImage?.count || count, videoSeconds: config.videoSeconds }
|
? { size: normalizedImage?.size || ratio, quality: normalizedImage?.quality || quality, count: normalizedImage?.count || count, videoSeconds: config.videoSeconds }
|
||||||
: mode === "video"
|
: {}),
|
||||||
? { size: normalizedVideo?.ratio ?? ratio, videoSeconds: normalizedVideo?.seconds || seconds, vquality: (normalizedVideo?.resolution ?? videoQuality).replace(/p$/i, "") }
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (mode === "text") {
|
if (mode === "text") {
|
||||||
@@ -957,7 +958,7 @@ export default function CreatePage() {
|
|||||||
modelRequirements,
|
modelRequirements,
|
||||||
imageProfile,
|
imageProfile,
|
||||||
videoProfile,
|
videoProfile,
|
||||||
config,
|
config: generationConfig,
|
||||||
onModelChange: (value: string) => updateConfig(mode === "text" ? "textModel" : mode === "image" ? "imageModel" : "videoModel", value),
|
onModelChange: (value: string) => updateConfig(mode === "text" ? "textModel" : mode === "image" ? "imageModel" : "videoModel", value),
|
||||||
ratio,
|
ratio,
|
||||||
setRatio: setComposerRatio,
|
setRatio: setComposerRatio,
|
||||||
|
|||||||
50
web/test/creation-generation-config.test.ts
Normal file
50
web/test/creation-generation-config.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
|
||||||
|
import { modelQuoteRequest } from "../src/lib/model-pricing";
|
||||||
|
import { creationVideoConfig } from "../src/pages/create/creation-generation-config";
|
||||||
|
import { prepareBackendGenerationTask } from "../src/services/api/generation-task";
|
||||||
|
import { defaultConfig, type AiConfig } from "../src/stores/use-config-store";
|
||||||
|
|
||||||
|
function videoConfig(generateAudio: { supported: boolean; default: boolean }): AiConfig {
|
||||||
|
const capabilityConfig = defaultModelCapabilityConfig();
|
||||||
|
capabilityConfig.video!.generateAudio = generateAudio;
|
||||||
|
capabilityConfig.video!.watermark = { supported: false, default: false };
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
model: "gravity::minimax-h3-a",
|
||||||
|
videoModel: "gravity::minimax-h3-a",
|
||||||
|
videoGenerateAudio: "true",
|
||||||
|
videoWatermark: "true",
|
||||||
|
channels: [{
|
||||||
|
id: "gravity", name: "万有引力", baseUrl: "/api", apiKey: "", apiFormat: "openai", scope: "system",
|
||||||
|
models: ["minimax-h3-a"],
|
||||||
|
modelCosts: [{ model: "minimax-h3-a", capability: "video", billingMode: "per_second", unitPriceMicrocredits: 1, capabilityConfig }],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("创作台无声视频报价、提交与重试不携带全局音频和水印默认值", async () => {
|
||||||
|
const config = videoConfig({ supported: false, default: false });
|
||||||
|
const requestConfig = creationVideoConfig(config, config.videoModel, { ratio: "16:9", seconds: "6", videoQuality: "480" });
|
||||||
|
const quote = modelQuoteRequest(requestConfig, requestConfig.model, "video");
|
||||||
|
expect(quote?.intent.options).toMatchObject({ size: "16:9", vquality: "480", videoSeconds: 6, videoGenerateAudio: false, videoWatermark: false });
|
||||||
|
|
||||||
|
for (const retryOf of [undefined, "failed-task"]) {
|
||||||
|
const task = await prepareBackendGenerationTask({ mode: "video", prompt: "小猫在流泪", config: requestConfig, retryOf });
|
||||||
|
expect(task.input).toMatchObject({
|
||||||
|
config: { size: "16:9", vquality: "480", videoSeconds: "6", videoGenerateAudio: "false", videoWatermark: "false" },
|
||||||
|
...(retryOf ? { metadata: { retryOf } } : {}),
|
||||||
|
});
|
||||||
|
expect(task.operation).toBe("text_to_video");
|
||||||
|
}
|
||||||
|
expect(config.videoGenerateAudio).toBe("true");
|
||||||
|
expect(config.videoWatermark).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([false, true])("创作台支持同步音频时遵循模型默认值 %s", (audioDefault) => {
|
||||||
|
const config = videoConfig({ supported: true, default: audioDefault });
|
||||||
|
config.videoGenerateAudio = String(!audioDefault);
|
||||||
|
const requestConfig = creationVideoConfig(config, config.videoModel, { ratio: "16:9", seconds: "6", videoQuality: "480" });
|
||||||
|
expect(requestConfig.videoGenerateAudio).toBe(String(audioDefault));
|
||||||
|
expect(modelQuoteRequest(requestConfig, requestConfig.model, "video")?.intent.options?.videoGenerateAudio).toBe(audioDefault);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user