Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qwen3-tts-server/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fi
echo " Python: $py_version"

# ── Virtual environment ──────────────────────────────────────────────────────
if [[ -d venv ]] && [[ ! -x venv/bin/python3 ]] && [[ ! -x venv/bin/python ]]; then
if [[ -d venv ]] && { [[ ! -x venv/bin/python3 ]] && [[ ! -x venv/bin/python ]]; } || [[ ! -e venv/bin/pip ]]; then
warn "Existing virtual environment looks stale — recreating it."
rm -rf venv
fi
Expand Down
39 changes: 38 additions & 1 deletion src/audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ function getPlaybackCommand(platform, volume, cachePath) {
return { cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", cachePath] };
}
if (platform === "linux") {
return { cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", cachePath] };
try { execSync("command -v ffplay", { stdio: "ignore" }); return { cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", cachePath] }; } catch {}
try { execSync("command -v paplay", { stdio: "ignore" }); return { cmd: "paplay", args: [cachePath] }; } catch {}
try { execSync("command -v pw-play", { stdio: "ignore" }); return { cmd: "pw-play", args: [cachePath] }; } catch {}
}
return null;
}
Expand Down Expand Up @@ -344,6 +346,35 @@ function downloadChatterbox(phrase, cachePath, config, voicePath, ttsParams) {
});
}

function downloadEspeak(phrase, cachePath) {
return new Promise((resolve) => {
const proc = spawn("espeak-ng", [
"-w", cachePath,
"-s", "150",
phrase,
], { stdio: ["ignore", "ignore", "ignore"] });
proc.on("close", () => resolve());
proc.on("error", () => resolve());
});
}

function downloadPiper(phrase, cachePath, config) {
return new Promise((resolve) => {
const piperBin = config.piper_binary || process.env.PIPER_BIN || "";
const piperModel = config.piper_model || "";
if (!piperModel) return resolve();
const args = ["-m", piperModel, "-f", cachePath];
const extraArgs = config.piper_args || [];
const proc = spawn(piperBin || "piper", [...args, ...extraArgs], {
stdio: ["pipe", "ignore", "ignore"],
});
proc.stdin.write(phrase);
proc.stdin.end();
proc.on("close", () => resolve());
proc.on("error", () => resolve());
});
}

function downloadQwen(phrase, cachePath, config, voiceId) {
return new Promise((resolve) => {
const qwenUrl = config.qwen_tts_url || "http://localhost:8100";
Expand Down Expand Up @@ -396,6 +427,12 @@ function downloadQwen(phrase, cachePath, config, voiceId) {
}

async function downloadToCache(phrase, cachePath, config, voicePath, ttsParams, refText) {
if (config.tts_backend === "espeak") {
return downloadEspeak(phrase, cachePath);
}
if (config.tts_backend === "piper") {
return downloadPiper(phrase, cachePath, config);
}
if (config.tts_backend === "qwen") {
const voiceId = await registerVoiceWithQwen(config, voicePath, refText);
return downloadQwen(phrase, cachePath, config, voiceId);
Expand Down
12 changes: 8 additions & 4 deletions src/llm.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ function generatePhraseLocal(context, config, style, llmTemperature, examples) {
const model = local.model || "default";
const maxTokens = local.max_tokens || 50;
const timeout = local.timeout || 15000;
const apiKey = local.api_key || config.llm_api_key || process.env.ANTHROPIC_AUTH_TOKEN || process.env.API_KEY || "";

const messages = [
{ role: "system", content: buildSystemPrompt(style, "status-report", examples) },
Expand All @@ -213,14 +214,17 @@ function generatePhraseLocal(context, config, style, llmTemperature, examples) {
const isHttps = url.protocol === "https:";
const reqFn = isHttps ? httpsRequest : httpRequest;

const headers = {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
};
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;

const req = reqFn(
url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
},
headers,
timeout,
},
(res) => {
Expand Down
43 changes: 42 additions & 1 deletion src/voxlert.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
*/

import { resolvePrefix, DEFAULT_PREFIX } from "./prefix.js";
import { appendFileSync, mkdirSync } from "fs";
import { appendFileSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
import { hostname } from "os";
import { execSync } from "child_process";
import { loadConfig, EVENT_MAP, CONTEXTUAL_EVENTS, FALLBACK_PHRASES } from "./config.js";
import { extractContext, generatePhrase } from "./llm.js";
import { speakPhrase } from "./audio.js";
import { showOverlay } from "./overlay.js";
import { loadPack } from "./packs.js";
import { join } from "path";
import { STATE_DIR, LOG_FILE, HOOK_DEBUG_LOG } from "./paths.js";
import { appendLog } from "./activity-log.js";

const TASK_START_FILE = join(STATE_DIR, "task-start.json");

function debugLog(msg, data) {
try {
mkdirSync(STATE_DIR, { recursive: true });
Expand All @@ -43,6 +46,31 @@ function logFallback(eventName, reason, detail) {
}
}

function getTaskStartTime() {
try {
if (existsSync(TASK_START_FILE)) {
const data = JSON.parse(readFileSync(TASK_START_FILE, "utf-8"));
return data.timestamp || 0;
}
} catch {}
return 0;
}

function setTaskStartTime() {
try {
mkdirSync(STATE_DIR, { recursive: true });
writeFileSync(TASK_START_FILE, JSON.stringify({ timestamp: Date.now() }));
} catch {}
}

function shouldAnnounceStop(config) {
const minDuration = (config.min_task_duration_ms ?? 30000);
const start = getTaskStartTime();
if (!start) return true; // no start recorded, allow
const elapsed = Date.now() - start;
return elapsed >= minDuration;
}

/**
* Auto-detect tmux pane context from the environment.
* Returns an object with { pane_id, session_name, window_index } when
Expand Down Expand Up @@ -102,6 +130,19 @@ export async function processHookEvent(eventData) {
return;
}

// Track task start time on user prompt submit (always, regardless of category filter)
if (eventName === "UserPromptSubmit") {
setTaskStartTime();
debugLog("processHookEvent task start recorded", { source });
}

// Skip short events for all categories (always, regardless of category filter)
if (!shouldAnnounceStop(config)) {
const elapsed = Date.now() - getTaskStartTime();
debugLog("processHookEvent skip: short task", { source, eventName, elapsed_ms: elapsed });
return;
}

// Check source-specific overrides (config.sources.<source>.enabled / .categories)
const sourceOverride = (config.sources || {})[source] || {};
if (sourceOverride.enabled === false) {
Expand Down