Spaces:
No application file
No application file
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Voice Lie Detector</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 40px; | |
| max-width: 500px; | |
| width: 100%; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| } | |
| h1 { | |
| text-align: center; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 30px; | |
| font-size: 32px; | |
| } | |
| .info-box { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-bottom: 30px; | |
| font-size: 14px; | |
| text-align: center; | |
| } | |
| .control-section { | |
| margin-bottom: 30px; | |
| text-align: center; | |
| } | |
| button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| border-radius: 50px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin: 10px; | |
| box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 15px 35px rgba(102, 126, 234, 0.6); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .stop-btn { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| box-shadow: 0 10px 25px rgba(245, 87, 108, 0.4); | |
| } | |
| .stop-btn:hover { | |
| box-shadow: 0 15px 35px rgba(245, 87, 108, 0.6); | |
| } | |
| .sensitivity-control { | |
| margin-bottom: 20px; | |
| padding: 15px; | |
| background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| border-radius: 10px; | |
| } | |
| .sensitivity-label { | |
| font-size: 14px; | |
| font-weight: bold; | |
| color: #333; | |
| margin-bottom: 8px; | |
| } | |
| .sensitivity-slider { | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #ddd; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .sensitivity-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| cursor: pointer; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| } | |
| .sensitivity-slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| cursor: pointer; | |
| border: none; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| } | |
| .sensitivity-value { | |
| text-align: center; | |
| font-size: 12px; | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| .visualizer { | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: center; | |
| gap: 4px; | |
| height: 150px; | |
| margin: 30px 0; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
| border-radius: 15px; | |
| min-height: 200px; | |
| } | |
| .bar { | |
| width: 8px; | |
| background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 4px; | |
| transition: height 0.05s ease; | |
| min-height: 5px; | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin: 30px 0; | |
| } | |
| .stat-card { | |
| background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| color: white; | |
| font-weight: bold; | |
| } | |
| .stat-card.pitch { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .stat-card.intensity { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| } | |
| .stat-card.stability { | |
| background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| } | |
| .stat-card.verdict { | |
| background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); | |
| grid-column: 1 / -1; | |
| font-size: 18px; | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| opacity: 0.9; | |
| margin-bottom: 5px; | |
| } | |
| .stat-value { | |
| font-size: 24px; | |
| } | |
| .verdict-text { | |
| font-size: 28px; | |
| } | |
| .confidence { | |
| font-size: 14px; | |
| margin-top: 10px; | |
| opacity: 0.9; | |
| } | |
| .meter { | |
| width: 100%; | |
| height: 10px; | |
| background: rgba(255, 255, 255, 0.3); | |
| border-radius: 5px; | |
| margin-top: 10px; | |
| overflow: hidden; | |
| } | |
| .meter-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #43e97b 0%, #38f9d7 100%); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .status-message { | |
| text-align: center; | |
| margin-top: 20px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| color: #333; | |
| font-weight: bold; | |
| min-height: 40px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .status-message.recording { | |
| background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); | |
| color: white; | |
| animation: pulse 1s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| .recording-indicator { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| background: white; | |
| border-radius: 50%; | |
| margin-right: 10px; | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| @media (max-width: 600px) { | |
| .container { | |
| padding: 20px; | |
| } | |
| h1 { | |
| font-size: 24px; | |
| } | |
| button { | |
| padding: 12px 30px; | |
| font-size: 14px; | |
| } | |
| .visualizer { | |
| min-height: 150px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎤 Voice Lie Detector</h1> | |
| <div class="info-box"> | |
| ⚡ Real-time voice analysis - Analyzes tone, pitch stability & stress | |
| </div> | |
| <div class="sensitivity-control"> | |
| <div class="sensitivity-label">Sensitivity: <span id="sensitivityText">5</span></div> | |
| <input type="range" id="sensitivity" class="sensitivity-slider" min="1" max="10" value="5"> | |
| <div class="sensitivity-value">Low ← → High</div> | |
| </div> | |
| <div class="control-section"> | |
| <button id="startBtn">Start Recording</button> | |
| <button id="stopBtn" class="stop-btn" disabled>Stop Recording</button> | |
| </div> | |
| <div class="visualizer" id="visualizer"> | |
| <!-- Bars will be added here --> | |
| </div> | |
| <div class="stats"> | |
| <div class="stat-card pitch"> | |
| <div class="stat-label">Pitch Frequency</div> | |
| <div class="stat-value" id="pitchValue">0 Hz</div> | |
| </div> | |
| <div class="stat-card intensity"> | |
| <div class="stat-label">Voice Intensity</div> | |
| <div class="stat-value" id="intensityValue">0 dB</div> | |
| </div> | |
| <div class="stat-card stability"> | |
| <div class="stat-label">Stability Index</div> | |
| <div class="stat-value" id="stabilityValue">100%</div> | |
| </div> | |
| <div class="stat-card verdict"> | |
| <div class="stat-label">Verdict</div> | |
| <div class="verdict-text" id="verdict">Ready</div> | |
| <div class="meter"> | |
| <div class="meter-fill" id="meter"></div> | |
| </div> | |
| <div class="confidence" id="confidence">Awaiting recording...</div> | |
| </div> | |
| </div> | |
| <div class="status-message" id="status">Tap "Start Recording" to begin</div> | |
| </div> | |
| <script> | |
| let audioContext; | |
| let analyser; | |
| let microphone; | |
| let isRecording = false; | |
| let mediaStream; | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const visualizer = document.getElementById('visualizer'); | |
| const pitchValue = document.getElementById('pitchValue'); | |
| const intensityValue = document.getElementById('intensityValue'); | |
| const stabilityValue = document.getElementById('stabilityValue'); | |
| const verdict = document.getElementById('verdict'); | |
| const confidence = document.getElementById('confidence'); | |
| const meter = document.getElementById('meter'); | |
| const status = document.getElementById('status'); | |
| const sensitivitySlider = document.getElementById('sensitivity'); | |
| const sensitivityText = document.getElementById('sensitivityText'); | |
| let pitchHistory = []; | |
| let intensityHistory = []; | |
| let baselinePitch = null; | |
| let baselineIntensity = null; | |
| let frameCount = 0; | |
| startBtn.addEventListener('click', startRecording); | |
| stopBtn.addEventListener('click', stopRecording); | |
| sensitivitySlider.addEventListener('change', (e) => { | |
| sensitivityText.textContent = e.target.value; | |
| }); | |
| async function startRecording() { | |
| try { | |
| mediaStream = await navigator.mediaDevices.getUserMedia({ | |
| audio: { | |
| echoCancellation: false, | |
| noiseSuppression: false, | |
| autoGainControl: false | |
| } | |
| }); | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 4096; | |
| analyser.smoothingTimeConstant = 0.8; | |
| microphone = audioContext.createMediaStreamSource(mediaStream); | |
| microphone.connect(analyser); | |
| isRecording = true; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| status.textContent = '🔴 Recording... Speak something!'; | |
| status.classList.add('recording'); | |
| status.innerHTML = '<span class="recording-indicator"></span>Recording... Speak something!'; | |
| pitchHistory = []; | |
| intensityHistory = []; | |
| baselinePitch = null; | |
| baselineIntensity = null; | |
| frameCount = 0; | |
| analyzeAudio(); | |
| } catch (error) { | |
| status.textContent = '❌ Microphone access denied. Please allow access.'; | |
| status.classList.remove('recording'); | |
| console.error('Microphone error:', error); | |
| } | |
| } | |
| function stopRecording() { | |
| isRecording = false; | |
| if (mediaStream) { | |
| mediaStream.getTracks().forEach(track => track.stop()); | |
| } | |
| if (microphone) { | |
| microphone.disconnect(); | |
| } | |
| if (audioContext) { | |
| audioContext.close(); | |
| } | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| if (pitchHistory.length > 0) { | |
| analyzeResults(); | |
| } else { | |
| verdict.textContent = '⚠️ No Data'; | |
| confidence.textContent = 'Speak more clearly or louder'; | |
| } | |
| status.textContent = '✅ Recording stopped. Analysis complete!'; | |
| status.classList.remove('recording'); | |
| } | |
| function analyzeAudio() { | |
| const dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| analyser.getByteFrequencyData(dataArray); | |
| const timeDomainData = new Uint8Array(analyser.fftSize); | |
| analyser.getByteTimeDomainData(timeDomainData); | |
| // Check if there's actual audio | |
| const isSilent = checkIfSilent(timeDomainData); | |
| if (!isSilent) { | |
| // Calculate intensity | |
| const intensity = calculateIntensity(timeDomainData); | |
| intensityHistory.push(intensity); | |
| // Detect pitch with improved algorithm | |
| const pitch = detectPitchFast(timeDomainData); | |
| if (pitch > 60 && pitch < 400) { | |
| pitchHistory.push(pitch); | |
| } | |
| // Update live display | |
| updateVisualizer(dataArray); | |
| updateLiveStats(); | |
| frameCount++; | |
| } | |
| if (isRecording) { | |
| requestAnimationFrame(analyzeAudio); | |
| } | |
| } | |
| function checkIfSilent(timeDomainData) { | |
| let maxValue = 0; | |
| for (let i = 0; i < timeDomainData.length; i++) { | |
| const normalized = Math.abs((timeDomainData[i] - 128) / 128); | |
| if (normalized > maxValue) maxValue = normalized; | |
| } | |
| return maxValue < 0.02; // Threshold for silence | |
| } | |
| function calculateIntensity(timeDomainData) { | |
| let sum = 0; | |
| for (let i = 0; i < timeDomainData.length; i++) { | |
| const normalized = (timeDomainData[i] - 128) / 128; | |
| sum += normalized * normalized; | |
| } | |
| const rms = Math.sqrt(sum / timeDomainData.length); | |
| return 20 * Math.log10(Math.max(rms, 0.001)); | |
| } | |
| function detectPitchFast(timeDomainData) { | |
| // Improved autocorrelation for better pitch detection | |
| const sampleRate = 44100; | |
| const minPeriod = sampleRate / 400; // Min freq 400Hz | |
| const maxPeriod = sampleRate / 60; // Max freq 60Hz | |
| let maxValue = -Infinity; | |
| let maxIndex = 0; | |
| for (let lag = Math.floor(minPeriod); lag < Math.floor(maxPeriod); lag++) { | |
| let sum = 0; | |
| let sumSquareA = 0; | |
| let sumSquareB = 0; | |
| for (let i = 0; i < timeDomainData.length - lag; i++) { | |
| const sample1 = (timeDomainData[i] - 128) / 256; | |
| const sample2 = (timeDomainData[i + lag] - 128) / 256; | |
| sum += sample1 * sample2; | |
| sumSquareA += sample1 * sample1; | |
| sumSquareB += sample2 * sample2; | |
| } | |
| const correlation = sum / Math.sqrt((sumSquareA * sumSquareB) + 0.001); | |
| if (correlation > maxValue) { | |
| maxValue = correlation; | |
| maxIndex = lag; | |
| } | |
| } | |
| if (maxIndex > 0 && maxValue > 0.7) { | |
| return sampleRate / maxIndex; | |
| } | |
| return 0; | |
| } | |
| function updateVisualizer(dataArray) { | |
| visualizer.innerHTML = ''; | |
| const barCount = 32; | |
| const step = Math.floor(dataArray.length / barCount); | |
| for (let i = 0; i < barCount; i++) { | |
| const value = dataArray[i * step]; | |
| const bar = document.createElement('div'); | |
| bar.className = 'bar'; | |
| bar.style.height = (value / 255) * 200 + 'px'; | |
| visualizer.appendChild(bar); | |
| } | |
| } | |
| function updateLiveStats() { | |
| if (pitchHistory.length > 0) { | |
| const avgPitch = pitchHistory.reduce((a, b) => a + b) / pitchHistory.length; | |
| pitchValue.textContent = Math.round(avgPitch) + ' Hz'; | |
| if (baselinePitch === null && pitchHistory.length > 5) { | |
| baselinePitch = avgPitch; | |
| } | |
| } | |
| if (intensityHistory.length > 0) { | |
| const avgIntensity = intensityHistory.reduce((a, b) => a + b) / intensityHistory.length; | |
| intensityValue.textContent = Math.round(avgIntensity * 10) / 10 + ' dB'; | |
| if (baselineIntensity === null && intensityHistory.length > 5) { | |
| baselineIntensity = avgIntensity; | |
| } | |
| } | |
| if (pitchHistory.length > 20) { | |
| const stability = calculateStability(pitchHistory.slice(-30)); | |
| stabilityValue.textContent = Math.round(stability) + '%'; | |
| } | |
| } | |
| function calculateStability(pitchData) { | |
| if (pitchData.length < 2) return 100; | |
| const mean = pitchData.reduce((a, b) => a + b) / pitchData.length; | |
| const variance = pitchData.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / pitchData.length; | |
| const stdDev = Math.sqrt(variance); | |
| const stability = Math.max(0, 100 - (stdDev / mean) * 80); | |
| return Math.min(100, stability); | |
| } | |
| function analyzeResults() { | |
| if (pitchHistory.length < 10) { | |
| verdict.textContent = '⚠️ Not Enough Data'; | |
| confidence.textContent = 'Record for longer and speak clearly'; | |
| meter.style.width = '0%'; | |
| return; | |
| } | |
| const avgPitch = pitchHistory.reduce((a, b) => a + b) / pitchHistory.length; | |
| const avgIntensity = intensityHistory.length > 0 | |
| ? intensityHistory.reduce((a, b) => a + b) / intensityHistory.length | |
| : -20; | |
| const stability = calculateStability(pitchHistory); | |
| const sensitivity = parseInt(sensitivitySlider.value); | |
| let deceptionScore = 0; | |
| // Calculate variations | |
| const pitchVariation = ((Math.max(...pitchHistory) - Math.min(...pitchHistory)) / avgPitch) * 100; | |
| const intensityVariation = intensityHistory.length > 5 | |
| ? ((Math.max(...intensityHistory) - Math.min(...intensityHistory)) / Math.abs(avgIntensity)) * 100 | |
| : 0; | |
| // Apply sensitivity multiplier | |
| const sensitivityMultiplier = sensitivity / 5; | |
| // Score calculation | |
| if (pitchVariation > 15) { | |
| deceptionScore += 30 * sensitivityMultiplier; // Pitch instability | |
| } | |
| if (intensityVariation > 20) { | |
| deceptionScore += 25 * sensitivityMultiplier; // Volume changes | |
| } | |
| if (stability < 70) { | |
| deceptionScore += 25 * sensitivityMultiplier; // Low stability | |
| } | |
| if (pitchVariation > 30) { | |
| deceptionScore += 20 * sensitivityMultiplier; // Very high pitch jump | |
| } | |
| deceptionScore = Math.min(100, Math.max(0, deceptionScore)); | |
| meter.style.width = deceptionScore + '%'; | |
| if (deceptionScore > 70) { | |
| verdict.textContent = '🔴 LIKELY LIE'; | |
| confidence.textContent = `Confidence: ${Math.round(deceptionScore)}% - High stress & instability detected`; | |
| } else if (deceptionScore > 50) { | |
| verdict.textContent = '🟡 UNCERTAIN'; | |
| confidence.textContent = `Confidence: ${Math.round(deceptionScore)}% - Some stress signals detected`; | |
| } else if (deceptionScore > 30) { | |
| verdict.textContent = '🟢 LIKELY TRUTH'; | |
| confidence.textContent = `Confidence: ${100 - Math.round(deceptionScore)}% - Voice appears relatively calm`; | |
| } else { | |
| verdict.textContent = '✅ VERY TRUTHFUL'; | |
| confidence.textContent = `Confidence: ${100 - Math.round(deceptionScore)}% - No significant stress detected`; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |