|
@@ -10,6 +10,7 @@ import plotly.express as px
|
|
|
import plotly.graph_objects as go
|
|
import plotly.graph_objects as go
|
|
|
import json
|
|
import json
|
|
|
import os
|
|
import os
|
|
|
|
|
+import zipfile
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
from fpdf import FPDF
|
|
from fpdf import FPDF
|
|
|
|
|
|
|
@@ -463,6 +464,71 @@ def generate_batch_report(data, uploaded_files_map=None):
|
|
|
|
|
|
|
|
return bytes(pdf.output(dest='S'))
|
|
return bytes(pdf.output(dest='S'))
|
|
|
|
|
|
|
|
|
|
+def generate_batch_zip(data, uploaded_files, pdf_bytes=None):
|
|
|
|
|
+ """Generates a complete ZIP bundle containing raw, annotated images and metadata."""
|
|
|
|
|
+ zip_buffer = io.BytesIO()
|
|
|
|
|
+
|
|
|
|
|
+ # Map uploaded files for easy lookup
|
|
|
|
|
+ files_map = {f.name: f.getvalue() for f in uploaded_files}
|
|
|
|
|
+
|
|
|
|
|
+ with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
|
|
|
|
+ # 1. Save Raw Images
|
|
|
|
|
+ for fname, content in files_map.items():
|
|
|
|
|
+ zip_file.writestr(f"raw/{fname}", content)
|
|
|
|
|
+
|
|
|
|
|
+ # 2. Save Annotated Images
|
|
|
|
|
+ # Group detections by filename
|
|
|
|
|
+ results_by_file = {}
|
|
|
|
|
+ for res in data.get('detailed_results', []):
|
|
|
|
|
+ fname = res['filename']
|
|
|
|
|
+ if fname not in results_by_file:
|
|
|
|
|
+ results_by_file[fname] = []
|
|
|
|
|
+ results_by_file[fname].append(res['detection'])
|
|
|
|
|
+
|
|
|
|
|
+ for fname, detections in results_by_file.items():
|
|
|
|
|
+ # The filename in detailed_results might be the unique one from backend (e.g. "uuid_name.jpg")
|
|
|
|
|
+ # We need to find the matching original file based on the suffix or name
|
|
|
|
|
+ original_fname = None
|
|
|
|
|
+ for ofname in files_map.keys():
|
|
|
|
|
+ if ofname in fname: # Basic match
|
|
|
|
|
+ original_fname = ofname
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if original_fname:
|
|
|
|
|
+ img = Image.open(io.BytesIO(files_map[original_fname])).convert("RGB")
|
|
|
|
|
+ img_annotated = annotate_image(img, detections)
|
|
|
|
|
+
|
|
|
|
|
+ # Save annotated to bytes
|
|
|
|
|
+ img_byte_arr = io.BytesIO()
|
|
|
|
|
+ img_annotated.save(img_byte_arr, format='JPEG')
|
|
|
|
|
+ zip_file.writestr(f"annotated/annotated_{original_fname}", img_byte_arr.getvalue())
|
|
|
|
|
+
|
|
|
|
|
+ # 3. Save Summary Metadata (CSV)
|
|
|
|
|
+ summary_rows = []
|
|
|
|
|
+ for res in data.get('detailed_results', []):
|
|
|
|
|
+ det = res['detection']
|
|
|
|
|
+ summary_rows.append({
|
|
|
|
|
+ "filename": res['filename'],
|
|
|
|
|
+ "bunch_id": det.get('bunch_id'),
|
|
|
|
|
+ "class": det.get('class'),
|
|
|
|
|
+ "confidence": det.get('confidence'),
|
|
|
|
|
+ "is_health_alert": det.get('is_health_alert')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if summary_rows:
|
|
|
|
|
+ df = pd.DataFrame(summary_rows)
|
|
|
|
|
+ csv_data = df.to_csv(index=False)
|
|
|
|
|
+ zip_file.writestr("detection_summary.csv", csv_data)
|
|
|
|
|
+
|
|
|
|
|
+ # 4. Save Backend Manifest
|
|
|
|
|
+ if 'manifest_preview' in data:
|
|
|
|
|
+ zip_file.writestr("manifest.json", json.dumps(data['manifest_preview'], indent=4))
|
|
|
|
|
+
|
|
|
|
|
+ # 5. Save PDF Report if provided
|
|
|
|
|
+ if pdf_bytes:
|
|
|
|
|
+ zip_file.writestr(f"PalmOil_ExecutiveReport_{data.get('batch_id', 'Batch')}.pdf", pdf_bytes)
|
|
|
|
|
+
|
|
|
|
|
+ return zip_buffer.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Tabs ---
|
|
# --- Tabs ---
|
|
@@ -782,6 +848,16 @@ with tab2:
|
|
|
width='stretch'
|
|
width='stretch'
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ # --- Full Batch ZIP ---
|
|
|
|
|
+ with st.spinner("📦 Preparing Batch Bundle..."):
|
|
|
|
|
+ zip_bytes = generate_batch_zip(res_data, uploaded_files, pdf_bytes)
|
|
|
|
|
+ st.download_button(
|
|
|
|
|
+ label="📦 Download Full Batch Bundle (ZIP)",
|
|
|
|
|
+ data=zip_bytes,
|
|
|
|
|
+ file_name=f"PalmOil_BatchBundle_{res_data.get('batch_id', 'Bundle')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip",
|
|
|
|
|
+ mime="application/zip",
|
|
|
|
|
+ width='stretch'
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
if st.button("Clear Results & Start New Batch", width='stretch'):
|
|
if st.button("Clear Results & Start New Batch", width='stretch'):
|
|
|
st.session_state.last_batch_results = None
|
|
st.session_state.last_batch_results = None
|