"""
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
- Use clear, high-resolution images for the best accuracy.
- Analyse multiple images at once with the 📂 Batch Analysis tab.
- Curious about the model? Check the ℹ️ Model Info tab.
""", 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)