← back to Voice Chat
auto-shutoff: 30s idle countdown with visible timer + resets on speech
326417dde28251c4e8f89bae097078d145f49f80 · 2026-05-11 08:31:00 -0700 · Steve Abrams
Files touched
M public/index.htmlM public/voice-chat.js
Diff
commit 326417dde28251c4e8f89bae097078d145f49f80
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 08:31:00 2026 -0700
auto-shutoff: 30s idle countdown with visible timer + resets on speech
---
public/index.html | 9 +++++++++
public/voice-chat.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 56 insertions(+)
diff --git a/public/index.html b/public/index.html
index 13af434..c319814 100644
--- a/public/index.html
+++ b/public/index.html
@@ -162,6 +162,14 @@
font-variant-numeric: tabular-nums;
letter-spacing: 0.4px;
}
+ .countdown {
+ color: var(--muted);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: 0.4px;
+ }
+ .countdown.warn { color: #facc15; }
+ .countdown.crit { color: var(--accent); font-weight: 600; }
.options {
display: flex;
gap: 8px;
@@ -205,6 +213,7 @@
<button id="mic">START</button>
<div class="meta">
<div id="status" class="status">idle</div>
+ <div id="countdown" class="countdown"></div>
<div id="latency" class="latency"></div>
<div class="options">
<select id="voicePicker" title="system voice"></select>
diff --git a/public/voice-chat.js b/public/voice-chat.js
index 2e68635..be982a0 100644
--- a/public/voice-chat.js
+++ b/public/voice-chat.js
@@ -21,8 +21,52 @@ let history = []; // [{role, content}]
let stopFlag = false;
let pendingSpeechAt = null; // ms — set when user-final committed, used to compute mouth-to-mouth
+// ── auto-shutoff ────────────────────────────────────────────
+// Mic stops itself after IDLE_TIMEOUT_MS of inactivity.
+// Activity (user speech detected) resets the timer.
+const IDLE_TIMEOUT_MS = 30_000;
+let idleStartedAt = 0;
+let countdownTimer = null;
+
function setStatus(t) { $status.textContent = t; }
+// ── idle countdown ───────────────────────────────────────────
+function startIdleCountdown() {
+ idleStartedAt = Date.now();
+ stopIdleCountdown();
+ countdownTimer = setInterval(() => {
+ const remaining = Math.max(0, IDLE_TIMEOUT_MS - (Date.now() - idleStartedAt));
+ const secs = Math.ceil(remaining / 1000);
+ renderCountdown(secs);
+ if (remaining <= 0) {
+ stopIdleCountdown();
+ stopFlag = true;
+ try { rec && rec.stop(); } catch {}
+ try { speechSynthesis.cancel(); } catch {}
+ setStatus("auto-stopped after 30s idle — tap START to resume");
+ }
+ }, 250);
+}
+function resetIdleCountdown() {
+ if (countdownTimer) idleStartedAt = Date.now();
+}
+function stopIdleCountdown() {
+ if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
+ renderCountdown(null);
+}
+function renderCountdown(secs) {
+ const el = document.getElementById("countdown");
+ if (!el) return;
+ if (secs == null) {
+ el.textContent = "";
+ el.classList.remove("warn", "crit");
+ } else {
+ el.textContent = `auto-stop in ${secs}s`;
+ el.classList.toggle("warn", secs <= 10 && secs > 5);
+ el.classList.toggle("crit", secs <= 5);
+ }
+}
+
function bubble(role, text, opts = {}) {
// Clear empty-placeholder once we have content
const empty = $transcript.querySelector(".empty");
@@ -112,11 +156,13 @@ function newRec() {
$mic.classList.add("listening");
$mic.textContent = "STOP";
listening = true;
+ startIdleCountdown();
};
r.onend = () => {
$mic.classList.remove("listening");
$mic.textContent = "START";
listening = false;
+ stopIdleCountdown();
if (!stopFlag) {
// Auto-restart on accidental end (Chrome cuts off ~60s)
setTimeout(() => { if (!stopFlag) try { r.start(); } catch {} }, 100);
@@ -136,6 +182,7 @@ function newRec() {
if (e.results[i].isFinal) final += piece;
else interim += piece;
}
+ if (interim || final) resetIdleCountdown(); // any speech activity defers shutoff
if (interim) {
if (!interimEl) interimEl = bubble("me", "", { interim: true });
interimEl.textContent = interim;
← 6475eea pivot to free stack: browser STT+TTS + Mac1 ollama qwen3:8b
·
back to Voice Chat
·
broaden .gitignore with snapshot/build excludes aad10fa →