ShutterSearch / app.py
Subash-Lamichhane's picture
Merge github code to hugging face
75932df verified
Raw
History Blame
16.2 kB
# import gradio as gr
# # Ensure the new dropdown helpers are imported
# from logic import (
# run_ingest, run_search, load_caption_browser, load_collections_view,
# update_collections_dropdown, update_search_dropdown
# )
# from caption_store import entry_count, get_all_collections
# # Import UI Views
# from ui.sidebar import render_sidebar
# from ui.home import render_home
# from ui.ingest import render_ingest
# from ui.search import render_search
# from ui.collections import render_collections
# from ui.captions import render_captions
# from ui.about import render_about
# # Define explicit Dark Mode theme variables
# # dark_theme = gr.themes.Soft(
# # primary_hue="green",
# # secondary_hue="emerald"
# # ).set(
# # body_background_fill="#0b0f19",
# # body_background_fill_dark="#0b0f19",
# # block_background_fill="#1e293b",
# # block_background_fill_dark="#1e293b",
# # block_border_color="#334155",
# # block_border_color_dark="#334155",
# # input_background_fill="#0f172a",
# # input_background_fill_dark="#0f172a",
# # text_color="#f1f5f9",
# # text_color_dark="#f1f5f9",
# # )
# # Load Custom Stylesheet
# with open("style.css", "r") as f:
# custom_css = f.read()
# with gr.Blocks(css=custom_css) as demo:
# # 1. Render Navigation Sidebar
# nav_btns, stats_label = render_sidebar()
# # 2. Render Main Content Container Pages
# with gr.Column(elem_classes="main-content"):
# home_page, start_btn = render_home()
# ingest_page, upload, coll_dropdown, use_new_coll, new_coll_name, ingest_btn, ingest_status = render_ingest()
# search_page, search_query, search_col_filter, search_btn, search_status_msg, search_gallery = render_search()
# colls_page, view_coll_selector, refresh_coll_btn, coll_status, coll_gallery = render_collections()
# caps_page, refresh_cap_btn, cap_status, caption_table = render_captions()
# about_page = render_about()
# # Routing definitions
# pages = [home_page, ingest_page, search_page, colls_page, caps_page, about_page]
# page_names = ["home", "ingest", "search", "collections", "captions", "about"]
# def switch_page(page_name):
# return [gr.update(visible=(name == page_name)) for name in page_names]
# # --- Page Routing Event Handlers ---
# nav_btns["home"].click(fn=lambda: switch_page("home"), outputs=pages)
# nav_btns["ingest"].click(fn=lambda: switch_page("ingest"), outputs=pages)
# # Use logic helpers to keep dropdown selections safe when changing tabs
# nav_btns["search"].click(
# fn=lambda: switch_page("search") + [update_search_dropdown()],
# outputs=pages + [search_col_filter]
# )
# nav_btns["collections"].click(
# fn=lambda: switch_page("collections") + [update_collections_dropdown()],
# outputs=pages + [view_coll_selector]
# )
# nav_btns["captions"].click(fn=lambda: switch_page("captions"), outputs=pages)
# nav_btns["about"].click(fn=lambda: switch_page("about"), outputs=pages)
# start_btn.click(fn=lambda: switch_page("ingest"), outputs=pages)
# # --- Component Value Interaction Bindings ---
# # Toggle dropdown/text field depending on toggle state
# def toggle_collection_inputs(is_new):
# return (
# gr.update(visible=not is_new), # Hide selector if creating new
# gr.update(visible=is_new) # Show text box if creating new
# )
# use_new_coll.change(
# fn=toggle_collection_inputs,
# inputs=use_new_coll,
# outputs=[coll_dropdown, new_coll_name]
# )
# ingest_btn.click(
# fn=run_ingest,
# inputs=[upload, coll_dropdown, use_new_coll, new_coll_name],
# outputs=[ingest_status, caption_table, coll_dropdown]
# ).then(
# fn=lambda: f"Total Photos: {entry_count()}",
# outputs=stats_label
# )
# # Search Logic Action triggers
# search_btn.click(fn=run_search, inputs=[search_query, search_col_filter], outputs=[search_gallery, search_status_msg])
# search_query.submit(fn=run_search, inputs=[search_query, search_col_filter], outputs=[search_gallery, search_status_msg])
# # Gallery view loaders
# view_coll_selector.change(fn=load_collections_view, inputs=view_coll_selector, outputs=[coll_gallery, coll_status])
# refresh_coll_btn.click(fn=load_collections_view, inputs=view_coll_selector, outputs=[coll_gallery, coll_status])
# # Captions refresh actions
# refresh_cap_btn.click(fn=load_caption_browser, outputs=[caption_table, cap_status])
# # Initial startup data population
# demo.load(fn=load_caption_browser, outputs=[caption_table, cap_status])
# demo.load(
# fn=lambda: load_collections_view(get_all_collections()[0] if get_all_collections() else "General"),
# outputs=[coll_gallery, coll_status]
# )
# if __name__ == "__main__":
# demo.launch()
import os
import gradio as gr
# Import robust business logic operations
from logic import (
run_ingest, run_search, load_caption_browser, load_collections_view,
update_collections_dropdown, update_search_dropdown, zip_selected_files
)
from caption_store import entry_count, get_all_collections
# Import modular UI Page builders
from ui.sidebar import render_sidebar
from ui.home import render_home
from ui.ingest import render_ingest
from ui.search import render_search
from ui.collections import render_collections
from ui.captions import render_captions
from ui.about import render_about
# Load layout parameters from global stylesheet
with open("style.css", "r") as f:
custom_css = f.read()
with gr.Blocks(css=custom_css, title="ShutterSearch — Photo Archive", fill_height=False) as demo:
# 1. Render Navigation Sidebar
nav_btns = render_sidebar()
# 2. Render Main Content Container Pages
with gr.Column(elem_classes="main-content"):
home_page, start_btn = render_home()
ingest_page, upload, coll_dropdown, use_new_coll, new_coll_name, ingest_btn, ingest_status = render_ingest()
# Search layout with selection returns
(
search_page, search_query, search_col_filter, search_btn, search_status_msg, search_gallery,
selected_search_paths, loaded_search_paths, search_selection_status, search_download_btn,
search_download_file, search_clear_selection_btn, search_select_all_btn
) = render_search()
# Collections layout with selection returns
(
colls_page, view_coll_selector, refresh_coll_btn, coll_status, coll_gallery,
selected_paths, loaded_original_paths, selection_status, download_btn,
download_file, clear_selection_btn, select_all_btn
) = render_collections()
caps_page, refresh_cap_btn, cap_status, caption_table = render_captions()
about_page = render_about()
# Define page indexes for visibility toggles
pages = [home_page, ingest_page, search_page, colls_page, caps_page, about_page]
page_names = ["home", "ingest", "search", "collections", "captions", "about"]
def switch_page(page_name):
"""Generates dynamic updates to manage current visible pages."""
return [gr.update(visible=(name == page_name)) for name in page_names]
# --- Sidebar Navigation Interactions ---
nav_btns["home"].click(fn=lambda: switch_page("home"), outputs=pages)
nav_btns["ingest"].click(fn=lambda: switch_page("ingest"), outputs=pages)
# Safe helpers are invoked here to avoid component value schema mismatch issues
nav_btns["search"].click(
fn=lambda: switch_page("search") + [update_search_dropdown()],
outputs=pages + [search_col_filter]
)
nav_btns["collections"].click(
fn=lambda: switch_page("collections") + [update_collections_dropdown()],
outputs=pages + [view_coll_selector]
)
nav_btns["captions"].click(fn=lambda: switch_page("captions"), outputs=pages)
nav_btns["about"].click(fn=lambda: switch_page("about"), outputs=pages)
start_btn.click(fn=lambda: switch_page("ingest"), outputs=pages)
# --- Ingestion View Handlers ---
def toggle_collection_inputs(is_new):
"""Toggles rendering states of manual and standard collections selectors."""
return (
gr.update(visible=not is_new), # Hide selector if creating new
gr.update(visible=is_new) # Show text box if creating new
)
use_new_coll.change(
fn=toggle_collection_inputs,
inputs=use_new_coll,
outputs=[coll_dropdown, new_coll_name]
)
ingest_btn.click(
fn=run_ingest,
inputs=[upload, coll_dropdown, use_new_coll, new_coll_name],
outputs=[ingest_status, caption_table, coll_dropdown]
)
# --- Shared Selection Utility Functions ---
def format_selection_status(count, filenames):
"""Generates double-linebreak formatted selection statuses to divide output lines."""
if count == 0:
return "**0** image(s) selected for download packaging.\n\n*No files currently selected.*"
name_string = ", ".join(filenames)
if len(name_string) > 150:
name_string = name_string[:150] + "..."
# Double linebreak forces a paragraph divide onto a separate line
return f"**{count}** image(s) selected for download packaging.\n\n📋 **Selected Files:** `{name_string}`"
def handle_gallery_selection(evt: gr.SelectData, current_selections, raw_paths, gallery_data):
"""Toggles visual labels dynamically and outputs double-linebreak reports."""
clicked_file = raw_paths[evt.index]
if clicked_file in current_selections:
current_selections.remove(clicked_file)
else:
current_selections.append(clicked_file)
updated_gallery = []
for idx, path in enumerate(raw_paths):
thumb_path = gallery_data[idx][0]
basename = os.path.basename(path)
label = f"✅ {basename}" if path in current_selections else basename
updated_gallery.append((thumb_path, label))
filenames = [os.path.basename(f) for f in current_selections]
summary = format_selection_status(len(current_selections), filenames)
return current_selections, summary, updated_gallery
def handle_select_all(raw_paths, gallery_data):
"""Packages all source paths inside current array list and displays checkmarks."""
if not raw_paths:
return [], format_selection_status(0, []), []
current_selections = list(raw_paths)
updated_gallery = []
for idx, path in enumerate(raw_paths):
thumb_path = gallery_data[idx][0]
basename = os.path.basename(path)
updated_gallery.append((thumb_path, f"✅ {basename}"))
filenames = [os.path.basename(f) for f in current_selections]
summary = format_selection_status(len(current_selections), filenames)
return current_selections, summary, updated_gallery
def clear_selection(raw_paths, gallery_data):
"""Flushes storage state lists and clears visual checkmarks from labels."""
updated_gallery = []
for idx, path in enumerate(raw_paths):
thumb_path = gallery_data[idx][0]
basename = os.path.basename(path)
updated_gallery.append((thumb_path, basename))
return [], format_selection_status(0, []), updated_gallery, gr.update(visible=False)
def handle_selected_download(selected_list):
"""Compiles specified absolute paths into a downloadable archive container."""
if not selected_list:
return gr.update(visible=False), "⚠️ Download failure: No selections marked."
file_path, status_msg = zip_selected_files(selected_list)
if file_path:
return gr.update(value=file_path, visible=True), status_msg
return gr.update(visible=False), status_msg
# --- Search View Bindings ---
def trigger_search_load(query, col_filter):
images, original_paths, status = run_search(query, col_filter)
return images, original_paths, [], format_selection_status(0, []), status, gr.update(visible=False)
search_btn.click(
fn=trigger_search_load,
inputs=[search_query, search_col_filter],
outputs=[search_gallery, loaded_search_paths, selected_search_paths, search_selection_status, search_status_msg, search_download_file]
)
search_query.submit(
fn=trigger_search_load,
inputs=[search_query, search_col_filter],
outputs=[search_gallery, loaded_search_paths, selected_search_paths, search_selection_status, search_status_msg, search_download_file]
)
# Search Interactive Select Hooks
search_gallery.select(
fn=handle_gallery_selection,
inputs=[selected_search_paths, loaded_search_paths, search_gallery],
outputs=[selected_search_paths, search_selection_status, search_gallery]
)
search_select_all_btn.click(
fn=handle_select_all,
inputs=[loaded_search_paths, search_gallery],
outputs=[selected_search_paths, search_selection_status, search_gallery]
)
search_clear_selection_btn.click(
fn=clear_selection,
inputs=[loaded_search_paths, search_gallery],
outputs=[selected_search_paths, search_selection_status, search_gallery, search_download_file]
)
search_download_btn.click(
fn=handle_selected_download,
inputs=selected_search_paths,
outputs=[search_download_file, search_selection_status]
)
# --- Collections View Bindings ---
def trigger_collection_load(selected_collection):
images, original_paths, status = load_collections_view(selected_collection)
return images, original_paths, [], format_selection_status(0, []), gr.update(visible=False)
view_coll_selector.change(
fn=trigger_collection_load,
inputs=view_coll_selector,
outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file]
)
refresh_coll_btn.click(
fn=trigger_collection_load,
inputs=view_coll_selector,
outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file]
)
# Collections Interactive Select Hooks
coll_gallery.select(
fn=handle_gallery_selection,
inputs=[selected_paths, loaded_original_paths, coll_gallery],
outputs=[selected_paths, selection_status, coll_gallery]
)
select_all_btn.click(
fn=handle_select_all,
inputs=[loaded_original_paths, coll_gallery],
outputs=[selected_paths, selection_status, coll_gallery]
)
clear_selection_btn.click(
fn=clear_selection,
inputs=[loaded_original_paths, coll_gallery],
outputs=[selected_paths, selection_status, coll_gallery, download_file]
)
download_btn.click(
fn=handle_selected_download,
inputs=selected_paths,
outputs=[download_file, selection_status]
)
# --- Caption Browser Refresh Bindings ---
refresh_cap_btn.click(fn=load_caption_browser, outputs=[caption_table, cap_status])
# Initial startup payload loaders
demo.load(fn=load_caption_browser, outputs=[caption_table, cap_status])
demo.load(
fn=trigger_collection_load,
inputs=view_coll_selector,
outputs=[coll_gallery, loaded_original_paths, selected_paths, selection_status, download_file]
)
if __name__ == "__main__":
demo.launch()