ai-chatbot/lib/video-canvas/audio-contract.ts

198 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { z } from "zod";
const qwenVoices = [
{ id: "Cherry", name: "芊悦" },
{ id: "Serena", name: "苏瑶" },
];
export const audioModelContracts = {
"qwen3-tts-instruct-flash": {
kind: "tts",
label: "千问3 TTS 指令版",
voices: qwenVoices,
instructions: true,
controls: false,
formats: ["wav"],
},
"qwen3-tts-flash": {
kind: "tts",
label: "千问3 TTS",
voices: [...qwenVoices, { id: "Ryan", name: "甜茶" }, { id: "Jennifer", name: "詹妮弗" }],
instructions: false,
controls: false,
formats: ["wav"],
},
"qwen-audio-3.0-tts-flash": {
kind: "tts",
label: "千问 Audio 3.0 Flash",
voices: [
{ id: "longanhuan_v3.6", name: "龙安欢" },
{ id: "longanfengyue", name: "龙安风悦" },
{ id: "longanyuanfei", name: "龙安元妃" },
{ id: "longanlingxi", name: "龙安灵希" },
],
instructions: true,
controls: true,
formats: ["mp3", "wav"],
},
"qwen-audio-3.0-tts-plus": {
kind: "tts",
label: "千问 Audio 3.0 Plus",
voices: [
{ id: "longanlingxin", name: "龙安灵心" },
{ id: "longanlufeng", name: "龙安鲁风" },
],
instructions: true,
controls: true,
formats: ["mp3", "wav"],
},
"fun-music-v1": {
kind: "music",
label: "Fun-Music 音乐",
voices: [],
instructions: false,
controls: false,
formats: ["mp3", "wav"],
},
} satisfies Record<
string,
{
kind: string;
label: string;
voices: { id: string; name: string }[];
instructions: boolean;
controls: boolean;
formats: string[];
}
>;
export type AudioModelId = keyof typeof audioModelContracts;
export const audioRequestSchema = z.object({
mode: z.enum(["tts", "music"]),
modelRef: z.string().min(1),
text: z.string().trim().max(2000).default(""),
lyrics: z.string().trim().max(2000).default(""),
voice: z.string().max(100).optional(),
language: z
.enum([
"Auto",
"Chinese",
"English",
"German",
"Italian",
"Portuguese",
"Spanish",
"Japanese",
"Korean",
"French",
"Russian",
])
.default("Chinese"),
instructions: z.string().trim().max(1200).default(""),
format: z.enum(["mp3", "wav"]).default("wav"),
instrumental: z.boolean().default(true),
gender: z.enum(["male", "female"]).default("female"),
rate: z.number().min(0.5).max(2).default(1),
pitch: z.number().min(0.5).max(2).default(1),
volume: z.number().int().min(0).max(100).default(50),
});
export type AudioRequest = z.infer<typeof audioRequestSchema>;
export function audioContract(id: string) {
return Object.hasOwn(audioModelContracts, id)
? audioModelContracts[id as AudioModelId]
: undefined;
}
export function officialAudioBase(base: string, modelId?: string) {
const url = new URL(base);
if (
url.protocol !== "https:" ||
url.username ||
url.password ||
!/^(?:dashscope(?:-intl|-us)?\.aliyuncs\.com|[a-zA-Z0-9-]+\.(?:cn-beijing|ap-southeast-1)\.maas\.aliyuncs\.com)$/.test(
url.hostname,
)
)
throw Error("请选择阿里云官方音频服务线路");
if (
(modelId === "fun-music-v1" || modelId?.startsWith("qwen-audio-")) &&
url.hostname !== "dashscope.aliyuncs.com" &&
!url.hostname.endsWith(".cn-beijing.maas.aliyuncs.com")
)
throw Error("所选模型仅支持阿里云北京地域");
return url.origin;
}
export function buildAudioRequest(value: unknown, modelId: string) {
const p = audioRequestSchema.parse(value),
contract = audioContract(modelId);
if (!contract || contract.kind !== p.mode) throw Error("音频模式与模型不匹配");
if (!contract.formats.includes(p.format)) throw Error("所选模型不支持这个音频格式");
if (p.mode === "music") {
if ((p.instrumental && !p.text) || (!p.text && !p.lyrics)) throw Error("请输入音乐描述或歌词");
if (
!p.instrumental &&
p.lyrics &&
(p.lyrics.length < 5 || (/[\u3400-\u9fff]/.test(p.lyrics) && p.lyrics.length > 350))
)
throw Error("中文歌词需 5–350 字,英文歌词需 5–2000 字");
return {
path: "/api/v1/services/audio/music/generation",
body: {
model: modelId,
input: {
...(p.text ? { prompt: p.text } : {}),
...(!p.instrumental && p.lyrics ? { lyrics: p.lyrics } : {}),
is_instrumental: p.instrumental,
...(!p.instrumental ? { gender: p.gender } : {}),
format: p.format,
},
},
};
}
if (!p.text || p.text.length > 600) throw Error("配音文本需 1–600 字");
if (!contract.voices.some((v) => v.id === p.voice)) throw Error("请选择当前模型支持的音色");
if (!contract.instructions && p.instructions) throw Error("所选模型不支持风格指令");
if (!contract.controls && (p.rate !== 1 || p.pitch !== 1 || p.volume !== 50))
throw Error("所选模型不支持数值语速、音调和音量参数,可改用风格指令");
const modern = modelId.startsWith("qwen-audio-");
if (modern && !["Chinese", "English", "Auto"].includes(p.language))
throw Error("当前系统音色支持中文和英文");
return {
path: modern
? "/api/v1/services/audio/tts/SpeechSynthesizer"
: "/api/v1/services/aigc/multimodal-generation/generation",
body: {
model: modelId,
input: {
text: p.text,
voice: p.voice,
...(modern
? {
format: p.format,
rate: p.rate,
pitch: p.pitch,
volume: p.volume,
...(p.instructions ? { instruction: p.instructions } : {}),
...(p.language !== "Auto"
? { language_hints: [p.language === "Chinese" ? "zh" : "en"] }
: {}),
}
: {
language_type: p.language,
...(p.instructions
? { instructions: p.instructions, optimize_instructions: true }
: {}),
}),
},
},
};
}
export const audioCheckpointSchema = z.object({
operation: z.literal("audio"),
modelRef: z.string(),
modelId: z.string(),
url: z
.string()
.url()
.refine((v) => /^https?:\/\//i.test(v)),
format: z.enum(["mp3", "wav"]),
requestId: z.string().optional(),
expiresAt: z.number().optional(),
});