Download templates/step1_reference.html from rethinks/childYb: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/spaces/rethinks/childYb/resolve/main/templates/step1_reference.html
- Command line
-
hf download hf://spaces/rethinks/childYb/templates/step1_reference.html
-
curl -L -o step1_reference.html https://huggingface.co/spaces/rethinks/childYb/resolve/main/templates/step1_reference.html
25.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Step 1: Upload Reference Photos - Smart Photo Selection</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: #f5f7fa; | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| /* Header */ | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 30px 20px; | |
| text-align: center; | |
| color: white; | |
| } | |
| .header h1 { | |
| font-size: 28px; | |
| font-weight: 600; | |
| margin-bottom: 5px; | |
| } | |
| .header h1 .highlight { | |
| color: #ffd700; | |
| } | |
| .header p { | |
| opacity: 0.9; | |
| font-size: 14px; | |
| } | |
| /* Main Container */ | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 30px 20px; | |
| } | |
| /* Step Indicator */ | |
| .steps { | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| gap: 15px; | |
| margin-bottom: 40px; | |
| flex-wrap: wrap; | |
| } | |
| .step { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| min-width: 80px; | |
| } | |
| .step-circle { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| background: #e0e0e0; | |
| color: #999; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| font-size: 16px; | |
| transition: all 0.3s; | |
| } | |
| .step.active .step-circle { | |
| background: #667eea; | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .step.completed .step-circle { | |
| background: #4CAF50; | |
| color: white; | |
| } | |
| .step-label { | |
| font-size: 12px; | |
| color: #666; | |
| text-align: center; | |
| max-width: 80px; | |
| } | |
| .step.active .step-label { | |
| color: #667eea; | |
| font-weight: 500; | |
| } | |
| .step-connector { | |
| width: 40px; | |
| height: 2px; | |
| background: #e0e0e0; | |
| margin-top: 20px; | |
| } | |
| /* Card */ | |
| .card { | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 2px 20px rgba(0,0,0,0.08); | |
| padding: 40px; | |
| margin-bottom: 20px; | |
| } | |
| .card-icon { | |
| width: 60px; | |
| height: 60px; | |
| margin: 0 auto 20px; | |
| background: #f0f4ff; | |
| border-radius: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .card-icon svg { | |
| width: 30px; | |
| height: 30px; | |
| color: #667eea; | |
| } | |
| .card h2 { | |
| text-align: center; | |
| font-size: 22px; | |
| font-weight: 600; | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .card > p { | |
| text-align: center; | |
| color: #666; | |
| margin-bottom: 30px; | |
| } | |
| /* Upload Area */ | |
| .upload-area { | |
| border: 2px dashed #d0d7e3; | |
| border-radius: 12px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| background: #fafbfc; | |
| } | |
| .upload-area:hover { | |
| border-color: #667eea; | |
| background: #f0f4ff; | |
| } | |
| .upload-area.drag-over { | |
| border-color: #667eea; | |
| background: #e8edff; | |
| } | |
| .upload-icon { | |
| font-size: 48px; | |
| margin-bottom: 15px; | |
| } | |
| .upload-area h3 { | |
| font-size: 18px; | |
| font-weight: 500; | |
| margin-bottom: 8px; | |
| color: #333; | |
| } | |
| .upload-area p { | |
| color: #888; | |
| font-size: 14px; | |
| margin-bottom: 20px; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 12px 28px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 15px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-family: inherit; | |
| } | |
| .btn-primary { | |
| background: #667eea; | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: #5a6fd6; | |
| transform: translateY(-1px); | |
| } | |
| .btn-primary:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .btn-secondary { | |
| background: #f0f0f0; | |
| color: #666; | |
| } | |
| .btn-secondary:hover { | |
| background: #e0e0e0; | |
| } | |
| .btn-success { | |
| background: #4CAF50; | |
| color: white; | |
| } | |
| .btn-success:hover { | |
| background: #43a047; | |
| } | |
| .btn-success:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| /* Reference Preview */ | |
| .reference-section { | |
| margin-top: 30px; | |
| } | |
| .reference-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| .reference-header h3 { | |
| font-size: 16px; | |
| font-weight: 500; | |
| color: #333; | |
| } | |
| .reference-count { | |
| background: #667eea; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| .reference-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); | |
| gap: 15px; | |
| } | |
| .reference-card { | |
| position: relative; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| background: white; | |
| } | |
| .reference-card img { | |
| width: 100%; | |
| height: 140px; | |
| object-fit: cover; | |
| } | |
| .reference-card .status { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| padding: 8px; | |
| font-size: 11px; | |
| text-align: center; | |
| font-weight: 500; | |
| } | |
| .reference-card .status.success { | |
| background: rgba(76, 175, 80, 0.95); | |
| color: white; | |
| } | |
| .reference-card .status.error { | |
| background: rgba(244, 67, 54, 0.95); | |
| color: white; | |
| } | |
| /* Info Box */ | |
| .info-box { | |
| background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-top: 30px; | |
| } | |
| .info-box h4 { | |
| color: #1565c0; | |
| font-size: 14px; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .info-box ul { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .info-box li { | |
| color: #1976d2; | |
| font-size: 13px; | |
| padding: 6px 0; | |
| padding-left: 20px; | |
| position: relative; | |
| } | |
| .info-box li::before { | |
| content: "•"; | |
| position: absolute; | |
| left: 0; | |
| color: #1976d2; | |
| } | |
| /* Navigation */ | |
| .nav-buttons { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 30px; | |
| gap: 15px; | |
| } | |
| /* Skip Option */ | |
| .skip-option { | |
| text-align: center; | |
| margin-top: 25px; | |
| padding-top: 25px; | |
| border-top: 1px solid #eee; | |
| } | |
| .skip-option a { | |
| color: #888; | |
| text-decoration: none; | |
| font-size: 14px; | |
| } | |
| .skip-option a:hover { | |
| color: #667eea; | |
| text-decoration: underline; | |
| } | |
| /* How it works */ | |
| .how-it-works { | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 2px 20px rgba(0,0,0,0.08); | |
| padding: 30px 40px; | |
| } | |
| .how-it-works h3 { | |
| font-size: 16px; | |
| font-weight: 600; | |
| margin-bottom: 20px; | |
| color: #333; | |
| } | |
| .how-it-works ol { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .how-it-works li { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| padding: 10px 0; | |
| color: #555; | |
| font-size: 14px; | |
| } | |
| .how-it-works .step-num { | |
| width: 24px; | |
| height: 24px; | |
| background: #f0f4ff; | |
| color: #667eea; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: 600; | |
| flex-shrink: 0; | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 30px; | |
| color: #999; | |
| } | |
| .empty-state p { | |
| font-size: 14px; | |
| } | |
| @media (max-width: 600px) { | |
| .card { | |
| padding: 25px 20px; | |
| } | |
| .steps { | |
| gap: 10px; | |
| } | |
| .step-connector { | |
| width: 20px; | |
| } | |
| .nav-buttons { | |
| flex-direction: column; | |
| } | |
| .nav-buttons .btn { | |
| width: 100%; | |
| } | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.7); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: opacity 0.3s, visibility 0.3s; | |
| } | |
| .loading-overlay.show { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 4px solid rgba(255, 255, 255, 0.3); | |
| border-top-color: #667eea; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| color: white; | |
| font-size: 18px; | |
| font-weight: 500; | |
| } | |
| .loading-subtext { | |
| color: rgba(255, 255, 255, 0.7); | |
| font-size: 14px; | |
| margin-top: 8px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <div class="header"> | |
| <h1>Create a Personalized Photo Book for <span class="highlight">Your Child</span>!</h1> | |
| <p>AI-powered photo selection to find the best moments</p> | |
| </div> | |
| <div class="container"> | |
| <!-- Step Indicator --> | |
| <div class="steps"> | |
| <div class="step active"> | |
| <div class="step-circle">1</div> | |
| <div class="step-label">Reference</div> | |
| </div> | |
| <div class="step-connector"></div> | |
| <div class="step"> | |
| <div class="step-circle">2</div> | |
| <div class="step-label">Pre-processing</div> | |
| </div> | |
| <div class="step-connector"></div> | |
| <div class="step"> | |
| <div class="step-circle">3</div> | |
| <div class="step-label">Review</div> | |
| </div> | |
| <div class="step-connector"></div> | |
| <div class="step"> | |
| <div class="step-circle">4</div> | |
| <div class="step-label">Detecting Favorites</div> | |
| </div> | |
| </div> | |
| <!-- Main Card --> | |
| <div class="card"> | |
| <div class="card-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/> | |
| </svg> | |
| </div> | |
| <h2>Upload Reference Photos of Your Child</h2> | |
| <p>Upload 2-3 clear photos so the AI can learn to recognize your child</p> | |
| <!-- Upload Area --> | |
| <div class="upload-area" id="drop-zone"> | |
| <div class="upload-icon">📷</div> | |
| <h3>Drag & Drop Photos Here</h3> | |
| <p>or click to browse</p> | |
| <input type="file" id="file-input" multiple accept=".jpg,.jpeg,.png,.heic,.heif,.webp" hidden> | |
| <button class="btn btn-primary" onclick="document.getElementById('file-input').click()"> | |
| Select Photos | |
| </button> | |
| </div> | |
| <!-- Reference Preview Section --> | |
| <div class="reference-section" id="reference-section"> | |
| <div class="reference-header"> | |
| <h3>Reference Photos</h3> | |
| <span class="reference-count" id="ref-count">0 loaded</span> | |
| </div> | |
| <div class="reference-grid" id="reference-grid"> | |
| <div class="empty-state" id="empty-state"> | |
| <p>No photos uploaded yet</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Box --> | |
| <div class="info-box"> | |
| <h4>💡 Tips for Best Results</h4> | |
| <ul> | |
| <li>Use photos with <strong>clear, front-facing</strong> views of your child's face</li> | |
| <li>Good lighting helps - avoid backlit or very dark photos</li> | |
| <li>Different expressions/angles help the AI recognize them better</li> | |
| <li>Make sure the face is clearly visible (not too small or far away)</li> | |
| </ul> | |
| </div> | |
| <!-- Navigation --> | |
| <div class="nav-buttons"> | |
| <button class="btn btn-secondary" onclick="clearReferences()" id="clear-btn" disabled> | |
| Clear All | |
| </button> | |
| <button class="btn btn-success" onclick="goToStep2()" id="next-btn" disabled> | |
| Next: Upload Event Photos → | |
| </button> | |
| </div> | |
| <!-- Skip Option --> | |
| <div class="skip-option"> | |
| <a href="/step2">Skip this step (process all photos without face filtering)</a> | |
| </div> | |
| </div> | |
| <!-- How it works --> | |
| <div class="how-it-works"> | |
| <h3>How it works:</h3> | |
| <ol> | |
| <li><span class="step-num">1</span> Upload 2-3 clear photos of your child's face</li> | |
| <li><span class="step-num">2</span> The AI learns to recognize your child</li> | |
| <li><span class="step-num">3</span> Upload all your event/school photos</li> | |
| <li><span class="step-num">4</span> AI finds photos containing your child</li> | |
| <li><span class="step-num">5</span> Review and select the best ones</li> | |
| </ol> | |
| </div> | |
| </div> | |
| <script> | |
| let sessionId = '{{ session_id }}'; | |
| let referenceCount = 0; | |
| // File input change handler | |
| document.getElementById('file-input').addEventListener('change', function(e) { | |
| uploadReferencePhotos(e.target.files); | |
| }); | |
| // Drag and drop | |
| const dropZone = document.getElementById('drop-zone'); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('drag-over'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => { | |
| dropZone.classList.remove('drag-over'); | |
| }); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('drag-over'); | |
| uploadReferencePhotos(e.dataTransfer.files); | |
| }); | |
| let modelLoaded = false; // Track if model has been loaded | |
| let modelLoading = false; // Track if model is currently loading | |
| // Preload model immediately when script runs (IIFE) | |
| (function preloadModelNow() { | |
| if (modelLoaded || modelLoading) return; | |
| modelLoading = true; | |
| // Show loading indicator immediately in bottom-right corner | |
| const statusEl = document.createElement('div'); | |
| statusEl.id = 'model-status'; | |
| statusEl.style.cssText = 'position: fixed; bottom: 20px; right: 20px; background: #667eea; color: white; padding: 12px 18px; border-radius: 8px; font-size: 14px; box-shadow: 0 4px 15px rgba(0,0,0,0.25); z-index: 9999; display: flex; align-items: center; gap: 10px;'; | |
| statusEl.innerHTML = '<span style="display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 1s linear infinite;"></span> Loading AI model...'; | |
| document.body.appendChild(statusEl); | |
| console.log('[Preload] Starting model preload...'); | |
| fetch('/preload_model') | |
| .then(response => response.json()) | |
| .then(data => { | |
| console.log('[Preload] Response:', data); | |
| if (data.success) { | |
| modelLoaded = true; | |
| statusEl.innerHTML = '✓ AI model ready'; | |
| statusEl.style.background = '#28a745'; | |
| setTimeout(() => statusEl.remove(), 2000); | |
| } else { | |
| statusEl.innerHTML = '⚠ Model will load on first upload'; | |
| statusEl.style.background = '#ffc107'; | |
| statusEl.style.color = '#333'; | |
| setTimeout(() => statusEl.remove(), 3000); | |
| } | |
| modelLoading = false; | |
| }) | |
| .catch(e => { | |
| console.error('[Preload] Error:', e); | |
| statusEl.innerHTML = '⚠ Model will load on first upload'; | |
| statusEl.style.background = '#ffc107'; | |
| statusEl.style.color = '#333'; | |
| setTimeout(() => statusEl.remove(), 3000); | |
| modelLoading = false; | |
| }); | |
| })(); | |
| async function uploadReferencePhotos(files) { | |
| const formData = new FormData(); | |
| for (let file of files) { | |
| formData.append('files', file); | |
| } | |
| // Show loading overlay | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const loadingText = document.getElementById('loading-text'); | |
| const loadingSubtext = document.getElementById('loading-subtext'); | |
| loadingText.textContent = 'Detecting face...'; | |
| loadingSubtext.textContent = 'Analyzing your photo'; | |
| loadingOverlay.classList.add('show'); | |
| try { | |
| const response = await fetch('/upload_reference', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| // Hide loading overlay | |
| loadingOverlay.classList.remove('show'); | |
| // Model is now loaded for future uploads | |
| modelLoaded = true; | |
| if (data.error) { | |
| alert('Error: ' + data.error); | |
| return; | |
| } | |
| // Update session ID | |
| sessionId = data.session_id; | |
| // Hide empty state | |
| document.getElementById('empty-state').style.display = 'none'; | |
| // Add each result to the grid | |
| const grid = document.getElementById('reference-grid'); | |
| for (let result of data.results) { | |
| addReferenceCard(result); | |
| } | |
| // Update count | |
| referenceCount = data.total_references; | |
| updateUI(); | |
| } catch (error) { | |
| // Hide loading overlay on error | |
| loadingOverlay.classList.remove('show'); | |
| console.error('Upload error:', error); | |
| alert('Upload failed. Please try again.'); | |
| } | |
| } | |
| function addReferenceCard(result) { | |
| const grid = document.getElementById('reference-grid'); | |
| const card = document.createElement('div'); | |
| card.className = 'reference-card'; | |
| const img = document.createElement('img'); | |
| img.src = '/reference_thumbnail/' + result.thumbnail; | |
| img.alt = result.filename; | |
| const status = document.createElement('div'); | |
| status.className = 'status ' + (result.success ? 'success' : 'error'); | |
| if (result.success) { | |
| status.textContent = `Face detected (${(result.face_used.confidence * 100).toFixed(0)}%)`; | |
| } else { | |
| status.textContent = result.error || 'No face detected'; | |
| } | |
| card.appendChild(img); | |
| card.appendChild(status); | |
| grid.appendChild(card); | |
| } | |
| function updateUI() { | |
| document.getElementById('ref-count').textContent = `${referenceCount} loaded`; | |
| document.getElementById('clear-btn').disabled = referenceCount === 0; | |
| document.getElementById('next-btn').disabled = referenceCount === 0; | |
| } | |
| async function clearReferences() { | |
| if (!confirm('Clear all reference photos?')) return; | |
| try { | |
| await fetch('/clear_references', { method: 'POST' }); | |
| // Clear grid and show empty state | |
| document.getElementById('reference-grid').innerHTML = '<div class="empty-state" id="empty-state"><p>No photos uploaded yet</p></div>'; | |
| referenceCount = 0; | |
| updateUI(); | |
| } catch (error) { | |
| console.error('Clear error:', error); | |
| } | |
| } | |
| function goToStep2() { | |
| if (referenceCount === 0) { | |
| alert('Please upload at least 1 reference photo'); | |
| return; | |
| } | |
| window.location.href = '/step2'; | |
| } | |
| // Check for existing references on page load | |
| async function checkExistingReferences() { | |
| try { | |
| const response = await fetch('/reference_status'); | |
| const data = await response.json(); | |
| if (data.reference_count > 0) { | |
| referenceCount = data.reference_count; | |
| document.getElementById('empty-state').style.display = 'none'; | |
| updateUI(); | |
| } | |
| } catch (error) { | |
| console.error('Status check error:', error); | |
| } | |
| } | |
| // Initialize | |
| checkExistingReferences(); | |
| </script> | |
| <!-- Loading Overlay --> | |
| <div id="loading-overlay" class="loading-overlay"> | |
| <div class="loading-spinner"></div> | |
| <div id="loading-text" class="loading-text">Loading AI model...</div> | |
| <div id="loading-subtext" class="loading-subtext">First load downloads the model (~120MB)</div> | |
| </div> | |
| </body> | |
| </html> | |