@siduri-x/voice 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +14 -0
- package/dist/index.js +44 -1
- package/dist/index.test.js +28 -0
- package/organ-manifest.json +26 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { VoiceOrgan, AudioEvent, ExperienceAdapter, ExperienceEvent, ExperienceAdapterResult } from '@siduri-x/core';
|
|
2
|
+
export interface RvcPostProcessorConfig {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
serviceUrl?: string;
|
|
5
|
+
modelName?: string;
|
|
6
|
+
modelPath?: string;
|
|
7
|
+
indexPath?: string;
|
|
8
|
+
pitchShift?: number;
|
|
9
|
+
f0Method?: 'rmvpe' | 'pm' | 'harvest' | 'crepe';
|
|
10
|
+
indexRate?: number;
|
|
11
|
+
filterRadius?: number;
|
|
12
|
+
protect?: number;
|
|
13
|
+
}
|
|
2
14
|
export interface VoicevoxConfig {
|
|
3
15
|
baseUrl: string;
|
|
4
16
|
speakerId: number;
|
|
5
17
|
maxQueueDepth?: number;
|
|
6
18
|
timeoutMs?: number;
|
|
7
19
|
maxTextLength?: number;
|
|
20
|
+
rvc?: RvcPostProcessorConfig;
|
|
8
21
|
}
|
|
9
22
|
export declare class VoicevoxAdapter implements VoiceOrgan, ExperienceAdapter {
|
|
10
23
|
private config;
|
|
@@ -28,4 +41,5 @@ export declare class VoicevoxAdapter implements VoiceOrgan, ExperienceAdapter {
|
|
|
28
41
|
private emit;
|
|
29
42
|
private processQueue;
|
|
30
43
|
synthesize(text: string): Promise<Uint8Array>;
|
|
44
|
+
applyRvc(inputWav: Uint8Array): Promise<Uint8Array>;
|
|
31
45
|
}
|
package/dist/index.js
CHANGED
|
@@ -163,7 +163,50 @@ class VoicevoxAdapter {
|
|
|
163
163
|
throw new Error(`Voicevox synthesis failed: ${synthResponse.statusText}`);
|
|
164
164
|
}
|
|
165
165
|
const buffer = await synthResponse.arrayBuffer();
|
|
166
|
-
|
|
166
|
+
const rawWav = new Uint8Array(buffer);
|
|
167
|
+
if (this.config.rvc?.enabled && (this.config.rvc?.serviceUrl || process.env.RVC_SERVICE_URL || this.config.rvc?.modelName || this.config.rvc?.modelPath)) {
|
|
168
|
+
return await this.applyRvc(rawWav);
|
|
169
|
+
}
|
|
170
|
+
return rawWav;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async applyRvc(inputWav) {
|
|
177
|
+
const rvcConfig = this.config.rvc || {};
|
|
178
|
+
const serviceUrl = rvcConfig.serviceUrl || process.env.RVC_SERVICE_URL || 'http://localhost:50055';
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
181
|
+
try {
|
|
182
|
+
const formData = new FormData();
|
|
183
|
+
const blob = new Blob([inputWav.buffer], { type: 'audio/wav' });
|
|
184
|
+
formData.append('audio', blob, 'input.wav');
|
|
185
|
+
if (rvcConfig.modelName)
|
|
186
|
+
formData.append('model', rvcConfig.modelName);
|
|
187
|
+
if (rvcConfig.modelPath)
|
|
188
|
+
formData.append('model_path', rvcConfig.modelPath);
|
|
189
|
+
if (rvcConfig.indexPath)
|
|
190
|
+
formData.append('index_path', rvcConfig.indexPath);
|
|
191
|
+
formData.append('pitch_shift', String(rvcConfig.pitchShift ?? 0));
|
|
192
|
+
formData.append('f0_method', rvcConfig.f0Method ?? 'rmvpe');
|
|
193
|
+
formData.append('index_rate', String(rvcConfig.indexRate ?? 0.75));
|
|
194
|
+
const convertUrl = new URL('/convert', serviceUrl);
|
|
195
|
+
const res = await fetch(convertUrl.toString(), {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
body: formData,
|
|
198
|
+
signal: controller.signal,
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
console.warn(`[VoicevoxAdapter] RVC conversion failed (${res.status}): fallback to base audio.`);
|
|
202
|
+
return inputWav;
|
|
203
|
+
}
|
|
204
|
+
const convertedBuffer = await res.arrayBuffer();
|
|
205
|
+
return new Uint8Array(convertedBuffer);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
console.warn(`[VoicevoxAdapter] RVC service error (${e.message}): fallback to base audio.`);
|
|
209
|
+
return inputWav;
|
|
167
210
|
}
|
|
168
211
|
finally {
|
|
169
212
|
clearTimeout(timer);
|
package/dist/index.test.js
CHANGED
|
@@ -41,6 +41,34 @@ describe('VoicevoxAdapter Queue Semantics', () => {
|
|
|
41
41
|
expect(synthCall[0]).toContain("/synthesis?speaker=1");
|
|
42
42
|
expect(JSON.parse(synthCall[1].body).some).toBe("query_data");
|
|
43
43
|
});
|
|
44
|
+
test('synthesize chains RVC post-processing when configured', async () => {
|
|
45
|
+
const rvcAdapter = new index_1.VoicevoxAdapter({
|
|
46
|
+
baseUrl: 'http://localhost:50021',
|
|
47
|
+
speakerId: 1,
|
|
48
|
+
rvc: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
serviceUrl: 'http://localhost:50055',
|
|
51
|
+
modelName: 'sparkle',
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
global.fetch.mockResolvedValueOnce({
|
|
55
|
+
ok: true,
|
|
56
|
+
json: async () => ({ some: "query_data" })
|
|
57
|
+
});
|
|
58
|
+
global.fetch.mockResolvedValueOnce({
|
|
59
|
+
ok: true,
|
|
60
|
+
arrayBuffer: async () => new ArrayBuffer(8)
|
|
61
|
+
});
|
|
62
|
+
global.fetch.mockResolvedValueOnce({
|
|
63
|
+
ok: true,
|
|
64
|
+
arrayBuffer: async () => new ArrayBuffer(16)
|
|
65
|
+
});
|
|
66
|
+
const audio = await rvcAdapter.synthesize("Hello with RVC");
|
|
67
|
+
expect(audio.length).toBe(16);
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledTimes(3);
|
|
69
|
+
const rvcCall = global.fetch.mock.calls[2];
|
|
70
|
+
expect(rvcCall[0]).toBe("http://localhost:50055/convert");
|
|
71
|
+
});
|
|
44
72
|
});
|
|
45
73
|
describe('VoicevoxAdapter T5 Experience Event Interface', () => {
|
|
46
74
|
let adapter;
|
package/organ-manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siduri-x/voice",
|
|
3
3
|
"organType": "voice",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"displayName": "Voice (VOICEVOX Speech Synthesis)",
|
|
6
6
|
"description": "Queued speech synthesis and audio rendering lifecycle adapter",
|
|
7
7
|
"entrypoint": "./dist/index.js",
|
|
@@ -31,6 +31,19 @@
|
|
|
31
31
|
"maxQueueDepth": {
|
|
32
32
|
"type": "number",
|
|
33
33
|
"default": 50
|
|
34
|
+
},
|
|
35
|
+
"rvc": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"enabled": { "type": "boolean", "default": false },
|
|
39
|
+
"serviceUrl": { "type": "string", "default": "http://localhost:50055" },
|
|
40
|
+
"modelName": { "type": "string" },
|
|
41
|
+
"modelPath": { "type": "string" },
|
|
42
|
+
"indexPath": { "type": "string" },
|
|
43
|
+
"pitchShift": { "type": "number", "default": 0 },
|
|
44
|
+
"f0Method": { "type": "string", "enum": ["rmvpe", "pm", "harvest", "crepe"], "default": "rmvpe" },
|
|
45
|
+
"indexRate": { "type": "number", "default": 0.75 }
|
|
46
|
+
}
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
},
|
|
@@ -41,6 +54,13 @@
|
|
|
41
54
|
"secret": false,
|
|
42
55
|
"default": "http://localhost:50021",
|
|
43
56
|
"description": "Base URL of the running VOICEVOX engine"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "RVC_SERVICE_URL",
|
|
60
|
+
"required": false,
|
|
61
|
+
"secret": false,
|
|
62
|
+
"default": "http://localhost:50055",
|
|
63
|
+
"description": "Base URL of headless RVC voice conversion microservice"
|
|
44
64
|
}
|
|
45
65
|
],
|
|
46
66
|
"services": [
|
|
@@ -48,6 +68,11 @@
|
|
|
48
68
|
"name": "VOICEVOX Engine",
|
|
49
69
|
"kind": "http_service",
|
|
50
70
|
"optional": false
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "RVC Headless Service",
|
|
74
|
+
"kind": "http_service",
|
|
75
|
+
"optional": true
|
|
51
76
|
}
|
|
52
77
|
],
|
|
53
78
|
"database": null,
|