stashface / web /interface.py
cc1234's picture
feat: enhance visual display of detected faces with improved layout and image rendering
b413f4d
raw
history blame
19.9 kB
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)