|
|
import gradio as gr |
|
|
from typing import Dict, List |
|
|
from style import Style |
|
|
|
|
|
class UIManager: |
|
|
"""Manages all UI components and styling for Pixcribe""" |
|
|
|
|
|
def __init__(self): |
|
|
|
|
|
self.custom_css = Style.get_all_css() |
|
|
|
|
|
def create_header(self): |
|
|
"""Create application header""" |
|
|
return gr.HTML(""" |
|
|
<div class="app-header"> |
|
|
<h1 class="app-title">✨ Pixcribe</h1> |
|
|
<p class="app-subtitle">AI-Powered Social Media Caption Generator</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
def create_info_banner(self): |
|
|
"""Create informational banner about model loading and processing times""" |
|
|
return gr.HTML(""" |
|
|
<div style=" |
|
|
background: linear-gradient(135deg, #E8F4F8 0%, #D4E9F2 100%); |
|
|
border-left: 4px solid #3498DB; |
|
|
border-radius: 16px; |
|
|
padding: 24px 32px; |
|
|
margin: 0 auto 48px auto; |
|
|
max-width: 1200px; |
|
|
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.12); |
|
|
"> |
|
|
<div style="display: flex; align-items: start; gap: 20px;"> |
|
|
<div style="font-size: 32px; line-height: 1; margin-top: 4px;">⏱️</div> |
|
|
<div style="flex: 1;"> |
|
|
<h3 style=" |
|
|
margin: 0 0 12px 0; |
|
|
font-size: 20px; |
|
|
font-weight: 700; |
|
|
color: #2C3E50; |
|
|
letter-spacing: -0.02em; |
|
|
"> |
|
|
Please Note: Processing Time |
|
|
</h3> |
|
|
<p style=" |
|
|
margin: 0 0 12px 0; |
|
|
font-size: 15px; |
|
|
line-height: 1.6; |
|
|
color: #5D6D7E; |
|
|
"> |
|
|
<strong style="color: #2980B9;">Initial setup and model loading may take a while</strong> as multiple AI models |
|
|
are initialized and cached. This includes YOLOv11 object detection, OpenCLIP semantic analysis, |
|
|
Qwen2.5-VL caption generation, and other advanced models. |
|
|
</p> |
|
|
<p style=" |
|
|
margin: 0; |
|
|
font-size: 15px; |
|
|
line-height: 1.6; |
|
|
color: #5D6D7E; |
|
|
"> |
|
|
✨ <strong style="color: #27AE60;">Processing time varies depending on system resources.</strong> |
|
|
Thank you for your patience while we generate high-quality captions! |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
def create_footer(self): |
|
|
"""Create application footer""" |
|
|
return gr.HTML(""" |
|
|
<div class="app-footer"> |
|
|
<p class="footer-text"> |
|
|
Powered by advanced AI models |
|
|
</p> |
|
|
<p class="footer-models"> |
|
|
YOLOv11 · OpenCLIP ViT-H/14 · Qwen2.5-VL-7B · EasyOCR · Places365 · U2-Net |
|
|
</p> |
|
|
<p class="footer-text" style="margin-top: 32px;"> |
|
|
© 2025 Pixcribe · Built for creators |
|
|
</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
def format_captions_with_copy(self, captions: List[Dict]) -> str: |
|
|
"""Format captions as HTML with copy functionality""" |
|
|
if not captions: |
|
|
return "<p style='color: #6C757D; padding: 24px;'>No captions generated</p>" |
|
|
|
|
|
captions_html = "" |
|
|
for i, cap in enumerate(captions): |
|
|
caption_text = cap.get('caption', '') |
|
|
hashtags = cap.get('hashtags', []) |
|
|
tone = cap.get('tone', 'unknown').title() |
|
|
|
|
|
|
|
|
caption_id = f"caption_{i}" |
|
|
|
|
|
|
|
|
full_text = f"{caption_text}\n\n{' '.join([f'#{tag}' for tag in hashtags])}" |
|
|
|
|
|
captions_html += f""" |
|
|
<div class="caption-card" id="{caption_id}"> |
|
|
<button class="copy-button" onclick="copyCaption{i}()" id="copy-btn-{i}"> |
|
|
📋 Copy |
|
|
</button> |
|
|
<div class="caption-header">Caption {i+1} · {tone}</div> |
|
|
<div class="caption-text">{caption_text}</div> |
|
|
<div class="caption-hashtags"> |
|
|
{' '.join([f'#{tag}' for tag in hashtags])} |
|
|
</div> |
|
|
<textarea id="caption-text-{i}" style="position: absolute; left: -9999px;">{full_text}</textarea> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function copyCaption{i}() {{ |
|
|
const text = document.getElementById('caption-text-{i}').value; |
|
|
const btn = document.getElementById('copy-btn-{i}'); |
|
|
|
|
|
// Try modern clipboard API first |
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {{ |
|
|
navigator.clipboard.writeText(text).then(() => {{ |
|
|
btn.innerHTML = '✓ Copied!'; |
|
|
btn.classList.add('copied'); |
|
|
setTimeout(() => {{ |
|
|
btn.innerHTML = '📋 Copy'; |
|
|
btn.classList.remove('copied'); |
|
|
}}, 2000); |
|
|
}}).catch(() => {{ |
|
|
// Fallback to old method |
|
|
fallbackCopy{i}(); |
|
|
}}); |
|
|
}} else {{ |
|
|
// Fallback for older browsers |
|
|
fallbackCopy{i}(); |
|
|
}} |
|
|
}} |
|
|
|
|
|
function fallbackCopy{i}() {{ |
|
|
const textarea = document.getElementById('caption-text-{i}'); |
|
|
const btn = document.getElementById('copy-btn-{i}'); |
|
|
textarea.style.position = 'static'; |
|
|
textarea.style.opacity = '0'; |
|
|
textarea.select(); |
|
|
try {{ |
|
|
document.execCommand('copy'); |
|
|
btn.innerHTML = '✓ Copied!'; |
|
|
btn.classList.add('copied'); |
|
|
setTimeout(() => {{ |
|
|
btn.innerHTML = '📋 Copy'; |
|
|
btn.classList.remove('copied'); |
|
|
}}, 2000); |
|
|
}} catch (err) {{ |
|
|
btn.innerHTML = '✗ Failed'; |
|
|
setTimeout(() => {{ |
|
|
btn.innerHTML = '📋 Copy'; |
|
|
}}, 2000); |
|
|
}} |
|
|
textarea.style.position = 'absolute'; |
|
|
textarea.style.opacity = '1'; |
|
|
}} |
|
|
</script> |
|
|
""" |
|
|
|
|
|
return captions_html |
|
|
|
|
|
def create_batch_progress_html(self, current: int, total: int, percent: float, estimated_remaining: int) -> str: |
|
|
""" |
|
|
Create HTML for batch processing progress display. |
|
|
|
|
|
Args: |
|
|
current: Number of images completed |
|
|
total: Total number of images |
|
|
percent: Completion percentage (0-100) |
|
|
estimated_remaining: Estimated remaining time in seconds |
|
|
|
|
|
Returns: |
|
|
Formatted HTML string with progress bar and information |
|
|
""" |
|
|
|
|
|
minutes = int(estimated_remaining // 60) |
|
|
seconds = int(estimated_remaining % 60) |
|
|
time_str = f"{minutes}m {seconds}s" if minutes > 0 else f"{seconds}s" |
|
|
|
|
|
html = f""" |
|
|
<div class="progress-container"> |
|
|
<div class="progress-bar-wrapper"> |
|
|
<div class="progress-bar-fill" style="width: {percent}%;"></div> |
|
|
</div> |
|
|
<div class="progress-text"> |
|
|
Processing image {current} of {total} |
|
|
</div> |
|
|
<div class="progress-stats"> |
|
|
<span>Progress: {percent:.1f}%</span> |
|
|
{f'<span>Estimated time remaining: {time_str}</span>' if estimated_remaining > 0 else ''} |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
return html |
|
|
|
|
|
def format_batch_results_html(self, batch_results: Dict) -> str: |
|
|
""" |
|
|
Format batch processing results as HTML. |
|
|
|
|
|
Args: |
|
|
batch_results: Dictionary containing batch processing results |
|
|
|
|
|
Returns: |
|
|
Formatted HTML string with all batch results |
|
|
""" |
|
|
results = batch_results.get('results', {}) |
|
|
|
|
|
if not results: |
|
|
return "<p style='color: #6C757D; padding: 24px; text-align: center;'>No results to display</p>" |
|
|
|
|
|
|
|
|
total_processed = batch_results.get('total_processed', 0) |
|
|
total_success = batch_results.get('total_success', 0) |
|
|
total_failed = batch_results.get('total_failed', 0) |
|
|
total_time = batch_results.get('total_time', 0) |
|
|
avg_time = batch_results.get('average_time_per_image', 0) |
|
|
|
|
|
html_parts = [] |
|
|
|
|
|
|
|
|
html_parts.append(f""" |
|
|
<div class="batch-summary-card"> |
|
|
<div class="summary-title">✓ Batch Processing Complete</div> |
|
|
<div class="summary-stats"> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-value">{total_processed}</div> |
|
|
<div class="stat-label">Total Processed</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-value" style="color: #27AE60;">{total_success}</div> |
|
|
<div class="stat-label">Successful</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-value" style="color: #E74C3C;">{total_failed}</div> |
|
|
<div class="stat-label">Failed</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-value">{total_time:.1f}s</div> |
|
|
<div class="stat-label">Total Time</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-value">{avg_time:.1f}s</div> |
|
|
<div class="stat-label">Avg Per Image</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
html_parts.append('<div class="batch-results-container">') |
|
|
|
|
|
|
|
|
for img_idx in sorted(results.keys()): |
|
|
img_result = results[img_idx] |
|
|
status = img_result.get('status', 'unknown') |
|
|
|
|
|
|
|
|
if status == 'success': |
|
|
status_icon = '✓' |
|
|
status_color = '#27AE60' |
|
|
else: |
|
|
status_icon = '✗' |
|
|
status_color = '#E74C3C' |
|
|
|
|
|
|
|
|
card_html = f""" |
|
|
<details class="batch-result-card" open> |
|
|
<summary class="card-header"> |
|
|
<span class="card-status" style="color: {status_color};">{status_icon}</span> |
|
|
<span class="card-title">Image {img_idx}</span> |
|
|
</summary> |
|
|
<div class="card-content"> |
|
|
""" |
|
|
|
|
|
if status == 'success': |
|
|
result_data = img_result.get('result', {}) |
|
|
captions = result_data.get('captions', []) |
|
|
|
|
|
|
|
|
for cap in captions: |
|
|
tone = cap.get('tone', 'Unknown').upper() |
|
|
caption_text = cap.get('caption', '') |
|
|
hashtags = cap.get('hashtags', []) |
|
|
|
|
|
card_html += f""" |
|
|
<div class="caption-section"> |
|
|
<div class="caption-label">{tone} Style</div> |
|
|
<div class="caption-text">{caption_text}</div> |
|
|
<div class="hashtags-list"> |
|
|
{''.join([f'<span class="hashtag-item">#{tag}</span>' for tag in hashtags])} |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
detections = result_data.get('detections', []) |
|
|
if detections: |
|
|
object_names = [det.get('class_name', 'unknown') for det in detections] |
|
|
card_html += f""" |
|
|
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #E9ECEF;"> |
|
|
<strong style="color: #495057;">Detected Objects:</strong> |
|
|
<span style="color: #6C757D;"> {', '.join(object_names)}</span> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
brands = result_data.get('brands', []) |
|
|
if brands: |
|
|
brand_names = [ |
|
|
brand[0] if isinstance(brand, tuple) else brand |
|
|
for brand in brands |
|
|
] |
|
|
card_html += f""" |
|
|
<div style="margin-top: 12px;"> |
|
|
<strong style="color: #495057;">Detected Brands:</strong> |
|
|
<span style="color: #6C757D;"> {', '.join(brand_names)}</span> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
else: |
|
|
|
|
|
error = img_result.get('error', {}) |
|
|
error_type = error.get('type', 'Unknown Error') |
|
|
error_message = error.get('message', 'No error message available') |
|
|
|
|
|
card_html += f""" |
|
|
<div class="error-card-content"> |
|
|
<div class="error-title">{error_type}</div> |
|
|
<div class="error-message">{error_message}</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
card_html += """ |
|
|
</div> |
|
|
</details> |
|
|
""" |
|
|
|
|
|
html_parts.append(card_html) |
|
|
|
|
|
|
|
|
html_parts.append('</div>') |
|
|
|
|
|
return ''.join(html_parts) |
|
|
|
|
|
def create_export_panel_html(self) -> str: |
|
|
""" |
|
|
Create HTML for export panel with download buttons. |
|
|
|
|
|
Returns: |
|
|
Formatted HTML string for export panel |
|
|
""" |
|
|
return """ |
|
|
<div class="export-panel"> |
|
|
<div class="export-panel-title">📥 Export Batch Results</div> |
|
|
<div class="export-buttons-row"> |
|
|
<button class="export-button" id="export-json-btn"> |
|
|
<span class="export-button-icon">📄</span> |
|
|
<span>Download JSON</span> |
|
|
</button> |
|
|
<button class="export-button" id="export-csv-btn"> |
|
|
<span class="export-button-icon">📊</span> |
|
|
<span>Download CSV</span> |
|
|
</button> |
|
|
<button class="export-button" id="export-zip-btn"> |
|
|
<span class="export-button-icon">📦</span> |
|
|
<span>Download ZIP</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
print("✓ UIManager (V5 with Batch Processing Support) defined") |
|
|
|