Spaces:
Running
Running
add functionality capture video and detect water container. check how much ml of water it can have do measurement automatically. - Follow Up Deployment
9a35b42 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hydration Tracker</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.18.0/dist/tf.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.2/dist/coco-ssd.min.js"></script> | |
| <style> | |
| .wave { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .wave::before { | |
| content: ""; | |
| position: absolute; | |
| left: 0; | |
| bottom: 0; | |
| right: 0; | |
| background-repeat: repeat; | |
| height: 10px; | |
| background-size: 20px 20px; | |
| background-image: radial-gradient(circle at 10px -5px, transparent 12px, #3b82f6 13px); | |
| } | |
| .wave::after { | |
| content: ""; | |
| position: absolute; | |
| left: 0; | |
| bottom: 0; | |
| right: 0; | |
| background-repeat: repeat; | |
| height: 15px; | |
| background-size: 40px 20px; | |
| background-image: radial-gradient(circle at 10px 15px, #3b82f6 12px, transparent 13px); | |
| } | |
| .glass { | |
| transition: all 0.5s ease; | |
| } | |
| .glass:hover { | |
| transform: translateY(-5px); | |
| } | |
| .progress-ring__circle { | |
| transition: stroke-dashoffset 0.5s; | |
| transform: rotate(-90deg); | |
| transform-origin: 50% 50%; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-blue-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="max-w-3xl mx-auto"> | |
| <!-- Header --> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold text-blue-800 mb-2">Hydration Tracker</h1> | |
| <p class="text-blue-600">Track your daily water intake and stay hydrated</p> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="bg-white rounded-xl shadow-lg p-6 mb-8"> | |
| <!-- Progress Circle --> | |
| <div class="flex flex-col items-center mb-8"> | |
| <div class="relative w-48 h-48 mb-4"> | |
| <svg class="w-full h-full" viewBox="0 0 100 100"> | |
| <!-- Background circle --> | |
| <circle cx="50" cy="50" r="45" fill="none" stroke="#e2e8f0" stroke-width="8"/> | |
| <!-- Progress circle --> | |
| <circle id="progress-circle" cx="50" cy="50" r="45" fill="none" stroke="#3b82f6" | |
| stroke-width="8" stroke-dasharray="283" stroke-dashoffset="283"/> | |
| </svg> | |
| <div class="absolute inset-0 flex flex-col items-center justify-center"> | |
| <span id="progress-percent" class="text-3xl font-bold text-blue-800">0%</span> | |
| <span class="text-blue-600">Daily Goal</span> | |
| </div> | |
| </div> | |
| <div id="progress-text" class="text-center"> | |
| <p class="text-lg font-semibold text-blue-800">Start drinking water!</p> | |
| <p class="text-blue-600">0 of 8 glasses (2L)</p> | |
| </div> | |
| </div> | |
| <!-- Video Capture --> | |
| <div class="mb-8 bg-blue-50 rounded-lg p-6"> | |
| <h2 class="text-xl font-semibold text-blue-800 mb-4">Measure Container Automatically</h2> | |
| <div class="flex flex-col items-center"> | |
| <video id="video" width="320" height="240" autoplay class="mb-4 rounded-lg bg-gray-200"></video> | |
| <canvas id="canvas" width="320" height="240" class="hidden"></canvas> | |
| <button id="capture-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"> | |
| <i class="fas fa-camera mr-2"></i>Capture & Measure | |
| </button> | |
| <div id="measure-result" class="mt-4 text-center hidden"> | |
| <p class="font-medium text-blue-800">Detected Container:</p> | |
| <p id="detected-container" class="text-lg font-bold text-blue-800">-</p> | |
| <p id="detected-size" class="text-blue-600">- ml</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Water Container Selection --> | |
| <div class="mb-8"> | |
| <h2 class="text-xl font-semibold text-blue-800 mb-4">Select Your Container</h2> | |
| <div class="grid grid-cols-2 sm:grid-cols-4 gap-4"> | |
| <div class="glass cursor-pointer bg-blue-50 rounded-lg p-4 flex flex-col items-center" | |
| data-size="250" data-name="Small Glass"> | |
| <div class="w-16 h-24 bg-blue-100 rounded-b-lg rounded-t-full relative wave mb-2" | |
| style="height: 40%;"></div> | |
| <span class="text-sm font-medium text-blue-800">Small Glass</span> | |
| <span class="text-xs text-blue-600">250ml</span> | |
| </div> | |
| <div class="glass cursor-pointer bg-blue-50 rounded-lg p-4 flex flex-col items-center" | |
| data-size="350" data-name="Medium Glass"> | |
| <div class="w-16 h-24 bg-blue-100 rounded-b-lg rounded-t-full relative wave mb-2" | |
| style="height: 60%;"></div> | |
| <span class="text-sm font-medium text-blue-800">Medium Glass</span> | |
| <span class="text-xs text-blue-600">350ml</span> | |
| </div> | |
| <div class="glass cursor-pointer bg-blue-50 rounded-lg p-4 flex flex-col items-center" | |
| data-size="500" data-name="Large Glass"> | |
| <div class="w-16 h-24 bg-blue-100 rounded-b-lg rounded-t-full relative wave mb-2" | |
| style="height: 80%;"></div> | |
| <span class="text-sm font-medium text-blue-800">Large Glass</span> | |
| <span class="text-xs text-blue-600">500ml</span> | |
| </div> | |
| <div class="glass cursor-pointer bg-blue-50 rounded-lg p-4 flex flex-col items-center" | |
| data-size="1000" data-name="Bottle"> | |
| <div class="w-16 h-24 bg-blue-100 rounded-b-lg rounded-t-full relative wave mb-2" | |
| style="height: 100%;"></div> | |
| <span class="text-sm font-medium text-blue-800">Bottle</span> | |
| <span class="text-xs text-blue-600">1L</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Add Water Intake --> | |
| <div class="mb-8"> | |
| <h2 class="text-xl font-semibold text-blue-800 mb-4">Add Water Intake</h2> | |
| <div class="flex flex-col sm:flex-row gap-4"> | |
| <div class="flex-1"> | |
| <label for="custom-amount" class="block text-sm font-medium text-blue-700 mb-1">Custom Amount (ml)</label> | |
| <input type="number" id="custom-amount" | |
| class="w-full px-4 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| </div> | |
| <div class="flex items-end"> | |
| <button id="add-water" | |
| class="w-full sm:w-auto px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition"> | |
| Add Water | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Today's Intake --> | |
| <div> | |
| <h2 class="text-xl font-semibold text-blue-800 mb-4">Today's Intake</h2> | |
| <div id="intake-list" class="space-y-3"> | |
| <div class="text-center py-4 text-blue-600" id="empty-state"> | |
| No water intake recorded yet. Add your first glass! | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-blue-100"> | |
| <div class="flex justify-between items-center"> | |
| <span class="font-medium text-blue-800">Total:</span> | |
| <span id="total-intake" class="font-bold text-blue-800">0 ml</span> | |
| </div> | |
| <div class="flex justify-between items-center mt-1"> | |
| <span class="font-medium text-blue-800">Glasses:</span> | |
| <span id="total-glasses" class="font-bold text-blue-800">0</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Hydration Tips --> | |
| <div class="bg-white rounded-xl shadow-lg p-6"> | |
| <h2 class="text-xl font-semibold text-blue-800 mb-4">Hydration Tips</h2> | |
| <div class="space-y-4"> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 mt-1 mr-3 text-blue-500"> | |
| <i class="fas fa-check-circle"></i> | |
| </div> | |
| <p class="text-blue-700">Aim for 8 glasses (2 liters) of water per day as a general guideline.</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 mt-1 mr-3 text-blue-500"> | |
| <i class="fas fa-check-circle"></i> | |
| </div> | |
| <p class="text-blue-700">Drink more if you're active, in hot weather, or pregnant/breastfeeding.</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 mt-1 mr-3 text-blue-500"> | |
| <i class="fas fa-check-circle"></i> | |
| </div> | |
| <p class="text-blue-700">Your urine color is a good indicator - aim for pale yellow.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| // Video Capture Elements | |
| const video = document.getElementById('video'); | |
| const canvas = document.getElementById('canvas'); | |
| const captureBtn = document.getElementById('capture-btn'); | |
| const measureResult = document.getElementById('measure-result'); | |
| const detectedContainer = document.getElementById('detected-container'); | |
| const detectedSize = document.getElementById('detected-size'); | |
| // Load COCO-SSD model | |
| let model; | |
| try { | |
| model = await cocoSsd.load(); | |
| console.log('Model loaded successfully'); | |
| } catch (err) { | |
| console.error('Failed to load model:', err); | |
| } | |
| // Start video stream | |
| if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
| navigator.mediaDevices.getUserMedia({ video: true }) | |
| .then(function(stream) { | |
| video.srcObject = stream; | |
| }) | |
| .catch(function(err) { | |
| console.error("Error accessing camera:", err); | |
| }); | |
| } | |
| // Capture and detect button | |
| captureBtn.addEventListener('click', async function() { | |
| if (!model) { | |
| alert('Model not loaded yet. Please wait...'); | |
| return; | |
| } | |
| // Draw video frame to canvas | |
| const context = canvas.getContext('2d'); | |
| context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
| // Detect objects | |
| const predictions = await model.detect(canvas); | |
| console.log('Predictions:', predictions); | |
| // Find drinking glasses or bottles | |
| const container = predictions.find(p => | |
| ['bottle', 'cup', 'wine glass'].includes(p.class) | |
| ); | |
| if (container) { | |
| // Estimate size based on bounding box dimensions | |
| const area = container.bbox[2] * container.bbox[3]; | |
| let size; | |
| if (container.class === 'bottle') { | |
| size = Math.round(area * 0.15); // Bottle size estimation | |
| if (size > 1500) size = 1000; | |
| else if (size > 800) size = 750; | |
| else size = 500; | |
| } else { | |
| size = Math.round(area * 0.3); // Glass size estimation | |
| if (size > 400) size = 500; | |
| else if (size > 300) size = 350; | |
| else size = 250; | |
| } | |
| detectedContainer.textContent = container.class.charAt(0).toUpperCase() + container.class.slice(1); | |
| detectedSize.textContent = `${size} ml`; | |
| measureResult.classList.remove('hidden'); | |
| // Auto-fill the custom amount field | |
| customAmount.value = size; | |
| } else { | |
| detectedContainer.textContent = 'No container detected'; | |
| detectedSize.textContent = ''; | |
| measureResult.classList.remove('hidden'); | |
| } | |
| }); | |
| // Constants | |
| const DAILY_GOAL_ML = 2000; // 2 liters | |
| const GLASS_SIZE_ML = 250; // Standard glass size | |
| // DOM Elements | |
| const progressCircle = document.getElementById('progress-circle'); | |
| const progressPercent = document.getElementById('progress-percent'); | |
| const progressText = document.getElementById('progress-text'); | |
| const totalIntake = document.getElementById('total-intake'); | |
| const totalGlasses = document.getElementById('total-glasses'); | |
| const intakeList = document.getElementById('intake-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const addWaterBtn = document.getElementById('add-water'); | |
| const customAmount = document.getElementById('custom-amount'); | |
| const glassContainers = document.querySelectorAll('.glass'); | |
| // State | |
| let currentIntake = 0; | |
| let glassesCount = 0; | |
| // Initialize | |
| updateUI(); | |
| // Event Listeners | |
| glassContainers.forEach(glass => { | |
| glass.addEventListener('click', function() { | |
| const size = parseInt(this.dataset.size); | |
| const name = this.dataset.name; | |
| addWater(size, name); | |
| }); | |
| }); | |
| addWaterBtn.addEventListener('click', function() { | |
| const amount = parseInt(customAmount.value); | |
| if (amount && amount > 0) { | |
| addWater(amount, 'Custom Amount'); | |
| customAmount.value = ''; | |
| } | |
| }); | |
| // Functions | |
| function addWater(amount, containerName) { | |
| currentIntake += amount; | |
| glassesCount = Math.round(currentIntake / GLASS_SIZE_ML); | |
| // Add to history | |
| if (emptyState) emptyState.style.display = 'none'; | |
| const now = new Date(); | |
| const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| const intakeItem = document.createElement('div'); | |
| intakeItem.className = 'flex justify-between items-center bg-blue-50 p-3 rounded-lg'; | |
| intakeItem.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3"> | |
| <i class="fas fa-tint text-blue-500"></i> | |
| </div> | |
| <div> | |
| <p class="font-medium text-blue-800">${containerName}</p> | |
| <p class="text-xs text-blue-600">${timeString}</p> | |
| </div> | |
| </div> | |
| <span class="font-bold text-blue-800">${amount} ml</span> | |
| `; | |
| intakeList.prepend(intakeItem); | |
| updateUI(); | |
| // Show feedback based on progress | |
| const percent = Math.min(Math.round((currentIntake / DAILY_GOAL_ML) * 100), 100); | |
| if (percent >= 100) { | |
| showFeedback("Great job! You've reached your daily goal!", "text-green-600"); | |
| } else if (percent >= 75) { | |
| showFeedback("Almost there! Keep going!", "text-blue-600"); | |
| } else if (percent >= 50) { | |
| showFeedback("Halfway there! You're doing well.", "text-blue-600"); | |
| } else if (percent >= 25) { | |
| showFeedback("Good start! Keep hydrating.", "text-blue-600"); | |
| } | |
| } | |
| function updateUI() { | |
| const percent = Math.min((currentIntake / DAILY_GOAL_ML) * 100, 100); | |
| const dashoffset = 283 - (283 * percent / 100); | |
| progressCircle.style.strokeDashoffset = dashoffset; | |
| progressPercent.textContent = `${Math.round(percent)}%`; | |
| totalIntake.textContent = `${currentIntake} ml`; | |
| totalGlasses.textContent = glassesCount; | |
| // Update progress text | |
| if (percent >= 100) { | |
| progressText.innerHTML = ` | |
| <p class="text-lg font-semibold text-green-600">Goal Achieved!</p> | |
| <p class="text-green-600">${glassesCount} of 8 glasses (${(currentIntake/1000).toFixed(1)}L of 2L)</p> | |
| `; | |
| } else { | |
| progressText.innerHTML = ` | |
| <p class="text-lg font-semibold text-blue-800">${glassesCount >= 4 ? 'Good progress!' : 'Keep drinking!'}</p> | |
| <p class="text-blue-600">${glassesCount} of 8 glasses (${(currentIntake/1000).toFixed(1)}L of 2L)</p> | |
| `; | |
| } | |
| } | |
| function showFeedback(message, colorClass) { | |
| const feedback = document.createElement('div'); | |
| feedback.className = `fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-white shadow-lg rounded-lg px-6 py-3 flex items-center ${colorClass} animate-bounce`; | |
| feedback.innerHTML = ` | |
| <i class="fas fa-check-circle mr-2"></i> | |
| <span>${message}</span> | |
| `; | |
| document.body.appendChild(feedback); | |
| setTimeout(() => { | |
| feedback.classList.remove('animate-bounce'); | |
| feedback.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => feedback.remove(), 300); | |
| }, 3000); | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=manmohanai/faceimotion" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |