vibeautomation commited on
Commit
e63b97b
·
verified ·
1 Parent(s): d1a4273

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +4 -396
app.py CHANGED
@@ -1,397 +1,5 @@
1
- """
2
- SanitizeAI - Watermark & Metadata Sanitizer Web Application
3
- Built with Streamlit and the SanitizeAI core engine.
4
 
5
- Tabs:
6
- 1. Image Cleaner: Removes EXIF/PNG AI metadata (SD, ComfyUI, Midjourney, C2PA) and invisible AI watermarks.
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)