Pixcribe / ui_manager.py
DawnC's picture
Upload 5 files
f3a4ad9 verified
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):
# Use centralized Style class for all CSS
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()
# Create unique ID for each caption
caption_id = f"caption_{i}"
# Full text to copy (caption + hashtags)
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
"""
# Convert remaining time to minutes and seconds
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>"
# Build summary card
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 = []
# Summary card
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>
""")
# Start results container
html_parts.append('<div class="batch-results-container">')
# Process each image result
for img_idx in sorted(results.keys()):
img_result = results[img_idx]
status = img_result.get('status', 'unknown')
# Determine status icon and color
if status == 'success':
status_icon = '✓'
status_color = '#27AE60'
else:
status_icon = '✗'
status_color = '#E74C3C'
# Build card
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', [])
# Display 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>
"""
# Display detected objects
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>
"""
# Display detected brands
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:
# Display error information
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)
# Close results container
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")