audio-source-separation-mcp
Version:
MCP服务器,用于火山引擎音源分离,支持人声/伴奏分离
443 lines (379 loc) • 13.4 kB
text/typescript
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);
});
}