feat: add mcp server, reorganize repo
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
assetsInlineLimit: 0,
|
||||
})
|
||||
Reference in New Issue
Block a user