Spaces:
Configuration error
Configuration error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,397 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
Built with Streamlit and the SanitizeAI core engine.
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
2. Video Cleaner: Uses FFmpeg to extract frames, cleans each frame, and stitches back together with audio.
|
| 8 |
-
3. Text Cleaner: Purges hidden zero-width spaces, Unicode tracking markers, homoglyphs, and AI conversational markers.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import os
|
| 12 |
-
import re
|
| 13 |
-
import sys
|
| 14 |
-
import time
|
| 15 |
-
import tempfile
|
| 16 |
-
import shutil
|
| 17 |
-
import subprocess
|
| 18 |
-
from pathlib import Path
|
| 19 |
-
from io import BytesIO
|
| 20 |
-
|
| 21 |
-
import streamlit as st
|
| 22 |
-
from PIL import Image
|
| 23 |
-
|
| 24 |
-
# Import modular Core Engine functions
|
| 25 |
-
from engine import (
|
| 26 |
-
CleanMode,
|
| 27 |
-
clean_image,
|
| 28 |
-
analyze_image_metadata,
|
| 29 |
-
clean_text_content,
|
| 30 |
-
get_ffmpeg_path,
|
| 31 |
-
get_video_fps,
|
| 32 |
-
FFMPEG_PATH,
|
| 33 |
-
HAS_DIFFUSION_REMOVER as HAS_REMOVER,
|
| 34 |
-
sanitize_video_pipeline,
|
| 35 |
-
calculate_logo_bbox,
|
| 36 |
-
probe_video_metadata,
|
| 37 |
-
inpaint_frame_logo,
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
# Page configuration
|
| 41 |
-
st.set_page_config(
|
| 42 |
-
page_title="SanitizeAI - Watermark & Metadata Sanitizer",
|
| 43 |
-
page_icon="🛡️",
|
| 44 |
-
layout="wide",
|
| 45 |
-
initial_sidebar_state="expanded"
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
# Custom Styling
|
| 49 |
-
st.markdown("""
|
| 50 |
-
<style>
|
| 51 |
-
.main-header {
|
| 52 |
-
font-size: 2.2rem;
|
| 53 |
-
font-weight: 700;
|
| 54 |
-
background: linear-gradient(135deg, #1e293b 0%, #3b82f6 100%);
|
| 55 |
-
-webkit-background-clip: text;
|
| 56 |
-
-webkit-text-fill-color: transparent;
|
| 57 |
-
margin-bottom: 0.2rem;
|
| 58 |
-
}
|
| 59 |
-
.sub-header {
|
| 60 |
-
color: #64748b;
|
| 61 |
-
font-size: 1.05rem;
|
| 62 |
-
margin-bottom: 1.5rem;
|
| 63 |
-
}
|
| 64 |
-
.card-box {
|
| 65 |
-
background-color: #f8fafc;
|
| 66 |
-
border: 1px solid #e2e8f0;
|
| 67 |
-
border-radius: 10px;
|
| 68 |
-
padding: 1.2rem;
|
| 69 |
-
margin-bottom: 1rem;
|
| 70 |
-
}
|
| 71 |
-
.badge-success {
|
| 72 |
-
background-color: #dcfce7;
|
| 73 |
-
color: #15803d;
|
| 74 |
-
padding: 4px 10px;
|
| 75 |
-
border-radius: 12px;
|
| 76 |
-
font-size: 0.85rem;
|
| 77 |
-
font-weight: 600;
|
| 78 |
-
}
|
| 79 |
-
.badge-warning {
|
| 80 |
-
background-color: #fef3c7;
|
| 81 |
-
color: #b45309;
|
| 82 |
-
padding: 4px 10px;
|
| 83 |
-
border-radius: 12px;
|
| 84 |
-
font-size: 0.85rem;
|
| 85 |
-
font-weight: 600;
|
| 86 |
-
}
|
| 87 |
-
.stTabs [data-baseweb="tab-list"] {
|
| 88 |
-
gap: 8px;
|
| 89 |
-
}
|
| 90 |
-
.stTabs [data-baseweb="tab"] {
|
| 91 |
-
height: 50px;
|
| 92 |
-
white-space: pre-wrap;
|
| 93 |
-
border-radius: 8px 8px 0px 0px;
|
| 94 |
-
gap: 8px;
|
| 95 |
-
padding-top: 10px;
|
| 96 |
-
padding-bottom: 10px;
|
| 97 |
-
font-weight: 600;
|
| 98 |
-
}
|
| 99 |
-
</style>
|
| 100 |
-
""", unsafe_allow_html=True)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# -----------------------------------------------------------------------------
|
| 104 |
-
# SIDEBAR
|
| 105 |
-
# -----------------------------------------------------------------------------
|
| 106 |
-
|
| 107 |
-
with st.sidebar:
|
| 108 |
-
st.image("https://img.icons8.com/color/96/shield-clean.png", width=70)
|
| 109 |
-
st.title("SanitizeAI")
|
| 110 |
-
st.caption("AI Metadata & Watermark Purger v0.1.29")
|
| 111 |
-
|
| 112 |
-
st.subheader("⚙️ System Status")
|
| 113 |
-
if FFMPEG_PATH:
|
| 114 |
-
st.markdown("<span class='badge-success'>✓ FFmpeg Available</span>", unsafe_allow_html=True)
|
| 115 |
-
else:
|
| 116 |
-
st.markdown("<span class='badge-warning'>⚠️ FFmpeg Not Found</span>", unsafe_allow_html=True)
|
| 117 |
-
st.caption("Video frame stitching requires FFmpeg.")
|
| 118 |
-
|
| 119 |
-
if HAS_REMOVER:
|
| 120 |
-
st.markdown("<span class='badge-success'>✓ Diffusion Engine Ready</span>", unsafe_allow_html=True)
|
| 121 |
-
else:
|
| 122 |
-
st.markdown("<span class='badge-warning'>⚠️ PyTorch/Diffusers Limited</span>", unsafe_allow_html=True)
|
| 123 |
-
|
| 124 |
-
st.divider()
|
| 125 |
-
st.subheader("Global Options")
|
| 126 |
-
keep_standard_meta = st.checkbox("Keep Standard Metadata (Author/Title/DPI)", value=True, help="Retains camera/DPI info while stripping AI tags.")
|
| 127 |
-
|
| 128 |
-
st.info("💡 **Tip**: Use 'Metadata Stripping' for instant lossless cleaning, or 'Deep Watermark Removal' to erase invisible AI watermarks like SynthID.")
|
| 129 |
-
|
| 130 |
-
st.divider()
|
| 131 |
-
st.subheader("🔌 API Access")
|
| 132 |
-
st.caption("FastAPI backend is ready for n8n/Make workflows at `/v1/clean` with `X-API-Key`.")
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
# -----------------------------------------------------------------------------
|
| 136 |
-
# MAIN APP BODY
|
| 137 |
-
# -----------------------------------------------------------------------------
|
| 138 |
-
|
| 139 |
-
st.markdown("<div class='main-header'>🛡️ SanitizeAI - Watermark & Metadata Sanitizer</div>", unsafe_allow_html=True)
|
| 140 |
-
st.markdown("<div class='sub-header'>Remove invisible AI watermarks (SynthID, StableSignature, TreeRing), strip AI metadata (SD, ComfyUI, C2PA), and sanitize AI-generated text and video frames.</div>", unsafe_allow_html=True)
|
| 141 |
-
|
| 142 |
-
tab1, tab2, tab3 = st.tabs(["🖼️ Image Cleaner", "🎬 Video Cleaner", "📝 Text Cleaner"])
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
# =============================================================================
|
| 146 |
-
# TAB 1: IMAGE CLEANER
|
| 147 |
-
# =============================================================================
|
| 148 |
-
with tab1:
|
| 149 |
-
st.subheader("Image Metadata & AI Watermark Cleaner")
|
| 150 |
-
st.markdown("Upload images to inspect and remove AI generation prompts, C2PA digital signatures, and invisible watermarks.")
|
| 151 |
-
|
| 152 |
-
uploaded_files = st.file_uploader("Upload Image(s)", type=["png", "jpg", "jpeg", "webp"], accept_multiple_files=True, key="img_uploader")
|
| 153 |
-
|
| 154 |
-
col_mode, col_opt = st.columns([1, 1])
|
| 155 |
-
with col_mode:
|
| 156 |
-
clean_mode = st.radio(
|
| 157 |
-
"Cleaning Engine Mode",
|
| 158 |
-
[
|
| 159 |
-
"⭐ SynthID & Pixel Watermark Destructor (Recommended)",
|
| 160 |
-
"⭐ Visual Logo Purge + SynthID Destructor (Google Star / Corner Logo)",
|
| 161 |
-
"Metadata Stripping Only (Fast & Lossless)",
|
| 162 |
-
"Deep Diffusion Regeneration Attack"
|
| 163 |
-
],
|
| 164 |
-
help="Pixel Destructor alters spatial/frequency noise to break invisible SynthID watermarks. Visual Logo Purge inpaints corner watermarks."
|
| 165 |
-
)
|
| 166 |
-
with col_opt:
|
| 167 |
-
logo_loc_img = "bottom_right"
|
| 168 |
-
synth_strength_img = 0.5
|
| 169 |
-
device_choice = "auto"
|
| 170 |
-
|
| 171 |
-
if "Logo" in clean_mode:
|
| 172 |
-
logo_loc_img = st.selectbox("Visual Logo Location", ["bottom_right", "bottom_left", "all_corners", "top_right", "top_left"], key="img_logo_loc")
|
| 173 |
-
if "SynthID" in clean_mode or "Logo" in clean_mode:
|
| 174 |
-
synth_strength_img = st.slider("SynthID Destruction Intensity", min_value=0.1, max_value=1.0, value=0.5, step=0.1, key="img_strength")
|
| 175 |
-
elif "Deep" in clean_mode:
|
| 176 |
-
synth_strength_img = st.slider("Denoising Strength", min_value=0.01, max_value=0.20, value=0.04, step=0.01, help="Higher values erase stronger watermarks but slightly modify fine textures.")
|
| 177 |
-
device_choice = st.selectbox("Inference Device", ["auto", "cpu", "cuda"], index=0)
|
| 178 |
-
else:
|
| 179 |
-
st.caption("⚡ Header Metadata mode strips EXIF/PNGinfo/C2PA tags without altering pixels.")
|
| 180 |
-
|
| 181 |
-
if uploaded_files:
|
| 182 |
-
st.divider()
|
| 183 |
-
for idx, file_obj in enumerate(uploaded_files):
|
| 184 |
-
with st.container():
|
| 185 |
-
st.markdown(f"#### 📄 `{file_obj.name}` ({file_obj.size / 1024:.1f} KB)")
|
| 186 |
-
|
| 187 |
-
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 188 |
-
tmp_path = Path(tmp_dir) / file_obj.name
|
| 189 |
-
out_path = Path(tmp_dir) / f"sanitized_{file_obj.name}"
|
| 190 |
-
|
| 191 |
-
file_bytes = file_obj.getvalue()
|
| 192 |
-
with open(tmp_path, "wb") as f:
|
| 193 |
-
f.write(file_bytes)
|
| 194 |
-
|
| 195 |
-
# Analysis
|
| 196 |
-
analysis = analyze_image_metadata(tmp_path)
|
| 197 |
-
|
| 198 |
-
c1, c2 = st.columns([1, 1])
|
| 199 |
-
with c1:
|
| 200 |
-
st.image(str(tmp_path), caption="Original Input", use_container_width=True)
|
| 201 |
-
if analysis["detected_keys"]:
|
| 202 |
-
st.warning(f"⚠️ Found {analysis['total_fields']} AI Metadata field(s): `{', '.join(analysis['detected_keys'])}`")
|
| 203 |
-
else:
|
| 204 |
-
st.info("ℹ️ No explicit AI metadata keys detected in file headers.")
|
| 205 |
-
if analysis["has_c2pa"]:
|
| 206 |
-
st.error("🔒 C2PA Content Credentials signature detected.")
|
| 207 |
-
|
| 208 |
-
with c2:
|
| 209 |
-
if st.button(f"✨ Clean Image #{idx+1}", key=f"btn_clean_{idx}"):
|
| 210 |
-
with st.spinner("Processing image..."):
|
| 211 |
-
start_t = time.time()
|
| 212 |
-
|
| 213 |
-
# Use modular engine.py
|
| 214 |
-
cleaned_bytes, mime_type = clean_image(
|
| 215 |
-
image_input=file_bytes,
|
| 216 |
-
mode=clean_mode,
|
| 217 |
-
intensity=synth_strength_img,
|
| 218 |
-
logo_location=logo_loc_img,
|
| 219 |
-
keep_standard_metadata=keep_standard_meta,
|
| 220 |
-
device=device_choice,
|
| 221 |
-
)
|
| 222 |
-
out_path.write_bytes(cleaned_bytes)
|
| 223 |
-
|
| 224 |
-
duration = time.time() - start_t
|
| 225 |
-
out_size = len(cleaned_bytes)
|
| 226 |
-
|
| 227 |
-
st.image(cleaned_bytes, caption="Sanitized Output", use_container_width=True)
|
| 228 |
-
st.success(f"✓ Cleaned in {duration:.2f}s | Output size: {out_size / 1024:.1f} KB")
|
| 229 |
-
|
| 230 |
-
st.download_button(
|
| 231 |
-
label=f"⬇️ Download Cleaned {file_obj.name}",
|
| 232 |
-
data=cleaned_bytes,
|
| 233 |
-
file_name=f"sanitized_{file_obj.name}",
|
| 234 |
-
mime=mime_type,
|
| 235 |
-
key=f"dl_{idx}"
|
| 236 |
-
)
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
# =============================================================================
|
| 240 |
-
# TAB 2: VIDEO CLEANER
|
| 241 |
-
# =============================================================================
|
| 242 |
-
with tab2:
|
| 243 |
-
st.subheader("Video Frame & Audio Sanitizer")
|
| 244 |
-
st.markdown("Combines **Visible Watermark Logo Inpainting / Delogo**, **Invisible SynthID Watermark Destruction**, and **EXIF/C2PA Metadata Stripping** into one unified video pipeline.")
|
| 245 |
-
|
| 246 |
-
if not FFMPEG_PATH:
|
| 247 |
-
st.error("❌ FFmpeg binary was not found on the system. Please ensure `ffmpeg` or `imageio-ffmpeg` is installed.")
|
| 248 |
-
else:
|
| 249 |
-
video_file = st.file_uploader("Upload Video File", type=["mp4", "mov", "avi", "mkv", "webm"], key="vid_uploader")
|
| 250 |
-
|
| 251 |
-
st.markdown("### ⚙️ Sanitization Pipeline Settings")
|
| 252 |
-
|
| 253 |
-
c_pipe1, c_pipe2, c_pipe3 = st.columns(3)
|
| 254 |
-
with c_pipe1:
|
| 255 |
-
opt_visible_logo = st.checkbox("🎨 Remove Visible Watermark Logo", value=True, help="Inpaints or delogos visual on-screen logos (e.g. Google Veo/Imagen 4-pointed star logo).")
|
| 256 |
-
with c_pipe2:
|
| 257 |
-
opt_synthid = st.checkbox("🔮 Remove Invisible SynthID Watermarks", value=True, help="Alters frequency/spatial latent noise to break invisible SynthID signatures.")
|
| 258 |
-
with c_pipe3:
|
| 259 |
-
opt_metadata = st.checkbox("🏷️ Remove AI Metadata Headers", value=True, help="Strips EXIF/PNG parameters and C2PA credentials.")
|
| 260 |
-
|
| 261 |
-
v_col1, v_col2 = st.columns([1, 1])
|
| 262 |
-
with v_col1:
|
| 263 |
-
logo_region = st.selectbox(
|
| 264 |
-
"Visible Logo Region Target",
|
| 265 |
-
[
|
| 266 |
-
"Bottom Right Corner (Google Veo / Imagen / Sora)",
|
| 267 |
-
"Bottom Left Corner",
|
| 268 |
-
"Top Right Corner",
|
| 269 |
-
"Top Left Corner",
|
| 270 |
-
"Custom Bounding Box (x, y, w, h)"
|
| 271 |
-
],
|
| 272 |
-
index=0,
|
| 273 |
-
disabled=not opt_visible_logo,
|
| 274 |
-
help="Targets the region where the visible watermark logo is located."
|
| 275 |
-
)
|
| 276 |
-
|
| 277 |
-
logo_loc_key = "bottom_right"
|
| 278 |
-
if "Bottom Left" in logo_region:
|
| 279 |
-
logo_loc_key = "bottom_left"
|
| 280 |
-
elif "Top Right" in logo_region:
|
| 281 |
-
logo_loc_key = "top_right"
|
| 282 |
-
elif "Top Left" in logo_region:
|
| 283 |
-
logo_loc_key = "top_left"
|
| 284 |
-
elif "Custom" in logo_region:
|
| 285 |
-
logo_loc_key = "custom"
|
| 286 |
-
|
| 287 |
-
with v_col2:
|
| 288 |
-
preset_quality = st.select_slider("Video Quality (CRF)", options=[18, 20, 23, 26, 28], value=18, help="Lower CRF = higher video quality (18 = visually lossless).")
|
| 289 |
-
engine_backend = st.radio("Delogo Filter Engine", ["OpenCV Local Inpainter (High Quality)", "FFmpeg Delogo Filter (Ultra Fast)"], index=0, disabled=not opt_visible_logo)
|
| 290 |
-
backend_key = "ffmpeg_delogo" if "FFmpeg" in engine_backend else "opencv"
|
| 291 |
-
|
| 292 |
-
# Custom Bounding Box Controls
|
| 293 |
-
custom_bbox_val = None
|
| 294 |
-
if opt_visible_logo:
|
| 295 |
-
with st.expander("📐 Fine-Tune Bounding Box Coordinates (x, y, width, height)", expanded=(logo_loc_key == "custom")):
|
| 296 |
-
st.caption("Enter custom pixel coordinates or leave as -1 to use automatic corner calculation.")
|
| 297 |
-
col_x, col_y, col_w, col_h = st.columns(4)
|
| 298 |
-
cx = col_x.number_input("X Position (px)", value=-1, step=10)
|
| 299 |
-
cy = col_y.number_input("Y Position (px)", value=-1, step=10)
|
| 300 |
-
cw = col_w.number_input("Width (px)", value=-1, step=10)
|
| 301 |
-
ch = col_h.number_input("Height (px)", value=-1, step=10)
|
| 302 |
-
if cx >= 0 and cy >= 0 and cw > 0 and ch > 0:
|
| 303 |
-
custom_bbox_val = (int(cx), int(cy), int(cw), int(ch))
|
| 304 |
-
|
| 305 |
-
if video_file:
|
| 306 |
-
st.divider()
|
| 307 |
-
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 308 |
-
tmp_dir_p = Path(tmp_dir)
|
| 309 |
-
in_vid = tmp_dir_p / video_file.name
|
| 310 |
-
out_vid = tmp_dir_p / f"sanitized_{video_file.name}"
|
| 311 |
-
|
| 312 |
-
with open(in_vid, "wb") as f:
|
| 313 |
-
f.write(video_file.getbuffer())
|
| 314 |
-
|
| 315 |
-
st.video(str(in_vid))
|
| 316 |
-
|
| 317 |
-
if st.button("🚀 Process & Sanitize Video"):
|
| 318 |
-
progress_bar = st.progress(0, text="Initializing Video Pipeline...")
|
| 319 |
-
|
| 320 |
-
meta_info = probe_video_metadata(in_vid, FFMPEG_PATH)
|
| 321 |
-
calc_bbox = calculate_logo_bbox(meta_info["width"], meta_info["height"], location=logo_loc_key, custom_bbox=custom_bbox_val)
|
| 322 |
-
|
| 323 |
-
st.caption(f"📹 Video Specs: **{meta_info['width']}x{meta_info['height']} @ {meta_info['fps']:.2f} FPS** | Target Logo Bounding Box `(x={calc_bbox[0]}, y={calc_bbox[1]}, w={calc_bbox[2]}, h={calc_bbox[3]})`")
|
| 324 |
-
|
| 325 |
-
def on_progress(pct: int, msg: str):
|
| 326 |
-
progress_bar.progress(pct, text=msg)
|
| 327 |
-
|
| 328 |
-
try:
|
| 329 |
-
sanitize_video_pipeline(
|
| 330 |
-
input_video=in_vid,
|
| 331 |
-
output_video=out_vid,
|
| 332 |
-
ffmpeg_path=FFMPEG_PATH,
|
| 333 |
-
remove_visible_logo=opt_visible_logo,
|
| 334 |
-
remove_synthid=opt_synthid,
|
| 335 |
-
remove_metadata=opt_metadata,
|
| 336 |
-
logo_location=logo_loc_key,
|
| 337 |
-
custom_bbox=custom_bbox_val,
|
| 338 |
-
engine_backend=backend_key,
|
| 339 |
-
crf_quality=preset_quality,
|
| 340 |
-
progress_callback=on_progress
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
progress_bar.progress(100, text="Sanitization Complete!")
|
| 344 |
-
|
| 345 |
-
if out_vid.exists():
|
| 346 |
-
st.success("🎉 Video successfully sanitized! Clean MP4 container rebuilt & watermarks purged.")
|
| 347 |
-
st.video(str(out_vid))
|
| 348 |
-
|
| 349 |
-
with open(out_vid, "rb") as vf:
|
| 350 |
-
st.download_button(
|
| 351 |
-
label="⬇️ Download Sanitized Video",
|
| 352 |
-
data=vf.read(),
|
| 353 |
-
file_name=f"sanitized_{video_file.name}",
|
| 354 |
-
mime="video/mp4"
|
| 355 |
-
)
|
| 356 |
-
else:
|
| 357 |
-
st.error("Video assembly failed. Check FFmpeg output.")
|
| 358 |
-
except Exception as err:
|
| 359 |
-
st.error(f"Pipeline error during video processing: {err}")
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
# =============================================================================
|
| 363 |
-
# TAB 3: TEXT CLEANER
|
| 364 |
-
# =============================================================================
|
| 365 |
-
with tab3:
|
| 366 |
-
st.subheader("AI Text Sanitizer & Hidden Marker Purger")
|
| 367 |
-
st.markdown("Removes zero-width spaces (`\\u200B`), invisible Unicode tracking markers, homoglyphs, and AI conversational artifacts.")
|
| 368 |
-
|
| 369 |
-
text_input = st.text_area("Paste AI-Generated Text Here", height=200, placeholder="Paste text here to detect and purge hidden zero-width spaces and tracking markers...")
|
| 370 |
-
|
| 371 |
-
t_c1, t_c2 = st.columns([1, 1])
|
| 372 |
-
with t_c1:
|
| 373 |
-
opt_ai_phrases = st.checkbox("Remove AI Conversational Introductions/Outros", value=True)
|
| 374 |
-
with t_c2:
|
| 375 |
-
st.caption("Detects zero-width spaces, soft hyphens, directionality markers, and Cyrillic homoglyphs.")
|
| 376 |
-
|
| 377 |
-
if text_input.strip():
|
| 378 |
-
cleaned_text, stats = clean_text_content(text_input, remove_ai_phrases=opt_ai_phrases)
|
| 379 |
-
|
| 380 |
-
st.divider()
|
| 381 |
-
st.markdown("### 📊 Sanitization Results")
|
| 382 |
-
|
| 383 |
-
m1, m2, m3, m4 = st.columns(4)
|
| 384 |
-
m1.metric("Characters Before", stats["initial_len"])
|
| 385 |
-
m2.metric("Characters After", stats["final_len"])
|
| 386 |
-
m3.metric("Zero-Width Purged", stats["zero_width"], delta=f"-{stats['zero_width']}" if stats["zero_width"] > 0 else None)
|
| 387 |
-
m4.metric("Homoglyphs / Markers", stats["homoglyphs"] + stats["control_chars"], delta=f"-{stats['homoglyphs']}" if stats["homoglyphs"] > 0 else None)
|
| 388 |
-
|
| 389 |
-
st.markdown("### 📝 Cleaned Text")
|
| 390 |
-
st.text_area("Sanitized Output", value=cleaned_text, height=200, key="cleaned_txt_output")
|
| 391 |
-
|
| 392 |
-
st.download_button(
|
| 393 |
-
label="⬇️ Download Sanitized Text (.txt)",
|
| 394 |
-
data=cleaned_text,
|
| 395 |
-
file_name="sanitized_text.txt",
|
| 396 |
-
mime="text/plain"
|
| 397 |
-
)
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from api import app
|
|
|
|
| 3 |
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|