Spaces:
Running
Running
| import gradio as gr | |
| import base64 | |
| import io | |
| from PIL import Image as PILImage | |
| from models.data_manager import DataManager | |
| from models.image_processor import ( | |
| image_search_performers, | |
| find_faces_in_sprite | |
| ) | |
| class WebInterface: | |
| def __init__(self, data_manager: DataManager, default_threshold: float = 0.5): | |
| """ | |
| Initialize the web interface. | |
| Parameters: | |
| data_manager: DataManager instance | |
| default_threshold: Default confidence threshold | |
| """ | |
| self.data_manager = data_manager | |
| self.default_threshold = default_threshold | |
| def get_country_flag(self, country_code): | |
| """ | |
| Convert country code to flag emoji | |
| Parameters: | |
| country_code: ISO 2-letter country code (e.g., 'US', 'GB', 'FR') | |
| Returns: | |
| str: Flag emoji or empty string if not found | |
| """ | |
| if not country_code or len(country_code) != 2: | |
| return "" | |
| # Common country code to flag emoji mapping | |
| flag_map = { | |
| 'AD': '๐ฆ๐ฉ', 'AE': '๐ฆ๐ช', 'AF': '๐ฆ๐ซ', 'AG': '๐ฆ๐ฌ', 'AI': '๐ฆ๐ฎ', 'AL': '๐ฆ๐ฑ', 'AM': '๐ฆ๐ฒ', 'AO': '๐ฆ๐ด', | |
| 'AQ': '๐ฆ๐ถ', 'AR': '๐ฆ๐ท', 'AS': '๐ฆ๐ธ', 'AT': '๐ฆ๐น', 'AU': '๐ฆ๐บ', 'AW': '๐ฆ๐ผ', 'AX': '๐ฆ๐ฝ', 'AZ': '๐ฆ๐ฟ', | |
| 'BA': '๐ง๐ฆ', 'BB': '๐ง๐ง', 'BD': '๐ง๐ฉ', 'BE': '๐ง๐ช', 'BF': '๐ง๐ซ', 'BG': '๐ง๐ฌ', 'BH': '๐ง๐ญ', 'BI': '๐ง๐ฎ', | |
| 'BJ': '๐ง๐ฏ', 'BL': '๐ง๐ฑ', 'BM': '๐ง๐ฒ', 'BN': '๐ง๐ณ', 'BO': '๐ง๐ด', 'BQ': '๐ง๐ถ', 'BR': '๐ง๐ท', 'BS': '๐ง๐ธ', | |
| 'BT': '๐ง๐น', 'BV': '๐ง๐ป', 'BW': '๐ง๐ผ', 'BY': '๐ง๐พ', 'BZ': '๐ง๐ฟ', 'CA': '๐จ๐ฆ', 'CC': '๐จ๐จ', 'CD': '๐จ๐ฉ', | |
| 'CF': '๐จ๐ซ', 'CG': '๐จ๐ฌ', 'CH': '๐จ๐ญ', 'CI': '๐จ๐ฎ', 'CK': '๐จ๐ฐ', 'CL': '๐จ๐ฑ', 'CM': '๐จ๐ฒ', 'CN': '๐จ๐ณ', | |
| 'CO': '๐จ๐ด', 'CR': '๐จ๐ท', 'CU': '๐จ๐บ', 'CV': '๐จ๐ป', 'CW': '๐จ๐ผ', 'CX': '๐จ๐ฝ', 'CY': '๐จ๐พ', 'CZ': '๐จ๐ฟ', | |
| 'DE': '๐ฉ๐ช', 'DJ': '๐ฉ๐ฏ', 'DK': '๐ฉ๐ฐ', 'DM': '๐ฉ๐ฒ', 'DO': '๐ฉ๐ด', 'DZ': '๐ฉ๐ฟ', 'EC': '๐ช๐จ', 'EE': '๐ช๐ช', | |
| 'EG': '๐ช๐ฌ', 'EH': '๐ช๐ญ', 'ER': '๐ช๐ท', 'ES': '๐ช๐ธ', 'ET': '๐ช๐น', 'FI': '๐ซ๐ฎ', 'FJ': '๐ซ๐ฏ', 'FK': '๐ซ๐ฐ', | |
| 'FM': '๐ซ๐ฒ', 'FO': '๐ซ๐ด', 'FR': '๐ซ๐ท', 'GA': '๐ฌ๐ฆ', 'GB': '๐ฌ๐ง', 'GD': '๐ฌ๐ฉ', 'GE': '๐ฌ๐ช', 'GF': '๐ฌ๐ซ', | |
| 'GG': '๐ฌ๐ฌ', 'GH': '๐ฌ๐ญ', 'GI': '๐ฌ๐ฎ', 'GL': '๐ฌ๐ฑ', 'GM': '๐ฌ๐ฒ', 'GN': '๐ฌ๐ณ', 'GP': '๐ฌ๐ต', 'GQ': '๐ฌ๐ถ', | |
| 'GR': '๐ฌ๐ท', 'GS': '๐ฌ๐ธ', 'GT': '๐ฌ๐น', 'GU': '๐ฌ๐บ', 'GW': '๐ฌ๐ผ', 'GY': '๐ฌ๐พ', 'HK': '๐ญ๐ฐ', 'HM': '๐ญ๐ฒ', | |
| 'HN': '๐ญ๐ณ', 'HR': '๐ญ๐ท', 'HT': '๐ญ๐น', 'HU': '๐ญ๐บ', 'ID': '๐ฎ๐ฉ', 'IE': '๐ฎ๐ช', 'IL': '๐ฎ๐ฑ', 'IM': '๐ฎ๐ฒ', | |
| 'IN': '๐ฎ๐ณ', 'IO': '๐ฎ๐ด', 'IQ': '๐ฎ๐ถ', 'IR': '๐ฎ๐ท', 'IS': '๐ฎ๐ธ', 'IT': '๐ฎ๐น', 'JE': '๐ฏ๐ช', 'JM': '๐ฏ๐ฒ', | |
| 'JO': '๐ฏ๐ด', 'JP': '๐ฏ๐ต', 'KE': '๐ฐ๐ช', 'KG': '๐ฐ๐ฌ', 'KH': '๐ฐ๐ญ', 'KI': '๐ฐ๐ฎ', 'KM': '๐ฐ๐ฒ', 'KN': '๐ฐ๐ณ', | |
| 'KP': '๐ฐ๐ต', 'KR': '๐ฐ๐ท', 'KW': '๐ฐ๐ผ', 'KY': '๐ฐ๐พ', 'KZ': '๐ฐ๐ฟ', 'LA': '๐ฑ๐ฆ', 'LB': '๐ฑ๐ง', 'LC': '๐ฑ๐จ', | |
| 'LI': '๐ฑ๐ฎ', 'LK': '๐ฑ๐ฐ', 'LR': '๐ฑ๐ท', 'LS': '๐ฑ๐ธ', 'LT': '๐ฑ๐น', 'LU': '๐ฑ๐บ', 'LV': '๐ฑ๐ป', 'LY': '๐ฑ๐พ', | |
| 'MA': '๐ฒ๐ฆ', 'MC': '๐ฒ๐จ', 'MD': '๐ฒ๐ฉ', 'ME': '๐ฒ๐ช', 'MF': '๐ฒ๐ซ', 'MG': '๐ฒ๐ฌ', 'MH': '๐ฒ๐ญ', 'MK': '๐ฒ๐ฐ', | |
| 'ML': '๐ฒ๐ฑ', 'MM': '๐ฒ๐ฒ', 'MN': '๐ฒ๐ณ', 'MO': '๐ฒ๐ด', 'MP': '๐ฒ๐ต', 'MQ': '๐ฒ๐ถ', 'MR': '๐ฒ๐ท', 'MS': '๐ฒ๐ธ', | |
| 'MT': '๐ฒ๐น', 'MU': '๐ฒ๐บ', 'MV': '๐ฒ๐ป', 'MW': '๐ฒ๐ผ', 'MX': '๐ฒ๐ฝ', 'MY': '๐ฒ๐พ', 'MZ': '๐ฒ๐ฟ', 'NA': '๐ณ๐ฆ', | |
| 'NC': '๐ณ๐จ', 'NE': '๐ณ๐ช', 'NF': '๐ณ๐ซ', 'NG': '๐ณ๐ฌ', 'NI': '๐ณ๐ฎ', 'NL': '๐ณ๐ฑ', 'NO': '๐ณ๐ด', 'NP': '๐ณ๐ต', | |
| 'NR': '๐ณ๐ท', 'NU': '๐ณ๐บ', 'NZ': '๐ณ๐ฟ', 'OM': '๐ด๐ฒ', 'PA': '๐ต๐ฆ', 'PE': '๐ต๐ช', 'PF': '๐ต๐ซ', 'PG': '๐ต๐ฌ', | |
| 'PH': '๐ต๐ญ', 'PK': '๐ต๐ฐ', 'PL': '๐ต๐ฑ', 'PM': '๐ต๐ฒ', 'PN': '๐ต๐ณ', 'PR': '๐ต๐ท', 'PS': '๐ต๐ธ', 'PT': '๐ต๐น', | |
| 'PW': '๐ต๐ผ', 'PY': '๐ต๐พ', 'QA': '๐ถ๐ฆ', 'RE': '๐ท๐ช', 'RO': '๐ท๐ด', 'RS': '๐ท๐ธ', 'RU': '๐ท๐บ', 'RW': '๐ท๐ผ', | |
| 'SA': '๐ธ๐ฆ', 'SB': '๐ธ๐ง', 'SC': '๐ธ๐จ', 'SD': '๐ธ๐ฉ', 'SE': '๐ธ๐ช', 'SG': '๐ธ๐ฌ', 'SH': '๐ธ๐ญ', 'SI': '๐ธ๐ฎ', | |
| 'SJ': '๐ธ๐ฏ', 'SK': '๐ธ๐ฐ', 'SL': '๐ธ๐ฑ', 'SM': '๐ธ๐ฒ', 'SN': '๐ธ๐ณ', 'SO': '๐ธ๐ด', 'SR': '๐ธ๐ท', 'SS': '๐ธ๐ธ', | |
| 'ST': '๐ธ๐น', 'SV': '๐ธ๐ป', 'SX': '๐ธ๐ฝ', 'SY': '๐ธ๐พ', 'SZ': '๐ธ๐ฟ', 'TC': '๐น๐จ', 'TD': '๐น๐ฉ', 'TF': '๐น๐ซ', | |
| 'TG': '๐น๐ฌ', 'TH': '๐น๐ญ', 'TJ': '๐น๐ฏ', 'TK': '๐น๐ฐ', 'TL': '๐น๐ฑ', 'TM': '๐น๐ฒ', 'TN': '๐น๐ณ', 'TO': '๐น๐ด', | |
| 'TR': '๐น๐ท', 'TT': '๐น๐น', 'TV': '๐น๐ป', 'TW': '๐น๐ผ', 'TZ': '๐น๐ฟ', 'UA': '๐บ๐ฆ', 'UG': '๐บ๐ฌ', 'UM': '๐บ๐ฒ', | |
| 'US': '๐บ๐ธ', 'UY': '๐บ๐พ', 'UZ': '๐บ๐ฟ', 'VA': '๐ป๐ฆ', 'VC': '๐ป๐จ', 'VE': '๐ป๐ช', 'VG': '๐ป๐ฌ', 'VI': '๐ป๐ฎ', | |
| 'VN': '๐ป๐ณ', 'VU': '๐ป๐บ', 'WF': '๐ผ๐ซ', 'WS': '๐ผ๐ธ', 'YE': '๐พ๐ช', 'YT': '๐พ๐น', 'ZA': '๐ฟ๐ฆ', 'ZM': '๐ฟ๐ฒ', | |
| 'ZW': '๐ฟ๐ผ' | |
| } | |
| return flag_map.get(country_code.upper(), "") | |
| def multiple_image_search(self, img, threshold, results): | |
| """Wrapper for the multiple image search function""" | |
| try: | |
| return image_search_performers(img, self.data_manager, threshold, results) | |
| except ValueError as e: | |
| if "No faces found" in str(e): | |
| return {"error": "No faces detected in the uploaded image. Please try uploading an image with visible faces."} | |
| else: | |
| raise e | |
| def format_results_for_visual_display(self, json_results): | |
| """ | |
| Convert JSON results to visual components for better UX | |
| Parameters: | |
| json_results: List of face detection results from image_search_performers | |
| Returns: | |
| tuple: (gallery_images, html_content) | |
| """ | |
| if not json_results: | |
| return [], "<p>No faces detected or no matches found.</p>" | |
| # Handle error case | |
| if isinstance(json_results, dict) and "error" in json_results: | |
| error_html = f""" | |
| <div class="performer-card"> | |
| <div class="face-info"> | |
| <h3 style="color: #ff6b6b;">Error</h3> | |
| <p>{json_results['error']}</p> | |
| </div> | |
| </div> | |
| """ | |
| return [], error_html | |
| gallery_images = [] | |
| html_parts = [] | |
| html_parts.append(""" | |
| <style> | |
| body, .gradio-container { | |
| background-color: #1e1e1e !important; | |
| color: #d4d4d4 !important; | |
| } | |
| .performer-card { | |
| border: 1px solid #404040; | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin: 16px 0; | |
| background: #2d2d2d; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| color: #d4d4d4; | |
| } | |
| .face-info { | |
| background: #3c3c3c; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 24px; | |
| border: 1px solid #4a4a4a; | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 20px; | |
| } | |
| .face-info-content { | |
| flex: 1; | |
| } | |
| .face-info h3 { | |
| color: #ffffff; | |
| margin-top: 0; | |
| font-size: 1.4em; | |
| } | |
| .performer-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 24px; | |
| margin-top: 16px; | |
| } | |
| .performer-item { | |
| border: 1px solid #4a4a4a; | |
| border-radius: 12px; | |
| padding: 24px; | |
| background: #333333; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .performer-item:hover { | |
| border-color: #569cd6; | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.4); | |
| transform: translateY(-2px); | |
| } | |
| .performer-image { | |
| width: 120px; | |
| height: 120px; | |
| border-radius: 12px; | |
| object-fit: cover; | |
| margin: 0 auto 16px auto; | |
| display: block; | |
| border: 2px solid #4a4a4a; | |
| transition: all 0.3s ease; | |
| text-align: center; | |
| } | |
| .performer-image:hover { | |
| border-color: #569cd6; | |
| transform: scale(1.05); | |
| } | |
| .performer-item h4 { | |
| color: #ffffff; | |
| margin: 16px 0 8px 0; | |
| font-size: 1.2em; | |
| } | |
| .performer-item h4 a { | |
| color: #569cd6; | |
| text-decoration: none; | |
| transition: color 0.3s ease; | |
| } | |
| .performer-item h4 a:hover { | |
| color: #9cdcfe; | |
| text-decoration: underline; | |
| } | |
| .performer-item p { | |
| color: #cccccc; | |
| margin: 8px 0; | |
| } | |
| .performer-item small { | |
| color: #999999; | |
| } | |
| .confidence-bar { | |
| background: #404040; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| height: 28px; | |
| margin: 12px 0; | |
| border: 1px solid #4a4a4a; | |
| width: 100%; | |
| max-width: 200px; | |
| } | |
| .confidence-fill { | |
| height: 100%; | |
| transition: width 0.5s ease; | |
| text-align: center; | |
| line-height: 28px; | |
| color: white; | |
| font-size: 13px; | |
| font-weight: bold; | |
| text-shadow: 0 1px 2px rgba(0,0,0,0.5); | |
| } | |
| .high-confidence { | |
| background: linear-gradient(135deg, #4caf50, #66bb6a); | |
| } | |
| .medium-confidence { | |
| background: linear-gradient(135deg, #ff9800, #ffb74d); | |
| } | |
| .low-confidence { | |
| background: linear-gradient(135deg, #f44336, #ef5350); | |
| } | |
| .face-info p strong { | |
| color: #9cdcfe; | |
| } | |
| .country-flag { | |
| font-size: 1.2em; | |
| margin-right: 6px; | |
| vertical-align: middle; | |
| } | |
| </style> | |
| """) | |
| for i, face_result in enumerate(json_results): | |
| # Convert base64 face image to PIL for gallery | |
| try: | |
| face_image_data = base64.b64decode(face_result['image']) | |
| face_pil = PILImage.open(io.BytesIO(face_image_data)) | |
| gallery_images.append(face_pil) | |
| except Exception as e: | |
| print(f"Error decoding face image: {e}") | |
| continue | |
| # Create HTML for this face | |
| face_confidence = face_result['confidence'] | |
| performers = face_result['performers'] | |
| # Create base64 data URL for the detected face image | |
| face_image_b64 = f"data:image/jpeg;base64,{face_result['image']}" | |
| html_parts.append(f""" | |
| <div class="performer-card"> | |
| <div class="face-info"> | |
| <div class="detected-face"> | |
| <img src="{face_image_b64}" alt="Detected Face {i+1}" style="width: 120px; height: 120px; border-radius: 12px; object-fit: cover; border: 2px solid #569cd6; box-shadow: 0 4px 12px rgba(0,0,0,0.3);"> | |
| </div> | |
| <div class="face-info-content"> | |
| <h3>Face {i+1}</h3> | |
| <p><strong>Detection Confidence:</strong> {face_confidence:.1%}</p> | |
| <p><strong>Matches Found:</strong> {len(performers)}</p> | |
| </div> | |
| </div> | |
| """) | |
| if performers: | |
| html_parts.append('<div class="performer-grid">') | |
| for performer in performers: | |
| confidence_class = "high-confidence" if performer['confidence'] >= 80 else "medium-confidence" if performer['confidence'] >= 60 else "low-confidence" | |
| country_code = performer.get('country', '') | |
| country_flag = self.get_country_flag(country_code) | |
| country_display = f"{country_flag} {country_code}" if country_flag else (country_code if country_code else 'Unknown') | |
| html_parts.append(f""" | |
| <div class="performer-item"> | |
| <img src="{performer['image']}" alt="{performer['name']}" class="performer-image" onerror="this.style.display='none'"> | |
| <h4><a href="{performer['performer_url']}" target="_blank">{performer['name']}</a></h4> | |
| <p><strong>Country:</strong> {country_display}</p> | |
| <div class="confidence-bar"> | |
| <div class="confidence-fill {confidence_class}" style="width: {performer['confidence']}%"> | |
| {performer['confidence']}% | |
| </div> | |
| </div> | |
| <p><small>Distance: {performer.get('distance', 'N/A')}</small></p> | |
| </div> | |
| """) | |
| html_parts.append('</div>') | |
| else: | |
| html_parts.append('<p><em>No performer matches found for this face.</em></p>') | |
| html_parts.append('</div>') | |
| return gallery_images, ''.join(html_parts) | |
| def multiple_image_search_with_visual(self, img, threshold, results): | |
| """ | |
| Enhanced search function that returns both JSON and visual components | |
| Returns: | |
| tuple: (json_results, gallery_images, html_content) | |
| """ | |
| try: | |
| json_results = self.multiple_image_search(img, threshold, results) | |
| gallery_images, html_content = self.format_results_for_visual_display(json_results) | |
| return json_results, gallery_images, html_content | |
| except Exception as e: | |
| error_msg = f"<div class='performer-card'><h3>Error</h3><p>{str(e)}</p></div>" | |
| return [], [], error_msg | |
| def _create_json_search_interface(self): | |
| """Create the JSON API search interface""" | |
| with gr.Blocks() as interface: | |
| gr.Markdown("# Face Recognition API") | |
| gr.Markdown("Upload an image and get JSON results - perfect for API integration.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| img_input = gr.Image(type="pil") | |
| threshold = gr.Slider( | |
| label="threshold", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=self.default_threshold | |
| ) | |
| results_count = gr.Slider( | |
| label="results", | |
| minimum=0, | |
| maximum=50, | |
| value=3, | |
| step=1 | |
| ) | |
| search_btn = gr.Button("Search") | |
| with gr.Column(): | |
| json_output = gr.JSON(label="JSON Results") | |
| search_btn.click( | |
| fn=self.multiple_image_search, | |
| inputs=[img_input, threshold, results_count], | |
| outputs=json_output, | |
| api_name="multiple_image_search" | |
| ) | |
| return interface | |
| def _create_visual_search_interface(self): | |
| """Create the visual search interface""" | |
| with gr.Blocks() as interface: | |
| gr.Markdown("# Who is in the photo?") | |
| gr.Markdown("Upload an image of a person(s) and we'll show you who it is with photos and details.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| img_input = gr.Image(type="pil") | |
| threshold = gr.Slider( | |
| label="threshold", | |
| minimum=0.0, | |
| maximum=1.0, | |
| value=self.default_threshold | |
| ) | |
| results_count = gr.Slider( | |
| label="results", | |
| minimum=0, | |
| maximum=50, | |
| value=3, | |
| step=1 | |
| ) | |
| search_btn = gr.Button("Search") | |
| with gr.Column(): | |
| performer_info = gr.HTML( | |
| label="Performer Information", | |
| value="<p>Upload an image and click search to see results.</p>" | |
| ) | |
| def visual_search_wrapper(img, threshold, results): | |
| """Wrapper that returns only visual components""" | |
| json_results, gallery_images, html_content = self.multiple_image_search_with_visual(img, threshold, results) | |
| return html_content | |
| search_btn.click( | |
| fn=visual_search_wrapper, | |
| inputs=[img_input, threshold, results_count], | |
| outputs=[performer_info], | |
| api_name="multiple_image_search_with_visual" | |
| ) | |
| return interface | |
| def _create_faces_in_sprite_interface(self): | |
| """Create the faces in sprite interface""" | |
| with gr.Blocks() as interface: | |
| gr.Markdown("# Find Faces in Sprite") | |
| with gr.Row(): | |
| with gr.Column(): | |
| img_input = gr.Image() | |
| vtt_input = gr.File(label="VTT file") | |
| search_btn = gr.Button("Process") | |
| with gr.Column(): | |
| output = gr.JSON(label="Results") | |
| search_btn.click( | |
| fn=find_faces_in_sprite, | |
| inputs=[img_input, vtt_input], | |
| outputs=output | |
| ) | |
| return interface | |
| def launch(self, server_name="0.0.0.0", server_port=7860, share=True): | |
| """Launch the web interface""" | |
| with gr.Blocks( | |
| css=""" | |
| .gradio-container { | |
| background-color: #1e1e1e !important; | |
| color: #d4d4d4 !important; | |
| } | |
| .dark { | |
| --background-fill-primary: #2d2d2d; | |
| --background-fill-secondary: #3c3c3c; | |
| --border-color-primary: #404040; | |
| --block-title-text-color: #ffffff; | |
| --body-text-color: #d4d4d4; | |
| } | |
| """ | |
| ) as demo: | |
| with gr.Tabs(): | |
| with gr.TabItem("Visual Search"): | |
| self._create_visual_search_interface() | |
| with gr.TabItem("JSON API"): | |
| self._create_json_search_interface() | |
| with gr.TabItem("Faces in Sprite"): | |
| self._create_faces_in_sprite_interface() | |
| demo.queue().launch(server_name=server_name, server_port=server_port, share=share, ssr_mode=False) | |