DawnC commited on
Commit
f3a4ad9
Β·
verified Β·
1 Parent(s): 2afab78

Upload 5 files

Browse files

Crate new Features: Support batch images upload

Files changed (5) hide show
  1. app.py +275 -49
  2. batch_processing_manager.py +449 -0
  3. pixcribe_pipeline.py +53 -3
  4. style.py +765 -0
  5. ui_manager.py +215 -518
app.py CHANGED
@@ -2,73 +2,251 @@ import gradio as gr
2
  import torch
3
  from PIL import Image
4
  import spaces
 
 
 
 
5
 
6
  from pixcribe_pipeline import PixcribePipeline
7
  from ui_manager import UIManager
8
 
9
  # Initialize Pipeline and UI Manager
10
- print("Initializing Pixcribe...")
11
- print("⏳ Loading models (this may take 60-90 seconds on first run)...")
12
  pipeline = PixcribePipeline(yolo_variant='l')
13
  ui_manager = UIManager()
14
  print("βœ… All models loaded successfully!")
15
 
 
 
 
 
16
  @spaces.GPU(duration=180)
17
- def process_wrapper(image, yolo_variant, caption_language):
18
- """Process image and return formatted results
 
19
 
20
- This function uses GPU-accelerated models:
21
- - YOLOv11 (object detection)
22
- - OpenCLIP ViT-H/14 (semantic understanding)
23
- - EasyOCR (text extraction)
24
- - Places365 (scene analysis)
25
- - Qwen2.5-VL-7B (caption generation)
26
 
27
- Total processing time: ~2-3 seconds on L4 GPU
 
 
 
 
 
 
 
28
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- if image is None:
31
- return None, "<div style='color: #E74C3C; padding: 24px; text-align: center;'>Please upload an image</div>"
 
 
 
 
 
32
 
33
  try:
34
- platform = 'instagram'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- results = pipeline.process_image(image, platform, yolo_variant, caption_language)
 
37
 
38
- if results is None:
39
- return None, "<div style='color: #E74C3C; padding: 24px; text-align: center;'>Processing failed. Check terminal logs for details.</div>"
 
 
 
 
 
40
 
 
41
  except Exception as e:
42
- import traceback
43
- error_msg = traceback.format_exc()
44
- print("="*60)
45
- print("ERROR DETAILS:")
46
- print(error_msg)
47
- print("="*60)
48
-
49
- # Return detailed error to user
50
- error_html = f"""
51
- <div style='background: #FADBD8; border: 2px solid #E74C3C; border-radius: 20px; padding: 28px; margin: 16px 0;'>
52
- <h3 style='color: #C0392B; margin-top: 0; font-size: 22px;'>❌ Processing Error</h3>
53
- <p style='color: #E74C3C; font-weight: bold; font-size: 17px; margin-bottom: 16px;'>{str(e)}</p>
54
- <details style='margin-top: 12px;'>
55
- <summary style='cursor: pointer; color: #C0392B; font-weight: bold; font-size: 16px;'>View Full Error Trace</summary>
56
- <pre style='background: white; padding: 16px; border-radius: 12px; overflow-x: auto; font-size: 13px; color: #2C3E50; margin-top: 12px;'>{error_msg}</pre>
57
- </details>
58
- </div>
59
- """
60
- return None, error_html
61
 
62
- # Get visualized image with brand boxes
63
- visualized_image = results.get('visualized_image', image)
 
 
 
 
64
 
65
- # Format captions with copy functionality
66
- captions_html = ui_manager.format_captions_with_copy(results['captions'])
 
 
67
 
68
- return visualized_image, captions_html
69
 
70
  # Create Gradio Interface
71
- with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe - AI Social Media Captions") as app:
72
 
73
  # Header
74
  ui_manager.create_header()
@@ -81,13 +259,14 @@ with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe - AI Social Media Capt
81
  # Left - Upload Card
82
  with gr.Column(scale=1):
83
  with gr.Group(elem_classes="upload-card"):
84
- image_input = gr.Image(
85
- type="pil",
86
- label="Upload Image",
 
87
  elem_classes="upload-area"
88
  )
89
 
90
- # Right - Detected Objects
91
  with gr.Column(scale=1):
92
  with gr.Group(elem_classes="results-card"):
93
  gr.Markdown("### Detected Objects", elem_classes="section-title")
@@ -137,7 +316,7 @@ with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe - AI Social Media Capt
137
  </div>
138
  """)
139
 
140
- # Caption Results (Full Width)
141
  with gr.Group(elem_classes="caption-results-container"):
142
  gr.Markdown("### πŸ“ Generated Captions", elem_classes="section-title")
143
  caption_output = gr.HTML(
@@ -145,6 +324,25 @@ with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe - AI Social Media Capt
145
  elem_id="caption-results"
146
  )
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # Footer
149
  ui_manager.create_footer()
150
 
@@ -176,9 +374,37 @@ with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe - AI Social Media Capt
176
 
177
  # Connect button to processing function
178
  analyze_btn.click(
179
- fn=process_wrapper,
180
  inputs=[image_input, yolo_variant, caption_language],
181
- outputs=[visualized_image, caption_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  )
183
 
184
  if __name__ == "__main__":
 
2
  import torch
3
  from PIL import Image
4
  import spaces
5
+ import os
6
+ import json
7
+ import tempfile
8
+ from typing import List, Optional
9
 
10
  from pixcribe_pipeline import PixcribePipeline
11
  from ui_manager import UIManager
12
 
13
  # Initialize Pipeline and UI Manager
14
+ print("Initializing Pixcribe V5 with Batch Processing...")
15
+ print("⏳ Loading models (this may take a while)...")
16
  pipeline = PixcribePipeline(yolo_variant='l')
17
  ui_manager = UIManager()
18
  print("βœ… All models loaded successfully!")
19
 
20
+ # Global variable to store latest batch results and images for export
21
+ latest_batch_results = None
22
+ latest_batch_images = None
23
+
24
  @spaces.GPU(duration=180)
25
+ def process_images_wrapper(files, yolo_variant, caption_language, progress=gr.Progress()):
26
+ """
27
+ Process single or multiple images with progress tracking.
28
 
29
+ This function automatically detects whether to use single-image or batch processing
30
+ based on the number of files uploaded.
 
 
 
 
31
 
32
+ Args:
33
+ files: List of uploaded file objects (or single file)
34
+ yolo_variant: YOLO model variant ('m', 'l', 'x')
35
+ caption_language: Caption language ('zh', 'en')
36
+ progress: Gradio Progress object for progress updates
37
+
38
+ Returns:
39
+ Tuple of (visualized_image, caption_html, batch_results_html, export_panel_visibility)
40
  """
41
+ global latest_batch_results, latest_batch_images
42
+
43
+ # Validate input
44
+ if files is None or (isinstance(files, list) and len(files) == 0):
45
+ error_msg = "<div style='color: #E74C3C; padding: 24px; text-align: center;'>Please upload at least one image</div>"
46
+ return None, error_msg, "", gr.update(visible=False)
47
+
48
+ # Convert single file to list
49
+ if not isinstance(files, list):
50
+ files = [files]
51
+
52
+ # Check maximum limit
53
+ if len(files) > 10:
54
+ error_msg = "<div style='color: #E74C3C; padding: 24px; text-align: center;'>Maximum 10 images allowed. Please select fewer images.</div>"
55
+ return None, error_msg, "", gr.update(visible=False)
56
+
57
+ # Load images from files
58
+ images = []
59
+ for file in files:
60
+ try:
61
+ if hasattr(file, 'name'):
62
+ # File object from Gradio
63
+ img = Image.open(file.name)
64
+ else:
65
+ # Direct path
66
+ img = Image.open(file)
67
+
68
+ # Convert to RGB if needed
69
+ if img.mode != 'RGB':
70
+ img = img.convert('RGB')
71
+
72
+ images.append(img)
73
+ except Exception as e:
74
+ print(f"⚠️ Warning: Failed to load image {file}: {str(e)}")
75
+ continue
76
+
77
+ if len(images) == 0:
78
+ error_msg = "<div style='color: #E74C3C; padding: 24px; text-align: center;'>No valid images found. Please upload valid image files.</div>"
79
+ return None, error_msg, "", gr.update(visible=False)
80
+
81
+ platform = 'instagram' # Fixed platform
82
+
83
+ # Single image processing mode
84
+ if len(images) == 1:
85
+ try:
86
+ results = pipeline.process_image(
87
+ image=images[0],
88
+ platform=platform,
89
+ yolo_variant=yolo_variant,
90
+ language=caption_language
91
+ )
92
+
93
+ if results is None:
94
+ error_msg = "<div style='color: #E74C3C; padding: 24px; text-align: center;'>Processing failed. Check terminal logs for details.</div>"
95
+ return None, error_msg, "", gr.update(visible=False)
96
+
97
+ # Get visualized image with brand boxes
98
+ visualized_image = results.get('visualized_image', images[0])
99
+
100
+ # Format captions with copy functionality
101
+ captions_html = ui_manager.format_captions_with_copy(results['captions'])
102
+
103
+ # Clear batch results when in single mode
104
+ latest_batch_results = None
105
+ latest_batch_images = None
106
+
107
+ return visualized_image, captions_html, "", gr.update(visible=False)
108
+
109
+ except Exception as e:
110
+ import traceback
111
+ error_msg = traceback.format_exc()
112
+ print("="*60)
113
+ print("ERROR DETAILS:")
114
+ print(error_msg)
115
+ print("="*60)
116
+
117
+ error_html = f"""
118
+ <div style='background: #FADBD8; border: 2px solid #E74C3C; border-radius: 20px; padding: 28px; margin: 16px 0;'>
119
+ <h3 style='color: #C0392B; margin-top: 0; font-size: 22px;'>❌ Processing Error</h3>
120
+ <p style='color: #E74C3C; font-weight: bold; font-size: 17px; margin-bottom: 16px;'>{str(e)}</p>
121
+ <details style='margin-top: 12px;'>
122
+ <summary style='cursor: pointer; color: #C0392B; font-weight: bold; font-size: 16px;'>View Full Error Trace</summary>
123
+ <pre style='background: white; padding: 16px; border-radius: 12px; overflow-x: auto; font-size: 13px; color: #2C3E50; margin-top: 12px;'>{error_msg}</pre>
124
+ </details>
125
+ </div>
126
+ """
127
+ return None, error_html, "", gr.update(visible=False)
128
+
129
+ # Batch processing mode (2+ images)
130
+ else:
131
+ try:
132
+ # Define progress callback
133
+ def update_progress(progress_info):
134
+ current = progress_info['current']
135
+ total = progress_info['total']
136
+ percent = progress_info['percent']
137
+
138
+ # Update Gradio progress
139
+ progress(percent / 100, desc=f"Processing image {current}/{total}")
140
+
141
+ # Process batch
142
+ batch_results = pipeline.process_batch(
143
+ images=images,
144
+ platform=platform,
145
+ yolo_variant=yolo_variant,
146
+ language=caption_language,
147
+ progress_callback=update_progress
148
+ )
149
+
150
+ # Store results globally for export
151
+ latest_batch_results = batch_results
152
+ latest_batch_images = images
153
+
154
+ # Format batch results as HTML
155
+ batch_html = ui_manager.format_batch_results_html(batch_results)
156
+
157
+ # Return None for single image display, batch results HTML, and show export panel
158
+ return None, "", batch_html, gr.update(visible=True)
159
+
160
+ except Exception as e:
161
+ import traceback
162
+ error_msg = traceback.format_exc()
163
+ print("="*60)
164
+ print("BATCH PROCESSING ERROR:")
165
+ print(error_msg)
166
+ print("="*60)
167
+
168
+ error_html = f"""
169
+ <div style='background: #FADBD8; border: 2px solid #E74C3C; border-radius: 20px; padding: 28px; margin: 16px 0;'>
170
+ <h3 style='color: #C0392B; margin-top: 0; font-size: 22px;'>❌ Batch Processing Error</h3>
171
+ <p style='color: #E74C3C; font-weight: bold; font-size: 17px; margin-bottom: 16px;'>{str(e)}</p>
172
+ <details style='margin-top: 12px;'>
173
+ <summary style='cursor: pointer; color: #C0392B; font-weight: bold; font-size: 16px;'>View Full Error Trace</summary>
174
+ <pre style='background: white; padding: 16px; border-radius: 12px; overflow-x: auto; font-size: 13px; color: #2C3E50; margin-top: 12px;'>{error_msg}</pre>
175
+ </details>
176
+ </div>
177
+ """
178
+ return None, error_html, "", gr.update(visible=False)
179
 
180
+
181
+ def export_json_handler():
182
+ """Export batch results to JSON file."""
183
+ global latest_batch_results
184
+
185
+ if latest_batch_results is None:
186
+ return None
187
 
188
  try:
189
+ # Create temporary file
190
+ temp_dir = tempfile.gettempdir()
191
+ output_path = os.path.join(temp_dir, "pixcribe_batch_results.json")
192
+
193
+ # Export to JSON
194
+ pipeline.batch_processor.export_to_json(latest_batch_results, output_path)
195
+
196
+ return output_path
197
+ except Exception as e:
198
+ print(f"Export JSON error: {str(e)}")
199
+ return None
200
+
201
+
202
+ def export_csv_handler():
203
+ """Export batch results to CSV file."""
204
+ global latest_batch_results
205
 
206
+ if latest_batch_results is None:
207
+ return None
208
 
209
+ try:
210
+ # Create temporary file
211
+ temp_dir = tempfile.gettempdir()
212
+ output_path = os.path.join(temp_dir, "pixcribe_batch_results.csv")
213
+
214
+ # Export to CSV
215
+ pipeline.batch_processor.export_to_csv(latest_batch_results, output_path)
216
 
217
+ return output_path
218
  except Exception as e:
219
+ print(f"Export CSV error: {str(e)}")
220
+ return None
221
+
222
+
223
+ def export_zip_handler():
224
+ """Export batch results to ZIP archive."""
225
+ global latest_batch_results, latest_batch_images
226
+
227
+ if latest_batch_results is None or latest_batch_images is None:
228
+ return None
229
+
230
+ try:
231
+ # Create temporary file
232
+ temp_dir = tempfile.gettempdir()
233
+ output_path = os.path.join(temp_dir, "pixcribe_batch_results.zip")
 
 
 
 
234
 
235
+ # Export to ZIP
236
+ pipeline.batch_processor.export_to_zip(
237
+ latest_batch_results,
238
+ latest_batch_images,
239
+ output_path
240
+ )
241
 
242
+ return output_path
243
+ except Exception as e:
244
+ print(f"Export ZIP error: {str(e)}")
245
+ return None
246
 
 
247
 
248
  # Create Gradio Interface
249
+ with gr.Blocks(css=ui_manager.custom_css, title="Pixcribe V5 - AI Social Media Captions") as app:
250
 
251
  # Header
252
  ui_manager.create_header()
 
259
  # Left - Upload Card
260
  with gr.Column(scale=1):
261
  with gr.Group(elem_classes="upload-card"):
262
+ image_input = gr.File(
263
+ file_count="multiple",
264
+ file_types=["image"],
265
+ label="Upload Images (Max 10)",
266
  elem_classes="upload-area"
267
  )
268
 
269
+ # Right - Detected Objects (Single Image Mode)
270
  with gr.Column(scale=1):
271
  with gr.Group(elem_classes="results-card"):
272
  gr.Markdown("### Detected Objects", elem_classes="section-title")
 
316
  </div>
317
  """)
318
 
319
+ # Single Image Caption Results (Full Width)
320
  with gr.Group(elem_classes="caption-results-container"):
321
  gr.Markdown("### πŸ“ Generated Captions", elem_classes="section-title")
322
  caption_output = gr.HTML(
 
324
  elem_id="caption-results"
325
  )
326
 
327
+ # Batch Results Display (Initially Hidden)
328
+ batch_results_output = gr.HTML(
329
+ label="",
330
+ visible=True
331
+ )
332
+
333
+ # Export Panel (Initially Hidden)
334
+ with gr.Group(elem_classes="export-panel", visible=False) as export_panel:
335
+ gr.Markdown("### πŸ“₯ Export Batch Results", elem_classes="section-title-left")
336
+
337
+ with gr.Row():
338
+ json_btn = gr.Button("πŸ“„ Download JSON", variant="secondary")
339
+ csv_btn = gr.Button("πŸ“Š Download CSV", variant="secondary")
340
+ zip_btn = gr.Button("πŸ“¦ Download ZIP", variant="secondary")
341
+
342
+ json_file = gr.File(label="JSON Export", visible=False)
343
+ csv_file = gr.File(label="CSV Export", visible=False)
344
+ zip_file = gr.File(label="ZIP Export", visible=False)
345
+
346
  # Footer
347
  ui_manager.create_footer()
348
 
 
374
 
375
  # Connect button to processing function
376
  analyze_btn.click(
377
+ fn=process_images_wrapper,
378
  inputs=[image_input, yolo_variant, caption_language],
379
+ outputs=[visualized_image, caption_output, batch_results_output, export_panel]
380
+ )
381
+
382
+ # Connect export buttons
383
+ json_btn.click(
384
+ fn=export_json_handler,
385
+ inputs=[],
386
+ outputs=[json_file]
387
+ ).then(
388
+ lambda: gr.update(visible=True),
389
+ outputs=[json_file]
390
+ )
391
+
392
+ csv_btn.click(
393
+ fn=export_csv_handler,
394
+ inputs=[],
395
+ outputs=[csv_file]
396
+ ).then(
397
+ lambda: gr.update(visible=True),
398
+ outputs=[csv_file]
399
+ )
400
+
401
+ zip_btn.click(
402
+ fn=export_zip_handler,
403
+ inputs=[],
404
+ outputs=[zip_file]
405
+ ).then(
406
+ lambda: gr.update(visible=True),
407
+ outputs=[zip_file]
408
  )
409
 
410
  if __name__ == "__main__":
batch_processing_manager.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import json
3
+ import csv
4
+ import zipfile
5
+ from io import BytesIO
6
+ from typing import List, Dict, Optional, Callable
7
+ from PIL import Image
8
+ import traceback
9
+
10
+ class BatchProcessingManager:
11
+ """
12
+ Manages batch processing of multiple images with progress tracking,
13
+ error handling, and result export functionality.
14
+
15
+ Follows the Facade pattern by delegating actual image processing
16
+ to the PixcribePipeline instance.
17
+ """
18
+
19
+ def __init__(self, pipeline=None):
20
+ """
21
+ Initialize the Batch Processing Manager.
22
+
23
+ Args:
24
+ pipeline: Reference to PixcribePipeline instance for processing images
25
+ """
26
+ self.pipeline = pipeline
27
+ self.results = {} # Store processing results indexed by image number
28
+ self.timing_data = [] # Track processing time for each image
29
+
30
+ def process_batch(
31
+ self,
32
+ images: List[Image.Image],
33
+ platform: str = 'instagram',
34
+ yolo_variant: str = 'l',
35
+ language: str = 'zh',
36
+ progress_callback: Optional[Callable] = None
37
+ ) -> Dict:
38
+ """
39
+ Process a batch of images with progress tracking.
40
+
41
+ Args:
42
+ images: List of PIL Image objects to process (max 10)
43
+ platform: Target social media platform
44
+ yolo_variant: YOLO model variant ('m', 'l', 'x')
45
+ language: Caption language ('zh', 'en')
46
+ progress_callback: Optional callback function for progress updates
47
+
48
+ Returns:
49
+ Dictionary containing batch processing summary and results
50
+
51
+ Raises:
52
+ ValueError: If images list is empty or exceeds 10 images
53
+ """
54
+ # Validate input
55
+ if not images:
56
+ raise ValueError("Images list cannot be empty")
57
+
58
+ if len(images) > 10:
59
+ raise ValueError("Maximum 10 images allowed per batch")
60
+
61
+ # Initialize results storage
62
+ self.results = {}
63
+ self.timing_data = []
64
+ total_images = len(images)
65
+
66
+ # Record batch start time
67
+ batch_start_time = time.time()
68
+
69
+ print(f"\n{'='*60}")
70
+ print(f"Starting batch processing: {total_images} images")
71
+ print(f"Platform: {platform} | Variant: {yolo_variant} | Language: {language}")
72
+ print(f"{'='*60}\n")
73
+
74
+ # Process each image
75
+ for idx, image in enumerate(images):
76
+ image_start_time = time.time()
77
+ image_index = idx + 1
78
+
79
+ try:
80
+ print(f"[{image_index}/{total_images}] Processing image {image_index}...")
81
+
82
+ # Call pipeline's process_image method
83
+ result = self.pipeline.process_image(
84
+ image=image,
85
+ platform=platform,
86
+ yolo_variant=yolo_variant,
87
+ language=language
88
+ )
89
+
90
+ # Store successful result
91
+ self.results[image_index] = {
92
+ 'status': 'success',
93
+ 'result': result,
94
+ 'image_index': image_index,
95
+ 'error': None
96
+ }
97
+
98
+ print(f"βœ“ Image {image_index} processed successfully")
99
+
100
+ except Exception as e:
101
+ # Store error result
102
+ error_trace = traceback.format_exc()
103
+ self.results[image_index] = {
104
+ 'status': 'failed',
105
+ 'result': None,
106
+ 'image_index': image_index,
107
+ 'error': {
108
+ 'type': type(e).__name__,
109
+ 'message': str(e),
110
+ 'traceback': error_trace
111
+ }
112
+ }
113
+
114
+ print(f"βœ— Image {image_index} failed: {str(e)}")
115
+
116
+ # Record processing time for this image
117
+ image_elapsed = time.time() - image_start_time
118
+ self.timing_data.append(image_elapsed)
119
+
120
+ # Calculate progress information
121
+ completed = image_index
122
+ percent = (completed / total_images) * 100
123
+
124
+ # Estimate remaining time based on average processing time
125
+ avg_time = sum(self.timing_data) / len(self.timing_data)
126
+ remaining_images = total_images - completed
127
+ estimated_remaining = avg_time * remaining_images
128
+
129
+ # Call progress callback if provided
130
+ if progress_callback:
131
+ progress_info = {
132
+ 'current': completed,
133
+ 'total': total_images,
134
+ 'percent': percent,
135
+ 'estimated_remaining': estimated_remaining,
136
+ 'latest_result': self.results[image_index],
137
+ 'image_index': image_index
138
+ }
139
+ progress_callback(progress_info)
140
+
141
+ # Calculate batch summary
142
+ batch_elapsed = time.time() - batch_start_time
143
+ total_processed = len(self.results)
144
+ total_failed = sum(1 for r in self.results.values() if r['status'] == 'failed')
145
+ total_success = total_processed - total_failed
146
+
147
+ print(f"\n{'='*60}")
148
+ print(f"Batch processing completed!")
149
+ print(f"Total: {total_processed} | Success: {total_success} | Failed: {total_failed}")
150
+ print(f"Total time: {batch_elapsed:.2f}s | Avg per image: {batch_elapsed/total_processed:.2f}s")
151
+ print(f"{'='*60}\n")
152
+
153
+ # Return batch summary
154
+ return {
155
+ 'results': self.results,
156
+ 'total_processed': total_processed,
157
+ 'total_success': total_success,
158
+ 'total_failed': total_failed,
159
+ 'total_time': batch_elapsed,
160
+ 'average_time_per_image': batch_elapsed / total_processed if total_processed > 0 else 0
161
+ }
162
+
163
+ def get_result(self, image_index: int) -> Optional[Dict]:
164
+ """
165
+ Get processing result for a specific image.
166
+
167
+ Args:
168
+ image_index: Index of the image (1-based)
169
+
170
+ Returns:
171
+ Result dictionary or None if index doesn't exist
172
+ """
173
+ return self.results.get(image_index)
174
+
175
+ def get_all_results(self) -> Dict:
176
+ """
177
+ Get all processing results.
178
+
179
+ Returns:
180
+ Complete results dictionary
181
+ """
182
+ return self.results
183
+
184
+ def clear_results(self):
185
+ """Clear all stored results to free memory."""
186
+ self.results = {}
187
+ self.timing_data = []
188
+ print("βœ“ Batch results cleared")
189
+
190
+ def export_to_json(self, results: Dict, output_path: str) -> str:
191
+ """
192
+ Export batch results to JSON format.
193
+
194
+ Args:
195
+ results: Results dictionary from process_batch
196
+ output_path: Path to save JSON file
197
+
198
+ Returns:
199
+ Path to the saved JSON file
200
+ """
201
+ # Prepare export data
202
+ export_data = {
203
+ 'batch_summary': {
204
+ 'total_processed': results.get('total_processed', 0),
205
+ 'total_success': results.get('total_success', 0),
206
+ 'total_failed': results.get('total_failed', 0),
207
+ 'total_time': results.get('total_time', 0),
208
+ 'average_time_per_image': results.get('average_time_per_image', 0)
209
+ },
210
+ 'images': []
211
+ }
212
+
213
+ # Process each image result
214
+ for img_idx, img_result in results.get('results', {}).items():
215
+ if img_result['status'] == 'success':
216
+ result_data = img_result['result']
217
+ image_export = {
218
+ 'image_index': img_idx,
219
+ 'status': 'success',
220
+ 'captions': result_data.get('captions', []),
221
+ 'detected_objects': [
222
+ det['class_name'] for det in result_data.get('detections', [])
223
+ ],
224
+ 'detected_brands': [
225
+ brand[0] if isinstance(brand, tuple) else brand
226
+ for brand in result_data.get('brands', [])
227
+ ],
228
+ 'scene_info': result_data.get('scene', {}),
229
+ 'lighting': result_data.get('lighting', {})
230
+ }
231
+ else:
232
+ image_export = {
233
+ 'image_index': img_idx,
234
+ 'status': 'failed',
235
+ 'error': img_result.get('error', {})
236
+ }
237
+
238
+ export_data['images'].append(image_export)
239
+
240
+ # Write to JSON file
241
+ with open(output_path, 'w', encoding='utf-8') as f:
242
+ json.dump(export_data, f, ensure_ascii=False, indent=2)
243
+
244
+ print(f"βœ“ Batch results exported to JSON: {output_path}")
245
+ return output_path
246
+
247
+ def export_to_csv(self, results: Dict, output_path: str) -> str:
248
+ """
249
+ Export batch results to CSV format.
250
+
251
+ Args:
252
+ results: Results dictionary from process_batch
253
+ output_path: Path to save CSV file
254
+
255
+ Returns:
256
+ Path to the saved CSV file
257
+ """
258
+ # Define CSV headers
259
+ headers = [
260
+ 'image_index',
261
+ 'status',
262
+ 'caption_professional',
263
+ 'caption_creative',
264
+ 'caption_authentic',
265
+ 'detected_objects',
266
+ 'detected_brands',
267
+ 'hashtags'
268
+ ]
269
+
270
+ # Prepare rows
271
+ rows = []
272
+ for img_idx, img_result in results.get('results', {}).items():
273
+ if img_result['status'] == 'success':
274
+ result_data = img_result['result']
275
+ captions = result_data.get('captions', [])
276
+
277
+ # Extract captions by tone
278
+ caption_professional = ''
279
+ caption_creative = ''
280
+ caption_authentic = ''
281
+ all_hashtags = []
282
+
283
+ for cap in captions:
284
+ tone = cap.get('tone', '').lower()
285
+ caption_text = cap.get('caption', '')
286
+ hashtags = cap.get('hashtags', [])
287
+
288
+ if 'professional' in tone:
289
+ caption_professional = caption_text
290
+ elif 'creative' in tone:
291
+ caption_creative = caption_text
292
+ elif 'authentic' in tone or 'casual' in tone:
293
+ caption_authentic = caption_text
294
+
295
+ all_hashtags.extend(hashtags)
296
+
297
+ # Remove duplicates from hashtags
298
+ all_hashtags = list(set(all_hashtags))
299
+
300
+ row = {
301
+ 'image_index': img_idx,
302
+ 'status': 'success',
303
+ 'caption_professional': caption_professional,
304
+ 'caption_creative': caption_creative,
305
+ 'caption_authentic': caption_authentic,
306
+ 'detected_objects': ', '.join([
307
+ det['class_name'] for det in result_data.get('detections', [])
308
+ ]),
309
+ 'detected_brands': ', '.join([
310
+ brand[0] if isinstance(brand, tuple) else brand
311
+ for brand in result_data.get('brands', [])
312
+ ]),
313
+ 'hashtags': ' '.join([f'#{tag}' for tag in all_hashtags])
314
+ }
315
+ else:
316
+ row = {
317
+ 'image_index': img_idx,
318
+ 'status': 'failed',
319
+ 'caption_professional': '',
320
+ 'caption_creative': '',
321
+ 'caption_authentic': '',
322
+ 'detected_objects': '',
323
+ 'detected_brands': '',
324
+ 'hashtags': ''
325
+ }
326
+
327
+ rows.append(row)
328
+
329
+ # Write to CSV file
330
+ with open(output_path, 'w', newline='', encoding='utf-8') as f:
331
+ writer = csv.DictWriter(f, fieldnames=headers)
332
+ writer.writeheader()
333
+ writer.writerows(rows)
334
+
335
+ print(f"βœ“ Batch results exported to CSV: {output_path}")
336
+ return output_path
337
+
338
+ def export_to_zip(self, results: Dict, images: List[Image.Image], output_path: str) -> str:
339
+ """
340
+ Export batch results to ZIP archive with images and text files.
341
+
342
+ Args:
343
+ results: Results dictionary from process_batch
344
+ images: List of original PIL Image objects
345
+ output_path: Path to save ZIP file
346
+
347
+ Returns:
348
+ Path to the saved ZIP file
349
+ """
350
+ with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
351
+ for img_idx, img_result in results.get('results', {}).items():
352
+ if img_result['status'] == 'success':
353
+ # Save original image
354
+ image_filename = f"image_{img_idx:03d}.jpg"
355
+
356
+ # Convert PIL image to bytes
357
+ img_buffer = BytesIO()
358
+ images[img_idx - 1].save(img_buffer, format='JPEG', quality=95)
359
+ img_buffer.seek(0)
360
+
361
+ zipf.writestr(image_filename, img_buffer.read())
362
+
363
+ # Save caption text file
364
+ text_filename = f"image_{img_idx:03d}.txt"
365
+ text_content = self._format_result_as_text(img_result['result'])
366
+ zipf.writestr(text_filename, text_content)
367
+
368
+ print(f"βœ“ Added to ZIP: {image_filename} and {text_filename}")
369
+
370
+ print(f"βœ“ Batch results exported to ZIP: {output_path}")
371
+ return output_path
372
+
373
+ def _format_result_as_text(self, result: Dict) -> str:
374
+ """
375
+ Format a single image result as plain text for ZIP export.
376
+
377
+ Args:
378
+ result: Single image processing result dictionary
379
+
380
+ Returns:
381
+ Formatted text string
382
+ """
383
+ lines = []
384
+ lines.append("=" * 60)
385
+ lines.append("PIXCRIBE - AI GENERATED SOCIAL MEDIA CONTENT")
386
+ lines.append("=" * 60)
387
+ lines.append("")
388
+
389
+ # Captions section
390
+ captions = result.get('captions', [])
391
+ for i, cap in enumerate(captions, 1):
392
+ tone = cap.get('tone', 'Unknown').upper()
393
+ caption_text = cap.get('caption', '')
394
+ hashtags = cap.get('hashtags', [])
395
+
396
+ lines.append(f"CAPTION {i} - {tone} STYLE")
397
+ lines.append("-" * 60)
398
+ lines.append(caption_text)
399
+ lines.append("")
400
+ lines.append("Hashtags:")
401
+ lines.append(' '.join([f'#{tag}' for tag in hashtags]))
402
+ lines.append("")
403
+ lines.append("")
404
+
405
+ # Detected objects section
406
+ detections = result.get('detections', [])
407
+ if detections:
408
+ lines.append("DETECTED OBJECTS")
409
+ lines.append("-" * 60)
410
+ object_names = [det['class_name'] for det in detections]
411
+ lines.append(', '.join(object_names))
412
+ lines.append("")
413
+
414
+ # Detected brands section
415
+ brands = result.get('brands', [])
416
+ if brands:
417
+ lines.append("DETECTED BRANDS")
418
+ lines.append("-" * 60)
419
+ brand_names = [
420
+ brand[0] if isinstance(brand, tuple) else brand
421
+ for brand in brands
422
+ ]
423
+ lines.append(', '.join(brand_names))
424
+ lines.append("")
425
+
426
+ # Scene information
427
+ scene_info = result.get('scene', {})
428
+ if scene_info:
429
+ lines.append("SCENE ANALYSIS")
430
+ lines.append("-" * 60)
431
+
432
+ if 'lighting' in scene_info:
433
+ lighting = scene_info['lighting'].get('top', 'Unknown')
434
+ lines.append(f"Lighting: {lighting}")
435
+
436
+ if 'mood' in scene_info:
437
+ mood = scene_info['mood'].get('top', 'Unknown')
438
+ lines.append(f"Mood: {mood}")
439
+
440
+ lines.append("")
441
+
442
+ lines.append("=" * 60)
443
+ lines.append("Generated by Pixcribe V5 - AI Social Media Caption Generator")
444
+ lines.append("=" * 60)
445
+
446
+ return '\n'.join(lines)
447
+
448
+
449
+ print("βœ“ BatchProcessingManager defined")
pixcribe_pipeline.py CHANGED
@@ -2,7 +2,7 @@ import sys
2
  import time
3
  import traceback
4
  from PIL import Image
5
- from typing import Dict
6
 
7
  from image_processor_manager import ImageProcessorManager
8
  from yolo_detection_manager import YOLODetectionManager
@@ -18,6 +18,7 @@ from scene_compatibility_manager import SceneCompatibilityManager
18
  from caption_generation_manager import CaptionGenerationManager
19
  from detection_fusion_manager import DetectionFusionManager
20
  from output_processing_manager import OutputProcessingManager
 
21
 
22
  class PixcribePipeline:
23
  """Main Facade coordinating all components (V2 with multi-language support)"""
@@ -67,9 +68,12 @@ class PixcribePipeline:
67
  # Initialize OutputProcessingManager with PromptLibrary for smart hashtag generation
68
  self.output_processor = OutputProcessingManager(self.prompt_library)
69
 
 
 
 
70
  elapsed = time.time() - start_time
71
  print("="*60)
72
- print(f"βœ“ Pipeline initialized successfully (Time: {elapsed:.2f}s)")
73
  print("="*60)
74
 
75
  def process_image(self, image, platform='instagram', yolo_variant='l', language='zh') -> Dict:
@@ -332,4 +336,50 @@ class PixcribePipeline:
332
  # Re-raise exception so it can be caught and displayed
333
  raise
334
 
335
- print("βœ“ PixcribePipeline (V2 with VLM Verification, Scene Compatibility, and Adaptive Weights) defined")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import time
3
  import traceback
4
  from PIL import Image
5
+ from typing import Dict, List, Callable, Optional
6
 
7
  from image_processor_manager import ImageProcessorManager
8
  from yolo_detection_manager import YOLODetectionManager
 
18
  from caption_generation_manager import CaptionGenerationManager
19
  from detection_fusion_manager import DetectionFusionManager
20
  from output_processing_manager import OutputProcessingManager
21
+ from batch_processing_manager import BatchProcessingManager
22
 
23
  class PixcribePipeline:
24
  """Main Facade coordinating all components (V2 with multi-language support)"""
 
68
  # Initialize OutputProcessingManager with PromptLibrary for smart hashtag generation
69
  self.output_processor = OutputProcessingManager(self.prompt_library)
70
 
71
+ # Initialize BatchProcessingManager with pipeline reference
72
+ self.batch_processor = BatchProcessingManager(pipeline=self)
73
+
74
  elapsed = time.time() - start_time
75
  print("="*60)
76
+ print(f"βœ“ Pipeline V5 initialized successfully with batch processing (Time: {elapsed:.2f}s)")
77
  print("="*60)
78
 
79
  def process_image(self, image, platform='instagram', yolo_variant='l', language='zh') -> Dict:
 
336
  # Re-raise exception so it can be caught and displayed
337
  raise
338
 
339
+ def process_batch(
340
+ self,
341
+ images: List[Image.Image],
342
+ platform: str = 'instagram',
343
+ yolo_variant: str = 'l',
344
+ language: str = 'zh',
345
+ progress_callback: Optional[Callable] = None
346
+ ) -> Dict:
347
+ """
348
+ Process multiple images in batch with progress tracking.
349
+
350
+ This method provides a Facade interface to the BatchProcessingManager,
351
+ allowing batch processing through the main Pipeline API.
352
+
353
+ Args:
354
+ images: List of PIL Image objects to process (max 10)
355
+ platform: Target social media platform ('instagram', 'tiktok', 'xiaohongshu')
356
+ yolo_variant: YOLO model variant ('m', 'l', 'x')
357
+ language: Caption language ('zh' for Traditional Chinese, 'en' for English)
358
+ progress_callback: Optional callback function for progress updates
359
+
360
+ Returns:
361
+ Dictionary containing:
362
+ - results: Dict mapping image index to processing results
363
+ - total_processed: Total number of images processed
364
+ - total_success: Number of successfully processed images
365
+ - total_failed: Number of failed images
366
+ - total_time: Total processing time in seconds
367
+ - average_time_per_image: Average time per image in seconds
368
+
369
+ Raises:
370
+ ValueError: If images list is empty or exceeds 10 images
371
+
372
+ Example:
373
+ >>> images = [Image.open(f'image{i}.jpg') for i in range(1, 6)]
374
+ >>> results = pipeline.process_batch(images, platform='instagram')
375
+ >>> print(f"Processed {results['total_success']}/{results['total_processed']} images")
376
+ """
377
+ return self.batch_processor.process_batch(
378
+ images=images,
379
+ platform=platform,
380
+ yolo_variant=yolo_variant,
381
+ language=language,
382
+ progress_callback=progress_callback
383
+ )
384
+
385
+ print("βœ“ PixcribePipeline V5 (with Batch Processing) defined")
style.py ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Style:
2
+ """Manages all CSS styling for Pixcribe application"""
3
+
4
+ @staticmethod
5
+ def get_all_css() -> str:
6
+ """
7
+ Return complete CSS styling for the entire application.
8
+
9
+ Returns:
10
+ Complete CSS string with all styling rules
11
+ """
12
+ return """
13
+ /* ==================== Global Reset & Base ==================== */
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ .gradio-container {
21
+ background: linear-gradient(135deg, #F8F9FA 0%, #E9ECEF 100%) !important;
22
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
23
+ padding: 0 !important;
24
+ max-width: 100% !important;
25
+ min-height: 100vh !important;
26
+ }
27
+
28
+ /* Main content wrapper - Generous padding to prevent edge clipping */
29
+ .contain {
30
+ max-width: 1600px !important;
31
+ margin: 0 auto !important;
32
+ padding: 64px 96px 96px 96px !important;
33
+ }
34
+
35
+ /* ==================== Header ==================== */
36
+ .app-header {
37
+ text-align: center;
38
+ margin-bottom: 72px;
39
+ animation: fadeInDown 0.8s ease-out;
40
+ padding: 0 32px;
41
+ }
42
+
43
+ @keyframes fadeInDown {
44
+ from {
45
+ opacity: 0;
46
+ transform: translateY(-30px);
47
+ }
48
+ to {
49
+ opacity: 1;
50
+ transform: translateY(0);
51
+ }
52
+ }
53
+
54
+ .app-title {
55
+ font-size: 72px;
56
+ font-weight: 800;
57
+ background: linear-gradient(135deg, #2C3E50 0%, #34495E 100%);
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
+ margin-bottom: 24px;
62
+ letter-spacing: -0.05em;
63
+ line-height: 1.1;
64
+ }
65
+
66
+ .app-subtitle {
67
+ font-size: 26px;
68
+ font-weight: 400;
69
+ color: #6C757D;
70
+ margin-bottom: 0;
71
+ letter-spacing: 0.01em;
72
+ }
73
+
74
+ /* ==================== Layout ==================== */
75
+ .main-row {
76
+ gap: 48px !important;
77
+ margin-bottom: 48px !important;
78
+ }
79
+
80
+ /* Left column elegant container */
81
+ .main-row > .column:first-child {
82
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(252, 253, 254, 0.6) 100%) !important;
83
+ border-radius: 28px !important;
84
+ padding: 40px !important;
85
+ border: 1px solid rgba(52, 152, 219, 0.08) !important;
86
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
87
+ }
88
+
89
+ /* Right column elegant container */
90
+ .main-row > .column:last-child {
91
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(252, 253, 254, 0.6) 100%) !important;
92
+ border-radius: 28px !important;
93
+ padding: 40px !important;
94
+ border: 1px solid rgba(52, 152, 219, 0.08) !important;
95
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
96
+ }
97
+
98
+ /* ==================== Premium Cards - Light & Spacious ==================== */
99
+ .upload-card {
100
+ background: rgba(255, 255, 255, 0.95) !important;
101
+ border-radius: 32px !important;
102
+ box-shadow:
103
+ 0 4px 16px rgba(0, 0, 0, 0.06),
104
+ 0 2px 4px rgba(0, 0, 0, 0.03),
105
+ 0 1px 2px rgba(0, 0, 0, 0.02) !important;
106
+ border: 1px solid rgba(0, 0, 0, 0.05) !important;
107
+ padding: 48px !important;
108
+ margin-bottom: 32px !important;
109
+ transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
110
+ overflow: visible !important;
111
+ }
112
+
113
+ .results-card {
114
+ background: transparent !important;
115
+ border-radius: 0 !important;
116
+ box-shadow: none !important;
117
+ border: none !important;
118
+ padding: 0 !important;
119
+ margin-bottom: 32px !important;
120
+ overflow: visible !important;
121
+ }
122
+
123
+ /* Caption Results Container - Elegant Design */
124
+ .caption-results-container {
125
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.85) 0%, rgba(252, 253, 254, 0.7) 100%) !important;
126
+ border-radius: 28px !important;
127
+ padding: 44px !important;
128
+ border: 1px solid rgba(52, 152, 219, 0.1) !important;
129
+ box-shadow:
130
+ 0 4px 20px rgba(0, 0, 0, 0.04),
131
+ 0 2px 8px rgba(52, 152, 219, 0.03) !important;
132
+ margin-bottom: 40px !important;
133
+ overflow: visible !important;
134
+ }
135
+
136
+ .upload-card:hover {
137
+ box-shadow:
138
+ 0 8px 32px rgba(0, 0, 0, 0.10),
139
+ 0 4px 8px rgba(0, 0, 0, 0.06) !important;
140
+ transform: translateY(-6px);
141
+ border-color: rgba(52, 152, 219, 0.3) !important;
142
+ }
143
+
144
+ /* ==================== Upload Area ==================== */
145
+ .upload-area {
146
+ border: 3px dashed rgba(52, 152, 219, 0.35) !important;
147
+ border-radius: 28px !important;
148
+ background: linear-gradient(135deg, rgba(52, 152, 219, 0.03) 0%, rgba(52, 152, 219, 0.06) 100%) !important;
149
+ padding: 96px 40px !important;
150
+ text-align: center !important;
151
+ transition: all 0.3s ease !important;
152
+ min-height: 360px !important;
153
+ }
154
+
155
+ .upload-area:hover {
156
+ border-color: #3498DB !important;
157
+ background: linear-gradient(135deg, rgba(52, 152, 219, 0.06) 0%, rgba(52, 152, 219, 0.12) 100%) !important;
158
+ transform: scale(1.02);
159
+ }
160
+
161
+ /* ==================== Section Titles - Consistent Spacing ==================== */
162
+ .section-title {
163
+ font-size: 28px !important;
164
+ font-weight: 700 !important;
165
+ color: #2C3E50 !important;
166
+ margin-bottom: 20px !important;
167
+ letter-spacing: -0.02em !important;
168
+ padding-bottom: 0 !important;
169
+ border-bottom: none !important;
170
+ text-align: left !important;
171
+ margin-top: 0 !important;
172
+ }
173
+
174
+ .section-title-left {
175
+ font-size: 28px !important;
176
+ font-weight: 700 !important;
177
+ color: #2C3E50 !important;
178
+ margin-bottom: 20px !important;
179
+ margin-top: 0 !important;
180
+ letter-spacing: -0.02em !important;
181
+ text-align: left !important;
182
+ border-bottom: none !important;
183
+ padding-bottom: 0 !important;
184
+ }
185
+
186
+ /* ==================== Form Elements - Generous Padding ==================== */
187
+ .settings-row {
188
+ gap: 24px !important;
189
+ margin-bottom: 28px !important;
190
+ }
191
+
192
+ .radio-group {
193
+ background: rgba(248, 249, 250, 0.5) !important;
194
+ border-radius: 20px !important;
195
+ padding: 24px 28px !important;
196
+ border: none !important;
197
+ margin-bottom: 24px !important;
198
+ border: 1px solid rgba(0, 0, 0, 0.04) !important;
199
+ }
200
+
201
+ .radio-group:last-child {
202
+ margin-bottom: 0 !important;
203
+ }
204
+
205
+ /* Inline radio groups for side-by-side layout */
206
+ .radio-group-inline {
207
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.7) 0%, rgba(248, 249, 250, 0.5) 100%) !important;
208
+ border-radius: 16px !important;
209
+ padding: 20px !important;
210
+ border: 1px solid rgba(52, 152, 219, 0.1) !important;
211
+ margin-bottom: 0 !important;
212
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03) !important;
213
+ transition: all 0.3s ease !important;
214
+ }
215
+
216
+ .radio-group-inline:hover {
217
+ box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08) !important;
218
+ border-color: rgba(52, 152, 219, 0.2) !important;
219
+ }
220
+
221
+ .radio-group label {
222
+ color: #6C757D !important;
223
+ font-weight: 600 !important;
224
+ font-size: 14px !important;
225
+ margin-bottom: 16px !important;
226
+ letter-spacing: 0.08em !important;
227
+ text-transform: uppercase !important;
228
+ display: block !important;
229
+ text-align: left !important;
230
+ }
231
+
232
+ /* Radio group title (the actual input label) */
233
+ .radio-group > label:first-child {
234
+ color: #2C3E50 !important;
235
+ font-weight: 700 !important;
236
+ font-size: 19px !important;
237
+ margin-bottom: 16px !important;
238
+ letter-spacing: -0.02em !important;
239
+ text-transform: none !important;
240
+ }
241
+
242
+ /* Inline radio group title - BIGGER and BOLD */
243
+ .radio-group-inline > label:first-child {
244
+ color: #2C3E50 !important;
245
+ font-weight: 700 !important;
246
+ font-size: 18px !important;
247
+ margin-bottom: 14px !important;
248
+ letter-spacing: -0.01em !important;
249
+ text-transform: none !important;
250
+ display: block !important;
251
+ }
252
+
253
+ .radio-group input[type="radio"] {
254
+ accent-color: #3498DB !important;
255
+ width: 22px !important;
256
+ height: 22px !important;
257
+ margin-right: 14px !important;
258
+ }
259
+
260
+ /* Radio option labels */
261
+ .radio-group > div > label {
262
+ color: #495057 !important;
263
+ font-weight: 500 !important;
264
+ font-size: 17px !important;
265
+ letter-spacing: -0.01em !important;
266
+ text-transform: none !important;
267
+ padding: 14px 20px !important;
268
+ border-radius: 14px !important;
269
+ transition: all 0.2s ease !important;
270
+ cursor: pointer !important;
271
+ display: flex !important;
272
+ align-items: center !important;
273
+ }
274
+
275
+ /* Inline radio option labels - BIGGER */
276
+ .radio-group-inline > div > label {
277
+ color: #495057 !important;
278
+ font-weight: 500 !important;
279
+ font-size: 16px !important;
280
+ letter-spacing: -0.01em !important;
281
+ text-transform: none !important;
282
+ padding: 12px 16px !important;
283
+ border-radius: 10px !important;
284
+ transition: all 0.2s ease !important;
285
+ cursor: pointer !important;
286
+ display: flex !important;
287
+ align-items: center !important;
288
+ background: rgba(255, 255, 255, 0.6) !important;
289
+ margin-bottom: 8px !important;
290
+ border: 1px solid rgba(0, 0, 0, 0.04) !important;
291
+ }
292
+
293
+ .radio-group > div > label:hover {
294
+ background: rgba(52, 152, 219, 0.08) !important;
295
+ }
296
+
297
+ .radio-group-inline > div > label:hover {
298
+ background: rgba(52, 152, 219, 0.12) !important;
299
+ transform: translateX(4px);
300
+ }
301
+
302
+ /* ==================== Button ==================== */
303
+ .generate-button {
304
+ background: linear-gradient(135deg, #3498DB 0%, #2980B9 100%) !important;
305
+ color: white !important;
306
+ border: none !important;
307
+ border-radius: 20px !important;
308
+ padding: 24px 64px !important;
309
+ font-size: 19px !important;
310
+ font-weight: 700 !important;
311
+ cursor: pointer !important;
312
+ box-shadow:
313
+ 0 6px 24px rgba(52, 152, 219, 0.35),
314
+ 0 3px 6px rgba(52, 152, 219, 0.25) !important;
315
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
316
+ letter-spacing: -0.02em !important;
317
+ width: 100% !important;
318
+ margin-top: 24px !important;
319
+ }
320
+
321
+ .generate-button:hover {
322
+ transform: translateY(-6px) scale(1.02) !important;
323
+ box-shadow:
324
+ 0 16px 48px rgba(52, 152, 219, 0.45),
325
+ 0 6px 12px rgba(52, 152, 219, 0.35) !important;
326
+ }
327
+
328
+ .generate-button:active {
329
+ transform: translateY(-3px) scale(1.01) !important;
330
+ }
331
+
332
+ /* ==================== Caption Cards - Light & Elegant ==================== */
333
+ .caption-card {
334
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 249, 250, 0.95) 100%);
335
+ backdrop-filter: blur(20px);
336
+ border: 1px solid rgba(0, 0, 0, 0.06);
337
+ border-radius: 28px;
338
+ padding: 32px 36px;
339
+ margin-bottom: 28px;
340
+ transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
341
+ box-shadow:
342
+ 0 4px 16px rgba(0, 0, 0, 0.05),
343
+ 0 2px 4px rgba(0, 0, 0, 0.03);
344
+ position: relative;
345
+ }
346
+
347
+ .caption-card:hover {
348
+ box-shadow:
349
+ 0 8px 32px rgba(0, 0, 0, 0.10),
350
+ 0 4px 8px rgba(0, 0, 0, 0.06);
351
+ transform: translateY(-6px);
352
+ border-color: rgba(52, 152, 219, 0.3);
353
+ }
354
+
355
+ .caption-header {
356
+ font-size: 15px;
357
+ font-weight: 700;
358
+ color: #6C757D;
359
+ text-transform: uppercase;
360
+ letter-spacing: 0.14em;
361
+ margin-bottom: 20px;
362
+ }
363
+
364
+ .caption-text {
365
+ font-size: 21px;
366
+ font-weight: 400;
367
+ color: #2C3E50;
368
+ line-height: 1.8;
369
+ margin-bottom: 24px;
370
+ letter-spacing: -0.01em;
371
+ }
372
+
373
+ .caption-hashtags {
374
+ font-size: 18px;
375
+ font-weight: 600;
376
+ color: #3498DB;
377
+ margin-bottom: 0;
378
+ word-wrap: break-word;
379
+ line-height: 1.75;
380
+ }
381
+
382
+ /* Copy Button */
383
+ .copy-button {
384
+ position: absolute;
385
+ top: 28px;
386
+ right: 28px;
387
+ background: rgba(52, 152, 219, 0.10);
388
+ border: 1px solid rgba(52, 152, 219, 0.25);
389
+ border-radius: 14px;
390
+ padding: 12px 20px;
391
+ font-size: 15px;
392
+ font-weight: 600;
393
+ color: #3498DB;
394
+ cursor: pointer;
395
+ transition: all 0.2s ease;
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 8px;
399
+ }
400
+
401
+ .copy-button:hover {
402
+ background: rgba(52, 152, 219, 0.18);
403
+ border-color: #3498DB;
404
+ transform: translateY(-2px);
405
+ box-shadow: 0 4px 12px rgba(52, 152, 219, 0.25);
406
+ }
407
+
408
+ .copy-button:active {
409
+ transform: translateY(0);
410
+ }
411
+
412
+ .copy-button.copied {
413
+ background: rgba(39, 174, 96, 0.15);
414
+ border-color: #27AE60;
415
+ color: #27AE60;
416
+ }
417
+
418
+ /* ==================== Footer ==================== */
419
+ .app-footer {
420
+ text-align: center;
421
+ margin-top: 96px;
422
+ padding-top: 64px;
423
+ border-top: 3px solid rgba(0, 0, 0, 0.08);
424
+ animation: fadeInUp 0.8s ease-out 0.3s backwards;
425
+ }
426
+
427
+ @keyframes fadeInUp {
428
+ from {
429
+ opacity: 0;
430
+ transform: translateY(30px);
431
+ }
432
+ to {
433
+ opacity: 1;
434
+ transform: translateY(0);
435
+ }
436
+ }
437
+
438
+ .footer-text {
439
+ font-size: 17px;
440
+ color: #6C757D;
441
+ line-height: 2.0;
442
+ letter-spacing: -0.01em;
443
+ font-weight: 500;
444
+ }
445
+
446
+ .footer-models {
447
+ font-size: 15px;
448
+ color: #ADB5BD;
449
+ margin-top: 20px;
450
+ font-weight: 600;
451
+ letter-spacing: 0.03em;
452
+ }
453
+
454
+ /* ==================== Image Display ==================== */
455
+ .image-container {
456
+ border-radius: 28px !important;
457
+ overflow: hidden !important;
458
+ box-shadow:
459
+ 0 6px 24px rgba(0, 0, 0, 0.10),
460
+ 0 3px 6px rgba(0, 0, 0, 0.06) !important;
461
+ }
462
+
463
+ .image-container img {
464
+ border-radius: 28px !important;
465
+ box-shadow:
466
+ 0 6px 24px rgba(0, 0, 0, 0.12),
467
+ 0 3px 6px rgba(0, 0, 0, 0.08) !important;
468
+ }
469
+
470
+ /* ==================== Responsive Design ==================== */
471
+ @media (max-width: 768px) {
472
+ .contain {
473
+ padding: 48px 32px 64px 32px !important;
474
+ }
475
+
476
+ .app-title {
477
+ font-size: 52px;
478
+ }
479
+
480
+ .app-subtitle {
481
+ font-size: 20px;
482
+ }
483
+
484
+ .upload-card, .options-card, .results-card {
485
+ padding: 40px !important;
486
+ }
487
+
488
+ .upload-area {
489
+ padding: 64px 32px !important;
490
+ min-height: 280px !important;
491
+ }
492
+
493
+ .caption-card {
494
+ padding: 28px;
495
+ }
496
+
497
+ .section-title {
498
+ font-size: 30px !important;
499
+ }
500
+
501
+ .copy-button {
502
+ top: 20px;
503
+ right: 20px;
504
+ padding: 10px 16px;
505
+ font-size: 14px;
506
+ }
507
+ }
508
+
509
+ /* ==================== Loading Animation ==================== */
510
+ @keyframes shimmer {
511
+ 0% {
512
+ background-position: -1000px 0;
513
+ }
514
+ 100% {
515
+ background-position: 1000px 0;
516
+ }
517
+ }
518
+
519
+ .loading {
520
+ animation: shimmer 2s infinite;
521
+ background: linear-gradient(to right, #f8f9fa 4%, #e9ecef 25%, #f8f9fa 36%);
522
+ background-size: 1000px 100%;
523
+ }
524
+
525
+ /* ==================== Batch Processing Styles ==================== */
526
+ .batch-results-container {
527
+ max-width: 1400px !important;
528
+ margin: 0 auto !important;
529
+ padding: 24px 0 !important;
530
+ }
531
+
532
+ .batch-result-card {
533
+ background: white !important;
534
+ border-radius: 16px !important;
535
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06) !important;
536
+ margin-bottom: 20px !important;
537
+ transition: all 0.3s ease !important;
538
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
539
+ }
540
+
541
+ .batch-result-card:hover {
542
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12) !important;
543
+ }
544
+
545
+ .card-header {
546
+ padding: 20px 24px !important;
547
+ border-bottom: 1px solid #E9ECEF !important;
548
+ cursor: pointer !important;
549
+ display: flex !important;
550
+ align-items: center !important;
551
+ gap: 16px !important;
552
+ }
553
+
554
+ .card-thumbnail {
555
+ width: 80px !important;
556
+ height: 80px !important;
557
+ object-fit: cover !important;
558
+ border-radius: 8px !important;
559
+ border: 2px solid #F8F9FA !important;
560
+ }
561
+
562
+ .card-title {
563
+ font-size: 18px !important;
564
+ font-weight: 600 !important;
565
+ color: #2C3E50 !important;
566
+ flex: 1 !important;
567
+ }
568
+
569
+ .card-status {
570
+ font-size: 24px !important;
571
+ }
572
+
573
+ .card-content {
574
+ padding: 24px !important;
575
+ font-size: 15px !important;
576
+ line-height: 1.6 !important;
577
+ }
578
+
579
+ .caption-section {
580
+ margin-bottom: 24px !important;
581
+ padding: 16px !important;
582
+ background: #F8F9FA !important;
583
+ border-radius: 12px !important;
584
+ }
585
+
586
+ .caption-label {
587
+ font-weight: 600 !important;
588
+ color: #495057 !important;
589
+ margin-bottom: 8px !important;
590
+ font-size: 14px !important;
591
+ text-transform: uppercase !important;
592
+ letter-spacing: 0.5px !important;
593
+ }
594
+
595
+ .caption-text {
596
+ line-height: 1.7 !important;
597
+ color: #2C3E50 !important;
598
+ margin-bottom: 12px !important;
599
+ }
600
+
601
+ .hashtags-list {
602
+ display: flex !important;
603
+ flex-wrap: wrap !important;
604
+ gap: 8px !important;
605
+ margin-top: 12px !important;
606
+ }
607
+
608
+ .hashtag-item {
609
+ background: #E8F4F8 !important;
610
+ color: #2980B9 !important;
611
+ padding: 6px 12px !important;
612
+ border-radius: 8px !important;
613
+ font-size: 13px !important;
614
+ font-weight: 500 !important;
615
+ }
616
+
617
+ /* Progress Display Styles */
618
+ .progress-container {
619
+ padding: 24px 32px !important;
620
+ background: linear-gradient(135deg, #F0F3F7 0%, #E8EDF3 100%) !important;
621
+ border-radius: 16px !important;
622
+ margin: 24px 0 !important;
623
+ }
624
+
625
+ .progress-bar-wrapper {
626
+ height: 12px !important;
627
+ background: #DFE4EA !important;
628
+ border-radius: 12px !important;
629
+ overflow: hidden !important;
630
+ margin: 16px 0 !important;
631
+ }
632
+
633
+ .progress-bar-fill {
634
+ height: 100% !important;
635
+ background: linear-gradient(90deg, #3498DB 0%, #2ECC71 100%) !important;
636
+ border-radius: 12px !important;
637
+ transition: width 0.4s ease !important;
638
+ }
639
+
640
+ .progress-text {
641
+ font-size: 15px !important;
642
+ color: #495057 !important;
643
+ text-align: center !important;
644
+ font-weight: 500 !important;
645
+ }
646
+
647
+ .progress-stats {
648
+ display: flex !important;
649
+ justify-content: space-between !important;
650
+ margin-top: 12px !important;
651
+ font-size: 14px !important;
652
+ color: #6C757D !important;
653
+ }
654
+
655
+ /* Export Panel Styles */
656
+ .export-panel {
657
+ padding: 24px !important;
658
+ background: linear-gradient(135deg, #F8F9FA 0%, #ECEFF3 100%) !important;
659
+ border-radius: 16px !important;
660
+ margin: 32px 0 !important;
661
+ border: 1px solid #DEE2E6 !important;
662
+ }
663
+
664
+ .export-panel-title {
665
+ font-size: 18px !important;
666
+ font-weight: 700 !important;
667
+ color: #2C3E50 !important;
668
+ margin-bottom: 16px !important;
669
+ }
670
+
671
+ .export-buttons-row {
672
+ display: flex !important;
673
+ gap: 16px !important;
674
+ flex-wrap: wrap !important;
675
+ }
676
+
677
+ .export-button {
678
+ padding: 12px 24px !important;
679
+ background: linear-gradient(135deg, #3498DB 0%, #2980B9 100%) !important;
680
+ color: white !important;
681
+ border: none !important;
682
+ border-radius: 12px !important;
683
+ cursor: pointer !important;
684
+ font-size: 15px !important;
685
+ font-weight: 600 !important;
686
+ transition: all 0.3s ease !important;
687
+ display: flex !important;
688
+ align-items: center !important;
689
+ gap: 8px !important;
690
+ }
691
+
692
+ .export-button:hover {
693
+ background: linear-gradient(135deg, #2980B9 0%, #21618C 100%) !important;
694
+ transform: translateY(-2px) !important;
695
+ box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3) !important;
696
+ }
697
+
698
+ .export-button-icon {
699
+ font-size: 18px !important;
700
+ }
701
+
702
+ /* Batch Summary Card */
703
+ .batch-summary-card {
704
+ background: linear-gradient(135deg, #E8F8F5 0%, #D5F4E6 100%) !important;
705
+ border-left: 4px solid #27AE60 !important;
706
+ border-radius: 16px !important;
707
+ padding: 24px 32px !important;
708
+ margin: 24px 0 !important;
709
+ }
710
+
711
+ .summary-title {
712
+ font-size: 20px !important;
713
+ font-weight: 700 !important;
714
+ color: #27AE60 !important;
715
+ margin-bottom: 16px !important;
716
+ }
717
+
718
+ .summary-stats {
719
+ display: grid !important;
720
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important;
721
+ gap: 16px !important;
722
+ }
723
+
724
+ .stat-item {
725
+ text-align: center !important;
726
+ padding: 12px !important;
727
+ }
728
+
729
+ .stat-value {
730
+ font-size: 32px !important;
731
+ font-weight: 700 !important;
732
+ color: #2C3E50 !important;
733
+ }
734
+
735
+ .stat-label {
736
+ font-size: 14px !important;
737
+ color: #6C757D !important;
738
+ margin-top: 4px !important;
739
+ text-transform: uppercase !important;
740
+ letter-spacing: 0.5px !important;
741
+ }
742
+
743
+ /* Error Display */
744
+ .error-card-content {
745
+ background: #FADBD8 !important;
746
+ border: 2px solid #E74C3C !important;
747
+ border-radius: 12px !important;
748
+ padding: 16px !important;
749
+ color: #C0392B !important;
750
+ }
751
+
752
+ .error-title {
753
+ font-weight: 700 !important;
754
+ font-size: 16px !important;
755
+ margin-bottom: 8px !important;
756
+ }
757
+
758
+ .error-message {
759
+ font-size: 14px !important;
760
+ line-height: 1.5 !important;
761
+ }
762
+ """
763
+
764
+
765
+ print("βœ“ Style class defined")
ui_manager.py CHANGED
@@ -1,527 +1,13 @@
1
  import gradio as gr
2
  from typing import Dict, List
 
3
 
4
  class UIManager:
5
  """Manages all UI components and styling for Pixcribe"""
6
 
7
  def __init__(self):
8
- self.custom_css = self._get_custom_css()
9
-
10
- def _get_custom_css(self) -> str:
11
- """Return complete CSS styling - Elegant light design"""
12
- return """
13
- /* ==================== Global Reset & Base ==================== */
14
- * {
15
- margin: 0;
16
- padding: 0;
17
- box-sizing: border-box;
18
- }
19
-
20
- .gradio-container {
21
- background: linear-gradient(135deg, #F8F9FA 0%, #E9ECEF 100%) !important;
22
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif !important;
23
- padding: 0 !important;
24
- max-width: 100% !important;
25
- min-height: 100vh !important;
26
- }
27
-
28
- /* Main content wrapper - Generous padding to prevent edge clipping */
29
- .contain {
30
- max-width: 1600px !important;
31
- margin: 0 auto !important;
32
- padding: 64px 96px 96px 96px !important;
33
- }
34
-
35
- /* ==================== Header ==================== */
36
- .app-header {
37
- text-align: center;
38
- margin-bottom: 72px;
39
- animation: fadeInDown 0.8s ease-out;
40
- padding: 0 32px;
41
- }
42
-
43
- @keyframes fadeInDown {
44
- from {
45
- opacity: 0;
46
- transform: translateY(-30px);
47
- }
48
- to {
49
- opacity: 1;
50
- transform: translateY(0);
51
- }
52
- }
53
-
54
- .app-title {
55
- font-size: 72px;
56
- font-weight: 800;
57
- background: linear-gradient(135deg, #2C3E50 0%, #34495E 100%);
58
- -webkit-background-clip: text;
59
- -webkit-text-fill-color: transparent;
60
- background-clip: text;
61
- margin-bottom: 24px;
62
- letter-spacing: -0.05em;
63
- line-height: 1.1;
64
- }
65
-
66
- .app-subtitle {
67
- font-size: 26px;
68
- font-weight: 400;
69
- color: #6C757D;
70
- margin-bottom: 0;
71
- letter-spacing: 0.01em;
72
- }
73
-
74
- /* ==================== Layout ==================== */
75
- .main-row {
76
- gap: 48px !important;
77
- margin-bottom: 48px !important;
78
- }
79
-
80
- /* Left column elegant container */
81
- .main-row > .column:first-child {
82
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(252, 253, 254, 0.6) 100%) !important;
83
- border-radius: 28px !important;
84
- padding: 40px !important;
85
- border: 1px solid rgba(52, 152, 219, 0.08) !important;
86
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
87
- }
88
-
89
- /* Right column elegant container */
90
- .main-row > .column:last-child {
91
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(252, 253, 254, 0.6) 100%) !important;
92
- border-radius: 28px !important;
93
- padding: 40px !important;
94
- border: 1px solid rgba(52, 152, 219, 0.08) !important;
95
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
96
- }
97
-
98
- /* ==================== Premium Cards - Light & Spacious ==================== */
99
- .upload-card {
100
- background: rgba(255, 255, 255, 0.95) !important;
101
- border-radius: 32px !important;
102
- box-shadow:
103
- 0 4px 16px rgba(0, 0, 0, 0.06),
104
- 0 2px 4px rgba(0, 0, 0, 0.03),
105
- 0 1px 2px rgba(0, 0, 0, 0.02) !important;
106
- border: 1px solid rgba(0, 0, 0, 0.05) !important;
107
- padding: 48px !important;
108
- margin-bottom: 32px !important;
109
- transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
110
- overflow: visible !important;
111
- }
112
-
113
- .results-card {
114
- background: transparent !important;
115
- border-radius: 0 !important;
116
- box-shadow: none !important;
117
- border: none !important;
118
- padding: 0 !important;
119
- margin-bottom: 32px !important;
120
- overflow: visible !important;
121
- }
122
-
123
- /* Caption Results Container - Elegant Design */
124
- .caption-results-container {
125
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.85) 0%, rgba(252, 253, 254, 0.7) 100%) !important;
126
- border-radius: 28px !important;
127
- padding: 44px !important;
128
- border: 1px solid rgba(52, 152, 219, 0.1) !important;
129
- box-shadow:
130
- 0 4px 20px rgba(0, 0, 0, 0.04),
131
- 0 2px 8px rgba(52, 152, 219, 0.03) !important;
132
- margin-bottom: 40px !important;
133
- overflow: visible !important;
134
- }
135
-
136
- .upload-card:hover {
137
- box-shadow:
138
- 0 8px 32px rgba(0, 0, 0, 0.10),
139
- 0 4px 8px rgba(0, 0, 0, 0.06) !important;
140
- transform: translateY(-6px);
141
- border-color: rgba(52, 152, 219, 0.3) !important;
142
- }
143
-
144
- /* ==================== Upload Area ==================== */
145
- .upload-area {
146
- border: 3px dashed rgba(52, 152, 219, 0.35) !important;
147
- border-radius: 28px !important;
148
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.03) 0%, rgba(52, 152, 219, 0.06) 100%) !important;
149
- padding: 96px 40px !important;
150
- text-align: center !important;
151
- transition: all 0.3s ease !important;
152
- min-height: 360px !important;
153
- }
154
-
155
- .upload-area:hover {
156
- border-color: #3498DB !important;
157
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.06) 0%, rgba(52, 152, 219, 0.12) 100%) !important;
158
- transform: scale(1.02);
159
- }
160
-
161
- /* ==================== Section Titles - Consistent Spacing ==================== */
162
- .section-title {
163
- font-size: 28px !important;
164
- font-weight: 700 !important;
165
- color: #2C3E50 !important;
166
- margin-bottom: 20px !important;
167
- letter-spacing: -0.02em !important;
168
- padding-bottom: 0 !important;
169
- border-bottom: none !important;
170
- text-align: left !important;
171
- margin-top: 0 !important;
172
- }
173
-
174
- .section-title-left {
175
- font-size: 28px !important;
176
- font-weight: 700 !important;
177
- color: #2C3E50 !important;
178
- margin-bottom: 20px !important;
179
- margin-top: 0 !important;
180
- letter-spacing: -0.02em !important;
181
- text-align: left !important;
182
- border-bottom: none !important;
183
- padding-bottom: 0 !important;
184
- }
185
-
186
- /* ==================== Form Elements - Generous Padding ==================== */
187
- .settings-row {
188
- gap: 24px !important;
189
- margin-bottom: 28px !important;
190
- }
191
-
192
- .radio-group {
193
- background: rgba(248, 249, 250, 0.5) !important;
194
- border-radius: 20px !important;
195
- padding: 24px 28px !important;
196
- border: none !important;
197
- margin-bottom: 24px !important;
198
- border: 1px solid rgba(0, 0, 0, 0.04) !important;
199
- }
200
-
201
- .radio-group:last-child {
202
- margin-bottom: 0 !important;
203
- }
204
-
205
- /* Inline radio groups for side-by-side layout */
206
- .radio-group-inline {
207
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.7) 0%, rgba(248, 249, 250, 0.5) 100%) !important;
208
- border-radius: 16px !important;
209
- padding: 20px !important;
210
- border: 1px solid rgba(52, 152, 219, 0.1) !important;
211
- margin-bottom: 0 !important;
212
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03) !important;
213
- transition: all 0.3s ease !important;
214
- }
215
-
216
- .radio-group-inline:hover {
217
- box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08) !important;
218
- border-color: rgba(52, 152, 219, 0.2) !important;
219
- }
220
-
221
- .radio-group label {
222
- color: #6C757D !important;
223
- font-weight: 600 !important;
224
- font-size: 14px !important;
225
- margin-bottom: 16px !important;
226
- letter-spacing: 0.08em !important;
227
- text-transform: uppercase !important;
228
- display: block !important;
229
- text-align: left !important;
230
- }
231
-
232
- /* Radio group title (the actual input label) */
233
- .radio-group > label:first-child {
234
- color: #2C3E50 !important;
235
- font-weight: 700 !important;
236
- font-size: 19px !important;
237
- margin-bottom: 16px !important;
238
- letter-spacing: -0.02em !important;
239
- text-transform: none !important;
240
- }
241
-
242
- /* Inline radio group title - BIGGER and BOLD */
243
- .radio-group-inline > label:first-child {
244
- color: #2C3E50 !important;
245
- font-weight: 700 !important;
246
- font-size: 18px !important;
247
- margin-bottom: 14px !important;
248
- letter-spacing: -0.01em !important;
249
- text-transform: none !important;
250
- display: block !important;
251
- }
252
-
253
- .radio-group input[type="radio"] {
254
- accent-color: #3498DB !important;
255
- width: 22px !important;
256
- height: 22px !important;
257
- margin-right: 14px !important;
258
- }
259
-
260
- /* Radio option labels */
261
- .radio-group > div > label {
262
- color: #495057 !important;
263
- font-weight: 500 !important;
264
- font-size: 17px !important;
265
- letter-spacing: -0.01em !important;
266
- text-transform: none !important;
267
- padding: 14px 20px !important;
268
- border-radius: 14px !important;
269
- transition: all 0.2s ease !important;
270
- cursor: pointer !important;
271
- display: flex !important;
272
- align-items: center !important;
273
- }
274
-
275
- /* Inline radio option labels - BIGGER */
276
- .radio-group-inline > div > label {
277
- color: #495057 !important;
278
- font-weight: 500 !important;
279
- font-size: 16px !important;
280
- letter-spacing: -0.01em !important;
281
- text-transform: none !important;
282
- padding: 12px 16px !important;
283
- border-radius: 10px !important;
284
- transition: all 0.2s ease !important;
285
- cursor: pointer !important;
286
- display: flex !important;
287
- align-items: center !important;
288
- background: rgba(255, 255, 255, 0.6) !important;
289
- margin-bottom: 8px !important;
290
- border: 1px solid rgba(0, 0, 0, 0.04) !important;
291
- }
292
-
293
- .radio-group > div > label:hover {
294
- background: rgba(52, 152, 219, 0.08) !important;
295
- }
296
-
297
- .radio-group-inline > div > label:hover {
298
- background: rgba(52, 152, 219, 0.12) !important;
299
- transform: translateX(4px);
300
- }
301
-
302
- /* ==================== Button ==================== */
303
- .generate-button {
304
- background: linear-gradient(135deg, #3498DB 0%, #2980B9 100%) !important;
305
- color: white !important;
306
- border: none !important;
307
- border-radius: 20px !important;
308
- padding: 24px 64px !important;
309
- font-size: 19px !important;
310
- font-weight: 700 !important;
311
- cursor: pointer !important;
312
- box-shadow:
313
- 0 6px 24px rgba(52, 152, 219, 0.35),
314
- 0 3px 6px rgba(52, 152, 219, 0.25) !important;
315
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
316
- letter-spacing: -0.02em !important;
317
- width: 100% !important;
318
- margin-top: 24px !important;
319
- }
320
-
321
- .generate-button:hover {
322
- transform: translateY(-6px) scale(1.02) !important;
323
- box-shadow:
324
- 0 16px 48px rgba(52, 152, 219, 0.45),
325
- 0 6px 12px rgba(52, 152, 219, 0.35) !important;
326
- }
327
-
328
- .generate-button:active {
329
- transform: translateY(-3px) scale(1.01) !important;
330
- }
331
-
332
- /* ==================== Caption Cards - Light & Elegant ==================== */
333
- .caption-card {
334
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 249, 250, 0.95) 100%);
335
- backdrop-filter: blur(20px);
336
- border: 1px solid rgba(0, 0, 0, 0.06);
337
- border-radius: 28px;
338
- padding: 32px 36px;
339
- margin-bottom: 28px;
340
- transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
341
- box-shadow:
342
- 0 4px 16px rgba(0, 0, 0, 0.05),
343
- 0 2px 4px rgba(0, 0, 0, 0.03);
344
- position: relative;
345
- }
346
-
347
- .caption-card:hover {
348
- box-shadow:
349
- 0 8px 32px rgba(0, 0, 0, 0.10),
350
- 0 4px 8px rgba(0, 0, 0, 0.06);
351
- transform: translateY(-6px);
352
- border-color: rgba(52, 152, 219, 0.3);
353
- }
354
-
355
- .caption-header {
356
- font-size: 15px;
357
- font-weight: 700;
358
- color: #6C757D;
359
- text-transform: uppercase;
360
- letter-spacing: 0.14em;
361
- margin-bottom: 20px;
362
- }
363
-
364
- .caption-text {
365
- font-size: 21px;
366
- font-weight: 400;
367
- color: #2C3E50;
368
- line-height: 1.8;
369
- margin-bottom: 24px;
370
- letter-spacing: -0.01em;
371
- }
372
-
373
- .caption-hashtags {
374
- font-size: 18px;
375
- font-weight: 600;
376
- color: #3498DB;
377
- margin-bottom: 0;
378
- word-wrap: break-word;
379
- line-height: 1.75;
380
- }
381
-
382
- /* Copy Button */
383
- .copy-button {
384
- position: absolute;
385
- top: 28px;
386
- right: 28px;
387
- background: rgba(52, 152, 219, 0.10);
388
- border: 1px solid rgba(52, 152, 219, 0.25);
389
- border-radius: 14px;
390
- padding: 12px 20px;
391
- font-size: 15px;
392
- font-weight: 600;
393
- color: #3498DB;
394
- cursor: pointer;
395
- transition: all 0.2s ease;
396
- display: flex;
397
- align-items: center;
398
- gap: 8px;
399
- }
400
-
401
- .copy-button:hover {
402
- background: rgba(52, 152, 219, 0.18);
403
- border-color: #3498DB;
404
- transform: translateY(-2px);
405
- box-shadow: 0 4px 12px rgba(52, 152, 219, 0.25);
406
- }
407
-
408
- .copy-button:active {
409
- transform: translateY(0);
410
- }
411
-
412
- .copy-button.copied {
413
- background: rgba(39, 174, 96, 0.15);
414
- border-color: #27AE60;
415
- color: #27AE60;
416
- }
417
-
418
- /* ==================== Footer ==================== */
419
- .app-footer {
420
- text-align: center;
421
- margin-top: 96px;
422
- padding-top: 64px;
423
- border-top: 3px solid rgba(0, 0, 0, 0.08);
424
- animation: fadeInUp 0.8s ease-out 0.3s backwards;
425
- }
426
-
427
- @keyframes fadeInUp {
428
- from {
429
- opacity: 0;
430
- transform: translateY(30px);
431
- }
432
- to {
433
- opacity: 1;
434
- transform: translateY(0);
435
- }
436
- }
437
-
438
- .footer-text {
439
- font-size: 17px;
440
- color: #6C757D;
441
- line-height: 2.0;
442
- letter-spacing: -0.01em;
443
- font-weight: 500;
444
- }
445
-
446
- .footer-models {
447
- font-size: 15px;
448
- color: #ADB5BD;
449
- margin-top: 20px;
450
- font-weight: 600;
451
- letter-spacing: 0.03em;
452
- }
453
-
454
- /* ==================== Image Display ==================== */
455
- .image-container {
456
- border-radius: 28px !important;
457
- overflow: hidden !important;
458
- box-shadow:
459
- 0 6px 24px rgba(0, 0, 0, 0.10),
460
- 0 3px 6px rgba(0, 0, 0, 0.06) !important;
461
- }
462
-
463
- .image-container img {
464
- border-radius: 28px !important;
465
- box-shadow:
466
- 0 6px 24px rgba(0, 0, 0, 0.12),
467
- 0 3px 6px rgba(0, 0, 0, 0.08) !important;
468
- }
469
-
470
- /* ==================== Responsive Design ==================== */
471
- @media (max-width: 768px) {
472
- .contain {
473
- padding: 48px 32px 64px 32px !important;
474
- }
475
-
476
- .app-title {
477
- font-size: 52px;
478
- }
479
-
480
- .app-subtitle {
481
- font-size: 20px;
482
- }
483
-
484
- .upload-card, .options-card, .results-card {
485
- padding: 40px !important;
486
- }
487
-
488
- .upload-area {
489
- padding: 64px 32px !important;
490
- min-height: 280px !important;
491
- }
492
-
493
- .caption-card {
494
- padding: 28px;
495
- }
496
-
497
- .section-title {
498
- font-size: 30px !important;
499
- }
500
-
501
- .copy-button {
502
- top: 20px;
503
- right: 20px;
504
- padding: 10px 16px;
505
- font-size: 14px;
506
- }
507
- }
508
-
509
- /* ==================== Loading Animation ==================== */
510
- @keyframes shimmer {
511
- 0% {
512
- background-position: -1000px 0;
513
- }
514
- 100% {
515
- background-position: 1000px 0;
516
- }
517
- }
518
-
519
- .loading {
520
- animation: shimmer 2s infinite;
521
- background: linear-gradient(to right, #f8f9fa 4%, #e9ecef 25%, #f8f9fa 36%);
522
- background-size: 1000px 100%;
523
- }
524
- """
525
 
526
  def create_header(self):
527
  """Create application header"""
@@ -678,4 +164,215 @@ class UIManager:
678
 
679
  return captions_html
680
 
681
- print("βœ“ UIManager defined")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  from typing import Dict, List
3
+ from style import Style
4
 
5
  class UIManager:
6
  """Manages all UI components and styling for Pixcribe"""
7
 
8
  def __init__(self):
9
+ # Use centralized Style class for all CSS
10
+ self.custom_css = Style.get_all_css()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  def create_header(self):
13
  """Create application header"""
 
164
 
165
  return captions_html
166
 
167
+ def create_batch_progress_html(self, current: int, total: int, percent: float, estimated_remaining: int) -> str:
168
+ """
169
+ Create HTML for batch processing progress display.
170
+
171
+ Args:
172
+ current: Number of images completed
173
+ total: Total number of images
174
+ percent: Completion percentage (0-100)
175
+ estimated_remaining: Estimated remaining time in seconds
176
+
177
+ Returns:
178
+ Formatted HTML string with progress bar and information
179
+ """
180
+ # Convert remaining time to minutes and seconds
181
+ minutes = int(estimated_remaining // 60)
182
+ seconds = int(estimated_remaining % 60)
183
+ time_str = f"{minutes}m {seconds}s" if minutes > 0 else f"{seconds}s"
184
+
185
+ html = f"""
186
+ <div class="progress-container">
187
+ <div class="progress-bar-wrapper">
188
+ <div class="progress-bar-fill" style="width: {percent}%;"></div>
189
+ </div>
190
+ <div class="progress-text">
191
+ Processing image {current} of {total}
192
+ </div>
193
+ <div class="progress-stats">
194
+ <span>Progress: {percent:.1f}%</span>
195
+ {f'<span>Estimated time remaining: {time_str}</span>' if estimated_remaining > 0 else ''}
196
+ </div>
197
+ </div>
198
+ """
199
+ return html
200
+
201
+ def format_batch_results_html(self, batch_results: Dict) -> str:
202
+ """
203
+ Format batch processing results as HTML.
204
+
205
+ Args:
206
+ batch_results: Dictionary containing batch processing results
207
+
208
+ Returns:
209
+ Formatted HTML string with all batch results
210
+ """
211
+ results = batch_results.get('results', {})
212
+
213
+ if not results:
214
+ return "<p style='color: #6C757D; padding: 24px; text-align: center;'>No results to display</p>"
215
+
216
+ # Build summary card
217
+ total_processed = batch_results.get('total_processed', 0)
218
+ total_success = batch_results.get('total_success', 0)
219
+ total_failed = batch_results.get('total_failed', 0)
220
+ total_time = batch_results.get('total_time', 0)
221
+ avg_time = batch_results.get('average_time_per_image', 0)
222
+
223
+ html_parts = []
224
+
225
+ # Summary card
226
+ html_parts.append(f"""
227
+ <div class="batch-summary-card">
228
+ <div class="summary-title">βœ“ Batch Processing Complete</div>
229
+ <div class="summary-stats">
230
+ <div class="stat-item">
231
+ <div class="stat-value">{total_processed}</div>
232
+ <div class="stat-label">Total Processed</div>
233
+ </div>
234
+ <div class="stat-item">
235
+ <div class="stat-value" style="color: #27AE60;">{total_success}</div>
236
+ <div class="stat-label">Successful</div>
237
+ </div>
238
+ <div class="stat-item">
239
+ <div class="stat-value" style="color: #E74C3C;">{total_failed}</div>
240
+ <div class="stat-label">Failed</div>
241
+ </div>
242
+ <div class="stat-item">
243
+ <div class="stat-value">{total_time:.1f}s</div>
244
+ <div class="stat-label">Total Time</div>
245
+ </div>
246
+ <div class="stat-item">
247
+ <div class="stat-value">{avg_time:.1f}s</div>
248
+ <div class="stat-label">Avg Per Image</div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ """)
253
+
254
+ # Start results container
255
+ html_parts.append('<div class="batch-results-container">')
256
+
257
+ # Process each image result
258
+ for img_idx in sorted(results.keys()):
259
+ img_result = results[img_idx]
260
+ status = img_result.get('status', 'unknown')
261
+
262
+ # Determine status icon and color
263
+ if status == 'success':
264
+ status_icon = 'βœ“'
265
+ status_color = '#27AE60'
266
+ else:
267
+ status_icon = 'βœ—'
268
+ status_color = '#E74C3C'
269
+
270
+ # Build card
271
+ card_html = f"""
272
+ <details class="batch-result-card" open>
273
+ <summary class="card-header">
274
+ <span class="card-status" style="color: {status_color};">{status_icon}</span>
275
+ <span class="card-title">Image {img_idx}</span>
276
+ </summary>
277
+ <div class="card-content">
278
+ """
279
+
280
+ if status == 'success':
281
+ result_data = img_result.get('result', {})
282
+ captions = result_data.get('captions', [])
283
+
284
+ # Display captions
285
+ for cap in captions:
286
+ tone = cap.get('tone', 'Unknown').upper()
287
+ caption_text = cap.get('caption', '')
288
+ hashtags = cap.get('hashtags', [])
289
+
290
+ card_html += f"""
291
+ <div class="caption-section">
292
+ <div class="caption-label">{tone} Style</div>
293
+ <div class="caption-text">{caption_text}</div>
294
+ <div class="hashtags-list">
295
+ {''.join([f'<span class="hashtag-item">#{tag}</span>' for tag in hashtags])}
296
+ </div>
297
+ </div>
298
+ """
299
+
300
+ # Display detected objects
301
+ detections = result_data.get('detections', [])
302
+ if detections:
303
+ object_names = [det.get('class_name', 'unknown') for det in detections]
304
+ card_html += f"""
305
+ <div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #E9ECEF;">
306
+ <strong style="color: #495057;">Detected Objects:</strong>
307
+ <span style="color: #6C757D;"> {', '.join(object_names)}</span>
308
+ </div>
309
+ """
310
+
311
+ # Display detected brands
312
+ brands = result_data.get('brands', [])
313
+ if brands:
314
+ brand_names = [
315
+ brand[0] if isinstance(brand, tuple) else brand
316
+ for brand in brands
317
+ ]
318
+ card_html += f"""
319
+ <div style="margin-top: 12px;">
320
+ <strong style="color: #495057;">Detected Brands:</strong>
321
+ <span style="color: #6C757D;"> {', '.join(brand_names)}</span>
322
+ </div>
323
+ """
324
+
325
+ else:
326
+ # Display error information
327
+ error = img_result.get('error', {})
328
+ error_type = error.get('type', 'Unknown Error')
329
+ error_message = error.get('message', 'No error message available')
330
+
331
+ card_html += f"""
332
+ <div class="error-card-content">
333
+ <div class="error-title">{error_type}</div>
334
+ <div class="error-message">{error_message}</div>
335
+ </div>
336
+ """
337
+
338
+ card_html += """
339
+ </div>
340
+ </details>
341
+ """
342
+
343
+ html_parts.append(card_html)
344
+
345
+ # Close results container
346
+ html_parts.append('</div>')
347
+
348
+ return ''.join(html_parts)
349
+
350
+ def create_export_panel_html(self) -> str:
351
+ """
352
+ Create HTML for export panel with download buttons.
353
+
354
+ Returns:
355
+ Formatted HTML string for export panel
356
+ """
357
+ return """
358
+ <div class="export-panel">
359
+ <div class="export-panel-title">πŸ“₯ Export Batch Results</div>
360
+ <div class="export-buttons-row">
361
+ <button class="export-button" id="export-json-btn">
362
+ <span class="export-button-icon">πŸ“„</span>
363
+ <span>Download JSON</span>
364
+ </button>
365
+ <button class="export-button" id="export-csv-btn">
366
+ <span class="export-button-icon">πŸ“Š</span>
367
+ <span>Download CSV</span>
368
+ </button>
369
+ <button class="export-button" id="export-zip-btn">
370
+ <span class="export-button-icon">πŸ“¦</span>
371
+ <span>Download ZIP</span>
372
+ </button>
373
+ </div>
374
+ </div>
375
+ """
376
+
377
+
378
+ print("βœ“ UIManager (V5 with Batch Processing Support) defined")