@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 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
- return new Uint8Array(buffer);
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);
@@ -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;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@siduri-x/voice",
3
3
  "organType": "voice",
4
- "version": "1.0.1",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/voice",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {