@kajidog/mcp-tts-voicevox 0.0.6 → 0.0.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/README.md +29 -47
- package/dist/index.js +1 -1
- package/dist/voicevox/api.js +3 -3
- package/dist/voicevox/error.js +89 -0
- package/dist/voicevox/index.js +79 -9
- package/dist/voicevox/player.js +55 -141
- package/dist/voicevox/queue/__tests__/manager.test.js +122 -91
- package/dist/voicevox/queue/audio-generator.js +11 -6
- package/dist/voicevox/queue/audio-player.js +2 -3
- package/dist/voicevox/queue/file-manager.js +35 -0
- package/dist/voicevox/queue/manager.js +104 -70
- package/dist/voicevox/queue/types.js +1 -0
- package/dist/voicevox/utils.js +8 -0
- package/package.json +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/voicevox/client.js +0 -391
- package/dist/voicevox/generator.d.ts +0 -6
- package/dist/voicevox/generator.d.ts.map +0 -1
- package/dist/voicevox/generator.js +0 -37
- package/dist/voicevox/generator.js.map +0 -1
- package/dist/voicevox/index.d.ts +0 -11
- package/dist/voicevox/index.d.ts.map +0 -1
- package/dist/voicevox/index.js.map +0 -1
- package/dist/voicevox/player.d.ts +0 -6
- package/dist/voicevox/player.d.ts.map +0 -1
- package/dist/voicevox/player.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,81 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
VOICEVOXを使用した音声合成MCPサーバー
|
|
4
4
|
|
|
5
|
+
## 特徴
|
|
6
|
+
|
|
7
|
+
- **キュー管理機能** - 複数の音声合成リクエストを効率的に処理
|
|
8
|
+
- **プリフェッチ** - 次の音声を事前に生成し、再生をスムーズに
|
|
9
|
+
|
|
5
10
|
## 必要条件
|
|
6
11
|
|
|
7
12
|
- Node.js
|
|
8
13
|
- [VOICEVOXエンジン](https://voicevox.hiroshiba.jp/)
|
|
9
14
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- テキストから音声を合成して再生
|
|
13
|
-
- テキストから音声合成用クエリを生成
|
|
14
|
-
- 音声合成用クエリから音声ファイルを生成
|
|
15
|
-
- テキストまたはクエリを音声生成キューに追加
|
|
16
|
-
|
|
17
|
-
## 使い方
|
|
18
|
-
|
|
19
|
-
### インストール
|
|
15
|
+
## インストール
|
|
20
16
|
|
|
21
17
|
```bash
|
|
22
18
|
npm install -g @kajidog/mcp-tts-voicevox
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
## 使い方
|
|
22
|
+
|
|
23
|
+
### MCPサーバーとして
|
|
26
24
|
|
|
27
25
|
1. VOICEVOXエンジンを起動
|
|
28
|
-
2.
|
|
26
|
+
2. MCPサーバーを起動
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
29
|
npx @kajidog/mcp-tts-voicevox
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
###
|
|
32
|
+
### ライブラリとして
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
プロジェクトに直接インポートして使用することも可能です:
|
|
37
35
|
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
text: "こんにちは!", // 読み上げるテキスト
|
|
41
|
-
speaker: 1 // 話者ID(オプション)
|
|
42
|
-
});
|
|
36
|
+
```bash
|
|
37
|
+
npm install @kajidog/mcp-tts-voicevox
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
```javascript
|
|
41
|
+
import { VoicevoxClient } from "@kajidog/mcp-tts-voicevox";
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
// クライアントを初期化
|
|
44
|
+
const client = new VoicevoxClient({
|
|
45
|
+
url: "http://localhost:50021", // VOICEVOXエンジンのURL
|
|
46
|
+
defaultSpeaker: 1, // デフォルト話者ID(オプション)
|
|
47
|
+
defaultSpeedScale: 1.0 // デフォルト速度(オプション)
|
|
51
48
|
});
|
|
52
49
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
#### 3. 音声合成用クエリから音声ファイルを生成
|
|
50
|
+
// テキストを音声に変換して再生
|
|
51
|
+
await client.speak("こんにちは");
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
query: query, // 音声合成用クエリ
|
|
62
|
-
output: "/path/to/output.wav", // 出力ファイルパス
|
|
63
|
-
speaker: 1 // 話者ID(オプション)
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// 生成された音声ファイルのパス
|
|
67
|
-
const filePath = fileResult.content[0].text;
|
|
53
|
+
// テキストから音声ファイルを生成
|
|
54
|
+
const filePath = await client.generateAudioFile("こんにちは", "./output.wav");
|
|
68
55
|
```
|
|
69
56
|
|
|
70
|
-
##
|
|
71
|
-
|
|
72
|
-
1. **テキストを直接音声に変換**
|
|
73
|
-
- `speak` - 短いテキストをすぐに読み上げたいとき
|
|
74
|
-
|
|
75
|
-
2. **細かい音声設定をカスタマイズ**
|
|
76
|
-
- `generate_query` → クエリ編集 → `synthesize_file` の流れで高度な調整が可能
|
|
57
|
+
## 主な機能
|
|
77
58
|
|
|
78
|
-
|
|
79
|
-
|
|
59
|
+
- **テキスト読み上げ** (`speak`) - テキストを音声に変換して再生
|
|
60
|
+
- **クエリ生成** (`generate_query`) - 音声合成用クエリの作成
|
|
61
|
+
- **ファイル生成** (`synthesize_file`) - クエリから音声ファイルを生成
|
|
80
62
|
|
|
81
63
|
## 環境変数
|
|
82
64
|
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const types_1 = require("./voicevox/types");
|
|
|
10
10
|
Object.defineProperty(exports, "VoicevoxError", { enumerable: true, get: function () { return types_1.VoicevoxError; } });
|
|
11
11
|
const server = new mcp_js_1.McpServer({
|
|
12
12
|
name: "MCP TTS Voicevox",
|
|
13
|
-
version: "0.0.
|
|
13
|
+
version: "0.0.7",
|
|
14
14
|
description: "A Voicevox server that converts text to speech for playback and saving.",
|
|
15
15
|
});
|
|
16
16
|
// VoicevoxClientを一度だけインスタンス化
|
package/dist/voicevox/api.js
CHANGED
|
@@ -16,7 +16,7 @@ class VoicevoxApi {
|
|
|
16
16
|
*/
|
|
17
17
|
async generateQuery(text, speaker = 1) {
|
|
18
18
|
try {
|
|
19
|
-
const endpoint = `/audio_query?text=${encodeURIComponent(text)}&speaker=${speaker}`;
|
|
19
|
+
const endpoint = `/audio_query?text=${encodeURIComponent(text)}&speaker=${encodeURIComponent(speaker.toString())}`;
|
|
20
20
|
const query = await this.makeRequest("post", endpoint, null, {
|
|
21
21
|
"Content-Type": "application/json",
|
|
22
22
|
});
|
|
@@ -31,7 +31,7 @@ class VoicevoxApi {
|
|
|
31
31
|
*/
|
|
32
32
|
async synthesize(query, speaker = 1) {
|
|
33
33
|
try {
|
|
34
|
-
return await this.makeRequest("post", `/synthesis?speaker=${speaker}`, query, {
|
|
34
|
+
return await this.makeRequest("post", `/synthesis?speaker=${encodeURIComponent(speaker.toString())}`, query, {
|
|
35
35
|
"Content-Type": "application/json",
|
|
36
36
|
Accept: "audio/wav",
|
|
37
37
|
}, "arraybuffer");
|
|
@@ -45,7 +45,7 @@ class VoicevoxApi {
|
|
|
45
45
|
*/
|
|
46
46
|
async generateQueryFromPreset(text, presetId, coreVersion) {
|
|
47
47
|
try {
|
|
48
|
-
let endpoint = `/audio_query_from_preset?text=${encodeURIComponent(text)}&preset_id=${presetId}`;
|
|
48
|
+
let endpoint = `/audio_query_from_preset?text=${encodeURIComponent(text)}&preset_id=${encodeURIComponent(presetId.toString())}`;
|
|
49
49
|
if (coreVersion) {
|
|
50
50
|
endpoint += `&core_version=${encodeURIComponent(coreVersion)}`;
|
|
51
51
|
}
|
package/dist/voicevox/error.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ErrorHandler = exports.VoicevoxError = exports.VoicevoxErrorCode = void 0;
|
|
3
4
|
exports.handleError = handleError;
|
|
4
5
|
exports.formatError = formatError;
|
|
5
6
|
/**
|
|
@@ -24,3 +25,91 @@ function formatError(message, error) {
|
|
|
24
25
|
console.error(`${message}: ${errorMsg}`, error);
|
|
25
26
|
return `${message}: ${errorMsg}`;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* VOICEVOX関連のエラーコード
|
|
30
|
+
*/
|
|
31
|
+
var VoicevoxErrorCode;
|
|
32
|
+
(function (VoicevoxErrorCode) {
|
|
33
|
+
VoicevoxErrorCode["API_CONNECTION_ERROR"] = "api_connection_error";
|
|
34
|
+
VoicevoxErrorCode["QUERY_GENERATION_ERROR"] = "query_generation_error";
|
|
35
|
+
VoicevoxErrorCode["SYNTHESIS_ERROR"] = "synthesis_error";
|
|
36
|
+
VoicevoxErrorCode["FILE_OPERATION_ERROR"] = "file_operation_error";
|
|
37
|
+
VoicevoxErrorCode["PLAYBACK_ERROR"] = "playback_error";
|
|
38
|
+
VoicevoxErrorCode["QUEUE_OPERATION_ERROR"] = "queue_operation_error";
|
|
39
|
+
VoicevoxErrorCode["UNKNOWN_ERROR"] = "unknown_error";
|
|
40
|
+
})(VoicevoxErrorCode || (exports.VoicevoxErrorCode = VoicevoxErrorCode = {}));
|
|
41
|
+
/**
|
|
42
|
+
* VOICEVOXエラークラス
|
|
43
|
+
*/
|
|
44
|
+
class VoicevoxError extends Error {
|
|
45
|
+
constructor(message, code = VoicevoxErrorCode.UNKNOWN_ERROR, originalError) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "VoicevoxError";
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.originalError = originalError;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* エラー発生箇所とスタックトレースを含むエラーの詳細情報を取得
|
|
53
|
+
*/
|
|
54
|
+
getDetailedMessage() {
|
|
55
|
+
let details = `${this.message} [${this.code}]`;
|
|
56
|
+
if (this.originalError instanceof Error) {
|
|
57
|
+
details += `\nOriginal Error: ${this.originalError.message}`;
|
|
58
|
+
if (this.originalError.stack) {
|
|
59
|
+
details += `\nStack: ${this.originalError.stack}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return details;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.VoicevoxError = VoicevoxError;
|
|
66
|
+
/**
|
|
67
|
+
* エラーハンドリングユーティリティクラス
|
|
68
|
+
* アプリケーション全体で統一されたエラーハンドリングを提供
|
|
69
|
+
*/
|
|
70
|
+
class ErrorHandler {
|
|
71
|
+
/**
|
|
72
|
+
* エラーをVoicevoxError形式に変換して例外をスロー
|
|
73
|
+
*/
|
|
74
|
+
static throw(message, code = VoicevoxErrorCode.UNKNOWN_ERROR, originalError) {
|
|
75
|
+
console.error(`[${code}] ${message}`, originalError);
|
|
76
|
+
throw new VoicevoxError(message, code, originalError);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* APIエラーを処理
|
|
80
|
+
*/
|
|
81
|
+
static handleApiError(message, error) {
|
|
82
|
+
return this.throw(message, VoicevoxErrorCode.API_CONNECTION_ERROR, error);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* クエリ生成エラーを処理
|
|
86
|
+
*/
|
|
87
|
+
static handleQueryGenerationError(message, error) {
|
|
88
|
+
return this.throw(message, VoicevoxErrorCode.QUERY_GENERATION_ERROR, error);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 音声合成エラーを処理
|
|
92
|
+
*/
|
|
93
|
+
static handleSynthesisError(message, error) {
|
|
94
|
+
return this.throw(message, VoicevoxErrorCode.SYNTHESIS_ERROR, error);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* ファイル操作エラーを処理
|
|
98
|
+
*/
|
|
99
|
+
static handleFileError(message, error) {
|
|
100
|
+
return this.throw(message, VoicevoxErrorCode.FILE_OPERATION_ERROR, error);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 再生エラーを処理
|
|
104
|
+
*/
|
|
105
|
+
static handlePlaybackError(message, error) {
|
|
106
|
+
return this.throw(message, VoicevoxErrorCode.PLAYBACK_ERROR, error);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* キュー操作エラーを処理
|
|
110
|
+
*/
|
|
111
|
+
static handleQueueError(message, error) {
|
|
112
|
+
return this.throw(message, VoicevoxErrorCode.QUEUE_OPERATION_ERROR, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.ErrorHandler = ErrorHandler;
|
package/dist/voicevox/index.js
CHANGED
|
@@ -18,12 +18,14 @@ exports.VoicevoxClient = void 0;
|
|
|
18
18
|
const player_1 = require("./player");
|
|
19
19
|
const utils_1 = require("./utils");
|
|
20
20
|
const error_1 = require("./error");
|
|
21
|
+
const api_1 = require("./api");
|
|
21
22
|
class VoicevoxClient {
|
|
22
23
|
constructor(config) {
|
|
23
24
|
this.validateConfig(config);
|
|
24
25
|
this.defaultSpeaker = config.defaultSpeaker ?? 1;
|
|
25
26
|
this.defaultSpeedScale = config.defaultSpeedScale ?? 1.0;
|
|
26
27
|
this.maxSegmentLength = 150;
|
|
28
|
+
this.api = new api_1.VoicevoxApi(config.url);
|
|
27
29
|
this.player = new player_1.VoicevoxPlayer(config.url);
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
@@ -38,10 +40,30 @@ class VoicevoxClient {
|
|
|
38
40
|
const speakerId = this.getSpeakerId(speaker);
|
|
39
41
|
const speed = this.getSpeedScale(speedScale);
|
|
40
42
|
const segments = (0, utils_1.splitText)(text, this.maxSegmentLength);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const queueManager = this.player.getQueueManager();
|
|
44
|
+
if (segments.length === 0) {
|
|
45
|
+
return "テキストが空です";
|
|
46
|
+
}
|
|
47
|
+
// 最初のセグメントを優先的に処理して再生を早く開始
|
|
48
|
+
if (segments.length > 0) {
|
|
49
|
+
const firstQuery = await this.generateQuery(segments[0], speakerId);
|
|
50
|
+
firstQuery.speedScale = speed;
|
|
51
|
+
await queueManager.enqueueQuery(firstQuery, speakerId);
|
|
52
|
+
}
|
|
53
|
+
// 残りのセグメントは非同期で処理
|
|
54
|
+
if (segments.length > 1) {
|
|
55
|
+
// 残りのセグメントを非同期で処理する関数
|
|
56
|
+
const processRemainingSegments = async () => {
|
|
57
|
+
for (let i = 1; i < segments.length; i++) {
|
|
58
|
+
const query = await this.generateQuery(segments[i], speakerId);
|
|
59
|
+
query.speedScale = speed;
|
|
60
|
+
await queueManager.enqueueQuery(query, speakerId);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// 非同期で開始するが結果を待たない
|
|
64
|
+
processRemainingSegments().catch((error) => {
|
|
65
|
+
console.error("残りのセグメント処理中にエラーが発生しました:", error);
|
|
66
|
+
});
|
|
45
67
|
}
|
|
46
68
|
return `音声生成キューに追加しました: ${text}`;
|
|
47
69
|
}
|
|
@@ -59,7 +81,8 @@ class VoicevoxClient {
|
|
|
59
81
|
async generateQuery(text, speaker, speedScale) {
|
|
60
82
|
try {
|
|
61
83
|
const speakerId = this.getSpeakerId(speaker);
|
|
62
|
-
|
|
84
|
+
// 直接APIを使用してクエリを生成
|
|
85
|
+
const query = await this.api.generateQuery(text, speakerId);
|
|
63
86
|
query.speedScale = this.getSpeedScale(speedScale);
|
|
64
87
|
return query;
|
|
65
88
|
}
|
|
@@ -79,14 +102,33 @@ class VoicevoxClient {
|
|
|
79
102
|
try {
|
|
80
103
|
const speakerId = this.getSpeakerId(speaker);
|
|
81
104
|
const speed = this.getSpeedScale(speedScale);
|
|
105
|
+
// キューマネージャーにアクセス
|
|
106
|
+
const queueManager = this.player.getQueueManager();
|
|
82
107
|
if (typeof textOrQuery === "string") {
|
|
108
|
+
// テキストからクエリを生成
|
|
83
109
|
const query = await this.generateQuery(textOrQuery, speakerId);
|
|
84
110
|
query.speedScale = speed;
|
|
85
|
-
|
|
111
|
+
// 直接APIで音声合成
|
|
112
|
+
const audioData = await this.api.synthesize(query, speakerId);
|
|
113
|
+
// 一時ファイル保存またはパス指定の保存
|
|
114
|
+
if (!outputPath) {
|
|
115
|
+
return await queueManager.saveTempAudioFile(audioData);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return await this.player.synthesizeToFile(query, outputPath, speakerId);
|
|
119
|
+
}
|
|
86
120
|
}
|
|
87
121
|
else {
|
|
122
|
+
// クエリを使って音声合成
|
|
88
123
|
const query = { ...textOrQuery, speedScale: speed };
|
|
89
|
-
|
|
124
|
+
const audioData = await this.api.synthesize(query, speakerId);
|
|
125
|
+
// 一時ファイル保存またはパス指定の保存
|
|
126
|
+
if (!outputPath) {
|
|
127
|
+
return await queueManager.saveTempAudioFile(audioData);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return await this.player.synthesizeToFile(query, outputPath, speakerId);
|
|
131
|
+
}
|
|
90
132
|
}
|
|
91
133
|
}
|
|
92
134
|
catch (error) {
|
|
@@ -98,12 +140,40 @@ class VoicevoxClient {
|
|
|
98
140
|
try {
|
|
99
141
|
const speakerId = this.getSpeakerId(speaker);
|
|
100
142
|
const speed = this.getSpeedScale(speedScale);
|
|
143
|
+
const queueManager = this.player.getQueueManager();
|
|
101
144
|
if (typeof textOrQuery === "string") {
|
|
102
|
-
|
|
145
|
+
// テキストの場合:分割して処理
|
|
146
|
+
const segments = (0, utils_1.splitText)(textOrQuery, this.maxSegmentLength);
|
|
147
|
+
if (segments.length === 0) {
|
|
148
|
+
return "テキストが空です";
|
|
149
|
+
}
|
|
150
|
+
// 最初のセグメントを優先処理
|
|
151
|
+
if (segments.length > 0) {
|
|
152
|
+
const firstQuery = await this.generateQuery(segments[0], speakerId);
|
|
153
|
+
firstQuery.speedScale = speed;
|
|
154
|
+
await queueManager.enqueueQuery(firstQuery, speakerId);
|
|
155
|
+
}
|
|
156
|
+
// 残りのセグメントは非同期で処理
|
|
157
|
+
if (segments.length > 1) {
|
|
158
|
+
// 非同期で処理する関数
|
|
159
|
+
const processRemainingSegments = async () => {
|
|
160
|
+
for (let i = 1; i < segments.length; i++) {
|
|
161
|
+
const query = await this.generateQuery(segments[i], speakerId);
|
|
162
|
+
query.speedScale = speed;
|
|
163
|
+
await queueManager.enqueueQuery(query, speakerId);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
// 非同期で開始するが結果を待たない
|
|
167
|
+
processRemainingSegments().catch((error) => {
|
|
168
|
+
console.error("残りのセグメント処理中にエラーが発生しました:", error);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return `テキストをキューに追加しました: ${textOrQuery}`;
|
|
103
172
|
}
|
|
104
173
|
else {
|
|
174
|
+
// クエリをキューに追加
|
|
105
175
|
const query = { ...textOrQuery, speedScale: speed };
|
|
106
|
-
await
|
|
176
|
+
await queueManager.enqueueQuery(query, speakerId);
|
|
107
177
|
return "クエリをキューに追加しました";
|
|
108
178
|
}
|
|
109
179
|
}
|
package/dist/voicevox/player.js
CHANGED
|
@@ -1,47 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.VoicevoxPlayer = void 0;
|
|
37
4
|
const queue_1 = require("./queue");
|
|
38
5
|
const error_1 = require("./error");
|
|
39
6
|
const api_1 = require("./api");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
7
|
+
/**
|
|
8
|
+
* エラーハンドラープロキシ
|
|
9
|
+
* メソッドをエラーハンドリングで包む高階関数
|
|
10
|
+
*/
|
|
11
|
+
function withErrorHandling(method, errorMessage) {
|
|
12
|
+
return method().catch((error) => {
|
|
13
|
+
throw (0, error_1.handleError)(errorMessage, error);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
45
16
|
/**
|
|
46
17
|
* VOICEVOX音声プレイヤークラス
|
|
47
18
|
* キュー管理システムを使用して音声の合成と再生を行う
|
|
@@ -54,9 +25,9 @@ class VoicevoxPlayer {
|
|
|
54
25
|
*/
|
|
55
26
|
constructor(voicevoxUrl = "http://localhost:50021", prefetchSize = 2) {
|
|
56
27
|
// APIインスタンスを作成
|
|
57
|
-
|
|
28
|
+
const api = new api_1.VoicevoxApi(voicevoxUrl);
|
|
58
29
|
// キューマネージャーにAPIインスタンスを注入
|
|
59
|
-
this.queueManager = new queue_1.VoicevoxQueueManager(
|
|
30
|
+
this.queueManager = new queue_1.VoicevoxQueueManager(api, prefetchSize);
|
|
60
31
|
// デフォルトで再生を開始
|
|
61
32
|
this.queueManager.startPlayback();
|
|
62
33
|
// エラーイベントのログ記録
|
|
@@ -72,12 +43,9 @@ class VoicevoxPlayer {
|
|
|
72
43
|
* @param speaker 話者ID
|
|
73
44
|
*/
|
|
74
45
|
async enqueue(text, speaker = 1) {
|
|
75
|
-
|
|
46
|
+
return withErrorHandling(async () => {
|
|
76
47
|
await this.queueManager.enqueueText(text, speaker);
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
(0, error_1.handleError)("キューへの追加中にエラーが発生しました", error);
|
|
80
|
-
}
|
|
48
|
+
}, "テキストのキュー追加中にエラーが発生しました");
|
|
81
49
|
}
|
|
82
50
|
/**
|
|
83
51
|
* クエリを使ってキューに追加
|
|
@@ -85,127 +53,65 @@ class VoicevoxPlayer {
|
|
|
85
53
|
* @param speaker 話者ID
|
|
86
54
|
*/
|
|
87
55
|
async enqueueWithQuery(query, speaker = 1) {
|
|
88
|
-
|
|
56
|
+
return withErrorHandling(async () => {
|
|
89
57
|
await this.queueManager.enqueueQuery(query, speaker);
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
(0, error_1.handleError)("クエリからの音声生成中にエラーが発生しました", error);
|
|
93
|
-
}
|
|
58
|
+
}, "クエリのキュー追加中にエラーが発生しました");
|
|
94
59
|
}
|
|
95
60
|
/**
|
|
96
61
|
* テキストから音声合成用クエリを生成
|
|
62
|
+
* キューマネージャーの内部機能を使用
|
|
97
63
|
* @param text 合成するテキスト
|
|
98
64
|
* @param speaker 話者ID
|
|
99
65
|
*/
|
|
100
66
|
async generateQuery(text, speaker = 1) {
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let retryCount = 0;
|
|
108
|
-
return await new Promise((resolve, reject) => {
|
|
109
|
-
const checkQuery = () => {
|
|
110
|
-
const status = this.queueManager.getItemStatus(item.id);
|
|
111
|
-
const currentItem = this.queueManager
|
|
112
|
-
.getQueue()
|
|
113
|
-
.find((i) => i.id === item.id);
|
|
114
|
-
if (currentItem?.query) {
|
|
115
|
-
// クエリが取得できたらキューから削除して返す
|
|
116
|
-
const query = { ...currentItem.query };
|
|
117
|
-
this.queueManager.removeItem(item.id);
|
|
118
|
-
resolve(query);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (status === queue_1.QueueItemStatus.ERROR) {
|
|
122
|
-
this.queueManager.removeItem(item.id);
|
|
123
|
-
reject(new Error("クエリの生成に失敗しました"));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (retryCount >= maxRetries) {
|
|
127
|
-
this.queueManager.removeItem(item.id);
|
|
128
|
-
reject(new Error("クエリの生成がタイムアウトしました"));
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
retryCount++;
|
|
132
|
-
setTimeout(checkQuery, retryInterval);
|
|
133
|
-
};
|
|
134
|
-
checkQuery();
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
throw (0, error_1.handleError)("音声クエリ生成中にエラーが発生しました", error);
|
|
139
|
-
}
|
|
67
|
+
return withErrorHandling(async () => {
|
|
68
|
+
// AudioGeneratorのgenerateQueryを直接呼び出す
|
|
69
|
+
return await this.queueManager
|
|
70
|
+
.getAudioGenerator()
|
|
71
|
+
.generateQuery(text, speaker);
|
|
72
|
+
}, "音声合成クエリの生成中にエラーが発生しました");
|
|
140
73
|
}
|
|
141
74
|
/**
|
|
142
75
|
* 音声合成用クエリから音声ファイルを生成
|
|
76
|
+
* 重複を避けるためにQueueManagerの機能に委譲
|
|
77
|
+
*
|
|
143
78
|
* @param query 音声合成用クエリ
|
|
144
79
|
* @param output 出力ファイルパスまたは出力ディレクトリ(省略時は一時ディレクトリに生成)
|
|
145
80
|
* @param speaker 話者ID
|
|
146
81
|
*/
|
|
147
82
|
async synthesizeToFile(query, output, speaker = 1) {
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
|
|
83
|
+
return withErrorHandling(async () => {
|
|
84
|
+
// クエリから音声を合成
|
|
85
|
+
const api = this.queueManager.getApi();
|
|
86
|
+
const audioData = await api.synthesize(query, speaker);
|
|
87
|
+
// ファイル保存処理をFileManagerに完全に委譲
|
|
88
|
+
const fileManager = this.queueManager.getFileManager();
|
|
89
|
+
// 出力パスが指定されていない場合は一時ファイル、指定されている場合は指定パスに保存
|
|
152
90
|
if (!output) {
|
|
153
|
-
|
|
154
|
-
await fsPromises.writeFile(tempFilePath, Buffer.from(audioData));
|
|
155
|
-
return tempFilePath;
|
|
91
|
+
return await fileManager.saveTempAudioFile(audioData);
|
|
156
92
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
let isDir = false;
|
|
160
|
-
try {
|
|
161
|
-
const outputStat = await (0, promises_1.stat)(output);
|
|
162
|
-
isDir = outputStat.isDirectory();
|
|
93
|
+
else {
|
|
94
|
+
return await fileManager.saveAudioFile(audioData, output);
|
|
163
95
|
}
|
|
164
|
-
|
|
165
|
-
// ファイルまたはディレクトリが存在しない場合
|
|
166
|
-
// 末尾がスラッシュで終わる場合はディレクトリと見なす
|
|
167
|
-
isDir = output.endsWith("/") || output.endsWith("\\");
|
|
168
|
-
}
|
|
169
|
-
// ディレクトリの場合、ファイル名を生成
|
|
170
|
-
if (isDir) {
|
|
171
|
-
const filename = `voice-${(0, uuid_1.v4)()}.wav`;
|
|
172
|
-
targetPath = (0, path_1.join)(output, filename);
|
|
173
|
-
}
|
|
174
|
-
// 出力ディレクトリが存在するか確認し、存在しない場合は作成
|
|
175
|
-
await fsPromises.mkdir((0, path_1.dirname)(targetPath), { recursive: true });
|
|
176
|
-
// 音声データを指定された出力先に書き込み
|
|
177
|
-
await fsPromises.writeFile(targetPath, Buffer.from(audioData));
|
|
178
|
-
return targetPath;
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
throw (0, error_1.handleError)("音声ファイル生成中にエラーが発生しました", error);
|
|
182
|
-
}
|
|
96
|
+
}, "音声ファイル生成中にエラーが発生しました");
|
|
183
97
|
}
|
|
184
98
|
/**
|
|
185
|
-
*
|
|
186
|
-
* @returns 一時ファイルのパス
|
|
99
|
+
* 再生を開始
|
|
187
100
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return (0, path_1.join)((0, os_1.tmpdir)(), uniqueFilename);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* キューをクリア
|
|
194
|
-
*/
|
|
195
|
-
clearQueue() {
|
|
196
|
-
this.queueManager.clearQueue();
|
|
101
|
+
startPlayback() {
|
|
102
|
+
this.queueManager.startPlayback();
|
|
197
103
|
}
|
|
198
104
|
/**
|
|
199
105
|
* 再生を一時停止
|
|
200
106
|
*/
|
|
201
107
|
pausePlayback() {
|
|
202
|
-
|
|
108
|
+
this.queueManager.pausePlayback();
|
|
203
109
|
}
|
|
204
110
|
/**
|
|
205
111
|
* 再生を再開
|
|
206
112
|
*/
|
|
207
113
|
resumePlayback() {
|
|
208
|
-
|
|
114
|
+
this.queueManager.resumePlayback();
|
|
209
115
|
}
|
|
210
116
|
/**
|
|
211
117
|
* キュー内のアイテム数を取得
|
|
@@ -214,17 +120,25 @@ class VoicevoxPlayer {
|
|
|
214
120
|
return this.queueManager.getQueue().length;
|
|
215
121
|
}
|
|
216
122
|
/**
|
|
217
|
-
*
|
|
218
|
-
|
|
123
|
+
* キューが空かどうかを確認
|
|
124
|
+
*/
|
|
125
|
+
isQueueEmpty() {
|
|
126
|
+
return this.queueManager.getQueue().length === 0;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* キューが再生中かどうかを確認
|
|
219
130
|
*/
|
|
220
|
-
|
|
221
|
-
this.queueManager
|
|
131
|
+
isPlaying() {
|
|
132
|
+
return this.queueManager
|
|
133
|
+
.getQueue()
|
|
134
|
+
.some((item) => item.status === queue_1.QueueItemStatus.PLAYING);
|
|
222
135
|
}
|
|
223
136
|
/**
|
|
224
|
-
*
|
|
137
|
+
* キューマネージャーインスタンスを取得
|
|
138
|
+
* 高度な操作のため公開
|
|
225
139
|
*/
|
|
226
|
-
|
|
227
|
-
this.queueManager
|
|
140
|
+
getQueueManager() {
|
|
141
|
+
return this.queueManager;
|
|
228
142
|
}
|
|
229
143
|
}
|
|
230
144
|
exports.VoicevoxPlayer = VoicevoxPlayer;
|