import streamlit as st import pandas as pd import os # ========================================== # 1. PAGE CONFIGURATION & STATE # ========================================== st.set_page_config(page_title="Horror Reference Library", layout="wide") st.title("📽️ Horror Reference Library") st.markdown("### Search 11,500+ Cinematic AI-Tagged Comic Panels") # This is the "Memory" for the Load More button if 'display_limit' not in st.session_state: st.session_state.display_limit = 100 # This resets the count back to 100 anytime you change a filter def reset_limit(): st.session_state.display_limit = 100 # ========================================== # 2. DATA BUCKETING & CLEANING # ========================================== def categorize_camera(text): text = str(text).lower() if 'dutch' in text: return 'Dutch Angle' elif 'extreme close' in text or 'ecu' in text: return 'Extreme Close Up' elif 'close' in text or 'cu' in text: return 'Close Up' elif 'wide' in text or 'long' in text or 'establishing' in text: return 'Wide Shot' elif 'mid' in text or 'medium' in text: return 'Mid Shot' elif 'low angle' in text or 'looking up' in text: return 'Low Angle' elif 'high angle' in text or 'looking down' in text: return 'High Angle' elif 'pov' in text or 'point of view' in text: return 'Point of View' else: return 'Other / Mixed' def categorize_mood(text): text = str(text).lower() if 'tense' in text or 'suspense' in text or 'anxiety' in text: return 'Tense & Suspenseful' elif 'action' in text or 'chaos' in text or 'dynamic' in text: return 'Action & Chaos' elif 'creepy' in text or 'eerie' in text or 'ominous' in text: return 'Creepy & Eerie' elif 'gore' in text or 'violent' in text or 'blood' in text: return 'Gore & Violence' elif 'sad' in text or 'melancholy' in text or 'somber' in text: return 'Somber & Melancholic' else: return 'Neutral / Standard' def categorize_lighting(text): text = str(text).lower() if 'silhouette' in text: return 'Silhouetted' elif 'high contrast' in text or 'chiaroscuro' in text: return 'High Contrast' elif 'low key' in text or 'shadow' in text or 'dark' in text: return 'Low Key (Shadowy)' elif 'harsh' in text or 'bright' in text: return 'Harsh & Bright' elif 'flat' in text or 'even' in text: return 'Flat Lighting' else: return 'Standard Lighting' def categorize_location(row): text = str(row.get('location_setup', '')).lower() + " " + str(row.get('description', '')).lower() if any(w in text for w in ['indoor', 'interior', 'room', 'house', 'building', 'office', 'corridor', 'hallway', 'wall', 'window', 'door', 'basement', 'stairs']): return 'Indoor' if any(w in text for w in ['outdoor', 'exterior', 'street', 'sky', 'forest', 'mountain', 'landscape', 'city', 'outside', 'woods', 'road', 'night', 'moon', 'ocean']): return 'Outdoor' return 'Unspecified / Mixed' def categorize_subject(row): text = str(row.get('staging', '')).lower() + " " + str(row.get('description', '')).lower() if any(w in text for w in ['group', 'crowd', 'three', 'four', 'multiple', 'several', 'guests', 'army', 'mob', 'people']): return 'Group (3+ People)' if any(w in text for w in ['two', 'couple', 'duo', 'both', 'pair']): return 'Two Characters' if any(w in text for w in ['man', 'woman', 'boy', 'girl', 'figure', 'character', 'person', 'creature', 'monster']): return 'Single Subject' return 'Object / Environment' def categorize_action(row): text = str(row.get('staging', '')).lower() + " " + str(row.get('description', '')).lower() if any(w in text for w in ['action', 'fight', 'strike', 'combat', 'running', 'chasing', 'attack', 'lunging', 'falling', 'fleeing', 'struggle', 'violence', 'grab']): return 'Action Sequence' if any(w in text for w in ['dialogue', 'talking', 'discussing', 'speaking', 'speech', 'conversation', 'yelling', 'screaming', 'whispering', 'saying']): return 'Dialogue / Conversation' if any(w in text for w in ['reacts', 'reaction', 'looking', 'staring', 'observing', 'gazing', 'watching', 'shock', 'listening']): return 'Reaction / Observation' return 'Static / Establishing' @st.cache_data def load_data(): df = pd.read_csv("horror_shot_database.csv") df['broad_camera'] = df['camera_angle'].apply(categorize_camera) df['broad_mood'] = df['mood'].apply(categorize_mood) df['broad_lighting'] = (df['mood'].fillna('') + " " + df['description'].fillna('')).apply(categorize_lighting) df['location_type'] = df.apply(categorize_location, axis=1) df['subject_type'] = df.apply(categorize_subject, axis=1) df['action_type'] = df.apply(categorize_action, axis=1) return df try: df = load_data() except Exception as e: st.error(f"Error loading database: {e}") st.stop() # ========================================== # 3. SHOTDECK-STYLE SEARCH & FILTERS # ========================================== st.sidebar.header("🔍 Search Library") # Notice the on_change=reset_limit on all inputs now! search_query = st.sidebar.text_input("Keyword Search", placeholder="e.g., monster, shadow, weapon...", on_change=reset_limit) st.sidebar.write("---") st.sidebar.header("📂 Filter Categories") with st.sidebar.expander("🌍 Location & Subjects", expanded=True): all_locations = ["Any"] + sorted(df['location_type'].unique().tolist()) selected_location = st.selectbox("Setting", all_locations, on_change=reset_limit) all_subjects = ["Any"] + sorted(df['subject_type'].unique().tolist()) selected_subject = st.selectbox("Characters in Frame", all_subjects, on_change=reset_limit) with st.sidebar.expander("🎬 Action & Scene Type", expanded=True): all_actions = ["Any"] + sorted(df['action_type'].unique().tolist()) selected_action = st.selectbox("Scene Action", all_actions, on_change=reset_limit) with st.sidebar.expander("🎥 Camera & Framing"): all_angles = ["Any"] + sorted(df['broad_camera'].unique().tolist()) selected_angle = st.selectbox("Shot Type", all_angles, on_change=reset_limit) with st.sidebar.expander("🎭 Atmosphere"): all_lighting = ["Any"] + sorted(df['broad_lighting'].unique().tolist()) selected_lighting = st.selectbox("Lighting Style", all_lighting, on_change=reset_limit) all_moods = ["Any"] + sorted(df['broad_mood'].unique().tolist()) selected_mood = st.selectbox("Mood", all_moods, on_change=reset_limit) # ========================================== # 4. FILTERING LOGIC # ========================================== results = df.copy() if search_query: results = results[results['description'].str.contains(search_query, case=False, na=False)] if selected_location != "Any": results = results[results['location_type'] == selected_location] if selected_subject != "Any": results = results[results['subject_type'] == selected_subject] if selected_action != "Any": results = results[results['action_type'] == selected_action] if selected_angle != "Any": results = results[results['broad_camera'] == selected_angle] if selected_lighting != "Any": results = results[results['broad_lighting'] == selected_lighting] if selected_mood != "Any": results = results[results['broad_mood'] == selected_mood] base_url = "https://huggingface.co/datasets/Roshanurs/Horror-Reference-Data/resolve/main/Panels_Out" valid_images = [] for idx, row in results.iterrows(): img_url = f"{base_url}/{row['filename']}" valid_images.append({ "url": img_url, "filename": row['filename'], "desc": row['description'] }) st.markdown(f"**Found {len(valid_images)} matching shots**") st.write("---") # ========================================== # 5. THE HTML GALLERY (Ultimate Anti-Flicker) # ========================================== if len(valid_images) > 0: current_limit = st.session_state.display_limit display_list = valid_images[:current_limit] for i in range(0, len(display_list), 4): cols = st.columns(4) for j in range(4): if i + j < len(display_list): img_data = display_list[i + j] with cols[j]: # We bypass st.image and use pure HTML with loading="lazy" html_card = f"""
""" st.markdown(html_card, unsafe_allow_html=True) # The Load More Button if len(valid_images) > current_limit: st.write("---") col1, col2, col3 = st.columns([1, 2, 1]) with col2: if st.button("⬇️ Load 100 More Images", use_container_width=True): st.session_state.display_limit += 100 st.rerun() else: st.warning("No shots found matching those exact parameters. Try widening your search!")