abdullahmubeen10 commited on
Commit
bd3d0bb
·
verified ·
1 Parent(s): 59325dd

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +512 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,514 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ from ultralytics import YOLO
3
+ import cv2
4
+ import tempfile
5
+ import os
6
+ import base64
7
+ import numpy as np
8
+ import time
9
+ import csv
10
+ import zipfile
11
+ from io import BytesIO, StringIO
12
+ from datetime import datetime
13
+
14
+ st.set_page_config(page_title="Flood Level Detection", layout="wide")
15
+
16
+ st.markdown("""
17
+ <style>
18
+ body {
19
+ background: linear-gradient(to right, #e3f2fd 0%, #ffffff 25%, #ffffff 75%, #e3f2fd 100%);
20
+ }
21
+ .stApp {
22
+ background: linear-gradient(to right, #e3f2fd 0%, #ffffff 25%, #ffffff 75%, #e3f2fd 100%);
23
+ }
24
+ </style>
25
+ """, unsafe_allow_html=True)
26
+
27
+ st.markdown("""
28
+ <style>
29
+ .block-container {padding-top: 2rem; padding-bottom: 8rem;}
30
+ .main-title {font-size:34px;font-weight:bold;text-align:center;color:#004080;margin-top:40px;margin-bottom:35px;}
31
+ .settings-box {border:2px solid #363738;border-radius:10px;padding:15px;background-color:#E8E8E8;}
32
+ .yellow-header {background-color:#363738;color:white;font-weight:bold;text-align:center;padding:6px;border-radius:6px;margin-bottom:10px;}
33
+ .centered-status {display:flex;justify-content:center;align-items:center;margin-top:15px;}
34
+ .status-text {text-align:center;font-size:16px;font-weight:bold;}
35
+ .progress-container {display:flex;justify-content:center;align-items:center;width:100%;margin-top:15px;}
36
+ .progress-bar-wrapper {width:60%;}
37
+ div[data-testid="stButton"] > button {
38
+ background: linear-gradient(to right, #e3f2fd 0%, #ffffff 25%, #ffffff 75%, #e3f2fd 100%) !important;
39
+ color: #004080 !important;
40
+ font-weight: bold !important;
41
+ border: 2px solid #004080 !important;
42
+ border-radius: 8px !important;
43
+ padding: 10px 20px !important;
44
+ transition: all 0.3s ease !important;
45
+ }
46
+ div[data-testid="stButton"] > button:hover {
47
+ background: linear-gradient(to right, #bbdefb 0%, #e3f2fd 25%, #e3f2fd 75%, #bbdefb 100%) !important;
48
+ border-color: #0d47a1 !important;
49
+ box-shadow: 0 4px 12px rgba(13, 71, 161, 0.2) !important;
50
+ }
51
+ div[data-testid="stDownloadButton"] > button {
52
+ background: linear-gradient(to right, #e3f2fd 0%, #ffffff 25%, #ffffff 75%, #e3f2fd 100%) !important;
53
+ color: #004080 !important;
54
+ font-weight: bold !important;
55
+ border: 2px solid #004080 !important;
56
+ border-radius: 8px !important;
57
+ padding: 10px 20px !important;
58
+ transition: all 0.3s ease !important;
59
+ }
60
+ div[data-testid="stDownloadButton"] > button:hover {
61
+ background: linear-gradient(to right, #bbdefb 0%, #e3f2fd 25%, #e3f2fd 75%, #bbdefb 100%) !important;
62
+ border-color: #0d47a1 !important;
63
+ box-shadow: 0 4px 12px rgba(13, 71, 161, 0.2) !important;
64
+ }
65
+ div[data-testid="stNumberInput"] label {
66
+ font-size: 20px !important;
67
+ font-weight: bold !important;
68
+ color: #004080 !important;
69
+ }
70
+ div[data-testid="stNumberInput"] input {
71
+ font-size: 24px !important;
72
+ font-weight: bold !important;
73
+ height: 50px !important;
74
+ }
75
+ div[data-testid="stRadio"] label {
76
+ font-size: 18px !important;
77
+ font-weight: bold !important;
78
+ color: #004080 !important;
79
+ }
80
+ </style>
81
+ """, unsafe_allow_html=True)
82
+
83
+ st.markdown("""
84
+ <style>
85
+ .header-container {
86
+ display: flex;
87
+ justify-content: center;
88
+ align-items: center;
89
+ position: relative;
90
+ margin-top: 30px;
91
+ margin-bottom: 25px;
92
+ background: transparent;
93
+ padding: 0px 0;
94
+ border: none;
95
+ }
96
+ .header-title {
97
+ font-size: 36px;
98
+ font-weight: bold;
99
+ color: #004080;
100
+ text-align: center;
101
+ }
102
+ .header-logo {
103
+ position: absolute;
104
+ right: 80px;
105
+ top: 50%;
106
+ transform: translateY(-50%);
107
+ }
108
+ .header-logo img {
109
+ height: 70px;
110
+ width: auto;
111
+ border-radius: 8px;
112
+ }
113
+ </style>
114
+ """, unsafe_allow_html=True)
115
+
116
+ # --- Load logo ---
117
+ with open("assets/logo3u.png", "rb") as img_file:
118
+ logo_data = base64.b64encode(img_file.read()).decode()
119
+
120
+ # --- Header layout ---
121
+ st.markdown(f"""
122
+ <style>
123
+ .header-logo img {{
124
+ height: 120px; /* increase this value to make the image bigger */
125
+ }}
126
+ </style>
127
+
128
+ <div class="header-container">
129
+ <div class="header-title">FLOOD-DEPTH-ML</div>
130
+ <div class="header-logo">
131
+ <img src="data:image/png;base64,{logo_data}">
132
+ </div>
133
+ </div>
134
+ """, unsafe_allow_html=True)
135
+
136
+ # ---------------------------
137
+ # LOAD MODEL
138
+ # ---------------------------
139
+ MODEL_PATH = "best_car.pt"
140
+ if not os.path.exists(MODEL_PATH):
141
+ st.error("❌ Model file not found! Please place your model as 'best_car.pt'")
142
+ st.stop()
143
+ model = YOLO(MODEL_PATH)
144
+
145
+ # ---------------------------
146
+ # LAYOUT
147
+ # ---------------------------
148
+ col1, col2, col3 = st.columns([1.2, 2.6, 1.2])
149
+
150
+ # ---------------------------
151
+ # LEFT PANEL
152
+ # ---------------------------
153
+ with col1:
154
+ st.subheader("📥 Input Source")
155
+ input_type = st.radio("Choose Input Type", ["Upload File", "Use Webcam"])
156
+ uploaded_file = None
157
+ if input_type == "Upload File":
158
+ uploaded_file = st.file_uploader("Upload Image or Video", type=["jpg", "png", "mp4", "avi"])
159
+
160
+ analyze_btn = st.button("🔍 Analyze", width='stretch')
161
+ download_area = st.empty()
162
+
163
+ # ---------------------------
164
+ # CENTER PANEL
165
+ # ---------------------------
166
+ with col2:
167
+ st.subheader("🎥 Detection Display")
168
+ display_area = st.empty()
169
+ controls_area = st.container()
170
+ status_area = st.empty()
171
+
172
+ # ---------------------------
173
+ # RIGHT PANEL (settings)
174
+ # ---------------------------
175
+ with col3:
176
+ st.markdown("<div class='settings-box'>", unsafe_allow_html=True)
177
+ st.markdown("<div class='yellow-header'>⚙️ SETTINGS</div>", unsafe_allow_html=True)
178
+
179
+ default_skip = 1
180
+ try:
181
+ raw_input = st.number_input(
182
+ "⏱️ Analyze every Nth frame (1 = all, 10 = skip 10 frames)",
183
+ min_value=1,
184
+ max_value=60,
185
+ value=default_skip,
186
+ step=1,
187
+ key="frame_skip"
188
+ )
189
+ fps_input = int(raw_input)
190
+ except Exception:
191
+ fps_input = default_skip
192
+
193
+ st.markdown("### 🌊 Flood Levels")
194
+ level_placeholders = {f"Level {i}": st.empty() for i in range(5)}
195
+ for level in level_placeholders:
196
+ level_placeholders[level].markdown(
197
+ f"<div style='text-align:left;font-size:20px;font-weight:bold;margin:4px;'>{level}: 0</div>",
198
+ unsafe_allow_html=True
199
+ )
200
+
201
+ st.markdown("### 🧾 Labelling Criteria")
202
+ if os.path.exists("assets/scheme.png"):
203
+ st.image("assets/scheme.png", caption="Reference Criteria", width='stretch')
204
+ st.markdown("</div>", unsafe_allow_html=True)
205
+
206
+
207
+ # ---------------------------
208
+ # FOOTER
209
+ # ---------------------------
210
+ def show_footer_logos():
211
+ logo1 = "assets/logo1.png"
212
+ if not (os.path.exists(logo1)):
213
+ return
214
+ with open(logo1, "rb") as f:
215
+ a = base64.b64encode(f.read()).decode()
216
+ st.markdown(f"""
217
+ <div style="position:fixed;left:0;bottom:0;width:100%;background-color:white;display:flex;justify-content:center;align-items:center;gap:10px;padding:5px 0;box-shadow:0 -2px 8px rgba(0,0,0,0.15);z-index:999;">
218
+ <img src="data:image/png;base64,{a}" style="height:70px;">
219
+ </div>
220
+ """, unsafe_allow_html=True)
221
+
222
+
223
+ show_footer_logos()
224
+
225
+
226
+ # ---------------------------
227
+ # HELPERS
228
+ # ---------------------------
229
+ def update_levels(counts, high_danger=False):
230
+ for i, key in enumerate(level_placeholders.keys()):
231
+ color = "#FF0000" if high_danger and key in ["Level 3", "Level 4"] else "#004080"
232
+ level_placeholders[key].markdown(
233
+ f"<div style='text-align:left;font-size:18px;font-weight:bold;margin:4px;color:{color};'>{key}: {counts.get(key, 0)}</div>",
234
+ unsafe_allow_html=True
235
+ )
236
+
237
+ if "pause_loop_counter" not in st.session_state:
238
+ st.session_state.pause_loop_counter = 0
239
+
240
+ def detect_and_count(frame):
241
+ if frame is None:
242
+ return None, {f"Level {i}": 0 for i in range(5)}, False
243
+
244
+ results = model(frame)
245
+ annotated = results[0].plot()
246
+ level_counts = {f"Level {i}": 0 for i in range(5)}
247
+ high_danger = False
248
+
249
+ try:
250
+ for box in results[0].boxes:
251
+ cls = int(box.cls[0])
252
+ key = f"Level {cls}"
253
+ if key in level_counts:
254
+ level_counts[key] += 1
255
+ if cls in [3, 4]:
256
+ high_danger = True
257
+ except Exception:
258
+ pass
259
+ return annotated, level_counts, high_danger
260
+
261
+
262
+ # ---------------------------
263
+ # SESSION STATE INIT
264
+ # ---------------------------
265
+ if "processing" not in st.session_state:
266
+ st.session_state.processing = False
267
+ if "tmp_video_path" not in st.session_state:
268
+ st.session_state.tmp_video_path = None
269
+ if "paused" not in st.session_state:
270
+ st.session_state.paused = False
271
+ if "zip_ready" not in st.session_state:
272
+ st.session_state.zip_ready = False
273
+ if "zip_data" not in st.session_state:
274
+ st.session_state.zip_data = None
275
+ if "zip_filename" not in st.session_state:
276
+ st.session_state.zip_filename = None
277
+ if "webcam_active" not in st.session_state:
278
+ st.session_state.webcam_active = False
279
+ if "webcam_cap" not in st.session_state:
280
+ st.session_state.webcam_cap = None
281
+ if "webcam_frames" not in st.session_state:
282
+ st.session_state.webcam_frames = []
283
+ if "last_webcam_frame" not in st.session_state:
284
+ st.session_state.last_webcam_frame = None
285
+ if "last_webcam_counts" not in st.session_state:
286
+ st.session_state.last_webcam_counts = {}
287
+ if "report_log" not in st.session_state:
288
+ st.session_state.report_log = []
289
+ if "webcam_frame_counter" not in st.session_state:
290
+ st.session_state.webcam_frame_counter = 0
291
+ if "last_frame_bytes" not in st.session_state:
292
+ st.session_state.last_frame_bytes = None
293
+
294
+ # ---------------------------
295
+ # UPLOAD FILE PROCESSING
296
+ # ---------------------------
297
+ if input_type == "Upload File" and analyze_btn and uploaded_file:
298
+ ext = uploaded_file.name.split('.')[-1].lower()
299
+
300
+ # Save file to temp
301
+ tfile = tempfile.NamedTemporaryFile(delete=False, suffix="." + ext)
302
+ tfile.write(uploaded_file.read())
303
+ tfile.close()
304
+ st.session_state.tmp_video_path = tfile.name
305
+ st.session_state.processing = True
306
+ st.session_state.paused = False
307
+ st.session_state.zip_ready = False
308
+ st.session_state.report_log = []
309
+
310
+ # Process uploaded file
311
+ if st.session_state.processing and st.session_state.tmp_video_path:
312
+ path = st.session_state.tmp_video_path
313
+ ext = path.split('.')[-1].lower()
314
+
315
+ # IMAGE PROCESSING
316
+ if ext in ["jpg", "jpeg", "png"]:
317
+ frame = cv2.imread(path)
318
+ if frame is None:
319
+ st.error("Could not read image file")
320
+ else:
321
+ annotated, counts, high_danger = detect_and_count(frame)
322
+ update_levels(counts, high_danger)
323
+ display_area.image(annotated, channels="BGR", caption="Detection Result", width='stretch')
324
+
325
+ if high_danger:
326
+ status_area.error("🚨 HIGH DANGER DETECTED!")
327
+
328
+ _, enc = cv2.imencode('.jpg', annotated)
329
+ with download_area:
330
+ st.download_button("⬇️ Download Detected Image", enc.tobytes(),
331
+ "detected_image.jpg", "image/jpeg", width='stretch')
332
+ st.session_state.processing = False
333
+
334
+ # VIDEO PROCESSING
335
+ elif ext in ["mp4", "avi", "mov"]:
336
+ cap = cv2.VideoCapture(path)
337
+ if not cap.isOpened():
338
+ st.error("Could not open video file")
339
+ st.session_state.processing = False
340
+ else:
341
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
342
+ fps = max(int(cap.get(cv2.CAP_PROP_FPS)), 20)
343
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
344
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
345
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
346
+
347
+ frame_skip = st.session_state.get("frame_skip", 1)
348
+ output_fps = max(1, int(fps / frame_skip))
349
+
350
+ output_path = os.path.join(tempfile.gettempdir(), f"flood_output_{timestamp}.mp4")
351
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
352
+ video_writer = cv2.VideoWriter(output_path, fourcc, output_fps, (width, height))
353
+ if not video_writer.isOpened():
354
+ st.error("❌ VideoWriter failed to open — check codec or path.")
355
+ st.stop()
356
+
357
+ frame_idx = 0
358
+ processed_count = 0
359
+ total_to_process = max(1, total_frames // frame_skip)
360
+ last_frame_bytes = None
361
+ last_counts, last_danger = {}, False
362
+
363
+ # Create controls and placeholders BEFORE the loop
364
+ with controls_area:
365
+ colA, colB, colC = st.columns(3)
366
+ with colA:
367
+ pause_btn = st.button("⏸️ Pause", key="pause_btn", use_container_width=True)
368
+ with colB:
369
+ resume_btn = st.button("▶️ Resume", key="resume_btn", use_container_width=True)
370
+ with colC:
371
+ download_placeholder = st.empty()
372
+
373
+ if pause_btn:
374
+ st.session_state.paused = True
375
+ st.session_state.pause_loop_counter = 0
376
+ if resume_btn:
377
+ st.session_state.paused = False
378
+ st.session_state.pause_loop_counter = 0
379
+
380
+ # Create centered progress bar
381
+ prog_col1, prog_col2, prog_col3 = st.columns([0.5, 3, 0.5])
382
+ with prog_col2:
383
+ progress_bar = st.progress(0.0)
384
+
385
+ # Initialize storage for last frame
386
+ if 'last_frame_bytes' not in st.session_state:
387
+ st.session_state.last_frame_bytes = None
388
+ st.session_state.last_counts = {}
389
+ st.session_state.last_danger = False
390
+
391
+ # --- Main Loop ---
392
+ while cap.isOpened() and frame_idx < total_frames:
393
+ # Save last processed frame info to session state
394
+ if last_frame_bytes:
395
+ st.session_state.last_frame_bytes = last_frame_bytes
396
+ st.session_state.last_counts = last_counts
397
+ st.session_state.last_danger = last_danger
398
+
399
+ # --- Improved Pause Handling ---
400
+ if st.session_state.paused:
401
+ if st.session_state.last_frame_bytes:
402
+ np_img = np.frombuffer(st.session_state.last_frame_bytes, np.uint8)
403
+ paused_frame = cv2.imdecode(np_img, cv2.IMREAD_COLOR)
404
+ display_area.image(paused_frame, channels="BGR", width='stretch')
405
+
406
+ update_levels(st.session_state.last_counts, st.session_state.last_danger)
407
+ status_area.info("⏸️ Video Paused ")
408
+
409
+ with download_placeholder.container():
410
+ st.download_button(
411
+ "⬇️ Download Paused Frame",
412
+ st.session_state.last_frame_bytes,
413
+ file_name=f"paused_frame_{frame_idx}.jpg",
414
+ mime="image/jpeg",
415
+ key=f"paused_dl_{st.session_state.pause_loop_counter}",
416
+ use_container_width=True
417
+ )
418
+ st.session_state.pause_loop_counter += 1
419
+ else:
420
+ status_area.info("No frame available yet.")
421
+ time.sleep(0.3)
422
+ continue
423
+ else:
424
+ # Show download button during processing
425
+ if st.session_state.last_frame_bytes:
426
+ with download_placeholder.container():
427
+ st.download_button(
428
+ "⬇️ Download Current Frame",
429
+ st.session_state.last_frame_bytes,
430
+ file_name=f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg",
431
+ mime="image/jpeg",
432
+ key=f"current_dl_{processed_count}",
433
+ use_container_width=True
434
+ )
435
+ else:
436
+ download_placeholder.empty()
437
+
438
+ ret, frame = cap.read()
439
+ if not ret:
440
+ break
441
+
442
+ annotated, counts, high_danger = detect_and_count(frame)
443
+ if annotated is not None:
444
+ update_levels(counts, high_danger)
445
+ display_area.image(annotated, channels="BGR", width='stretch')
446
+
447
+ _, enc = cv2.imencode('.jpg', annotated)
448
+ last_frame_bytes = enc.tobytes()
449
+ last_counts, last_danger = counts, high_danger
450
+
451
+ video_writer.write(annotated)
452
+ processed_count += 1
453
+ with prog_col2:
454
+ progress_bar.progress(min(processed_count / total_to_process, 1.0))
455
+
456
+ if high_danger:
457
+ status_area.markdown(f"<div class='centered-status'><div class='status-text' style='color:#FF0000;'>🚨 HIGH DANGER! Frame {processed_count}/{total_to_process}</div></div>", unsafe_allow_html=True)
458
+ else:
459
+ status_area.markdown(f"<div class='centered-status'><div class='status-text'>▶️ Processing frame {processed_count}/{total_to_process}</div></div>", unsafe_allow_html=True)
460
+
461
+ st.session_state.report_log.append([
462
+ frame_idx + 1,
463
+ counts.get("Level 0", 0),
464
+ counts.get("Level 1", 0),
465
+ counts.get("Level 2", 0),
466
+ counts.get("Level 3", 0),
467
+ counts.get("Level 4", 0)
468
+ ])
469
+
470
+ frame_idx += frame_skip
471
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
472
+ time.sleep(0.03)
473
+
474
+ cap.release()
475
+ video_writer.release()
476
+ status_area.success("✅ Video processing finished!")
477
+ st.session_state.processing = False
478
+
479
+ # --- ZIP Export ---
480
+ zip_buffer = BytesIO()
481
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
482
+ if os.path.exists(output_path):
483
+ zipf.write(output_path, arcname="detected_video.mp4")
484
+
485
+ csv_stream = StringIO()
486
+ csv_writer = csv.writer(csv_stream)
487
+ csv_writer.writerow(["Frame No", "Level 0", "Level 1", "Level 2", "Level 3", "Level 4"])
488
+ csv_writer.writerows(st.session_state.report_log)
489
+ zipf.writestr("flood_level_report.csv", csv_stream.getvalue())
490
+
491
+ zip_buffer.seek(0)
492
+ st.session_state.zip_data = zip_buffer.getvalue()
493
+ st.session_state.zip_filename = f"flood_analysis_{timestamp}.zip"
494
+ st.session_state.zip_ready = True
495
+
496
+ try:
497
+ if os.path.exists(output_path):
498
+ os.remove(output_path)
499
+ if os.path.exists(st.session_state.tmp_video_path):
500
+ os.remove(st.session_state.tmp_video_path)
501
+ except Exception:
502
+ pass
503
 
504
+ # --- ZIP DOWNLOAD (left panel) ---
505
+ if st.session_state.zip_ready:
506
+ with download_area:
507
+ st.download_button(
508
+ label="📦 Download ZIP (Video + Report)",
509
+ data=st.session_state.zip_data,
510
+ file_name=st.session_state.zip_filename,
511
+ mime="application/zip",
512
+ key="zip_download_button",
513
+ use_container_width=True
514
+ )