| import streamlit as st |
| import pandas as pd |
| import os |
|
|
| |
| |
| |
| 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") |
|
|
| |
| if 'display_limit' not in st.session_state: |
| st.session_state.display_limit = 100 |
|
|
| |
| def reset_limit(): |
| st.session_state.display_limit = 100 |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| st.sidebar.header("π Search Library") |
|
|
| |
| 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) |
|
|
| |
| |
| |
| 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("---") |
|
|
| |
| |
| |
| 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]: |
| |
| html_card = f""" |
| <div style="margin-bottom: 15px;"> |
| <img src='{img_data["url"]}' style='width: 100%; border-radius: 6px; display: block;' loading='lazy'> |
| <a href='{img_data["url"]}' target='_blank'> |
| <button style='width:100%; padding:8px; margin-top: 8px; border-radius:4px; border:1px solid #444; background:#222; color:white; cursor:pointer;'> |
| View Full Size |
| </button> |
| </a> |
| </div> |
| """ |
| st.markdown(html_card, unsafe_allow_html=True) |
| |
| |
| 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!") |