opencode-interrupt-plugin 0.4.6 → 0.4.8

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/tts/index.js CHANGED
@@ -129,6 +129,8 @@ export class TTSStreamer extends EventEmitter {
129
129
  return this.engine.isPlaying || this.queue.length > 0 || this.processing || (Date.now() - this.lastActivity < 3000);
130
130
  }
131
131
  resetSession() {
132
+ this.stopped = false;
133
+ this.processing = false;
132
134
  this.buffer.clear();
133
135
  this.queue = [];
134
136
  this.spokenText = "";
@@ -136,9 +138,15 @@ export class TTSStreamer extends EventEmitter {
136
138
  }
137
139
  async processQueue() {
138
140
  this.processing = true;
141
+ debug(`[interrupt-tts] processQueue start, queue=${this.queue.length}, stopped=${this.stopped}`);
139
142
  while (this.queue.length > 0 && !this.stopped) {
140
143
  const sentence = this.queue.shift();
141
- await this.engine.speak(sentence);
144
+ try {
145
+ await this.engine.speak(sentence);
146
+ }
147
+ catch (err) {
148
+ debug(`[interrupt-tts] TTS error in processQueue: ${err}`);
149
+ }
142
150
  if (!this.stopped) {
143
151
  this.spokenText += sentence + " ";
144
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-interrupt-plugin",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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",