Download index.html from mrdbourke/keeptrack-frames-viewer: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/mrdbourke/keeptrack-frames-viewer/resolve/main/index.html
- Command line
-
hf download hf://spaces/mrdbourke/keeptrack-frames-viewer/index.html
-
curl -L -o index.html https://huggingface.co/spaces/mrdbourke/keeptrack-frames-viewer/resolve/main/index.html
16.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Frame Viewer</title> | |
| <style> | |
| /* Reset some default styles */ | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: #f5f5f5; | |
| color: #333; | |
| line-height: 1.6; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; /* Ensure body takes at least viewport height */ | |
| } | |
| /* Header Styling */ | |
| header { | |
| background-color: #2c3e50; | |
| color: #ecf0f1; | |
| padding: 15px 20px; | |
| text-align: center; | |
| font-size: 1.5em; | |
| font-weight: bold; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| margin-bottom: 20px; | |
| position: relative; /* For positioning the sort dropdown */ | |
| } | |
| /* Sorting Dropdown Styling */ | |
| #sort-container { | |
| position: absolute; | |
| top: 15px; | |
| right: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| #sort-container label { | |
| color: #ecf0f1; | |
| font-size: 0.9em; | |
| } | |
| #sort-select { | |
| padding: 5px 10px; | |
| border: none; | |
| border-radius: 4px; | |
| font-size: 0.9em; | |
| cursor: pointer; | |
| } | |
| /* Main Container */ | |
| #main-container { | |
| display: flex; | |
| flex-direction: column; /* Changed to column layout */ | |
| align-items: center; | |
| padding: 20px; | |
| gap: 20px; /* Reduced gap */ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| flex: 1; /* Allow main container to grow */ | |
| } | |
| /* Image Section */ | |
| #image-section { | |
| width: 90%; /* Adjust as needed, but keep it responsive */ | |
| max-width: 900px; /* Limit maximum width */ | |
| background-color: #fff; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
| text-align: center; | |
| margin-bottom: 10px; /* Space between image and buttons */ | |
| } | |
| #image-canvas { | |
| width: 100%; | |
| height: auto; | |
| border-radius: 5px; | |
| border: 1px solid #ddd; | |
| display: block; /* Ensure image takes full width of its container */ | |
| } | |
| /* Metadata Section */ | |
| #metadata-section { | |
| width: 90%; | |
| max-width: 900px; | |
| background-color: #fff; | |
| padding: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
| } | |
| #metadata-form h2 { | |
| margin-bottom: 15px; | |
| font-size: 1.3em; | |
| border-bottom: 1px solid #ccc; | |
| padding-bottom: 10px; | |
| } | |
| /* Form Fields */ | |
| #metadata-form label { | |
| display: block; | |
| margin-top: 10px; | |
| font-weight: 500; | |
| color: #555; | |
| } | |
| #metadata-form input, | |
| #metadata-form textarea, | |
| #metadata-form select { | |
| width: 100%; | |
| padding: 8px 10px; | |
| margin-top: 5px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| background-color: #fafafa; | |
| transition: border-color 0.3s; | |
| font-size: 0.95em; | |
| } | |
| #metadata-form input:focus, | |
| #metadata-form textarea:focus, | |
| #metadata-form select:focus { | |
| border-color: #7f8c8d; | |
| outline: none; | |
| } | |
| /* Button Container */ | |
| #button-container { | |
| margin-top: 10px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| justify-content: center; /* Center buttons */ | |
| } | |
| /* Buttons */ | |
| button { | |
| padding: 10px 15px; | |
| font-size: 0.95em; | |
| cursor: pointer; | |
| border: none; | |
| border-radius: 4px; | |
| transition: background-color 0.3s, transform 0.2s; | |
| color: #fff; | |
| min-width: 100px; /* Ensures buttons have a decent size even on smaller screens */ | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| } | |
| /* Specific Button Styles */ | |
| #prev-btn, #next-btn { | |
| background-color: #3498db; | |
| flex: 1; | |
| } | |
| #prev-btn:disabled, #next-btn:disabled { | |
| background-color: #95a5a6; | |
| cursor: not-allowed; | |
| } | |
| #save-btn { | |
| background-color: #2980b9; | |
| flex: 1; | |
| } | |
| #export-btn { | |
| background-color: #27ae60; | |
| flex: 1; | |
| } | |
| #delete-btn { | |
| background-color: #e74c3c; | |
| flex: 1; | |
| } | |
| /* Footer (Optional) */ | |
| footer { | |
| background-color: #2c3e50; | |
| color: #ecf0f1; | |
| text-align: center; | |
| padding: 10px 0; | |
| margin-top: 20px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| KeepTrack Frame Viewer | |
| <!-- Sorting Dropdown --> | |
| <div id="sort-container"> | |
| <label for="sort-select">Sort By:</label> | |
| <select id="sort-select"> | |
| <option value="item_number">Item Number</option> | |
| <option value="estimated_worth">Estimated Worth</option> | |
| <option value="overall_certainty_flag">Overall Certainty Flag</option> | |
| </select> | |
| </div> | |
| </header> | |
| <div id="main-container"> | |
| <div id="image-section"> | |
| <canvas id="image-canvas"></canvas> | |
| <div id="button-container"> | |
| <button id="prev-btn" title="Previous Frame">Previous</button> | |
| <button id="next-btn" title="Next Frame">Next</button> | |
| </div> | |
| </div> | |
| <div id="metadata-section"> | |
| <form id="metadata-form"> | |
| <h2>Metadata</h2> | |
| <div id="form-fields"></div> | |
| </form> | |
| <div id="button-container"> | |
| <button type="button" id="save-btn">Save Changes</button> | |
| <button type="button" id="export-btn">Export Metadata</button> | |
| <button type="button" id="delete-btn">Delete Item</button> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| © 2024 KeepTrack Frame Viewer App | |
| </footer> | |
| <script> | |
| // Global Variables | |
| let frames = []; | |
| let currentIndex = 0; | |
| const canvas = document.getElementById('image-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const formFields = document.getElementById('form-fields'); | |
| const saveBtn = document.getElementById('save-btn'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const deleteBtn = document.getElementById('delete-btn'); | |
| const sortSelect = document.getElementById('sort-select'); // Added | |
| // Load JSON Data | |
| fetch('frames_metadata_with_boxes.json') | |
| .then(response => response.json()) | |
| .then(data => { | |
| frames = data; | |
| if (frames.length > 0) { | |
| sortFrames(); // Initial sort based on default selection | |
| renderFrame(currentIndex); | |
| } else { | |
| alert('No frames found in the JSON file.'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error loading JSON:', error); | |
| alert('Failed to load frames_metadata_with_boxes.json. Please ensure the file exists and is correctly formatted.'); | |
| }); | |
| // Event Listeners for Navigation | |
| prevBtn.addEventListener('click', () => { | |
| if (currentIndex > 0) { | |
| currentIndex--; | |
| renderFrame(currentIndex); | |
| } | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| if (currentIndex < frames.length - 1) { | |
| currentIndex++; | |
| renderFrame(currentIndex); | |
| } | |
| }); | |
| // Save Changes Button | |
| saveBtn.addEventListener('click', () => { | |
| saveFormData(); | |
| alert('Changes saved in memory. To export, click "Export Metadata".'); | |
| }); | |
| // Export Button | |
| exportBtn.addEventListener('click', () => { | |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(frames, null, 2)); | |
| const downloadAnchor = document.createElement('a'); | |
| downloadAnchor.setAttribute("href", dataStr); | |
| downloadAnchor.setAttribute("download", "modified_frames_metadata_with_boxes.json"); | |
| document.body.appendChild(downloadAnchor); | |
| downloadAnchor.click(); | |
| downloadAnchor.remove(); | |
| }); | |
| // Delete Item Button | |
| deleteBtn.addEventListener('click', () => { | |
| if (confirm('Are you sure you want to delete this item?')) { | |
| frames.splice(currentIndex, 1); | |
| if (frames.length === 0) { | |
| alert('All items have been deleted.'); | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| formFields.innerHTML = ''; | |
| prevBtn.disabled = true; | |
| nextBtn.disabled = true; | |
| saveBtn.disabled = true; | |
| exportBtn.disabled = true; | |
| deleteBtn.disabled = true; | |
| } else { | |
| if (currentIndex >= frames.length) { | |
| currentIndex = frames.length - 1; | |
| } | |
| renderFrame(currentIndex); | |
| } | |
| } | |
| }); | |
| // Event Listener for Sorting | |
| sortSelect.addEventListener('change', () => { | |
| sortFrames(); | |
| currentIndex = 0; // Reset to first frame after sorting | |
| renderFrame(currentIndex); | |
| }); | |
| // Function to Render a Frame | |
| function renderFrame(index) { | |
| const frame = frames[index]; | |
| const img = new Image(); | |
| img.src = frame.frame_filename; | |
| img.onload = () => { | |
| // Resize canvas to match image if necessary | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| // Draw Image | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| // Draw Bounding Boxes | |
| if (frame.boxes_converted && Array.isArray(frame.boxes_converted)) { | |
| frame.boxes_converted.forEach(box => { | |
| const [ymin, xmin, ymax, xmax] = box; | |
| const width = xmax - xmin; | |
| const height = ymax - ymin; | |
| ctx.strokeStyle = 'green'; | |
| ctx.lineWidth = 4; | |
| ctx.strokeRect(xmin, ymin, width, height); | |
| }); | |
| } | |
| }; | |
| img.onerror = () => { | |
| alert(`Failed to load image: ${frame.frame_filename}`); | |
| }; | |
| // Populate Metadata Form | |
| populateForm(frame); | |
| // Update Navigation Buttons | |
| updateNavButtons(); | |
| } | |
| // Function to Update Navigation Buttons | |
| function updateNavButtons() { | |
| prevBtn.disabled = currentIndex === 0; | |
| nextBtn.disabled = currentIndex === frames.length - 1; | |
| } | |
| // Function to Populate the Form with Metadata | |
| function populateForm(frame) { | |
| formFields.innerHTML = ''; // Clear previous fields | |
| // Define which fields to include in the form | |
| const fields = [ | |
| { key: 'item_number', label: 'Item Number', type: 'number' }, | |
| { key: 'item_name', label: 'Item Name', type: 'text' }, | |
| { key: 'item_type', label: 'Item Type', type: 'text' }, | |
| { key: 'item_description', label: 'Item Description', type: 'textarea' }, | |
| { key: 'item_brand', label: 'Item Brand', type: 'text' }, | |
| { key: 'item_condition', label: 'Item Condition', type: 'text' }, | |
| { key: 'number_of_items', label: 'Number of Items', type: 'number' }, | |
| { key: 'estimated_worth', label: 'Estimated Worth', type: 'number', step: '0.01' }, | |
| { key: 'estimated_worth_flag', label: 'Estimated Worth Flag', type: 'number' }, | |
| { key: 'mentioned_worth', label: 'Mentioned Worth', type: 'text' }, | |
| { key: 'room', label: 'Room', type: 'text' }, | |
| { key: 'timestamp', label: 'Timestamp', type: 'text' }, | |
| { key: 'overall_certainty_flag', label: 'Overall Certainty Flag', type: 'number' }, | |
| { key: 'is_similar_to', label: 'Is Similar To', type: 'text' }, | |
| { key: 'frame_filename', label: 'Frame Filename', type: 'text', disabled: true } | |
| ]; | |
| fields.forEach(field => { | |
| const label = document.createElement('label'); | |
| label.textContent = field.label; | |
| const input = field.type === 'textarea' ? document.createElement('textarea') : document.createElement('input'); | |
| if (field.type !== 'textarea') { | |
| input.type = field.type; | |
| } | |
| input.value = frame[field.key] !== 'nan' ? frame[field.key] : ''; | |
| input.id = field.key; | |
| input.name = field.key; | |
| if (field.step) input.step = field.step; | |
| if (field.disabled) input.disabled = true; | |
| label.appendChild(input); | |
| formFields.appendChild(label); | |
| }); | |
| } | |
| // Function to Save Form Data Back to Frames Array | |
| function saveFormData() { | |
| const frame = frames[currentIndex]; | |
| const inputs = formFields.querySelectorAll('input, textarea'); | |
| inputs.forEach(input => { | |
| if (input.disabled) return; // Skip disabled fields | |
| const key = input.name; | |
| let value = input.value; | |
| // Convert numeric fields | |
| const numericFields = [ | |
| 'item_number', | |
| 'number_of_items', | |
| 'estimated_worth', | |
| 'estimated_worth_flag', | |
| 'overall_certainty_flag' | |
| ]; | |
| if (numericFields.includes(key)) { | |
| value = value === '' ? 'nan' : Number(value); | |
| if (isNaN(value)) value = 'nan'; | |
| } | |
| frame[key] = value; | |
| }); | |
| } | |
| // Function to Sort Frames Based on Selected Field | |
| function sortFrames() { | |
| const sortBy = sortSelect.value; | |
| frames.sort((a, b) => { | |
| let aValue = a[sortBy]; | |
| let bValue = b[sortBy]; | |
| // Handle 'nan' values by treating them as less than any number or string | |
| if (aValue === 'nan') return 1; | |
| if (bValue === 'nan') return -1; | |
| // Convert to numbers if sorting by numerical fields | |
| if (sortBy === 'item_number' || sortBy === 'estimated_worth' || sortBy === 'overall_certainty_flag') { | |
| aValue = Number(aValue); | |
| bValue = Number(bValue); | |
| if (isNaN(aValue)) aValue = -Infinity; | |
| if (isNaN(bValue)) bValue = -Infinity; | |
| } | |
| if (aValue < bValue) return -1; | |
| if (aValue > bValue) return 1; | |
| return 0; | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |