liedetector / index.html
ameenmarashi's picture
Create index.html
348ac8c verified
<!DOCTYPE html>
<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>