UNPKG

tts-mcp

Version:

OpenAI Text to Speech APIを活用したコマンドラインツールとMCPサーバー

275 lines (274 loc) 11.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initializeClient = initializeClient; exports.startMcpServer = startMcpServer; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const openai_1 = require("openai"); const z = __importStar(require("zod")); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); // 一時ディレクトリのパスを取得するために使用 const play_sound_1 = __importDefault(require("play-sound")); const utils_1 = require("./utils"); // プレイヤーの初期化 const player = (0, play_sound_1.default)({}); // ログファイルパス let logFile = path_1.default.join(process.cwd(), 'tts-mcp.log'); /** * ログファイルにメッセージを追加します * @param {string} message - ログメッセージ */ async function logToFile(message) { const timestamp = new Date().toISOString(); const logMessage = `${timestamp} - ${message}\n`; try { await fs_1.promises.appendFile(logFile, logMessage); } catch (error) { // ログ書き込みエラーは無視(エラー処理のループを避けるため) } } /** * OpenAI APIのクライアントを初期化します * @param {string} apiKey - OpenAI APIキー * @returns {OpenAI} OpenAIクライアントインスタンス */ function initializeClient(apiKey) { if (!apiKey) { throw new Error('OpenAI APIキーが設定されていません。--api-keyオプションか環境変数OPENAI_API_KEYを設定してください。'); } return new openai_1.OpenAI({ apiKey: apiKey }); } /** * テキストを音声に変換して再生します * @param {TTSPlayOptions} options - 変換オプション * @returns {Promise<TTSPlayResult>} 処理結果 */ async function textToSpeechAndPlay(options) { const client = initializeClient(options.apiKey); let tempFilePath = null; try { await logToFile('音声生成開始...'); // 入力の検証 const safeVoice = (0, utils_1.validateVoice)(options.voice); const safeModel = (0, utils_1.validateModel)(options.model); const safeFormat = (0, utils_1.validateFormat)(options.format); const response = await client.audio.speech.create({ model: safeModel, voice: safeVoice, input: options.text, speed: options.speed, response_format: safeFormat, ...(options.instructions ? { instructions: options.instructions } : {}) }); // 音声データを取得 const buffer = Buffer.from(await response.arrayBuffer()); // 直接一時ファイルパスを生成 tempFilePath = path_1.default.join(os_1.default.tmpdir(), `speech_${Date.now()}.${safeFormat}`); // バッファをファイルに書き込む await fs_1.promises.writeFile(tempFilePath, buffer); await logToFile(`音声ファイルを作成しました: ${tempFilePath}`); // 再生開始時間を記録 const startTime = Date.now(); await logToFile(`音声を再生します: ${tempFilePath}`); // 音声を再生(Promise化) await new Promise((resolve, reject) => { player.play(tempFilePath, (err) => { if (err) reject(err); else resolve(); }); }); // 再生時間を計算(秒単位) const duration = ((Date.now() - startTime) / 1000).toFixed(1); await logToFile(`音声の再生が完了しました(再生時間: ${duration}秒)`); // 一時ファイルの削除を試みる(任意) try { await fs_1.promises.unlink(tempFilePath); await logToFile(`一時ファイルを削除しました: ${tempFilePath}`); } catch (cleanupError) { await logToFile(`一時ファイルの削除に失敗しました: ${cleanupError.message}`); // 削除に失敗しても処理は続行 } return { duration, textLength: options.text.length }; } catch (error) { // エラーが発生した場合、一時ファイルが存在していれば削除を試みる if (tempFilePath) { try { await fs_1.promises.unlink(tempFilePath); } catch (cleanupError) { // 削除エラーは無視 } } // APIエラーをより詳細にログ記録 if (error.response) { const apiError = error.response.data; await logToFile(`OpenAI API エラー:`); await logToFile(`- ステータス: ${error.response.status}`); await logToFile(`- メッセージ: ${apiError.error?.message || 'エラー詳細なし'}`); await logToFile(`- タイプ: ${apiError.error?.type || 'エラータイプなし'}`); // 元のエラーもログに記録 await logToFile(`生のレスポンスデータ: ${JSON.stringify(error.response.data)}`); } else { // 一般的なエラー await logToFile(`エラー: ${error.message}`); } throw error; } } /** * MCPサーバーを作成して設定します * @param {MCPServerConfig} config - サーバー設定 * @returns {Promise<McpServer>} 設定済みMCPサーバー */ async function createMcpServer(config) { // カスタムログファイルがあれば設定 if (config.logFile) { logFile = config.logFile; } // ログファイルを初期化 await logToFile('---------------------------------------'); await logToFile(`MCPサーバーを初期化しています...`); await logToFile(`設定: モデル=${config.model}, 音声=${config.voice}, フォーマット=${config.format}`); // MCPサーバーを作成 const server = new mcp_js_1.McpServer({ name: "tts-mcp", version: "1.1.0", // サポートする機能を明示的に定義 capabilities: { tools: {} } }); // テキスト音声変換と再生ツールを追加 server.tool("text-to-speech", "Converts text to speech and plays it using OpenAI's TTS API", { text: z.string().describe("The text content to be converted to speech"), speed: z.number().min(0.25).max(4.0).optional().default(1.0).describe("Speech speed factor (0.25 to 4.0, default: 1.0)"), instructions: z.string().optional().describe("Optional instructions to guide the speech generation (e.g. emotions, style)"), }, async (params) => { try { const result = await textToSpeechAndPlay({ text: params.text, model: config.model, voice: config.voice, speed: params.speed, format: config.format, instructions: params.instructions, apiKey: config.apiKey }); return { content: [ { type: "text", text: `テキストを音声で再生しました(再生時間: ${result.duration}秒)` } ], metadata: { duration: result.duration, text_length: result.textLength } }; } catch (error) { return { content: [ { type: "text", text: `エラー: 音声の生成または再生に失敗しました` }, { type: "text", text: `詳細: ${error.message}` }, { type: "text", text: `対処方法: OpenAI APIキーの確認、テキスト内容の確認、または別のボイスやモデルの使用をお試しください。` } ], isError: true }; } }); return server; } /** * MCPサーバーを起動します * @param {MCPServerConfig} config - サーバー設定 * @returns {Promise<void>} */ async function startMcpServer(config) { // 入力値の検証 const safeModel = (0, utils_1.validateModel)(config.model); const safeVoice = (0, utils_1.validateVoice)(config.voice); const safeFormat = (0, utils_1.validateFormat)(config.format); // 検証済みの値で設定を上書き const validatedConfig = { ...config, model: safeModel, voice: safeVoice, format: safeFormat }; // stderr経由でコンテキスト情報を出力 process.stderr.write(`モデル=${safeModel}, 音声=${safeVoice}, フォーマット=${safeFormat}\n`); try { // サーバーを作成 const server = await createMcpServer(validatedConfig); // STDIOトランスポートを使用 const transport = new stdio_js_1.StdioServerTransport(); // サーバーを接続して開始 await server.connect(transport); await logToFile("MCPサーバーが起動しました"); } catch (error) { await logToFile(`MCPサーバー起動エラー: ${error.message}`); // stderrにもエラーを出力 process.stderr.write(`エラー: ${error.message}\n`); throw error; } }