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.
- package/bin/ttc +1 -1
- package/package.json +1 -1
- 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} (
|
|
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
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
|
-
|
|
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
|
-
|
|
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 };
|