opencode-interrupt-plugin 0.4.5 → 0.4.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.
@@ -1,14 +1,15 @@
1
1
  import { EventEmitter } from 'events';
2
2
  export declare class AudioMonitor extends EventEmitter {
3
- private pollTimer;
3
+ private proc;
4
4
  private isRunning;
5
5
  private threshold;
6
- private pollMs;
7
- constructor(threshold?: number, pollMs?: number);
6
+ private windowSamples;
7
+ private sumSquares;
8
+ private totalSamples;
9
+ constructor(threshold?: number, windowMs?: number);
8
10
  start(): void;
9
- private poll;
11
+ private startCapture;
10
12
  stop(): void;
11
- private handleRMS;
12
13
  setThreshold(threshold: number): void;
13
14
  isActive(): boolean;
14
15
  }
@@ -1,72 +1,84 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { EventEmitter } from 'events';
3
3
  import { debug } from '../log.js';
4
+ const SAMPLE_RATE = 44100;
5
+ const CHANNELS = 1;
6
+ const BITS = 16;
4
7
  export class AudioMonitor extends EventEmitter {
5
- pollTimer = null;
8
+ proc = null;
6
9
  isRunning = false;
7
10
  threshold;
8
- pollMs;
9
- constructor(threshold = 0.02, pollMs = 100) {
11
+ windowSamples;
12
+ sumSquares = 0;
13
+ totalSamples = 0;
14
+ constructor(threshold = 0.02, windowMs = 100) {
10
15
  super();
11
16
  this.threshold = threshold;
12
- this.pollMs = pollMs;
17
+ this.windowSamples = Math.floor(SAMPLE_RATE * windowMs / 1000);
13
18
  }
14
19
  start() {
15
20
  if (this.isRunning)
16
21
  return;
17
- debug('[interrupt-monitor] Starting audio monitor');
22
+ debug('[interrupt-monitor] Starting audio monitor (continuous mode)');
18
23
  this.isRunning = true;
19
24
  this.emit('started');
20
- this.poll();
25
+ this.startCapture();
21
26
  }
22
- poll() {
23
- if (!this.isRunning)
24
- return;
25
- const proc = spawn('sox', [
27
+ startCapture() {
28
+ this.sumSquares = 0;
29
+ this.totalSamples = 0;
30
+ this.proc = spawn('sox', [
26
31
  '-q', '-d',
27
- '-t', 'sox', '-',
28
- 'trim', '0', '0.1',
29
- 'stat',
30
- ], { stdio: ['ignore', 'ignore', 'pipe'] });
31
- let out = '';
32
- proc.stderr?.on('data', (d) => { out += d.toString(); });
33
- proc.on('exit', (code) => {
32
+ '-t', 'raw',
33
+ '-b', String(BITS),
34
+ '-e', 'signed',
35
+ '-r', String(SAMPLE_RATE),
36
+ '-c', String(CHANNELS),
37
+ '-',
38
+ ]);
39
+ this.proc.stdout?.on('data', (buf) => {
34
40
  if (!this.isRunning)
35
41
  return;
36
- if (code !== 0) {
37
- debug(`[interrupt-monitor] sox exited with code ${code}`);
42
+ const BYTES_PER_SAMPLE = BITS / 8;
43
+ for (let i = 0; i + BYTES_PER_SAMPLE <= buf.length; i += BYTES_PER_SAMPLE) {
44
+ const sample = buf.readInt16LE(i);
45
+ this.sumSquares += sample * sample;
46
+ this.totalSamples++;
38
47
  }
39
- const rms = parseRMSFromStat(out);
40
- if (rms !== null) {
48
+ if (this.totalSamples >= this.windowSamples) {
49
+ const rms = Math.sqrt(this.sumSquares / this.totalSamples) / 32768;
41
50
  debug(`[interrupt-monitor] RMS: ${rms.toFixed(6)} (threshold: ${this.threshold})`);
42
- this.handleRMS(rms);
43
- }
44
- else {
45
- debug(`[interrupt-monitor] Failed to parse RMS from stderr (${out.length} chars)`);
51
+ if (rms > this.threshold) {
52
+ this.emit('voice-detected', rms);
53
+ }
54
+ this.sumSquares = 0;
55
+ this.totalSamples = 0;
46
56
  }
47
- this.pollTimer = setTimeout(() => this.poll(), this.pollMs);
48
57
  });
49
- proc.on('error', (err) => {
58
+ this.proc.on('error', (err) => {
50
59
  if (!this.isRunning)
51
60
  return;
52
- debug(`[interrupt-monitor] sox spawn error: ${err.message}`);
61
+ debug(`[interrupt-monitor] sox error: ${err.message}`);
53
62
  this.emit('error', new Error('sox not found. Install it: brew install sox (macOS) or apt install sox (Linux)'));
54
63
  this.isRunning = false;
55
64
  });
65
+ this.proc.on('close', (code) => {
66
+ if (!this.isRunning)
67
+ return;
68
+ debug(`[interrupt-monitor] sox exited (code ${code}), restarting...`);
69
+ if (this.isRunning) {
70
+ this.startCapture();
71
+ }
72
+ });
56
73
  }
57
74
  stop() {
58
75
  this.isRunning = false;
59
- if (this.pollTimer) {
60
- clearTimeout(this.pollTimer);
61
- this.pollTimer = null;
76
+ if (this.proc) {
77
+ this.proc.kill();
78
+ this.proc = null;
62
79
  }
63
80
  this.emit('stopped');
64
81
  }
65
- handleRMS(rms) {
66
- if (rms > this.threshold) {
67
- this.emit('voice-detected', rms);
68
- }
69
- }
70
82
  setThreshold(threshold) {
71
83
  this.threshold = threshold;
72
84
  }
@@ -74,17 +86,3 @@ export class AudioMonitor extends EventEmitter {
74
86
  return this.isRunning;
75
87
  }
76
88
  }
77
- function parseRMSLine(line) {
78
- const match = line.match(/RMS\s+amplitude:\s+([\d.]+)/);
79
- if (match)
80
- return parseFloat(match[1]);
81
- return null;
82
- }
83
- function parseRMSFromStat(output) {
84
- for (const line of output.split('\n')) {
85
- const rms = parseRMSLine(line);
86
- if (rms !== null)
87
- return rms;
88
- }
89
- return null;
90
- }
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ const DEFAULT_TTS_VOICE = "en-US-AvaNeural";
2
2
  const DEFAULT_TTS_RATE = "+25%";
3
3
  export const FREE_DEFAULTS = {
4
4
  isLicensed: false,
5
- micThreshold: 0.02,
5
+ micThreshold: 0.008,
6
6
  sensitivity: 'medium',
7
7
  timingWindowMs: 30000,
8
8
  maxCorrectionLength: 1000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-interrupt-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Streaming TTS + voice interruption for OpenCode. Speaks responses as they arrive and detects when you talk over it.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",