openfree commited on
Commit
81989c5
ยท
verified ยท
1 Parent(s): 0e4a61f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +490 -0
app.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import cv2
3
+ import numpy as np
4
+ import tempfile
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional, Tuple
8
+ from moviepy.editor import VideoFileClip
9
+ import torch
10
+ from PIL import Image
11
+
12
+ # ==============================
13
+ # Streamlit page config & Custom CSS
14
+ # ==============================
15
+ st.set_page_config(
16
+ page_title="Ansim Blur - Face Privacy Protection",
17
+ page_icon="๐Ÿ”’",
18
+ layout="wide",
19
+ initial_sidebar_state="expanded"
20
+ )
21
+
22
+ # Custom CSS - ๋ ˆ์ด์•„์›ƒ ์•ˆ์ •ํ™”๋ฅผ ์œ„ํ•œ ์ˆ˜์ •
23
+ st.markdown("""
24
+ <style>
25
+ /* ์ปจํ…Œ์ด๋„ˆ ๊ณ ์ • ๋†’์ด ์„ค์ •์œผ๋กœ ํ”๋“ค๋ฆผ ๋ฐฉ์ง€ */
26
+ .image-container {
27
+ min-height: 400px;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ }
32
+
33
+ /* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์ •ํ™” */
34
+ .main .block-container {
35
+ max-width: 1400px;
36
+ padding-top: 2rem;
37
+ padding-bottom: 2rem;
38
+ }
39
+
40
+ /* ์ปฌ๋Ÿผ ๊ณ ์ • */
41
+ [data-testid="column"] {
42
+ min-height: 500px;
43
+ }
44
+
45
+ /* ์ด๋ฏธ์ง€ ์—…๋กœ๋” ์˜์—ญ ๊ณ ์ • */
46
+ [data-testid="stFileUploader"] {
47
+ min-height: 150px;
48
+ }
49
+
50
+ /* ๋ฒ„ํŠผ ์˜์—ญ ๊ณ ์ • */
51
+ .stButton {
52
+ min-height: 60px;
53
+ }
54
+
55
+ /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์˜์—ญ ๊ณ ์ • */
56
+ .stProgress {
57
+ min-height: 30px;
58
+ }
59
+
60
+ /* ํ—ค๋” ์Šคํƒ€์ผ๋ง */
61
+ h1 {
62
+ background: linear-gradient(120deg, #a855f7, #ec4899);
63
+ -webkit-background-clip: text;
64
+ -webkit-text-fill-color: transparent;
65
+ font-size: 3rem !important;
66
+ font-weight: 700 !important;
67
+ text-align: center;
68
+ margin-bottom: 1rem !important;
69
+ }
70
+
71
+ /* ์นด๋“œ ์Šคํƒ€์ผ */
72
+ .stat-card {
73
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
74
+ padding: 1rem;
75
+ border-radius: 12px;
76
+ color: white;
77
+ text-align: center;
78
+ height: 100px;
79
+ display: flex;
80
+ flex-direction: column;
81
+ justify-content: center;
82
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
83
+ }
84
+
85
+ .stat-number {
86
+ font-size: 2rem;
87
+ margin-bottom: 0.3rem;
88
+ }
89
+
90
+ .stat-label {
91
+ font-size: 0.85rem;
92
+ opacity: 0.95;
93
+ }
94
+
95
+ /* ๋ฒ„ํŠผ ์Šคํƒ€์ผ ๊ฐœ์„  */
96
+ .stButton > button {
97
+ background: linear-gradient(135deg, #a855f7 0%, #ec4899 100%);
98
+ color: white;
99
+ border: none;
100
+ padding: 0.7rem 1.5rem;
101
+ font-size: 1rem;
102
+ font-weight: 600;
103
+ border-radius: 25px;
104
+ width: 100%;
105
+ transition: transform 0.2s;
106
+ }
107
+
108
+ .stButton > button:hover {
109
+ transform: translateY(-2px);
110
+ }
111
+
112
+ /* ์‚ฌ์ด๋“œ๋ฐ” ์Šคํƒ€์ผ */
113
+ .css-1d391kg {
114
+ background-color: #f8f7ff;
115
+ }
116
+
117
+ /* Info ๋ฐ•์Šค */
118
+ .info-box {
119
+ background: #f0f4ff;
120
+ border-left: 4px solid #667eea;
121
+ padding: 1rem;
122
+ border-radius: 8px;
123
+ margin: 1rem 0;
124
+ }
125
+ </style>
126
+ """, unsafe_allow_html=True)
127
+
128
+ # ==============================
129
+ # Header Section
130
+ # ==============================
131
+ st.markdown("<h1>๐Ÿ”’ Ansim Blur</h1>", unsafe_allow_html=True)
132
+ st.markdown("<p style='text-align: center; color: #6b7280; margin-bottom: 1rem;'>Advanced Face Privacy Protection</p>", unsafe_allow_html=True)
133
+
134
+ # Discord ๋ฐฐ์ง€๋ฅผ ๊ฐ€์šด๋ฐ ์ •๋ ฌ
135
+ st.markdown("""
136
+ <div style='text-align: center; margin-bottom: 2rem;'>
137
+ <a href="https://discord.gg/openfreeai" target="_blank">
138
+ <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="Discord badge">
139
+ </a>
140
+ </div>
141
+ """, unsafe_allow_html=True)
142
+
143
+ # Stats ์นด๋“œ - ์ปจํ…Œ์ด๋„ˆ๋กœ ๊ณ ์ •
144
+ stats_container = st.container()
145
+ with stats_container:
146
+ col1, col2, col3, col4 = st.columns(4)
147
+ with col1:
148
+ st.markdown("""<div class='stat-card'><div class='stat-number'>๐Ÿ–ผ๏ธ</div><div class='stat-label'>Image Support</div></div>""", unsafe_allow_html=True)
149
+ with col2:
150
+ st.markdown("""<div class='stat-card'><div class='stat-number'>๐ŸŽฅ</div><div class='stat-label'>Video Processing</div></div>""", unsafe_allow_html=True)
151
+ with col3:
152
+ st.markdown("""<div class='stat-card'><div class='stat-number'>โšก</div><div class='stat-label'>Real-time</div></div>""", unsafe_allow_html=True)
153
+ with col4:
154
+ st.markdown("""<div class='stat-card'><div class='stat-number'>๐Ÿ›ก๏ธ</div><div class='stat-label'>Privacy First</div></div>""", unsafe_allow_html=True)
155
+
156
+ st.markdown("---")
157
+
158
+ # ==============================
159
+ # Model loader
160
+ # ==============================
161
+ @st.cache_resource(show_spinner=False)
162
+ def load_model(model_path: str = "yolov8-face-hf.pt", device: Optional[str] = None):
163
+ from ultralytics import YOLO
164
+ if device is None:
165
+ if torch.cuda.is_available():
166
+ device = "cuda"
167
+ elif torch.backends.mps.is_available():
168
+ device = "mps"
169
+ else:
170
+ device = "cpu"
171
+ model = YOLO(model_path)
172
+ model.to(device)
173
+ return model, device
174
+
175
+ with st.spinner("Loading AI model..."):
176
+ model, device = load_model()
177
+
178
+ # ==============================
179
+ # Sidebar - ๊ณ ์ •๋œ ์„ค์ •
180
+ # ==============================
181
+ with st.sidebar:
182
+ st.markdown("## โš™๏ธ Configuration")
183
+ st.info(f"Device: **{device.upper()}**")
184
+
185
+ st.markdown("### Detection Settings")
186
+ conf = st.slider("Confidence Threshold", 0.05, 0.9, 0.25, 0.01)
187
+ iou = st.slider("NMS IoU", 0.1, 0.9, 0.45, 0.01)
188
+ expand_ratio = st.slider("Box Expansion", 0.0, 0.5, 0.05, 0.01)
189
+
190
+ st.markdown("### Blur Settings")
191
+ mode_choice = st.selectbox("Style", ["Gaussian Blur", "Mosaic Effect"])
192
+
193
+ if mode_choice == "Gaussian Blur":
194
+ blur_kernel = st.slider("Blur Intensity", 15, 151, 51, 2)
195
+ mosaic = 15
196
+ else:
197
+ mosaic = st.slider("Mosaic Size", 5, 40, 15, 1)
198
+ blur_kernel = 51
199
+
200
+ use_half = st.checkbox("Half Precision (CUDA)", value=False)
201
+
202
+ # ==============================
203
+ # Helper functions
204
+ # ==============================
205
+ def _ensure_odd(x: int) -> int:
206
+ return x if x % 2 == 1 else x + 1
207
+
208
+ def _choose_writer_size(w: int, h: int) -> Tuple[int, int]:
209
+ return (w if w % 2 == 0 else w - 1, h if h % 2 == 0 else h - 1)
210
+
211
+ def _apply_anonymization(face_roi: np.ndarray, mode: str, blur_kernel: int, mosaic: int = 15) -> np.ndarray:
212
+ if face_roi.size == 0:
213
+ return face_roi
214
+ if mode == "Gaussian Blur":
215
+ k = _ensure_odd(max(blur_kernel, 15))
216
+ return cv2.GaussianBlur(face_roi, (k, k), 0)
217
+ else:
218
+ m = max(2, mosaic)
219
+ h, w = face_roi.shape[:2]
220
+ face_small = cv2.resize(face_roi, (max(1, w // m), max(1, h // m)), interpolation=cv2.INTER_LINEAR)
221
+ return cv2.resize(face_small, (w, h), interpolation=cv2.INTER_NEAREST)
222
+
223
+ def blur_faces_image(image_bgr, conf, iou, expand_ratio, mode, blur_kernel, mosaic, use_half):
224
+ h, w = image_bgr.shape[:2]
225
+ face_count = 0
226
+
227
+ with torch.no_grad():
228
+ if use_half and device == "cuda":
229
+ torch.set_default_dtype(torch.float16)
230
+ results = model.predict(image_bgr, conf=conf, iou=iou, verbose=False, device=device)
231
+ if use_half and device == "cuda":
232
+ torch.set_default_dtype(torch.float32)
233
+
234
+ for r in results:
235
+ boxes = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes, "xyxy") else []
236
+ face_count = len(boxes)
237
+ for x1, y1, x2, y2 in boxes:
238
+ x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
239
+
240
+ if expand_ratio > 0:
241
+ bw = x2 - x1
242
+ bh = y2 - y1
243
+ dx = int(bw * expand_ratio)
244
+ dy = int(bh * expand_ratio)
245
+ x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
246
+
247
+ x1 = max(0, min(w, x1))
248
+ x2 = max(0, min(w, x2))
249
+ y1 = max(0, min(h, y1))
250
+ y2 = max(0, min(h, y2))
251
+ if x2 <= x1 or y2 <= y1:
252
+ continue
253
+
254
+ roi = image_bgr[y1:y2, x1:x2]
255
+ image_bgr[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
256
+
257
+ return image_bgr, face_count
258
+
259
+ def blur_faces_video(input_path, output_path, conf, iou, expand_ratio, mode, blur_kernel, mosaic, update_callback, use_half):
260
+ cap = cv2.VideoCapture(input_path)
261
+ if not cap.isOpened():
262
+ raise IOError("Cannot open video")
263
+
264
+ in_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
265
+ in_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
266
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
267
+ frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
268
+
269
+ out_w, out_h = _choose_writer_size(in_w, in_h)
270
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
271
+ temp_video_path = str(Path(output_path).with_name("blurred_temp_video.mp4"))
272
+ out = cv2.VideoWriter(temp_video_path, fourcc, fps, (out_w, out_h))
273
+
274
+ idx = 0
275
+ total_faces = 0
276
+ try:
277
+ while True:
278
+ ret, frame = cap.read()
279
+ if not ret:
280
+ break
281
+ frame = cv2.resize(frame, (out_w, out_h))
282
+
283
+ with torch.no_grad():
284
+ if use_half and device == "cuda":
285
+ torch.set_default_dtype(torch.float16)
286
+ results = model.predict(frame, conf=conf, iou=iou, verbose=False, device=device)
287
+ if use_half and device == "cuda":
288
+ torch.set_default_dtype(torch.float32)
289
+
290
+ h, w = frame.shape[:2]
291
+ r0 = results[0] if len(results) else None
292
+ boxes = r0.boxes.xyxy if (r0 and hasattr(r0, "boxes")) else []
293
+ total_faces += len(boxes)
294
+
295
+ for b in boxes:
296
+ x1, y1, x2, y2 = map(int, b)
297
+ if expand_ratio > 0:
298
+ bw = x2 - x1
299
+ bh = y2 - y1
300
+ dx = int(bw * expand_ratio)
301
+ dy = int(bh * expand_ratio)
302
+ x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
303
+
304
+ x1 = max(0, min(w, x1))
305
+ x2 = max(0, min(w, x2))
306
+ y1 = max(0, min(h, y1))
307
+ y2 = max(0, min(h, y2))
308
+ if x2 <= x1 or y2 <= y1:
309
+ continue
310
+
311
+ roi = frame[y1:y2, x1:x2]
312
+ frame[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
313
+
314
+ out.write(frame)
315
+ idx += 1
316
+ if update_callback and frames > 0:
317
+ update_callback(min(0.98, idx / frames), idx, frames, total_faces)
318
+ finally:
319
+ cap.release()
320
+ out.release()
321
+
322
+ try:
323
+ if update_callback:
324
+ update_callback(0.99, idx, frames, total_faces)
325
+ original = VideoFileClip(input_path)
326
+ processed = VideoFileClip(temp_video_path).set_audio(original.audio)
327
+ processed.write_videofile(
328
+ output_path,
329
+ codec="libx264",
330
+ audio_codec="aac",
331
+ threads=1,
332
+ logger=None
333
+ )
334
+ if update_callback:
335
+ update_callback(1.0, idx, frames, total_faces)
336
+ return output_path, total_faces
337
+ except Exception as e:
338
+ print("Audio merging failed:", e)
339
+ return temp_video_path, total_faces
340
+
341
+ # ==============================
342
+ # Main Interface - ๊ณ ์ •๋œ ๋ ˆ์ด์•„์›ƒ
343
+ # ==============================
344
+ tab1, tab2 = st.tabs(["๐Ÿ“ธ Image Processing", "๐ŸŽฌ Video Processing"])
345
+
346
+ with tab1:
347
+ # ๊ณ ์ •๋œ ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ
348
+ main_container = st.container()
349
+
350
+ with main_container:
351
+ # 2๊ฐœ์˜ ๊ณ ์ •๋œ ์ปฌ๋Ÿผ
352
+ col1, col2 = st.columns(2, gap="large")
353
+
354
+ # ์™ผ์ชฝ ์ปฌ๋Ÿผ - ์ž…๋ ฅ
355
+ with col1:
356
+ st.markdown("### Input")
357
+
358
+ # ํŒŒ์ผ ์—…๋กœ๋” ์ปจํ…Œ์ด๋„ˆ
359
+ upload_container = st.container()
360
+ with upload_container:
361
+ uploaded_file = st.file_uploader(
362
+ "Choose an image",
363
+ type=["jpg", "png", "jpeg"],
364
+ key="img_upload"
365
+ )
366
+
367
+ # ์›๋ณธ ์ด๋ฏธ์ง€ ํ‘œ์‹œ ์˜์—ญ (๊ณ ์ • ๋†’์ด)
368
+ original_placeholder = st.empty()
369
+ info_placeholder = st.empty()
370
+
371
+ if uploaded_file:
372
+ file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
373
+ image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
374
+ original_placeholder.image(cv2.cvtColor(image, cv2.COLOR_BGR2RGB), caption="Original", use_container_width=True)
375
+ h, w = image.shape[:2]
376
+ info_placeholder.info(f"Size: {w} ร— {h} pixels")
377
+ else:
378
+ # ๋นˆ ๊ณต๊ฐ„ ์œ ์ง€
379
+ original_placeholder.markdown("<div class='image-container'><p style='text-align:center;color:#999;'>No image uploaded</p></div>", unsafe_allow_html=True)
380
+ info_placeholder.empty()
381
+
382
+ # ์˜ค๋ฅธ์ชฝ ์ปฌ๋Ÿผ - ๊ฒฐ๊ณผ
383
+ with col2:
384
+ st.markdown("### Result")
385
+
386
+ # ๋ฒ„ํŠผ ์ปจํ…Œ์ด๋„ˆ
387
+ button_container = st.container()
388
+
389
+ # ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€ ํ‘œ์‹œ ์˜์—ญ (๊ณ ์ • ๋†’์ด)
390
+ result_placeholder = st.empty()
391
+ success_placeholder = st.empty()
392
+ download_placeholder = st.empty()
393
+
394
+ with button_container:
395
+ if uploaded_file:
396
+ if st.button("๐Ÿ” Process Image", type="primary", use_container_width=True):
397
+ with st.spinner("Processing..."):
398
+ result, face_count = blur_faces_image(
399
+ image.copy(), conf, iou, expand_ratio,
400
+ mode_choice, blur_kernel, mosaic, use_half
401
+ )
402
+
403
+ result_placeholder.image(cv2.cvtColor(result, cv2.COLOR_BGR2RGB), caption="Processed", use_container_width=True)
404
+ success_placeholder.success(f"Blurred {face_count} face(s)")
405
+
406
+ _, buffer = cv2.imencode('.jpg', result)
407
+ download_placeholder.download_button(
408
+ "โฌ‡๏ธ Download",
409
+ data=buffer.tobytes(),
410
+ file_name="blurred.jpg",
411
+ mime="image/jpeg",
412
+ use_container_width=True
413
+ )
414
+ else:
415
+ result_placeholder.markdown("<div class='image-container'><p style='text-align:center;color:#999;'>Results will appear here</p></div>", unsafe_allow_html=True)
416
+
417
+ with tab2:
418
+ video_container = st.container()
419
+
420
+ with video_container:
421
+ col1, col2 = st.columns(2, gap="large")
422
+
423
+ with col1:
424
+ st.markdown("### Input Video")
425
+
426
+ video_upload = st.file_uploader(
427
+ "Choose a video",
428
+ type=["mp4", "avi", "mov", "mkv"],
429
+ key="video_upload"
430
+ )
431
+
432
+ video_placeholder = st.empty()
433
+
434
+ if video_upload:
435
+ video_placeholder.video(video_upload)
436
+ else:
437
+ video_placeholder.markdown("<div class='image-container'><p style='text-align:center;color:#999;'>No video uploaded</p></div>", unsafe_allow_html=True)
438
+
439
+ with col2:
440
+ st.markdown("### Processed Video")
441
+
442
+ process_button = st.empty()
443
+ progress_placeholder = st.empty()
444
+ stats_placeholder = st.empty()
445
+ result_video_placeholder = st.empty()
446
+ download_video_placeholder = st.empty()
447
+
448
+ if video_upload:
449
+ if process_button.button("๐ŸŽฌ Process Video", type="primary", use_container_width=True):
450
+ # Save uploaded file
451
+ input_path = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4").name
452
+ with open(input_path, "wb") as f:
453
+ f.write(video_upload.read())
454
+
455
+ output_path = str(Path(tempfile.gettempdir()) / "blurred_video.mp4")
456
+
457
+ def update_progress(value, current_frame=0, total_frames=0, faces=0):
458
+ percent = int(value * 100)
459
+ progress_placeholder.progress(value)
460
+ if total_frames > 0:
461
+ stats_placeholder.info(f"๐Ÿ“Š Frame: {current_frame}/{total_frames} | Progress: {percent}% | Faces: {faces}")
462
+
463
+ try:
464
+ final_output, total_faces = blur_faces_video(
465
+ input_path, output_path,
466
+ conf=conf, iou=iou, expand_ratio=expand_ratio,
467
+ mode=mode_choice, blur_kernel=blur_kernel,
468
+ mosaic=mosaic,
469
+ update_callback=update_progress, use_half=use_half
470
+ )
471
+
472
+ stats_placeholder.success(f"โœ… Complete! Blurred {total_faces} faces.")
473
+ result_video_placeholder.video(final_output)
474
+
475
+ with open(final_output, "rb") as file:
476
+ download_video_placeholder.download_button(
477
+ "โฌ‡๏ธ Download Video",
478
+ file,
479
+ file_name="blurred_video.mp4",
480
+ mime="video/mp4",
481
+ use_container_width=True
482
+ )
483
+ except Exception as e:
484
+ stats_placeholder.error(f"โŒ Error: {e}")
485
+ finally:
486
+ if os.path.exists(input_path):
487
+ os.remove(input_path)
488
+ else:
489
+ result_video_placeholder.markdown("<div class='image-container'><p style='text-align:center;color:#999;'>Processed video will appear here</p></div>", unsafe_allow_html=True)
490
+