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; 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(), });