""" streamlit_app.py — ArtLens: AI vs Real Art Detector Hugging Face Space: Silvio0/Ai-Generated-vs-Real-Prediction """ import io import numpy as np import streamlit as st from pathlib import Path from PIL import Image # ── Path Configuration ─────────────────────────────────────────────────────── SRC_DIR = Path(__file__).resolve().parent BASE_DIR = SRC_DIR.parent MODEL_PATH = SRC_DIR / "model_final_ai_vs_real.keras" IMG_DIR = BASE_DIR / "img" AI_ART_DIR = IMG_DIR / "AiArt" REAL_ART_DIR = IMG_DIR / "RealArt" AI_SAMPLES = [AI_ART_DIR / f"Ai-Image-{i}.jpg" for i in range(1, 4)] REAL_SAMPLES = [REAL_ART_DIR / f"Real-Image-{i}.jpg" for i in range(1, 4)] IMG_SIZE = (224, 224) # ── Config TOML: XSRF fix + Force Light Mode ──────────────────────────────── _config_dir = Path.home() / ".streamlit" _config_file = _config_dir / "config.toml" _config_dir.mkdir(parents=True, exist_ok=True) if not _config_file.exists(): _config_file.write_text( "[server]\n" "enableXsrfProtection = false\n" "enableCORS = false\n" "maxUploadSize = 200\n\n" "[theme]\n" 'base = "light"\n' 'primaryColor = "#0ea5e9"\n' 'backgroundColor = "#f0f9ff"\n' 'secondaryBackgroundColor = "#e0f2fe"\n' 'textColor = "#0c1a2e"\n' ) # ── Page Config ────────────────────────────────────────────────────────────── st.set_page_config( page_title="ArtLens — AI vs Real Art", page_icon="🔍", layout="wide", initial_sidebar_state="collapsed", ) # ── Custom CSS ─────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── Model Loading ──────────────────────────────────────────────────────────── @st.cache_resource(show_spinner="⏳ Loading model, please wait...") def load_model(): try: import tensorflow as tf if not MODEL_PATH.exists(): st.error(f"❌ Model not found at: `{MODEL_PATH}`") return None return tf.keras.models.load_model(str(MODEL_PATH)) except Exception as exc: st.error(f"❌ Failed to load model: {exc}") return None # ── Inference ──────────────────────────────────────────────────────────────── def predict_image(model, pil_img: Image.Image) -> dict: img = pil_img.convert("RGB").resize(IMG_SIZE) arr = np.array(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, axis=0) raw = float(model.predict(arr, verbose=0)[0][0]) if raw >= 0.5: label, conf_real, conf_ai = "🖼️ Real Art", raw, 1.0 - raw else: label, conf_ai, conf_real = "🤖 AI Generated", 1.0 - raw, raw return {"label": label, "conf_ai": conf_ai, "conf_real": conf_real, "raw_prob": raw} # ── Helpers ────────────────────────────────────────────────────────────────── def safe_open_image(path: Path) -> "Image.Image | None": try: if not path.exists(): return None img = Image.open(path) img.load() # Force load seluruh data ke memory agar tidak ada lazy I/O return img except Exception: return None # Cached loader khusus untuk Example Images — cegah reload dari disk tiap rerun @st.cache_data(show_spinner=False) def load_example_thumbnail(path_str: str, target_w: int = 900, target_h: int = 675) -> "Image.Image | None": """Load + resize ke ukuran konsisten sehingga layout tidak bergeser.""" path = Path(path_str) try: if not path.exists(): return None img = Image.open(path).convert("RGB") img.load() # Crop tengah ke rasio 4:3 lalu resize — pastikan semua thumbnail sama tingginya orig_w, orig_h = img.size target_ratio = target_w / target_h orig_ratio = orig_w / orig_h if orig_ratio > target_ratio: # Terlalu lebar → crop kiri-kanan new_w = int(orig_h * target_ratio) left = (orig_w - new_w) // 2 img = img.crop((left, 0, left + new_w, orig_h)) else: # Terlalu tinggi → crop atas-bawah new_h = int(orig_w / target_ratio) top = (orig_h - new_h) // 2 img = img.crop((0, top, orig_w, top + new_h)) return img.resize((target_w, target_h), Image.LANCZOS) except Exception: return None def bytes_to_pil(data: bytes) -> Image.Image: return Image.open(io.BytesIO(data)) def render_prediction(result: dict): label = result["label"] conf_ai = result["conf_ai"] conf_real = result["conf_real"] dominant = max(conf_ai, conf_real) * 100 if "Real" in label: st.markdown(f"""

{label}

Model confidence: {dominant:.1f}%

""", unsafe_allow_html=True) else: st.markdown(f"""

{label}

Model confidence: {dominant:.1f}%

""", unsafe_allow_html=True) st.markdown(f"""
🤖 AI Generated {conf_ai*100:.1f}%
""", unsafe_allow_html=True) st.progress(conf_ai) st.markdown(f"""
🖼️ Real Art {conf_real*100:.1f}%
""", unsafe_allow_html=True) st.progress(conf_real) # ── Load Model ─────────────────────────────────────────────────────────────── model = load_model() # ── Hero Banner ────────────────────────────────────────────────────────────── st.markdown("""
✦ Powered by ResNet50V2 Transfer Learning

🔍 ArtLens

Detect whether artwork was created by AI or a human — fast, accurate, and easy to use.

""", unsafe_allow_html=True) # ── Tabs ───────────────────────────────────────────────────────────────────── tab_home, tab_single, tab_batch, tab_example, tab_info = st.tabs([ "🏠 Home", "🖼️ Single Image", "📂 Batch Analysis", "📌 Example Images", "ℹ️ Model Info", ]) # ── Auto-switch flag — JS akan diinjeksi di BAWAH setelah semua tab di-render ── _should_switch_to_single = st.session_state.pop("go_to_single", False) # ═════════════════════════════════════════════════════════════════════════════ # ============================================================================= # TAB 0 — HOME # ============================================================================= with tab_home: st.markdown('
👋 Welcome to ArtLens
', unsafe_allow_html=True) # What is ArtLens card st.markdown("""

🔍 What is ArtLens?

ArtLens is an AI-powered detector that tells you whether artwork was created by a human or generated by AI. It uses a ResNet50V2 model trained on thousands of paintings and AI-generated images.

""", unsafe_allow_html=True) # How to use card st.markdown("""

🚀 How to Use

1

Go to the Single Image tab

Submit one image and see the prediction result right away.

2

Choose your input method

Upload File — drag & drop or browse a JPG / PNG / WEBP from your device.
Live Camera — point your camera at an artwork and capture a photo on the spot.

3

Or try an example image first

Head to the 📌 Example Images tab. Pick the 🤖 AI Art or 🖼️ Real Art sub-tab, then click Use on any image.
Once the button shows ✅ Selected, head back to the Single Image tab yourself — the prediction will run right away.

4

Read the result

The right panel shows AI Generated or Real Art, with a confidence score and breakdown bar for both classes.

""", unsafe_allow_html=True) # Quick tips card st.markdown("""

💡 Quick Tips

""", unsafe_allow_html=True) # TAB 1 — SINGLE IMAGE # ═════════════════════════════════════════════════════════════════════════════ with tab_single: col_left, col_right = st.columns([1, 1], gap="large") # ── Left Column: Input ───────────────────────────────────────────────── with col_left: st.markdown('
📥 Image Input
', unsafe_allow_html=True) input_mode = st.radio( "mode", options=["📁 Upload File", "📷 Live Camera"], horizontal=True, label_visibility="collapsed", key="input_mode", ) pil_input: "Image.Image | None" = None # ── Upload File ────────────────────────────────────────────────── if "Upload" in input_mode: uploaded = st.file_uploader( " ", type=["jpg", "jpeg", "png", "webp"], label_visibility="collapsed", key="file_uploader", ) st.caption("Supports JPG · PNG · WEBP · up to 200 MB") if uploaded is not None: # Cache ke session_state agar tidak re-read bytes tiap rerun → cegah flicker if st.session_state.get("_upload_cached_name") != uploaded.name: raw_img = bytes_to_pil(uploaded.read()) raw_img.load() # Force load ke memory st.session_state["_upload_cached_pil"] = raw_img st.session_state["_upload_cached_name"] = uploaded.name pil_input = st.session_state["_upload_cached_pil"] st.session_state.pop("camera_result", None) st.session_state.pop("example_img", None) st.session_state.pop("example_name", None) st.markdown( f'
📎 {uploaded.name}
', unsafe_allow_html=True ) st.image(pil_input, caption=f"📎 {uploaded.name}", use_container_width=True) # ── Live Camera ──────────────────────────────────────────────── else: st.info("📷 Point your camera at the artwork, then click **Take Photo** — preview and prediction will appear instantly.") cam_photo = st.camera_input( "Take a live photo", label_visibility="collapsed", key="camera_widget", ) if cam_photo is not None: # Cache camera photo in session_state to prevent flicker on rerun cam_id = hash(cam_photo.getvalue()) if st.session_state.get("_cam_cached_id") != cam_id: raw_cam = bytes_to_pil(cam_photo.getvalue()) raw_cam.load() # Force full load into memory st.session_state["_cam_cached_pil"] = raw_cam st.session_state["_cam_cached_id"] = cam_id pil_input = st.session_state["_cam_cached_pil"] st.session_state.pop("example_img", None) st.session_state.pop("example_name", None) # No manual preview here — st.camera_input already shows the captured photo # ── Selected Example Preview ────────────────────────────────── if pil_input is None and "example_img" in st.session_state: ex_name = st.session_state.get("example_name", "Example Image") st.markdown( f'
✅ Using: {ex_name}
', unsafe_allow_html=True ) st.image( st.session_state["example_img"], caption=ex_name, use_container_width=True, ) _, col_del2 = st.columns([3, 1]) with col_del2: if st.button("🗑️ Delete", key="clear_example"): st.session_state.pop("example_img", None) st.session_state.pop("example_name", None) st.rerun() # Use example if no other input available if pil_input is None and "example_img" in st.session_state: pil_input = st.session_state["example_img"] # ── Right Column: Prediction Results ─────────────────────────────────────── with col_right: st.markdown('
🔮 Prediction Results
', unsafe_allow_html=True) if pil_input is None: st.info( "👈 **Start here!** \n" "Upload an image, take a photo, or pick one from the **📌 Example Images** tab." ) elif model is None: st.error("❌ Model failed to load. Make sure the `.keras` file is available in the `src/` folder.") else: with st.spinner("🔍 Analyzing image..."): result = predict_image(model, pil_input) render_prediction(result) # ═════════════════════════════════════════════════════════════════════════════ # TAB 2 — BATCH ANALYSIS # ═════════════════════════════════════════════════════════════════════════════ with tab_batch: st.markdown('
📂 Batch Analysis
', unsafe_allow_html=True) st.caption("Upload multiple images at once for simultaneous analysis.") batch_files = st.file_uploader( "Upload images (can be more than one)", type=["jpg", "jpeg", "png", "webp"], accept_multiple_files=True, key="batch_uploader", ) if batch_files: if model is None: st.error("❌ Model failed to load.") else: results = [] progress_bar = st.progress(0, text="Analyzing...") preview_cols = st.columns(min(len(batch_files), 4)) for i, f in enumerate(batch_files): img = bytes_to_pil(f.read()) res = predict_image(model, img) results.append({ "File Name": f.name, "Prediction": res["label"], "Conf. AI (%)": f"{res['conf_ai']*100:.1f}", "Conf. Real Art (%)": f"{res['conf_real']*100:.1f}", }) with preview_cols[i % 4]: st.image(img, caption=f.name[:20], use_container_width=True) if "Real" in res["label"]: st.success(res["label"], icon="🖼️") else: st.warning(res["label"], icon="🤖") progress_bar.progress( (i + 1) / len(batch_files), text=f"Analyzing {i+1}/{len(batch_files)}..." ) progress_bar.empty() st.divider() st.markdown(f'
📊 Results — {len(results)} Images
', unsafe_allow_html=True) try: import pandas as pd df = pd.DataFrame(results) st.dataframe(df, use_container_width=True) ai_count = sum(1 for r in results if "AI" in r["Prediction"]) real_count = len(results) - ai_count c1, c2, c3 = st.columns(3) c1.metric("Total Images", len(results)) c2.metric("🤖 AI Generated", ai_count) c3.metric("🖼️ Real Art", real_count) except ImportError: for r in results: st.write(r) # ═════════════════════════════════════════════════════════════════════════════ # TAB 3 — EXAMPLE IMAGES # ═════════════════════════════════════════════════════════════════════════════ with tab_example: st.markdown('
📌 Example Images
', unsafe_allow_html=True) st.caption("Click **Use** on any image to try it — you will be taken to the Single Image tab automatically.") # Sub-tabs: AI Art vs Real Art — single column layout inside each ex_tab_ai, ex_tab_real = st.tabs(["🤖 AI Art", "🖼️ Real Art"]) with ex_tab_ai: any_ai = False for i, path in enumerate(AI_SAMPLES): img = load_example_thumbnail(str(path)) if img: any_ai = True is_selected = st.session_state.get("example_name") == f"🤖 AI Art #{i+1}" st.image(img, caption=f"AI Art #{i+1}", use_container_width=True) btn_label = "✅ Selected" if is_selected else f"Use AI Art #{i+1}" if st.button(btn_label, key=f"ex_use_ai_{i}", disabled=is_selected, use_container_width=True): st.session_state["example_img"] = safe_open_image(path) st.session_state["example_name"] = f"🤖 AI Art #{i+1}" st.session_state.pop("camera_result", None) st.session_state["go_to_single"] = True st.rerun() if not any_ai: st.warning("⚠️ AI Art example images not found. Make sure the `img/AiArt/` folder exists.") with ex_tab_real: any_real = False for i, path in enumerate(REAL_SAMPLES): img = load_example_thumbnail(str(path)) if img: any_real = True is_selected = st.session_state.get("example_name") == f"🖼️ Real Art #{i+1}" st.image(img, caption=f"Real Art #{i+1}", use_container_width=True) btn_label = "✅ Selected" if is_selected else f"Use Real Art #{i+1}" if st.button(btn_label, key=f"ex_use_real_{i}", disabled=is_selected, use_container_width=True): st.session_state["example_img"] = safe_open_image(path) st.session_state["example_name"] = f"🖼️ Real Art #{i+1}" st.session_state.pop("camera_result", None) st.session_state["go_to_single"] = True st.rerun() if not any_real: st.warning("⚠️ Real Art example images not found. Make sure the `img/RealArt/` folder exists.") # ═════════════════════════════════════════════════════════════════════════════ # TAB 4 — MODEL INFO # ═════════════════════════════════════════════════════════════════════════════ with tab_info: st.markdown('
ℹ️ Model Information
', unsafe_allow_html=True) col_a, col_b = st.columns(2) with col_a: st.markdown("**📋 Configuration**") st.markdown(f""" | Item | Value | |------|-------| | Architecture | Transfer Learning (ResNet50V2) | | Input Size | `{IMG_SIZE[0]} × {IMG_SIZE[1]}` px | | Output | Binary (AI / Real) | | Threshold | `0.5` (sigmoid) | | Model | `{MODEL_PATH.name}` | | Status | `{"✅ Available" if MODEL_PATH.exists() else "❌ Not found"}` | """) with col_b: st.markdown("**📁 Path Structure**") st.code( f"BASE_DIR : {BASE_DIR}\n" f"SRC_DIR : {SRC_DIR}\n" f"IMG_DIR : {IMG_DIR}\n" f" AiArt/ : {AI_ART_DIR}\n" f" Real/ : {REAL_ART_DIR}" ) st.divider() if model is not None: st.success("✅ Model loaded successfully and ready to use.") with st.expander("Model Summary — click to view"): lines: list = [] model.summary(print_fn=lambda x: lines.append(x)) st.code("\n".join(lines), language="text") else: st.error("❌ Model failed to load. Make sure the `.keras` file is in the `src/` folder.") st.divider() st.markdown(""" **ℹ️ Deployment Notes on HuggingFace Spaces** If you encounter an `AxiosError 403` error when uploading images, add a `.streamlit/config.toml` file to the root of your repo: ```toml [server] enableXsrfProtection = false enableCORS = false maxUploadSize = 200 ``` Or make sure your Dockerfile includes: ```dockerfile COPY .streamlit /app/.streamlit ``` """) # ═════════════════════════════════════════════════════════════════════════════ # AUTO-SWITCH — Injeksi JS DI SINI, setelah SEMUA tab selesai di-render # Dengan retry logic: coba klik sampai 15x tiap 100ms jika tab belum siap # ═════════════════════════════════════════════════════════════════════════════ if _should_switch_to_single: st.markdown(""" """, unsafe_allow_html=True) # ═════════════════════════════════════════════════════════════════════════════ # FOOTER # ═════════════════════════════════════════════════════════════════════════════ st.markdown("""

Developed by

Gabriella Jovanka Bustan — A11.2023.14861
Silvio Christian, Joe — A11.2023.14864
Muhamad Taqi — A11.2023.14888
Hanaafi Arya Ditta — A11.2023.15132

""", unsafe_allow_html=True)