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.
- package/README.md +17 -1
- package/cli/index.js +183 -15
- 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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
42
|
+
"url": "https://github.com/daniel-mf-92/voicci-cli.git"
|
|
43
43
|
},
|
|
44
44
|
"bugs": {
|
|
45
|
-
"url": "https://github.com/
|
|
45
|
+
"url": "https://github.com/daniel-mf-92/voicci-cli/issues"
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://voicci.com/voicci-cli",
|
|
48
48
|
"dependencies": {
|