feat(web): 角色与音频工作台 - 完善素材选择、生成参数与同步交互
This commit is contained in:
1 parent
5650da6d31
commit
22e7331d04
77 files changed
+3641
-721
No files matched your search
@@ -4,7 +4,7 @@ import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-polic
|
||||
import { assertCanvasImageReferenceLimit, buildGenerationConfig, canvasImageReferenceLimitError, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
|
||||
import { readNodeGenerationSpec, synchronizeGenerationSpec } from "../src/lib/canvas/generation-contract";
|
||||
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
|
||||
import { groupModelsByDisplayName, inferVideoOperation, modelCompatibilityError, modelGroupReferenceLimits, modelPromptLengthError, resolveCompatibleModel, resolveModelGenerationDefaults, resolveModelVideoBooleanOptions } from "../src/lib/model-selection";
|
||||
import { audioModelForConnectionInput, 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";
|
||||
|
||||
@@ -41,7 +41,7 @@ function policyConfig(): AiConfig {
|
||||
return { ...defaultConfig, channels: [channel], models, videoModels: models, model: models[0], videoModel: models[0] };
|
||||
}
|
||||
|
||||
function node(id: string, type: CanvasNodeType, generationMode?: "image" | "video"): CanvasNodeData {
|
||||
function node(id: string, type: CanvasNodeType, generationMode?: "image" | "video" | "audio"): CanvasNodeData {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
@@ -470,6 +470,104 @@ describe("画布连线能力", () => {
|
||||
const nodes = [character, node("target", CanvasNodeType.Audio)];
|
||||
expect(canvasConnectionError(config, nodes, [], { fromNodeId: "character", toNodeId: "target" })).toBe("");
|
||||
});
|
||||
|
||||
test("默认音频模型不支持图片时,从图片新建音频节点改选豆包音频", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
audioModel: "openai::tts-1",
|
||||
audioModels: ["openai::tts-1", "doubao::seed-audio-1.0"],
|
||||
channels: [
|
||||
{ id: "openai", name: "OpenAI", baseUrl: "https://api.example.com", apiKey: "k", apiFormat: "openai" as const, interfaceType: "openai-audio", models: ["tts-1"], modelCosts: [{ model: "tts-1", capability: "audio" as const, protocol: "openai-audio", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 }] },
|
||||
{ id: "doubao", name: "豆包语音", baseUrl: "https://openspeech.bytedance.com", apiKey: "k", apiFormat: "openai" as const, models: ["seed-audio-1.0"], modelCosts: [{ model: "seed-audio-1.0", capability: "audio" as const, protocol: "doubao-streaming-tts", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 }] },
|
||||
],
|
||||
} satisfies AiConfig;
|
||||
const imageInput = { textCount: 0, imageCount: 1, videoCount: 0, audioCount: 0, characterCount: 0 };
|
||||
const model = audioModelForConnectionInput(config, imageInput);
|
||||
expect(model).toBe("doubao::seed-audio-1.0");
|
||||
const target = { ...node("target", CanvasNodeType.Audio), metadata: { model, generationMode: "audio" as const } };
|
||||
expect(canvasConnectionError(config, [node("picture", CanvasNodeType.Image), target], [], { fromNodeId: "picture", toNodeId: "target" })).toBe("");
|
||||
expect(audioModelForConnectionInput(config, { ...imageInput, imageCount: 0 })).toBe("openai::tts-1");
|
||||
});
|
||||
|
||||
test("同显示名模型组里有豆包音频时,图片可以连接到音频节点", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
audioModel: "doubao::family-audio",
|
||||
channels: [{
|
||||
id: "doubao",
|
||||
name: "豆包语音",
|
||||
baseUrl: "https://openspeech.bytedance.com",
|
||||
apiKey: "test-key",
|
||||
apiFormat: "openai" as const,
|
||||
models: ["family-audio", "seed-audio-1.0"],
|
||||
modelCosts: [
|
||||
{ model: "family-audio", displayName: "豆包音频", capability: "audio" as const, protocol: "openai-audio", billingMode: "fixed_request" as const, unitPriceMicrocredits: 2 },
|
||||
{ model: "seed-audio-1.0", displayName: "豆包音频", capability: "audio" as const, protocol: "doubao-streaming-tts", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 },
|
||||
],
|
||||
}],
|
||||
} satisfies AiConfig;
|
||||
const target = { ...node("target", CanvasNodeType.Audio), metadata: { model: "doubao::family-audio", generationMode: "audio" as const } };
|
||||
expect(canvasConnectionError(config, [node("picture", CanvasNodeType.Image), target], [], { fromNodeId: "picture", toNodeId: "target" })).toBe("");
|
||||
});
|
||||
|
||||
test("豆包音频生成允许图片连接到音频节点", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
model: "relay::cinema-image",
|
||||
audioModel: "",
|
||||
audioModels: ["doubao::seed-audio-1.0"],
|
||||
channels: [{
|
||||
id: "doubao",
|
||||
name: "豆包语音",
|
||||
baseUrl: "https://openspeech.bytedance.com",
|
||||
apiKey: "test-key",
|
||||
apiFormat: "openai" as const,
|
||||
models: ["seed-audio-1.0"],
|
||||
modelCosts: [{ model: "seed-audio-1.0", capability: "audio" as const, protocol: "doubao-streaming-tts", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 }],
|
||||
}],
|
||||
} satisfies AiConfig;
|
||||
const nodes = [node("picture", CanvasNodeType.Image), node("target", CanvasNodeType.Audio)];
|
||||
expect(canvasConnectionError(config, nodes, [], { fromNodeId: "picture", toNodeId: "target" })).toBe("");
|
||||
});
|
||||
|
||||
test("豆包音频生成同时拒绝图片和音频一起连接", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
audioModel: "doubao::seed-audio-1.0",
|
||||
channels: [{
|
||||
id: "doubao",
|
||||
name: "豆包语音",
|
||||
baseUrl: "https://openspeech.bytedance.com",
|
||||
apiKey: "test-key",
|
||||
apiFormat: "openai" as const,
|
||||
models: ["seed-audio-1.0"],
|
||||
modelCosts: [{ model: "seed-audio-1.0", capability: "audio" as const, protocol: "doubao-streaming-tts", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 }],
|
||||
}],
|
||||
} satisfies AiConfig;
|
||||
const nodes = [node("picture", CanvasNodeType.Image), node("voice", CanvasNodeType.Audio), node("target", CanvasNodeType.Audio)];
|
||||
const imageConnection = { id: "image", fromNodeId: "picture", toNodeId: "target" };
|
||||
expect(canvasConnectionError(config, nodes, [imageConnection], { fromNodeId: "voice", toNodeId: "target" })).toContain("不能同时连接");
|
||||
});
|
||||
|
||||
test("不支持参考音频的普通音频模型仍然拒绝音频连线", () => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
model: "openai::tts-1",
|
||||
audioModel: "openai::tts-1",
|
||||
channels: [{
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
baseUrl: "https://api.example.com",
|
||||
apiKey: "test-key",
|
||||
apiFormat: "openai" as const,
|
||||
interfaceType: "openai-audio",
|
||||
models: ["tts-1"],
|
||||
modelCosts: [{ model: "tts-1", capability: "audio" as const, protocol: "openai-audio", billingMode: "fixed_request" as const, unitPriceMicrocredits: 1 }],
|
||||
}],
|
||||
} satisfies AiConfig;
|
||||
const nodes = [node("voice", CanvasNodeType.Audio), node("target", CanvasNodeType.Audio)];
|
||||
expect(canvasConnectionError(config, nodes, [], { fromNodeId: "voice", toNodeId: "target" })).toContain("不支持参考音频");
|
||||
});
|
||||
});
|
||||
|
||||
describe("图片参考图上限", () => {
|
||||
|
||||
Reference in new issue
Block a user