Skip to content

Commit d451e80

Browse files
authored
Merge pull request #21 from ClipABit/feature/cli-31-frontend-extract
Feature/cli 31 frontend extract
2 parents 4571873 + 929fa49 commit d451e80

File tree

4 files changed

+439
-197
lines changed

4 files changed

+439
-197
lines changed

frontend/streamlit/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ClipABit Web Client
22

3-
Streamlit interface for ClipABit technical demo. Supports uploading and searching through videos in a demo repository
3+
Streamlit interface for ClipABit technical demo. Supports uploading and searching through videos in a demo repository
44

55
## Quick Start
66

frontend/streamlit/app.py

Lines changed: 6 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -1,203 +1,13 @@
1-
import io
2-
import time
3-
import requests
41
import 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
97
st.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

Comments
 (0)