opencode-interrupt-plugin 0.4.4 → 0.4.6
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/dist/audio/monitor.js +6 -5
- package/dist/audio/overlap.js +4 -3
- package/dist/config.js +1 -1
- package/dist/index.js +3 -2
- package/dist/log.d.ts +3 -0
- package/dist/log.js +20 -0
- package/dist/tts/engine.js +4 -3
- package/dist/tts/index.js +5 -4
- package/package.json +1 -1
package/dist/audio/monitor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
+
import { debug } from '../log.js';
|
|
3
4
|
export class AudioMonitor extends EventEmitter {
|
|
4
5
|
pollTimer = null;
|
|
5
6
|
isRunning = false;
|
|
@@ -13,7 +14,7 @@ export class AudioMonitor extends EventEmitter {
|
|
|
13
14
|
start() {
|
|
14
15
|
if (this.isRunning)
|
|
15
16
|
return;
|
|
16
|
-
|
|
17
|
+
debug('[interrupt-monitor] Starting audio monitor');
|
|
17
18
|
this.isRunning = true;
|
|
18
19
|
this.emit('started');
|
|
19
20
|
this.poll();
|
|
@@ -33,22 +34,22 @@ export class AudioMonitor extends EventEmitter {
|
|
|
33
34
|
if (!this.isRunning)
|
|
34
35
|
return;
|
|
35
36
|
if (code !== 0) {
|
|
36
|
-
|
|
37
|
+
debug(`[interrupt-monitor] sox exited with code ${code}`);
|
|
37
38
|
}
|
|
38
39
|
const rms = parseRMSFromStat(out);
|
|
39
40
|
if (rms !== null) {
|
|
40
|
-
|
|
41
|
+
debug(`[interrupt-monitor] RMS: ${rms.toFixed(6)} (threshold: ${this.threshold})`);
|
|
41
42
|
this.handleRMS(rms);
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
44
|
-
|
|
45
|
+
debug(`[interrupt-monitor] Failed to parse RMS from stderr (${out.length} chars)`);
|
|
45
46
|
}
|
|
46
47
|
this.pollTimer = setTimeout(() => this.poll(), this.pollMs);
|
|
47
48
|
});
|
|
48
49
|
proc.on('error', (err) => {
|
|
49
50
|
if (!this.isRunning)
|
|
50
51
|
return;
|
|
51
|
-
|
|
52
|
+
debug(`[interrupt-monitor] sox spawn error: ${err.message}`);
|
|
52
53
|
this.emit('error', new Error('sox not found. Install it: brew install sox (macOS) or apt install sox (Linux)'));
|
|
53
54
|
this.isRunning = false;
|
|
54
55
|
});
|
package/dist/audio/overlap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AudioMonitor } from './monitor.js';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
+
import { debug } from '../log.js';
|
|
3
4
|
export class VoiceOverlapDetector extends EventEmitter {
|
|
4
5
|
monitor;
|
|
5
6
|
active = false;
|
|
@@ -37,17 +38,17 @@ export class VoiceOverlapDetector extends EventEmitter {
|
|
|
37
38
|
handleVoiceDetected(rms) {
|
|
38
39
|
const ttsPlaying = this.getIsTTSPlaying();
|
|
39
40
|
if (!ttsPlaying) {
|
|
40
|
-
|
|
41
|
+
debug(`[interrupt-overlap] Voice detected (RMS=${rms.toFixed(6)}) but TTS not playing, ignoring`);
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
44
|
if (Date.now() < this.cooldownUntil) {
|
|
44
|
-
|
|
45
|
+
debug(`[interrupt-overlap] In cooldown (${this.cooldownUntil - Date.now()}ms remaining), ignoring`);
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
const now = Date.now();
|
|
48
49
|
this.cooldownUntil = now + this.COOLDOWN_MS;
|
|
49
50
|
const partialContent = this.getPartialContent();
|
|
50
|
-
|
|
51
|
+
debug(`[interrupt-overlap] OVERLAP! RMS=${rms.toFixed(6)}, stopping TTS, partial=${partialContent.slice(0, 80)}`);
|
|
51
52
|
this.stopTTS();
|
|
52
53
|
const event = {
|
|
53
54
|
partialTTSContent: partialContent,
|
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveConfig } from './config.js';
|
|
2
2
|
import { checkLicense } from './license/guard.js';
|
|
3
|
+
import { debug } from './log.js';
|
|
3
4
|
import { getSessionState, updateSessionState, clearSessionState, } from './store.js';
|
|
4
5
|
import { prepareInjection } from './injector.js';
|
|
5
6
|
import { onTTSStart, onTTSEnd, isTTSTool } from './audio/tts-tracker.js';
|
|
@@ -50,10 +51,10 @@ export const InterruptPlugin = (userConfig = {}) => {
|
|
|
50
51
|
}, () => ttsStreamer.getPartialContent());
|
|
51
52
|
overlapDetector.on('voice-overlap', (event) => {
|
|
52
53
|
if (!activeSessionId) {
|
|
53
|
-
|
|
54
|
+
debug(`[interrupt] Voice overlap event but no active session, ignoring`);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
debug(`[interrupt] Voice overlap detected (RMS: ${event.rmsLevel.toFixed(4)})`);
|
|
57
58
|
updateSessionState(activeSessionId, {
|
|
58
59
|
wasInterrupted: true,
|
|
59
60
|
partialContentAtInterrupt: event.partialTTSContent,
|
package/dist/log.d.ts
ADDED
package/dist/log.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { appendFileSync } from 'fs';
|
|
2
|
+
const LOG_FILE = '/tmp/interrupt-debug.log';
|
|
3
|
+
let enabled = true;
|
|
4
|
+
export function enableDebugLogging() {
|
|
5
|
+
enabled = true;
|
|
6
|
+
}
|
|
7
|
+
export function disableDebugLogging() {
|
|
8
|
+
enabled = false;
|
|
9
|
+
}
|
|
10
|
+
export function debug(...args) {
|
|
11
|
+
if (!enabled)
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
const msg = args.map(a => String(a)).join(' ');
|
|
15
|
+
appendFileSync(LOG_FILE, `[${new Date().toISOString().slice(11, 23)}] ${msg}\n`);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// ignore
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/tts/engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { debug } from "../log.js";
|
|
4
5
|
const VENV_PYTHON = join(process.env.HOME || "/home/pystar", ".config", "opencode", "tts-venv", "bin", "python");
|
|
5
6
|
const DEFAULT_VOICE = "en-US-AvaNeural";
|
|
6
7
|
const DEFAULT_RATE = "+25%";
|
|
@@ -40,14 +41,14 @@ export class TTSEngine extends EventEmitter {
|
|
|
40
41
|
let playDone = false;
|
|
41
42
|
const checkDone = () => {
|
|
42
43
|
if (genDone && playDone) {
|
|
43
|
-
|
|
44
|
+
debug(`[interrupt-tts] stream done text="${text.slice(0, 60)}" total=${Date.now() - t0}ms`);
|
|
44
45
|
resolve();
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
48
|
this.generator.on("close", (code) => {
|
|
48
49
|
genDone = true;
|
|
49
50
|
if (code !== 0) {
|
|
50
|
-
|
|
51
|
+
debug(`[interrupt-tts] edge-tts exited ${code}`);
|
|
51
52
|
}
|
|
52
53
|
checkDone();
|
|
53
54
|
});
|
|
@@ -78,7 +79,7 @@ export class TTSEngine extends EventEmitter {
|
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
stop() {
|
|
81
|
-
|
|
82
|
+
debug(`[interrupt-engine] stop() called, playing=${this.playing}`);
|
|
82
83
|
if (this.generator) {
|
|
83
84
|
this.generator.kill("SIGTERM");
|
|
84
85
|
this.generator = null;
|
package/dist/tts/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { SentenceBuffer } from "./buffer.js";
|
|
3
3
|
import { TTSEngine } from "./engine.js";
|
|
4
|
+
import { debug } from "../log.js";
|
|
4
5
|
export class TTSStreamer extends EventEmitter {
|
|
5
6
|
buffer = new SentenceBuffer();
|
|
6
7
|
engine;
|
|
@@ -36,7 +37,7 @@ export class TTSStreamer extends EventEmitter {
|
|
|
36
37
|
return;
|
|
37
38
|
this.lastActivity = Date.now();
|
|
38
39
|
const sentences = this.buffer.feed(chunk);
|
|
39
|
-
|
|
40
|
+
debug(`[interrupt-tts] chunk len=${chunk.text.length} sentences=${sentences.length} queue=${this.queue.length} processing=${this.processing} buflen=${this.buffer.length}`);
|
|
40
41
|
if (sentences.length > 0) {
|
|
41
42
|
this.cancelForceFlush();
|
|
42
43
|
for (const s of sentences) {
|
|
@@ -57,7 +58,7 @@ export class TTSStreamer extends EventEmitter {
|
|
|
57
58
|
return;
|
|
58
59
|
const text = this.buffer.flush();
|
|
59
60
|
if (text) {
|
|
60
|
-
|
|
61
|
+
debug(`[interrupt-tts] force-flush "${text.slice(0, 60)}"`);
|
|
61
62
|
this.queue.push(text);
|
|
62
63
|
if (!this.processing) {
|
|
63
64
|
this.processQueue();
|
|
@@ -76,7 +77,7 @@ export class TTSStreamer extends EventEmitter {
|
|
|
76
77
|
return;
|
|
77
78
|
const remaining = this.buffer.flush();
|
|
78
79
|
if (remaining) {
|
|
79
|
-
|
|
80
|
+
debug(`[interrupt-tts] flush remaining="${remaining.slice(0, 80)}"`);
|
|
80
81
|
this.queue.push(remaining);
|
|
81
82
|
}
|
|
82
83
|
if (!this.processing)
|
|
@@ -104,7 +105,7 @@ export class TTSStreamer extends EventEmitter {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
stop() {
|
|
107
|
-
|
|
108
|
+
debug(`[interrupt-tts] stop() called, playing=${this.engine.isPlaying}, queue=${this.queue.length}, processing=${this.processing}`);
|
|
108
109
|
this.stopped = true;
|
|
109
110
|
this.queue = [];
|
|
110
111
|
this.engine.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-interrupt-plugin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|