UNPKG

audio-source-separation-mcp

Version:

MCP服务器,用于火山引擎音源分离,支持人声/伴奏分离

443 lines (379 loc) 13.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import * as path from 'path'; import { Service } from "@volcengine/openapi"; interface SeparationPayload { model: string; audio_info: { format: string; sample_rate: number; channel: number; }; extra: { output_format: string; output_sample_rate: number; output_channel: number; }; } interface ApiRequestBody { token: string; appkey: string; namespace: string; data: string; payload: string; } interface ApiResponse { status_code: number; status_text?: string; data: string; task_id?: string; } class AudioSeparationServer { private server: Server; constructor() { console.error("🚀 启动音源分离MCP服务器..."); console.error(`🔧 环境变量检查:`); console.error(` VOLCENGINE_ACCESS_KEY_ID: ${process.env.VOLCENGINE_ACCESS_KEY_ID ? '已设置' : '未设置'}`); console.error(` VOLCENGINE_SECRET_KEY: ${process.env.VOLCENGINE_SECRET_KEY ? '已设置' : '未设置'}`); console.error(` VOLCENGINE_APP_KEY: ${process.env.VOLCENGINE_APP_KEY ? '已设置' : '未设置'}`); this.server = new Server( { name: "audio-source-separation-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); } private setupHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("📋 处理工具列表请求"); return { tools: [ { name: "extract_vocals", description: "提取人声(去除伴奏)", inputSchema: { type: "object", properties: { audio_path: { type: "string", description: "音频文件路径" }, use_enhanced_model: { type: "boolean", description: "是否使用降噪增强模型", default: false }, output_format: { type: "string", enum: ["wav", "mp3", "aac"], description: "输出音频格式", default: "wav" }, output_path: { type: "string", description: "输出文件路径(可选)" } }, required: ["audio_path"] } }, { name: "extract_accompaniment", description: "提取伴奏(去除人声)", inputSchema: { type: "object", properties: { audio_path: { type: "string", description: "音频文件路径" }, use_enhanced_model: { type: "boolean", description: "是否使用降噪增强模型", default: false }, output_format: { type: "string", enum: ["wav", "mp3", "aac"], description: "输出音频格式", default: "wav" }, output_path: { type: "string", description: "输出文件路径(可选)" } }, required: ["audio_path"] } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error(`🔧 处理工具调用: ${request.params.name}`); const { name, arguments: args } = request.params; try { switch (name) { case "extract_vocals": return await this.handleExtractVocals(args); case "extract_accompaniment": return await this.handleExtractAccompaniment(args); default: throw new McpError( ErrorCode.MethodNotFound, `未知工具: ${name}` ); } } catch (error) { console.error(`❌ 工具执行错误:`, error); throw new McpError( ErrorCode.InternalError, `工具执行失败: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async handleExtractVocals(args: any) { const { audio_path, use_enhanced_model = false, output_format = "wav", output_path } = args; if (!audio_path) { throw new McpError(ErrorCode.InvalidParams, "缺少必需参数: audio_path"); } // 检查环境变量 const hasCredentials = process.env.VOLCENGINE_ACCESS_KEY_ID && process.env.VOLCENGINE_SECRET_KEY && process.env.VOLCENGINE_APP_KEY; if (!hasCredentials) { return { content: [ { type: "text", text: `提取人声配置: 输入文件: ${audio_path} 使用增强模型: ${use_enhanced_model ? '是' : '否'} 输出格式: ${output_format} ${output_path ? `输出路径: ${output_path}` : ''} ❌ 火山引擎API凭证缺失! 当前环境变量状态: - VOLCENGINE_ACCESS_KEY_ID: ${process.env.VOLCENGINE_ACCESS_KEY_ID ? '✅ 已设置' : '❌ 未设置'} - VOLCENGINE_SECRET_KEY: ${process.env.VOLCENGINE_SECRET_KEY ? '✅ 已设置' : '❌ 未设置'} - VOLCENGINE_APP_KEY: ${process.env.VOLCENGINE_APP_KEY ? '✅ 已设置' : '❌ 未设置'} 请检查Claude Desktop配置中的环境变量设置。` } ] }; } // 选择人声提取模型 const model = use_enhanced_model ? 'bs_4track_vocal' : '2track_vocal'; // 执行音频分离 return await this.performAudioSeparation(audio_path, model, output_format, output_path); } private async handleExtractAccompaniment(args: any) { const { audio_path, use_enhanced_model = false, output_format = "wav", output_path } = args; if (!audio_path) { throw new McpError(ErrorCode.InvalidParams, "缺少必需参数: audio_path"); } // 检查环境变量 const hasCredentials = process.env.VOLCENGINE_ACCESS_KEY_ID && process.env.VOLCENGINE_SECRET_KEY && process.env.VOLCENGINE_APP_KEY; if (!hasCredentials) { return { content: [ { type: "text", text: `提取伴奏配置: 输入文件: ${audio_path} 使用增强模型: ${use_enhanced_model ? '是' : '否'} 输出格式: ${output_format} ${output_path ? `输出路径: ${output_path}` : ''} ❌ 火山引擎API凭证缺失! 当前环境变量状态: - VOLCENGINE_ACCESS_KEY_ID: ${process.env.VOLCENGINE_ACCESS_KEY_ID ? '✅ 已设置' : '❌ 未设置'} - VOLCENGINE_SECRET_KEY: ${process.env.VOLCENGINE_SECRET_KEY ? '✅ 已设置' : '❌ 未设置'} - VOLCENGINE_APP_KEY: ${process.env.VOLCENGINE_APP_KEY ? '✅ 已设置' : '❌ 未设置'} 请检查Claude Desktop配置中的环境变量设置。` } ] }; } // 选择伴奏提取模型 const model = use_enhanced_model ? 'bs_4track_acc' : '2track_acc'; // 执行音频分离 return await this.performAudioSeparation(audio_path, model, output_format, output_path); } // 实际的音频分离函数 private async performAudioSeparation(audioPath: string, model: string, outputFormat: string, outputPath?: string) { console.error(`🎵 开始音频分离: ${audioPath}`); try { // 读取音频文件 console.error('📖 读取音频文件...'); const audioBuffer = await fs.readFile(audioPath); const base64Audio = audioBuffer.toString('base64'); console.error(`📦 文件大小: ${Math.round(audioBuffer.length / 1024)}KB`); console.error(`🔢 Base64长度: ${base64Audio.length}`); // 构建请求参数 const payload: SeparationPayload = { model: model, audio_info: { format: path.extname(audioPath).slice(1).toLowerCase(), sample_rate: 44100, channel: 2 }, extra: { output_format: outputFormat, output_sample_rate: 44100, output_channel: 1 } }; const requestBody: ApiRequestBody = { token: await this.getVolcengineToken(), appkey: process.env.VOLCENGINE_APP_KEY!, namespace: 'MusicSourceSeparate', data: base64Audio, payload: JSON.stringify(payload) }; console.error('🌐 调用火山引擎API...'); // 调用火山引擎API const response = await fetch('https://sami.bytedance.com/api/v1/invoke', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API调用失败: ${response.status} ${response.statusText}`); } const result: ApiResponse = await response.json(); console.error(`📋 API响应状态: ${result.status_code}`); console.error(`📋 API响应消息: ${result.status_text}`); // 检查API响应状态 - 根据火山引擎文档,成功的状态码是20000000 if (result.status_code !== 20000000) { throw new Error(`音源分离失败: ${result.status_text || `状态码: ${result.status_code}`}`); } if (!result.data) { throw new Error('API响应中缺少音频数据'); } // 解码并保存结果 console.error('💾 保存分离结果...'); const outputBuffer = Buffer.from(result.data, 'base64'); // 生成输出路径 const finalOutputPath = outputPath || this.generateOutputPath(audioPath, model, outputFormat); await fs.writeFile(finalOutputPath, outputBuffer); console.error(`✅ 音频分离完成: ${finalOutputPath}`); return { content: [ { type: "text", text: `🎉 音频分离成功! 输入文件: ${audioPath} 分离模型: ${model} 输出格式: ${outputFormat} 输出文件: ${finalOutputPath} 输出大小: ${Math.round(outputBuffer.length / 1024)}KB 任务ID: ${result.task_id || 'N/A'} ✅ 文件已保存到指定位置。` } ] }; } catch (error) { console.error(`❌ 音频分离失败:`, error); return { content: [ { type: "text", text: `❌ 音频分离失败: 输入文件: ${audioPath} 分离模型: ${model} 错误信息: ${error instanceof Error ? error.message : String(error)} 请检查: 1. 文件路径是否正确 2. 文件格式是否受支持 3. 网络连接是否正常 4. API凭证是否有效` } ] }; } } // 获取火山引擎Token private async getVolcengineToken(): Promise<string> { console.error('🔐 获取火山引擎Token...'); const options = { accessKeyId: process.env.VOLCENGINE_ACCESS_KEY_ID!, secretKey: process.env.VOLCENGINE_SECRET_KEY!, region: 'cn-north-1', host: 'open.volcengineapi.com', serviceName: 'sami', defaultVersion: '2021-07-27', }; const samiService = new Service(options); const bodyObj = { token_version: 'volc-auth-v1', appkey: process.env.VOLCENGINE_APP_KEY!, expiration: 36000, }; const res = await samiService.fetchOpenAPI({ pathname: "/", Action: "GetToken", Version: "2021-07-27", method: "POST", headers: { 'content-type': 'application/json', }, data: JSON.stringify(bodyObj) }); // 直接使用响应结果 const responseData = res as any; if (!responseData?.token) { throw new Error(responseData?.msg ?? '获取token失败'); } console.error('✅ Token获取成功'); return responseData.token; } // 生成输出路径 private generateOutputPath(inputPath: string, model: string, outputFormat: string): string { const dir = path.dirname(inputPath); const nameWithoutExt = path.parse(inputPath).name; const timestamp = Date.now(); const suffix = model.includes('vocal') ? 'vocal' : 'accompaniment'; return path.join(dir, `${nameWithoutExt}_${suffix}_${timestamp}.${outputFormat}`); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("✅ 音源分离MCP服务器启动成功"); } } export async function main(): Promise<void> { try { const server = new AudioSeparationServer(); await server.run(); } catch (error) { console.error("❌ 服务器启动失败:", error); process.exit(1); } } import { fileURLToPath } from 'url'; // 检查是否直接运行此文件 const __filename = fileURLToPath(import.meta.url); if (process.argv[1] === __filename) { main().catch((error) => { console.error("❌ 服务器启动失败:", error); process.exit(1); }); }