feat: add mcp server, reorganize repo

This commit is contained in:
2026-09-03 17:20:08 +05:00
parent c1dda4d257
commit 1fd8ad6b50
48 changed files with 1691 additions and 12467 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rubber Duck - Voice Activity Detector</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🦆</text></svg>" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<div id="duck-wrapper">
<div id="duck-indicator" class="silent">
<svg viewBox="0 0 100 100" class="duck-icon">
<path d="M35 60 Q20 55 15 45 Q10 35 20 30 Q25 28 30 30 Q28 20 35 15 Q42 10 50 15 Q55 12 60 18 Q65 15 68 22 Q75 25 78 32 Q85 35 82 45 Q80 50 75 52 Q80 58 75 65 Q70 72 60 70 Q55 75 45 75 Q40 72 35 70 Z" fill="#FFD700" stroke="#DAA520" stroke-width="2"/>
<circle cx="45" cy="38" r="5" fill="#333"/>
<path d="M38 28 Q42 25 46 28" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round"/>
<ellipse cx="68" cy="45" rx="10" ry="6" fill="#FF8C00" opacity="0.6"/>
</svg>
<div id="glow-ring"></div>
</div>
</div>
<p id="status-text">Click "Start" to begin</p>
<button id="toggle-btn" class="btn">Start</button>
<p id="error-text" class="error hidden"></p>
<p class="hint">Grant microphone access to start voice detection</p>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
+121
View File
@@ -0,0 +1,121 @@
const indicator = document.getElementById('duck-indicator')
const statusText = document.getElementById('status-text')
const toggleBtn = document.getElementById('toggle-btn')
const errorText = document.getElementById('error-text')
let detector = null
class SmartVoiceDetector {
constructor(options = {}) {
this.threshold = options.threshold ?? -50
this.silenceDelay = options.silenceDelay ?? 700
this.onSpeechStart = options.onSpeechStart || (() => {})
this.onSpeechEnd = options.onSpeechEnd || (() => {})
this.isSpeaking = false
this.timeout = null
this.audioContext = null
this.running = false
}
async start() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
this.audioContext = new AudioContext()
const source = this.audioContext.createMediaStreamSource(stream)
const analyser = this.audioContext.createAnalyser()
analyser.fftSize = 1024
analyser.smoothingTimeConstant = 0.4
source.connect(analyser)
const bufferLength = analyser.frequencyBinCount
const dataArray = new Float32Array(bufferLength)
const sampleRate = this.audioContext.sampleRate
const minHz = 85
const maxHz = 3000
const minIndex = Math.floor(minHz / (sampleRate / analyser.fftSize))
const maxIndex = Math.ceil(maxHz / (sampleRate / analyser.fftSize))
this.running = true
const check = () => {
if (!this.running) return
analyser.getFloatFrequencyData(dataArray)
let maxVolume = -Infinity
for (let i = minIndex; i <= maxIndex; i++) {
if (dataArray[i] > maxVolume) maxVolume = dataArray[i]
}
if (maxVolume > this.threshold) {
if (!this.isSpeaking) {
this.isSpeaking = true
this.onSpeechStart()
}
clearTimeout(this.timeout)
this.timeout = null
} else if (this.isSpeaking && !this.timeout) {
this.timeout = setTimeout(() => {
this.isSpeaking = false
this.onSpeechEnd()
}, this.silenceDelay)
}
requestAnimationFrame(check)
}
check()
}
stop() {
this.running = false
if (this.audioContext) {
this.audioContext.close()
this.audioContext = null
}
}
}
toggleBtn.addEventListener('click', async () => {
if (detector) {
detector.stop()
detector = null
statusText.textContent = 'Stopped'
toggleBtn.textContent = 'Start'
indicator.classList.remove('speaking')
indicator.classList.add('silent')
return
}
toggleBtn.disabled = true
statusText.textContent = 'Starting...'
try {
detector = new SmartVoiceDetector({
threshold: -50,
onSpeechStart: () => {
indicator.classList.remove('silent')
indicator.classList.add('speaking')
statusText.textContent = 'Voice detected'
},
onSpeechEnd: () => {
indicator.classList.remove('speaking')
indicator.classList.add('silent')
statusText.textContent = 'Listening...'
},
})
await detector.start()
statusText.textContent = 'Listening...'
toggleBtn.textContent = 'Stop'
toggleBtn.disabled = false
} catch (err) {
console.error('Mic error:', err)
errorText.textContent = err.message || String(err)
errorText.classList.remove('hidden')
statusText.textContent = 'Error'
toggleBtn.textContent = 'Retry'
toggleBtn.disabled = false
}
})
+33
View File
@@ -0,0 +1,33 @@
{
"name": "rubber-duck",
"version": "0.0.1",
"description": "Best debug tool - rubber duck",
"keywords": [
"rubber",
"duck",
"debug",
"tool",
"fun"
],
"homepage": "https://github.com/Ku6epXBOCTuK/rubber-duck#readme",
"bugs": {
"url": "https://github.com/Ku6epXBOCTuK/rubber-duck/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Ku6epXBOCTuK/rubber-duck.git"
},
"license": "MIT",
"author": "Ku6epXBOCTuK",
"type": "module",
"main": "index.js",
"scripts": {
"dev": "vite",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.0.11"
}
}
+107
View File
@@ -0,0 +1,107 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
color: #fff;
}
.container {
text-align: center;
}
#duck-wrapper {
position: relative;
display: inline-block;
}
#duck-indicator {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 160px;
height: 160px;
transition: filter 0.3s ease;
}
.duck-icon {
width: 120px;
height: 120px;
position: relative;
z-index: 2;
}
#glow-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
transition: all 0.3s ease;
z-index: 1;
pointer-events: none;
}
#duck-indicator.silent #glow-ring {
background: radial-gradient(circle, rgba(255, 50, 50, 0.3) 0%, transparent 70%);
box-shadow: 0 0 40px 10px rgba(255, 50, 50, 0.2), inset 0 0 40px 10px rgba(255, 50, 50, 0.05);
}
#duck-indicator.speaking #glow-ring {
background: radial-gradient(circle, rgba(50, 255, 50, 0.4) 0%, transparent 70%);
box-shadow: 0 0 60px 20px rgba(50, 255, 50, 0.3), inset 0 0 60px 20px rgba(50, 255, 50, 0.1);
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.85; }
}
#status-text {
margin-top: 20px;
font-size: 1.2rem;
opacity: 0.8;
}
.btn {
margin-top: 16px;
padding: 10px 28px;
font-size: 1rem;
border: none;
border-radius: 8px;
background: #16213e;
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
.btn:hover {
background: #0f3460;
}
.hint {
margin-top: 24px;
font-size: 0.85rem;
opacity: 0.5;
}
.error {
margin-top: 12px;
color: #ff6b6b;
font-size: 0.9rem;
}
.hidden {
display: none;
}
+5
View File
@@ -0,0 +1,5 @@
import { defineConfig } from 'vite'
export default defineConfig({
assetsInlineLimit: 0,
})