Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import json | |
| import os | |
| import uuid | |
| from datetime import datetime | |
| from PIL import Image | |
| import base64 | |
| from io import BytesIO | |
| import streamlit.components.v1 as components | |
| from translations import get_translation, SUPPORTED_LANGUAGES | |
| from utils import load_users, save_users, load_labels, save_labels, validate_image, get_categories, calculate_statistics | |
| # Page configuration | |
| st.set_page_config( | |
| page_title="LabelIt! ๐ฎ๐ณ", | |
| page_icon="๐ท๏ธ", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Initialize session state | |
| if 'user' not in st.session_state: | |
| st.session_state.user = None | |
| if 'language' not in st.session_state: | |
| st.session_state.language = 'en' | |
| if 'location_fetched' not in st.session_state: | |
| st.session_state.location_fetched = False | |
| if 'user_location' not in st.session_state: | |
| st.session_state.user_location = {"lat": None, "lon": None, "accuracy": None, "method": None, "timestamp": None} | |
| if 'manual_location_mode' not in st.session_state: | |
| st.session_state.manual_location_mode = False | |
| # Check for existing data structure (for Hugging Face Spaces compatibility) | |
| DATA_DIR = 'data' | |
| IMAGES_DIR = os.path.join(DATA_DIR, 'images') | |
| USERS_FILE = os.path.join(DATA_DIR, 'users.json') | |
| LABELS_FILE = os.path.join(DATA_DIR, 'labels.json') | |
| # Only create directories if they don't exist and we have permissions | |
| if not os.path.exists(DATA_DIR): | |
| try: | |
| os.makedirs(DATA_DIR, exist_ok=True) | |
| except (OSError, PermissionError): | |
| st.error("Data directory not found. Please ensure 'data' folder exists in the workspace.") | |
| if not os.path.exists(IMAGES_DIR): | |
| try: | |
| os.makedirs(IMAGES_DIR, exist_ok=True) | |
| except (OSError, PermissionError): | |
| st.error("Images directory not found. Please ensure 'data/images' folder exists in the workspace.") | |
| # Initialize data files only if they don't exist | |
| if os.path.exists(DATA_DIR) and not os.path.exists(USERS_FILE): | |
| try: | |
| with open(USERS_FILE, 'w') as f: | |
| json.dump({}, f) | |
| except (OSError, PermissionError): | |
| pass | |
| if os.path.exists(DATA_DIR) and not os.path.exists(LABELS_FILE): | |
| try: | |
| with open(LABELS_FILE, 'w') as f: | |
| json.dump({}, f) | |
| except (OSError, PermissionError): | |
| pass | |
| def get_enhanced_location(): | |
| """Enhanced location capture with multiple fallback methods and accuracy indicators""" | |
| if not st.session_state.location_fetched and not st.session_state.manual_location_mode: | |
| st.markdown("### ๐ Location Capture for Image Upload") | |
| # Show importance message | |
| st.info("๐ **Location Required**: Each image upload needs GPS coordinates for better dataset quality and regional insights.") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| # Enhanced JavaScript location component with multiple fallback methods | |
| location_data = components.html( | |
| """ | |
| <script> | |
| let locationAttempts = 0; | |
| const maxAttempts = 3; | |
| function tryGetLocation() { | |
| locationAttempts++; | |
| document.getElementById("status").innerHTML = `๐ Attempting high-accuracy GPS location (${locationAttempts}/${maxAttempts})...`; | |
| if (navigator.geolocation) { | |
| const options = { | |
| enableHighAccuracy: true, | |
| timeout: 20000, | |
| maximumAge: 30000 | |
| }; | |
| navigator.geolocation.getCurrentPosition( | |
| function(position) { | |
| const lat = position.coords.latitude; | |
| const lon = position.coords.longitude; | |
| const accuracy = position.coords.accuracy; | |
| const altitude = position.coords.altitude; | |
| const heading = position.coords.heading; | |
| const speed = position.coords.speed; | |
| const timestamp = new Date().toISOString(); | |
| let accuracyLevel = "Low"; | |
| let accuracyColor = "#e74c3c"; | |
| let accuracyEmoji = "๐ก"; | |
| if (accuracy <= 10) { | |
| accuracyLevel = "High"; | |
| accuracyColor = "#27ae60"; | |
| accuracyEmoji = "๐ข"; | |
| } else if (accuracy <= 50) { | |
| accuracyLevel = "Medium"; | |
| accuracyColor = "#f39c12"; | |
| accuracyEmoji = "๐ "; | |
| } else { | |
| accuracyEmoji = "๐ด"; | |
| } | |
| document.getElementById("status").innerHTML = ` | |
| <div style="background: linear-gradient(45deg, #27ae60, #2ecc71); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);"> | |
| <div style="font-weight: bold; margin-bottom: 8px;">โ GPS Location Captured Successfully!</div> | |
| <div style="font-size: 0.95em; margin-bottom: 5px;">๐ <strong>${lat.toFixed(6)}, ${lon.toFixed(6)}</strong></div> | |
| <div style="font-size: 0.85em; margin-bottom: 5px;"> | |
| ${accuracyEmoji} Accuracy: <span style="color: ${accuracyColor}; font-weight: bold;">${accuracyLevel}</span> (ยฑ${accuracy.toFixed(0)}m) | |
| </div> | |
| <div style="font-size: 0.8em; opacity: 0.9;">โฐ ${new Date(timestamp).toLocaleString()}</div> | |
| ${altitude ? `<div style="font-size: 0.8em; opacity: 0.9;">โฐ๏ธ Altitude: ${altitude.toFixed(0)}m</div>` : ''} | |
| </div> | |
| `; | |
| window.parent.postMessage({ | |
| type: 'streamlit:setComponentValue', | |
| value: { | |
| lat: lat, | |
| lon: lon, | |
| accuracy: accuracy, | |
| altitude: altitude, | |
| heading: heading, | |
| speed: speed, | |
| method: "GPS", | |
| timestamp: timestamp, | |
| success: true | |
| } | |
| }, '*'); | |
| }, | |
| function(error) { | |
| let errorMsg = "๐ GPS location unavailable"; | |
| let errorDetail = ""; | |
| switch(error.code) { | |
| case error.PERMISSION_DENIED: | |
| errorMsg = "๐ซ Location access denied"; | |
| errorDetail = "Please allow location access in your browser settings"; | |
| break; | |
| case error.POSITION_UNAVAILABLE: | |
| errorMsg = "๐ GPS position unavailable"; | |
| errorDetail = "Your device's GPS might be disabled"; | |
| break; | |
| case error.TIMEOUT: | |
| errorMsg = "โฑ๏ธ GPS request timed out"; | |
| errorDetail = "GPS is taking too long to respond"; | |
| break; | |
| } | |
| if (locationAttempts < maxAttempts) { | |
| document.getElementById("status").innerHTML = ` | |
| <div style="background: linear-gradient(45deg, #f39c12, #e67e22); color: white; padding: 15px; border-radius: 10px; margin: 10px 0;"> | |
| <div style="font-weight: bold; margin-bottom: 5px;">${errorMsg}</div> | |
| <div style="font-size: 0.9em; margin-bottom: 5px;">${errorDetail}</div> | |
| <div style="font-size: 0.8em;">๐ Retrying in 2 seconds... (${locationAttempts}/${maxAttempts})</div> | |
| </div> | |
| `; | |
| setTimeout(tryGetLocation, 2000); | |
| } else { | |
| // Try IP-based location as fallback | |
| tryIPLocation(); | |
| } | |
| }, | |
| options | |
| ); | |
| } else { | |
| tryIPLocation(); | |
| } | |
| } | |
| function tryIPLocation() { | |
| document.getElementById("status").innerHTML = ` | |
| <div style="background: linear-gradient(45deg, #3498db, #2980b9); color: white; padding: 15px; border-radius: 10px; margin: 10px 0;"> | |
| <div style="font-weight: bold;">๐ Trying IP-based location...</div> | |
| <div style="font-size: 0.9em; opacity: 0.9;">Falling back to approximate location</div> | |
| </div> | |
| `; | |
| // Multiple IP geolocation services for reliability | |
| const services = [ | |
| 'https://ipapi.co/json/', | |
| 'https://api.ipify.org?format=json', | |
| 'https://httpbin.org/ip' | |
| ]; | |
| fetch('https://ipapi.co/json/') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.latitude && data.longitude && !data.error) { | |
| document.getElementById("status").innerHTML = ` | |
| <div style="background: linear-gradient(45deg, #3498db, #2980b9); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);"> | |
| <div style="font-weight: bold; margin-bottom: 8px;">๐ IP-based Location Found</div> | |
| <div style="font-size: 0.95em; margin-bottom: 5px;">๐ <strong>${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)}</strong></div> | |
| <div style="font-size: 0.85em; margin-bottom: 5px;"> | |
| ๐ Accuracy: <span style="color: #f39c12; font-weight: bold;">Approximate</span> (City-level) | |
| </div> | |
| <div style="font-size: 0.8em; opacity: 0.9;">๐๏ธ ${data.city || 'Unknown'}, ${data.country_name || data.country || 'Unknown'}</div> | |
| <div style="font-size: 0.8em; opacity: 0.9;">๐ ISP: ${data.org || 'Unknown'}</div> | |
| </div> | |
| `; | |
| window.parent.postMessage({ | |
| type: 'streamlit:setComponentValue', | |
| value: { | |
| lat: data.latitude, | |
| lon: data.longitude, | |
| accuracy: 10000, | |
| method: "IP", | |
| timestamp: new Date().toISOString(), | |
| city: data.city, | |
| country: data.country_name || data.country, | |
| region: data.region, | |
| timezone: data.timezone, | |
| isp: data.org, | |
| success: true | |
| } | |
| }, '*'); | |
| } else { | |
| showLocationUnavailable(); | |
| } | |
| }) | |
| .catch(() => { | |
| showLocationUnavailable(); | |
| }); | |
| } | |
| function showLocationUnavailable() { | |
| document.getElementById("status").innerHTML = ` | |
| <div style="background: linear-gradient(45deg, #e74c3c, #c0392b); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);"> | |
| <div style="font-weight: bold; margin-bottom: 8px;">โ ๏ธ Location Not Available</div> | |
| <div style="font-size: 0.9em; margin-bottom: 5px;">Unable to detect your location automatically</div> | |
| <div style="font-size: 0.85em; opacity: 0.9;">โข You can continue uploading without location data</div> | |
| <div style="font-size: 0.85em; opacity: 0.9;">โข Or enter coordinates manually using the button</div> | |
| </div> | |
| `; | |
| window.parent.postMessage({ | |
| type: 'streamlit:setComponentValue', | |
| value: {lat: null, lon: null, success: false} | |
| }, '*'); | |
| } | |
| // Start location capture immediately | |
| tryGetLocation(); | |
| </script> | |
| <div id="status" style="padding: 15px; background: linear-gradient(45deg, #74b9ff, #0984e3); color: white; border-radius: 10px; text-align: center; font-weight: 500; box-shadow: 0 4px 15px rgba(116, 185, 255, 0.3);"> | |
| ๐ Initializing advanced location services... | |
| </div> | |
| """, | |
| height=140 | |
| ) | |
| with col2: | |
| if st.button("๐ Enter Manually", help="Enter coordinates manually if automatic detection fails"): | |
| st.session_state.manual_location_mode = True | |
| st.rerun() | |
| # Process the location data | |
| if location_data and isinstance(location_data, dict): | |
| if location_data.get('success') and location_data.get('lat') and location_data.get('lon'): | |
| st.session_state.user_location = { | |
| "lat": float(location_data['lat']), | |
| "lon": float(location_data['lon']), | |
| "accuracy": location_data.get('accuracy'), | |
| "method": location_data.get('method', 'Unknown'), | |
| "timestamp": location_data.get('timestamp'), | |
| "city": location_data.get('city'), | |
| "country": location_data.get('country') | |
| } | |
| st.session_state.location_fetched = True | |
| st.rerun() | |
| elif location_data.get('success') == False: | |
| st.session_state.location_fetched = True | |
| elif st.session_state.manual_location_mode: | |
| st.markdown("### ๐ Manual Location Entry") | |
| col1, col2, col3 = st.columns([1, 1, 1]) | |
| with col1: | |
| manual_lat = st.number_input( | |
| "Latitude", | |
| value=0.0, | |
| format="%.6f", | |
| help="Enter latitude (-90 to 90)" | |
| ) | |
| with col2: | |
| manual_lon = st.number_input( | |
| "Longitude", | |
| value=0.0, | |
| format="%.6f", | |
| help="Enter longitude (-180 to 180)" | |
| ) | |
| with col3: | |
| st.write("") # Spacing | |
| if st.button("โ Use These Coordinates"): | |
| if -90 <= manual_lat <= 90 and -180 <= manual_lon <= 180: | |
| st.session_state.user_location = { | |
| "lat": manual_lat, | |
| "lon": manual_lon, | |
| "accuracy": None, | |
| "method": "Manual", | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| st.session_state.location_fetched = True | |
| st.session_state.manual_location_mode = False | |
| st.success("โ Manual coordinates saved!") | |
| st.rerun() | |
| else: | |
| st.error("โ Invalid coordinates. Latitude: -90 to 90, Longitude: -180 to 180") | |
| if st.button("๐ Back to Auto-Detection"): | |
| st.session_state.manual_location_mode = False | |
| st.rerun() | |
| return st.session_state.user_location | |
| def display_analytics_sidebar(): | |
| """Display real-time analytics in the sidebar""" | |
| with st.sidebar: | |
| st.markdown("## ๐ Live Analytics") | |
| # Calculate statistics | |
| stats = calculate_statistics() | |
| # Display key metrics with enhanced styling | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #667eea, #764ba2); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 2em;">๐ฅ {stats['total_users']}</h3> | |
| <p style="margin: 5px 0 0 0;">Total Contributors</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #f093fb, #f5576c); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 2em;">๐ผ๏ธ {stats['total_images']}</h3> | |
| <p style="margin: 5px 0 0 0;">Total Images</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #4facfe, #00f2fe); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 2em;">๐ท๏ธ {stats['total_labels']}</h3> | |
| <p style="margin: 5px 0 0 0;">Total Labels</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #43e97b, #38f9d7); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 2em;">๐ {stats['languages_used']}</h3> | |
| <p style="margin: 5px 0 0 0;">Languages Used</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Location statistics | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #fd79a8, #e84393); color: white; padding: 15px; border-radius: 10px; margin: 10px 0; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 2em;">๐ {stats['images_with_location']}</h3> | |
| <p style="margin: 5px 0 0 0;">Images with GPS Data</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # GPS accuracy breakdown | |
| if stats['gps_accuracy_breakdown']: | |
| st.markdown("### ๐ฏ GPS Accuracy Levels") | |
| for accuracy_level, count in sorted(stats['gps_accuracy_breakdown'].items(), key=lambda x: x[1], reverse=True): | |
| percentage = (count / stats['images_with_location'] * 100) if stats['images_with_location'] > 0 else 0 | |
| accuracy_emoji = {"High": "๐ข", "Medium": "๐ ", "Low": "๐ด", "Unknown": "โช"}.get(accuracy_level, "โช") | |
| st.markdown(f"{accuracy_emoji} **{accuracy_level}**: {count} ({percentage:.1f}%)") | |
| # Location methods breakdown | |
| if stats['location_methods']: | |
| st.markdown("### ๐ Location Capture Methods") | |
| for method, count in sorted(stats['location_methods'].items(), key=lambda x: x[1], reverse=True): | |
| percentage = (count / stats['images_with_location'] * 100) if stats['images_with_location'] > 0 else 0 | |
| method_emoji = {"GPS": "๐ฐ๏ธ", "IP": "๐", "Manual": "๐"}.get(method, "โ") | |
| st.markdown(f"{method_emoji} **{method}**: {count} ({percentage:.1f}%)") | |
| # Language usage breakdown | |
| if stats['language_breakdown']: | |
| st.markdown("### ๐ฃ๏ธ Language Usage") | |
| for lang_code, count in sorted(stats['language_breakdown'].items(), key=lambda x: x[1], reverse=True): | |
| lang_name = SUPPORTED_LANGUAGES.get(lang_code, lang_code) | |
| percentage = (count / stats['total_labels'] * 100) if stats['total_labels'] > 0 else 0 | |
| st.markdown(f"**{lang_name}**: {count} labels ({percentage:.1f}%)") | |
| # Category breakdown | |
| if stats['category_breakdown']: | |
| st.markdown("### ๐ Category Breakdown") | |
| for category, count in sorted(stats['category_breakdown'].items(), key=lambda x: x[1], reverse=True): | |
| percentage = (count / stats['total_images'] * 100) if stats['total_images'] > 0 else 0 | |
| st.markdown(f"**{get_translation(st.session_state.language, category.lower())}**: {count} ({percentage:.1f}%)") | |
| # Country/Region statistics | |
| if stats['country_breakdown']: | |
| st.markdown("### ๐ Top Countries") | |
| for country, count in sorted(stats['country_breakdown'].items(), key=lambda x: x[1], reverse=True)[:5]: | |
| percentage = (count / stats['images_with_location'] * 100) if stats['images_with_location'] > 0 else 0 | |
| st.markdown(f"๐ด **{country}**: {count} ({percentage:.1f}%)") | |
| def register_user(): | |
| """Enhanced user registration form""" | |
| st.subheader(get_translation(st.session_state.language, "register")) | |
| with st.form("register_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| username = st.text_input(get_translation(st.session_state.language, "username")) | |
| password = st.text_input(get_translation(st.session_state.language, "password"), type="password") | |
| confirm_password = st.text_input(get_translation(st.session_state.language, "confirm_password"), type="password") | |
| with col2: | |
| full_name = st.text_input(get_translation(st.session_state.language, "full_name")) | |
| email = st.text_input(get_translation(st.session_state.language, "email")) | |
| phone = st.text_input(get_translation(st.session_state.language, "phone")) | |
| age = st.number_input(get_translation(st.session_state.language, "age"), min_value=13, max_value=120, value=25) | |
| preferred_language = st.selectbox( | |
| get_translation(st.session_state.language, "preferred_language"), | |
| options=list(SUPPORTED_LANGUAGES.keys()), | |
| format_func=lambda x: SUPPORTED_LANGUAGES[x] | |
| ) | |
| submitted = st.form_submit_button(get_translation(st.session_state.language, "register")) | |
| if submitted: | |
| if not username or not password or not full_name or not email: | |
| st.error(get_translation(st.session_state.language, "fill_all_fields")) | |
| return | |
| if password != confirm_password: | |
| st.error(get_translation(st.session_state.language, "passwords_dont_match")) | |
| return | |
| users = load_users() | |
| if username in users: | |
| st.error(get_translation(st.session_state.language, "username_exists")) | |
| return | |
| users[username] = { | |
| 'password': password, | |
| 'preferred_language': preferred_language, | |
| 'created_at': datetime.now().isoformat(), | |
| 'user_details': { | |
| 'full_name': full_name, | |
| 'email': email, | |
| 'phone': phone, | |
| 'age': age | |
| } | |
| } | |
| save_users(users) | |
| st.success(get_translation(st.session_state.language, "registration_successful")) | |
| st.rerun() | |
| def login_user(): | |
| """User login form""" | |
| st.subheader(get_translation(st.session_state.language, "login")) | |
| with st.form("login_form"): | |
| username = st.text_input(get_translation(st.session_state.language, "username")) | |
| password = st.text_input(get_translation(st.session_state.language, "password"), type="password") | |
| submitted = st.form_submit_button(get_translation(st.session_state.language, "login")) | |
| if submitted: | |
| if not username or not password: | |
| st.error(get_translation(st.session_state.language, "fill_all_fields")) | |
| return | |
| users = load_users() | |
| if username not in users: | |
| st.error(get_translation(st.session_state.language, "invalid_credentials")) | |
| return | |
| if users[username]['password'] != password: | |
| st.error(get_translation(st.session_state.language, "invalid_credentials")) | |
| return | |
| st.session_state.user = username | |
| st.session_state.language = users[username]['preferred_language'] | |
| # Reset location fetch status for new login | |
| st.session_state.location_fetched = False | |
| st.session_state.manual_location_mode = False | |
| st.success(get_translation(st.session_state.language, "login_successful")) | |
| st.rerun() | |
| def upload_image(): | |
| """Enhanced image upload form with location display""" | |
| st.subheader(f"๐ค {get_translation(st.session_state.language, 'upload_image')}") | |
| # Initialize upload counter for form reset | |
| if 'upload_counter' not in st.session_state: | |
| st.session_state.upload_counter = 0 | |
| # Show enhanced location status | |
| location = get_enhanced_location() | |
| if location and location.get('lat'): | |
| method_icon = "๐ฐ๏ธ" if location.get('method') == "GPS" else "๐" if location.get('method') == "IP" else "๐" | |
| accuracy_text = "" | |
| if location.get('accuracy'): | |
| if location['accuracy'] <= 10: | |
| accuracy_text = f" (ยฑ{location['accuracy']:.0f}m - High Accuracy)" | |
| elif location['accuracy'] <= 50: | |
| accuracy_text = f" (ยฑ{location['accuracy']:.0f}m - Medium Accuracy)" | |
| else: | |
| accuracy_text = f" (ยฑ{location['accuracy']:.0f}m - Low Accuracy)" | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(45deg, #00b894, #00cec9); color: white; padding: 15px; border-radius: 10px; margin: 15px 0; text-align: center; font-weight: 500; box-shadow: 0 4px 15px rgba(0, 184, 148, 0.3);"> | |
| {method_icon} Location Ready: {location['lat']:.6f}, {location['lon']:.6f}{accuracy_text} | |
| <br><small>Method: {location.get('method', 'Unknown')} | Captured: {datetime.fromisoformat(location.get('timestamp', datetime.now().isoformat())).strftime('%H:%M:%S') if location.get('timestamp') else 'Unknown'}</small> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if st.button("๐ Refresh Location"): | |
| st.session_state.location_fetched = False | |
| st.session_state.manual_location_mode = False | |
| st.rerun() | |
| # Use unique form key to ensure proper reset | |
| with st.form(f"upload_form_{st.session_state.upload_counter}", clear_on_submit=True): | |
| uploaded_file = st.file_uploader( | |
| get_translation(st.session_state.language, "choose_image"), | |
| type=['png', 'jpg', 'jpeg', 'gif'], | |
| help=get_translation(st.session_state.language, "image_help") | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| title = st.text_input(get_translation(st.session_state.language, "image_title")) | |
| description = st.text_area(get_translation(st.session_state.language, "image_description")) | |
| with col2: | |
| label = st.text_input(get_translation(st.session_state.language, "native_language_label")) | |
| category = st.selectbox( | |
| get_translation(st.session_state.language, "category"), | |
| options=get_categories(), | |
| format_func=lambda x: get_translation(st.session_state.language, x.lower()) | |
| ) | |
| submitted = st.form_submit_button(get_translation(st.session_state.language, "upload")) | |
| if submitted: | |
| if not uploaded_file or not title or not description or not label: | |
| st.error(get_translation(st.session_state.language, "fill_all_fields")) | |
| return | |
| # Validate image | |
| validation_result = validate_image(uploaded_file) | |
| if not validation_result['valid']: | |
| st.error(get_translation(st.session_state.language, validation_result['error'])) | |
| return | |
| # Check if location is available (but allow upload without it) | |
| current_location = st.session_state.get('user_location', {}) | |
| location_status = "No location data" | |
| if current_location and current_location.get('lat'): | |
| location_status = f"๐ {current_location.get('method', 'Unknown')} location captured" | |
| else: | |
| st.warning("โ ๏ธ **Location not captured** - Image will be uploaded without GPS coordinates. For better dataset quality, try refreshing location.") | |
| # Generate unique filename | |
| file_extension = uploaded_file.name.split('.')[-1] | |
| unique_filename = f"{uuid.uuid4()}.{file_extension}" | |
| # Save image to existing images directory | |
| image_path = os.path.join(IMAGES_DIR, unique_filename) | |
| if os.path.exists(IMAGES_DIR): | |
| try: | |
| image = Image.open(uploaded_file) | |
| image.save(image_path) | |
| except (OSError, PermissionError): | |
| st.error("Unable to save image. Please check if 'data/images' folder exists and has write permissions.") | |
| return | |
| else: | |
| st.error("Images directory not found. Please ensure 'data/images' folder exists in the workspace.") | |
| return | |
| # Prepare comprehensive location metadata | |
| location_metadata = None | |
| if current_location and current_location.get('lat'): | |
| location_metadata = { | |
| 'latitude': current_location['lat'], | |
| 'longitude': current_location['lon'], | |
| 'accuracy': current_location.get('accuracy'), | |
| 'altitude': current_location.get('altitude'), | |
| 'heading': current_location.get('heading'), | |
| 'speed': current_location.get('speed'), | |
| 'method': current_location.get('method', 'Unknown'), | |
| 'timestamp': current_location.get('timestamp', datetime.now().isoformat()), | |
| 'city': current_location.get('city'), | |
| 'country': current_location.get('country'), | |
| 'region': current_location.get('region'), | |
| 'timezone': current_location.get('timezone'), | |
| 'isp': current_location.get('isp') | |
| } | |
| # Save comprehensive label data | |
| labels = load_labels() | |
| entry_id = str(uuid.uuid4()) | |
| labels[entry_id] = { | |
| 'image_path': image_path, | |
| 'filename': unique_filename, | |
| 'title': title, | |
| 'description': description, | |
| 'category': category, | |
| 'uploaded_by': st.session_state.user, | |
| 'uploaded_at': datetime.now().isoformat(), | |
| 'location': location_metadata, # Comprehensive location data | |
| 'location_status': location_status, | |
| 'labels': [ | |
| { | |
| 'text': label, | |
| 'language': st.session_state.language, | |
| 'added_by': st.session_state.user, | |
| 'added_at': datetime.now().isoformat() | |
| } | |
| ] | |
| } | |
| # Save labels and verify the save operation | |
| try: | |
| save_labels(labels) | |
| st.success("Data saved successfully!") | |
| except Exception as e: | |
| st.error(f"Error saving data: {str(e)}") | |
| return | |
| # Show success message with location info | |
| if location_metadata: | |
| success_msg = f"โ {get_translation(st.session_state.language, 'upload_successful')} with {location_metadata['method']} coordinates!" | |
| else: | |
| success_msg = f"โ {get_translation(st.session_state.language, 'upload_successful')} (without location data)" | |
| st.success(success_msg) | |
| # Debug: Show what was saved | |
| st.info(f"Saved entry ID: {entry_id}") | |
| st.info(f"Image path: {image_path}") | |
| st.info(f"Data directory exists: {os.path.exists(DATA_DIR)}") | |
| st.info(f"Labels file exists: {os.path.exists(LABELS_FILE)}") | |
| # Initialize counter if it doesn't exist | |
| if 'upload_counter' not in st.session_state: | |
| st.session_state.upload_counter = 0 | |
| st.session_state.upload_counter += 1 | |
| # Reset location for next upload to get fresh coordinates | |
| st.session_state.location_fetched = False | |
| st.session_state.manual_location_mode = False | |
| st.rerun() | |
| def display_image_feed(): | |
| """Enhanced image feed with improved styling and functionality""" | |
| st.subheader(f"๐ฑ {get_translation(st.session_state.language, 'image_feed')}") | |
| # Category filter | |
| categories = ['All'] + get_categories() | |
| selected_category = st.selectbox( | |
| get_translation(st.session_state.language, "filter_by_category"), | |
| options=categories, | |
| format_func=lambda x: get_translation(st.session_state.language, x.lower()) if x != 'All' else get_translation(st.session_state.language, 'all') | |
| ) | |
| labels = load_labels() | |
| if not labels: | |
| st.info(get_translation(st.session_state.language, "no_images_yet")) | |
| return | |
| # Filter by category | |
| filtered_labels = labels | |
| if selected_category != 'All': | |
| filtered_labels = {k: v for k, v in labels.items() if v.get('category') == selected_category} | |
| # Display images in a grid | |
| for entry_id, entry_data in sorted(filtered_labels.items(), key=lambda x: x[1]['uploaded_at'], reverse=True): | |
| with st.container(): | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| # Display image | |
| image_path = entry_data['image_path'] | |
| if os.path.exists(image_path): | |
| image = Image.open(image_path) | |
| st.image(image, width=300, caption=entry_data['title']) | |
| else: | |
| st.error(get_translation(st.session_state.language, "image_not_found")) | |
| with col2: | |
| # Display metadata | |
| st.markdown(f"### {entry_data['title']}") | |
| st.markdown(f"**{get_translation(st.session_state.language, 'image_description')}:** {entry_data['description']}") | |
| st.markdown(f"**{get_translation(st.session_state.language, 'category')}:** {get_translation(st.session_state.language, entry_data['category'].lower())}") | |
| st.markdown(f"**๐ค Uploaded by:** {entry_data['uploaded_by']}") | |
| st.markdown(f"**๐ Date:** {datetime.fromisoformat(entry_data['uploaded_at']).strftime('%Y-%m-%d %H:%M')}") | |
| # Enhanced location display - handle both old and new location formats | |
| location = entry_data.get('location') | |
| if location and (location.get('latitude') or location.get('lat')): | |
| # Handle both old format (lat/lon) and new format (latitude/longitude) | |
| lat = location.get('latitude') or location.get('lat', 0) | |
| lon = location.get('longitude') or location.get('lon', 0) | |
| method_icon = "๐ฐ๏ธ" if location.get('method') == "GPS" else "๐" if location.get('method') == "IP" else "๐" | |
| accuracy_info = "" | |
| if location.get('accuracy'): | |
| accuracy_info = f" (ยฑ{location['accuracy']:.0f}m accuracy)" | |
| location_text = f"{method_icon} {lat:.6f}, {lon:.6f}{accuracy_info}" | |
| if location.get('city') and location.get('country'): | |
| location_text += f" - {location['city']}, {location['country']}" | |
| st.markdown(f"**๐ Location:** {location_text}") | |
| # Display labels | |
| st.markdown(f"**{get_translation(st.session_state.language, 'labels')}:**") | |
| for label_data in entry_data['labels']: | |
| lang_name = SUPPORTED_LANGUAGES.get(label_data['language'], label_data['language']) | |
| st.markdown(f"- **{label_data['text']}** ({lang_name}) - by {label_data['added_by']}") | |
| # Add new label form | |
| with st.expander(get_translation(st.session_state.language, "add_label")): | |
| with st.form(f"label_form_{entry_id}"): | |
| new_label = st.text_input(get_translation(st.session_state.language, "new_label")) | |
| label_language = st.selectbox( | |
| get_translation(st.session_state.language, "label_language"), | |
| options=list(SUPPORTED_LANGUAGES.keys()), | |
| format_func=lambda x: SUPPORTED_LANGUAGES[x] | |
| ) | |
| if st.form_submit_button(get_translation(st.session_state.language, "add_label")): | |
| if not new_label: | |
| st.error(get_translation(st.session_state.language, "enter_label")) | |
| else: | |
| # Check if label already exists | |
| existing_labels = [l['text'].lower() for l in entry_data['labels']] | |
| if new_label.lower() in existing_labels: | |
| st.error(get_translation(st.session_state.language, "label_exists")) | |
| else: | |
| # Add new label | |
| labels[entry_id]['labels'].append({ | |
| 'text': new_label, | |
| 'language': label_language, | |
| 'added_by': st.session_state.user, | |
| 'added_at': datetime.now().isoformat() | |
| }) | |
| save_labels(labels) | |
| st.success(get_translation(st.session_state.language, "label_added")) | |
| st.rerun() | |
| st.markdown("---") | |
| def main(): | |
| """Main application function""" | |
| # Header with language selector | |
| col1, col2, col3 = st.columns([2, 1, 1]) | |
| with col1: | |
| st.title("๐ท๏ธ LabelIt! ๐ฎ๐ณ") | |
| st.markdown(get_translation(st.session_state.language, "app_subtitle")) | |
| with col2: | |
| # Language selector | |
| selected_language = st.selectbox( | |
| "๐ Language", | |
| options=list(SUPPORTED_LANGUAGES.keys()), | |
| format_func=lambda x: SUPPORTED_LANGUAGES[x], | |
| index=list(SUPPORTED_LANGUAGES.keys()).index(st.session_state.language) | |
| ) | |
| if selected_language != st.session_state.language: | |
| st.session_state.language = selected_language | |
| st.rerun() | |
| with col3: | |
| if st.session_state.user: | |
| if st.button(get_translation(st.session_state.language, "logout")): | |
| st.session_state.user = None | |
| st.session_state.location_fetched = False | |
| st.session_state.manual_location_mode = False | |
| st.rerun() | |
| # Display analytics sidebar | |
| display_analytics_sidebar() | |
| # Main content | |
| if not st.session_state.user: | |
| # Authentication tabs | |
| tab1, tab2 = st.tabs([ | |
| get_translation(st.session_state.language, "login"), | |
| get_translation(st.session_state.language, "register") | |
| ]) | |
| with tab1: | |
| login_user() | |
| with tab2: | |
| register_user() | |
| else: | |
| # User is logged in - show user details in sidebar | |
| with st.sidebar: | |
| st.markdown("---") | |
| st.markdown(f"### ๐ค {get_translation(st.session_state.language, 'welcome')}, {st.session_state.user}!") | |
| users = load_users() | |
| user_details = users[st.session_state.user]['user_details'] | |
| st.markdown(f"**{get_translation(st.session_state.language, 'full_name')}:** {user_details['full_name']}") | |
| st.markdown(f"**{get_translation(st.session_state.language, 'email')}:** {user_details['email']}") | |
| st.markdown(f"**{get_translation(st.session_state.language, 'phone')}:** {user_details['phone']}") | |
| st.markdown(f"**{get_translation(st.session_state.language, 'age')}:** {user_details['age']}") | |
| # Main application tabs | |
| tab1, tab2 = st.tabs([ | |
| get_translation(st.session_state.language, "upload_image"), | |
| get_translation(st.session_state.language, "feed") | |
| ]) | |
| with tab1: | |
| upload_image() | |
| with tab2: | |
| display_image_feed() | |
| if __name__ == "__main__": | |
| main() | |