@vxnus/siduri 0.1.0 → 0.1.1

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.
@@ -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.1.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']);
@@ -150,9 +150,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
150
150
  });
151
151
  const pkgObj = JSON.parse(files['package.json']);
152
152
  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')}`,
153
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.1.tgz')}`,
154
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.1.tgz')}`,
155
+ '@siduri-x/hands': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-hands-1.0.1.tgz')}`,
156
156
  };
157
157
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
158
158
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -185,9 +185,9 @@ describe('Phase 5: Clean-Machine Distribution & E2E Integration Suite', () => {
185
185
  });
186
186
  const pkgObj = JSON.parse(files['package.json']);
187
187
  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')}`,
188
+ '@siduri-x/core': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-core-1.0.1.tgz')}`,
189
+ '@siduri-x/brain': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-brain-1.0.1.tgz')}`,
190
+ '@siduri-x/memory': `file:${node_path_1.default.join(tempPackDir, 'siduri-x-memory-1.0.1.tgz')}`,
191
191
  };
192
192
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'package.json'), JSON.stringify(pkgObj, null, 2) + '\n');
193
193
  node_fs_1.default.writeFileSync(node_path_1.default.join(instanceDir, 'siduri.config.json'), files['siduri.config.json']);
@@ -35,19 +35,41 @@ async function configureVoice(_context) {
35
35
  default: '1',
36
36
  validate: (v) => !isNaN(parseInt(v, 10)) || 'Speaker ID must be an integer.',
37
37
  },
38
+ {
39
+ type: 'confirm',
40
+ name: 'enableRvc',
41
+ message: 'Enable Headless RVC (Voice Conversion via ./assets/voice/[name])?',
42
+ default: false,
43
+ },
38
44
  ]);
39
45
  const speakerId = parseInt(answers.speakerId, 10) || 1;
46
+ const companionSlug = _context.companionName.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'default';
47
+ const config = {
48
+ provider: 'voicevox',
49
+ baseUrl: answers.baseUrl.trim(),
50
+ speakerId,
51
+ maxQueueDepth: 50,
52
+ };
53
+ const summary = {
54
+ Provider: 'VOICEVOX',
55
+ 'Speaker ID': speakerId,
56
+ 'Base URL': answers.baseUrl.trim(),
57
+ };
58
+ if (answers.enableRvc) {
59
+ config.rvc = {
60
+ enabled: true,
61
+ serviceUrl: 'http://localhost:50055',
62
+ modelName: companionSlug,
63
+ modelPath: `./assets/voice/${companionSlug}/${companionSlug}.pth`,
64
+ indexPath: `./assets/voice/${companionSlug}/${companionSlug}.index`,
65
+ pitchShift: 0,
66
+ f0Method: 'rmvpe',
67
+ indexRate: 0.75,
68
+ };
69
+ summary['RVC Post-Processor'] = `Enabled (assets/voice/${companionSlug}/)`;
70
+ }
40
71
  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
- },
72
+ config,
73
+ summary,
52
74
  };
53
75
  }
@@ -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.1";
4
4
  export declare const colors: {
5
5
  cyan: string;
6
6
  dim: string;
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ const doctor_1 = require("./doctor");
26
26
  const db_1 = require("./db");
27
27
  const configurators_1 = require("./configurators");
28
28
  const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile);
29
- exports.CLI_VERSION = '0.1.0';
29
+ exports.CLI_VERSION = '0.1.1';
30
30
  exports.colors = {
31
31
  cyan: '\u001b[36m',
32
32
  dim: '\u001b[2m',
@@ -244,7 +244,12 @@ async function runCreateWizard(targetDir) {
244
244
  if (files['docker-compose.yml']) {
245
245
  await (0, promises_1.writeFile)(node_path_1.default.join(projectDir, 'docker-compose.yml'), files['docker-compose.yml'], 'utf8');
246
246
  }
247
- if (files.createAssetsBodyDir) {
247
+ if (files.createAssetsDirs && files.createAssetsDirs.length > 0) {
248
+ for (const dir of files.createAssetsDirs) {
249
+ await (0, promises_1.mkdir)(node_path_1.default.join(projectDir, dir), { recursive: true });
250
+ }
251
+ }
252
+ else if (files.createAssetsBodyDir) {
248
253
  await (0, promises_1.mkdir)(node_path_1.default.join(projectDir, 'assets/body/model'), { recursive: true });
249
254
  }
250
255
  printSuccess(`Generated standalone files at ${projectDir}`);
@@ -26,9 +26,9 @@ function runReleaseCheck(repoRoot = node_path_1.default.resolve(__dirname, '../.
26
26
  { name: '@siduri-x/vision', dir: 'packages/organs/vision', isOrgan: true, tarName: 'siduri-x-vision-1.0.1.tgz' },
27
27
  { name: '@siduri-x/hands', dir: 'packages/organs/hands', isOrgan: true, tarName: 'siduri-x-hands-1.0.1.tgz' },
28
28
  { name: '@siduri-x/body', dir: 'packages/organs/body', isOrgan: true, tarName: 'siduri-x-body-1.0.1.tgz' },
29
- { name: '@siduri-x/voice', dir: 'packages/organs/voice', isOrgan: true, tarName: 'siduri-x-voice-1.0.1.tgz' },
29
+ { name: '@siduri-x/voice', dir: 'packages/organs/voice', isOrgan: true, tarName: 'siduri-x-voice-1.0.2.tgz' },
30
30
  { name: '@siduri-x/observation', dir: 'packages/organs/observation', isOrgan: true, tarName: 'siduri-x-observation-1.0.1.tgz' },
31
- { name: '@vxnus/siduri', dir: 'cli', isOrgan: false, tarName: 'vxnus-siduri-0.1.0.tgz' },
31
+ { name: '@vxnus/siduri', dir: 'cli', isOrgan: false, tarName: 'vxnus-siduri-0.1.1.tgz' },
32
32
  ];
33
33
  let packagesChecked = 0;
34
34
  let tarballsInspected = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxnus/siduri",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Experimental CLI for installing and configuring Siduri companions",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {