Spaces:
Running
Running
| import hashlib | |
| import os | |
| import streamlit as st | |
| import requests | |
| from PIL import Image | |
| from io import BytesIO | |
| CACHE_DIR = os.path.join(os.path.dirname(__file__), "image_cache") | |
| os.makedirs(CACHE_DIR, exist_ok=True) | |
| def _cache_path(url: str) -> str: | |
| """Return the local file path for a cached image URL.""" | |
| key = hashlib.md5(url.encode()).hexdigest() | |
| return os.path.join(CACHE_DIR, f"{key}.png") | |
| def resolve_image_url(url: str) -> str: | |
| """If the URL is a Wikimedia Commons File: page, resolve it to the direct | |
| image URL via the MediaWiki API. Otherwise return the URL unchanged. | |
| Cached in-memory so each File: page is only looked up once per session.""" | |
| if "commons.wikimedia.org/wiki/File:" in url: | |
| filename = url.split("/wiki/File:")[-1] | |
| api_url = ( | |
| "https://commons.wikimedia.org/w/api.php" | |
| f"?action=query&titles=File:{filename}" | |
| "&prop=imageinfo&iiprop=url&format=json" | |
| ) | |
| headers = {"User-Agent": "Mozilla/5.0 (compatible; StreamlitApp/1.0; +https://streamlit.io)"} | |
| try: | |
| r = requests.get(api_url, headers=headers, timeout=10) | |
| pages = r.json()["query"]["pages"] | |
| page = next(iter(pages.values())) | |
| return page["imageinfo"][0]["url"] | |
| except Exception: | |
| return url | |
| return url | |
| def load_image(url: str): | |
| """Load an image from disk cache if available, otherwise download it, | |
| save it to the cache folder, and return a PIL Image. | |
| The cache persists across app restarts — images are only downloaded once.""" | |
| path = _cache_path(url) | |
| if os.path.exists(path): | |
| try: | |
| return Image.open(path) | |
| except Exception: | |
| pass # corrupted cache file — re-download below | |
| headers = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (compatible; StreamlitApp/1.0; " | |
| "+https://streamlit.io)" | |
| ) | |
| } | |
| try: | |
| direct_url = resolve_image_url(url) | |
| response = requests.get(direct_url, headers=headers, timeout=10) | |
| response.raise_for_status() | |
| img = Image.open(BytesIO(response.content)) | |
| img.save(path, format="PNG") # persist to disk | |
| return img | |
| except Exception: | |
| return None | |
| # List of 8 scenarios based on the spreadsheet rules | |
| # Images sourced from Wikimedia Commons (public domain / CC licensed) | |
| # Supports both commons.wikimedia.org/wiki/File: page URLs and direct upload URLs | |
| scenarios = [ | |
| { | |
| # Scenario 1: Different pictures of the same person (Messi) | |
| "label": "Different pictures of the same person", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Lionel-Messi-Argentina-2022-FIFA-World-Cup_(cropped).jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg", | |
| "answer": "Yes", | |
| "feedback": "Different photos of the same subject should be marked Yes." | |
| }, | |
| { | |
| # Scenario 2: Identical or resized picture (same image, two sizes) | |
| "label": "Identical or resized picture", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg", | |
| "answer": "Yes", | |
| "feedback": "Identical images (even at different sizes) require a Yes response." | |
| }, | |
| { | |
| # Scenario 3: Different pictures of the same landmark (Eiffel Tower) | |
| "label": "Different pictures of the same landmark", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Tour_eiffel_at_sunrise_from_the_trocadero.jpg", | |
| "answer": "Yes", | |
| "feedback": "Different photos of the same landmark are the same subject." | |
| }, | |
| { | |
| # Scenario 4: Subject vs. representation (Eiffel Tower vs. keychain) | |
| "label": "Subject vs. representation (landmark vs. keychain)", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Eiffel_Tower_Keychain.jpg", | |
| "answer": "No", | |
| "feedback": "A landmark and a keychain are not the same subject." | |
| }, | |
| { | |
| # Scenario 5: Person vs. associated item (player vs. jersey) | |
| "label": "Person vs. associated item (player vs. jersey)", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Adidas_Messi_shirt_rear.JPG", | |
| "answer": "No", | |
| "feedback": "A person and an associated item are not the same subject." | |
| }, | |
| { | |
| # Scenario 6: Person vs. their signature | |
| "label": "Person vs. their signature", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:President_Barack_Obama.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Health_insurance_reform_bill_signature_20100323_(1).jpg", | |
| "answer": "No", | |
| "feedback": "A person and a signature are not the same subject." | |
| }, | |
| { | |
| # Scenario 7: Person vs. their tombstone | |
| "label": "Person vs. their tombstone", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_by_Napoleon_Sarony._Three-quarter-length_photograph,_seated.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_%C3%A9_mort_dans_cette_maison.jpg", | |
| "answer": "No", | |
| "feedback": "A person and their tombstone are not the same subject." | |
| }, | |
| { | |
| # Scenario 8: Same person at different ages (Einstein young vs old) | |
| "label": "Same person at different ages", | |
| "image1_url": "https://commons.wikimedia.org/wiki/File:JimmyCarterPortrait2.jpg", | |
| "image2_url": "https://commons.wikimedia.org/wiki/File:Jimmy_Carter_and_Rosalynn_Carter_on_Plains_Peanut_Festival_(cropped).jpg", | |
| "answer": "Yes", | |
| "feedback": "The same person at different ages is still the same subject." | |
| } | |
| ] | |
| def check_answer(scenario_idx, user_answer): | |
| scenario = scenarios[scenario_idx] | |
| correct = scenario["answer"] | |
| feedback = scenario["feedback"] | |
| if user_answer == correct: | |
| return f"✅ Correct! {feedback}" | |
| else: | |
| return f"❌ Incorrect. The correct answer is **{correct}**. {feedback}" | |
| # --- Session state init --- | |
| if "scenario_idx" not in st.session_state: | |
| st.session_state.scenario_idx = 0 | |
| if "feedback" not in st.session_state: | |
| st.session_state.feedback = None | |
| if "answered" not in st.session_state: | |
| st.session_state.answered = False | |
| # Preload all images once at startup | |
| if "images" not in st.session_state: | |
| with st.spinner("Loading all images, please wait…"): | |
| st.session_state.images = [ | |
| ( | |
| load_image(s["image1_url"]), | |
| load_image(s["image2_url"]), | |
| ) | |
| for s in scenarios | |
| ] | |
| st.title("Image Subject Comparison Tutorial") | |
| st.markdown( | |
| "This is a small tutorial for Depictor. It is meant to provide examples for how to make " | |
| "the decisions for the questions asked." | |
| ) | |
| st.markdown("---") | |
| st.markdown( | |
| "This tutorial demonstrates rules for determining if two images depict the same subject. " | |
| "Answer each scenario to advance to the next one." | |
| ) | |
| idx = st.session_state.scenario_idx | |
| total = len(scenarios) | |
| if idx >= total: | |
| st.success("🎉 You've completed all scenarios! Well done.") | |
| if st.button("🔄 Restart"): | |
| st.session_state.scenario_idx = 0 | |
| st.session_state.feedback = None | |
| st.session_state.answered = False | |
| st.rerun() | |
| else: | |
| scenario = scenarios[idx] | |
| st.markdown(f"### Scenario {idx + 1} of {total}: *{scenario['label']}*") | |
| st.progress(idx / total) | |
| img1, img2 = st.session_state.images[idx] | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if img1: | |
| st.image(img1, caption="Image 1", use_container_width=True) | |
| else: | |
| st.error("Image 1 could not be loaded.") | |
| st.markdown(f"[🔗 View source]({scenario['image1_url']})") | |
| with col2: | |
| if img2: | |
| st.image(img2, caption="Image 2", use_container_width=True) | |
| else: | |
| st.error("Image 2 could not be loaded.") | |
| st.markdown(f"[🔗 View source]({scenario['image2_url']})") | |
| st.markdown("### Are these the same subject?") | |
| if not st.session_state.answered: | |
| btn_col1, btn_col2 = st.columns(2) | |
| with btn_col1: | |
| if st.button("✅ Yes", use_container_width=True): | |
| st.session_state.feedback = check_answer(idx, "Yes") | |
| st.session_state.answered = True | |
| st.rerun() | |
| with btn_col2: | |
| if st.button("❌ No", use_container_width=True): | |
| st.session_state.feedback = check_answer(idx, "No") | |
| st.session_state.answered = True | |
| st.rerun() | |
| else: | |
| st.info(st.session_state.feedback) | |
| if st.button("➡️ Next Scenario", use_container_width=True): | |
| st.session_state.scenario_idx += 1 | |
| st.session_state.feedback = None | |
| st.session_state.answered = False | |
| st.rerun() | |