claude-phone-local 2.0.2 → 2.0.4

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.
@@ -1071,18 +1071,6 @@ async function setupDevice(config) {
1071
1071
  return true;
1072
1072
  }
1073
1073
  },
1074
- {
1075
- type: 'input',
1076
- name: 'voiceId',
1077
- message: 'ElevenLabs voice ID:',
1078
- default: existingDevice?.voiceId || config.api.elevenlabs.defaultVoiceId || '',
1079
- validate: (input) => {
1080
- if (!input || input.trim() === '') {
1081
- return 'Voice ID is required';
1082
- }
1083
- return true;
1084
- }
1085
- },
1086
1074
  {
1087
1075
  type: 'input',
1088
1076
  name: 'prompt',
@@ -1097,39 +1085,88 @@ async function setupDevice(config) {
1097
1085
  }
1098
1086
  ]);
1099
1087
 
1100
- // Validate voice ID with ElevenLabs API
1101
- const voiceSpinner = ora('Validating ElevenLabs voice ID...').start();
1102
- const voiceValidation = await validateVoiceId(config.api.elevenlabs.apiKey, answers.voiceId);
1088
+ const device = {
1089
+ name: answers.name,
1090
+ extension: answers.extension,
1091
+ authId: answers.authId,
1092
+ password: answers.password,
1093
+ prompt: answers.prompt
1094
+ };
1103
1095
 
1104
- if (!voiceValidation.valid) {
1105
- voiceSpinner.fail(`Voice ID validation failed: ${voiceValidation.error}`);
1106
- console.log(chalk.yellow('\n⚠️ You can continue setup, but the voice ID may not work.'));
1107
- const { continueAnyway } = await inquirer.prompt([
1096
+ if (config.api.mode === 'local') {
1097
+ // Local mode: ask for a Piper voice name, not an ElevenLabs voice ID.
1098
+ // Falls back to the call-wide default voice picked in setupAPIKeys().
1099
+ const { voice } = await inquirer.prompt([
1108
1100
  {
1109
- type: 'confirm',
1110
- name: 'continueAnyway',
1111
- message: 'Continue anyway?',
1112
- default: false
1101
+ type: 'input',
1102
+ name: 'voice',
1103
+ message: 'Piper voice for this device (default: ' + (config.api.local?.voice || 'en_US-lessac-medium') + '):',
1104
+ default: existingDevice?.voice || config.api.local?.voice || 'en_US-lessac-medium'
1113
1105
  }
1114
1106
  ]);
1115
1107
 
1116
- if (!continueAnyway) {
1117
- // Let user re-enter voice ID
1118
- console.log(chalk.gray('\nReturning to device setup...'));
1119
- return setupDevice(config);
1108
+ device.voice = voice;
1109
+
1110
+ const voicesDir = path.join(config.paths.ttsLocal, 'voices');
1111
+ const dlSpinner = ora(`Checking Piper voice "${voice}"...`).start();
1112
+ try {
1113
+ const result = await ensureVoiceModel(voice, voicesDir);
1114
+ if (result.downloaded) {
1115
+ dlSpinner.succeed(chalk.green(`Downloaded voice "${voice}"`));
1116
+ } else if (result.skipped) {
1117
+ dlSpinner.succeed(chalk.green(`Voice "${voice}" already present`));
1118
+ } else {
1119
+ dlSpinner.warn(chalk.yellow(result.error));
1120
+ }
1121
+ } catch (error) {
1122
+ dlSpinner.warn(chalk.yellow(`Could not auto-download voice: ${error.message}`));
1123
+ console.log(chalk.gray(
1124
+ ` Known auto-downloadable voices: ${Object.keys(KNOWN_VOICES).join(', ')}\n` +
1125
+ ` For others, place the .onnx + .onnx.json pair in ${voicesDir}/\n`
1126
+ ));
1120
1127
  }
1121
1128
  } else {
1122
- voiceSpinner.succeed(`Voice ID validated: ${voiceValidation.name}`);
1123
- }
1129
+ // Cloud mode: original ElevenLabs voice ID flow.
1130
+ const { voiceId } = await inquirer.prompt([
1131
+ {
1132
+ type: 'input',
1133
+ name: 'voiceId',
1134
+ message: 'ElevenLabs voice ID:',
1135
+ default: existingDevice?.voiceId || config.api.elevenlabs.defaultVoiceId || '',
1136
+ validate: (input) => {
1137
+ if (!input || input.trim() === '') {
1138
+ return 'Voice ID is required';
1139
+ }
1140
+ return true;
1141
+ }
1142
+ }
1143
+ ]);
1124
1144
 
1125
- const device = {
1126
- name: answers.name,
1127
- extension: answers.extension,
1128
- authId: answers.authId,
1129
- password: answers.password,
1130
- voiceId: answers.voiceId,
1131
- prompt: answers.prompt
1132
- };
1145
+ const voiceSpinner = ora('Validating ElevenLabs voice ID...').start();
1146
+ const voiceValidation = await validateVoiceId(config.api.elevenlabs.apiKey, voiceId);
1147
+
1148
+ if (!voiceValidation.valid) {
1149
+ voiceSpinner.fail(`Voice ID validation failed: ${voiceValidation.error}`);
1150
+ console.log(chalk.yellow('\n⚠️ You can continue setup, but the voice ID may not work.'));
1151
+ const { continueAnyway } = await inquirer.prompt([
1152
+ {
1153
+ type: 'confirm',
1154
+ name: 'continueAnyway',
1155
+ message: 'Continue anyway?',
1156
+ default: false
1157
+ }
1158
+ ]);
1159
+
1160
+ if (!continueAnyway) {
1161
+ console.log(chalk.gray('\nReturning to device setup...'));
1162
+ return setupDevice(config);
1163
+ }
1164
+ } else {
1165
+ voiceSpinner.succeed(`Voice ID validated: ${voiceValidation.name}`);
1166
+ }
1167
+
1168
+ device.voiceId = voiceId;
1169
+ }
1133
1170
 
1134
1171
  // Replace first device or add new
1135
1172
  if (config.devices.length > 0) {
@@ -1,4 +1,4 @@
1
- import { execSync } from 'child_process';
1
+ import { execSync, execFileSync } from 'child_process';
2
2
 
3
3
  /**
4
4
  * Check disk space availability
@@ -80,6 +80,34 @@ function getDiskSpace(platform) {
80
80
  const availableKB = parseInt(parts[3], 10);
81
81
  return availableKB * 1024; // Convert KB to bytes
82
82
  }
83
+
84
+ return null;
85
+ }
86
+
87
+ if (platform.os === 'win32') {
88
+ // Check the drive the CLI's config directory (~/.claude-phone) lives on,
89
+ // since that's where the ./data volume actually gets written - not
90
+ // necessarily the same drive as the OS install.
91
+ const driveLetter = (process.env.USERPROFILE || process.env.HOMEDRIVE || 'C:')
92
+ .slice(0, 2)
93
+ .toUpperCase();
94
+
95
+ // wmic is deprecated but still present on every supported Windows
96
+ // release; avoids a PowerShell startup cost on every prereq check.
97
+ // execFileSync (argument array, no shell) - driveLetter is untrusted
98
+ // env-derived input, so it must not be interpolated into a shell string.
99
+ const output = execFileSync(
100
+ 'wmic',
101
+ ['logicaldisk', 'where', `DeviceID='${driveLetter}'`, 'get', 'FreeSpace', '/value'],
102
+ { encoding: 'utf-8', stdio: 'pipe' }
103
+ );
104
+
105
+ const match = output.match(/FreeSpace=(\d+)/);
106
+ if (match) {
107
+ return parseInt(match[1], 10);
108
+ }
109
+
110
+ return null;
83
111
  }
84
112
 
85
113
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-phone-local",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Local/offline fork of NetworkChuck's claude-phone: talk to Claude Code over 3CX/SIP with faster-whisper STT + Piper TTS in one Docker container.",
5
5
  "type": "module",
6
6
  "bin": {