/** * minimax-key2 TTS 测试脚本 * 使用 minimax-key2 配置发送 2 个 "你好" TTS 请求 * 流程: 创建任务 → 轮询状态 → 下载音频 */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // ============================================================ // minimax-key2 配置 (来自 server/src/config/models.json) // ============================================================ const CONFIG = { apiKey: 'sk-cp-BOVwOolqOEmHycg8MzDX_UgQ_bDEK3snjPUFuwolUCb_TxMxhhlAfYhTT0_tqRvEz5K56xpl-aOoJtj97MuAY4UIA-PCQLVetUCY56i8LFimNuQqrrq5xWA', ttsApiPath: 'https://api.minimaxi.com', model: 'speech-2.8-hd', voice: 'audiobook_female_1', // Cherry }; const OUTPUT_DIR = path.resolve(__dirname, '../test-results/minimax-key2-tts'); const TEXT = '你好'; // ============================================================ // TAR 解包工具:从 tar buffer 中提取 MP3 // ============================================================ function extractMp3FromTar(buffer: Buffer): Buffer | null { let offset = 0; while (offset < buffer.length - 512) { const nameSlice = buffer.subarray(offset, offset + 100); const nameEnd = nameSlice.indexOf(0); const filename = nameEnd >= 0 ? Buffer.from(nameSlice.subarray(0, nameEnd)).toString() : nameSlice.toString(); if (!filename || filename.trim().length === 0) break; const sizeStr = Buffer.from(buffer.subarray(offset + 124, offset + 135)).toString().trim(); const fileSize = parseInt(sizeStr, 8); const contentOffset = offset + 512; if (filename.endsWith('.mp3')) { console.log(` 📦 从 tar 中提取 ${filename} (${fileSize} bytes)`); return Buffer.from(buffer.subarray(contentOffset, contentOffset + fileSize)); } const paddedSize = Math.ceil(fileSize / 512) * 512; offset = contentOffset + paddedSize; } return null; } // ============================================================ // 单个 TTS 请求完整流程 // ============================================================ async function ttsRequest(text: string, requestId: number): Promise { const tag = `[请求#${requestId}]`; console.log(`\n${'='.repeat(60)}`); console.log(`${tag} 开始 TTS 合成: "${text}"`); console.log(`${'='.repeat(60)}`); // ---- 步骤 1: 创建异步任务 ---- console.log(`${tag} 📤 步骤1: 创建异步任务...`); const createRes = await fetch(`${CONFIG.ttsApiPath}/v1/t2a_async_v2`, { method: 'POST', headers: { 'Authorization': `Bearer ${CONFIG.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: CONFIG.model, text, voice_setting: { voice_id: CONFIG.voice, speed: 1, vol: 1, pitch: 1, }, audio_setting: { audio_sample_rate: 32000, bitrate: 128000, format: 'mp3', channel: 1, }, }), }); const createData: any = await createRes.json(); console.log(`${tag} 响应:`, JSON.stringify(createData, null, 2)); if (createData.base_resp?.status_code !== 0) { throw new Error(`${tag} 创建任务失败: ${createData.base_resp?.status_msg || JSON.stringify(createData)}`); } const task_id = String(createData.task_id); const task_token = createData.task_token; console.log(`${tag} ✅ 任务已创建, task_id: ${task_id}`); // ---- 步骤 2: 轮询任务状态 ---- console.log(`${tag} 🔄 步骤2: 轮询中...`); const maxPollMs = 2 * 60 * 1000; // 最多 2 分钟 const pollInterval = 3000; const startTime = Date.now(); let file_id: string | null = null; while (Date.now() - startTime < maxPollMs) { const queryRes = await fetch( `${CONFIG.ttsApiPath}/v1/query/t2a_async_query_v2?task_id=${task_id}&task_token=${task_token}`, { headers: { 'Authorization': `Bearer ${CONFIG.apiKey}` } } ); const queryData: any = await queryRes.json(); const status = queryData.status; process.stdout.write(`\r${tag} 状态: ${status} (${Math.round((Date.now() - startTime) / 1000)}s)`); if (status === 'Success') { file_id = queryData.file_id ? String(queryData.file_id) : null; console.log(`\n${tag} ✅ 任务完成, file_id: ${file_id}`); break; } if (status === 'Failed') { throw new Error(`${tag} 任务失败: ${queryData.status_msg || '未知错误'}`); } await new Promise(r => setTimeout(r, pollInterval)); } if (!file_id) throw new Error(`${tag} 轮询超时`); // ---- 步骤 3: 下载音频 ---- console.log(`${tag} ⬇️ 步骤3: 下载音频...`); const downloadRes = await fetch( `${CONFIG.ttsApiPath}/v1/files/retrieve_content?file_id=${file_id}`, { headers: { 'Authorization': `Bearer ${CONFIG.apiKey}` } } ); const arrayBuffer = await downloadRes.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const mp3Data = extractMp3FromTar(buffer); if (!mp3Data) throw new Error(`${tag} tar 中未找到 MP3 文件`); if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const outputPath = path.join(OUTPUT_DIR, `output-${requestId}.mp3`); fs.writeFileSync(outputPath, mp3Data); console.log(`${tag} ✅ 音频已保存: ${outputPath} (${mp3Data.length} bytes)`); } // ============================================================ // 主流程:同时发送 2 个 TTS 请求 // ============================================================ async function main() { console.log('🚀 MiniMax Key2 TTS 测试 - 并行 2 个请求'); console.log(`📝 文本: "${TEXT}"`); console.log(`🎤 音色: ${CONFIG.voice}`); console.log(`🤖 模型: ${CONFIG.model}`); console.log(`📁 输出: ${OUTPUT_DIR}`); const startTime = Date.now(); try { await Promise.all([ ttsRequest(TEXT, 1), ttsRequest(TEXT, 2), ]); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`\n${'='.repeat(60)}`); console.log(`🎉 全部完成! 耗时: ${elapsed}s`); console.log(`📂 输出目录: ${OUTPUT_DIR}`); } catch (err: any) { console.error(`\n❌ 测试失败:`, err.message); process.exit(1); } } main();