talk-to-copilot 1.0.3 → 1.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.
Files changed (3) hide show
  1. package/bin/ttc +1 -1
  2. package/package.json +1 -1
  3. package/src/config.js +72 -12
package/bin/ttc CHANGED
@@ -77,7 +77,7 @@ function runSetup() {
77
77
  console.log('\n─────────────────────────────────────');
78
78
  console.log('Config:', cfg.CONFIG_PATH);
79
79
  console.log(` modelPath: ${config.modelPath || '(not set)'}`);
80
- console.log(` audioDevice: ${config.audioDevice} (avfoundation mic index)`);
80
+ console.log(` audioDevice: ${config.audioDevice} (auto-detected override in config if wrong)`);
81
81
  console.log(` autoSubmit: ${config.autoSubmit} (auto-press Enter after transcription)`);
82
82
  console.log('\nHotkeys (inside talk):');
83
83
  console.log(' Ctrl+R → Start / stop voice recording');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talk-to-copilot",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Voice + screenshot input wrapper for GitHub Copilot CLI — use your mic and screen instead of typing",
5
5
  "bin": {
6
6
  "ttc": "bin/ttc"
package/src/config.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const { execFileSync } = require('child_process');
6
7
 
7
8
  const CONFIG_PATH = path.join(os.homedir(), '.copilot', 'talk-to-copilot.json');
8
9
 
@@ -20,26 +21,85 @@ const WHISPER_MODEL_CANDIDATES = [
20
21
  '/usr/local/share/whisper.cpp/models/ggml-base.en.bin',
21
22
  ];
22
23
 
24
+ /** Score an audio device name — higher = more likely to be the real microphone. */
25
+ function scoreMicDevice(name) {
26
+ const n = name.toLowerCase();
27
+
28
+ // Hard skip — virtual/loopback/software devices almost never contain the user's voice
29
+ if (/teams|zoom|loopback|soundflower|blackhole|virtual|aggregate|multi.output/.test(n)) return -1;
30
+
31
+ if (n.includes('built-in')) return 100;
32
+ if (n.includes('macbook') && n.includes('microphone')) return 90;
33
+ if (n.includes('microphone')) return 80; // any real-sounding mic
34
+ if (n.includes('iphone') || n.includes('ipad')) return 50; // continuity mic — fine but secondary
35
+
36
+ return 10; // unknown device — prefer over virtual but below named mics
37
+ }
38
+
39
+ /**
40
+ * Parse `ffmpeg -f avfoundation -list_devices true -i ""` stderr and return
41
+ * the avfoundation index (e.g. ":2") for the best microphone found.
42
+ * Returns null if detection fails.
43
+ */
44
+ function detectMicrophone() {
45
+ try {
46
+ const output = execFileSync('ffmpeg', [
47
+ '-f', 'avfoundation', '-list_devices', 'true', '-i', '',
48
+ ], { encoding: 'utf8', stdio: ['ignore', 'ignore', 'pipe'] });
49
+
50
+ return parseBestMic(output);
51
+ } catch (err) {
52
+ // ffmpeg exits non-zero when listing devices — stderr is in err.stderr
53
+ return parseBestMic(err.stderr || '');
54
+ }
55
+ }
56
+
57
+ function parseBestMic(output) {
58
+ // Match lines like: [AVFoundation indev @ 0x...] [2] MacBook Pro Microphone
59
+ const deviceRe = /\[AVFoundation.*?\]\s+\[(\d+)\]\s+(.+)/g;
60
+
61
+ let best = null;
62
+ let bestScore = -Infinity;
63
+ let inAudioSection = false;
64
+
65
+ for (const line of output.split('\n')) {
66
+ if (line.includes('AVFoundation audio devices')) { inAudioSection = true; continue; }
67
+ if (line.includes('AVFoundation video devices')) { inAudioSection = false; continue; }
68
+ if (!inAudioSection) continue;
69
+
70
+ const m = deviceRe.exec(line);
71
+ deviceRe.lastIndex = 0; // reset for next iteration
72
+ if (!m) continue;
73
+
74
+ const [, index, name] = m;
75
+ const score = scoreMicDevice(name.trim());
76
+ if (score > bestScore) {
77
+ bestScore = score;
78
+ best = `:${index}`;
79
+ }
80
+ }
81
+
82
+ return best;
83
+ }
84
+
23
85
  function findWhisperModel() {
24
86
  return WHISPER_MODEL_CANDIDATES.find(p => fs.existsSync(p)) || null;
25
87
  }
26
88
 
27
89
  function load() {
90
+ const fileConfig = fs.existsSync(CONFIG_PATH)
91
+ ? (() => { try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; } })()
92
+ : {};
93
+
94
+ // Auto-detect the microphone unless the user has explicitly set audioDevice in their config
95
+ const audioDevice = fileConfig.audioDevice || detectMicrophone() || ':0';
96
+
28
97
  const defaults = {
29
98
  modelPath: findWhisperModel(),
30
- audioDevice: ':0', // avfoundation default mic
31
- autoSubmit: false, // whether to press Enter after injecting transcription
32
- recordKey: 'ctrl+r',
33
- screenshotKey: 'ctrl+p',
99
+ autoSubmit: false,
34
100
  };
35
101
 
36
- if (!fs.existsSync(CONFIG_PATH)) return defaults;
37
-
38
- try {
39
- return Object.assign(defaults, JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')));
40
- } catch {
41
- return defaults;
42
- }
102
+ return Object.assign(defaults, fileConfig, { audioDevice });
43
103
  }
44
104
 
45
105
  function save(config) {
@@ -47,4 +107,4 @@ function save(config) {
47
107
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
48
108
  }
49
109
 
50
- module.exports = { load, save, findWhisperModel, CONFIG_PATH };
110
+ module.exports = { load, save, findWhisperModel, detectMicrophone, CONFIG_PATH };