1- import io
2- import time
3- import requests
41import streamlit as st
5- from config import Config
62
3+ demo_page = st .Page ("pages/search_demo.py" , title = "Search Demo" , icon = "🔎" )
4+ about_page = st .Page ("pages/about.py" , title = "About ClipABit" , icon = "ℹ️" )
5+ pg = st .navigation ([about_page , demo_page ])
76
8- # Page configuration
97st .set_page_config (
108 page_title = "ClipABit" ,
9+ page_icon = "🎬" ,
1110 layout = "wide" ,
12- initial_sidebar_state = "collapsed"
11+ initial_sidebar_state = "collapsed" ,
1312)
14-
15- # Initialize session state
16- if 'search_results' not in st .session_state :
17- st .session_state .search_results = None
18-
19- # API endpoints from config
20- SEARCH_API_URL = Config .SEARCH_API_URL
21- UPLOAD_API_URL = Config .UPLOAD_API_URL
22- STATUS_API_URL = Config .STATUS_API_URL
23- LIST_VIDEOS_API_URL = Config .LIST_VIDEOS_API_URL
24- NAMESPACE = Config .NAMESPACE
25-
26-
27- def search_videos (query : str ):
28- """Send search query to backend."""
29- try :
30- resp = requests .get (SEARCH_API_URL , params = {"query" : query , "namespace" : NAMESPACE }, timeout = 30 )
31- if resp .status_code == 200 :
32- return resp .json ()
33- else :
34- return {"error" : f"Search failed with status { resp .status_code } " }
35- except requests .RequestException as e :
36- return {"error" : str (e )}
37-
38-
39- @st .cache_data (ttl = 60 , show_spinner = "Fetching all videos in repository..." )
40- def fetch_all_videos ():
41- """Fetch all videos from the backend."""
42- try :
43- resp = requests .get (LIST_VIDEOS_API_URL , params = {"namespace" : NAMESPACE }, timeout = 30 )
44- if resp .status_code == 200 :
45- data = resp .json ()
46- return data .get ("videos" , [])
47- return []
48- except requests .RequestException :
49- return []
50-
51-
52- def upload_file_to_backend (file_bytes : bytes , filename : str , content_type : str | None = None ):
53- """Upload file to backend via multipart form-data."""
54- files = {"file" : (filename , io .BytesIO (file_bytes ), content_type or "application/octet-stream" )}
55- data = {"namespace" : NAMESPACE }
56- resp = requests .post (UPLOAD_API_URL , files = files , data = data , timeout = 300 )
57- return resp
58-
59-
60- # Upload dialog
61- @st .fragment
62- @st .dialog ("Upload Video" )
63- def upload_dialog ():
64- st .write ("Upload a video to add it to the searchable database." )
65-
66- uploaded = st .file_uploader ("Choose a video file" , type = ["mp4" , "mov" , "avi" , "mkv" , "webm" ])
67-
68- if uploaded is not None :
69- uploaded_bytes = uploaded .read ()
70-
71- # Show video preview
72- try :
73- st .video (io .BytesIO (uploaded_bytes ))
74- except Exception :
75- st .info ("Preview not available for this format." )
76-
77- # File metadata
78- st .write (f"**Filename:** { uploaded .name } " )
79- st .write (f"**Size:** { len (uploaded_bytes ):,} bytes ({ len (uploaded_bytes ) / 1024 / 1024 :.2f} MB)" )
80-
81- col1 , col2 = st .columns ([1 , 1 ])
82-
83- with col1 :
84- if st .button ("Upload" , type = "primary" , use_container_width = True ):
85- with st .spinner ("Uploading..." ):
86- try :
87- resp = upload_file_to_backend (uploaded_bytes , uploaded .name , uploaded .type )
88-
89- if resp .status_code == 200 :
90- data = resp .json ()
91- if data .get ("status" ) == "processing" :
92- job_id = data .get ("job_id" )
93- st .toast (f"Video uploaded! Job ID: { job_id } " )
94- time .sleep (1 )
95- st .rerun ()
96- else :
97- st .error ("Upload failed" )
98- else :
99- st .error (f"Upload failed with status { resp .status_code } " )
100- except requests .RequestException as e :
101- st .error (f"Upload failed: { e } " )
102-
103- with col2 :
104- if st .button ("Cancel" , use_container_width = True ):
105- st .rerun ()
106-
107-
108- # Main UI
109- st .title ("ClipABit" )
110- st .subheader ("Semantic Video Search - Demo" )
111-
112- # Upload button row
113- up_col1 , up_col2 = st .columns ([1 , 7 ])
114- with up_col1 :
115- if st .button ("Upload" , use_container_width = True ):
116- upload_dialog ()
117-
118- # insert vertical spaces
119- st .write ("" )
120- st .write ("" )
121-
122- # Header with search and clear button
123- col1 , col2 , col3 = st .columns ([6 , 1 , 1 ])
124-
125- with col1 :
126- search_query = st .text_input (
127- "Search" ,
128- placeholder = "Search for video content..." ,
129- label_visibility = "collapsed"
130- )
131-
132- with col2 :
133- if st .button ("Search" , type = "primary" , use_container_width = True ):
134- if search_query :
135- with st .spinner ("Searching..." ):
136- results = search_videos (search_query )
137- st .session_state .search_results = results
138- else :
139- st .warning ("Please enter a search query" )
140-
141- with col3 :
142- if st .button ("Clear" , use_container_width = True ):
143- st .session_state .search_results = None
144- st .rerun ()
145-
146-
147- st .markdown ("---" )
148-
149- # Custom CSS to force video containers to have a consistent aspect ratio
150- st .markdown ("""
151- <style>
152- .stVideo {
153- aspect-ratio: 16 / 9;
154- background-color: #000;
155- }
156- </style>
157- """ , unsafe_allow_html = True )
158-
159- # Display results or repository
160- if st .session_state .search_results :
161- st .subheader (f"Search Results for: '{ search_query } '" )
162-
163- results_data = st .session_state .search_results
164-
165- if "error" in results_data :
166- st .error (f"Error: { results_data ['error' ]} " )
167- elif "results" in results_data :
168- results = results_data ["results" ]
169- if results :
170- cols = st .columns (3 )
171- for idx , result in enumerate (results ):
172- metadata = result .get ("metadata" , {})
173- presigned_url = metadata .get ("presigned_url" )
174- start_time = metadata .get ("start_time_s" , 0 )
175- filename = metadata .get ("file_filename" , "Unknown Video" )
176- score = result .get ("score" , 0 )
177-
178- if presigned_url :
179- with cols [idx % 3 ]:
180- st .caption (f"**{ filename } ** (Score: { score :.2f} )" )
181- st .video (presigned_url , start_time = int (start_time ))
182- else :
183- st .info ("No matching videos found." )
184-
185- else :
186- st .subheader ("Video Repository" )
187-
188- # Fetch and display videos
189- videos = fetch_all_videos ()
190-
191- if videos :
192- # Create a grid of videos
193- cols = st .columns (3 )
194- for idx , video in enumerate (videos ):
195- with cols [idx % 3 ]:
196- st .caption (f"**{ video ['file_name' ]} **" )
197- st .video (video ['presigned_url' ])
198- else :
199- st .info ("No videos found in the repository." )
200-
201- # Footer
202- st .markdown ("---" )
203- st .caption ("ClipABit - Powered by CLIP embeddings and semantic search" )
13+ pg .run ()
0 commit comments