feat: add storage and training modules for snore detection

- Implemented `storage.py` for managing metadata storage, including sample addition, retrieval, and review state management.
- Created `training.py` for training a local model using Random Forest, including functions for training and predicting samples.
- Developed a web interface in `app.js` for capturing audio samples, managing labels, and training the model.
- Added HTML structure in `index.html` for the SnoreStopper control room with sections for sample capture, overnight gathering, training, and status display.
- Styled the application with `styles.css` to enhance user experience and interface aesthetics.
This commit is contained in:
2026-03-12 13:35:17 -04:00
commit 28012e70e0
21 changed files with 2680 additions and 0 deletions

107
web/index.html Normal file
View File

@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SnoreStopper Control Room</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/web/styles.css" />
</head>
<body>
<div class="ambient-backdrop" aria-hidden="true"></div>
<main class="layout">
<header class="hero">
<p class="eyebrow">Self-hosted sleep signal lab</p>
<h1>SnoreStopper</h1>
<p>
Capture ambient sleep audio, label snoring events, and train a local model from your own
nights.
</p>
</header>
<section class="panel controls">
<h2>Capture Sample</h2>
<div class="row">
<label for="deviceSelect">Input device</label>
<select id="deviceSelect"></select>
</div>
<div class="row split">
<div>
<label for="durationInput">Duration (seconds)</label>
<input id="durationInput" type="number" min="2" max="90" value="10" />
</div>
<div>
<label for="tagInput">Tag</label>
<input id="tagInput" type="text" maxlength="80" placeholder="night-1, side-sleep" />
</div>
</div>
<button id="captureButton" class="btn primary">Record Sample</button>
</section>
<section class="panel overnight">
<h2>Overnight Gatherer</h2>
<div class="row split overnight-grid">
<div>
<label for="overnightHours">Duration (hours)</label>
<input id="overnightHours" type="number" min="1" max="12" value="8" />
</div>
<div>
<label for="overnightClipSeconds">Clip length (seconds)</label>
<input id="overnightClipSeconds" type="number" min="2" max="90" value="20" />
</div>
<div>
<label for="overnightIntervalSeconds">Interval (seconds)</label>
<input id="overnightIntervalSeconds" type="number" min="2" max="90" value="30" />
</div>
</div>
<label class="toggle-row" for="overnightAutoWatch">
<input id="overnightAutoWatch" type="checkbox" checked />
Run snore watch on each overnight clip
</label>
<div class="actions">
<button id="overnightStartButton" class="btn primary">Start Overnight</button>
<button id="overnightStopButton" class="btn ghost">Stop</button>
</div>
<pre id="overnightOutput" class="output">No active overnight session.</pre>
</section>
<section class="panel training">
<h2>Training</h2>
<p>
Label samples as <code>snore</code>, <code>not_snore</code>, or <code>unclear</code>, then
train locally.
</p>
<button id="trainButton" class="btn">Train Model</button>
<pre id="trainingOutput" class="output">No training run yet.</pre>
</section>
<section class="panel status">
<h2>Status</h2>
<p id="statusBanner" aria-live="polite">Loading...</p>
</section>
<section class="panel pending">
<div class="samples-header">
<h2>Snore Watch Review Queue</h2>
<button id="refreshPendingButton" class="btn ghost">Refresh Queue</button>
</div>
<div id="pendingList" class="pending-list"></div>
</section>
<section class="panel samples">
<div class="samples-header">
<h2>Samples</h2>
<button id="refreshButton" class="btn ghost">Refresh</button>
</div>
<div id="samplesList" class="sample-list"></div>
</section>
</main>
<script src="/web/app.js" defer></script>
</body>
</html>