@kajidog/mcp-tts-voicevox 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024 kajidog
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # MCP TTS VOICEVOX
2
+
3
+ VOICEVOXを使用した音声合成MCPサーバー
4
+
5
+ ## 必要条件
6
+
7
+ - Node.js
8
+ - [VOICEVOXエンジン](https://voicevox.hiroshiba.jp/)
9
+
10
+ ## 使い方
11
+
12
+ ### インストール
13
+
14
+ ```bash
15
+ npm install -g @kajidog/mcp-tts-voicevox
16
+ ```
17
+
18
+ ### 実行
19
+
20
+ 1. VOICEVOXエンジンを起動
21
+ 2. 以下のコマンドを実行
22
+
23
+ ```bash
24
+ npx @kajidog/mcp-tts-voicevox
25
+ ```
26
+
27
+ ### MCPツールとして使用
28
+
29
+ ```typescript
30
+ await mcp.invoke("speak", {
31
+ text: "こんにちは!",
32
+ speaker: 1 // 話者ID(オプション)
33
+ });
34
+ ```
35
+
36
+ ## 環境変数
37
+
38
+ - `VOICEVOX_URL`: VOICEVOXエンジンのURL(デフォルト: `http://localhost:50021`)
39
+
40
+ ## ライセンス
41
+
42
+ ISC
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const zod_1 = require("zod");
7
+ const index_js_1 = require("./voicevox/index.js");
8
+ const server = new mcp_js_1.McpServer({
9
+ name: "MCP TTS Voicevox",
10
+ version: "0.0.2",
11
+ description: "Voicevoxで音声を生成します。",
12
+ });
13
+ server.tool("speak", { text: zod_1.z.string(), speaker: zod_1.z.number().optional() }, async ({ text, speaker }) => {
14
+ const voicevoxClient = new index_js_1.VoicevoxClient({
15
+ url: process.env.VOICEVOX_URL ?? "http://localhost:50021",
16
+ defaultSpeaker: 1,
17
+ });
18
+ const result = await voicevoxClient.speak(text, speaker);
19
+ return {
20
+ content: [{ type: "text", text: result }],
21
+ };
22
+ });
23
+ server.connect(new stdio_js_1.StdioServerTransport()).catch(console.error);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,GAEV,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,oBAAoB;IAC1B,OAAO,EAAE,OAAO;IAChB,WAAW,EAAE,oBAAoB;CAClC,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG,IAAI,cAAc,CACvC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,wBAAwB,CACrD,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,OAAO,EACP,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,EACpD,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,EAAE,EAAE,EAAE;IAC9B,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;KACnD,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACtC,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/test.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const voicevox_1 = require("./voicevox");
4
+ async function main() {
5
+ try {
6
+ console.log("VOICEVOXテストを開始します...");
7
+ const player = new voicevox_1.VoicevoxClient({
8
+ url: "http://localhost:50021",
9
+ });
10
+ // 複数のテキストをキューに追加
11
+ const result = await player.speak("おはようございます!今日の天気はあめ!", // 長い文章
12
+ 1);
13
+ console.log(result);
14
+ }
15
+ catch (error) {
16
+ console.error("テスト中にエラーが発生しました:", error);
17
+ process.exit(1);
18
+ }
19
+ }
20
+ main();
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VoicevoxClient = void 0;
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ class VoicevoxClient {
9
+ constructor(voicevoxUrl = "http://localhost:50021") {
10
+ this.voicevoxUrl = voicevoxUrl;
11
+ }
12
+ async generateAudioQuery(text, speaker = 1) {
13
+ console.log(`音声クエリを生成中... テキスト: "${text}", 話者: ${speaker}`);
14
+ try {
15
+ const response = await (0, node_fetch_1.default)(`${this.voicevoxUrl}/audio_query?text=${encodeURIComponent(text)}&speaker=${speaker}`, {
16
+ method: "POST",
17
+ headers: {
18
+ Accept: "application/json",
19
+ },
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`音声クエリの生成に失敗しました: ${response.status} ${response.statusText}`);
23
+ }
24
+ const query = await response.json();
25
+ return query;
26
+ }
27
+ catch (error) {
28
+ console.error("音声クエリの生成中にエラーが発生しました:", error);
29
+ throw error;
30
+ }
31
+ }
32
+ async synthesize(query, speaker = 1) {
33
+ try {
34
+ const response = await (0, node_fetch_1.default)(`${this.voicevoxUrl}/synthesis?speaker=${speaker}`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ Accept: "audio/wav",
39
+ },
40
+ body: JSON.stringify(query),
41
+ });
42
+ if (!response.ok) {
43
+ throw new Error(`音声合成に失敗しました: ${response.status} ${response.statusText}`);
44
+ }
45
+ const audioData = await response.arrayBuffer();
46
+ return audioData;
47
+ }
48
+ catch (error) {
49
+ console.error("音声合成中にエラーが発生しました:", error);
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+ exports.VoicevoxClient = VoicevoxClient;
@@ -0,0 +1,6 @@
1
+ export declare class VoicevoxGenerator {
2
+ private voicevoxUrl;
3
+ constructor(voicevoxUrl?: string);
4
+ generateAudio(text: string, speaker: number): Promise<ArrayBuffer>;
5
+ }
6
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/voicevox/generator.ts"],"names":[],"mappings":"AAEA,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,GAAE,MAAiC;IAK7C,aAAa,CACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC;CAuBxB"}
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VoicevoxGenerator = void 0;
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ class VoicevoxGenerator {
9
+ constructor(voicevoxUrl = "http://localhost:50021") {
10
+ this.voicevoxUrl = voicevoxUrl;
11
+ }
12
+ // 音声生成関数
13
+ async generateAudio(text, speaker) {
14
+ const queryResponse = await (0, node_fetch_1.default)(`${this.voicevoxUrl}/audio_query?text=${encodeURIComponent(text)}&speaker=${speaker}`, {
15
+ method: "POST",
16
+ });
17
+ if (!queryResponse.ok) {
18
+ throw new Error(`音声クエリの生成に失敗しました: ${queryResponse.status} ${queryResponse.statusText}`);
19
+ }
20
+ const query = await queryResponse.json();
21
+ // 音声合成を実行
22
+ const synthesisResponse = await (0, node_fetch_1.default)(`${this.voicevoxUrl}/synthesis?speaker=${speaker}`, {
23
+ method: "POST",
24
+ headers: {
25
+ Accept: "audio/wav",
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify(query),
29
+ });
30
+ if (!synthesisResponse.ok) {
31
+ throw new Error(`音声合成に失敗しました: ${synthesisResponse.status} ${synthesisResponse.statusText}`);
32
+ }
33
+ const audioData = await synthesisResponse.arrayBuffer();
34
+ return audioData;
35
+ }
36
+ }
37
+ exports.VoicevoxGenerator = VoicevoxGenerator;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/voicevox/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAE/B,MAAM,OAAO,iBAAiB;IAG5B,YAAY,cAAsB,wBAAwB;QACxD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED,SAAS;IACF,KAAK,CAAC,aAAa,CACxB,IAAY,EACZ,OAAe;QAEf,eAAe;QACf,MAAM,aAAa,GAAG,MAAM,KAAK,CAC/B,GAAG,IAAI,CAAC,WAAW,qBAAqB,kBAAkB,CACxD,IAAI,CACL,YAAY,OAAO,EAAE,CACvB,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC;QAEzC,UAAU;QACV,MAAM,iBAAiB,GAAG,MAAM,KAAK,CACnC,GAAG,IAAI,CAAC,WAAW,sBAAsB,OAAO,EAAE,EAClD;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SAC5B,CACF,CAAC;QAEF,OAAO,MAAM,iBAAiB,CAAC,WAAW,EAAE,CAAC;IAC/C,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ export declare class VoicevoxClient {
2
+ private audioGenerationQueue;
3
+ private audioPlaybackQueue;
4
+ private generator;
5
+ private player;
6
+ constructor(voicevoxUrl?: string);
7
+ private processAudioQueue;
8
+ private startQueueProcessing;
9
+ speak(text: string, speaker?: number): Promise<string>;
10
+ }
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voicevox/index.ts"],"names":[],"mappings":"AAGA,qBAAa,cAAc;IACzB,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAAiB;gBAEnB,WAAW,GAAE,MAAiC;YAO5C,iBAAiB;IAkB/B,OAAO,CAAC,oBAAoB;IAKf,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CAIvE"}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VoicevoxClient = void 0;
4
+ const player_1 = require("./player");
5
+ class VoicevoxClient {
6
+ constructor(config) {
7
+ this.maxSegmentLength = 150; // より長い区切りを許容
8
+ this.validateConfig(config);
9
+ this.defaultSpeaker = config.defaultSpeaker ?? 1;
10
+ this.player = new player_1.VoicevoxPlayer(config.url);
11
+ }
12
+ /**
13
+ * テキストを音声に変換して再生します
14
+ * @param text 変換するテキスト
15
+ * @param speaker 話者ID(オプション)
16
+ * @returns 処理結果のメッセージ
17
+ */
18
+ async speak(text, speaker) {
19
+ try {
20
+ const speakerId = speaker ?? this.defaultSpeaker;
21
+ const segments = this.splitText(text);
22
+ for (const segment of segments) {
23
+ await this.player.enqueue(segment, speakerId);
24
+ }
25
+ return `音声生成キューに追加しました: ${text}`;
26
+ }
27
+ catch (error) {
28
+ console.error("音声生成中にエラーが発生しました:", error);
29
+ throw new Error(`音声生成に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
30
+ }
31
+ }
32
+ /**
33
+ * テキストを自然な区切りで分割します
34
+ * @param text 分割するテキスト
35
+ * @returns 分割されたテキストの配列
36
+ */
37
+ splitText(text) {
38
+ // 文の区切りとなるパターン
39
+ const sentenceEndings = /([。!?])/g;
40
+ // 自然な区切りとなる接続詞や助詞
41
+ const naturalBreaks = /([が、しかし、でも、けれど、そして、また、または、それで、だから、ですから、そのため、したがって、ゆえに、])/g;
42
+ const segments = [];
43
+ let currentSegment = "";
44
+ // まず文末で分割
45
+ const parts = text.split(sentenceEndings);
46
+ for (let i = 0; i < parts.length; i++) {
47
+ const part = parts[i];
48
+ // 文末記号の場合は現在のセグメントに追加
49
+ if (sentenceEndings.test(part)) {
50
+ currentSegment += part;
51
+ if (currentSegment.trim()) {
52
+ segments.push(currentSegment.trim());
53
+ currentSegment = "";
54
+ }
55
+ continue;
56
+ }
57
+ // 空の部分はスキップ
58
+ if (!part.trim())
59
+ continue;
60
+ // 自然な区切りで分割
61
+ const subParts = part.split(naturalBreaks);
62
+ for (let j = 0; j < subParts.length; j++) {
63
+ const subPart = subParts[j];
64
+ // 接続詞や助詞の場合は現在のセグメントに追加
65
+ if (naturalBreaks.test(subPart)) {
66
+ currentSegment += subPart;
67
+ continue;
68
+ }
69
+ // 空の部分はスキップ
70
+ if (!subPart.trim())
71
+ continue;
72
+ // 現在のセグメントに追加
73
+ currentSegment += subPart;
74
+ // セグメントが最大長を超えた場合、または最後の部分の場合
75
+ if (currentSegment.length >= this.maxSegmentLength ||
76
+ (i === parts.length - 1 && j === subParts.length - 1)) {
77
+ if (currentSegment.trim()) {
78
+ segments.push(currentSegment.trim());
79
+ }
80
+ currentSegment = "";
81
+ }
82
+ }
83
+ }
84
+ // 残りのテキストがあれば追加
85
+ if (currentSegment.trim()) {
86
+ segments.push(currentSegment.trim());
87
+ }
88
+ return segments;
89
+ }
90
+ validateConfig(config) {
91
+ if (!config.url) {
92
+ throw new Error("VOICEVOXのURLが指定されていません");
93
+ }
94
+ try {
95
+ new URL(config.url);
96
+ }
97
+ catch {
98
+ throw new Error("無効なVOICEVOXのURLです");
99
+ }
100
+ }
101
+ }
102
+ exports.VoicevoxClient = VoicevoxClient;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/voicevox/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,OAAO,cAAc;IAMzB,YAAY,cAAsB,wBAAwB;QALlD,yBAAoB,GAAwC,EAAE,CAAC;QAC/D,uBAAkB,GAAkB,EAAE,CAAC;QAK7C,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAiB,CAAC,WAAW,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,GAAG,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,oBAAoB,EAAE,CAAC;IAC9B,CAAC;IAED,iBAAiB;IACT,KAAK,CAAC,iBAAiB;QAC7B,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,oBAAoB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAG,CAAC;gBAC7D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;gBACpE,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC1C,CAAC;YAED,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvC,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAG,CAAC;gBACnD,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YACzC,CAAC;YAED,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,WAAW;IACH,oBAAoB;QAC1B,IAAI,CAAC,iBAAiB,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChD,CAAC;IAED,aAAa;IACN,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,UAAkB,CAAC;QAClD,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAClD,OAAO,mBAAmB,IAAI,EAAE,CAAC;IACnC,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export declare class VoicevoxPlayer {
2
+ private voicevoxUrl;
3
+ constructor(voicevoxUrl?: string);
4
+ playAudio(audioData: ArrayBuffer): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/voicevox/player.ts"],"names":[],"mappings":"AAEA,qBAAa,cAAc;IACzB,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,GAAE,MAAiC;IAK7C,SAAS,CAAC,SAAS,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU9D"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VoicevoxPlayer = void 0;
7
+ const promises_1 = require("fs/promises");
8
+ const path_1 = require("path");
9
+ const os_1 = require("os");
10
+ const axios_1 = __importDefault(require("axios"));
11
+ const sound = require("sound-play");
12
+ class VoicevoxPlayer {
13
+ constructor(voicevoxUrl = "http://localhost:50021") {
14
+ this.queue = [];
15
+ this.isPlaying = false;
16
+ this.isGenerating = false;
17
+ this.prefetchSize = 2; // プリフェッチするアイテム数
18
+ this.voicevoxUrl = voicevoxUrl;
19
+ }
20
+ // キューに追加
21
+ async enqueue(text, speaker = 1) {
22
+ const item = { text, speaker };
23
+ this.queue.push(item);
24
+ await this.generateAudio(item); // 音声データの生成を待つ
25
+ this.prefetchAudio(); // 次の音声の事前生成を開始
26
+ this.processQueue(); // 再生キューの処理を開始
27
+ }
28
+ // キューをクリア
29
+ clearQueue() {
30
+ this.queue = [];
31
+ }
32
+ // 音声の事前生成
33
+ async prefetchAudio() {
34
+ if (this.isGenerating)
35
+ return;
36
+ this.isGenerating = true;
37
+ try {
38
+ // プリフェッチサイズまでの音声を生成
39
+ const itemsToGenerate = this.queue
40
+ .filter((item) => !item.audioData)
41
+ .slice(0, this.prefetchSize);
42
+ await Promise.all(itemsToGenerate.map((item) => this.generateAudio(item)));
43
+ }
44
+ catch (error) {
45
+ console.error("音声の事前生成中にエラーが発生しました:", error);
46
+ }
47
+ finally {
48
+ this.isGenerating = false;
49
+ }
50
+ }
51
+ // 音声生成処理
52
+ async generateAudio(item) {
53
+ try {
54
+ // 音声クエリを生成
55
+ const queryResponse = await axios_1.default.post(`${this.voicevoxUrl}/audio_query?text=${encodeURIComponent(item.text)}&speaker=${item.speaker}`, null, {
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ },
59
+ });
60
+ if (queryResponse.status !== 200) {
61
+ throw new Error(`音声クエリの生成に失敗しました: ${queryResponse.status}`);
62
+ }
63
+ const query = queryResponse.data;
64
+ // 音声を合成
65
+ const synthesisResponse = await axios_1.default.post(`${this.voicevoxUrl}/synthesis?speaker=${item.speaker}`, query, {
66
+ responseType: "arraybuffer",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Accept: "audio/wav",
70
+ },
71
+ });
72
+ if (synthesisResponse.status !== 200) {
73
+ throw new Error(`音声合成に失敗しました: ${synthesisResponse.status}`);
74
+ }
75
+ // 音声データを保存
76
+ item.audioData = synthesisResponse.data;
77
+ // 一時ファイルに保存
78
+ if (item.audioData) {
79
+ const tempFile = (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-${Date.now()}.wav`);
80
+ await (0, promises_1.writeFile)(tempFile, Buffer.from(item.audioData));
81
+ item.tempFile = tempFile;
82
+ }
83
+ }
84
+ catch (error) {
85
+ console.error("音声生成中にエラーが発生しました:", error);
86
+ throw error;
87
+ }
88
+ }
89
+ // キューの処理
90
+ async processQueue() {
91
+ if (this.isPlaying || this.queue.length === 0) {
92
+ return;
93
+ }
94
+ this.isPlaying = true;
95
+ try {
96
+ while (this.queue.length > 0) {
97
+ const item = this.queue[0];
98
+ // 音声データが生成されるまで待機
99
+ while (!item.audioData) {
100
+ await new Promise((resolve) => setTimeout(resolve, 100));
101
+ }
102
+ // 音声を再生
103
+ if (item.tempFile) {
104
+ await sound.play(item.tempFile);
105
+ // 再生が完了したら一時ファイルを削除
106
+ try {
107
+ await (0, promises_1.unlink)(item.tempFile);
108
+ }
109
+ catch (error) {
110
+ console.warn("一時ファイルの削除に失敗しました:", error);
111
+ }
112
+ }
113
+ // キューから削除
114
+ this.queue.shift();
115
+ // 次の音声の事前生成を開始
116
+ this.prefetchAudio();
117
+ }
118
+ }
119
+ catch (error) {
120
+ console.error("キュー処理中にエラーが発生しました:", error);
121
+ }
122
+ finally {
123
+ this.isPlaying = false;
124
+ }
125
+ }
126
+ }
127
+ exports.VoicevoxPlayer = VoicevoxPlayer;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.js","sourceRoot":"","sources":["../../src/voicevox/player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAE/B,MAAM,OAAO,cAAc;IAGzB,YAAY,cAAsB,wBAAwB;QACxD,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED,SAAS;IACF,KAAK,CAAC,SAAS,CAAC,SAAsB;QAC3C,QAAQ;QACR,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,WAAW,OAAO,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,WAAW;aAC5B;YACD,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;IACL,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kajidog/mcp-tts-voicevox",
3
+ "version": "0.0.2",
4
+ "description": "VOICEVOX integration for MCP - Text-to-Speech server using VOICEVOX engine",
5
+ "main": "./dist/index.js",
6
+ "bin": {
7
+ "mcp-tts-voicevox": "dist/index.js"
8
+ },
9
+ "type": "commonjs",
10
+ "scripts": {
11
+ "test": "ts-node src/test.ts",
12
+ "start": "node dist/index.js",
13
+ "build": "tsc",
14
+ "prepare": "npm run build",
15
+ "prepublishOnly": "npm test"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "keywords": [
23
+ "voicevox",
24
+ "tts",
25
+ "mcp"
26
+ ],
27
+ "author": {
28
+ "name": "kajidog",
29
+ "email": "dev@kajidog.com"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/kajidog/mcp-tts-voicevox.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/kajidog/mcp-tts-voicevox/issues"
37
+ },
38
+ "homepage": "https://github.com/kajidog/mcp-tts-voicevox#readme",
39
+ "license": "ISC",
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.8.0",
42
+ "axios": "^1.8.4",
43
+ "sound-play": "^1.1.0",
44
+ "zod": "^3.22.4"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0",
48
+ "ts-node": "^10.9.0",
49
+ "typescript": "^5.0.0"
50
+ }
51
+ }