| import gradio as gr |
| import os |
| import requests |
| from PIL import Image |
| import json |
|
|
| |
| API_BASE_URL = os.getenv("API_BASE_URL") |
| API_TOKEN = os.getenv("API_TOKEN") |
|
|
| def face_compare(frame1, frame2, request: gr.Request = None): |
| """Face comparison with enhanced result display""" |
| try: |
| url = f"{API_BASE_URL}" |
| |
| |
| files = {} |
| if frame1: |
| files['file1'] = open(frame1, 'rb') |
| if frame2: |
| files['file2'] = open(frame2, 'rb') |
| |
| if not files: |
| return "<div class='error-message'>Please upload both images</div>" |
| |
| |
| headers = { |
| "Authorization": f"Bearer {API_TOKEN}" |
| } |
|
|
| |
| response = requests.post(url=url, files=files, headers=headers) |
| result = response.json() |
| |
| |
| for file in files.values(): |
| file.close() |
| |
| |
| return format_face_comparison_result(result, frame1, frame2) |
| |
| except Exception as e: |
| return f"<div class='error-message'>Error processing request</div>" |
|
|
| def format_face_comparison_result(result, img1_path, img2_path): |
| """Format face comparison results with professional styling""" |
| |
| detections = result.get("detections", []) |
| matches = result.get("match", []) |
| |
| |
| html = "<div class='result-content'>" |
| |
| |
| if detections: |
| for i, detection in enumerate(detections): |
| face_image = detection.get("face", "") |
| first_face_index = detection.get("firstFaceIndex") |
| second_face_index = detection.get("secondFaceIndex") |
| |
| |
| if matches: |
| html += """ |
| <div> |
| <div class="matches-table"> |
| <table> |
| <thead> |
| <tr> |
| <th>First Face</th> |
| <th>Second Face</th> |
| <th>Similarity Score</th> |
| <th>Result</th> |
| </tr> |
| </thead> |
| <tbody> |
| """ |
| |
| |
| match_groups = {} |
| for match in matches: |
| first_face_index = match.get("firstFaceIndex", "N/A") |
| if first_face_index not in match_groups: |
| match_groups[first_face_index] = [] |
| match_groups[first_face_index].append(match) |
| |
| row_number = 1 |
| for first_face_index in sorted(match_groups.keys()): |
| for match in match_groups[first_face_index]: |
| first_face_index = match.get("firstFaceIndex", "N/A") |
| second_face_index = match.get("secondFaceIndex", "N/A") |
| similarity = match.get("similarity", 0) |
| |
| |
| first_face_img = "" |
| second_face_img = "" |
| |
| for detection in detections: |
| if detection.get("firstFaceIndex") == first_face_index: |
| first_face_img = detection.get("face", "") |
| if detection.get("secondFaceIndex") == second_face_index: |
| second_face_img = detection.get("face", "") |
| |
| |
| if similarity >= 0.6: |
| result_text = "same person" |
| result_class = "result-same" |
| else: |
| result_text = "different person" |
| result_class = "result-different" |
| |
| first_face_display = f"<img src='data:image/png;base64,{first_face_img}' class='table-face-thumbnail' />" if first_face_img else f"Face {first_face_index}" |
| second_face_display = f"<img src='data:image/png;base64,{second_face_img}' class='table-face-thumbnail' />" if second_face_img else f"Face {second_face_index}" |
| |
| html += f""" |
| <tr> |
| <td class="face-cell"> |
| <div class="face-display"> |
| {first_face_display} |
| <div class="face-label">Face {first_face_index}</div> |
| </div> |
| </td> |
| <td class="face-cell"> |
| <div class="face-display"> |
| {second_face_display} |
| <div class="face-label">Face {second_face_index}</div> |
| </div> |
| </td> |
| <td class="similarity-score">{similarity:.4f}</td> |
| <td><span class="result-text {result_class}">{result_text}</span></td> |
| </tr> |
| """ |
| row_number += 1 |
| |
| html += """ |
| </tbody> |
| </table> |
| </div> |
| </div> |
| """ |
| else: |
| html += "<div class='no-results'>No face matches found.</div>" |
| |
| html += "</div>" |
| return html |
|
|
|
|
| def get_custom_css(): |
| """Return simplified CSS styling that works for both light and dark themes""" |
| return """ |
| |
| /* Center everything */ |
| .container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| width: 100%; |
| } |
| |
| /* Header styling - logo and text in same line */ |
| .company-header { |
| background: var(--background-fill-primary); |
| padding: 10px; |
| text-align: center; |
| width: 100%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 25px; |
| flex-wrap: wrap; |
| } |
| |
| .header-logo { |
| flex-shrink: 0; |
| } |
| |
| .header-logo img { |
| width: 80px; |
| height: auto; |
| } |
| |
| .header-text { |
| text-align: center; |
| } |
| |
| .header-text h1 { |
| font-size: 2.4em !important; |
| font-weight: 700; |
| color: var(--body-text-color); |
| } |
| |
| .header-text p { |
| font-size: 1.3em !important; |
| color: var(--body-text-color); |
| opacity: 0.8; |
| } |
| |
| /* Main content layout */ |
| .main-content-row { |
| display: flex; |
| gap: 25px; |
| width: 100%; |
| } |
| |
| .upload-section { |
| flex: 2; |
| display: flex; |
| flex-direction: column; |
| gap: 20px; |
| } |
| |
| .result-section { |
| flex: 1.2; |
| } |
| |
| .upload-images-row { |
| display: flex; |
| gap: 20px; |
| width: 100%; |
| } |
| |
| .upload-image-col { |
| flex: 1; |
| } |
| |
| /* Button styling */ |
| .button-primary { |
| background: var(--button-primary-background-fill) !important; |
| border: none !important; |
| padding: 6px 12px !important; |
| font-size: 1.2em !important; |
| font-weight: 600 !important; |
| color: var(--button-primary-text-color) !important; |
| border-radius: 8px !important; |
| cursor: pointer !important; |
| transition: background-color 0.2s ease !important; |
| width: 100% !important; |
| } |
| |
| .button-primary:hover { |
| background: var(--button-primary-background-fill-hover) !important; |
| } |
| |
| .result-content { |
| width: 100%; |
| } |
| |
| /* Detection cards */ |
| .detections-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| gap: 15px; |
| justify-content: center; |
| } |
| |
| .detection-card { |
| background: var(--background-fill-secondary); |
| padding: 4px; |
| border-radius: 8px; |
| text-align: center; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| } |
| |
| .face-thumbnail { |
| width: 60px; |
| height: 60px; |
| border-radius: 50%; |
| object-fit: cover; |
| } |
| |
| /* Matching table - NEW STYLING */ |
| .matches-table { |
| display: flex; |
| justify-content: center; |
| width: 100%; |
| overflow-x: auto; |
| } |
| |
| .matches-table table { |
| width: 100%; |
| border-collapse: collapse; |
| font-size: 1em !important; |
| min-width: 450px; |
| } |
| |
| .matches-table th { |
| background: var(--background-fill-secondary); |
| color: var(--body-text-color); |
| padding: 4px 2px !important; |
| text-align: center; |
| font-size: 1em !important; |
| font-weight: 700; |
| border-bottom: 2px solid var(--border-color-primary); |
| } |
| |
| .matches-table td { |
| padding: 4px 2px !important; |
| border-bottom: 1px solid var(--border-color-primary); |
| text-align: center; |
| font-size: 0.95em !important; |
| color: var(--body-text-color); |
| } |
| |
| .face-cell { |
| vertical-align: middle; |
| } |
| |
| .face-display { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 5px; |
| } |
| |
| .table-face-thumbnail { |
| width: 70px; |
| height: 70px; |
| border-radius: 50%; |
| object-fit: cover; |
| border: 2px solid var(--border-color-primary); |
| } |
| |
| .face-label { |
| font-size: 0.9em !important; |
| color: var(--body-text-color); |
| opacity: 1; |
| font-weight: 600; |
| } |
| |
| .similarity-score { |
| font-weight: 700; |
| color: var(--body-text-color); |
| font-size: 1.05em !important; |
| } |
| |
| .result-text { |
| padding: 8px 12px !important; |
| border-radius: 12px; |
| font-size: 1.1em !important; |
| font-weight: 700; |
| text-transform: capitalize; |
| } |
| |
| .result-same { |
| background: #d4edda; |
| color: #155724; |
| } |
| |
| .result-different { |
| background: #f8d7da; |
| color: #721c24; |
| } |
| |
| .no-results { |
| text-align: center; |
| padding: 40px; |
| color: var(--body-text-color); |
| opacity: 0.7; |
| font-style: italic; |
| font-size: 1.1em !important; |
| } |
| |
| /* Error messages */ |
| .error-message { |
| background: var(--background-fill-secondary); |
| color: var(--body-text-color); |
| padding: 20px; |
| border-radius: 8px; |
| text-align: center; |
| width: 100%; |
| opacity: 0.9; |
| font-size: 1.1em !important; |
| } |
| |
| """ |
|
|
| |
| with gr.Blocks( |
| title="MiniAiLive - Face Recognition WebAPI Playground", |
| css=get_custom_css() |
| ) as demo: |
| |
| with gr.Column(elem_classes="container"): |
| |
| gr.HTML(""" |
| <div class="company-header"> |
| <div class="header-logo"> |
| <img src="https://miniai.live/wp-content/uploads/2025/11/logo_new.png" alt="MiniAiLive Logo"> |
| </div> |
| <div class="header-text"> |
| <h1>MiniAiLive Face Recognition WebAPI Playground</h1> |
| <p>Experience our NIST FRVT Top Ranked 1:1 & 1:N Face Matching Technology</p> |
| </div> |
| </div> |
| """) |
| |
| |
| with gr.Row(elem_classes="main-content-row"): |
| |
| with gr.Column(scale=0.6, elem_classes="upload-section"): |
| with gr.Row(elem_classes="upload-images-row"): |
| |
| with gr.Column(scale=1, elem_classes="upload-image-col"): |
| im_match_in1 = gr.Image( |
| type='filepath', |
| height=380, |
| label="First Image", |
| show_download_button=False |
| ) |
| gr.Examples( |
| examples=[ |
| "assets/1.jpg", |
| "assets/2.jpg", |
| "assets/3.jpg", |
| "assets/4.jpg", |
| ], |
| inputs=im_match_in1, |
| label="First Image Examples" |
| ) |
| |
| |
| with gr.Column(scale=1, elem_classes="upload-image-col"): |
| im_match_in2 = gr.Image( |
| type='filepath', |
| height=380, |
| label="Second Image", |
| show_download_button=False |
| ) |
| gr.Examples( |
| examples=[ |
| "assets/1-1.jpg", |
| "assets/2-1.jpg", |
| "assets/3-1.jpg", |
| "assets/4-1.jpg", |
| ], |
| inputs=im_match_in2, |
| label="Second Image Examples" |
| ) |
| |
| btn_f_match = gr.Button( |
| "Compare Faces 🚀", |
| variant='primary', |
| elem_classes="button-primary" |
| ) |
| |
| |
| with gr.Column(scale=0.4, elem_classes="result-section"): |
| txt_compare_out = gr.HTML( |
| value="<div style='text-align: center; padding: 10px; font-size: 1.1em;'>Results will appear here after comparison</div>" |
| ) |
| |
| |
| btn_f_match.click( |
| face_compare, |
| inputs=[im_match_in1, im_match_in2], |
| outputs=txt_compare_out |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch( |
| share=False, |
| show_api=False, |
| server_name="0.0.0.0", |
| server_port=7860 |
| ) |