This commit is contained in:
2026-05-01 10:34:37 +08:00
commit 4c55c59108
9 changed files with 1264 additions and 0 deletions

158
src/app.ts Normal file
View File

@@ -0,0 +1,158 @@
const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY;
if (!MINIMAX_API_KEY) {
throw new Error("MINIMAX_API_KEY environment variable is required");
}
interface VoiceSetting {
voice_id: string;
speed: number;
vol: number;
pitch: number;
emotion?: string;
}
interface AudioSetting {
sample_rate: number;
bitrate: number;
format: "mp3" | "wav" | "ogg" | "aac";
channel: number;
}
interface PronunciationDictEntry {
text: string;
pronunciation: string;
}
interface MiniMaxTTSRequest {
model: string;
text: string;
stream?: boolean;
voice_setting: VoiceSetting;
audio_setting: AudioSetting;
pronunciation_dict?: {
tone: PronunciationDictEntry[];
};
subtitle_enable?: boolean;
}
interface MiniMaxTTSResponse {
code: number;
desc: string;
data?: {
audio?: string;
audio_file?: string;
subtitle_info?: string;
};
}
const BASE_URL = "https://api.minimaxi.com/v1/t2a_v2";
export async function textToSpeech(
request: MiniMaxTTSRequest
): Promise<Buffer> {
const response = await fetch(BASE_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${MINIMAX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`MiniMax API error: ${response.status} ${response.statusText} - ${body}`);
}
const result = (await response.json()) as MiniMaxTTSResponse;
if (result.code && result.code !== 0) {
throw new Error(`MiniMax API error: ${result.code} - ${result.desc}`);
}
if (!result.data?.audio && !result.data?.audio_file) {
throw new Error("No audio data returned");
}
const audioHex = result.data.audio || result.data.audio_file!;
const audioBuffer = Buffer.from(audioHex, "hex");
return audioBuffer;
}
export async function textToSpeechStream(
request: MiniMaxTTSRequest,
onChunk: (chunk: Buffer) => void
): Promise<void> {
const response = await fetch(BASE_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${MINIMAX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...request, stream: true }),
});
if (!response.ok) {
throw new Error(`MiniMax API error: ${response.status} ${response.statusText}`);
}
if (!response.body) {
throw new Error("No response body");
}
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
onChunk(Buffer.from(value));
}
}
export const VOICE_IDS = {
male_qn_qingse: "male-qn-qingse",
female_tianmei: "female-tianmei",
male_baixian: "male-baixian",
female_aicheng: "female-aicheng",
male_yunyang: "male-yunyang",
female_xiaomo: "female-xiaomo",
} as const;
export type VoiceId = (typeof VOICE_IDS)[keyof typeof VOICE_IDS];
export const EMOTIONS = {
neutral: "neutral",
happy: "happy",
sad: "sad",
angry: "angry",
fearful: "fearful",
disgust: "disgust",
surprised: "surprised",
} as const;
export type Emotion = (typeof EMOTIONS)[keyof typeof EMOTIONS];
// Usage example:
// async function example() {
// const audio = await textToSpeech({
// model: "speech-2.8-hd",
// text: "今天是不是很开心呀(laughs),当然了!",
// stream: false,
// voice_setting: {
// voice_id: VOICE_IDS.male_qn_qingse,
// speed: 1,
// vol: 1,
// pitch: 0,
// emotion: EMOTIONS.happy,
// },
// audio_setting: {
// sample_rate: 32000,
// bitrate: 128000,
// format: "mp3",
// channel: 1,
// },
// });
//
// // Save audio to file (Node.js)
// await import("fs/promises").then(fs => fs.writeFile("output.mp3", audio));
// }