@iaforged/context-code 1.1.4 → 1.1.7

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 (34) hide show
  1. package/README.md +32 -8
  2. package/dist/src/commands/init.js +91 -219
  3. package/dist/src/commands/voice/index.js +6 -7
  4. package/dist/src/commands/voice/voice.js +87 -43
  5. package/dist/src/commands.js +1 -3
  6. package/dist/src/components/LogoV2/VoiceModeNotice.js +1 -1
  7. package/dist/src/components/PromptInput/VoiceIndicator.js +4 -4
  8. package/dist/src/components/Spinner.js +18 -18
  9. package/dist/src/constants/spinnerVerbs.js +9 -9
  10. package/dist/src/hooks/usePasteHandler.js +8 -8
  11. package/dist/src/hooks/useVoice.js +93 -805
  12. package/dist/src/hooks/useVoiceEnabled.js +3 -15
  13. package/dist/src/hooks/useVoiceIntegration.js +6 -25
  14. package/dist/src/keybindings/defaultBindings.js +9 -6
  15. package/dist/src/screens/REPL.js +10 -22
  16. package/dist/src/services/localDictation.js +520 -0
  17. package/dist/src/services/voice.js +9 -7
  18. package/dist/src/state/AppState.js +1 -3
  19. package/dist/src/tools/ConfigTool/ConfigTool.js +12 -15
  20. package/dist/src/tools/ConfigTool/supportedSettings.js +2 -2
  21. package/dist/src/utils/imagePaste.js +11 -5
  22. package/dist/src/utils/model/model.js +2 -0
  23. package/dist/src/utils/settings/types.js +2 -2
  24. package/dist/src/voice/voiceModeEnabled.js +5 -25
  25. package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  26. package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  27. package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  28. package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  29. package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  30. package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  31. package/dist/vendor/audio-capture-src/index.js +114 -0
  32. package/dist/vendor/audio-capture-src/index.ts +155 -0
  33. package/docs/comandos.md +132 -121
  34. package/package.json +1 -1
@@ -0,0 +1,520 @@
1
+ import { accessSync, constants } from 'fs';
2
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import { basename, join } from 'path';
5
+ import { execa } from 'execa';
6
+ import { saveGlobalConfig, getGlobalConfig } from '../utils/config.js';
7
+ import { logForDebugging } from '../utils/debug.js';
8
+ import { getClaudeConfigHomeDir } from '../utils/envUtils.js';
9
+ const SAMPLE_RATE = 16_000;
10
+ const CHANNELS = 1;
11
+ const BITS_PER_SAMPLE = 16;
12
+ const DEFAULT_MODEL_NAME = 'base';
13
+ const WHISPER_RELEASE_API = 'https://api.github.com/repos/ggml-org/whisper.cpp/releases/latest';
14
+ const WHISPER_MODEL_BASE_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
15
+ function fileExists(path) {
16
+ try {
17
+ accessSync(path, constants.F_OK);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function getStoredDictationConfig() {
25
+ return getGlobalConfig().dictationConfig;
26
+ }
27
+ function getConfiguredExecutable() {
28
+ const explicit = process.env.CONTEXT_CODE_DICTATION_EXECUTABLE ??
29
+ process.env.WHISPER_CPP_PATH;
30
+ if (explicit?.trim()) {
31
+ return explicit.trim();
32
+ }
33
+ const stored = getStoredDictationConfig()?.executablePath;
34
+ if (stored?.trim()) {
35
+ return stored.trim();
36
+ }
37
+ // Fallback al nombre por defecto en PATH segun plataforma. En macOS/Linux
38
+ // probamos primero `whisper-cli` (releases) y luego `whisper-cpp` (Homebrew).
39
+ if (process.platform === 'win32')
40
+ return 'whisper-cli.exe';
41
+ return 'whisper-cli';
42
+ }
43
+ function getConfiguredModel() {
44
+ const model = process.env.CONTEXT_CODE_DICTATION_MODEL ??
45
+ process.env.WHISPER_MODEL_PATH;
46
+ if (model?.trim()) {
47
+ return model.trim();
48
+ }
49
+ const stored = getStoredDictationConfig()?.modelPath;
50
+ return stored?.trim() ? stored.trim() : null;
51
+ }
52
+ function getConfiguredLanguage() {
53
+ const language = process.env.CONTEXT_CODE_DICTATION_LANGUAGE ?? process.env.WHISPER_LANGUAGE;
54
+ if (language?.trim()) {
55
+ return language.trim();
56
+ }
57
+ const stored = getStoredDictationConfig()?.language;
58
+ return stored?.trim() ? stored.trim() : 'es';
59
+ }
60
+ function getDictationConfig() {
61
+ const executable = getConfiguredExecutable();
62
+ const model = getConfiguredModel();
63
+ if (!executable || !model) {
64
+ return null;
65
+ }
66
+ return {
67
+ executable,
68
+ model,
69
+ language: getConfiguredLanguage(),
70
+ };
71
+ }
72
+ function getInstallRoot() {
73
+ return join(getClaudeConfigHomeDir(), 'dictation', 'whisper.cpp');
74
+ }
75
+ function normalizeModelName(modelName) {
76
+ const raw = (modelName ?? DEFAULT_MODEL_NAME).trim();
77
+ if (!raw) {
78
+ return DEFAULT_MODEL_NAME;
79
+ }
80
+ return raw.endsWith('.bin')
81
+ ? raw.replace(/^ggml-/, '').replace(/\.bin$/i, '')
82
+ : raw;
83
+ }
84
+ function buildModelFileName(modelName) {
85
+ return `ggml-${normalizeModelName(modelName)}.bin`;
86
+ }
87
+ function getPlatformArchiveExtension() {
88
+ return process.platform === 'win32' ? '.zip' : '.tar.gz';
89
+ }
90
+ function escapePowerShellLiteral(value) {
91
+ return value.replace(/'/g, "''");
92
+ }
93
+ async function fetchBuffer(url, label) {
94
+ const response = await fetch(url, {
95
+ headers: {
96
+ 'User-Agent': 'ContextCode-DictationInstaller',
97
+ Accept: 'application/octet-stream, application/json',
98
+ },
99
+ });
100
+ if (!response.ok) {
101
+ throw new Error(`No se pudo descargar ${label} (${response.status} ${response.statusText}).`);
102
+ }
103
+ return Buffer.from(await response.arrayBuffer());
104
+ }
105
+ async function fetchLatestRelease() {
106
+ const response = await fetch(WHISPER_RELEASE_API, {
107
+ headers: {
108
+ 'User-Agent': 'ContextCode-DictationInstaller',
109
+ Accept: 'application/vnd.github+json',
110
+ },
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`No se pudo consultar la ultima version de whisper.cpp (${response.status} ${response.statusText}).`);
114
+ }
115
+ return (await response.json());
116
+ }
117
+ function scoreReleaseAsset(asset) {
118
+ const name = asset.name.toLowerCase();
119
+ if (name.includes('source code')) {
120
+ return -100;
121
+ }
122
+ let score = 0;
123
+ const expectedExt = getPlatformArchiveExtension();
124
+ if (name.endsWith(expectedExt))
125
+ score += 10;
126
+ if (name.includes('bin'))
127
+ score += 5;
128
+ if (name.includes('build'))
129
+ score += 2;
130
+ switch (process.platform) {
131
+ case 'win32':
132
+ if (name.includes('win') || name.includes('windows') || name.includes('msvc')) {
133
+ score += 5;
134
+ }
135
+ if (name.includes('x64') || name.includes('amd64'))
136
+ score += 4;
137
+ if (name.includes('arm64'))
138
+ score -= 4;
139
+ break;
140
+ case 'darwin':
141
+ if (name.includes('mac') || name.includes('darwin') || name.includes('apple')) {
142
+ score += 5;
143
+ }
144
+ if (process.arch === 'arm64') {
145
+ if (name.includes('arm64'))
146
+ score += 4;
147
+ if (name.includes('x64') || name.includes('amd64'))
148
+ score -= 4;
149
+ }
150
+ break;
151
+ default:
152
+ if (name.includes('linux'))
153
+ score += 5;
154
+ if (process.arch === 'x64' && (name.includes('x64') || name.includes('amd64'))) {
155
+ score += 4;
156
+ }
157
+ if (process.arch === 'arm64' && name.includes('arm64'))
158
+ score += 4;
159
+ break;
160
+ }
161
+ return score;
162
+ }
163
+ function pickReleaseAsset(assets) {
164
+ const ranked = [...assets]
165
+ .map(asset => ({ asset, score: scoreReleaseAsset(asset) }))
166
+ .filter(entry => entry.score > 0)
167
+ .sort((a, b) => b.score - a.score);
168
+ return ranked[0]?.asset ?? null;
169
+ }
170
+ async function extractArchive(archivePath, destination) {
171
+ if (archivePath.endsWith('.zip')) {
172
+ if (process.platform === 'win32') {
173
+ await execa('powershell', [
174
+ '-NoProfile',
175
+ '-NonInteractive',
176
+ '-Command',
177
+ `Expand-Archive -LiteralPath '${escapePowerShellLiteral(archivePath)}' -DestinationPath '${escapePowerShellLiteral(destination)}' -Force`,
178
+ ], { windowsHide: true });
179
+ return;
180
+ }
181
+ await execa('unzip', ['-o', archivePath, '-d', destination], {
182
+ windowsHide: true,
183
+ });
184
+ return;
185
+ }
186
+ if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) {
187
+ await execa('tar', ['-xzf', archivePath, '-C', destination], {
188
+ windowsHide: true,
189
+ });
190
+ return;
191
+ }
192
+ throw new Error(`Formato de paquete no soportado: ${basename(archivePath)}`);
193
+ }
194
+ async function findFileRecursive(rootDir, predicate) {
195
+ const entries = await readdir(rootDir, { withFileTypes: true });
196
+ for (const entry of entries) {
197
+ const absolutePath = join(rootDir, entry.name);
198
+ if (entry.isDirectory()) {
199
+ const nested = await findFileRecursive(absolutePath, predicate);
200
+ if (nested) {
201
+ return nested;
202
+ }
203
+ continue;
204
+ }
205
+ if (predicate(absolutePath)) {
206
+ return absolutePath;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ async function findInstalledExecutable(installDir) {
212
+ const candidateNames = process.platform === 'win32'
213
+ ? ['whisper-cli.exe', 'main.exe']
214
+ : ['whisper-cli', 'whisper-cpp', 'main'];
215
+ return findFileRecursive(installDir, path => candidateNames.some(name => path.toLowerCase().endsWith(name.toLowerCase())));
216
+ }
217
+ function persistInstalledDictationConfig(config) {
218
+ saveGlobalConfig(current => ({
219
+ ...current,
220
+ dictationConfig: {
221
+ ...(current.dictationConfig ?? {}),
222
+ ...config,
223
+ },
224
+ }));
225
+ }
226
+ export function getLocalDictationSetupHint() {
227
+ return [
228
+ 'Configura Whisper local antes de usar /dictar.',
229
+ 'Opcion recomendada:',
230
+ '- Ejecuta /dictar install para descargar e instalar el backend automaticamente.',
231
+ 'Tambien puedes configurarlo manualmente con estas variables:',
232
+ '- CONTEXT_CODE_DICTATION_EXECUTABLE o WHISPER_CPP_PATH',
233
+ '- CONTEXT_CODE_DICTATION_MODEL o WHISPER_MODEL_PATH',
234
+ 'Ejemplo PowerShell:',
235
+ 'setx CONTEXT_CODE_DICTATION_EXECUTABLE "C:\\whisper\\whisper-cli.exe"',
236
+ 'setx CONTEXT_CODE_DICTATION_MODEL "C:\\whisper\\models\\ggml-base.bin"',
237
+ ].join('\n');
238
+ }
239
+ export async function checkLocalDictationConfiguration() {
240
+ const config = getDictationConfig();
241
+ if (!config) {
242
+ return { available: false, error: getLocalDictationSetupHint() };
243
+ }
244
+ if (!fileExists(config.model)) {
245
+ return {
246
+ available: false,
247
+ error: `No se encontro el modelo de dictado: ${config.model}\n` +
248
+ getLocalDictationSetupHint(),
249
+ };
250
+ }
251
+ try {
252
+ await execa(config.executable, ['--help'], {
253
+ timeout: 10_000,
254
+ windowsHide: true,
255
+ reject: false,
256
+ });
257
+ return { available: true };
258
+ }
259
+ catch (error) {
260
+ return {
261
+ available: false,
262
+ error: `No se pudo ejecutar el backend de dictado (${config.executable}).\n` +
263
+ `${error instanceof Error ? error.message : String(error)}`,
264
+ };
265
+ }
266
+ }
267
+ export async function getLocalDictationStatus() {
268
+ const config = getDictationConfig();
269
+ const check = await checkLocalDictationConfiguration();
270
+ const stored = getStoredDictationConfig();
271
+ if (!config || !check.available) {
272
+ return check.error ?? 'El dictado local no esta configurado.';
273
+ }
274
+ const lines = [
275
+ 'Dictado local configurado.',
276
+ `Backend: ${config.executable}`,
277
+ `Modelo: ${config.model}`,
278
+ ];
279
+ if (stored?.releaseTag) {
280
+ lines.push(`Release instalada: ${stored.releaseTag}`);
281
+ }
282
+ if (stored?.installedAt) {
283
+ lines.push(`Instalado: ${stored.installedAt}`);
284
+ }
285
+ return lines.join('\n');
286
+ }
287
+ async function isCommandAvailable(command) {
288
+ const probe = process.platform === 'win32' ? 'where' : 'which';
289
+ try {
290
+ const result = await execa(probe, [command], {
291
+ reject: false,
292
+ windowsHide: true,
293
+ timeout: 5_000,
294
+ });
295
+ return result.exitCode === 0;
296
+ }
297
+ catch {
298
+ return false;
299
+ }
300
+ }
301
+ async function getBrewPrefix() {
302
+ try {
303
+ const result = await execa('brew', ['--prefix'], {
304
+ reject: false,
305
+ windowsHide: true,
306
+ timeout: 10_000,
307
+ });
308
+ if (result.exitCode === 0) {
309
+ const prefix = result.stdout.trim();
310
+ return prefix || null;
311
+ }
312
+ }
313
+ catch {
314
+ // ignore
315
+ }
316
+ return null;
317
+ }
318
+ async function installViaHomebrew(modelName) {
319
+ const normalizedModelName = normalizeModelName(modelName);
320
+ const modelFileName = buildModelFileName(normalizedModelName);
321
+ const installDir = getInstallRoot();
322
+ // Instala (o reinstala/actualiza si ya estaba) whisper-cpp con brew.
323
+ const brewResult = await execa('brew', ['install', 'whisper-cpp'], {
324
+ reject: false,
325
+ windowsHide: true,
326
+ timeout: 600_000,
327
+ });
328
+ if (brewResult.exitCode !== 0) {
329
+ throw new Error(`brew install whisper-cpp fallo:\n${brewResult.stderr || brewResult.stdout || 'error desconocido'}`);
330
+ }
331
+ // Homebrew instala el binario como `whisper-cli` (igual que los releases),
332
+ // pero algunas versiones antiguas usaban `whisper-cpp`. Probamos ambos.
333
+ const brewPrefix = await getBrewPrefix();
334
+ const binNames = ['whisper-cli', 'whisper-cpp'];
335
+ const candidatePaths = [];
336
+ for (const name of binNames) {
337
+ if (brewPrefix)
338
+ candidatePaths.push(join(brewPrefix, 'bin', name));
339
+ candidatePaths.push(`/opt/homebrew/bin/${name}`);
340
+ candidatePaths.push(`/usr/local/bin/${name}`);
341
+ }
342
+ let executablePath = candidatePaths.find(fileExists) ?? null;
343
+ if (!executablePath) {
344
+ // Fallback: resolver via `which` para cualquiera de los nombres.
345
+ for (const name of binNames) {
346
+ const which = await execa('which', [name], {
347
+ reject: false,
348
+ windowsHide: true,
349
+ timeout: 5_000,
350
+ });
351
+ if (which.exitCode === 0 && which.stdout.trim()) {
352
+ executablePath = which.stdout.trim();
353
+ break;
354
+ }
355
+ }
356
+ }
357
+ if (!executablePath) {
358
+ throw new Error('brew instalo whisper-cpp pero no encontre el binario en el PATH.');
359
+ }
360
+ // Descarga el modelo GGML (Homebrew no lo trae) en nuestra carpeta privada.
361
+ const modelDir = join(installDir, 'models');
362
+ const modelPath = join(modelDir, modelFileName);
363
+ await mkdir(modelDir, { recursive: true });
364
+ if (!fileExists(modelPath)) {
365
+ await writeFile(modelPath, await fetchBuffer(`${WHISPER_MODEL_BASE_URL}/${modelFileName}?download=1`, `modelo ${modelFileName}`));
366
+ }
367
+ // Intenta resolver la version instalada para guardarla como releaseTag.
368
+ let releaseTag = 'homebrew';
369
+ try {
370
+ const versionResult = await execa(executablePath, ['--version'], {
371
+ reject: false,
372
+ windowsHide: true,
373
+ timeout: 10_000,
374
+ });
375
+ const text = `${versionResult.stdout}\n${versionResult.stderr}`;
376
+ const match = text.match(/v?\d+\.\d+(?:\.\d+)?/);
377
+ if (match)
378
+ releaseTag = `homebrew-${match[0]}`;
379
+ }
380
+ catch {
381
+ // ignore
382
+ }
383
+ persistInstalledDictationConfig({
384
+ executablePath,
385
+ installDir,
386
+ installedAt: new Date().toISOString(),
387
+ modelName: normalizedModelName,
388
+ modelPath,
389
+ releaseTag,
390
+ });
391
+ logForDebugging(`[dictation] Instalacion via Homebrew: ${executablePath}, modelo en ${modelPath}`);
392
+ return { executablePath, installDir, modelPath, releaseTag };
393
+ }
394
+ function getManualInstallInstructions() {
395
+ if (process.platform === 'darwin') {
396
+ return [
397
+ 'En macOS no hay binarios precompilados en los releases oficiales de whisper.cpp.',
398
+ 'Opciones para instalarlo manualmente:',
399
+ '1) Instala Homebrew (https://brew.sh) y luego ejecuta `brew install whisper-cpp`.',
400
+ '2) Compila desde fuente: clona https://github.com/ggml-org/whisper.cpp y',
401
+ ' ejecuta `cmake -B build && cmake --build build -j --config Release`.',
402
+ 'Despues apunta el backend con la variable CONTEXT_CODE_DICTATION_EXECUTABLE',
403
+ 'y el modelo con CONTEXT_CODE_DICTATION_MODEL, o vuelve a ejecutar /dictar install.',
404
+ ].join('\n');
405
+ }
406
+ if (process.platform === 'linux') {
407
+ return [
408
+ 'En Linux no hay binarios precompilados en los releases oficiales de whisper.cpp.',
409
+ 'Compila desde fuente:',
410
+ ' git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp',
411
+ ' cmake -B build && cmake --build build -j --config Release',
412
+ 'Despues exporta CONTEXT_CODE_DICTATION_EXECUTABLE y CONTEXT_CODE_DICTATION_MODEL,',
413
+ 'o vuelve a ejecutar /dictar install una vez tengas el binario en el PATH.',
414
+ ].join('\n');
415
+ }
416
+ return 'No encontre un binario compatible para tu plataforma.';
417
+ }
418
+ export async function installLocalDictation(modelName) {
419
+ // En macOS los releases oficiales solo publican binarios para Windows e iOS,
420
+ // asi que usamos Homebrew como camino preferido.
421
+ if (process.platform === 'darwin') {
422
+ if (await isCommandAvailable('brew')) {
423
+ return installViaHomebrew(modelName);
424
+ }
425
+ throw new Error(`No se encontro Homebrew en el sistema.\n${getManualInstallInstructions()}`);
426
+ }
427
+ const normalizedModelName = normalizeModelName(modelName);
428
+ const modelFileName = buildModelFileName(normalizedModelName);
429
+ const installDir = getInstallRoot();
430
+ const tempDir = await mkdtemp(join(tmpdir(), 'context-dictation-install-'));
431
+ try {
432
+ const release = await fetchLatestRelease();
433
+ const asset = pickReleaseAsset(release.assets);
434
+ if (!asset) {
435
+ throw new Error(`No encontre un binario compatible de whisper.cpp para ${process.platform}/${process.arch}.\n${getManualInstallInstructions()}`);
436
+ }
437
+ const archivePath = join(tempDir, asset.name);
438
+ const modelPath = join(installDir, 'models', modelFileName);
439
+ await mkdir(installDir, { recursive: true });
440
+ await writeFile(archivePath, await fetchBuffer(asset.browser_download_url, `whisper.cpp (${asset.name})`));
441
+ await rm(installDir, { recursive: true, force: true });
442
+ await mkdir(installDir, { recursive: true });
443
+ await extractArchive(archivePath, installDir);
444
+ const executablePath = await findInstalledExecutable(installDir);
445
+ if (!executablePath) {
446
+ throw new Error('La descarga termino, pero no encontre whisper-cli dentro del paquete instalado.');
447
+ }
448
+ await mkdir(join(installDir, 'models'), { recursive: true });
449
+ await writeFile(modelPath, await fetchBuffer(`${WHISPER_MODEL_BASE_URL}/${modelFileName}?download=1`, `modelo ${modelFileName}`));
450
+ persistInstalledDictationConfig({
451
+ executablePath,
452
+ installDir,
453
+ installedAt: new Date().toISOString(),
454
+ modelName: normalizedModelName,
455
+ modelPath,
456
+ releaseTag: release.tag_name,
457
+ });
458
+ logForDebugging(`[dictation] Instalacion completada en ${installDir} usando ${asset.name}`);
459
+ return {
460
+ executablePath,
461
+ installDir,
462
+ modelPath,
463
+ releaseTag: release.tag_name,
464
+ };
465
+ }
466
+ finally {
467
+ await rm(tempDir, { recursive: true, force: true });
468
+ }
469
+ }
470
+ function buildWavBuffer(chunks) {
471
+ const pcmData = Buffer.concat(chunks);
472
+ const byteRate = SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
473
+ const blockAlign = CHANNELS * (BITS_PER_SAMPLE / 8);
474
+ const header = Buffer.alloc(44);
475
+ header.write('RIFF', 0);
476
+ header.writeUInt32LE(36 + pcmData.length, 4);
477
+ header.write('WAVE', 8);
478
+ header.write('fmt ', 12);
479
+ header.writeUInt32LE(16, 16);
480
+ header.writeUInt16LE(1, 20);
481
+ header.writeUInt16LE(CHANNELS, 22);
482
+ header.writeUInt32LE(SAMPLE_RATE, 24);
483
+ header.writeUInt32LE(byteRate, 28);
484
+ header.writeUInt16LE(blockAlign, 32);
485
+ header.writeUInt16LE(BITS_PER_SAMPLE, 34);
486
+ header.write('data', 36);
487
+ header.writeUInt32LE(pcmData.length, 40);
488
+ return Buffer.concat([header, pcmData]);
489
+ }
490
+ export async function transcribePcmBuffers(chunks, opts) {
491
+ const config = getDictationConfig();
492
+ if (!config) {
493
+ throw new Error(getLocalDictationSetupHint());
494
+ }
495
+ const dir = await mkdtemp(join(tmpdir(), 'context-dictation-'));
496
+ const inputPath = join(dir, 'input.wav');
497
+ const outputPrefix = join(dir, 'result');
498
+ const outputTxtPath = `${outputPrefix}.txt`;
499
+ try {
500
+ await writeFile(inputPath, buildWavBuffer(chunks));
501
+ const args = ['-m', config.model, '-f', inputPath, '-otxt', '-of', outputPrefix];
502
+ const language = opts?.language || config.language;
503
+ if (language && language !== 'auto') {
504
+ args.push('-l', language);
505
+ }
506
+ logForDebugging(`[dictation] Running ${config.executable} ${args.map(part => JSON.stringify(part)).join(' ')}`);
507
+ const result = await execa(config.executable, args, {
508
+ timeout: 120_000,
509
+ windowsHide: true,
510
+ reject: false,
511
+ });
512
+ if (result.exitCode !== 0) {
513
+ throw new Error(result.stderr || result.stdout || 'Whisper finalizo con error.');
514
+ }
515
+ return (await readFile(outputTxtPath, 'utf8')).trim();
516
+ }
517
+ finally {
518
+ await rm(dir, { recursive: true, force: true });
519
+ }
520
+ }
@@ -16,7 +16,7 @@ let audioNapiPromise = null;
16
16
  function loadAudioNapi() {
17
17
  audioNapiPromise ??= (async () => {
18
18
  const t0 = Date.now();
19
- const mod = await import('audio-capture-napi');
19
+ const mod = await import('../../vendor/audio-capture-src/index.js');
20
20
  // vendor/audio-capture-src/index.ts defers require(...node) until the
21
21
  // first function call — trigger it here so timing reflects real cost.
22
22
  mod.isNativeAudioAvailable();
@@ -146,7 +146,9 @@ export async function checkVoiceDependencies() {
146
146
  if (process.platform === 'win32') {
147
147
  return {
148
148
  available: false,
149
- missing: ['Voice mode requires the native audio module (not loaded)'],
149
+ missing: [
150
+ 'El dictado local requiere el modulo nativo de audio y no se pudo cargar.',
151
+ ],
150
152
  installCommand: null,
151
153
  };
152
154
  }
@@ -189,7 +191,7 @@ export async function checkRecordingAvailability() {
189
191
  if (isRunningOnHomespace() || isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) {
190
192
  return {
191
193
  available: false,
192
- reason: 'Voice mode requires microphone access, but no audio device is available in this environment.\n\nTo use voice mode, run Context Code locally instead.',
194
+ reason: 'El dictado local necesita acceso al microfono, pero este entorno no tiene un dispositivo de audio disponible.\n\nPara usar dictado, ejecuta Context Code de forma local.',
193
195
  };
194
196
  }
195
197
  // Native audio module (cpal) handles everything on macOS, Linux, and Windows
@@ -201,10 +203,10 @@ export async function checkRecordingAvailability() {
201
203
  if (process.platform === 'win32') {
202
204
  return {
203
205
  available: false,
204
- reason: 'Voice recording requires the native audio module, which could not be loaded.',
206
+ reason: 'La grabacion para dictado requiere el modulo nativo de audio y no se pudo cargar.',
205
207
  };
206
208
  }
207
- const wslNoAudioReason = 'Voice mode could not access an audio device in WSL.\n\nWSL2 with WSLg (Windows 11) provides audio via PulseAudio if you are on Windows 10 or WSL1, run Context Code in native Windows instead.';
209
+ const wslNoAudioReason = 'El dictado local no pudo acceder a un dispositivo de audio en WSL.\n\nWSL2 con WSLg (Windows 11) expone audio mediante PulseAudio. Si estas en Windows 10 o WSL1, ejecuta Context Code directamente en Windows.';
208
210
  // On Linux (including WSL), probe arecord. hasCommand() is insufficient:
209
211
  // the binary can exist while the device open() fails (WSL1, Win10-WSL2,
210
212
  // headless Linux). WSL2+WSLg (Win11 default) works via PulseAudio RDP
@@ -239,8 +241,8 @@ export async function checkRecordingAvailability() {
239
241
  return {
240
242
  available: false,
241
243
  reason: pm
242
- ? `Voice mode requires SoX for audio recording. Install it with: ${pm.displayCommand}`
243
- : 'Voice mode requires SoX for audio recording. Install SoX manually:\n macOS: brew install sox\n Ubuntu/Debian: sudo apt-get install sox\n Fedora: sudo dnf install sox',
244
+ ? `El dictado local requiere SoX para grabar audio. Instalalo con: ${pm.displayCommand}`
245
+ : 'El dictado local requiere SoX para grabar audio. Instalalo manualmente:\n macOS: brew install sox\n Ubuntu/Debian: sudo apt-get install sox\n Fedora: sudo dnf install sox',
244
246
  };
245
247
  }
246
248
  return { available: true, reason: null };
@@ -1,4 +1,3 @@
1
- import { feature } from '../recovery/bunBundleShim.js';
2
1
  import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import { createRequire } from 'module';
4
3
  const require = createRequire(import.meta.url);
@@ -10,9 +9,8 @@ import { logForDebugging } from '../utils/debug.js';
10
9
  import { createDisabledBypassPermissionsContext, isBypassPermissionsModeDisabled } from '../utils/permissions/permissionSetup.js';
11
10
  import { applySettingsChange } from '../utils/settings/applySettingsChange.js';
12
11
  import { createStore } from './store.js';
13
- // DCE: voice context is ant-only. External builds get a passthrough.
14
12
  /* eslint-disable @typescript-eslint/no-require-imports */
15
- const VoiceProvider = feature('VOICE_MODE') ? require('../context/voice.js').VoiceProvider : ({ children }) => children;
13
+ const VoiceProvider = require('../context/voice.js').VoiceProvider;
16
14
  /* eslint-enable @typescript-eslint/no-require-imports */
17
15
  import { getDefaultAppState } from './AppStateStore.js';
18
16
  // TODO: Remove these re-exports once all callers import directly from
@@ -190,33 +190,31 @@ export const ConfigTool = buildTool({
190
190
  finalValue === true) {
191
191
  const { isVoiceModeEnabled } = await import('../../voice/voiceModeEnabled.js');
192
192
  if (!isVoiceModeEnabled()) {
193
- const { isAnthropicAuthEnabled } = await import('../../utils/auth.js');
194
193
  return {
195
194
  data: {
196
195
  success: false,
197
- error: !isAnthropicAuthEnabled()
198
- ? 'Voice mode requires a Claude.ai account. Please run /login to sign in.'
199
- : 'Voice mode is not available.',
196
+ error: 'El modo de dictado no esta disponible.',
200
197
  },
201
198
  };
202
199
  }
203
- const { isVoiceStreamAvailable } = await import('../../services/voiceStreamSTT.js');
204
200
  const { checkRecordingAvailability, checkVoiceDependencies, requestMicrophonePermission, } = await import('../../services/voice.js');
201
+ const { checkLocalDictationConfiguration } = await import('../../services/localDictation.js');
205
202
  const recording = await checkRecordingAvailability();
206
203
  if (!recording.available) {
207
204
  return {
208
205
  data: {
209
206
  success: false,
210
207
  error: recording.reason ??
211
- 'Voice mode is not available in this environment.',
208
+ 'El modo de dictado no esta disponible en este entorno.',
212
209
  },
213
210
  };
214
211
  }
215
- if (!isVoiceStreamAvailable()) {
212
+ const dictation = await checkLocalDictationConfiguration();
213
+ if (!dictation.available) {
216
214
  return {
217
215
  data: {
218
216
  success: false,
219
- error: 'Voice mode requires a Claude.ai account. Please run /login to sign in.',
217
+ error: dictation.error ?? 'El dictado local no esta configurado.',
220
218
  },
221
219
  };
222
220
  }
@@ -225,27 +223,26 @@ export const ConfigTool = buildTool({
225
223
  return {
226
224
  data: {
227
225
  success: false,
228
- error: 'No audio recording tool found.' +
229
- (deps.installCommand ? ` Run: ${deps.installCommand}` : ''),
226
+ error: 'No se encontro una herramienta de grabacion de audio.' +
227
+ (deps.installCommand ? ` Ejecuta: ${deps.installCommand}` : ''),
230
228
  },
231
229
  };
232
230
  }
233
231
  if (!(await requestMicrophonePermission())) {
234
232
  let guidance;
235
233
  if (process.platform === 'win32') {
236
- guidance = 'Settings \u2192 Privacy \u2192 Microphone';
234
+ guidance = 'Configuracion > Privacidad > Microfono';
237
235
  }
238
236
  else if (process.platform === 'linux') {
239
- guidance = "your system's audio settings";
237
+ guidance = 'la configuracion de audio del sistema';
240
238
  }
241
239
  else {
242
- guidance =
243
- 'System Settings \u2192 Privacy & Security \u2192 Microphone';
240
+ guidance = 'System Settings > Privacy & Security > Microphone';
244
241
  }
245
242
  return {
246
243
  data: {
247
244
  success: false,
248
- error: `Microphone access is denied. To enable it, go to ${guidance}, then try again.`,
245
+ error: `El acceso al microfono esta denegado. Habilitalo en ${guidance} y vuelve a intentar.`,
249
246
  },
250
247
  };
251
248
  }