@vxnus/siduri 0.1.0 → 0.1.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.
Files changed (38) hide show
  1. package/dist/builtin-manifests.js +26 -1
  2. package/dist/clean-machine-e2e.test.js +21 -13
  3. package/dist/configurators/voice.js +327 -20
  4. package/dist/configurators.test.js +2 -1
  5. package/dist/generator.d.ts +1 -0
  6. package/dist/generator.js +49 -2
  7. package/dist/generator.test.js +5 -2
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +7 -2
  10. package/dist/release-check.js +2 -2
  11. package/dist/web-dist/404/index.html +1 -1
  12. package/dist/web-dist/404.html +1 -1
  13. package/dist/web-dist/__next.__PAGE__.txt +3 -3
  14. package/dist/web-dist/__next._full.txt +3 -3
  15. package/dist/web-dist/__next._tree.txt +2 -2
  16. package/dist/web-dist/_next/static/chunks/1dc9n0p39yo65.css +1 -0
  17. package/dist/web-dist/_not-found/__next._full.txt +2 -2
  18. package/dist/web-dist/_not-found/__next._not-found.__PAGE__.txt +2 -2
  19. package/dist/web-dist/_not-found/__next._tree.txt +2 -2
  20. package/dist/web-dist/_not-found/index.html +1 -1
  21. package/dist/web-dist/_not-found/index.txt +2 -2
  22. package/dist/web-dist/chat/__next._full.txt +2 -2
  23. package/dist/web-dist/chat/__next._tree.txt +2 -2
  24. package/dist/web-dist/chat/__next.chat.__PAGE__.txt +2 -2
  25. package/dist/web-dist/chat/index.html +1 -1
  26. package/dist/web-dist/chat/index.txt +2 -2
  27. package/dist/web-dist/index.html +1 -1
  28. package/dist/web-dist/index.txt +3 -3
  29. package/dist/web-dist/operator/__next._full.txt +2 -2
  30. package/dist/web-dist/operator/__next._tree.txt +2 -2
  31. package/dist/web-dist/operator/__next.operator.__PAGE__.txt +2 -2
  32. package/dist/web-dist/operator/index.html +1 -1
  33. package/dist/web-dist/operator/index.txt +2 -2
  34. package/package.json +1 -1
  35. package/dist/web-dist/_next/static/chunks/2bu5_paf4sm3x.css +0 -1
  36. /package/dist/web-dist/_next/static/{_zOqKpMPc6RDqk04fUScI → i9YV06HVZxr6wJWWxhUhB}/_buildManifest.js +0 -0
  37. /package/dist/web-dist/_next/static/{_zOqKpMPc6RDqk04fUScI → i9YV06HVZxr6wJWWxhUhB}/_clientMiddlewareManifest.js +0 -0
  38. /package/dist/web-dist/_next/static/{_zOqKpMPc6RDqk04fUScI → i9YV06HVZxr6wJWWxhUhB}/_ssgManifest.js +0 -0
@@ -383,7 +383,7 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
383
383
  {
384
384
  name: '@siduri-x/voice',
385
385
  organType: 'voice',
386
- version: '1.0.1',
386
+ version: '1.0.2',
387
387
  displayName: 'Voice (VOICEVOX Speech Synthesis)',
388
388
  description: 'Queued speech synthesis and audio rendering lifecycle adapter',
389
389
  entrypoint: './dist/index.js',
@@ -408,6 +408,19 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
408
408
  maxQueueDepth: {
409
409
  type: 'number',
410
410
  default: 50
411
+ },
412
+ rvc: {
413
+ type: 'object',
414
+ properties: {
415
+ enabled: { type: 'boolean', default: false },
416
+ serviceUrl: { type: 'string', default: 'http://localhost:50055' },
417
+ modelName: { type: 'string' },
418
+ modelPath: { type: 'string' },
419
+ indexPath: { type: 'string' },
420
+ pitchShift: { type: 'number', default: 0 },
421
+ f0Method: { type: 'string', enum: ['rmvpe', 'pm', 'harvest', 'crepe'], default: 'rmvpe' },
422
+ indexRate: { type: 'number', default: 0.75 }
423
+ }
411
424
  }
412
425
  }
413
426
  },
@@ -418,6 +431,13 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
418
431
  secret: false,
419
432
  default: 'http://localhost:50021',
420
433
  description: 'Base URL of the running VOICEVOX engine'
434
+ },
435
+ {
436
+ name: 'RVC_SERVICE_URL',
437
+ required: false,
438
+ secret: false,
439
+ default: 'http://localhost:50055',
440
+ description: 'Base URL of headless RVC voice conversion microservice'
421
441
  }
422
442
  ],
423
443
  services: [
@@ -425,6 +445,11 @@ exports.BUILTIN_ORGAN_MANIFESTS = [
425
445
  name: 'VOICEVOX Engine',
426
446
  kind: 'http_service',
427
447
  optional: false
448
+ },
449
+ {
450
+ name: 'RVC Headless Service',
451
+ kind: 'http_service',
452
+ optional: true
428
453
  }
429
454
  ],
430
455
  database: null,
@@ -22,9 +22,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
22
22
  { filter: '@siduri-x/vision', tarName: 'siduri-x-vision-1.0.1.tgz', isOrgan: true },
23
23
  { filter: '@siduri-x/hands', tarName: 'siduri-x-hands-1.0.1.tgz', isOrgan: true },
24
24
  { filter: '@siduri-x/body', tarName: 'siduri-x-body-1.0.1.tgz', isOrgan: true },
25
- { filter: '@siduri-x/voice', tarName: 'siduri-x-voice-1.0.1.tgz', isOrgan: true },
25
+ { filter: '@siduri-x/voice', tarName: 'siduri-x-voice-1.0.2.tgz', isOrgan: true },
26
26
  { filter: '@siduri-x/observation', tarName: 'siduri-x-observation-1.0.1.tgz', isOrgan: true },
27
- { filter: '@vxnus/siduri', tarName: 'vxnus-siduri-0.1.0.tgz', isOrgan: false },
27
+ { filter: '@vxnus/siduri', tarName: 'vxnus-siduri-0.1.2.tgz', isOrgan: false },
28
28
  ];
29
29
  beforeAll(() => {
30
30
  // 1. Prepare clean directories
@@ -86,7 +86,7 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
86
86
  }
87
87
  });
88
88
  test('memory organ tarball packages migrations/001_initial_schema.sql', () => {
89
- const memoryTarPath = node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.0.tgz');
89
+ const memoryTarPath = node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.1.tgz');
90
90
  const listing = (0, node_child_process_1.execSync)(`tar -tzf ${memoryTarPath}`, { encoding: 'utf8' });
91
91
  expect(listing).toContain('package/migrations/001_initial_schema.sql');
92
92
  });
@@ -106,8 +106,8 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
106
106
  // Write instance files referencing packed tarballs directly for true clean machine install
107
107
  const pkgObj = JSON.parse(files['package.json']);
108
108
  pkgObj.dependencies = {
109
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
110
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
109
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.1.tgz')}`,
110
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.1.tgz')}`,
111
111
  };
112
112
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
113
113
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -123,7 +123,11 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
123
123
  expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/voice'))).toBe(false);
124
124
  expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'siduri-x-runtime.js'))).toBe(false);
125
125
  // 3. Run node src/index.js (starts cleanly without PostgreSQL or unselected organs)
126
- const startOutput = (0, node_child_process_1.execSync)('node src/index.js', { cwd: instanceDir, encoding: 'utf8' });
126
+ const startOutput = (0, node_child_process_1.execSync)('node src/index.js', {
127
+ cwd: instanceDir,
128
+ env: { ...process.env, PORT: '0' },
129
+ encoding: 'utf8',
130
+ });
127
131
  expect(startOutput).toContain('✓ Siduri [CleanBrainOnly] initialized with [Brain].');
128
132
  // 4. Verify doctor from standalone directory
129
133
  const doctorPass = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} doctor`, {
@@ -150,9 +154,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
150
154
  });
151
155
  const pkgObj = JSON.parse(files['package.json']);
152
156
  pkgObj.dependencies = {
153
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
154
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
155
- '@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.0.tgz')}`,
157
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.1.tgz')}`,
158
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.1.tgz')}`,
159
+ '@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.1.tgz')}`,
156
160
  };
157
161
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
158
162
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -162,7 +166,11 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
162
166
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'src/index.js'), files['src/index.js']);
163
167
  (0, node_child_process_1.execSync)('npm install --no-audit --no-fund', { cwd: instanceDir, stdio: 'pipe' });
164
168
  expect(node_fs_1.default.existsSync(node_path_1.default.join(instanceDir, 'node_modules/@siduri-x/memory'))).toBe(false);
165
- const startOutput = (0, node_child_process_1.execSync)('node src/index.js', { cwd: instanceDir, encoding: 'utf8' });
169
+ const startOutput = (0, node_child_process_1.execSync)('node src/index.js', {
170
+ cwd: instanceDir,
171
+ env: { ...process.env, PORT: '0' },
172
+ encoding: 'utf8',
173
+ });
166
174
  expect(startOutput).toContain('✓ Siduri [CleanBrainHands] initialized with [Brain, Hands].');
167
175
  const doctorOutput = (0, node_child_process_1.execSync)(`node ${node_path_1.default.resolve(repoRoot, 'cli/dist/index.js')} doctor`, {
168
176
  cwd: instanceDir,
@@ -185,9 +193,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
185
193
  });
186
194
  const pkgObj = JSON.parse(files['package.json']);
187
195
  pkgObj.dependencies = {
188
- '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.0.tgz')}`,
189
- '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.0.tgz')}`,
190
- '@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.0.tgz')}`,
196
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.1.tgz')}`,
197
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.1.tgz')}`,
198
+ '@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.1.tgz')}`,
191
199
  };
192
200
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
193
201
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -6,13 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.configureVoice = configureVoice;
7
7
  const inquirer_1 = __importDefault(require("inquirer"));
8
8
  async function configureVoice(_context) {
9
+ const companionSlug = _context.companionName.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'default';
9
10
  const { provider } = await inquirer_1.default.prompt({
10
11
  type: 'list',
11
12
  name: 'provider',
12
13
  message: 'Voice provider?',
13
14
  choices: [
14
- { name: 'VOICEVOX (Local high-fidelity neural speech synthesis engine)', value: 'voicevox' },
15
- { name: 'None (Disable voice synthesis)', value: 'none' },
15
+ {
16
+ name: 'RVC (Custom user-owned voice model via .pth / .index weights)',
17
+ value: 'rvc',
18
+ },
19
+ {
20
+ name: 'VOICEVOX (Standard VOICEVOX stock character voice banks)',
21
+ value: 'voicevox',
22
+ },
23
+ {
24
+ name: 'None (Disable voice synthesis)',
25
+ value: 'none',
26
+ },
16
27
  ],
17
28
  });
18
29
  if (provider === 'none') {
@@ -21,33 +32,329 @@ async function configureVoice(_context) {
21
32
  summary: { Provider: 'None (Voice disabled)' },
22
33
  };
23
34
  }
24
- const answers = await inquirer_1.default.prompt([
35
+ if (provider === 'rvc') {
36
+ const rvcAnswers = await inquirer_1.default.prompt([
37
+ {
38
+ type: 'input',
39
+ name: 'modelPath',
40
+ message: 'RVC Model Path (.pth weights):',
41
+ default: `./assets/voice/${companionSlug}/${companionSlug}.pth`,
42
+ },
43
+ {
44
+ type: 'input',
45
+ name: 'indexPath',
46
+ message: 'RVC Feature Index Path (.index, optional):',
47
+ default: `./assets/voice/${companionSlug}/${companionSlug}.index`,
48
+ },
49
+ {
50
+ type: 'input',
51
+ name: 'pitchShift',
52
+ message: 'Pitch Shift (semitones, e.g. 0 for neutral, 12 for male-to-female, -12 for female-to-male):',
53
+ default: '0',
54
+ validate: (v) => !isNaN(parseInt(v, 10)) || 'Pitch shift must be an integer.',
55
+ },
56
+ {
57
+ type: 'list',
58
+ name: 'f0Method',
59
+ message: 'Pitch Extraction Algorithm (F0 Method):',
60
+ choices: ['rmvpe', 'pm', 'harvest', 'crepe'],
61
+ default: 'rmvpe',
62
+ },
63
+ {
64
+ type: 'input',
65
+ name: 'baseUrl',
66
+ message: 'Base TTS Synthesis Engine URL (VOICEVOX):',
67
+ default: 'http://localhost:50021',
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'serviceUrl',
72
+ message: 'RVC Headless Service URL:',
73
+ default: 'http://localhost:50055',
74
+ },
75
+ ]);
76
+ const pitchShift = parseInt(rvcAnswers.pitchShift, 10) || 0;
77
+ const config = {
78
+ provider: 'voicevox',
79
+ baseUrl: rvcAnswers.baseUrl.trim(),
80
+ speakerId: 1,
81
+ maxQueueDepth: 50,
82
+ rvc: {
83
+ enabled: true,
84
+ serviceUrl: rvcAnswers.serviceUrl.trim(),
85
+ modelName: companionSlug,
86
+ modelPath: rvcAnswers.modelPath.trim(),
87
+ indexPath: rvcAnswers.indexPath.trim() || undefined,
88
+ pitchShift,
89
+ f0Method: rvcAnswers.f0Method,
90
+ indexRate: 0.75,
91
+ },
92
+ };
93
+ const summary = {
94
+ Provider: 'RVC (Custom Voice Model)',
95
+ 'Model Path': rvcAnswers.modelPath.trim(),
96
+ 'Pitch Shift': pitchShift,
97
+ 'F0 Method': rvcAnswers.f0Method,
98
+ 'RVC Service': rvcAnswers.serviceUrl.trim(),
99
+ 'Base TTS': rvcAnswers.baseUrl.trim(),
100
+ };
101
+ return {
102
+ config,
103
+ summary,
104
+ };
105
+ }
106
+ const DEFAULT_VOICEVOX_SPEAKERS = [
107
+ {
108
+ name: '四国めたん (Shikoku Metan)',
109
+ speaker_uuid: '7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff',
110
+ styles: [
111
+ { id: 2, name: 'ノーマル (Normal)' },
112
+ { id: 0, name: 'あまあま (Sweet)' },
113
+ { id: 4, name: 'ツンツン (Tsundere)' },
114
+ { id: 6, name: 'セクシー (Sexy)' },
115
+ { id: 36, name: 'ささやき (Whisper)' },
116
+ { id: 37, name: 'ヒソヒソ (Hisoniso)' },
117
+ ],
118
+ },
119
+ {
120
+ name: 'ずんだもん (Zundamon)',
121
+ speaker_uuid: '388f24ea-105d-4354-b46e-22d7d7ff4479',
122
+ styles: [
123
+ { id: 3, name: 'ノーマル (Normal)' },
124
+ { id: 1, name: 'あまあま (Sweet)' },
125
+ { id: 5, name: 'ツンツン (Tsundere)' },
126
+ { id: 7, name: 'セクシー (Sexy)' },
127
+ { id: 22, name: 'ささやき (Whisper)' },
128
+ { id: 38, name: 'ヒソヒソ (Hisoniso)' },
129
+ { id: 75, name: 'ヘロヘロ (Herohero)' },
130
+ { id: 76, name: 'なみだめ (Namidame)' },
131
+ ],
132
+ },
133
+ {
134
+ name: '春日部つむぎ (Kasukabe Tsumugi)',
135
+ speaker_uuid: '35b2c544-660e-401e-b033-0ab4756917cd',
136
+ styles: [{ id: 8, name: 'ノーマル (Normal)' }],
137
+ },
138
+ {
139
+ name: '雨晴はう (Rainy Hau)',
140
+ speaker_uuid: '3474ee95-c274-47f9-aa1a-8322163d77f1',
141
+ styles: [{ id: 10, name: 'ノーマル (Normal)' }],
142
+ },
143
+ {
144
+ name: '波音リツ (Namine Ritsu)',
145
+ speaker_uuid: 'b1a81618-b27b-40d2-b0ea-27a9ad408c4b',
146
+ styles: [{ id: 9, name: 'ノーマル (Normal)' }],
147
+ },
148
+ {
149
+ name: '玄野武宏 (Kurono Takehiro)',
150
+ speaker_uuid: 'c30dc15a-0992-4f8d-8bb8-ad3b76579016',
151
+ styles: [
152
+ { id: 11, name: 'ノーマル (Normal)' },
153
+ { id: 39, name: '喜び (Joy)' },
154
+ { id: 40, name: 'ツンツン (Tsundere)' },
155
+ { id: 41, name: '悲しみ (Sadness)' },
156
+ ],
157
+ },
158
+ {
159
+ name: '白上虎太郎 (Shirakami Kotaro)',
160
+ speaker_uuid: 'e50289b4-43ff-49e6-9b58-d894c23fb4ac',
161
+ styles: [
162
+ { id: 12, name: 'ふつう (Normal)' },
163
+ { id: 32, name: 'わーい (Yay)' },
164
+ { id: 33, name: 'びくびく (Timid)' },
165
+ { id: 34, name: 'おこ (Angry)' },
166
+ { id: 35, name: 'びえーん (Crying)' },
167
+ ],
168
+ },
169
+ {
170
+ name: '青山龍星 (Aoyama Ryusei)',
171
+ speaker_uuid: '4f51116a-d9ee-4516-925d-02f183e2af8d',
172
+ styles: [
173
+ { id: 13, name: 'ノーマル (Normal)' },
174
+ { id: 81, name: '熱血 (Passionate)' },
175
+ { id: 82, name: '不気味 (Eerie)' },
176
+ { id: 83, name: '囁き (Whisper)' },
177
+ ],
178
+ },
179
+ {
180
+ name: '冥鳴ひまり (Meimei Himari)',
181
+ speaker_uuid: '8ea4fcd4-a013-4095-abb3-11b5bf837d37',
182
+ styles: [{ id: 14, name: 'ノーマル (Normal)' }],
183
+ },
184
+ {
185
+ name: '九州そら (Kyushu Sora)',
186
+ speaker_uuid: '481fb609-6446-4870-9f46-90c4dd623403',
187
+ styles: [
188
+ { id: 16, name: 'ノーマル (Normal)' },
189
+ { id: 15, name: 'あまあま (Sweet)' },
190
+ { id: 17, name: 'ツンツン (Tsundere)' },
191
+ { id: 18, name: 'セクシー (Sexy)' },
192
+ { id: 19, name: 'ささやき (Whisper)' },
193
+ ],
194
+ },
195
+ {
196
+ name: 'モチノ・キョウコ (Mochino Kyoko)',
197
+ speaker_uuid: 'a402a5d2-085e-4b44-a034-7164ff24a3cf',
198
+ styles: [
199
+ { id: 20, name: 'ノーマル (Normal)' },
200
+ { id: 60, name: 'セクシー/タレント (Sexy)' },
201
+ { id: 61, name: '泣き (Crying)' },
202
+ ],
203
+ },
204
+ {
205
+ name: '剣崎雌雄 (Kenzaki Mesuo)',
206
+ speaker_uuid: '1a1756a4-263d-42ea-86a2-96f107f474e9',
207
+ styles: [{ id: 21, name: 'ノーマル (Normal)' }],
208
+ },
209
+ {
210
+ name: '後鬼 (Goki)',
211
+ speaker_uuid: '80991a54-61c4-4b5f-8700-11234c062085',
212
+ styles: [
213
+ { id: 27, name: '人間ver. (Human)' },
214
+ { id: 28, name: '鬼ver. (Oni)' },
215
+ ],
216
+ },
217
+ {
218
+ name: 'No.7 (Seven)',
219
+ speaker_uuid: 'c86c757c-cb24-4ea0-b6f7-33c872ff72e2',
220
+ styles: [
221
+ { id: 29, name: 'ノーマル (Normal)' },
222
+ { id: 30, name: 'アナウンス (Announce)' },
223
+ { id: 31, name: '読み聞かせ (Storytelling)' },
224
+ ],
225
+ },
226
+ {
227
+ name: 'ちび式じい (Chibishiki Jii)',
228
+ speaker_uuid: '1e19d52b-426b-4e12-b250-bfbe55c0c9cb',
229
+ styles: [{ id: 42, name: 'ノーマル (Normal)' }],
230
+ },
231
+ {
232
+ name: '櫻歌ミコ (Ouka Miko)',
233
+ speaker_uuid: '06155980-bfab-443b-a9b8-3e4b786c7ccb',
234
+ styles: [
235
+ { id: 43, name: 'ノーマル (Normal)' },
236
+ { id: 44, name: '第二形態 (2nd Form)' },
237
+ { id: 45, name: 'ロリ (Loli)' },
238
+ ],
239
+ },
240
+ {
241
+ name: '小夜/SAYO',
242
+ speaker_uuid: 'a8fb71e1-8738-4e16-b541-002f23ea3e45',
243
+ styles: [{ id: 46, name: 'ノーマル (Normal)' }],
244
+ },
245
+ {
246
+ name: 'ナースロボ_タイプT (Nurse Robot Type T)',
247
+ speaker_uuid: '882a636f-3bac-431a-966d-c5e6bba9f949',
248
+ styles: [
249
+ { id: 47, name: 'ノーマル (Normal)' },
250
+ { id: 48, name: '楽々 (Relaxed)' },
251
+ { id: 49, name: '恐怖 (Fear)' },
252
+ { id: 50, name: '内緒話 (Secret Talk)' },
253
+ ],
254
+ },
255
+ {
256
+ name: '猫使アル (Nekotsuka Aru)',
257
+ speaker_uuid: '7253579b-240e-4328-b0a6-574ab57597ea',
258
+ styles: [
259
+ { id: 55, name: 'ノーマル (Normal)' },
260
+ { id: 56, name: 'おちつき (Calm)' },
261
+ { id: 57, name: 'うきうき (Excited)' },
262
+ ],
263
+ },
264
+ {
265
+ name: '猫使ビィ (Nekotsuka Bi)',
266
+ speaker_uuid: 'cf6204c3-6316-43b9-a095-2eb49e29a914',
267
+ styles: [
268
+ { id: 58, name: 'ノーマル (Normal)' },
269
+ { id: 59, name: 'おちつき (Calm)' },
270
+ { id: 62, name: '人見知り (Shy)' },
271
+ ],
272
+ },
273
+ ];
274
+ async function getVoicevoxSpeakers(baseUrl) {
275
+ try {
276
+ const controller = new AbortController();
277
+ const timeout = setTimeout(() => controller.abort(), 2000);
278
+ const res = await fetch(new URL('/speakers', baseUrl).toString(), {
279
+ signal: controller.signal,
280
+ });
281
+ clearTimeout(timeout);
282
+ if (res.ok) {
283
+ const data = (await res.json());
284
+ if (Array.isArray(data) && data.length > 0) {
285
+ return data;
286
+ }
287
+ }
288
+ }
289
+ catch (_e) {
290
+ // If local instance is unreachable, fallback to the pre-populated list
291
+ }
292
+ return DEFAULT_VOICEVOX_SPEAKERS;
293
+ }
294
+ function buildSpeakerChoices(speakers) {
295
+ const choices = [];
296
+ for (const speaker of speakers) {
297
+ for (const style of speaker.styles) {
298
+ const label = `${speaker.name} - ${style.name} (ID: ${style.id})`;
299
+ choices.push({
300
+ name: label,
301
+ value: { id: style.id, label },
302
+ });
303
+ }
304
+ }
305
+ return choices;
306
+ }
307
+ // provider === 'voicevox'
308
+ const urlAnswer = await inquirer_1.default.prompt([
25
309
  {
26
310
  type: 'input',
27
311
  name: 'baseUrl',
28
312
  message: 'VOICEVOX Engine Base URL:',
29
313
  default: 'http://localhost:50021',
30
314
  },
315
+ ]);
316
+ const baseUrl = urlAnswer.baseUrl.trim() || 'http://localhost:50021';
317
+ const speakers = await getVoicevoxSpeakers(baseUrl);
318
+ const speakerChoices = buildSpeakerChoices(speakers);
319
+ const speakerAnswer = await inquirer_1.default.prompt([
31
320
  {
32
- type: 'input',
33
- name: 'speakerId',
34
- message: 'Default Speaker ID (e.g. 1 for Zundamon, 2 for Shikoku Metan):',
35
- default: '1',
36
- validate: (v) => !isNaN(parseInt(v, 10)) || 'Speaker ID must be an integer.',
321
+ type: 'list',
322
+ name: 'speaker',
323
+ message: 'Select VOICEVOX Character Voice Bank & Style:',
324
+ choices: speakerChoices,
325
+ pageSize: 15,
37
326
  },
38
327
  ]);
39
- const speakerId = parseInt(answers.speakerId, 10) || 1;
328
+ let speakerId = 1;
329
+ let speakerLabel = '四国めたん (Shikoku Metan) - ノーマル (Normal) (ID: 2)';
330
+ if (speakerAnswer && speakerAnswer.speaker) {
331
+ if (typeof speakerAnswer.speaker === 'object' && 'id' in speakerAnswer.speaker) {
332
+ speakerId = speakerAnswer.speaker.id;
333
+ speakerLabel = speakerAnswer.speaker.label;
334
+ }
335
+ else if (typeof speakerAnswer.speaker === 'number') {
336
+ speakerId = speakerAnswer.speaker;
337
+ speakerLabel = `Speaker ID: ${speakerId}`;
338
+ }
339
+ else if (typeof speakerAnswer.speaker === 'string') {
340
+ speakerId = parseInt(speakerAnswer.speaker, 10) || 1;
341
+ speakerLabel = `Speaker ID: ${speakerId}`;
342
+ }
343
+ }
344
+ const config = {
345
+ provider: 'voicevox',
346
+ baseUrl,
347
+ speakerId,
348
+ maxQueueDepth: 50,
349
+ };
350
+ const summary = {
351
+ Provider: 'VOICEVOX (Stock Voice Bank)',
352
+ 'Voice Bank': speakerLabel,
353
+ 'Speaker ID': speakerId,
354
+ 'Base URL': baseUrl,
355
+ };
40
356
  return {
41
- config: {
42
- provider: 'voicevox',
43
- baseUrl: answers.baseUrl.trim(),
44
- speakerId,
45
- maxQueueDepth: 50,
46
- },
47
- summary: {
48
- Provider: 'VOICEVOX',
49
- 'Speaker ID': speakerId,
50
- 'Base URL': answers.baseUrl.trim(),
51
- },
357
+ config,
358
+ summary,
52
359
  };
53
360
  }
@@ -191,7 +191,8 @@ describe('Guided Manifest-Driven Configuration UX Specification Tests', () => {
191
191
  test('Voice configurator configures VOICEVOX engine and speaker ID', async () => {
192
192
  inquirer_1.default.prompt
193
193
  .mockResolvedValueOnce({ provider: 'voicevox' })
194
- .mockResolvedValueOnce({ baseUrl: 'http://localhost:50021', speakerId: '2' });
194
+ .mockResolvedValueOnce({ baseUrl: 'http://localhost:50021' })
195
+ .mockResolvedValueOnce({ speaker: { id: 2, label: '四国めたん (Shikoku Metan) - ノーマル (Normal) (ID: 2)' } });
195
196
  const result = await (0, voice_1.configureVoice)({ companionName: 'Sparkle', manifest: voiceManifest });
196
197
  expect(result.config.provider).toBe('voicevox');
197
198
  expect(result.config.speakerId).toBe(2);
@@ -9,6 +9,7 @@ export interface GeneratedInstanceFiles {
9
9
  'public/index.html': string;
10
10
  'docker-compose.yml'?: string;
11
11
  createAssetsBodyDir?: boolean;
12
+ createAssetsDirs?: string[];
12
13
  }
13
14
  export interface InstanceGeneratorOptions {
14
15
  name: string;
package/dist/generator.js CHANGED
@@ -107,6 +107,18 @@ function generateInstanceFiles(options) {
107
107
  ' - "50021:50021"',
108
108
  ].join('\n'));
109
109
  }
110
+ if (hasVoice && voiceConfig?.rvc?.enabled) {
111
+ dockerServices.push([
112
+ ' rvc:',
113
+ ' image: ghcr.io/vxnus-studio/rvc-headless:latest',
114
+ ' ports:',
115
+ ' - "50055:50055"',
116
+ ' volumes:',
117
+ ' - ./assets/voice:/app/models',
118
+ ' environment:',
119
+ ' - RVC_MODELS_DIR=/app/models',
120
+ ].join('\n'));
121
+ }
110
122
  if (dockerServices.length > 0) {
111
123
  const composeLines = [
112
124
  'version: "3.8"',
@@ -201,7 +213,7 @@ function generateInstanceFiles(options) {
201
213
  // 6. src/index.js
202
214
  const importLines = [
203
215
  `import { createServer } from 'node:http';`,
204
- `import { readFile, stat } from 'node:fs/promises';`,
216
+ `import { readFile, stat, readdir } from 'node:fs/promises';`,
205
217
  `import path from 'node:path';`,
206
218
  `import { fileURLToPath } from 'node:url';`,
207
219
  `import { SiduriRuntime } from '@siduri-x/core';`,
@@ -367,6 +379,33 @@ function generateInstanceFiles(options) {
367
379
  ` return;`,
368
380
  ` }`,
369
381
  '',
382
+ ` // API: Model Catalog Discovery (Body & Voice)`,
383
+ ` if (pathname === '/api/models/body' && req.method === 'GET') {`,
384
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
385
+ ` try {`,
386
+ ` const bodyDir = path.join(rootDir, 'assets', 'body', 'model');`,
387
+ ` const entries = await readdir(bodyDir, { withFileTypes: true }).catch(() => []);`,
388
+ ` const models = entries.filter((e) => e.isDirectory()).map((e) => e.name);`,
389
+ ` res.end(JSON.stringify({ models: models.length ? models : ['default'] }));`,
390
+ ` } catch {`,
391
+ ` res.end(JSON.stringify({ models: ['default'] }));`,
392
+ ` }`,
393
+ ` return;`,
394
+ ` }`,
395
+ '',
396
+ ` if (pathname === '/api/models/voice' && req.method === 'GET') {`,
397
+ ` res.writeHead(200, { 'Content-Type': 'application/json' });`,
398
+ ` try {`,
399
+ ` const voiceDir = path.join(rootDir, 'assets', 'voice');`,
400
+ ` const entries = await readdir(voiceDir, { withFileTypes: true }).catch(() => []);`,
401
+ ` const models = entries.filter((e) => e.isDirectory()).map((e) => e.name);`,
402
+ ` res.end(JSON.stringify({ models: models.length ? models : ['default'] }));`,
403
+ ` } catch {`,
404
+ ` res.end(JSON.stringify({ models: ['default'] }));`,
405
+ ` }`,
406
+ ` return;`,
407
+ ` }`,
408
+ '',
370
409
  ` // Static files & Next.js apps/web export routing`,
371
410
  ` const normalizedPath = pathname.replace(/^[/]+|[/]+$/g, '');`,
372
411
  ` const candidatePaths = [`,
@@ -448,8 +487,15 @@ function generateInstanceFiles(options) {
448
487
  readmeLines.push('', `### ${dockerComposeYaml ? '4' : '3'}. Database Migrations`, 'Ensure `DATABASE_URL` in `.env` is reachable, then push the memory organ PostgreSQL schema:', '```bash', 'npx @vxnus/siduri db push', '```');
449
488
  }
450
489
  readmeLines.push('', `### ${hasMemory ? (dockerComposeYaml ? '5' : '4') : (dockerComposeYaml ? '4' : '3')}. Diagnostics & Health Probe`, 'Verify all environment variables, services, and organ connections:', '```bash', 'npm run doctor', '```', '', `### ${hasMemory ? (dockerComposeYaml ? '6' : '5') : (dockerComposeYaml ? '5' : '4')}. Start Companion & Web Console`, 'Launch your companion runtime and Web UI / Memory Control Panel:', '```bash', 'npm start', '```', 'Then open `http://localhost:3000` in your browser.');
490
+ const companionSlug = instanceName.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'default';
491
+ const createAssetsDirs = [];
451
492
  if (hasBody) {
452
- readmeLines.push('', '### Avatar Assets', 'Place your Live2D Cubism model assets into `./assets/body/model/`:', '- `model.model3.json`', '- `model.moc3`', '- textures directory');
493
+ createAssetsDirs.push(`assets/body/model/${companionSlug}`);
494
+ readmeLines.push('', '### Body & Avatar Models', `Place your Live2D Cubism model assets into \`./assets/body/model/${companionSlug}/\`:`, '- `model.model3.json`', '- `model.moc3`', '- textures directory');
495
+ }
496
+ if (hasVoice) {
497
+ createAssetsDirs.push(`assets/voice/${companionSlug}`);
498
+ readmeLines.push('', '### Voice & RVC Models', `Place your character RVC voice models into \`./assets/voice/${companionSlug}/\`:`, `- \`${companionSlug}.pth\` (Target voice weights)`, `- \`${companionSlug}.index\` (Feature index file)`);
453
499
  }
454
500
  readmeLines.push('');
455
501
  const readmeMd = readmeLines.join('\n');
@@ -463,6 +509,7 @@ function generateInstanceFiles(options) {
463
509
  'src/index.js': srcIndexJs,
464
510
  'public/index.html': webHtml,
465
511
  createAssetsBodyDir: hasBody,
512
+ createAssetsDirs,
466
513
  };
467
514
  if (dockerComposeYaml) {
468
515
  result['docker-compose.yml'] = dockerComposeYaml;
@@ -150,10 +150,13 @@ describe('Instance Generator Composition Invariants (Phase 3)', () => {
150
150
  expect(pkg.dependencies['@siduri-x/hands']).toBeDefined();
151
151
  expect(pkg.dependencies['@siduri-x/body']).toBeDefined();
152
152
  expect(pkg.dependencies['@siduri-x/voice']).toBeDefined();
153
- // Body asset directory requested
153
+ // Body & voice asset directories requested
154
154
  expect(files.createAssetsBodyDir).toBe(true);
155
+ expect(files.createAssetsDirs).toContain('assets/body/model/companion-full');
156
+ expect(files.createAssetsDirs).toContain('assets/voice/companion-full');
155
157
  // README mentions Live2D model assets & prerequisites
156
- expect(files['README.md']).toContain('assets/body/model');
158
+ expect(files['README.md']).toContain('assets/body/model/companion-full');
159
+ expect(files['README.md']).toContain('assets/voice/companion-full');
157
160
  expect(files['README.md']).toContain('Prerequisites');
158
161
  // Docker compose generated for memory & voice
159
162
  expect(files['docker-compose.yml']).toBeDefined();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { OrganManifest } from './manifest';
3
- export declare const CLI_VERSION = "0.1.0";
3
+ export declare const CLI_VERSION = "0.1.2";
4
4
  export declare const colors: {
5
5
  cyan: string;
6
6
  dim: string;