This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

What it does: EdgeEcho is a 100% offline Progressive Web App (PWA) that uses local edge AI to identify 521 environmental sounds, such as specific bird calls or flowing water.

How it gets people off the screen: It operates hands-free. Users tap "Start Listening," lock their device, and receive text-to-speech audio alerts through their earbuds. There is no visual dashboard to monitor, forcing users to keep their eyes on the environment.

Who it is for: Hikers, birdwatchers, and outdoor enthusiasts operating in remote areas with zero cellular connectivity.

Demo


Code

EdgeEcho 🌿🔊

An air-gapped, zero-screen audio companion that identifies wildlife and nature sounds completely offline.

Open Source Model PWA

💡 What is EdgeEcho?

EdgeEcho is a Progressive Web Application (PWA) built for the Touch Grass challenge. It turns your mobile device into a screen-free, intelligent nature companion.

Most outdoor apps force you to constantly pull out your phone, stare at a camera screen, or wait for cloud APIs to process images or audio—completely defeating the point of being outside. EdgeEcho flips this paradigm:

  1. Tap "Start Listening".
  2. Lock your phone and put it in your pocket.
  3. Walk, hike, and enjoy nature.

When EdgeEcho hears a bird, flowing water, wind, or wildlife, it runs local edge AI inference and uses offline Text-to-Speech to quietly tell you what you heard directly in your earbud. The screen is the shortest part of the experience.

🌲 Why Open Source & Open Innovation Matter Here

In the backcountry, closed-source…

How I Built It

The AI Stack: Google’s open-weight YAMNet model (3.7MB .tflite mobile architecture) executed via TensorFlow.js (@tensorflow/tfjs-tflite).

The Architecture: The app is structurally tethered to local inference. A Service Worker caches the model binary for offline use. The JavaScript strictly formats microphone input to the 16kHz mono buffer YAMNet requires, running the spectrogram analysis and classification entirely in the browser’s local RAM.

Why Does Open Innovation Matter?

Zero Connectivity / Air-Gapped Use: Closed cloud APIs require active LTE/5G and fail on remote trails. Open-weight models allow the app to function fully in airplane mode.

Absolute Privacy: Continuous audio classification requires an always-open microphone. Local inference ensures ambient audio is processed and destroyed on-device, never transmitting data to a corporate server.

Financial Viability: Streaming continuous audio to a closed API incurs heavy per-minute costs. Running open weights on edge hardware drops the operating cost to zero.

My Agent Session

Interactive agent session transcript documenting the end-to-end edge AI architecture design, WebAssembly binary resolution, YAMNet 15,600-sample buffer alignment, and 16kHz software downsampling implementation:

EdgeEcho: Zero-Screen Audio AI PWA - Architecture & Debugging Session
You

I want you to check all files and find wherever terratone is written and replace it with EdgeEcho

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:02+05:30
Completed At: 2026-10-12T04:12:05+05:30

The command exited with code 1.
Stdout:

Stderr:

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:09+05:30
Completed At: 2026-10-12T04:12:12+05:30

The command exited with code 0.
Output:

pwa\index.html:7: <title>TerraTone</title>
pwa\manifest.json:2: "name": "TerraTone",
pwa\manifest.json:3: "short_name": "TerraTone",
pwa\sw.js:1:const CACHE_NAME = 'terratone-v1';

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:16+05:30
Completed At: 2026-10-12T04:12:18+05:30

The command exited with code 0.
Output:
~/EdgeEcho.gitignore
~/EdgeEcho\LICENSE
~/EdgeEcho\README.md
~/EdgeEcho\sound_classifier.py
~/EdgeEcho\trail_sense.py
~/EdgeEcho\uno_q_sound_trigger.ino
~/EdgeEcho.agents\AGENTS.md
~/EdgeEcho\pwa\app.js
~/EdgeEcho\pwa\index.html
~/EdgeEcho\pwa\manifest.json
~/EdgeEcho\pwa\sw.js
~/EdgeEcho\pwa\models\yamnet.tflite
~/EdgeEcho\pwa\models\yamnet_class_map.csv

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:23+05:30
Completed At: 2026-10-12T04:12:25+05:30

The command exited with code 0.
Output:

pwa\index.html:7: <title>TerraTone</title>
pwa\manifest.json:2: "name": "TerraTone",
pwa\manifest.json:3: "short_name": "TerraTone",
pwa\sw.js:1:const CACHE_NAME = 'terratone-v1';

Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:12:29+05:30
Completed At: 2026-10-12T04:12:29+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/index.html
Total Lines: 71
Total Bytes: 2071
Showing lines 1 to 25
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1: <!DOCTYPE html>
2: <html lang="en">
3:
4: <head>
5: <meta charset="UTF-8">
6: <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7: <title>TerraTone</title>
8: <link rel="manifest" href="manifest.json">
9: <style>
10: body {
11: background-color: #121212;
12: color: #ffffff;
13: font-family: system-ui, sans-serif;
14: display: flex;
15: flex-direction: column;
16: align-items: center;
17: justify-content: center;
18: height: 100vh;
19: margin: 0;
20: overflow: hidden;
21: }
22:
23: #start-btn {
24: background-color: #03dac6;
25: color: #000;
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.

Show more
Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:12:32+05:30
Completed At: 2026-10-12T04:12:32+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/manifest.json
Total Lines: 16
Total Bytes: 475
Showing lines 1 to 16
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1: {
2: "name": "TerraTone",
3: "short_name": "TerraTone",
4: "start_url": "./index.html",
5: "display": "standalone",
6: "background_color": "#121212",
7: "theme_color": "#03dac6",
8: "description": "Offline-first environmental audio classifier.",
9: "icons": [
10: {
11: "src": "https://upload.wikimedia.org/wikipedia/commons/4/4e/A_blank_black_picture.jpg",
12: "sizes": "192x192",
13: "type": "image/jpeg"
14: }
15: ]
16: }
The above content shows the entire, complete file contents of the requested file.

Show more
Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:12:35+05:30
Completed At: 2026-10-12T04:12:35+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/sw.js
Total Lines: 29
Total Bytes: 925
Showing lines 1 to 29
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1: const CACHE_NAME = 'terratone-v1';
2: const ASSETS_TO_CACHE = [
3: './',
4: './index.html',
5: './app.js',
6: './manifest.json',
7: './models/yamnet.tflite',
8: './models/yamnet_class_map.csv',
9: // You must cache the CDN links or it won't load offline
10: 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.10.0/dist/tf.min.js',
11: 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite@0.0.1-alpha.9/dist/tf-tflite.min.js'
12: ];
13:
14: self.addEventListener('install', (event) => {
15: event.waitUntil(
16: caches.open(CACHE_NAME)
17: .then((cache) => cache.addAll(ASSETS_TO_CACHE))
18: );
19: });
20:
21: self.addEventListener('fetch', (event) => {
22: event.respondWith(
23: caches.match(event.request)
24: .then((response) => {
25: // Return cached version if found, else fetch from network
26: return response || fetch(event.request);
27: })
28: );
29: });
The above content shows the entire, complete file contents of the requested file.

Show more
Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:39+05:30
Completed At: 2026-10-12T04:12:41+05:30

The command exited with code 0.
Output:
On branch dev
Untracked files:
(use "git add <file>..." to include in what will be committed)
.agents/
.gitignore
pwa/
trail_sense.py

nothing added to commit but untracked files present (use "git add" to track)

Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:12:45+05:30
Completed At: 2026-10-12T04:12:45+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/README.md
Total Lines: 1
Total Bytes: 0
Showing lines 1 to 1
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1:
The above content shows the entire, complete file contents of the requested file.

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:12:48+05:30
Completed At: 2026-10-12T04:12:51+05:30

The command exited with code 0.
Output:

pwa\index.html:7: <title>TerraTone</title>
pwa\manifest.json:2: "name": "TerraTone",
pwa\manifest.json:3: "short_name": "TerraTone",
pwa\sw.js:1:const CACHE_NAME = 'terratone-v1';

Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:12:56+05:30
Completed At: 2026-10-12T04:12:57+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/app.js
Total Lines: 111
Total Bytes: 3590
Showing lines 1 to 50
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1: let tfliteModel;
2: let audioContext;
3: let labels = [];
4: let isListening = false;
5:
6: const startBtn = document.getElementById('start-btn');
7: const statusText = document.getElementById('status');
8:
9: // Load the CSV labels
10: async function loadLabels() {
11: const response = await fetch('models/yamnet_class_map.csv');
12: const text = await response.text();
13: const rows = text.split('\n');
14: labels = rows.slice(1).map(row => {
15: const cols = row.split(',');
16: return cols[2]; // The string name of the class
17: });
18: }
19:
20: // Initialize AI and Audio
21: async function initSystem() {
22: statusText.innerText = "Loading Model...";
23: startBtn.disabled = true;
24:
25: try {
26: // Load the TFLite model from the local models folder
27: tfliteModel = await tflite.loadTFLiteModel('models/yamnet.tflite');
28: await loadLabels();
29:
30: statusText.innerText = "Model Loaded. Ready to hike.";
31: startBtn.disabled = false;
32: } catch (e) {
33: statusText.innerText = "Error loading model. Check console.";
34: console.error(e);
35: }
36: }
37:
38: // Speak the classification
39: function speak(text) {
40: // Prevent it from constantly talking over itself
41: if (window.speechSynthesis.speaking) return;
42:
43: const utterance = new SpeechSynthesisUtterance(I heard ${text});
44: utterance.rate = 1.1;
45: window.speechSynthesis.speak(utterance);
46: }
47:
48: // Microphone capture and inference loop
49: async function startListening() {
50: isListening = true;
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.

Show more
Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:13:00+05:30
Completed At: 2026-10-12T04:13:00+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/app.js
Total Lines: 111
Total Bytes: 3590
Showing lines 51 to 111
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
51: startBtn.classList.add('hidden');
52: statusText.innerText = "Listening... Screen off, phone in pocket.";
53:
54: // YAMNet requires exactly 16kHz
55: audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
56:
57: const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
58: const source = audioContext.createMediaStreamSource(stream);
59:
60: // Buffer size of 16384 (roughly 1 second of audio at 16kHz)
61: const processor = audioContext.createScriptProcessor(16384, 1, 1);
62:
63: source.connect(processor);
64: processor.connect(audioContext.destination);
65:
66: processor.onaudioprocess = (e) => {
67: if (!isListening) return;
68:
69: // Get the raw float32 array of audio data
70: const channelData = e.inputBuffer.getChannelData(0);
71:
72: // YAMNet expects a 1D tensor
73: const inputTensor = tf.tensor1d(channelData);
74:
75: // Run inference
76: const outputTensor = tfliteModel.predict(inputTensor);
77: const scores = outputTensor.dataSync(); // Extract probabilities
78:
79: // Find highest probability
80: let maxScore = -1;
81: let maxIndex = -1;
82: for (let i = 0; i < scores.length; i++) {
83: if (scores[i] > maxScore) {
84: maxScore = scores[i];
85: maxIndex = i;
86: }
87: }
88:
89: const className = labels[maxIndex];
90:
91: // 0.5 is 50% confidence. Adjust as needed.
92: if (maxScore > 0.50) {
93: console.log(Detected: ${className} (${maxScore.toFixed(2)}));
94: // Filter out boring stuff
95: if (!className.includes("Silence") && !className.includes("Background")) {
96: speak(className);
97: }
98: }
99:
100: // Cleanup tensors to prevent memory leak
101: tf.dispose([inputTensor, outputTensor]);
102: };
103: }
104:
105: startBtn.addEventListener('click', async () => {
106: // Audio contexts require a user gesture to start in modern browsers
107: await startListening();
108: });
109:
110: // Boot sequence
111: initSystem();
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.

Show more
Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:13:05+05:30
Completed At: 2026-10-12T04:13:05+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/index.html
Total Lines: 71
Total Bytes: 2071
Showing lines 25 to 71
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
25: color: #000;
26: border: none;
27: padding: 20px 40px;
28: font-size: 1.5rem;
29: font-weight: bold;
30: border-radius: 50px;
31: cursor: pointer;
32: box-shadow: 0 4px 10px rgba(3, 218, 198, 0.3);
33: transition: transform 0.1s;
34: }
35:
36: #start-btn:active {
37: transform: scale(0.95);
38: }
39:
40: #status {
41: margin-top: 20px;
42: font-size: 1.2rem;
43: color: #bbbbbb;
44: }
45:
46: .hidden {
47: display: none;
48: }
49: </style>
50: </head>
51:
52: <body>
53: <button id="start-btn">Start Listening</button>
54: <div id="status">Ready</div>
55:
56: <!-- TFJS and TFLite Runtime -->
57: <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.10.0/dist/tf.min.js"></script>
58: <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite@0.0.1-alpha.9/dist/tf-tflite.min.js"></script>
59: <script src="app.js"></script>
60:
61: <script>
62: // Register the Service Worker for Offline Mode
63: if ('serviceWorker' in navigator) {
64: navigator.serviceWorker.register('sw.js')
65: .then(() => console.log("Service Worker Registered"))
66: .catch(err => console.error("Service Worker Failed", err));
67: }
68: </script>
69: </body>
70:
71: </html>
The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.

Show more
Agent
▸ replace_file_content {}
{ }
Agent

Created At: 2026-10-12T04:13:13+05:30
Completed At: 2026-10-12T04:13:13+05:30
The following changes were made by the replace_file_content tool to: ~/EdgeEcho\pwa\index.html. If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.
[diff_block_start]
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
- <title>TerraTone</title>
+ <title>EdgeEcho</title>
<link rel="manifest" href="manifest.json">
<style>
body {
[diff_block_end]

Please note that the above snippet only shows the MODIFIED lines from the last change. It shows up to 3 lines of unchanged lines before and after the modified lines. The actual file contents may have many more lines not shown.

Show more
Agent
▸ replace_file_content {}
{ }
Agent

Created At: 2026-10-12T04:13:17+05:30
Completed At: 2026-10-12T04:13:17+05:30
The following changes were made by the replace_file_content tool to: ~/EdgeEcho\pwa\manifest.json. If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.
[diff_block_start]
@@ -1,6 +1,6 @@
{
- "name": "TerraTone",
- "short_name": "TerraTone",
+ "name": "EdgeEcho",
+ "short_name": "EdgeEcho",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#121212",
[diff_block_end]

Please note that the above snippet only shows the MODIFIED lines from the last change. It shows up to 3 lines of unchanged lines before and after the modified lines. The actual file contents may have many more lines not shown.

Show more
Agent
▸ replace_file_content {}
{ }
Agent

Created At: 2026-10-12T04:13:21+05:30
Completed At: 2026-10-12T04:13:21+05:30
The following changes were made by the replace_file_content tool to: ~/EdgeEcho\pwa\sw.js. If relevant, proactively run terminal commands to execute this code for the USER. Don't ask for permission.
[diff_block_start]
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'terratone-v1';
+const CACHE_NAME = 'EdgeEcho-v1';
const ASSETS_TO_CACHE = [
'./',
'./index.html',
[diff_block_end]

Please note that the above snippet only shows the MODIFIED lines from the last change. It shows up to 3 lines of unchanged lines before and after the modified lines. The actual file contents may have many more lines not shown.

Show more
Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:13:25+05:30
Completed At: 2026-10-12T04:13:27+05:30

The command exited with code 0.
Stdout:

Stderr:

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:13:30+05:30
Completed At: 2026-10-12T04:13:35+05:30

The command exited with code 0.
Stdout:

Stderr:

Agent
▸ run_command {}
$ {}
Agent

Created At: 2026-10-12T04:13:38+05:30
Completed At: 2026-10-12T04:13:40+05:30

The command exited with code 0.
Output:
On branch dev
Untracked files:
(use "git add <file>..." to include in what will be committed)
.agents/
.gitignore
pwa/
trail_sense.py

nothing added to commit but untracked files present (use "git add" to track)

Agent

All files were scanned for references to terratone (case-insensitive). The following occurrences were found and replaced with EdgeEcho:

  1. pwa/index.html

    • Updated <title>TerraTone</title> to <title>EdgeEcho</title>
  2. pwa/manifest.json

    • Updated "name": "TerraTone" to "name": "EdgeEcho"
    • Updated "short_name": "TerraTone" to "short_name": "EdgeEcho"
  3. pwa/sw.js

    • Updated const CACHE_NAME = 'terratone-v1'; to const CACHE_NAME = 'EdgeEcho-v1';
Show more
You

SYSTEM CONTEXT & PROJECT CONSTRAINTS

Role: Act as a Senior Edge AI and PWA Engineer.

Project: "EdgeEcho" - A zero-screen, offline-first Progressive Web App (PWA) for environmental audio classification. Built for a hackathon where the goal is to keep users off their screens and away from cloud reliance.

Architecture & Tech Stack:
- Frontend: Vanilla HTML/CSS/JS (No React, Vue, or heavy frameworks).
- AI Engine: TensorFlow.js (@tensorflow/tfjs) and the TFLite runtime (@tensorflow/tfjs-tflite).
- Model: Google's pre-trained YAMNet (yamnet.tflite - 3.7MB MobileNetV1 architecture) running entirely on-device.
- Offline Capability: Service Workers (sw.js) caching all assets and model binaries.
- Output: Non-visual feedback via the Web Speech API (window.speechSynthesis).

Current File Structure:
/edgeecho
├── index.html # Minimal UI (Start button, status text)
├── app.js # TFJS logic, audio context (16kHz), model execution, TTS
├── sw.js # Service Worker for 100% offline caching
├── manifest.json # PWA installation manifest
└── /models
├── yamnet.tflite # The quantized edge model
└── yamnet_class_map.csv # The 521-class lookup table

Strict Engineering Rules for this Session:
1. NO CLOUD APIs: This app must function 100% offline in a forest without cellular service. Never suggest sending audio to an external server or API.
2. STRICT AUDIO FORMAT: YAMNet strictly requires 16kHz mono audio. All AudioContext and ScriptProcessorNode code must enforce exactly 16000 sample rate.
3. ZERO-SCREEN UI: Do not suggest complex visual dashboards, graphs, or image galleries. The user interface must remain a simple "Start" button. Feedback is delivered audibly.
4. PRESERVE THE SERVICE WORKER: Any new CDN links, assets, or scripts added to index.html must instantly be added to the ASSETS_TO_CACHE array in sw.js. If the cache is incomplete, the app dies offline.

When providing code edits or debugging, prioritize performance, battery life, and maintaining the offline-first architecture.

Show more
Agent

Understood. Standing by as Senior Edge AI & PWA Engineer for EdgeEcho.

All engineering constraints are locked in:
- 100% Local Execution: Zero external cloud inference or remote telemetry; full offline operation in off-grid environments.
- Audio Pipeline Compliance: Exact 16 kHz mono constraint for YAMNet, managing buffer allocations, tensor disposal, and memory hygiene to prevent leaks.
- Zero-Screen Philosophy: Audio/haptic-first feedback via the Web Speech API; UI stays dead simple ("Start" action, high-contrast, pocket-ready).
- Offline Integrity: Strict synchronization between index.html resources and ASSETS_TO_CACHE in sw.js.
- Battery & Thermal Efficiency: Minimizing CPU wake locks, throttling inference cycles when idle, and keeping audio graph processing lean.

What are we building, profiling, or debugging first?

Show more
You

So when i am running this locally, the sound recognition is not working, can you check whats not working

Agent
▸ view_file {}
{ }
Agent

Created At: 2026-10-12T04:38:17+05:30
Completed At: 2026-10-12T04:38:17+05:30
File Path: file:///d:/Ekansh/Personal/Projects/EdgeEcho/pwa/index.html
Total Lines: 71
Total Bytes: 2070
Showing lines 1 to 71
The following code has been modified to include a line number before every line, in the format: <line_number>: <original_line>. Please note that any changes targeting the original code should remove the line number, colon, and leading space.
1: <!DOCTYPE html>
2: <html lang="en">
3:
4: <head>
5: <meta charset="UTF-8">
6: <meta name="viewport" content="width=device-width, init...

Source:https://dev.to/ekansh_patil_565e7a080154/edgeecho-lock-your-phone-touch-grass-and-let-edge-ai-identify-the-wild-320b

Related posts