@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.
- package/dist/builtin-manifests.js +26 -1
- package/dist/clean-machine-e2e.test.js +21 -13
- package/dist/configurators/voice.js +327 -20
- package/dist/configurators.test.js +2 -1
- package/dist/generator.d.ts +1 -0
- package/dist/generator.js +49 -2
- package/dist/generator.test.js +5 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -2
- package/dist/release-check.js +2 -2
- package/dist/web-dist/404/index.html +1 -1
- package/dist/web-dist/404.html +1 -1
- package/dist/web-dist/__next.__PAGE__.txt +3 -3
- package/dist/web-dist/__next._full.txt +3 -3
- package/dist/web-dist/__next._tree.txt +2 -2
- package/dist/web-dist/_next/static/chunks/1dc9n0p39yo65.css +1 -0
- package/dist/web-dist/_not-found/__next._full.txt +2 -2
- package/dist/web-dist/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/dist/web-dist/_not-found/__next._tree.txt +2 -2
- package/dist/web-dist/_not-found/index.html +1 -1
- package/dist/web-dist/_not-found/index.txt +2 -2
- package/dist/web-dist/chat/__next._full.txt +2 -2
- package/dist/web-dist/chat/__next._tree.txt +2 -2
- package/dist/web-dist/chat/__next.chat.__PAGE__.txt +2 -2
- package/dist/web-dist/chat/index.html +1 -1
- package/dist/web-dist/chat/index.txt +2 -2
- package/dist/web-dist/index.html +1 -1
- package/dist/web-dist/index.txt +3 -3
- package/dist/web-dist/operator/__next._full.txt +2 -2
- package/dist/web-dist/operator/__next._tree.txt +2 -2
- package/dist/web-dist/operator/__next.operator.__PAGE__.txt +2 -2
- package/dist/web-dist/operator/index.html +1 -1
- package/dist/web-dist/operator/index.txt +2 -2
- package/package.json +1 -1
- package/dist/web-dist/_next/static/chunks/2bu5_paf4sm3x.css +0 -1
- /package/dist/web-dist/_next/static/{_zOqKpMPc6RDqk04fUScI → i9YV06HVZxr6wJWWxhUhB}/_buildManifest.js +0 -0
- /package/dist/web-dist/_next/static/{_zOqKpMPc6RDqk04fUScI → i9YV06HVZxr6wJWWxhUhB}/_clientMiddlewareManifest.js +0 -0
- /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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
110
|
-
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.
|
|
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', {
|
|
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.
|
|
154
|
-
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.
|
|
155
|
-
'@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.
|
|
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', {
|
|
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.
|
|
189
|
-
'@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.
|
|
190
|
-
'@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.
|
|
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
|
-
{
|
|
15
|
-
|
|
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
|
-
|
|
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: '
|
|
33
|
-
name: '
|
|
34
|
-
message: '
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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);
|
package/dist/generator.d.ts
CHANGED
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
|
-
|
|
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;
|
package/dist/generator.test.js
CHANGED
|
@@ -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
|
|
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();
|