@coeiro-operator/mcp 1.4.5 → 1.4.7
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/server.js +29 -990
- package/dist/server.js.map +1 -1
- package/dist/tools/debug.d.ts +10 -0
- package/dist/tools/debug.d.ts.map +1 -0
- package/dist/tools/debug.js +138 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/dictionary.d.ts +11 -0
- package/dist/tools/dictionary.d.ts.map +1 -0
- package/dist/tools/dictionary.js +69 -0
- package/dist/tools/dictionary.js.map +1 -0
- package/dist/tools/operator.d.ts +31 -0
- package/dist/tools/operator.d.ts.map +1 -0
- package/dist/tools/operator.js +204 -0
- package/dist/tools/operator.js.map +1 -0
- package/dist/tools/playback.d.ts +23 -0
- package/dist/tools/playback.d.ts.map +1 -0
- package/dist/tools/playback.js +219 -0
- package/dist/tools/playback.js.map +1 -0
- package/dist/tools/speech.d.ts +13 -0
- package/dist/tools/speech.d.ts.map +1 -0
- package/dist/tools/speech.js +313 -0
- package/dist/tools/speech.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +141 -0
- package/dist/utils.js.map +1 -0
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node --no-deprecation
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { z } from 'zod';
|
|
5
4
|
import * as path from 'path';
|
|
6
5
|
import { Command } from 'commander';
|
|
7
6
|
import { SayCoeiroink } from '@coeiro-operator/audio';
|
|
8
7
|
import { ConfigManager, getConfigDir, OperatorManager, CharacterInfoService, DictionaryService, TerminalBackground } from '@coeiro-operator/core';
|
|
9
8
|
import { logger, LoggerPresets } from '@coeiro-operator/common';
|
|
9
|
+
// Tool registration functions
|
|
10
|
+
import { registerOperatorAssignTool, registerOperatorReleaseTool, registerOperatorStatusTool, registerOperatorAvailableTool, registerOperatorStylesTool, } from './tools/operator.js';
|
|
11
|
+
import { registerSayTool,
|
|
12
|
+
// registerParallelGenerationControlTool,
|
|
13
|
+
} from './tools/speech.js';
|
|
14
|
+
import { registerQueueStatusTool, registerQueueClearTool, registerPlaybackStopTool, registerWaitForTaskCompletionTool, } from './tools/playback.js';
|
|
15
|
+
import { registerDictionaryRegisterTool } from './tools/dictionary.js';
|
|
16
|
+
import { registerDebugLogsTool } from './tools/debug.js';
|
|
10
17
|
// Commanderの設定と引数解析
|
|
11
18
|
const program = new Command();
|
|
12
19
|
program
|
|
@@ -100,995 +107,27 @@ catch (error) {
|
|
|
100
107
|
throw fallbackError;
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
id: styleId,
|
|
126
|
-
name: styleConfig.styleName,
|
|
127
|
-
personality: styleConfig.personality || character.personality,
|
|
128
|
-
speakingStyle: styleConfig.speakingStyle || character.speakingStyle,
|
|
129
|
-
morasPerSecond: styleConfig.morasPerSecond,
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
function formatAssignmentResult(assignResult, availableStyles) {
|
|
134
|
-
let resultText = `${assignResult.characterName} (${assignResult.characterId}) をアサインしました。\n\n`;
|
|
135
|
-
if (assignResult.currentStyle) {
|
|
136
|
-
resultText += `📍 現在のスタイル: ${assignResult.currentStyle.styleName}\n`;
|
|
137
|
-
resultText += ` 性格: ${assignResult.currentStyle.personality}\n`;
|
|
138
|
-
resultText += ` 話し方: ${assignResult.currentStyle.speakingStyle}\n`;
|
|
139
|
-
// 現在のスタイルの話速を取得
|
|
140
|
-
const currentStyleInfo = availableStyles.find(s => s.id === assignResult.currentStyle?.styleId);
|
|
141
|
-
if (currentStyleInfo?.morasPerSecond) {
|
|
142
|
-
resultText += ` 基準話速: ${currentStyleInfo.morasPerSecond} モーラ/秒\n`;
|
|
143
|
-
}
|
|
144
|
-
resultText += '\n';
|
|
145
|
-
}
|
|
146
|
-
if (availableStyles.length > 1) {
|
|
147
|
-
resultText += `🎭 利用可能なスタイル(切り替え可能):\n`;
|
|
148
|
-
availableStyles.forEach(style => {
|
|
149
|
-
const isCurrent = style.id === assignResult.currentStyle?.styleId;
|
|
150
|
-
const marker = isCurrent ? '→ ' : ' ';
|
|
151
|
-
resultText += `${marker}${style.id}: ${style.name}\n`;
|
|
152
|
-
resultText += ` 性格: ${style.personality}\n`;
|
|
153
|
-
resultText += ` 話し方: ${style.speakingStyle}\n`;
|
|
154
|
-
if (style.morasPerSecond) {
|
|
155
|
-
resultText += ` 基準話速: ${style.morasPerSecond} モーラ/秒\n`;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
resultText += `ℹ️ このキャラクターは1つのスタイルのみ利用可能です。\n`;
|
|
161
|
-
}
|
|
162
|
-
if (assignResult.greeting) {
|
|
163
|
-
resultText += `\n💬 "${assignResult.greeting}"\n`;
|
|
164
|
-
}
|
|
165
|
-
return resultText;
|
|
166
|
-
}
|
|
167
|
-
// Utility functions for operator styles
|
|
168
|
-
async function getTargetCharacter(manager, characterInfoService, characterId) {
|
|
169
|
-
if (characterId) {
|
|
170
|
-
try {
|
|
171
|
-
const character = await characterInfoService.getCharacterInfo(characterId);
|
|
172
|
-
if (!character) {
|
|
173
|
-
throw new Error(`キャラクター '${characterId}' が見つかりません`);
|
|
174
|
-
}
|
|
175
|
-
return { character, characterId };
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
throw new Error(`キャラクター '${characterId}' が見つかりません`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
const currentOperator = await manager.showCurrentOperator();
|
|
183
|
-
if (!currentOperator.characterId) {
|
|
184
|
-
throw new Error('現在オペレータが割り当てられていません。まず operator_assign を実行してください。');
|
|
185
|
-
}
|
|
186
|
-
const character = await characterInfoService.getCharacterInfo(currentOperator.characterId);
|
|
187
|
-
if (!character) {
|
|
188
|
-
throw new Error(`現在のオペレータ '${currentOperator.characterId}' のキャラクター情報が見つかりません`);
|
|
189
|
-
}
|
|
190
|
-
return { character, characterId: currentOperator.characterId };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function formatStylesResult(character, availableStyles) {
|
|
194
|
-
let resultText = `🎭 ${character.speakerName || character.characterId} のスタイル情報\n\n`;
|
|
195
|
-
resultText += `📋 基本情報:\n`;
|
|
196
|
-
resultText += ` 性格: ${character.personality}\n`;
|
|
197
|
-
resultText += ` 話し方: ${character.speakingStyle}\n`;
|
|
198
|
-
// defaultStyleIdからスタイル名を取得
|
|
199
|
-
const defaultStyleInfo = character.styles?.[character.defaultStyleId];
|
|
200
|
-
const defaultStyleName = defaultStyleInfo?.styleName || `ID:${character.defaultStyleId}`;
|
|
201
|
-
resultText += ` デフォルトスタイル: ${defaultStyleName}\n\n`;
|
|
202
|
-
if (availableStyles.length > 0) {
|
|
203
|
-
resultText += `🎨 利用可能なスタイル (${availableStyles.length}種類):\n`;
|
|
204
|
-
availableStyles.forEach(style => {
|
|
205
|
-
const isDefault = style.name === defaultStyleName;
|
|
206
|
-
const marker = isDefault ? '★ ' : ' ';
|
|
207
|
-
resultText += `${marker}${style.id}: ${style.name}\n`;
|
|
208
|
-
resultText += ` 性格: ${style.personality}\n`;
|
|
209
|
-
resultText += ` 話し方: ${style.speakingStyle}\n`;
|
|
210
|
-
if (style.morasPerSecond) {
|
|
211
|
-
resultText += ` 基準話速: ${style.morasPerSecond} モーラ/秒\n`;
|
|
212
|
-
}
|
|
213
|
-
if (isDefault) {
|
|
214
|
-
resultText += ` (デフォルトスタイル)\n`;
|
|
215
|
-
}
|
|
216
|
-
resultText += `\n`;
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
resultText += `⚠️ 利用可能なスタイルがありません。\n`;
|
|
221
|
-
}
|
|
222
|
-
return resultText;
|
|
223
|
-
}
|
|
224
|
-
// operator-manager操作ツール
|
|
225
|
-
server.registerTool('operator_assign', {
|
|
226
|
-
description: 'オペレータを割り当てます。通常は引数なしで実行し、ランダムに選択されます。特定のオペレータが必要な場合のみ名前を指定してください。スタイル切り替えはsayツールのstyleパラメータで日本語名を指定します。',
|
|
227
|
-
inputSchema: {
|
|
228
|
-
operator: z
|
|
229
|
-
.string()
|
|
230
|
-
.optional()
|
|
231
|
-
.describe("オペレータ名(省略推奨。特定のオペレータが必要な場合のみ英語表記で指定)"),
|
|
232
|
-
style: z
|
|
233
|
-
.string()
|
|
234
|
-
.optional()
|
|
235
|
-
.describe("指定するスタイル名(例: 'normal', 'ura', 'sleepy'など。省略時はキャラクターのデフォルト設定に従う)"),
|
|
236
|
-
},
|
|
237
|
-
}, async (args) => {
|
|
238
|
-
const { operator, style } = args || {};
|
|
239
|
-
logger.info('オペレータアサイン開始', { operator, style });
|
|
240
|
-
validateOperatorInput(operator);
|
|
241
|
-
try {
|
|
242
|
-
const assignResult = await assignOperator(operatorManager, operator, style);
|
|
243
|
-
logger.info('オペレータアサイン成功', {
|
|
244
|
-
characterId: assignResult.characterId,
|
|
245
|
-
characterName: assignResult.characterName,
|
|
246
|
-
});
|
|
247
|
-
// 背景画像を切り替え
|
|
248
|
-
if (terminalBackground) {
|
|
249
|
-
logger.error('🔧 TerminalBackground instance exists');
|
|
250
|
-
const isEnabled = await terminalBackground.isEnabled();
|
|
251
|
-
logger.error('📊 Terminal background enabled check:', { isEnabled });
|
|
252
|
-
if (isEnabled) {
|
|
253
|
-
logger.error('🔄 Switching background for character:', assignResult.characterId);
|
|
254
|
-
await terminalBackground.switchCharacter(assignResult.characterId);
|
|
255
|
-
logger.error('✅ 背景画像切り替え完了', { characterId: assignResult.characterId });
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
logger.error('⚠️ Terminal background is not enabled');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
logger.error('❌ TerminalBackground instance is null');
|
|
263
|
-
}
|
|
264
|
-
const character = await characterInfoService.getCharacterInfo(assignResult.characterId);
|
|
265
|
-
if (!character) {
|
|
266
|
-
throw new Error(`キャラクター情報が見つかりません: ${assignResult.characterId}`);
|
|
267
|
-
}
|
|
268
|
-
const availableStyles = extractStyleInfo(character);
|
|
269
|
-
const resultText = formatAssignmentResult(assignResult, availableStyles);
|
|
270
|
-
return {
|
|
271
|
-
content: [
|
|
272
|
-
{
|
|
273
|
-
type: 'text',
|
|
274
|
-
text: resultText,
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
throw new Error(`オペレータ割り当てエラー: ${error.message}`);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
server.registerTool('operator_release', {
|
|
284
|
-
description: '現在のオペレータを解放します',
|
|
285
|
-
inputSchema: {},
|
|
286
|
-
}, async () => {
|
|
287
|
-
try {
|
|
288
|
-
const result = await operatorManager.releaseOperator();
|
|
289
|
-
let releaseMessage;
|
|
290
|
-
if (result.wasAssigned) {
|
|
291
|
-
releaseMessage = `オペレータを解放しました: ${result.characterName}`;
|
|
292
|
-
logger.info(`オペレータ解放: ${result.characterId}`);
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
releaseMessage = 'オペレータは割り当てられていません';
|
|
296
|
-
logger.info('オペレータ未割り当て状態');
|
|
297
|
-
}
|
|
298
|
-
// 背景画像をクリア(オペレータの有無に関わらず実行)
|
|
299
|
-
if (terminalBackground) {
|
|
300
|
-
if (await terminalBackground.isEnabled()) {
|
|
301
|
-
await terminalBackground.clearBackground();
|
|
302
|
-
logger.info('背景画像クリア完了');
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
content: [
|
|
307
|
-
{
|
|
308
|
-
type: 'text',
|
|
309
|
-
text: releaseMessage,
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
throw new Error(`オペレータ解放エラー: ${error.message}`);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
server.registerTool('operator_status', {
|
|
319
|
-
description: '現在のオペレータ状況を確認します',
|
|
320
|
-
inputSchema: {},
|
|
321
|
-
}, async () => {
|
|
322
|
-
try {
|
|
323
|
-
const status = await operatorManager.showCurrentOperator();
|
|
324
|
-
return {
|
|
325
|
-
content: [
|
|
326
|
-
{
|
|
327
|
-
type: 'text',
|
|
328
|
-
text: status.message,
|
|
329
|
-
},
|
|
330
|
-
],
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
throw new Error(`オペレータ状況確認エラー: ${error.message}`);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
server.registerTool('operator_available', {
|
|
338
|
-
description: '利用可能なオペレータ一覧を表示します',
|
|
339
|
-
inputSchema: {},
|
|
340
|
-
}, async () => {
|
|
341
|
-
try {
|
|
342
|
-
const result = await operatorManager.getAvailableOperators();
|
|
343
|
-
let text = result.available.length > 0
|
|
344
|
-
? `利用可能なオペレータ: ${result.available.join(', ')}`
|
|
345
|
-
: '利用可能なオペレータがありません';
|
|
346
|
-
if (result.busy.length > 0) {
|
|
347
|
-
text += `\n仕事中のオペレータ: ${result.busy.join(', ')}`;
|
|
348
|
-
}
|
|
349
|
-
return {
|
|
350
|
-
content: [
|
|
351
|
-
{
|
|
352
|
-
type: 'text',
|
|
353
|
-
text: text,
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
throw new Error(`利用可能オペレータ確認エラー: ${error.message}`);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
// say音声出力ツール(src/say/index.js使用)
|
|
363
|
-
server.registerTool('say', {
|
|
364
|
-
description: 'COEIROINKを使って日本語音声を非同期で出力します(低レイテンシストリーミング対応、即座にレスポンス)',
|
|
365
|
-
inputSchema: {
|
|
366
|
-
message: z.string().describe('発話させるメッセージ(日本語)'),
|
|
367
|
-
voice: z.string().optional().describe('音声ID(省略時はオペレータ設定を使用)'),
|
|
368
|
-
rate: z.number().optional().describe('絶対速度(WPM、200 = 標準)'),
|
|
369
|
-
factor: z.number().optional().describe('相対速度(倍率、1.0 = 等速)'),
|
|
370
|
-
style: z
|
|
371
|
-
.string()
|
|
372
|
-
.optional()
|
|
373
|
-
.describe("スタイル名(日本語名をそのまま指定。例: ディアちゃんの場合 'のーまる', 'ひそひそ', 'セクシー')"),
|
|
374
|
-
},
|
|
375
|
-
}, async (args) => {
|
|
376
|
-
const { message, voice, rate, factor, style } = args;
|
|
377
|
-
try {
|
|
378
|
-
logger.debug('=== SAY TOOL DEBUG START ===');
|
|
379
|
-
logger.debug(`Input parameters:`);
|
|
380
|
-
logger.debug(` message: "${message}"`);
|
|
381
|
-
logger.debug(` voice: ${voice || 'null (will use operator voice)'}`);
|
|
382
|
-
logger.debug(` rate: ${rate || 'undefined (will use config default)'}`);
|
|
383
|
-
logger.debug(` factor: ${factor || 'undefined (will use speaker natural speed)'}`);
|
|
384
|
-
logger.debug(` style: ${style || 'undefined (will use operator default)'}`);
|
|
385
|
-
// rateとfactorの同時指定チェック
|
|
386
|
-
if (rate !== undefined && factor !== undefined) {
|
|
387
|
-
throw new Error('rateとfactorは同時に指定できません。どちらか一方を指定してください。');
|
|
388
|
-
}
|
|
389
|
-
// voice文字列をパース("characterId:styleName"形式に対応)
|
|
390
|
-
let parsedVoice = voice || null;
|
|
391
|
-
let parsedStyle = style;
|
|
392
|
-
if (voice && voice.includes(':')) {
|
|
393
|
-
const parts = voice.split(':');
|
|
394
|
-
if (parts.length === 2) {
|
|
395
|
-
parsedVoice = parts[0];
|
|
396
|
-
// styleパラメータが明示されていない場合のみ、voice文字列から抽出したstyleを使用
|
|
397
|
-
if (!style) {
|
|
398
|
-
parsedStyle = parts[1];
|
|
399
|
-
logger.debug(` voice文字列からパース: characterId="${parsedVoice}", style="${parsedStyle}"`);
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
logger.warn(`voice文字列にstyleが含まれていますが、styleパラメータが優先されます`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
throw new Error(`不正なvoice形式です: "${voice}"\n` +
|
|
407
|
-
`使用可能な形式:\n` +
|
|
408
|
-
` - "characterId" (例: "alma")\n` +
|
|
409
|
-
` - "characterId:styleName" (例: "alma:のーまる")`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
// Issue #58: オペレータ未アサイン時の再アサイン促進メッセージ
|
|
413
|
-
// voiceパラメータが指定されている場合はオペレータ不要
|
|
414
|
-
const currentOperator = await operatorManager.showCurrentOperator();
|
|
415
|
-
if (!currentOperator.characterId && !parsedVoice) {
|
|
416
|
-
// オペレータ未割り当て時に背景画像をクリア
|
|
417
|
-
if (terminalBackground) {
|
|
418
|
-
if (await terminalBackground.isEnabled()) {
|
|
419
|
-
await terminalBackground.clearBackground();
|
|
420
|
-
logger.info('オペレータ未割り当てのため背景画像をクリア');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// 利用可能なオペレータを取得
|
|
424
|
-
let availableOperators = [];
|
|
425
|
-
try {
|
|
426
|
-
const result = await operatorManager.getAvailableOperators();
|
|
427
|
-
availableOperators = result.available;
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
logger.warn(`Failed to get available operators: ${error.message}`);
|
|
431
|
-
}
|
|
432
|
-
let guidanceMessage = '⚠️ オペレータが割り当てられていません。\n\n';
|
|
433
|
-
guidanceMessage += '🔧 次の手順で進めてください:\n';
|
|
434
|
-
guidanceMessage += '1. operator_assign を実行(通常は引数なしで)\n';
|
|
435
|
-
guidanceMessage += '2. 再度 say コマンドで音声を生成\n\n';
|
|
436
|
-
if (availableOperators.length > 0) {
|
|
437
|
-
guidanceMessage += `🎭 利用可能なオペレータ: ${availableOperators.join(', ')}\n\n`;
|
|
438
|
-
guidanceMessage +=
|
|
439
|
-
"💡 operator_assign を引数なしで実行すると、ランダムに選択されます。";
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
guidanceMessage +=
|
|
443
|
-
'❌ 現在利用可能なオペレータがありません。しばらく待ってから再試行してください。';
|
|
444
|
-
}
|
|
445
|
-
guidanceMessage += '\n\n💡 または、voice パラメータで直接キャラクターを指定することもできます。';
|
|
446
|
-
return {
|
|
447
|
-
content: [
|
|
448
|
-
{
|
|
449
|
-
type: 'text',
|
|
450
|
-
text: guidanceMessage,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
// Issue #58: 動的タイムアウト延長 - sayコマンド実行時にオペレータ予約を延長
|
|
456
|
-
// ベストエフォート非同期処理(エラーは無視、音声生成をブロックしない)
|
|
457
|
-
// オペレータがアサインされている場合のみ予約を延長
|
|
458
|
-
if (currentOperator.characterId) {
|
|
459
|
-
operatorManager
|
|
460
|
-
.refreshOperatorReservation()
|
|
461
|
-
.then(refreshSuccess => {
|
|
462
|
-
if (refreshSuccess) {
|
|
463
|
-
logger.info(`Operator reservation refreshed for: ${currentOperator.characterId}`);
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
logger.warn(`Could not refresh operator reservation for: ${currentOperator.characterId} - operator may have already expired`);
|
|
467
|
-
}
|
|
468
|
-
})
|
|
469
|
-
.catch(error => {
|
|
470
|
-
logger.error(`Operator reservation refresh failed: ${error.message} - operator timeout extension failed`);
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
// スタイル検証(事前チェック)
|
|
474
|
-
// parsedStyleとparsedVoiceを使用
|
|
475
|
-
if (parsedStyle) {
|
|
476
|
-
try {
|
|
477
|
-
// voiceが指定されている場合はそのキャラクターのスタイル、なければ現在のオペレータのスタイルを検証
|
|
478
|
-
const targetCharacterId = parsedVoice || currentOperator.characterId;
|
|
479
|
-
if (!targetCharacterId) {
|
|
480
|
-
throw new Error(`キャラクター情報が取得できません`);
|
|
481
|
-
}
|
|
482
|
-
const character = await characterInfoService.getCharacterInfo(targetCharacterId);
|
|
483
|
-
if (!character) {
|
|
484
|
-
throw new Error(`キャラクター '${targetCharacterId}' が見つかりません`);
|
|
485
|
-
}
|
|
486
|
-
// 利用可能なスタイルを取得
|
|
487
|
-
const availableStyles = Object.values(character.styles || {});
|
|
488
|
-
// 指定されたスタイルが存在するか確認
|
|
489
|
-
const styleExists = availableStyles.some(s => s.styleName === parsedStyle);
|
|
490
|
-
if (!styleExists) {
|
|
491
|
-
const styleNames = availableStyles.map(s => s.styleName);
|
|
492
|
-
throw new Error(`指定されたスタイル '${parsedStyle}' が ${character.speakerName || targetCharacterId} には存在しません。\n` +
|
|
493
|
-
`利用可能なスタイル: ${styleNames.join(', ')}`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
logger.error(`スタイル検証エラー: ${error.message}`);
|
|
498
|
-
throw error;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
// 設定情報をログ出力
|
|
502
|
-
// NOTE: ConfigManagerはすでにsayCoeiroink内部で管理されているため、
|
|
503
|
-
// ここでは設定のログ出力をスキップ
|
|
504
|
-
logger.debug('Audio config is managed internally by SayCoeiroink');
|
|
505
|
-
logger.debug('==============================');
|
|
506
|
-
// 速度設定オプションを構築(CLIと同じ形式)
|
|
507
|
-
const speedOptions = {};
|
|
508
|
-
if (rate !== undefined) {
|
|
509
|
-
speedOptions.rate = rate;
|
|
510
|
-
}
|
|
511
|
-
if (factor !== undefined) {
|
|
512
|
-
speedOptions.factor = factor;
|
|
513
|
-
}
|
|
514
|
-
// MCP設計: 音声合成タスクをキューに投稿のみ(再生完了を待たない)
|
|
515
|
-
// - synthesize() はキューに追加して即座にレスポンス
|
|
516
|
-
// - 実際の音声合成・再生は背景のSpeechQueueで非同期処理
|
|
517
|
-
// - CLIとは異なり、MCPではウォームアップ・完了待機は実行しない
|
|
518
|
-
const result = sayCoeiroink.synthesize(message, {
|
|
519
|
-
voice: parsedVoice,
|
|
520
|
-
...speedOptions, // rateまたはfactorを展開
|
|
521
|
-
style: parsedStyle,
|
|
522
|
-
allowFallback: false, // MCPツールではオペレータが必須
|
|
523
|
-
});
|
|
524
|
-
// 結果をログ出力
|
|
525
|
-
logger.debug(`Result: ${JSON.stringify(result)}`);
|
|
526
|
-
// オペレータまたはvoice指定の情報を取得
|
|
527
|
-
const voiceInfo = currentOperator.characterId
|
|
528
|
-
? `オペレータ: ${currentOperator.characterId}`
|
|
529
|
-
: `voice指定: ${parsedVoice}${parsedStyle ? `:${parsedStyle}` : ''}`;
|
|
530
|
-
const modeInfo = `発声キューに追加 - ${voiceInfo}, タスクID: ${result.taskId}`;
|
|
531
|
-
logger.info(modeInfo);
|
|
532
|
-
logger.debug('=== SAY TOOL DEBUG END ===');
|
|
533
|
-
// 即座にレスポンスを返す(音声合成の完了を待たない)
|
|
534
|
-
// タスクIDとキュー長の情報も含める
|
|
535
|
-
const responseText = `音声合成を開始しました - ${voiceInfo}\n` +
|
|
536
|
-
`タスクID: ${result.taskId}\n` +
|
|
537
|
-
`キュー長: ${result.queueLength} 個`;
|
|
538
|
-
return {
|
|
539
|
-
content: [
|
|
540
|
-
{
|
|
541
|
-
type: 'text',
|
|
542
|
-
text: responseText,
|
|
543
|
-
},
|
|
544
|
-
],
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
catch (error) {
|
|
548
|
-
logger.debug(`SAY TOOL ERROR: ${error.message}`);
|
|
549
|
-
logger.debug(`Stack trace: ${error.stack}`);
|
|
550
|
-
throw new Error(`音声出力エラー: ${error.message}`);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
// ログ取得ツール
|
|
554
|
-
server.registerTool('debug_logs', {
|
|
555
|
-
description: 'デバッグ用ログの取得と表示。ログレベル・時刻・検索条件による絞り込み、統計情報の表示が可能',
|
|
556
|
-
inputSchema: {
|
|
557
|
-
action: z
|
|
558
|
-
.enum(['get', 'stats', 'clear'])
|
|
559
|
-
.describe('実行するアクション: get=ログ取得, stats=統計表示, clear=ログクリア'),
|
|
560
|
-
level: z
|
|
561
|
-
.array(z.enum(['error', 'warn', 'info', 'verbose', 'debug']))
|
|
562
|
-
.optional()
|
|
563
|
-
.describe('取得するログレベル(複数選択可)'),
|
|
564
|
-
since: z.string().optional().describe('この時刻以降のログを取得(ISO 8601形式)'),
|
|
565
|
-
limit: z
|
|
566
|
-
.number()
|
|
567
|
-
.min(1)
|
|
568
|
-
.max(1000)
|
|
569
|
-
.optional()
|
|
570
|
-
.describe('取得する最大ログエントリ数(1-1000)'),
|
|
571
|
-
search: z.string().optional().describe('ログメッセージ内の検索キーワード'),
|
|
572
|
-
format: z
|
|
573
|
-
.enum(['formatted', 'raw'])
|
|
574
|
-
.optional()
|
|
575
|
-
.describe('出力形式: formatted=整形済み, raw=生データ'),
|
|
576
|
-
},
|
|
577
|
-
}, async (args) => {
|
|
578
|
-
const { action = 'get', level, since, limit, search, format = 'formatted' } = args || {};
|
|
579
|
-
try {
|
|
580
|
-
switch (action) {
|
|
581
|
-
case 'get': {
|
|
582
|
-
const options = {};
|
|
583
|
-
if (level && level.length > 0) {
|
|
584
|
-
options.level = level;
|
|
585
|
-
}
|
|
586
|
-
if (since) {
|
|
587
|
-
try {
|
|
588
|
-
options.since = new Date(since);
|
|
589
|
-
}
|
|
590
|
-
catch {
|
|
591
|
-
throw new Error(`無効な日時形式です: ${since}`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (limit) {
|
|
595
|
-
options.limit = limit;
|
|
596
|
-
}
|
|
597
|
-
if (search) {
|
|
598
|
-
options.search = search;
|
|
599
|
-
}
|
|
600
|
-
const entries = logger.getLogEntries(options);
|
|
601
|
-
if (entries.length === 0) {
|
|
602
|
-
return {
|
|
603
|
-
content: [
|
|
604
|
-
{
|
|
605
|
-
type: 'text',
|
|
606
|
-
text: '条件に一致するログエントリが見つかりませんでした。',
|
|
607
|
-
},
|
|
608
|
-
],
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
let resultText;
|
|
612
|
-
if (format === 'raw') {
|
|
613
|
-
resultText = `ログエントリ (${entries.length}件):\n\n${JSON.stringify(entries, null, 2)}`;
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
resultText = `ログエントリ (${entries.length}件):\n\n`;
|
|
617
|
-
entries.forEach((entry, index) => {
|
|
618
|
-
resultText += `${index + 1}. [${entry.level.toUpperCase()}] ${entry.timestamp}\n`;
|
|
619
|
-
resultText += ` ${entry.message}\n`;
|
|
620
|
-
if (entry.args && entry.args.length > 0) {
|
|
621
|
-
resultText += ` 引数: ${entry.args
|
|
622
|
-
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
|
623
|
-
.join(', ')}\n`;
|
|
624
|
-
}
|
|
625
|
-
resultText += '\n';
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
return {
|
|
629
|
-
content: [
|
|
630
|
-
{
|
|
631
|
-
type: 'text',
|
|
632
|
-
text: resultText,
|
|
633
|
-
},
|
|
634
|
-
],
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
case 'stats': {
|
|
638
|
-
const stats = logger.getLogStats();
|
|
639
|
-
const statsText = `📊 ログ統計情報\n\n` +
|
|
640
|
-
`総エントリ数: ${stats.totalEntries}\n\n` +
|
|
641
|
-
`レベル別エントリ数:\n` +
|
|
642
|
-
` ERROR: ${stats.entriesByLevel.error}\n` +
|
|
643
|
-
` WARN: ${stats.entriesByLevel.warn}\n` +
|
|
644
|
-
` INFO: ${stats.entriesByLevel.info}\n` +
|
|
645
|
-
` VERB: ${stats.entriesByLevel.verbose}\n` +
|
|
646
|
-
` DEBUG: ${stats.entriesByLevel.debug}\n\n` +
|
|
647
|
-
`時刻範囲:\n` +
|
|
648
|
-
` 最古: ${stats.oldestEntry || 'なし'}\n` +
|
|
649
|
-
` 最新: ${stats.newestEntry || 'なし'}\n\n` +
|
|
650
|
-
`蓄積モード: ${logger.isAccumulating() ? 'ON' : 'OFF'}`;
|
|
651
|
-
return {
|
|
652
|
-
content: [
|
|
653
|
-
{
|
|
654
|
-
type: 'text',
|
|
655
|
-
text: statsText,
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
case 'clear': {
|
|
661
|
-
const beforeCount = logger.getLogStats().totalEntries;
|
|
662
|
-
logger.clearLogEntries();
|
|
663
|
-
return {
|
|
664
|
-
content: [
|
|
665
|
-
{
|
|
666
|
-
type: 'text',
|
|
667
|
-
text: `ログエントリをクリアしました(${beforeCount}件削除)`,
|
|
668
|
-
},
|
|
669
|
-
],
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
default:
|
|
673
|
-
throw new Error(`無効なアクション: ${action}`);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
catch (error) {
|
|
677
|
-
throw new Error(`ログ取得エラー: ${error.message}`);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
// スタイル情報表示ツール
|
|
681
|
-
server.registerTool('operator_styles', {
|
|
682
|
-
description: '現在のオペレータまたは指定したキャラクターの利用可能なスタイル一覧を表示します。キャラクターの基本情報、全スタイルの詳細(性格・話し方)、スタイル選択方法を確認できます。スタイル切り替えにはsayツールのstyleパラメータで日本語名を使用してください。',
|
|
683
|
-
inputSchema: {
|
|
684
|
-
character: z
|
|
685
|
-
.string()
|
|
686
|
-
.optional()
|
|
687
|
-
.describe('キャラクターID(省略時は現在のオペレータのスタイル情報を表示)'),
|
|
688
|
-
},
|
|
689
|
-
}, async (args) => {
|
|
690
|
-
const { character } = args || {};
|
|
691
|
-
try {
|
|
692
|
-
let targetCharacter;
|
|
693
|
-
let targetCharacterId;
|
|
694
|
-
if (character) {
|
|
695
|
-
// 指定されたキャラクターの情報を取得
|
|
696
|
-
try {
|
|
697
|
-
targetCharacter = await characterInfoService.getCharacterInfo(character);
|
|
698
|
-
if (!targetCharacter) {
|
|
699
|
-
throw new Error(`キャラクター '${character}' が見つかりません`);
|
|
700
|
-
}
|
|
701
|
-
targetCharacterId = character;
|
|
702
|
-
}
|
|
703
|
-
catch (error) {
|
|
704
|
-
throw new Error(`キャラクター '${character}' が見つかりません`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
else {
|
|
708
|
-
// 現在のオペレータの情報を取得
|
|
709
|
-
const currentOperator = await operatorManager.showCurrentOperator();
|
|
710
|
-
if (!currentOperator.characterId) {
|
|
711
|
-
throw new Error('現在オペレータが割り当てられていません。まず operator_assign を実行してください。');
|
|
712
|
-
}
|
|
713
|
-
targetCharacter = await characterInfoService.getCharacterInfo(currentOperator.characterId);
|
|
714
|
-
targetCharacterId = currentOperator.characterId;
|
|
715
|
-
if (!targetCharacter) {
|
|
716
|
-
throw new Error(`現在のオペレータ '${currentOperator.characterId}' のキャラクター情報が見つかりません`);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
// スタイル情報を取得
|
|
720
|
-
const availableStyles = extractStyleInfo(targetCharacter);
|
|
721
|
-
// 結果を整形
|
|
722
|
-
const resultText = formatStylesResult(targetCharacter, availableStyles);
|
|
723
|
-
return {
|
|
724
|
-
content: [
|
|
725
|
-
{
|
|
726
|
-
type: 'text',
|
|
727
|
-
text: resultText,
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
catch (error) {
|
|
733
|
-
throw new Error(`スタイル情報取得エラー: ${error.message}`);
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
// 並行生成制御ツール
|
|
737
|
-
server.registerTool('parallel_generation_control', {
|
|
738
|
-
description: 'チャンク並行生成機能の制御と設定管理を行います。生成の並行数、待機時間、先読み数、初回ポーズ機能などを調整できます。',
|
|
739
|
-
inputSchema: {
|
|
740
|
-
action: z
|
|
741
|
-
.enum(['enable', 'disable', 'status', 'update_options'])
|
|
742
|
-
.describe('実行するアクション'),
|
|
743
|
-
options: z
|
|
744
|
-
.object({
|
|
745
|
-
maxConcurrency: z.number().min(1).max(5).optional().describe('最大並行生成数(1-5)'),
|
|
746
|
-
delayBetweenRequests: z
|
|
747
|
-
.number()
|
|
748
|
-
.min(0)
|
|
749
|
-
.max(1000)
|
|
750
|
-
.optional()
|
|
751
|
-
.describe('リクエスト間隔(ms、0-1000)'),
|
|
752
|
-
bufferAheadCount: z.number().min(0).max(3).optional().describe('先読みチャンク数(0-3)'),
|
|
753
|
-
pauseUntilFirstComplete: z
|
|
754
|
-
.boolean()
|
|
755
|
-
.optional()
|
|
756
|
-
.describe('初回チャンク完了まで並行生成をポーズ(レイテンシ改善)'),
|
|
757
|
-
})
|
|
758
|
-
.optional()
|
|
759
|
-
.describe('更新するオプション(action=update_optionsの場合)'),
|
|
760
|
-
},
|
|
761
|
-
}, async (args) => {
|
|
762
|
-
const { action, options } = args || {};
|
|
763
|
-
try {
|
|
764
|
-
switch (action) {
|
|
765
|
-
case 'enable':
|
|
766
|
-
sayCoeiroink.setParallelGenerationEnabled(true);
|
|
767
|
-
return {
|
|
768
|
-
content: [
|
|
769
|
-
{
|
|
770
|
-
type: 'text',
|
|
771
|
-
text: '✅ 並行チャンク生成を有効化しました。\n\n⚡ 効果:\n- 複数チャンクの同時生成により高速化\n- レスポンシブな音声再生開始\n- 体感的なレイテンシ削減',
|
|
772
|
-
},
|
|
773
|
-
],
|
|
774
|
-
};
|
|
775
|
-
case 'disable':
|
|
776
|
-
sayCoeiroink.setParallelGenerationEnabled(false);
|
|
777
|
-
return {
|
|
778
|
-
content: [
|
|
779
|
-
{
|
|
780
|
-
type: 'text',
|
|
781
|
-
text: '⏸️ 並行チャンク生成を無効化しました。\n\n🔄 従来の逐次生成モードに戻りました。\n- 安定性重視の動作\n- メモリ使用量削減',
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
};
|
|
785
|
-
case 'status': {
|
|
786
|
-
const currentOptions = sayCoeiroink.getStreamControllerOptions();
|
|
787
|
-
const stats = sayCoeiroink.getGenerationStats();
|
|
788
|
-
return {
|
|
789
|
-
content: [
|
|
790
|
-
{
|
|
791
|
-
type: 'text',
|
|
792
|
-
text: `📊 並行生成ステータス\n\n` +
|
|
793
|
-
`🎛️ 設定:\n` +
|
|
794
|
-
` - 状態: ${currentOptions.maxConcurrency > 1 ? '✅ 並行生成' : '❌ 逐次生成'}\n` +
|
|
795
|
-
` - 最大並行数: ${currentOptions.maxConcurrency} ${currentOptions.maxConcurrency === 1 ? '(逐次モード)' : '(並行モード)'}\n` +
|
|
796
|
-
` - リクエスト間隔: ${currentOptions.delayBetweenRequests}ms\n` +
|
|
797
|
-
` - 先読み数: ${currentOptions.bufferAheadCount}\n` +
|
|
798
|
-
` - 初回ポーズ: ${currentOptions.pauseUntilFirstComplete ? '✅ 有効' : '❌ 無効'}\n\n` +
|
|
799
|
-
`📈 現在の統計:\n` +
|
|
800
|
-
` - アクティブタスク: ${stats.activeTasks}\n` +
|
|
801
|
-
` - 完了済み結果: ${stats.completedResults}\n` +
|
|
802
|
-
` - メモリ使用量: ${(stats.totalMemoryUsage / 1024).toFixed(1)}KB`,
|
|
803
|
-
},
|
|
804
|
-
],
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
case 'update_options':
|
|
808
|
-
if (options) {
|
|
809
|
-
sayCoeiroink.updateStreamControllerOptions(options);
|
|
810
|
-
const updatedOptions = sayCoeiroink.getStreamControllerOptions();
|
|
811
|
-
return {
|
|
812
|
-
content: [
|
|
813
|
-
{
|
|
814
|
-
type: 'text',
|
|
815
|
-
text: `⚙️ オプション更新完了\n\n` +
|
|
816
|
-
`🔧 新しい設定:\n` +
|
|
817
|
-
` - 最大並行数: ${updatedOptions.maxConcurrency} ${updatedOptions.maxConcurrency === 1 ? '(逐次モード)' : '(並行モード)'}\n` +
|
|
818
|
-
` - リクエスト間隔: ${updatedOptions.delayBetweenRequests}ms\n` +
|
|
819
|
-
` - 先読み数: ${updatedOptions.bufferAheadCount}\n` +
|
|
820
|
-
` - 初回ポーズ: ${updatedOptions.pauseUntilFirstComplete ? '✅ 有効' : '❌ 無効'}\n\n` +
|
|
821
|
-
`💡 次回の音声合成から適用されます。`,
|
|
822
|
-
},
|
|
823
|
-
],
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
throw new Error('update_optionsアクションにはoptionsパラメータが必要です');
|
|
828
|
-
}
|
|
829
|
-
default:
|
|
830
|
-
throw new Error(`無効なアクション: ${action}`);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
catch (error) {
|
|
834
|
-
throw new Error(`並行生成制御エラー: ${error.message}`);
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
// 辞書登録ツール
|
|
838
|
-
server.registerTool('dictionary_register', {
|
|
839
|
-
description: 'COEIROINKのユーザー辞書に単語を登録します。専門用語や固有名詞の読み方を正確に制御できます。',
|
|
840
|
-
inputSchema: {
|
|
841
|
-
word: z.string().describe('登録する単語(半角英数字も可、自動で全角変換されます)'),
|
|
842
|
-
yomi: z.string().describe('読み方(全角カタカナ)'),
|
|
843
|
-
accent: z.number().describe('アクセント位置(0:平板型、1以上:該当モーラが高い)'),
|
|
844
|
-
numMoras: z.number().describe('モーラ数(カタカナの音節数)'),
|
|
845
|
-
},
|
|
846
|
-
}, async (args) => {
|
|
847
|
-
const { word, yomi, accent, numMoras } = args;
|
|
848
|
-
try {
|
|
849
|
-
// 接続確認
|
|
850
|
-
const isConnected = await dictionaryService.checkConnection();
|
|
851
|
-
if (!isConnected) {
|
|
852
|
-
return {
|
|
853
|
-
content: [
|
|
854
|
-
{
|
|
855
|
-
type: 'text',
|
|
856
|
-
text: '❌ COEIROINKサーバーに接続できません。\n' +
|
|
857
|
-
'サーバーが起動していることを確認してください。',
|
|
858
|
-
},
|
|
859
|
-
],
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
// 単語を登録(DictionaryServiceが永続化まで処理)
|
|
863
|
-
const success = await dictionaryService.addWord({ word, yomi, accent, numMoras });
|
|
864
|
-
if (success) {
|
|
865
|
-
return {
|
|
866
|
-
content: [
|
|
867
|
-
{
|
|
868
|
-
type: 'text',
|
|
869
|
-
text: `✅ 単語を辞書に登録しました\n\n` +
|
|
870
|
-
`単語: ${word}\n` +
|
|
871
|
-
`読み方: ${yomi}\n` +
|
|
872
|
-
`アクセント: ${accent}\n` +
|
|
873
|
-
`モーラ数: ${numMoras}\n\n` +
|
|
874
|
-
`💾 辞書データは永続化され、次回起動時に自動登録されます。`,
|
|
875
|
-
},
|
|
876
|
-
],
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
else {
|
|
880
|
-
return {
|
|
881
|
-
content: [
|
|
882
|
-
{
|
|
883
|
-
type: 'text',
|
|
884
|
-
text: `❌ 辞書登録に失敗しました`,
|
|
885
|
-
},
|
|
886
|
-
],
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
logger.error(`Dictionary registration error:`, error);
|
|
892
|
-
throw new Error(`辞書登録エラー: ${error.message}`);
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
// キュー状態確認ツール
|
|
896
|
-
server.registerTool('queue_status', {
|
|
897
|
-
description: '音声キューの状態を確認します。現在のキュー長、処理状況、次に処理されるタスクIDを取得できます。',
|
|
898
|
-
inputSchema: {},
|
|
899
|
-
}, async () => {
|
|
900
|
-
try {
|
|
901
|
-
const status = sayCoeiroink.getSpeechQueueStatus();
|
|
902
|
-
let statusText = '📊 音声キューステータス\n\n';
|
|
903
|
-
statusText += `キュー長: ${status.queueLength} 個\n`;
|
|
904
|
-
statusText += `処理状態: ${status.isProcessing ? '🔄 処理中' : '⏸️ 待機中'}\n`;
|
|
905
|
-
if (status.nextTaskId !== null) {
|
|
906
|
-
statusText += `次のタスクID: ${status.nextTaskId}\n`;
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
statusText += `次のタスク: なし\n`;
|
|
910
|
-
}
|
|
911
|
-
if (status.queueLength === 0 && !status.isProcessing) {
|
|
912
|
-
statusText += '\n💡 キューは空で、待機中です。';
|
|
913
|
-
}
|
|
914
|
-
else if (status.isProcessing) {
|
|
915
|
-
statusText += '\n⚡ 音声処理が実行中です。';
|
|
916
|
-
}
|
|
917
|
-
return {
|
|
918
|
-
content: [
|
|
919
|
-
{
|
|
920
|
-
type: 'text',
|
|
921
|
-
text: statusText,
|
|
922
|
-
},
|
|
923
|
-
],
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
catch (error) {
|
|
927
|
-
throw new Error(`キューステータス取得エラー: ${error.message}`);
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
// キュークリアツール
|
|
931
|
-
server.registerTool('queue_clear', {
|
|
932
|
-
description: '音声キューをクリアします。taskIdsを指定すると特定のタスクのみ削除できます。省略時は全タスクを削除します。現在再生中の音声は停止しません。',
|
|
933
|
-
inputSchema: {
|
|
934
|
-
taskIds: z
|
|
935
|
-
.array(z.number())
|
|
936
|
-
.optional()
|
|
937
|
-
.describe('削除するタスクIDのリスト(省略時は全タスク削除)'),
|
|
938
|
-
},
|
|
939
|
-
}, async (args) => {
|
|
940
|
-
const { taskIds } = args || {};
|
|
941
|
-
try {
|
|
942
|
-
const statusBefore = sayCoeiroink.getSpeechQueueStatus();
|
|
943
|
-
const result = await sayCoeiroink.clearSpeechQueue(taskIds);
|
|
944
|
-
let resultText;
|
|
945
|
-
if (taskIds && taskIds.length > 0) {
|
|
946
|
-
// 特定タスクの削除
|
|
947
|
-
resultText = '🗑️ 指定されたタスクを削除しました\n\n';
|
|
948
|
-
resultText += `削除されたタスク数: ${result.removedCount} 個\n`;
|
|
949
|
-
resultText += `指定されたタスクID: ${taskIds.join(', ')}\n`;
|
|
950
|
-
if (result.removedCount < taskIds.length) {
|
|
951
|
-
resultText += `\n⚠️ 一部のタスクIDが見つかりませんでした。`;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
// 全タスクの削除
|
|
956
|
-
resultText = '🗑️ キューをクリアしました\n\n';
|
|
957
|
-
resultText += `削除されたタスク数: ${result.removedCount} 個\n`;
|
|
958
|
-
}
|
|
959
|
-
if (statusBefore.isProcessing) {
|
|
960
|
-
resultText += '\n⚠️ 注意: 現在再生中の音声は継続されます。';
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
content: [
|
|
964
|
-
{
|
|
965
|
-
type: 'text',
|
|
966
|
-
text: resultText,
|
|
967
|
-
},
|
|
968
|
-
],
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
throw new Error(`キュークリアエラー: ${error.message}`);
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
// 再生停止ツール
|
|
976
|
-
server.registerTool('playback_stop', {
|
|
977
|
-
description: '音声再生を停止します(チャンク境界で停止)。現在再生中のチャンクは最後まで再生され、次のチャンクから停止します。キューにあるタスクは削除されません。',
|
|
978
|
-
inputSchema: {},
|
|
979
|
-
}, async () => {
|
|
980
|
-
try {
|
|
981
|
-
sayCoeiroink.stopPlayback();
|
|
982
|
-
const status = sayCoeiroink.getSpeechQueueStatus();
|
|
983
|
-
let resultText = '⏹️ 音声再生の停止を要求しました\n\n';
|
|
984
|
-
resultText += '⚠️ 注意:\n';
|
|
985
|
-
resultText += '- 現在再生中のチャンク(文)は最後まで再生されます\n';
|
|
986
|
-
resultText += '- 次のチャンクからは再生されません\n';
|
|
987
|
-
resultText += `- キューにある ${status.queueLength} 個のタスクは削除されていません\n\n`;
|
|
988
|
-
resultText += '💡 タスクも削除する場合は queue_clear を使用してください';
|
|
989
|
-
return {
|
|
990
|
-
content: [
|
|
991
|
-
{
|
|
992
|
-
type: 'text',
|
|
993
|
-
text: resultText,
|
|
994
|
-
},
|
|
995
|
-
],
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
catch (error) {
|
|
999
|
-
throw new Error(`再生停止エラー: ${error.message}`);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
// タスク完了待機ツール
|
|
1003
|
-
server.registerTool('wait_for_task_completion', {
|
|
1004
|
-
description: '音声タスクの完了を待機します。デフォルトではすべてのタスクが完了するまで待ちますが、remainingQueueLengthを指定すると、キューが指定数になったときに解除されます。デバッグやテスト時に便利です。',
|
|
1005
|
-
inputSchema: {
|
|
1006
|
-
timeout: z
|
|
1007
|
-
.number()
|
|
1008
|
-
.min(1000)
|
|
1009
|
-
.max(60000)
|
|
1010
|
-
.optional()
|
|
1011
|
-
.describe('タイムアウト時間(ミリ秒、1000-60000、デフォルト30000)'),
|
|
1012
|
-
remainingQueueLength: z
|
|
1013
|
-
.number()
|
|
1014
|
-
.min(0)
|
|
1015
|
-
.optional()
|
|
1016
|
-
.describe('この数までキューが減ったら待ちを解除(デフォルト0=全タスク完了まで待機)'),
|
|
1017
|
-
},
|
|
1018
|
-
}, async (args) => {
|
|
1019
|
-
const { timeout = 30000, remainingQueueLength = 0 } = args || {};
|
|
1020
|
-
try {
|
|
1021
|
-
const startTime = Date.now();
|
|
1022
|
-
// 初期状態を取得
|
|
1023
|
-
const initialStatus = sayCoeiroink.getSpeechQueueStatus();
|
|
1024
|
-
// 待機対象がない場合(remainingQueueLengthを考慮)
|
|
1025
|
-
if (initialStatus.queueLength <= remainingQueueLength && !initialStatus.isProcessing) {
|
|
1026
|
-
return {
|
|
1027
|
-
content: [
|
|
1028
|
-
{
|
|
1029
|
-
type: 'text',
|
|
1030
|
-
text: remainingQueueLength === 0
|
|
1031
|
-
? '✅ 待機対象のタスクがありません(キューは空で、処理中のタスクもありません)'
|
|
1032
|
-
: `✅ キューは既に目標数(${remainingQueueLength}個)以下です(現在: ${initialStatus.queueLength}個)`,
|
|
1033
|
-
},
|
|
1034
|
-
],
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
// タイムアウト付きで待機
|
|
1038
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1039
|
-
setTimeout(() => reject(new Error('Timeout')), timeout);
|
|
1040
|
-
});
|
|
1041
|
-
try {
|
|
1042
|
-
// waitForQueueLength()を使用してイベントベースで待機
|
|
1043
|
-
await Promise.race([
|
|
1044
|
-
sayCoeiroink.waitForQueueLength(remainingQueueLength),
|
|
1045
|
-
timeoutPromise
|
|
1046
|
-
]);
|
|
1047
|
-
const waitedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1048
|
-
const finalStatus = sayCoeiroink.getSpeechQueueStatus();
|
|
1049
|
-
const completionMessage = remainingQueueLength === 0
|
|
1050
|
-
? '💡 すべての音声処理が完了しました。'
|
|
1051
|
-
: `💡 キューが目標数(${remainingQueueLength}個)になりました。`;
|
|
1052
|
-
return {
|
|
1053
|
-
content: [
|
|
1054
|
-
{
|
|
1055
|
-
type: 'text',
|
|
1056
|
-
text: `✅ 待機完了\n\n` +
|
|
1057
|
-
`待機時間: ${waitedSeconds}秒\n` +
|
|
1058
|
-
`最終ステータス:\n` +
|
|
1059
|
-
` - キュー長: ${finalStatus.queueLength} 個\n` +
|
|
1060
|
-
` - 処理状態: ${finalStatus.isProcessing ? '処理中' : '待機中'}\n\n` +
|
|
1061
|
-
completionMessage,
|
|
1062
|
-
},
|
|
1063
|
-
],
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
catch (error) {
|
|
1067
|
-
if (error.message === 'Timeout') {
|
|
1068
|
-
const currentStatus = sayCoeiroink.getSpeechQueueStatus();
|
|
1069
|
-
const timeoutMessage = remainingQueueLength === 0
|
|
1070
|
-
? `⚠️ タスクがまだ完了していません。`
|
|
1071
|
-
: `⚠️ キューが目標数(${remainingQueueLength}個)まで減っていません。`;
|
|
1072
|
-
return {
|
|
1073
|
-
content: [
|
|
1074
|
-
{
|
|
1075
|
-
type: 'text',
|
|
1076
|
-
text: `⏱️ タイムアウト(${timeout / 1000}秒)しました\n\n` +
|
|
1077
|
-
`現在のステータス:\n` +
|
|
1078
|
-
` - キュー長: ${currentStatus.queueLength} 個\n` +
|
|
1079
|
-
` - 処理状態: ${currentStatus.isProcessing ? '処理中' : '待機中'}\n\n` +
|
|
1080
|
-
timeoutMessage,
|
|
1081
|
-
},
|
|
1082
|
-
],
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
throw error;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
catch (error) {
|
|
1089
|
-
throw new Error(`タスク待機エラー: ${error.message}`);
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
110
|
+
// ツールの登録
|
|
111
|
+
logger.info('Registering MCP tools...');
|
|
112
|
+
// Operator tools
|
|
113
|
+
registerOperatorAssignTool(server, operatorManager, characterInfoService, terminalBackground);
|
|
114
|
+
registerOperatorReleaseTool(server, operatorManager, terminalBackground);
|
|
115
|
+
registerOperatorStatusTool(server, operatorManager);
|
|
116
|
+
registerOperatorAvailableTool(server, operatorManager);
|
|
117
|
+
registerOperatorStylesTool(server, operatorManager, characterInfoService);
|
|
118
|
+
// Speech tools
|
|
119
|
+
registerSayTool(server, sayCoeiroink, operatorManager, characterInfoService, terminalBackground);
|
|
120
|
+
// registerParallelGenerationControlTool(server, sayCoeiroink);
|
|
121
|
+
// Playback tools
|
|
122
|
+
registerQueueStatusTool(server, sayCoeiroink);
|
|
123
|
+
registerQueueClearTool(server, sayCoeiroink);
|
|
124
|
+
registerPlaybackStopTool(server, sayCoeiroink);
|
|
125
|
+
registerWaitForTaskCompletionTool(server, sayCoeiroink);
|
|
126
|
+
// Dictionary tool
|
|
127
|
+
registerDictionaryRegisterTool(server, dictionaryService);
|
|
128
|
+
// Debug tool
|
|
129
|
+
registerDebugLogsTool(server);
|
|
130
|
+
logger.info('All MCP tools registered successfully');
|
|
1092
131
|
// サーバーの起動
|
|
1093
132
|
async function main() {
|
|
1094
133
|
const transport = new StdioServerTransport();
|