| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Emotion Based Music Player</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script> |
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script> |
| <script src="https://sdk.scdn.co/spotify-player.js"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| body { |
| font-family: 'Inter', sans-serif; |
| } |
| .loader { |
| border-top-color: #3498db; |
| -webkit-animation: spin 1s linear infinite; |
| animation: spin 1s linear infinite; |
| } |
| @-webkit-keyframes spin { |
| 0% { -webkit-transform: rotate(0deg); } |
| 100% { -webkit-transform: rotate(360deg); } |
| } |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white flex flex-col items-center justify-center min-h-screen p-4"> |
|
|
| <div class="w-full max-w-4xl mx-auto bg-gray-800 rounded-2xl shadow-2xl p-6 md:p-8"> |
| <div class="text-center mb-6"> |
| <h1 class="text-3xl md:text-4xl font-bold text-white">Emotion-Powered Music</h1> |
| <p class="text-gray-400 mt-2">Let's find the perfect song for your mood.</p> |
| </div> |
|
|
| |
| <div id="spotify-login-section" class="text-center mb-6"> |
| <p class="mb-4 text-gray-300">Please connect your Spotify account to get started.</p> |
| <button id="spotify-login-btn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300"> |
| Login with Spotify |
| </button> |
| </div> |
|
|
| <div id="main-content" class="hidden"> |
| |
| <div class="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-6 shadow-lg"> |
| <video id="webcam" class="w-full h-full object-cover" autoplay playsinline></video> |
| <canvas id="overlay" class="absolute top-0 left-0 w-full h-full"></canvas> |
| <div id="loading" class="absolute inset-0 bg-black bg-opacity-75 flex flex-col items-center justify-center"> |
| <div class="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-24 w-24 mb-4"></div> |
| <p class="text-lg text-white">Initializing Camera & AI Model...</p> |
| </div> |
| <div id="emotion-display" class="absolute bottom-4 left-4 bg-black bg-opacity-60 text-white text-xl font-semibold px-4 py-2 rounded-lg hidden"> |
| Emotion: <span id="emotion-text">...</span> |
| </div> |
| </div> |
|
|
| |
| <div class="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4 mb-6"> |
| <button id="start-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 w-full sm:w-auto">Start Emotion Detection</button> |
| <button id="stop-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 w-full sm:w-auto" disabled>Stop Detection</button> |
| </div> |
|
|
| |
| <div id="spotify-player" class="bg-gray-700 p-4 rounded-lg shadow-md hidden"> |
| <h2 class="text-xl font-semibold mb-2 text-center">Now Playing</h2> |
| <div id="spotify-widget" class="text-center"> |
| <p>Music will appear here once an emotion is detected.</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="error-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> |
| <div class="bg-white text-gray-800 p-6 rounded-lg shadow-lg max-w-sm w-full"> |
| <h2 class="text-xl font-bold mb-4">Error</h2> |
| <p id="error-message"></p> |
| <button id="close-modal-btn" class="mt-4 bg-red-500 text-white py-2 px-4 rounded-lg">Close</button> |
| </div> |
| </div> |
|
|
|
|
| <script> |
| const videoElement = document.getElementById('webcam'); |
| const canvasElement = document.getElementById('overlay'); |
| const canvasCtx = canvasElement.getContext('2d'); |
| const loadingDiv = document.getElementById('loading'); |
| const emotionText = document.getElementById('emotion-text'); |
| const emotionDisplay = document.getElementById('emotion-display'); |
| const startBtn = document.getElementById('start-btn'); |
| const stopBtn = document.getElementById('stop-btn'); |
| const spotifyLoginBtn = document.getElementById('spotify-login-btn'); |
| const spotifyLoginSection = document.getElementById('spotify-login-section'); |
| const mainContent = document.getElementById('main-content'); |
| const spotifyPlayerDiv = document.getElementById('spotify-player'); |
| const spotifyWidget = document.getElementById('spotify-widget'); |
| const errorModal = document.getElementById('error-modal'); |
| const errorMessage = document.getElementById('error-message'); |
| const closeModalBtn = document.getElementById('close-modal-btn'); |
| |
| let faceMesh; |
| let ortSession; |
| let scalerMean; |
| let scalerScale; |
| let spotifyAccessToken = ''; |
| let spotifyPlayer; |
| let deviceId; |
| let isDetecting = false; |
| |
| const emotions = ['happy', 'sad', 'neutral', 'angry', 'disgust', 'surprise']; |
| const SPOTIFY_CLIENT_ID = 'a20b1a54a9314c38a3973d50c9324ca0'; |
| const REDIRECT_URI = window.location.origin + window.location.pathname; |
| const HUGGINGFACE_REPO_URL = "https://huggingface.co/PVK-VARMA/mood_play/resolve/main/"; |
| |
| |
| function showError(message) { |
| errorMessage.textContent = message; |
| errorModal.classList.remove('hidden'); |
| } |
| closeModalBtn.addEventListener('click', () => errorModal.classList.add('hidden')); |
| |
| |
| spotifyLoginBtn.addEventListener('click', () => { |
| const scopes = 'streaming user-read-email user-read-private user-modify-playback-state'; |
| const authUrl = `https://accounts.spotify.com/authorize?response_type=token&client_id=${SPOTIFY_CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`; |
| window.location = authUrl; |
| }); |
| |
| |
| window.onSpotifyWebPlaybackSDKReady = () => { |
| if (spotifyAccessToken) { |
| initializeSpotifyPlayer(); |
| } |
| }; |
| |
| window.addEventListener('load', () => { |
| const hash = window.location.hash; |
| if (hash) { |
| const params = new URLSearchParams(hash.substring(1)); |
| const token = params.get('access_token'); |
| if (token) { |
| spotifyAccessToken = token; |
| window.location.hash = ''; |
| spotifyLoginSection.classList.add('hidden'); |
| mainContent.classList.remove('hidden'); |
| |
| if (window.Spotify) { |
| initializeSpotifyPlayer(); |
| } |
| } |
| } |
| }); |
| |
| function initializeSpotifyPlayer() { |
| if (spotifyPlayer) return; |
| |
| spotifyPlayer = new Spotify.Player({ |
| name: 'Emotion Music Player', |
| getOAuthToken: cb => { cb(spotifyAccessToken); }, |
| volume: 0.5 |
| }); |
| |
| spotifyPlayer.addListener('ready', ({ device_id }) => { |
| console.log('Ready with Device ID', device_id); |
| deviceId = device_id; |
| }); |
| |
| spotifyPlayer.addListener('not_ready', ({ device_id }) => console.log('Device ID has gone offline', device_id)); |
| spotifyPlayer.addListener('initialization_error', ({ message }) => showError(`Spotify Player Init Error: ${message}`)); |
| spotifyPlayer.addListener('authentication_error', ({ message }) => showError(`Spotify Auth Error: ${message}`)); |
| spotifyPlayer.addListener('account_error', ({ message }) => showError(`Spotify Account Error: ${message}`)); |
| |
| spotifyPlayer.connect().then(success => { |
| if (success) console.log('The Spotify Player has connected successfully!'); |
| }); |
| } |
| |
| async function playSongForEmotion(emotion) { |
| if (!deviceId) { |
| showError("Spotify player is not ready."); |
| return; |
| } |
| |
| let searchQuery; |
| switch (emotion) { |
| case 'happy': searchQuery = 'genre:pop happy'; break; |
| case 'sad': searchQuery = 'genre:acoustic sad'; break; |
| case 'angry': searchQuery = 'genre:rock angry'; break; |
| case 'neutral': searchQuery = 'genre:ambient focus'; break; |
| case 'surprise': searchQuery = 'genre:electronic energetic'; break; |
| case 'disgust': searchQuery = 'genre:metal industrial'; break; |
| default: searchQuery = 'genre:chill'; |
| } |
| |
| try { |
| const response = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(searchQuery)}&type=track&limit=1`, { |
| headers: { 'Authorization': `Bearer ${spotifyAccessToken}` } |
| }); |
| if (!response.ok) throw new Error(`Spotify API error: ${response.statusText}`); |
| |
| const data = await response.json(); |
| const track = data.tracks.items[0]; |
| |
| if (track) { |
| await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, { |
| method: 'PUT', |
| body: JSON.stringify({ uris: [track.uri] }), |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${spotifyAccessToken}` |
| }, |
| }); |
| spotifyWidget.innerHTML = ` |
| <img src="${track.album.images[0].url}" alt="Album Art" class="w-24 h-24 mx-auto rounded-md mb-2"> |
| <p class="font-bold">${track.name}</p> |
| <p class="text-gray-400">${track.artists.map(a => a.name).join(', ')}</p> |
| `; |
| spotifyPlayerDiv.classList.remove('hidden'); |
| } else { |
| spotifyWidget.innerHTML = `<p>Could not find a song for "${emotion}".</p>`; |
| } |
| } catch (error) { |
| console.error('Error playing song:', error); |
| showError(`Failed to play song. Please check console for details.`); |
| } |
| } |
| |
| |
| async function loadModelAndScaler() { |
| try { |
| const modelUrl = `${HUGGINGFACE_REPO_URL}rf_emotion.onnx`; |
| ortSession = await ort.InferenceSession.create(modelUrl); |
| |
| const scalerUrl = `${HUGGINGFACE_REPO_URL}scaler.json`; |
| const response = await fetch(scalerUrl); |
| const scalerData = await response.json(); |
| scalerMean = scalerData.mean_; |
| scalerScale = scalerData.scale_; |
| |
| console.log("Model and scaler loaded successfully."); |
| } catch (error) { |
| console.error("Failed to load model or scaler:", error); |
| showError("Could not load the AI model from Hugging Face. Please check the repository URL and file names."); |
| throw error; |
| } |
| } |
| |
| function buildFeatures(landmarks, imageWidth, imageHeight) { |
| const features = []; |
| for (const landmark of landmarks) { |
| features.push(landmark.x * imageWidth, landmark.y * imageHeight); |
| } |
| return features; |
| } |
| |
| function scaleFeatures(features) { |
| if (!scalerMean || !scalerScale) return features; |
| return features.map((val, i) => (val - scalerMean[i]) / scalerScale[i]); |
| } |
| |
| |
| async function onResults(results) { |
| canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); |
| |
| if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0 && ortSession) { |
| const landmarks = results.multiFaceLandmarks[0]; |
| const w = videoElement.videoWidth; |
| const h = videoElement.videoHeight; |
| |
| if (w === 0 || h === 0) return; |
| |
| const features = buildFeatures(landmarks, w, h); |
| const scaledFeatures = scaleFeatures(features); |
| |
| try { |
| const tensor = new ort.Tensor('float32', scaledFeatures, [1, scaledFeatures.length]); |
| const feeds = { 'float_input': tensor }; |
| const modelResults = await ortSession.run(feeds); |
| |
| const prediction = modelResults.output_label.data[0]; |
| const predictedEmotion = emotions[prediction]; |
| emotionText.textContent = predictedEmotion; |
| |
| if (predictedEmotion !== window.lastPlayedEmotion) { |
| playSongForEmotion(predictedEmotion); |
| window.lastPlayedEmotion = predictedEmotion; |
| } |
| } catch(e) { |
| console.error("Error during model inference:", e); |
| } |
| } |
| } |
| |
| async function initializeDetection() { |
| await loadModelAndScaler(); |
| |
| faceMesh = new FaceMesh({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`}); |
| faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 }); |
| faceMesh.onResults(onResults); |
| |
| const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 } }); |
| videoElement.srcObject = stream; |
| |
| return new Promise((resolve) => { |
| videoElement.onloadedmetadata = () => { |
| videoElement.play(); |
| canvasElement.width = videoElement.videoWidth; |
| canvasElement.height = videoElement.videoHeight; |
| loadingDiv.classList.add('hidden'); |
| emotionDisplay.classList.remove('hidden'); |
| resolve(); |
| }; |
| }); |
| } |
| |
| async function detectionLoop() { |
| if (!isDetecting) return; |
| await faceMesh.send({image: videoElement}); |
| requestAnimationFrame(detectionLoop); |
| } |
| |
| |
| startBtn.addEventListener('click', async () => { |
| if (isDetecting) return; |
| startBtn.disabled = true; |
| startBtn.textContent = 'Initializing...'; |
| |
| try { |
| await initializeDetection(); |
| isDetecting = true; |
| detectionLoop(); |
| stopBtn.disabled = false; |
| } catch (error) { |
| showError("Failed to initialize. Check permissions and model URL."); |
| startBtn.disabled = false; |
| } finally { |
| startBtn.textContent = 'Start Emotion Detection'; |
| } |
| }); |
| |
| stopBtn.addEventListener('click', () => { |
| isDetecting = false; |
| const stream = videoElement.srcObject; |
| if (stream) { |
| stream.getTracks().forEach(track => track.stop()); |
| videoElement.srcObject = null; |
| } |
| startBtn.disabled = false; |
| stopBtn.disabled = true; |
| emotionText.textContent = '...'; |
| if (spotifyPlayer) spotifyPlayer.pause(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|