@kajidog/mcp-tts-voicevox 0.0.4 → 0.0.6
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/dist/index.js +73 -23
- package/dist/test.js +271 -120
- package/dist/voicevox/api.js +97 -0
- package/dist/voicevox/client.js +365 -28
- package/dist/voicevox/error.js +26 -0
- package/dist/voicevox/index.js +51 -98
- package/dist/voicevox/player.js +159 -254
- package/dist/voicevox/queue/__tests__/manager.test.js +478 -0
- package/dist/voicevox/queue/audio-generator.js +83 -0
- package/dist/voicevox/queue/audio-player.js +39 -0
- package/dist/voicevox/queue/event-manager.js +59 -0
- package/dist/voicevox/queue/file-manager.js +86 -0
- package/dist/voicevox/queue/index.js +22 -0
- package/dist/voicevox/queue/manager.js +314 -0
- package/dist/voicevox/queue/types.js +31 -0
- package/dist/voicevox/types.js +19 -0
- package/dist/voicevox/utils.js +73 -0
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.VoicevoxError = void 0;
|
|
4
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
6
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
7
|
const zod_1 = require("zod");
|
|
7
|
-
const
|
|
8
|
+
const voicevox_1 = require("./voicevox");
|
|
9
|
+
const types_1 = require("./voicevox/types");
|
|
10
|
+
Object.defineProperty(exports, "VoicevoxError", { enumerable: true, get: function () { return types_1.VoicevoxError; } });
|
|
8
11
|
const server = new mcp_js_1.McpServer({
|
|
9
12
|
name: "MCP TTS Voicevox",
|
|
10
|
-
version: "0.0.
|
|
13
|
+
version: "0.0.6",
|
|
11
14
|
description: "A Voicevox server that converts text to speech for playback and saving.",
|
|
12
15
|
});
|
|
13
16
|
// VoicevoxClientを一度だけインスタンス化
|
|
14
|
-
const voicevoxClient = new
|
|
17
|
+
const voicevoxClient = new voicevox_1.VoicevoxClient({
|
|
15
18
|
url: process.env.VOICEVOX_URL ?? "http://localhost:50021",
|
|
16
|
-
defaultSpeaker: 1,
|
|
19
|
+
defaultSpeaker: Number(process.env.VOICEVOX_DEFAULT_SPEAKER || "1"),
|
|
20
|
+
defaultSpeedScale: Number(process.env.VOICEVOX_DEFAULT_SPEED_SCALE || "1.0"),
|
|
17
21
|
});
|
|
18
22
|
// 共通のエラーハンドリング関数
|
|
19
23
|
const handleError = (error) => {
|
|
@@ -25,14 +29,38 @@ const handleError = (error) => {
|
|
|
25
29
|
};
|
|
26
30
|
// テキストを音声に変換して再生
|
|
27
31
|
server.tool("speak", "Convert text to speech and play it", {
|
|
28
|
-
text: zod_1.z
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
text: zod_1.z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("Text to be spoken (if both query and text are provided, query takes precedence)"),
|
|
35
|
+
speaker: zod_1.z
|
|
36
|
+
.number()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Speaker ID (optional, used if text is provided)"),
|
|
39
|
+
query: zod_1.z.string().optional().describe("Voice synthesis query"),
|
|
40
|
+
speedScale: zod_1.z
|
|
41
|
+
.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Playback speed (optional, default is from environment variable)"),
|
|
44
|
+
}, async ({ text, speaker, query, speedScale }) => {
|
|
31
45
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
if (query) {
|
|
47
|
+
// クエリが提供されている場合はそれを使用
|
|
48
|
+
const audioQuery = JSON.parse(query);
|
|
49
|
+
if (speedScale !== undefined) {
|
|
50
|
+
audioQuery.speedScale = speedScale;
|
|
51
|
+
}
|
|
52
|
+
const result = await voicevoxClient.enqueueAudioGeneration(audioQuery, speaker);
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: result }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// テキストからの通常の発話
|
|
59
|
+
const result = await voicevoxClient.speak(text, speaker, speedScale);
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: result }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
36
64
|
}
|
|
37
65
|
catch (error) {
|
|
38
66
|
return handleError(error);
|
|
@@ -42,9 +70,13 @@ server.tool("speak", "Convert text to speech and play it", {
|
|
|
42
70
|
server.tool("generate_query", "Generate a query for voice synthesis", {
|
|
43
71
|
text: zod_1.z.string().describe("Text for voice synthesis"),
|
|
44
72
|
speaker: zod_1.z.number().optional().describe("Speaker ID (optional)"),
|
|
45
|
-
|
|
73
|
+
speedScale: zod_1.z
|
|
74
|
+
.number()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Playback speed (optional, default is from environment variable)"),
|
|
77
|
+
}, async ({ text, speaker, speedScale }) => {
|
|
46
78
|
try {
|
|
47
|
-
const query = await voicevoxClient.generateQuery(text, speaker);
|
|
79
|
+
const query = await voicevoxClient.generateQuery(text, speaker, speedScale);
|
|
48
80
|
return {
|
|
49
81
|
content: [{ type: "text", text: JSON.stringify(query) }],
|
|
50
82
|
};
|
|
@@ -53,30 +85,48 @@ server.tool("generate_query", "Generate a query for voice synthesis", {
|
|
|
53
85
|
return handleError(error);
|
|
54
86
|
}
|
|
55
87
|
});
|
|
56
|
-
// 音声ファイル生成ツール
|
|
88
|
+
// 音声ファイル生成ツール
|
|
57
89
|
server.tool("synthesize_file", "Generate an audio file and return its absolute path", {
|
|
58
90
|
text: zod_1.z
|
|
59
91
|
.string()
|
|
60
92
|
.optional()
|
|
61
93
|
.describe("Text for voice synthesis (if both query and text are provided, query takes precedence)"),
|
|
62
|
-
query: zod_1.z.
|
|
63
|
-
output: zod_1.z.string().describe("Output path for the audio file"),
|
|
94
|
+
query: zod_1.z.string().optional().describe("Voice synthesis query"),
|
|
64
95
|
speaker: zod_1.z.number().optional().describe("Speaker ID (optional)"),
|
|
65
|
-
|
|
96
|
+
output: zod_1.z.string().describe("Output path for the audio file"),
|
|
97
|
+
speedScale: zod_1.z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Playback speed (optional, default is from environment variable)"),
|
|
101
|
+
}, async ({ text, query, speaker, output, speedScale }) => {
|
|
66
102
|
try {
|
|
67
|
-
if (
|
|
103
|
+
if (query) {
|
|
104
|
+
// クエリが提供されている場合はそれを使用
|
|
105
|
+
const audioQuery = JSON.parse(query);
|
|
106
|
+
if (speedScale !== undefined) {
|
|
107
|
+
audioQuery.speedScale = speedScale;
|
|
108
|
+
}
|
|
109
|
+
const filePath = await voicevoxClient.generateAudioFile(audioQuery, output, speaker);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: filePath }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else if (text) {
|
|
115
|
+
// テキストから音声ファイルを生成
|
|
116
|
+
const filePath = await voicevoxClient.generateAudioFile(text, output, speaker, speedScale);
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: filePath }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
68
122
|
throw new Error("queryパラメータとtextパラメータのどちらかを指定してください");
|
|
69
123
|
}
|
|
70
|
-
const filePath = await voicevoxClient.generateAudioFile(query ?? text, output, speaker);
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: "text", text: filePath }],
|
|
73
|
-
};
|
|
74
124
|
}
|
|
75
125
|
catch (error) {
|
|
76
126
|
return handleError(error);
|
|
77
127
|
}
|
|
78
128
|
});
|
|
79
129
|
server.connect(new stdio_js_1.StdioServerTransport()).catch((error) => {
|
|
80
|
-
console.error(error);
|
|
130
|
+
console.error("Error connecting to MCP transport:", error);
|
|
81
131
|
process.exit(1);
|
|
82
132
|
});
|
package/dist/test.js
CHANGED
|
@@ -37,159 +37,310 @@ const voicevox_1 = require("./voicevox");
|
|
|
37
37
|
const path_1 = require("path");
|
|
38
38
|
const os_1 = require("os");
|
|
39
39
|
const fs = __importStar(require("fs/promises"));
|
|
40
|
-
|
|
40
|
+
const soundPlay = require("sound-play");
|
|
41
|
+
// ----- ユーティリティ関数 -----
|
|
42
|
+
/**
|
|
43
|
+
* 音声ファイルを再生するユーティリティ関数
|
|
44
|
+
* @param filePath 再生する音声ファイルのパス
|
|
45
|
+
* @param description 再生内容の説明
|
|
46
|
+
*/
|
|
47
|
+
async function playAudioFile(filePath, description = "") {
|
|
48
|
+
try {
|
|
49
|
+
const displayText = description ? `${description} (${filePath})` : filePath;
|
|
50
|
+
console.log(`🔊 音声ファイル「${displayText}」の再生を開始します...`);
|
|
51
|
+
await soundPlay.play(filePath);
|
|
52
|
+
console.log(`✅ 音声ファイル「${displayText}」の再生が完了しました`);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`❌ 音声再生中にエラーが発生しました: ${error}`);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* ヘッダーを出力する関数
|
|
61
|
+
* @param title セクションタイトル
|
|
62
|
+
*/
|
|
63
|
+
function printHeader(title) {
|
|
64
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
65
|
+
console.log(`📌 ${title}`);
|
|
66
|
+
console.log(`${"=".repeat(80)}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* サブセクションのヘッダーを出力する関数
|
|
70
|
+
* @param title セクションタイトル
|
|
71
|
+
*/
|
|
72
|
+
function printSubHeader(title) {
|
|
73
|
+
console.log(`\n${"- ".repeat(40)}`);
|
|
74
|
+
console.log(`🔹 ${title}`);
|
|
75
|
+
console.log(`${"- ".repeat(40)}`);
|
|
76
|
+
}
|
|
77
|
+
// ----- クライアントテスト関数 -----
|
|
78
|
+
/**
|
|
79
|
+
* VoicevoxClientの基本機能をテストする
|
|
80
|
+
*/
|
|
81
|
+
async function testTextToSpeech(client, speaker) {
|
|
82
|
+
printSubHeader("テキストから音声再生のテスト");
|
|
83
|
+
const testText = "これはテストです。VOICEVOXの機能を検証します。";
|
|
84
|
+
// 1. speak テスト - テキストからの音声再生
|
|
85
|
+
console.log("➡️ テキストから直接音声再生");
|
|
86
|
+
const speakResult = await client.speak(testText, speaker);
|
|
87
|
+
console.log("✅ 結果:", speakResult);
|
|
88
|
+
// 2. generateQuery テスト - テキストから音声合成用クエリを生成
|
|
89
|
+
console.log("\n➡️ テキストから音声合成用クエリ生成");
|
|
90
|
+
const query = await client.generateQuery(testText, speaker);
|
|
91
|
+
console.log("✅ クエリ生成結果 (一部):", JSON.stringify(query).substring(0, 100) + "...");
|
|
92
|
+
return query;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 音声ファイル生成機能をテストする
|
|
96
|
+
*/
|
|
97
|
+
async function testAudioFileGeneration(client, query, speaker) {
|
|
98
|
+
printSubHeader("音声ファイル生成のテスト");
|
|
99
|
+
// 1. クエリから音声ファイルを生成
|
|
100
|
+
console.log("➡️ クエリから音声ファイル生成");
|
|
101
|
+
const outputPath = (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-${Date.now()}.wav`);
|
|
102
|
+
const filePath = await client.generateAudioFile(query, outputPath, speaker);
|
|
103
|
+
console.log(`✅ 音声ファイル生成: ${filePath}`);
|
|
104
|
+
// ファイルが存在するか確認
|
|
105
|
+
const fileExists = await fs
|
|
106
|
+
.stat(filePath)
|
|
107
|
+
.then(() => true)
|
|
108
|
+
.catch(() => false);
|
|
109
|
+
console.log(`📂 ファイルの存在確認: ${fileExists ? "✅ 存在します" : "❌ 存在しません"}`);
|
|
110
|
+
// 生成した音声ファイルを再生
|
|
111
|
+
if (fileExists) {
|
|
112
|
+
await playAudioFile(filePath, "クエリから生成した音声");
|
|
113
|
+
}
|
|
114
|
+
// 2. テキストから直接音声ファイルを生成
|
|
115
|
+
console.log("\n➡️ テキストから直接音声ファイル生成");
|
|
116
|
+
const directFilePath = await client.generateAudioFile("直接ファイルに変換するテスト。", undefined, speaker);
|
|
117
|
+
console.log(`✅ 直接音声ファイル生成: ${directFilePath}`);
|
|
118
|
+
await playAudioFile(directFilePath, "テキストから直接生成した音声");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 再生速度変更機能をテストする
|
|
122
|
+
*/
|
|
123
|
+
async function testSpeedScale(client, speaker) {
|
|
124
|
+
printSubHeader("再生速度の変更テスト");
|
|
125
|
+
// 1. 速い再生速度のテスト
|
|
126
|
+
console.log("➡️ 再生速度を1.5倍に設定したテスト");
|
|
127
|
+
const speedTestFilePath = await client.generateAudioFile("これは再生速度を1.5倍に設定したテストです。", (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-speed-${Date.now()}.wav`), speaker, 1.5 // 速度を1.5倍に設定
|
|
128
|
+
);
|
|
129
|
+
console.log(`✅ 再生速度1.5倍: ${speedTestFilePath}`);
|
|
130
|
+
await playAudioFile(speedTestFilePath, "速度を1.5倍に設定した音声");
|
|
131
|
+
// 遅い再生速度のテストはタイムアウトを起こしやすいため、条件付きでスキップ
|
|
132
|
+
const doSlowTest = process.env.TEST_SLOW_SPEED === "true";
|
|
133
|
+
if (doSlowTest) {
|
|
134
|
+
// 2. 遅い再生速度のテスト
|
|
135
|
+
console.log("\n➡️ 再生速度を0.8倍に設定したテスト");
|
|
136
|
+
try {
|
|
137
|
+
const slowSpeedTestFilePath = await client.generateAudioFile("これは再生速度を0.8倍に設定したテストです。ゆっくり話します。", (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-slow-${Date.now()}.wav`), speaker, 0.8 // 速度を0.8倍に設定
|
|
138
|
+
);
|
|
139
|
+
console.log(`✅ 再生速度0.8倍: ${slowSpeedTestFilePath}`);
|
|
140
|
+
await playAudioFile(slowSpeedTestFilePath, "速度を0.8倍に設定した音声");
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.warn(`⚠️ 遅い再生速度のテストはスキップされました: ${error}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log("\n⏭️ 再生速度0.8倍のテストはスキップします (タイムアウト防止のため)");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 音声生成キュー機能をテストする
|
|
152
|
+
*/
|
|
153
|
+
async function testAudioQueue(client, query, speaker) {
|
|
154
|
+
printSubHeader("音声生成キューテスト");
|
|
155
|
+
// クエリを使って音声生成キューに追加
|
|
156
|
+
console.log("➡️ クエリを使って音声生成キューへの追加");
|
|
157
|
+
const enqueueResult = await client.enqueueAudioGeneration(query, speaker);
|
|
158
|
+
console.log(`✅ キュー追加結果:`, enqueueResult);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* VoicevoxClientの基本機能をテストする統合関数
|
|
162
|
+
*/
|
|
41
163
|
async function testClient() {
|
|
42
164
|
try {
|
|
43
|
-
|
|
165
|
+
printHeader("VoicevoxClient直接テスト");
|
|
44
166
|
const client = new voicevox_1.VoicevoxClient({
|
|
45
167
|
url: "http://localhost:50021",
|
|
46
168
|
defaultSpeaker: 1,
|
|
169
|
+
defaultSpeedScale: 1.0,
|
|
47
170
|
});
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log("クエリ生成結果 (一部):", JSON.stringify(query).substring(0, 100) + "...");
|
|
59
|
-
// 3. synthesizeToFile テスト - クエリから音声ファイルを生成
|
|
60
|
-
console.log("\n----- クエリから音声ファイル生成のテスト -----");
|
|
61
|
-
const outputPath = (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-test-${Date.now()}.wav`);
|
|
62
|
-
const filePath = await client.generateAudioFile(query, outputPath, speaker);
|
|
63
|
-
console.log(`音声ファイル生成結果: ${filePath}`);
|
|
64
|
-
// ファイルが存在するか確認
|
|
65
|
-
const fileExists = await fs
|
|
66
|
-
.stat(filePath)
|
|
67
|
-
.then(() => true)
|
|
68
|
-
.catch(() => false);
|
|
69
|
-
console.log(`ファイルが存在するか: ${fileExists}`);
|
|
70
|
-
// 4. generateAudioFile テスト - テキストから直接音声ファイルを生成
|
|
71
|
-
console.log("\n----- テキストから直接音声ファイル生成のテスト -----");
|
|
72
|
-
const directFilePath = await client.generateAudioFile("直接ファイルに変換するテスト。", (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-direct-${Date.now()}.wav`), speaker);
|
|
73
|
-
console.log(`直接音声ファイル生成結果: ${directFilePath}`);
|
|
74
|
-
// 5. enqueueAudioGeneration テスト - クエリを使って音声生成キューに追加
|
|
75
|
-
console.log("\n----- クエリを使って音声生成キューへの追加テスト -----");
|
|
76
|
-
const enqueueResult = await client.enqueueAudioGeneration(query, speaker);
|
|
77
|
-
console.log(enqueueResult);
|
|
78
|
-
// 待機してキューの処理が完了するのを待つ
|
|
79
|
-
console.log("\n音声再生を待機しています...");
|
|
80
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
171
|
+
// テスト用の話者
|
|
172
|
+
const speaker = 5; // 四国めたん (ノーマル)
|
|
173
|
+
// 1. 基本的なテキスト読み上げとクエリ生成
|
|
174
|
+
const query = await testTextToSpeech(client, speaker);
|
|
175
|
+
// 2. 音声ファイル生成
|
|
176
|
+
await testAudioFileGeneration(client, query, speaker);
|
|
177
|
+
// 3. 再生速度変更
|
|
178
|
+
await testSpeedScale(client, speaker);
|
|
179
|
+
// 4. 音声生成キュー
|
|
180
|
+
await testAudioQueue(client, query, speaker);
|
|
81
181
|
return query; // 後のテストで使用するためにクエリを返す
|
|
82
182
|
}
|
|
83
183
|
catch (error) {
|
|
84
|
-
console.error("クライアントテスト中にエラーが発生しました:", error);
|
|
184
|
+
console.error("❌ クライアントテスト中にエラーが発生しました:", error);
|
|
85
185
|
throw error;
|
|
86
186
|
}
|
|
87
187
|
}
|
|
88
|
-
// MCP
|
|
188
|
+
// ----- MCPツール関数 -----
|
|
189
|
+
/**
|
|
190
|
+
* speak MCPツールをテストする
|
|
191
|
+
*/
|
|
192
|
+
async function testSpeakTool(client) {
|
|
193
|
+
printSubHeader("speak ツールのテスト");
|
|
194
|
+
// speak ツールハンドラ
|
|
195
|
+
const speakHandler = async (args) => {
|
|
196
|
+
try {
|
|
197
|
+
const { text, speaker, speedScale } = args;
|
|
198
|
+
console.log(`➡️ テキスト「${text}」を話者${speaker}、速度${speedScale || 1.0}で発話`);
|
|
199
|
+
const result = await client.speak(text, speaker, speedScale);
|
|
200
|
+
console.log("✅ speak 結果:", result);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: result }],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
|
+
console.error("❌ エラーが発生しました:", error);
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
// 通常のテスト
|
|
214
|
+
await speakHandler({ text: "MCPツールからのテスト発話です。", speaker: 1 });
|
|
215
|
+
// 速度を変更したテスト
|
|
216
|
+
await speakHandler({
|
|
217
|
+
text: "MCPツールから速度を1.3倍に設定したテスト発話です。",
|
|
218
|
+
speaker: 1,
|
|
219
|
+
speedScale: 1.3,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* generate_query MCPツールをテストする
|
|
224
|
+
*/
|
|
225
|
+
async function testGenerateQueryTool(client) {
|
|
226
|
+
printSubHeader("generate_query ツールのテスト");
|
|
227
|
+
// generate_query ツールハンドラ
|
|
228
|
+
const generateQueryHandler = async (args) => {
|
|
229
|
+
try {
|
|
230
|
+
const { text, speaker, speedScale } = args;
|
|
231
|
+
console.log(`➡️ テキスト「${text}」を話者${speaker}、速度${speedScale || 1.0}でクエリ生成`);
|
|
232
|
+
const generatedQuery = await client.generateQuery(text, speaker, speedScale);
|
|
233
|
+
const queryJson = JSON.stringify(generatedQuery);
|
|
234
|
+
console.log("✅ クエリ生成結果 (一部):", queryJson.substring(0, 100) + "...");
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: queryJson }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
241
|
+
console.error("❌ エラーが発生しました:", error);
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
// 実行
|
|
248
|
+
const queryResponse = await generateQueryHandler({
|
|
249
|
+
text: "MCPツールからのクエリ生成テスト。",
|
|
250
|
+
speaker: 1,
|
|
251
|
+
speedScale: 1.2,
|
|
252
|
+
});
|
|
253
|
+
// テキストからJSONに変換
|
|
254
|
+
return JSON.parse(queryResponse.content[0].text);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* synthesize_file MCPツールをテストする
|
|
258
|
+
*/
|
|
259
|
+
async function testSynthesizeFileTool(client, query) {
|
|
260
|
+
printSubHeader("synthesize_file ツールのテスト");
|
|
261
|
+
// synthesize_file ツールハンドラ
|
|
262
|
+
const synthesizeFileHandler = async (args) => {
|
|
263
|
+
try {
|
|
264
|
+
const { query: testQuery, output, speaker, speedScale } = args;
|
|
265
|
+
console.log(`➡️ クエリから音声ファイルを生成: 出力パス=${output}`);
|
|
266
|
+
const filePath = await client.generateAudioFile(testQuery, output, speaker, speedScale);
|
|
267
|
+
console.log("✅ ファイル生成結果:", filePath);
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: filePath }],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
274
|
+
console.error("❌ エラーが発生しました:", error);
|
|
275
|
+
return {
|
|
276
|
+
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
// 実行
|
|
281
|
+
const testOutputPath = "";
|
|
282
|
+
const fileResponse = await synthesizeFileHandler({
|
|
283
|
+
query: query,
|
|
284
|
+
output: testOutputPath,
|
|
285
|
+
speaker: 1,
|
|
286
|
+
speedScale: 0.9,
|
|
287
|
+
});
|
|
288
|
+
return fileResponse.content[0].text;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* MCPツールをテストする統合関数
|
|
292
|
+
*/
|
|
89
293
|
async function testMcpTools(query) {
|
|
90
294
|
try {
|
|
91
|
-
|
|
92
|
-
// VoicevoxClient
|
|
93
|
-
const
|
|
295
|
+
printHeader("MCPツールテスト");
|
|
296
|
+
// VoicevoxClientを初期化
|
|
297
|
+
const client = new voicevox_1.VoicevoxClient({
|
|
94
298
|
url: process.env.VOICEVOX_URL ?? "http://localhost:50021",
|
|
95
299
|
defaultSpeaker: 1,
|
|
300
|
+
defaultSpeedScale: Number(process.env.VOICEVOX_DEFAULT_SPEED_SCALE || "1.0"),
|
|
96
301
|
});
|
|
97
|
-
// 1. speak
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
catch (error) {
|
|
109
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
110
|
-
console.error("エラーが発生しました:", error);
|
|
111
|
-
return {
|
|
112
|
-
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
// 実行
|
|
117
|
-
await speakHandler({ text: "MCPツールからのテスト発話です。", speaker: 1 });
|
|
118
|
-
// 2. generate_query ツールテスト
|
|
119
|
-
console.log("\n----- generate_query ツールのテスト -----");
|
|
120
|
-
const generateQueryHandler = async (args) => {
|
|
121
|
-
try {
|
|
122
|
-
const { text, speaker } = args;
|
|
123
|
-
const generatedQuery = await voicevoxClient.generateQuery(text, speaker);
|
|
124
|
-
const queryJson = JSON.stringify(generatedQuery);
|
|
125
|
-
console.log("generate_query 結果 (一部):", queryJson.substring(0, 100) + "...");
|
|
126
|
-
return {
|
|
127
|
-
content: [{ type: "text", text: queryJson }],
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
132
|
-
console.error("エラーが発生しました:", error);
|
|
133
|
-
return {
|
|
134
|
-
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
// 実行
|
|
139
|
-
const queryResponse = await generateQueryHandler({
|
|
140
|
-
text: "MCPツールからのクエリ生成テスト。",
|
|
141
|
-
speaker: 1,
|
|
142
|
-
});
|
|
143
|
-
// テキストからJSONに変換
|
|
144
|
-
const generatedQuery = JSON.parse(queryResponse.content[0].text);
|
|
145
|
-
// 3. synthesize_file ツールテスト
|
|
146
|
-
console.log("\n----- synthesize_file ツールのテスト -----");
|
|
147
|
-
const synthesizeFileHandler = async (args) => {
|
|
148
|
-
try {
|
|
149
|
-
const { query: testQuery, output, speaker } = args;
|
|
150
|
-
const filePath = await voicevoxClient.generateAudioFile(testQuery, output, speaker);
|
|
151
|
-
console.log("synthesize_file 結果:", filePath);
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: "text", text: filePath }],
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
158
|
-
console.error("エラーが発生しました:", error);
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: "text", text: `エラー: ${errorMessage}` }],
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
// 実行
|
|
165
|
-
const testOutputPath = (0, path_1.join)((0, os_1.tmpdir)(), `voicevox-mcp-final-test-${Date.now()}.wav`);
|
|
166
|
-
await synthesizeFileHandler({
|
|
167
|
-
query: generatedQuery,
|
|
168
|
-
output: testOutputPath,
|
|
169
|
-
speaker: 1,
|
|
170
|
-
});
|
|
302
|
+
// 1. speak ツールのテスト
|
|
303
|
+
await testSpeakTool(client);
|
|
304
|
+
// 2. generate_query ツールのテスト
|
|
305
|
+
const generatedQuery = await testGenerateQueryTool(client);
|
|
306
|
+
// 3. synthesize_file ツールのテスト
|
|
307
|
+
const filePath = await testSynthesizeFileTool(client, generatedQuery);
|
|
308
|
+
// 生成したファイルを再生
|
|
309
|
+
printSubHeader("生成したファイルの再生テスト");
|
|
310
|
+
console.log(`➡️ ファイル ${filePath} を再生します...`);
|
|
311
|
+
await playAudioFile(filePath, "MCPツールで生成した音声ファイル");
|
|
312
|
+
console.log("✅ ファイルの再生が完了しました");
|
|
171
313
|
// 待機してキューの処理が完了するのを待つ
|
|
172
|
-
console.log("\
|
|
314
|
+
console.log("\n⏳ MCP音声再生を待機しています...");
|
|
173
315
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
174
316
|
}
|
|
175
317
|
catch (error) {
|
|
176
|
-
console.error("MCPツールテスト中にエラーが発生しました:", error);
|
|
318
|
+
console.error("❌ MCPツールテスト中にエラーが発生しました:", error);
|
|
177
319
|
throw error;
|
|
178
320
|
}
|
|
179
321
|
}
|
|
180
|
-
// メイン実行関数
|
|
322
|
+
// ----- メイン実行関数 -----
|
|
323
|
+
/**
|
|
324
|
+
* メイン実行関数
|
|
325
|
+
*/
|
|
181
326
|
async function main() {
|
|
182
327
|
try {
|
|
183
|
-
console.log("VOICEVOXテストを開始します...");
|
|
328
|
+
console.log("🚀 VOICEVOXテストを開始します...");
|
|
184
329
|
// クライアントテスト実行
|
|
185
330
|
const query = await testClient();
|
|
186
331
|
// MCPツールテスト実行
|
|
187
332
|
await testMcpTools(query);
|
|
188
|
-
console.log("\nすべてのテストが完了しました!");
|
|
333
|
+
console.log("\n🎉 すべてのテストが完了しました!");
|
|
189
334
|
}
|
|
190
335
|
catch (error) {
|
|
191
|
-
console.error("テスト中にエラーが発生しました:", error);
|
|
336
|
+
console.error("❌ テスト中にエラーが発生しました:", error);
|
|
192
337
|
process.exit(1);
|
|
193
338
|
}
|
|
339
|
+
finally {
|
|
340
|
+
// 強制的にプロセスを終了
|
|
341
|
+
console.log("👋 プロセスを終了します...");
|
|
342
|
+
setTimeout(() => process.exit(0), 1000);
|
|
343
|
+
}
|
|
194
344
|
}
|
|
345
|
+
// プログラム実行
|
|
195
346
|
main();
|
|
@@ -0,0 +1,97 @@
|
|
|
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.VoicevoxApi = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
const error_1 = require("./error");
|
|
10
|
+
class VoicevoxApi {
|
|
11
|
+
constructor(baseUrl) {
|
|
12
|
+
this.baseUrl = this.normalizeUrl(baseUrl);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* テキストから音声合成用クエリを生成
|
|
16
|
+
*/
|
|
17
|
+
async generateQuery(text, speaker = 1) {
|
|
18
|
+
try {
|
|
19
|
+
const endpoint = `/audio_query?text=${encodeURIComponent(text)}&speaker=${speaker}`;
|
|
20
|
+
const query = await this.makeRequest("post", endpoint, null, {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
});
|
|
23
|
+
return query;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw (0, error_1.handleError)("音声クエリ生成中にエラーが発生しました", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 音声合成用クエリから音声ファイルを生成
|
|
31
|
+
*/
|
|
32
|
+
async synthesize(query, speaker = 1) {
|
|
33
|
+
try {
|
|
34
|
+
return await this.makeRequest("post", `/synthesis?speaker=${speaker}`, query, {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Accept: "audio/wav",
|
|
37
|
+
}, "arraybuffer");
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw (0, error_1.handleError)("音声合成中にエラーが発生しました", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* プリセットを使用してテキストから音声合成用クエリを生成
|
|
45
|
+
*/
|
|
46
|
+
async generateQueryFromPreset(text, presetId, coreVersion) {
|
|
47
|
+
try {
|
|
48
|
+
let endpoint = `/audio_query_from_preset?text=${encodeURIComponent(text)}&preset_id=${presetId}`;
|
|
49
|
+
if (coreVersion) {
|
|
50
|
+
endpoint += `&core_version=${encodeURIComponent(coreVersion)}`;
|
|
51
|
+
}
|
|
52
|
+
const query = await this.makeRequest("post", endpoint, null, {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
});
|
|
55
|
+
return query;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw (0, error_1.handleError)("プリセットを使用した音声クエリ生成中にエラーが発生しました", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* APIリクエストを実行
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
async makeRequest(method, endpoint, data = null, headers = {}, responseType = "json") {
|
|
66
|
+
try {
|
|
67
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
68
|
+
const config = {
|
|
69
|
+
method,
|
|
70
|
+
url,
|
|
71
|
+
data,
|
|
72
|
+
headers,
|
|
73
|
+
responseType,
|
|
74
|
+
timeout: 30000,
|
|
75
|
+
};
|
|
76
|
+
const response = await (0, axios_1.default)(config);
|
|
77
|
+
if (response.status !== 200) {
|
|
78
|
+
throw new types_1.VoicevoxError(`APIリクエストに失敗しました: ${response.status}`, response.status);
|
|
79
|
+
}
|
|
80
|
+
return response.data;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
84
|
+
throw new types_1.VoicevoxError(`APIリクエストに失敗しました: ${error.message}`, error.response?.status || 0);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* URLの正規化
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
normalizeUrl(url) {
|
|
94
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.VoicevoxApi = VoicevoxApi;
|