Skip to content

Commit 52b7f6a

Browse files
authored
Merge branch 'staging' into cleanup
2 parents 129a5cd + 7a14d83 commit 52b7f6a

File tree

5 files changed

+496
-406
lines changed

5 files changed

+496
-406
lines changed

backend/api/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .fastapi_router import FastAPIRouter
2+
3+
__all__ = ["FastAPIRouter"]

backend/api/fastapi_router.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
__all__ = ["FastAPIRouter"]
2+
3+
import logging
4+
import time
5+
import uuid
6+
from fastapi import APIRouter, Form, HTTPException, UploadFile
7+
8+
logger = logging.getLogger(__name__)
9+
10+
class FastAPIRouter:
11+
def __init__(self, server_instance, is_internal_env):
12+
"""
13+
Initializes the API routes, giving them access to the server instance
14+
for calling background tasks and accessing shared state.
15+
"""
16+
self.server_instance = server_instance
17+
self.is_internal_env = is_internal_env
18+
self.router = APIRouter()
19+
self._register_routes()
20+
21+
def _register_routes(self):
22+
"""Registers all the FastAPI routes."""
23+
self.router.add_api_route("/health", self.health, methods=["GET"])
24+
self.router.add_api_route("/status", self.status, methods=["GET"])
25+
self.router.add_api_route("/upload", self.upload, methods=["POST"])
26+
self.router.add_api_route("/search", self.search, methods=["GET"])
27+
self.router.add_api_route("/videos", self.list_videos, methods=["GET"])
28+
self.router.add_api_route("/videos/{hashed_identifier}", self.delete_video, methods=["DELETE"])
29+
30+
async def health(self):
31+
"""
32+
Health check endpoint.
33+
Returns a simple status message indicating the service is running.
34+
"""
35+
return {"status": "ok"}
36+
37+
async def status(self, job_id: str):
38+
"""
39+
Check the status of a video processing job.
40+
41+
Args:
42+
job_id (str): The unique identifier for the video processing job.
43+
44+
Returns:
45+
dict: Contains:
46+
- job_id (str): The job identifier
47+
- status (str): 'processing', 'completed', or 'failed'
48+
- message (str, optional): If still processing or not found
49+
- result (dict, optional): Full job result if completed
50+
51+
This endpoint allows clients (e.g., frontend) to poll for job progress and retrieve results when ready.
52+
"""
53+
job_data = self.server_instance.job_store.get_job(job_id)
54+
if job_data is None:
55+
return {
56+
"job_id": job_id,
57+
"status": "processing",
58+
"message": "Job is still processing or not found"
59+
}
60+
return job_data
61+
62+
async def upload(self, file: UploadFile, namespace: str = Form("")):
63+
"""
64+
Handle video file upload and start background processing.
65+
66+
Args:
67+
file (UploadFile): The uploaded video file.
68+
namespace (str, optional): Namespace for Pinecone and R2 storage (default: "")
69+
70+
Returns:
71+
dict: Contains job_id, filename, content_type, size_bytes, status, and message.
72+
"""
73+
contents = await file.read()
74+
file_size = len(contents)
75+
job_id = str(uuid.uuid4())
76+
77+
self.server_instance.job_store.create_job(job_id, {
78+
"job_id": job_id,
79+
"filename": file.filename,
80+
"status": "processing",
81+
"size_bytes": file_size,
82+
"content_type": file.content_type,
83+
"namespace": namespace
84+
})
85+
86+
self.server_instance.process_video_background.spawn(contents, file.filename, job_id, namespace)
87+
88+
return {
89+
"job_id": job_id,
90+
"filename": file.filename,
91+
"content_type": file.content_type,
92+
"size_bytes": file_size,
93+
"status": "processing",
94+
"message": "Video uploaded successfully, processing in background"
95+
}
96+
97+
async def search(self, query: str, namespace: str = "", top_k: int = 10):
98+
"""
99+
Search endpoint - accepts a text query and returns semantic search results.
100+
101+
Args:
102+
query (str): The search query string (required)
103+
namespace (str, optional): Namespace for Pinecone search (default: "")
104+
top_k (int, optional): Number of top results to return (default: 10)
105+
106+
Returns:
107+
json: dict with 'query', 'results', and 'timing'
108+
109+
Raises:
110+
HTTPException: If search fails (500 Internal Server Error)
111+
"""
112+
try:
113+
t_start = time.perf_counter()
114+
logger.info(f"[Search] Query: '{query}' | namespace='{namespace}' | top_k={top_k}")
115+
116+
results = self.server_instance.searcher.search(
117+
query=query,
118+
top_k=top_k,
119+
namespace=namespace
120+
)
121+
122+
t_done = time.perf_counter()
123+
logger.info(f"[Search] Found {len(results)} chunk-level results in {t_done - t_start:.3f}s")
124+
125+
return {
126+
"query": query,
127+
"results": results,
128+
"timing": {
129+
"total_s": round(t_done - t_start, 3)
130+
}
131+
}
132+
except Exception as e:
133+
logger.error(f"[Search] Error: {e}")
134+
raise HTTPException(status_code=500, detail=str(e))
135+
136+
async def list_videos(self, namespace: str = "__default__"):
137+
"""
138+
List all videos stored in R2 for the given namespace.
139+
140+
Args:
141+
namespace (str, optional): Namespace for R2 storage (default: "__default__")
142+
143+
Returns:
144+
json: dict with 'status', 'namespace', and 'videos' list
145+
146+
Raises:
147+
HTTPException: If fetching videos fails (500 Internal Server Error)
148+
"""
149+
logger.info(f"[List Videos] Fetching videos for namespace: {namespace}")
150+
try:
151+
video_data = self.server_instance.r2_connector.fetch_all_video_data(namespace)
152+
return {
153+
"status": "success",
154+
"namespace": namespace,
155+
"videos": video_data
156+
}
157+
except Exception as e:
158+
logger.error(f"[List Videos] Error fetching videos: {e}")
159+
raise HTTPException(status_code=500, detail=str(e))
160+
161+
async def delete_video(self, hashed_identifier: str, filename: str, namespace: str = ""):
162+
"""
163+
Delete a video and its associated chunks from storage and database.
164+
165+
Args:
166+
hashed_identifier (str): The unique identifier of the video in R2 storage.
167+
filename (str): The original filename of the video.
168+
namespace (str, optional): Namespace for Pinecone and R2 storage (default: "")
169+
170+
Returns:
171+
dict: Contains status and message about deletion result.
172+
173+
Raises:
174+
HTTPException: If deletion fails at any step.
175+
- 500 Internal Server Error with details.
176+
- 400 Bad Request if parameters are missing.
177+
- 404 Not Found if video does not exist.
178+
- 403 Forbidden if deletion is not allowed.
179+
"""
180+
logger.info(f"[Delete Video] Request to delete video: {filename} ({hashed_identifier}) | namespace='{namespace}'")
181+
if not self.is_internal_env:
182+
raise HTTPException(status_code=403, detail="Video deletion is not allowed in the current environment.")
183+
184+
job_id = str(uuid.uuid4())
185+
self.server_instance.job_store.create_job(job_id, {
186+
"job_id": job_id,
187+
"hashed_identifier": hashed_identifier,
188+
"namespace": namespace,
189+
"status": "processing",
190+
"operation": "delete"
191+
})
192+
193+
self.server_instance.delete_video_background.spawn(job_id, hashed_identifier, namespace)
194+
195+
return {
196+
"job_id": job_id,
197+
"hashed_identifier": hashed_identifier,
198+
"namespace": namespace,
199+
"status": "processing",
200+
"message": "Video deletion started, processing in background"
201+
}
202+

0 commit comments

Comments
 (0)