voicci 1.0.10 → 1.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.
Files changed (3) hide show
  1. package/README.md +17 -1
  2. package/cli/index.js +183 -15
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -25,11 +25,27 @@ Transform books and PDFs into audiobooks using natural language with Claude Code
25
25
 
26
26
  ### Installation
27
27
 
28
+ **Prerequisites** (one-time):
29
+
30
+ - Node.js 22+
31
+ - Python 3.10+
32
+ - `pip3 install TTS torch torchaudio` — Voicci uses XTTS v2 locally (PyTorch + Coqui TTS; ~2 GB of model weights download on first run)
33
+
34
+ **Install the CLI:**
35
+
28
36
  ```bash
29
37
  npm install -g voicci
30
38
  ```
31
39
 
32
- That's it! The package installs both the CLI tool and Claude Code skill automatically.
40
+ **Verify everything is wired up:**
41
+
42
+ ```bash
43
+ voicci doctor
44
+ ```
45
+
46
+ `voicci doctor` prints a PASS/FAIL table for Node, Python, TTS, PyTorch, acceleration (MPS/CUDA/CPU), and the AI-editor skill directory. If anything fails, it tells you the exact command to fix it.
47
+
48
+ The npm package installs the CLI plus the Claude Code / OpenCode / Cursor / Windsurf skill files. **Restart your AI editor** after install so it picks up `/voicci`.
33
49
 
34
50
  ### Usage with AI Code Editors
35
51
 
package/cli/index.js CHANGED
@@ -21,6 +21,30 @@ const pkg = require('../package.json');
21
21
  const execAsync = promisify(exec);
22
22
  const execFileAsync = promisify(execFile);
23
23
 
24
+ // Cross-platform open command
25
+ function getOpenCommand() {
26
+ const platform = process.platform;
27
+ if (platform === 'darwin') return 'open';
28
+ if (platform === 'win32') return 'start';
29
+ return 'xdg-open'; // Linux
30
+ }
31
+
32
+ // Check if Python TTS dependencies are available
33
+ async function checkTTSDependencies() {
34
+ try {
35
+ await execFileAsync('python3', ['-c', 'from TTS.api import TTS; print("ok")']);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ // Check if input looks like a file path (has a supported extension)
43
+ function looksLikeFilePath(input) {
44
+ const ext = path.extname(input).toLowerCase();
45
+ return ['.pdf', '.txt'].includes(ext);
46
+ }
47
+
24
48
  const program = new Command();
25
49
 
26
50
  program
@@ -78,20 +102,24 @@ program
78
102
 
79
103
  // Process file or search query
80
104
  if (input) {
81
- // Check if input is a file path
82
- if (fs.existsSync(input)) {
83
- // Validate file path for security
84
- try {
85
- const validatedPath = pathValidator.validateFilePath(input, {
86
- mustExist: true,
87
- allowedExtensions: ['.pdf', '.txt']
88
- });
89
- await processFile(validatedPath, options);
90
- } catch (error) {
91
- console.error('Error: Invalid file path');
92
- console.error(error.message);
105
+ if (looksLikeFilePath(input)) {
106
+ // Input has a recognized file extension — must exist
107
+ if (!fs.existsSync(input)) {
108
+ console.error(`Error: File not found: ${input}`);
93
109
  process.exit(1);
94
110
  }
111
+ const validatedPath = pathValidator.validateFilePath(input, {
112
+ mustExist: true,
113
+ allowedExtensions: ['.pdf', '.txt']
114
+ });
115
+ await processFile(validatedPath, options);
116
+ } else if (fs.existsSync(input)) {
117
+ // Existing file without recognized extension
118
+ const validatedPath = pathValidator.validateFilePath(input, {
119
+ mustExist: true,
120
+ allowedExtensions: ['.pdf', '.txt']
121
+ });
122
+ await processFile(validatedPath, options);
95
123
  } else {
96
124
  // Treat as search query
97
125
  console.log(`Searching for: "${input}"\n`);
@@ -154,6 +182,17 @@ async function processFile(filePath, options = {}) {
154
182
  if (summaryOnly) return; // Don't create audiobook job
155
183
  }
156
184
 
185
+ // Check Python TTS dependencies before creating job
186
+ const hasTTS = await checkTTSDependencies();
187
+ if (!hasTTS) {
188
+ console.error('\n❌ Python TTS dependencies not found.');
189
+ console.error('\nTo generate audiobooks, install the required Python packages:');
190
+ console.error(' pip3 install TTS torch torchaudio\n');
191
+ console.error('This is a one-time setup (~2GB download).');
192
+ console.error('After installing, run your command again.\n');
193
+ throw new Error('Python TTS dependencies not installed');
194
+ }
195
+
157
196
  // Create job
158
197
  console.log('📋 Creating job...');
159
198
  const queue = new Queue();
@@ -212,7 +251,7 @@ async function generateSummary(filePath, text, summaryOnly = false) {
212
251
  console.log('─'.repeat(60));
213
252
  console.log(result.summary.substring(0, 500) + '...\n');
214
253
  console.log('─'.repeat(60));
215
- console.log(`\nOpen full summary: open "${summaryPath}"\n`);
254
+ console.log(`\nOpen full summary: ${getOpenCommand()} "${summaryPath}"\n`);
216
255
  }
217
256
  }
218
257
 
@@ -388,7 +427,7 @@ async function openAudiobook(jobId) {
388
427
  if (jobId === true || !jobId) {
389
428
  // Open audiobooks directory
390
429
  const audiobooksDir = config.paths.audiobooks;
391
- await execFileAsync('open', [audiobooksDir]);
430
+ await execFileAsync(getOpenCommand(), [audiobooksDir]);
392
431
  console.log(`Opened: ${audiobooksDir}\n`);
393
432
  } else {
394
433
  // Open specific job
@@ -406,7 +445,7 @@ async function openAudiobook(jobId) {
406
445
  return;
407
446
  }
408
447
 
409
- await execFileAsync('open', [job.output_dir]);
448
+ await execFileAsync(getOpenCommand(), [job.output_dir]);
410
449
  console.log(`Opened: ${job.output_dir}\n`);
411
450
  }
412
451
 
@@ -721,4 +760,133 @@ program
721
760
  console.log();
722
761
  });
723
762
 
763
+ program
764
+ .command('doctor')
765
+ .description('Verify Node, Python, TTS, PyTorch, and AI-editor skill installation')
766
+ .option('--json', 'Output machine-readable JSON')
767
+ .action(async (options) => {
768
+ const results = await runDoctor();
769
+ if (options.json) {
770
+ console.log(JSON.stringify(results, null, 2));
771
+ } else {
772
+ printDoctorReport(results);
773
+ }
774
+ const failed = results.checks.some(c => c.status === 'fail');
775
+ process.exit(failed ? 1 : 0);
776
+ });
777
+
778
+ async function runDoctor() {
779
+ const os = await import('os');
780
+ const checks = [];
781
+
782
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
783
+ checks.push({
784
+ name: 'Node.js ≥ 22',
785
+ status: nodeMajor >= 22 ? 'pass' : 'fail',
786
+ detail: `v${process.versions.node}`,
787
+ fix: nodeMajor >= 22 ? null : 'Install Node 22+: https://nodejs.org/en/download',
788
+ });
789
+
790
+ let pythonCmd = null;
791
+ let pythonVersion = null;
792
+ for (const cand of ['python3', 'python']) {
793
+ try {
794
+ const { stdout } = await execFileAsync(cand, ['--version']);
795
+ const m = stdout.match(/Python (\d+)\.(\d+)/);
796
+ if (m) {
797
+ pythonCmd = cand;
798
+ pythonVersion = `${m[1]}.${m[2]}`;
799
+ if (parseInt(m[1], 10) === 3 && parseInt(m[2], 10) >= 10) break;
800
+ }
801
+ } catch {}
802
+ }
803
+ const pyOk = pythonVersion && parseInt(pythonVersion.split('.')[0], 10) === 3 && parseInt(pythonVersion.split('.')[1], 10) >= 10;
804
+ checks.push({
805
+ name: 'Python ≥ 3.10',
806
+ status: pyOk ? 'pass' : 'fail',
807
+ detail: pythonVersion ? `${pythonCmd} ${pythonVersion}` : 'not found',
808
+ fix: pyOk ? null : 'Install Python 3.10+: https://www.python.org/downloads/',
809
+ });
810
+
811
+ let torchOk = false, torchDetail = 'not importable';
812
+ if (pythonCmd) {
813
+ try {
814
+ const { stdout } = await execFileAsync(pythonCmd, ['-c', 'import torch; print(torch.__version__)']);
815
+ torchOk = true;
816
+ torchDetail = `torch ${stdout.trim()}`;
817
+ } catch {}
818
+ }
819
+ checks.push({
820
+ name: 'PyTorch',
821
+ status: torchOk ? 'pass' : 'fail',
822
+ detail: torchDetail,
823
+ fix: torchOk ? null : `${pythonCmd || 'pip3'} -m pip install torch torchaudio`,
824
+ });
825
+
826
+ let ttsOk = false, ttsDetail = 'not importable';
827
+ if (pythonCmd) {
828
+ try {
829
+ const { stdout } = await execFileAsync(pythonCmd, ['-c', 'import TTS; print(TTS.__version__)']);
830
+ ttsOk = true;
831
+ ttsDetail = `TTS ${stdout.trim()}`;
832
+ } catch {}
833
+ }
834
+ checks.push({
835
+ name: 'TTS (XTTS v2)',
836
+ status: ttsOk ? 'pass' : 'fail',
837
+ detail: ttsDetail,
838
+ fix: ttsOk ? null : `${pythonCmd || 'pip3'} -m pip install TTS`,
839
+ });
840
+
841
+ let accel = 'cpu';
842
+ if (pythonCmd && torchOk) {
843
+ try {
844
+ const { stdout } = await execFileAsync(pythonCmd, ['-c',
845
+ 'import torch; print("mps" if torch.backends.mps.is_available() else ("cuda" if torch.cuda.is_available() else "cpu"))']);
846
+ accel = stdout.trim();
847
+ } catch {}
848
+ }
849
+ checks.push({
850
+ name: 'Acceleration',
851
+ status: accel === 'cpu' ? 'warn' : 'pass',
852
+ detail: accel.toUpperCase(),
853
+ fix: accel === 'cpu' ? 'CPU works but is slow. M-series/NVIDIA recommended.' : null,
854
+ });
855
+
856
+ const home = os.homedir();
857
+ const skillPath = path.join(home, '.claude', 'skills', 'voicci', 'SKILL.md');
858
+ const skillExists = fs.existsSync(skillPath);
859
+ checks.push({
860
+ name: 'Claude Code skill',
861
+ status: skillExists ? 'pass' : 'warn',
862
+ detail: skillExists ? skillPath : 'not installed (harmless if you do not use Claude Code)',
863
+ fix: skillExists ? null : 'Re-run: npm install -g voicci',
864
+ });
865
+
866
+ return {
867
+ voicciVersion: pkg.version,
868
+ platform: process.platform,
869
+ arch: process.arch,
870
+ checks,
871
+ };
872
+ }
873
+
874
+ function printDoctorReport(results) {
875
+ console.log('\n🩺 Voicci Doctor\n');
876
+ console.log(` voicci@${results.voicciVersion} · ${results.platform}/${results.arch}\n`);
877
+ const pad = (s, n) => (s + ' '.repeat(n)).slice(0, n);
878
+ for (const c of results.checks) {
879
+ const icon = c.status === 'pass' ? '✅' : c.status === 'warn' ? '⚠️ ' : '❌';
880
+ console.log(` ${icon} ${pad(c.name, 22)} ${c.detail}`);
881
+ if (c.fix) console.log(` ↳ ${c.fix}`);
882
+ }
883
+ const failed = results.checks.filter(c => c.status === 'fail');
884
+ console.log('');
885
+ if (failed.length === 0) {
886
+ console.log(' 🎉 All required prerequisites look good. You are ready to generate audiobooks.\n');
887
+ } else {
888
+ console.log(` ${failed.length} check(s) failed. Fix them and re-run \`voicci doctor\`.\n`);
889
+ }
890
+ }
891
+
724
892
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicci",
3
- "version": "1.0.10",
3
+ "version": "1.1.1",
4
4
  "description": "AI-Powered Audiobook Generator for Claude Code, OpenCode & AI Code Editors. Convert books and PDFs to audiobooks using natural language.",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "start": "node cli/index.js",
12
12
  "worker": "node backend/worker.js",
13
- "test": "node tests/test-cleaner.js",
13
+ "test": "node tests/test-cleaner.js && node tests/test-security.js",
14
14
  "postinstall": "node scripts/postinstall.js"
15
15
  },
16
16
  "keywords": [
@@ -22,7 +22,7 @@
22
22
  "text-to-speech",
23
23
  "cli",
24
24
  "pdf",
25
- "epub",
25
+ "book",
26
26
  "summarization",
27
27
  "claude-code",
28
28
  "claude-skill",
@@ -39,10 +39,10 @@
39
39
  "license": "MIT",
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/OlmiVanguard/voicci-cli.git"
42
+ "url": "https://github.com/daniel-mf-92/voicci-cli.git"
43
43
  },
44
44
  "bugs": {
45
- "url": "https://github.com/OlmiVanguard/voicci-cli/issues"
45
+ "url": "https://github.com/daniel-mf-92/voicci-cli/issues"
46
46
  },
47
47
  "homepage": "https://voicci.com/voicci-cli",
48
48
  "dependencies": {