diff --git a/web/src/lib/model-selection.ts b/web/src/lib/model-selection.ts index 45c34bc94..fab37d7c3 100644 --- a/web/src/lib/model-selection.ts +++ b/web/src/lib/model-selection.ts @@ -248,6 +248,8 @@ export function defaultImageParamsForModel(config: AiConfig, model: string): Pic export type ModelGenerationDefaults = Pick; +type ModelVideoBooleanOptions = Pick; + export function resolveModelGenerationDefaults( config: AiConfig, model: string, @@ -294,6 +296,20 @@ export function resolveModelGenerationDefaults( return {}; } +export function resolveModelVideoBooleanOptions( + config: AiConfig, + model: string, + explicit: Partial = {}, + fallback: Partial = {}, +): ModelVideoBooleanOptions { + const profile = modelCapabilityConfigFor(config, model).video!; + const defaults = resolveModelGenerationDefaults(config, model, "video", explicit, fallback); + return { + videoGenerateAudio: profile.generateAudio.supported ? defaults.videoGenerateAudio ?? String(profile.generateAudio.default) : "false", + videoWatermark: profile.watermark.supported ? defaults.videoWatermark ?? String(profile.watermark.default) : "false", + }; +} + export function maxModelInputCapacity(config: AiConfig, capability: "image" | "video", kind: "image" | "video" | "audio") { const options = selectableModelsByCapability(config, capability); if (!options.length) return null; diff --git a/web/src/pages/projects/detail/workflow-production-workbench.tsx b/web/src/pages/projects/detail/workflow-production-workbench.tsx index 44fcefdd7..c33d7a823 100644 --- a/web/src/pages/projects/detail/workflow-production-workbench.tsx +++ b/web/src/pages/projects/detail/workflow-production-workbench.tsx @@ -11,7 +11,7 @@ import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, videoDurationOptions } from "@/lib/model-capabilities"; import { modelQuoteRequest } from "@/lib/model-pricing"; import { customShotTitle, formatShotOrdinal, normalizeDefaultShotTitle } from "@/lib/shot-label"; -import { modelCompatibilityError, resolveCompatibleModel, type ModelRequirements } from "@/lib/model-selection"; +import { modelCompatibilityError, resolveCompatibleModel, resolveModelVideoBooleanOptions, type ModelRequirements } from "@/lib/model-selection"; import { formatVideoResolutionLabel } from "@/lib/video-generation-options"; import { submitBackendGenerationTask } from "@/services/api/generation-task"; import { quoteLogicalModel } from "@/services/api/logical-models"; @@ -145,6 +145,12 @@ export default function WorkflowProductionWorkbench(props: Props) { const activeProfile = useMemo(() => modelCapabilityConfigFor(effectiveConfig, routedModel), [effectiveConfig, routedModel]); const videoProfile = generationCapability === "video" ? activeProfile.video : undefined; const imageProfile = generationCapability === "image" ? activeProfile.image : undefined; + const videoBooleanOptions = useMemo(() => generationCapability === "video" + ? resolveModelVideoBooleanOptions(effectiveConfig, routedModel, {}, { + videoGenerateAudio: effectiveConfig.videoGenerateAudio, + videoWatermark: effectiveConfig.videoWatermark, + }) + : undefined, [effectiveConfig, generationCapability, routedModel]); const generationConfig = useMemo(() => ({ ...effectiveConfig, model: routedModel, @@ -154,7 +160,8 @@ export default function WorkflowProductionWorkbench(props: Props) { quality: imageQuality, vquality: resolution, videoSeconds: generationSeconds, - }), [aspectRatio, effectiveConfig, generationCapability, generationSeconds, imageQuality, resolution, routedModel]); + ...(videoBooleanOptions || {}), + }), [aspectRatio, effectiveConfig, generationCapability, generationSeconds, imageQuality, resolution, routedModel, videoBooleanOptions]); const priceChannel = resolveModelChannel(generationConfig, routedModel); const configuredCredits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", diff --git a/web/test/canvas-model-policy.test.ts b/web/test/canvas-model-policy.test.ts index 829f3f43c..34ec3c760 100644 --- a/web/test/canvas-model-policy.test.ts +++ b/web/test/canvas-model-policy.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test"; import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy"; import { assertCanvasImageReferenceLimit, buildGenerationConfig, canvasImageReferenceLimitError, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation"; import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities"; -import { groupModelsByDisplayName, inferVideoOperation, modelCompatibilityError, modelGroupReferenceLimits, modelPromptLengthError, resolveCompatibleModel, resolveModelGenerationDefaults } from "../src/lib/model-selection"; +import { groupModelsByDisplayName, inferVideoOperation, modelCompatibilityError, modelGroupReferenceLimits, modelPromptLengthError, resolveCompatibleModel, resolveModelGenerationDefaults, resolveModelVideoBooleanOptions } from "../src/lib/model-selection"; import { defaultConfig, normalizeModelOptionValue, type AiConfig, type ModelChannel } from "../src/stores/use-config-store"; import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas"; @@ -141,6 +141,59 @@ describe("逻辑模型选择", () => { expect(resolveModelGenerationDefaults(config, model, "video", {}, { videoSeconds: "6" }).videoSeconds).toBe("15"); }); + test("镜头视频不会向不支持同步音频的模型提交全局 true 默认值", () => { + const model = "platform::silent-video"; + const profile = defaultModelCapabilityConfig(undefined, "silent-video"); + profile.video!.generateAudio = { supported: false, default: false }; + profile.video!.watermark = { supported: false, default: false }; + const config: AiConfig = { + ...defaultConfig, + videoGenerateAudio: "true", + videoWatermark: "true", + channels: [{ + id: "platform", + name: "平台模型", + baseUrl: "/api", + apiKey: "system", + apiFormat: "openai", + scope: "system", + models: ["silent-video"], + modelCosts: [{ + model: "silent-video", + capability: "video", + billingMode: "per_second", + unitPriceMicrocredits: 1, + logicalModelId: "silent-video", + logicalCapabilitySpec: { + version: 1, + capability: "video", + options: { + videoGenerateAudio: { values: [false] }, + videoWatermark: { values: [false] }, + }, + }, + capabilityConfig: profile, + }], + }], + models: [model], + videoModels: [model], + videoModel: model, + model, + }; + + expect(resolveModelVideoBooleanOptions(config, model, {}, { + videoGenerateAudio: config.videoGenerateAudio, + videoWatermark: config.videoWatermark, + })).toEqual({ videoGenerateAudio: "false", videoWatermark: "false" }); + + profile.video!.generateAudio = { supported: true, default: true }; + config.videoGenerateAudio = "false"; + expect(resolveModelVideoBooleanOptions(config, model, {}, { + videoGenerateAudio: config.videoGenerateAudio, + videoWatermark: config.videoWatermark, + })).toEqual({ videoGenerateAudio: "true", videoWatermark: "false" }); + }); + test("渠道视频能力配置优先于旧的全局 6 秒和 1:1 配置", () => { const model = "autodl-channel::MiniMax H3";