diff --git a/SearchFrontend/search_ui/package-lock.json b/SearchFrontend/search_ui/package-lock.json
index a4f3059..e967326 100644
--- a/SearchFrontend/search_ui/package-lock.json
+++ b/SearchFrontend/search_ui/package-lock.json
@@ -1335,6 +1335,11 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
+ "cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="
+ },
"cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -2066,6 +2071,11 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
+ "immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
"import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2199,6 +2209,14 @@
"type-check": "~0.4.0"
}
},
+ "lie": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
+ "requires": {
+ "immediate": "~3.0.5"
+ }
+ },
"lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -2232,6 +2250,14 @@
"@types/trusted-types": "^2.0.2"
}
},
+ "localforage": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
+ "requires": {
+ "lie": "3.1.1"
+ }
+ },
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2270,6 +2296,15 @@
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw=="
},
+ "match-sorter": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.1.0.tgz",
+ "integrity": "sha512-0HX3BHPixkbECX+Vt7nS1vJ6P2twPgGTU3PMXjWrl1eyVCL24tFHeyYN1FN5RKLzve0TyzNI9qntqQGbebnfPQ==",
+ "requires": {
+ "@babel/runtime": "^7.23.8",
+ "remove-accents": "0.5.0"
+ }
+ },
"memoize": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz",
@@ -2334,6 +2369,11 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
+ "object-path": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz",
+ "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A=="
+ },
"optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2543,6 +2583,23 @@
"resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz",
"integrity": "sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw=="
},
+ "react-router": {
+ "version": "7.9.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
+ "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
+ "requires": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ }
+ },
+ "react-router-dom": {
+ "version": "7.9.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz",
+ "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==",
+ "requires": {
+ "react-router": "7.9.3"
+ }
+ },
"react-split-pane": {
"version": "0.1.92",
"resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz",
@@ -2591,6 +2648,11 @@
"memoize-one": ">=3.1.1 <6"
}
},
+ "remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
+ },
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -2723,6 +2785,11 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="
},
+ "set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
+ },
"shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -2748,6 +2815,14 @@
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
"integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="
},
+ "sort-by": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz",
+ "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==",
+ "requires": {
+ "object-path": "0.6.0"
+ }
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
diff --git a/SearchFrontend/search_ui/package.json b/SearchFrontend/search_ui/package.json
index f8f70e0..3593b55 100644
--- a/SearchFrontend/search_ui/package.json
+++ b/SearchFrontend/search_ui/package.json
@@ -22,15 +22,19 @@
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.2",
"flex-layout-system": "^2.0.3",
+ "localforage": "^1.10.0",
+ "match-sorter": "^8.1.0",
"react": "^19.1.1",
"react-calendar": "^6.0.0",
"react-date-range": "^2.0.1",
"react-datepicker": "^8.7.0",
"react-dom": "^19.1.1",
"react-flexbox-grid": "^2.1.2",
+ "react-router-dom": "^7.9.3",
"react-split-pane": "^0.1.92",
"react-table": "^7.8.0",
"rsuite": "^5.83.3",
+ "sort-by": "^1.2.0",
"timelines-chart": "^2.14.2",
"uplot": "^1.6.32",
"uplot-react": "^1.2.4"
diff --git a/SearchFrontend/search_ui/src/App.css b/SearchFrontend/search_ui/src/App.css
index dfa904a..8dc17ae 100644
--- a/SearchFrontend/search_ui/src/App.css
+++ b/SearchFrontend/search_ui/src/App.css
@@ -118,4 +118,59 @@
.controls-section {
padding: 8px 0;
}
+}
+
+
+/* Animated drawer for logout button */
+.controls-section.drawer-open {
+ /* position: fixed; */
+ /* top: 0; */
+ /* left: 0; */
+ /* width: 300px; */
+ /* max-width: 80vw; */
+ /* height: 100vh; */
+ background: #23272f;
+ /* box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3); */
+ /* padding: 60px 20px 20px 20px; */
+ transform: translateY(-100%);
+ transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+ z-index: 1500;
+ overflow-y: auto;
+ animation: slideInFromTop 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;
+}
+
+/* Animation keyframes */
+@keyframes slideInFromTop {
+ from {
+ transform: translateY(-100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Style the logout button inside the drawer */
+.controls-section.drawer-open .control-group {
+ display: block;
+ margin: 15px 0;
+ width: 100%;
+}
+
+.controls-section.drawer-open button {
+ width: 100%;
+ padding: 12px;
+ margin: 5px 0;
+ border: none;
+ border-radius: 6px;
+ background: #ff4757;
+ color: white;
+ font-size: 1.1em;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.controls-section.drawer-open button:hover {
+ background: #ff3838;
}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/App.jsx b/SearchFrontend/search_ui/src/App.jsx
index 2f52262..58c9293 100644
--- a/SearchFrontend/search_ui/src/App.jsx
+++ b/SearchFrontend/search_ui/src/App.jsx
@@ -1,9 +1,8 @@
"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
+import Login from "./components/Login";
import EmbedTimeline from "./components/EmbedTimeline";
import VideoPlayer from "./components/VideoPlayer";
-// import ModernDateRangeSelector from './components/ModernDateRangeSelector';
-import CompactDateRangePicker from "./components/CompactDateRangePicker";
import CustomDateRangePicker from "./components/DateRangePicker";
import "./App.css";
import StatusesDisplayHUD from "./components/StatusDisplay";
@@ -17,6 +16,9 @@ function App() {
const playerRef = useRef(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const playerInstanceRef = useRef(null);
+ const [selectedCamera, setSelectedCamera] = useState("Leopards 1");
+ const [selectedHighRes, setSelectedHighRes] = useState(false);
+ const [videoPlaying, setVideoPlaying] = useState(true);
// State for the values
window.chartRef = chartRef;
window.playerRef = playerRef;
@@ -31,11 +33,11 @@ function App() {
//
const [startRange, setStartRange] = useState(
- new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
+ new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000),
);
const [endRange, setEndRange] = useState(new Date());
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
- const [queryText, setQueryText] = useState("A clouded leopard and a human");
+ const [queryText, setQueryText] = useState("Two clouded leopards being aggressive");
const [sliderValue, setSliderValue] = useState(0);
// State to track last submitted values
@@ -43,6 +45,7 @@ function App() {
startRange,
endRange,
queryText,
+ selectedCamera,
});
// // Check if any value has changed
@@ -60,7 +63,10 @@ function App() {
const endChanged =
endRange.getTime() !== new Date(lastSubmitted.endRange).getTime();
- const queryChanged = textChanged || startChanged || endChanged;
+ const cameraChanged = selectedCamera !== lastSubmitted.selectedCamera;
+
+ const queryChanged =
+ textChanged || startChanged || endChanged || cameraChanged;
const streamComputeStatus = () => {
fetch("api/return_status")
@@ -109,14 +115,17 @@ function App() {
};
// Function to resubmit fetch
const handleResubmit = (doTestMode = false) => {
+ setVideoPlaying(false);
let startRangeUse;
let endRangeUse;
+ setDrawerOpen(false);
+
if (doTestMode == true) {
startRangeUse = new Date(
- new Date().getTime() - 2 * 24 * 60 * 60 * 1000
+ new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
);
endRangeUse = new Date(
- new Date().getTime() - 1 * 24 * 60 * 60 * 1000
+ new Date().getTime() - 1 * 24 * 60 * 60 * 1000,
);
} else {
startRangeUse = startRange;
@@ -131,9 +140,10 @@ function App() {
params.append("endRange", endRangeUse.toISOString());
params.append("threshold", 0.0);
params.append("query", queryText);
+ params.append("camera", selectedCamera);
setDataResults({ videos: [], breaks: [] });
- fetch("api/videos.json?" + params.toString())
+ authenticatedFetch("api/videos.json?" + params.toString())
.then((res) => res.json())
.then((data) => {
streamComputeStatus();
@@ -145,18 +155,36 @@ function App() {
startRange: startRangeUse,
endRange: endRangeUse,
queryText,
+ selectedCamera,
});
+
+
+
+
+
};
+
+ function selectHighResFunc(selectedHighRes, setSelectedHighRes, toggleCheckbox = true) {
+ console.log(selectedHighRes);
+ const params = new URLSearchParams();
+ params.append("do_high_res", !selectedHighRes);
+ authenticatedFetch("api/set_parameter?" + params.toString())
+ .then((res) => res.json())
+ .then((data) => {
+ if (toggleCheckbox) {
+ setSelectedHighRes(!selectedHighRes);
+ }
+ });
+ }
function updateDataAndValue(newValue) {
const floatValue = parseFloat(newValue);
setSliderValue(floatValue);
var newData = JSON.parse(JSON.stringify(original_data.current));
newData["videos"] = newData["videos"].filter(
- (vid) => vid["embed_scores"]["score"][1] >= floatValue
+ (vid) => vid["embed_scores"]["score"][1] >= floatValue,
);
- newData['threshold'] = floatValue;
- console.log(newData['threshold'])
+ newData["threshold"] = floatValue;
setDataResults(newData);
}
@@ -199,7 +227,7 @@ function App() {
series: [{}, { markLine: { data: [vv_new] } }],
},
false,
- ["series.markLine"]
+ ["series.markLine"],
);
} else {
chart.setOption(
@@ -207,67 +235,115 @@ function App() {
series: [{}, { markLine: markLine }],
},
false,
- ["series.markLine"]
+ ["series.markLine"],
);
}
}
-const handleTimelineClick = useCallback(
- (path, timeoffset) => {
+ const handleTimelineClick = useCallback((path, timeoffset) => {
if (playerRef.current && playerInstanceRef.current) {
const player = playerInstanceRef.current;
-
- console.log("Setting video source:", "api/" + path);
- console.log("Target time offset:", timeoffset);
-
- // Clear any existing source first
+ const token = localStorage.getItem("access_token");
+
+ const authenticatedUrl = `api/${path}?token=${token}`;
player.reset();
-
+
+ // Configure player for range request support
+ const videoElement = player.el().querySelector("video");
+ if (videoElement) {
+ videoElement.crossOrigin = "anonymous";
+ videoElement.preload = "metadata";
+ }
+
player.src({
- src: "api/" + path,
+ src: authenticatedUrl,
type: "video/mp4",
+ withCredentials: false,
});
-
+ setVideoPlaying(true);
+ // Ensure range headers are sent for seeking
+ player.ready(() => {
+ const tech = player.tech();
+ if (tech && tech.el_) {
+ tech.el_.addEventListener("loadstart", () => {
+ // Force range request capability
+ if (
+ tech.el_.seekable &&
+ tech.el_.seekable.length === 0
+ ) {
+ console.log(
+ "Video doesn't support seeking - range headers may be needed",
+ );
+ }
+ });
+ }
+ });
+
player.load();
-
+
// Add multiple event listeners for debugging
player.one("loadedmetadata", () => {
console.log("Video metadata loaded");
console.log("Video duration:", player.duration());
- player.currentTime(timeoffset);
-
- // Try to play after setting time
- const playPromise = player.play();
- if (playPromise !== undefined) {
- playPromise.then(() => {
- console.log("Video started playing");
- }).catch(error => {
- console.error("Error playing video:", error);
- });
- }
+ console.log("Time offset:", timeoffset);
});
-
+
+ // Wait for the video to be ready for seeking
+ player.one("canplay", () => {
+ console.log("Video can start playing - setting time");
+
+ const duration = player.duration();
+
+ // Ensure timeoffset is valid
+ const seekTime = Math.max(
+ 0,
+ Math.min(timeoffset, duration - 0.1),
+ );
+
+ console.log("Seeking to:", seekTime, "of", duration);
+ player.currentTime(seekTime);
+
+ // Wait a bit before playing to ensure seek completed
+ setTimeout(() => {
+ const playPromise = player.play();
+ if (playPromise !== undefined) {
+ playPromise
+ .then(() => {
+ console.log(
+ "Video started playing at time:",
+ player.currentTime(),
+ );
+ })
+ .catch((error) => {
+ console.error("Error playing video:", error);
+ });
+ }
+ }, 100);
+ });
+
player.one("error", (e) => {
console.error("Video error:", e);
console.error("Player error:", player.error());
});
-
+
player.one("loadstart", () => {
console.log("Load started");
});
-
+
player.one("canplay", () => {
console.log("Video can start playing");
});
} else {
console.error("Player ref not available");
}
- },
- []
-);
+ }, []);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
-
+ selectHighResFunc(
+ selectedHighRes,
+ setSelectedHighRes,
+ false
+ )
handleResubmit(params.get("test_mode") === "true");
}, []);
@@ -279,7 +355,7 @@ const handleTimelineClick = useCallback(
function pollForResult() {
const poll = () => {
- fetch("api/videos_result.json")
+ authenticatedFetch("api/videos_result.json")
.then((res) => res.json())
.then((data) => {
if (data.status === "processing") {
@@ -287,22 +363,254 @@ const handleTimelineClick = useCallback(
} else {
const max_value = Math.max(
...data["videos"].map(
- (vid) => vid["embed_scores"]["score"][1]
- )
+ (vid) => vid["embed_scores"]["score"][1],
+ ),
);
setSliderMax(max_value);
original_data.current = data;
setDataResults(data);
+ updateDataAndValue(sliderValue)
}
});
};
poll();
}
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [checkingAuth, setCheckingAuth] = useState(true);
+
+ // Check authentication on load
+ useEffect(() => {
+ const token = localStorage.getItem("access_token");
+ if (token) {
+ // Verify token is still valid
+ fetch("/api/videos_result.json", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+ .then((response) => {
+ if (response.ok) {
+ setIsAuthenticated(true);
+ } else {
+ localStorage.removeItem("access_token");
+ setIsAuthenticated(false);
+ }
+ })
+ .catch(() => {
+ localStorage.removeItem("access_token");
+ setIsAuthenticated(false);
+ })
+ .finally(() => {
+ setCheckingAuth(false);
+ });
+ } else {
+ setCheckingAuth(false);
+ }
+ }, []);
+
+ // Add authentication header to all API calls
+ const authenticatedFetch = useCallback((url, options = {}) => {
+ const token = localStorage.getItem("access_token");
+ const headers = {
+ ...options.headers,
+ Authorization: `Bearer ${token}`,
+ };
+ return fetch(url, { ...options, headers });
+ }, []);
+
+ const handleLoginSuccess = () => {
+ setIsAuthenticated(true);
+ handleResubmit(params.get("test_mode") === "true");
+ };
+
+ const handleLogout = () => {
+ localStorage.removeItem("access_token");
+ setIsAuthenticated(false);
+ };
+
+ if (checkingAuth) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
return (
+ {/* {drawerOpen && (
+
setDrawerOpen(false)}
+ style={{
+ position: "fixed",
+ top: 0,
+ left: 0,
+ width: "100vw",
+ height: "100vh",
+ background: "rgba(0, 0, 0, 0.5)",
+ zIndex: 1400,
+ opacity: drawerOpen ? 1 : 0,
+ transition: "opacity 0.3s ease",
+ }}
+ />
+ )} */}
+
+ {drawerOpen && (
+
+ )}
+
+
+
+
+
+
+
+
+ selectHighResFunc(
+ selectedHighRes,
+ setSelectedHighRes,
+ )
+ }
+ />
+ HD Video
+
- {sliderValue.toFixed(2)}
+
+ {sliderValue.toFixed(2)}
+
+ {videoPlaying && (
+
+
+
+ )}
+ {videoPlaying && (
+
+
+
+ )}
@@ -400,14 +735,16 @@ const handleTimelineClick = useCallback(
chartRef={chartRef}
data_in={dataResults}
onTimelineClick={handleTimelineClick}
+ authenticatedFetch={authenticatedFetch}
/>
-
+
diff --git a/SearchFrontend/search_ui/src/Explorer.css b/SearchFrontend/search_ui/src/Explorer.css
new file mode 100644
index 0000000..c861dd8
--- /dev/null
+++ b/SearchFrontend/search_ui/src/Explorer.css
@@ -0,0 +1,31 @@
+.multiple-timelines-videos {
+ display: flex;
+ flex-direction: column;
+ gap: 0; /* Remove gap to ensure exact 50% */
+ padding: 0; /* Remove padding for exact sizing */
+ box-sizing: border-box;
+}
+
+/* Make each VideoPlayerTimelineStack take exactly half the available space */
+.multiple-timelines-videos > * {
+ flex: 0 0 50%; /* Exact 50% - no grow, no shrink */
+ min-height: 0vh;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* Force Video.js to respect container dimensions */
+.video-timeline-container .video-js,
+.timelines-and-video-parent .video-js {
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.video-timeline-container .video-js .vjs-tech,
+.timelines-and-video-parent .video-js .vjs-tech {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: contain;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/Explorer.jsx b/SearchFrontend/search_ui/src/Explorer.jsx
new file mode 100644
index 0000000..8f76b8c
--- /dev/null
+++ b/SearchFrontend/search_ui/src/Explorer.jsx
@@ -0,0 +1,80 @@
+import React, { useState } from "react";
+import VideoPlayerMultiple from "./components/VideoPlayerMultiple.jsx";
+import TimelineStacked from "./components/TimelineStacked.jsx";
+import VideoPlayerTimelineStack from "./components/VideoPlayerTimelineStack.jsx";
+import VideoUploadDropzone from "./components/VideoUploadDropzone.jsx";
+import "./Explorer.css";
+
+function Explorer() {
+ const [queries, setQueries] = useState([
+ { id: 1, text: "", submitted: false },
+ ]);
+ const [nextId, setNextId] = useState(2);
+ const [uploadedVideos, setUploadedVideos] = useState([
+ // Default video
+ ]);
+
+ const handleQueryChange = (id, text) => {
+ setQueries((prev) =>
+ prev.map((query) => (query.id === id ? { ...query, text } : query))
+ );
+ };
+
+ const handleSubmitQuery = (id) => {
+ const query = queries.find((q) => q.id === id);
+ if (query && query.text.trim()) {
+ // Mark this query as submitted
+ setQueries((prev) =>
+ prev.map((q) => (q.id === id ? { ...q, submitted: true } : q))
+ );
+
+ // Add a new empty query box
+ setQueries((prev) => [
+ ...prev,
+ { id: nextId, text: "", submitted: false },
+ ]);
+ setNextId((prev) => prev + 1);
+
+ // TODO: Handle the query submission (API call, etc.)
+ console.log(`Submitted query: "${query.text}"`);
+ }
+ };
+
+ const handleRemoveQuery = (id) => {
+ if (queries.length > 1) {
+ setQueries((prev) => prev.filter((query) => query.id !== id));
+ }
+ };
+
+ const handleKeyPress = (e, id) => {
+ if (e.key === "Enter") {
+ handleSubmitQuery(id);
+ }
+ };
+
+ const handleVideoUpload = (videoUrl) => {
+ setUploadedVideos((prev) => [...prev, videoUrl]);
+ };
+
+ const handleVideoDelete = (index) => {
+ setUploadedVideos((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ return (
+
+ {/* Video Players */}
+
+ {uploadedVideos.map((videoSrc, index) => (
+ handleVideoDelete(index)}
+ />
+ ))}
+
+
+
+ );
+}
+
+export default Explorer;
diff --git a/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx b/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx
deleted file mode 100644
index 6c2fc21..0000000
--- a/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { useState } from "react";
-import DatePicker from "react-datepicker";
-import "react-datepicker/dist/react-datepicker.css";
-
-export default function CompactDateRangePicker({ startDate, endDate, setStartDate, setEndDate}) {
- // const [startDate, setStartDate] = useState(null);
- // const [endDate, setEndDate] = useState(null);
- console.log(startDate)
- console.log(endDate)
- console.log(setStartDate)
- console.log(setEndDate)
- return (
-
{
- setStartDate(start);
- setEndDate(end);
- if (end && onChange) onChange({ startDate: start, endDate: end });
- }}
- isClearable
- maxDate={new Date()}
- placeholderText="Select date range"
- withPortal
- />
- );
-}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx b/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx
index c4d5160..c7d6ca8 100644
--- a/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx
+++ b/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx
@@ -5,6 +5,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
chartRef,
data_in,
onTimelineClick,
+ authenticatedFetch,
markerTime,
}) {
// Use useRef instead of props/state to avoid re-renders
@@ -19,13 +20,14 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
// --- Constants ---
const BREAK_GAP = 0;
const ZOOM_THRESHOLD = 4 * 60 * 60 * 1000; // 1 hour in ms
+ const BREAK_THRESHOLD_SECONDS = 60 * 15;
const fetchLineData = async (startTime, endTime) => {
try {
const params = new URLSearchParams();
params.append("start_time", new Date(startTime).toISOString());
params.append("end_time", new Date(endTime).toISOString());
- const response = await fetch(
+ const response = await authenticatedFetch(
"/api/line_data.json?" + params.toString()
);
@@ -65,7 +67,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
const handleDataZoom = async (params, chart) => {
if (!params || !chart) return;
- console.log(zoomed_range.current);
+ // console.log(zoomed_range.current);
let plot_start_time = zoomed_range.current[0];
let plot_end_time = zoomed_range.current[1];
@@ -81,9 +83,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
let offset_start = dataZoom.startValue - plot_start_time;
let offset_end = dataZoom.endValue - plot_end_time;
- console.log(offset_start, offset_end);
+ // console.log(offset_start, offset_end);
if (offset_start < 0 || offset_end > 0) {
- console.log("Do force getting data");
isZoomedInRef.current = false;
}
@@ -109,11 +110,11 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
const fetchedLineData = await fetchLineData(startTime, endTime);
lineDataRef.current = fetchedLineData;
- console.log("Fetched updated data");
+ // console.log("Fetched updated data");
// Add line directly to chart without React re-render
addLineToChart(chart, fetchedLineData);
} else if (!shouldShowLine && isZoomedInRef.current) {
- console.log("zoomed out");
+ // console.log("zoomed out");
isZoomedInRef.current = false;
lineDataRef.current = null;
zoomed_range.current = [null, null];
@@ -192,6 +193,29 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
).sort((a, b) => new Date(a[0]) - new Date(b[0]));
}
+ function calculateDayBreaks(videos) {
+ const breaks = [];
+ if (videos.length < 3) {
+ return breaks;
+ }
+
+ for (let i = 0; i < videos.length - 1; i++) {
+ let end_now = new Date(videos[i]["end_time"]*1000)
+ let start_next = new Date(videos[i + 1]["start_time"]*1000)
+
+
+ Math.abs(start_next.getDay() - end_now.getDay())
+ if (Math.abs(start_next.getDay() - end_now.getDay()) > 0) {
+ // console.log("Day break:", end_now, start_next)
+ let c_st = end_now.getTime();
+ let e_st = start_next.getTime();
+ breaks.push([c_st, e_st, e_st/2 + c_st/2]);
+ }
+ }
+
+ return breaks;
+ }
+
function calculateBreaks(videos) {
const breaks = [];
if (videos.length < 3) {
@@ -202,9 +226,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
for (let i = 0; i < videos.length - 1; i++) {
let end_now = videos[i]["end_time"];
let start_next = videos[i + 1]["start_time"];
- if (start_next - end_now > 60 * 60) {
- // still in unix timestamp. break only if spaces of 60 minutes
- breaks.push([end_now, start_next]);
+ if (start_next - end_now > BREAK_THRESHOLD_SECONDS) {
+ breaks.push([end_now*1000, start_next*1000, (end_now/2 + start_next/2)*1000]);
}
}
@@ -229,8 +252,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
function prepareBreaks(breaksRaw) {
return breaksRaw.map(([start, end]) => ({
- start: new Date(1000 * start),
- end: new Date(1000 * end),
+ start: new Date(start),
+ end: new Date( end),
gap: BREAK_GAP,
isExpanded: false,
}));
@@ -322,8 +345,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
symbol: "none",
lineStyle: { width: 100, opacity: 0 },
data: [],
- smooth: true,
- sampling: 'lttb',
+ smooth: true,
+ sampling: "lttb",
z: 4,
};
}
@@ -332,9 +355,12 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
const videoData = prepareVideoData(data_in["videos"]);
const withNulls = videoData;
data_in["calc_breaks"] = calculateBreaks(data_in["videos"]);
-
+ data_in["day_breaks"] = calculateDayBreaks(data_in["videos"]);
+ window.data_in = data_in
// const withNulls = fillNulls(videoData);
- const breaks = prepareBreaks(data_in["calc_breaks"]);
+ const breaks = prepareBreaks(data_in["day_breaks"]);
+
+ window.breaks = breaks;
const virtualTime = buildVirtualTimeMapper(breaks);
const breaks_split = data_in["calc_breaks"].flat(1).map(function (x) {
@@ -344,14 +370,14 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
// breaks_split.unshift(new Date(videoData[0][0]).getTime())
// breaks_split.push(new Date(videoData.at(-1)[0]).getTime())
// }
- const paired_splits = [];
- for (let i = 0; i < breaks_split.length; i += 2) {
- paired_splits.push([
- breaks_split[i],
- breaks_split[i + 1],
- breaks_split[i] / 2 + breaks_split[i + 1] / 2,
- ]);
- }
+ const paired_splits = data_in['calc_breaks']
+ // for (let i = 0; i < breaks_split.length; i += 2) {
+ // paired_splits.push([
+ // breaks_split[i],
+ // breaks_split[i + 1],
+ // breaks_split[i] / 2 + breaks_split[i + 1] / 2,
+ // ]);
+ // }
const split_centers = paired_splits.map((d) => new Date(d[2]));
const splitCenterVirtualTimes = split_centers.map((d) => virtualTime(d));
const splitCenterLabels = split_centers.map((d) =>
@@ -416,12 +442,12 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
fontSize: 10,
},
}));
-
+ // console.log(breakMarkLines)
// Attach break mark lines to the first series
if (seriesNormal[0]) {
seriesNormal[0].markLine = {
symbol: ["none", "none"],
- data: [...(breakMarkLines || []), ...(splitCenterMarkLines || [])],
+ data: [...(breakMarkLines)],
lineStyle: { type: "dashed", color: "#888", width: 2 },
label: {
show: true,
@@ -437,7 +463,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
(virtualData.length > 0 ? virtualData[0][0] : 0) - 100000;
const virtual_x_max =
(virtualData.length > 0 ? virtualData[virtualData.length - 1][0] : 1) +
- 4100000;
+ 100000;
const option = {
animation: false,
@@ -547,7 +573,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
const dataCoord = echarts.convertFromPixel({ seriesIndex: 0 }, pixel);
- const res = await fetch("/api/events/click", {
+ const res = await authenticatedFetch("/api/events/click", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
diff --git a/SearchFrontend/search_ui/src/components/Login.css b/SearchFrontend/search_ui/src/components/Login.css
new file mode 100644
index 0000000..5057154
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/Login.css
@@ -0,0 +1,76 @@
+.login-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ background: #181a20;
+}
+
+.login-box {
+ background: #23272f;
+ padding: 40px;
+ border-radius: 12px;
+ box-shadow: 0 4px 24px rgba(0,0,0,0.3);
+ width: 100%;
+ max-width: 400px;
+}
+
+.login-box h2 {
+ color: #fff;
+ text-align: center;
+ margin-bottom: 30px;
+ font-weight: 600;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group input {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #343a40;
+ border-radius: 6px;
+ background: #181a20;
+ color: #fff;
+ font-size: 16px;
+ box-sizing: border-box;
+}
+
+.form-group input:focus {
+ outline: none;
+ border-color: #3a7afe;
+}
+
+.form-group input::placeholder {
+ color: #888;
+}
+
+button {
+ width: 100%;
+ padding: 12px;
+ background: #3a7afe;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+button:hover:not(:disabled) {
+ background: #2c5ce6;
+}
+
+button:disabled {
+ background: #555;
+ cursor: not-allowed;
+}
+
+.error {
+ color: #ff6b6b;
+ text-align: center;
+ margin-bottom: 15px;
+ font-size: 14px;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/Login.jsx b/SearchFrontend/search_ui/src/components/Login.jsx
new file mode 100644
index 0000000..964e3a9
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/Login.jsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import './Login.css';
+
+export default function Login({ onLoginSuccess }) {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ try {
+ const formData = new FormData();
+ formData.append('username', username);
+ formData.append('password', password);
+
+ const response = await fetch('api/login', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ localStorage.setItem('access_token', data.access_token);
+ onLoginSuccess();
+ } else {
+ setError('Invalid username or password');
+ }
+ } catch (error) {
+ setError('Login failed. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.css b/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.css
new file mode 100644
index 0000000..0799a8f
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.css
@@ -0,0 +1,103 @@
+.multiple-timelines-interface {
+ padding: 0;
+ margin: 0;
+}
+
+.input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ margin-bottom: 0;
+}
+
+.item-input {
+ flex: 1;
+ padding: 8px 12px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ margin: 0;
+ width: 50%;
+ height: 22px;
+ transition: opacity 0.2s;
+}
+
+.item-input:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.item-input:focus {
+ outline: none;
+ border-color: #007bff;
+}
+
+.submit-button {
+ padding: 8px 16px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ cursor: pointer;
+ margin: 0;
+ min-width: 100px; /* Fixed minimum width to prevent size change */
+ width: 25%;
+ height: 40px; /* Fixed height to prevent vertical size change */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ transition: background 0.2s ease; /* Only transition background, not size */
+ box-sizing: border-box; /* Ensure padding is included in width/height */
+}
+
+.submit-button:hover:not(:disabled) {
+ background: #0056b3;
+}
+
+.submit-button:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.submit-button.loading {
+ background: #ffc107;
+ cursor: not-allowed;
+}
+
+.submit-button.loading:hover {
+ background: #ffc107;
+}
+
+.spinner {
+ width: 14px; /* Fixed width */
+ height: 14px; /* Fixed height - same as width for perfect circle */
+ border: 2px solid transparent;
+ border-top: 2px solid #fff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ display: inline-block;
+ flex-shrink: 0; /* Prevent the spinner from shrinking */
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.timelines-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.timeline-item {
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: none;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.jsx b/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.jsx
new file mode 100644
index 0000000..71837d9
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/MultipleTimelinesInterface.jsx
@@ -0,0 +1,95 @@
+import React, { useState } from "react";
+import TimelineStacked from "./TimelineStacked";
+import "./MultipleTimelinesInterface.css";
+
+function MultipleTimelinesInterface({ video_path }) {
+ const [value, setValue] = useState("");
+ const [timelines, setTimelines] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async () => {
+ if (value.trim() && !isLoading) {
+ setIsLoading(true);
+
+ const prompt = value.trim();
+ const key = prompt;
+ const params = new URLSearchParams();
+ params.append("video_path", video_path);
+ params.append("prompt", prompt);
+
+ try {
+ const response = await fetch("api/match_scores?" + params.toString());
+ const data = await response.json();
+
+ const new_timeline_prefs = {
+ prompt: prompt,
+ key: key,
+ data: data,
+ };
+
+ setTimelines([...timelines, new_timeline_prefs]);
+ setValue(""); // Clear the input after submission
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ const handleKeyPress = (e) => {
+ if (e.key === "Enter") {
+ handleSubmit();
+ }
+ };
+
+ const handleDeleteTimeline = (timelineId) => {
+ setTimelines(prev => prev.filter(timeline => timeline.key !== timelineId));
+ };
+
+ return (
+
+
+ {timelines.map((timeline) => (
+
+ handleDeleteTimeline(timeline.key)}
+ />
+
+ ))}
+
+
+
+ setValue(e.target.value)}
+ onKeyPress={handleKeyPress}
+ className="item-input"
+ disabled={isLoading}
+ />
+
+
+
+
+ );
+}
+
+export default MultipleTimelinesInterface;
diff --git a/SearchFrontend/search_ui/src/components/TimelineStacked.css b/SearchFrontend/search_ui/src/components/TimelineStacked.css
new file mode 100644
index 0000000..7f8f4bb
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/TimelineStacked.css
@@ -0,0 +1,36 @@
+.timeline-stacked-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.delete-timeline-btn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 20px;
+ height: 20px;
+ background: rgba(255, 71, 87, 0.9);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1001;
+ transition: all 0.2s ease;
+ backdrop-filter: blur(4px);
+}
+
+.delete-timeline-btn:hover {
+ background: rgba(255, 71, 87, 1);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(255, 71, 87, 0.4);
+}
+
+.delete-timeline-btn:active {
+ transform: scale(0.95);
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/TimelineStacked.jsx b/SearchFrontend/search_ui/src/components/TimelineStacked.jsx
new file mode 100644
index 0000000..bed6583
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/TimelineStacked.jsx
@@ -0,0 +1,152 @@
+import ReactECharts from "echarts-for-react";
+import { useRef, useState, useEffect } from "react";
+import eventBus from "../util/EventBus.jsx";
+import "./TimelineStacked.css"; // We'll create this CSS file
+
+function TimelineStacked({ prompt, data, msg_bus_topic, onDelete }) {
+ const chartRef = useRef(null);
+ const [markerValue, setMarkerValue] = useState(0.0);
+ const unsubscribeRef = useRef(null);
+
+ // Calculate min and max x values from data
+ // Sort data by x values and calculate min/max
+ const getSortedDataAndLimits = () => {
+ if (!data || !data["data"] || data["data"].length === 0) {
+ return {
+ sortedData: [],
+ min: 0,
+ max: 100,
+ };
+ }
+
+ // Sort data by x values (first element of each point)
+ const sortedData = [...data["data"]].sort((a, b) => a[0] - b[0]);
+
+ const xValues = sortedData.map((point) => point[0]);
+ return {
+ sortedData: sortedData,
+ min: Math.min(...xValues),
+ max: Math.max(...xValues),
+ };
+ };
+
+ const { sortedData, min, max } = getSortedDataAndLimits();
+
+ useEffect(() => {
+ // Subscribe and store the unsubscribe function
+ const { unSubscribe } = eventBus.subscribe(msg_bus_topic, (obj) => {
+ setMarkerValue(obj["time"]);
+ });
+
+ unsubscribeRef.current = unSubscribe;
+
+ return () => {
+ if (unsubscribeRef.current) {
+ unsubscribeRef.current();
+ }
+ };
+ }, [msg_bus_topic]);
+
+ const handleDelete = () => {
+ // Unsubscribe from event bus to prevent memory leaks
+ if (unsubscribeRef.current) {
+ unsubscribeRef.current();
+ }
+
+ // Call parent's delete handler
+ if (onDelete) {
+ onDelete();
+ }
+ };
+
+ const option = {
+ animation: false,
+ grid: {
+ left: 0,
+ right: 0,
+ top: 20, // Add space for the delete button
+ bottom: 0,
+ containLabel: false,
+ },
+ graphic: [
+ {
+ type: "text",
+ left: 0,
+ top: 0,
+ style: {
+ text: prompt || "",
+ fontSize: 12,
+ fontWeight: "bold",
+ fill: "#FFFFFFF0",
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ padding: [4, 8],
+ borderRadius: 4,
+ },
+ z: 100,
+ },
+ ],
+ xAxis: {
+ type: "value",
+ min: min,
+ max: max,
+ show: false,
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { show: false },
+ splitLine: { show: false },
+ },
+ yAxis: {
+ type: "value",
+ show: true,
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { show: true },
+ splitLine: { show: false },
+ },
+ series: [
+ {
+ type: "line",
+ symbol: "none",
+ lineStyle: { width: 1, opacity: 1 },
+ data: sortedData,
+ smooth: false,
+ z: 4,
+ markLine: {
+ symbol: "none",
+ lineStyle: {
+ color: "#ff4757",
+ width: 2,
+ type: "solid",
+ },
+ data: [
+ {
+ xAxis: markerValue,
+ label: {
+ show: false,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+export default TimelineStacked;
diff --git a/SearchFrontend/search_ui/src/components/VidelUploadDropzone.css b/SearchFrontend/search_ui/src/components/VidelUploadDropzone.css
new file mode 100644
index 0000000..d395257
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VidelUploadDropzone.css
@@ -0,0 +1,89 @@
+.video-dropzone {
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ padding: 40px 20px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: #f8f9fa;
+ margin: 20px 0;
+ min-height: 150px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.video-dropzone:hover {
+ border-color: #007bff;
+ background: #f0f7ff;
+}
+
+.video-dropzone.drag-over {
+ border-color: #28a745;
+ background: #f0fff4;
+ transform: scale(1.02);
+}
+
+.video-dropzone.uploading {
+ border-color: #ffc107;
+ background: #fffbf0;
+ cursor: not-allowed;
+}
+
+.upload-prompt {
+ color: #666;
+}
+
+.upload-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+.upload-prompt p {
+ margin: 8px 0;
+ font-size: 16px;
+}
+
+.upload-subtitle {
+ color: #999;
+ font-size: 14px;
+}
+
+.supported-formats {
+ margin-top: 16px;
+ font-size: 12px;
+ color: #999;
+ font-style: italic;
+}
+
+.upload-status {
+ color: #666;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #007bff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.explorer-container {
+ padding: 20px;
+}
+
+.multiple-timelines-videos {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/VideoPlayer.jsx b/SearchFrontend/search_ui/src/components/VideoPlayer.jsx
index 382edae..e116cdc 100644
--- a/SearchFrontend/search_ui/src/components/VideoPlayer.jsx
+++ b/SearchFrontend/search_ui/src/components/VideoPlayer.jsx
@@ -1,72 +1,95 @@
-import React, { useRef, useEffect, forwardRef, useImperativeHandle } from "react";
+import React, {
+ useRef,
+ useEffect,
+ forwardRef,
+ useImperativeHandle,
+} from "react";
import videojs from "video.js";
import "video.js/dist/video-js.css";
-const VideoPlayer = function VideoPlayer({videoRef, playerInstanceRef, setMarkerTimeFunc}) {
-
+const VideoPlayer = function VideoPlayer({
+ videoRef,
+ playerInstanceRef,
+ setMarkerTimeFunc,
+ authenticatedFetch
+}) {
+ useEffect(() => {
+ // Prevent double init in StrictMode
+ if (!playerInstanceRef.current && videoRef.current) {
+ const token = localStorage.getItem("access_token"); // Move token here
-
- useEffect(() => {
- // Prevent double init in StrictMode
- if (!playerInstanceRef.current && videoRef.current) {
- playerInstanceRef.current = videojs(videoRef.current, {
- controls: true,
- preload: "auto",
- autoplay: true,
- });
+ playerInstanceRef.current = videojs(videoRef.current, {
+ controls: true,
+ preload: "auto",
+ autoplay: true,
+ fluid: true,
+ html5: {
+ vhs: {
+ overrideNative: true, // use Video.js native range handling
+ },
+ nativeVideoTracks: false,
+ },
+ responsive: true,
+ techOrder: ['html5'],
+ })
- playerInstanceRef.current.on('timeupdate', async function (event) {
-
- const res = await fetch('api/events/video_step', {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ timestamp: this.currentTime() }),
- });
- if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
- const { path, timeoffset, do_update, absolute_time} = await res.json();
- setMarkerTimeFunc(1000*absolute_time)
- if (do_update) {
- playerInstanceRef.current.src({ src: 'api/' + path, type: "video/mp4" });
+ playerInstanceRef.current.on("timeupdate", async function (event) {
+ const token = localStorage.getItem("access_token");
+ const res = await authenticatedFetch("api/events/video_step", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ timestamp: this.currentTime() }),
+ });
- // Seek after metadata is loaded
- playerInstanceRef.current.on("loadedmetadata", () => {
- playerInstanceRef.current.currentTime(timeoffset);
- });
+ if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
+ let js_ = await res.json()
+
+ const { path, timeoffset, do_update, absolute_time, halt } = js_
+ setMarkerTimeFunc(1000 * absolute_time);
+ if (halt) {
+ this.pause();
+ }
+ if (do_update) {
+ const authenticatedUrl = `api/${path}?token=${token}`;
+ console.log(authenticatedUrl)
+ playerInstanceRef.current.src({
+ src: authenticatedUrl,
+ type: "video/mp4",
+ });
+
+ // Seek after metadata is loaded
+ playerInstanceRef.current.on("loadedmetadata", () => {
+ playerInstanceRef.current.currentTime(timeoffset);
+ });
+ }
+ });
}
+ return () => {
+ if (playerInstanceRef.current) {
+ playerInstanceRef.current.dispose();
+ playerInstanceRef.current = null;
+ }
+ };
+ }, []);
- }
- )
- }
-
- return () => {
- if (playerInstanceRef.current) {
- playerInstanceRef.current.dispose();
- playerInstanceRef.current = null;
- }
- };
- }, []);
-
-
- return (
-
- );
+ return (
+
+ );
};
export default VideoPlayer;
diff --git a/SearchFrontend/search_ui/src/components/VideoPlayerMultiple.jsx b/SearchFrontend/search_ui/src/components/VideoPlayerMultiple.jsx
new file mode 100644
index 0000000..286448b
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VideoPlayerMultiple.jsx
@@ -0,0 +1,11 @@
+
+import videojs from "video.js";
+
+
+
+function VideoPlayerMultiple()
+{
+
+}
+
+export default VideoPlayerMultiple
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.css b/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.css
new file mode 100644
index 0000000..f5585d6
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.css
@@ -0,0 +1,81 @@
+.timelines-and-video-parent {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: row;
+ overflow: hidden;
+ box-sizing: border-box;
+ position: relative; /* Add this for absolute positioning of delete button */
+}
+
+.video-parent {
+ flex: 1;
+ height: 100%;
+ min-width: 0;
+ background: #000;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.timelines-parent {
+ flex: 1;
+ height: 100%;
+ min-width: 0;
+ background: #20232a;
+ color: #fff;
+ padding: 16px;
+ box-sizing: border-box;
+ overflow-y: auto;
+}
+
+/* Delete button styling */
+.delete-video-btn {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 28px;
+ height: 28px;
+ background: rgba(255, 71, 87, 0.9);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ transition: all 0.2s ease;
+ backdrop-filter: blur(4px);
+}
+
+.delete-video-btn:hover {
+ background: rgba(255, 71, 87, 1);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(255, 71, 87, 0.4);
+}
+
+.delete-video-btn:active {
+ transform: scale(0.95);
+}
+
+/* Force Video.js to fit properly within container */
+.video-parent .video-js {
+ max-width: 100%;
+ max-height: 100%;
+ width: auto !important;
+ height: auto !important;
+}
+
+.video-parent .video-js video,
+.video-parent .video-js .vjs-tech {
+ max-width: 100%;
+ max-height: 100%;
+ width: 100%;
+ height: 100%;
+ object-fit: contain !important;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.jsx b/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.jsx
new file mode 100644
index 0000000..e7500fd
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VideoPlayerTimelineStack.jsx
@@ -0,0 +1,103 @@
+import React, {
+ useRef,
+ useEffect,
+ forwardRef,
+ useImperativeHandle,
+} from "react";
+import videojs from "video.js";
+import eventBus from "../util/EventBus.jsx";
+
+import TimelineStacked from "./TimelineStacked.jsx";
+import "video.js/dist/video-js.css";
+import MultipleTimelinesInterface from "./MultipleTimelinesInterface.jsx";
+import "./VideoPlayerTimelineStack.css";
+
+const VideoPlayerTimelineStack = function VideoPlayerTimelineStack({
+ video_src,
+ onDelete
+}) {
+ const playerInstanceRef = useRef(null);
+ const videoRef = useRef(null);
+
+ useEffect(() => {
+ // Prevent double init in StrictMode
+ if (!playerInstanceRef.current && videoRef.current) {
+ playerInstanceRef.current = videojs(videoRef.current, {
+ controls: true,
+ preload: "auto",
+ autoplay: true,
+ fluid: true,
+ responsive: true,
+ fill: false,
+ aspectRatio: "16:9", // Add aspect ratio
+ width: 932,
+ height: 523, // Calculate based on aspect ratio
+ });
+ }
+
+ playerInstanceRef.current.on("timeupdate", async function (event) {
+ eventBus.publish(video_src, { time: this.currentTime() });
+ });
+
+ if (playerInstanceRef.current && videoRef.current) {
+ const player = playerInstanceRef.current;
+ player.src({ src: video_src, type: "video/mp4" });
+ }
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ console.log(entries);
+ console.log(entries);
+ });
+ const videoContainer = videoRef.current.closest(".video-parent");
+ if (videoContainer) {
+ resizeObserver.observe(videoContainer);
+ }
+
+ return () => {
+ if (playerInstanceRef.current) {
+ playerInstanceRef.current.dispose();
+ playerInstanceRef.current = null;
+ }
+ };
+ }, [video_src]); // Add video_src to dependencies
+
+ const handleDelete = () => {
+ // Clean up video player before deletion
+ if (playerInstanceRef.current) {
+ playerInstanceRef.current.dispose();
+ playerInstanceRef.current = null;
+ }
+
+ // Call parent's delete handler
+ if (onDelete) {
+ onDelete();
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default VideoPlayerTimelineStack;
diff --git a/SearchFrontend/search_ui/src/components/VideoUploadDropzone.css b/SearchFrontend/search_ui/src/components/VideoUploadDropzone.css
new file mode 100644
index 0000000..d395257
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VideoUploadDropzone.css
@@ -0,0 +1,89 @@
+.video-dropzone {
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ padding: 40px 20px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: #f8f9fa;
+ margin: 20px 0;
+ min-height: 150px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.video-dropzone:hover {
+ border-color: #007bff;
+ background: #f0f7ff;
+}
+
+.video-dropzone.drag-over {
+ border-color: #28a745;
+ background: #f0fff4;
+ transform: scale(1.02);
+}
+
+.video-dropzone.uploading {
+ border-color: #ffc107;
+ background: #fffbf0;
+ cursor: not-allowed;
+}
+
+.upload-prompt {
+ color: #666;
+}
+
+.upload-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+.upload-prompt p {
+ margin: 8px 0;
+ font-size: 16px;
+}
+
+.upload-subtitle {
+ color: #999;
+ font-size: 14px;
+}
+
+.supported-formats {
+ margin-top: 16px;
+ font-size: 12px;
+ color: #999;
+ font-style: italic;
+}
+
+.upload-status {
+ color: #666;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #007bff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.explorer-container {
+ padding: 20px;
+}
+
+.multiple-timelines-videos {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/components/VideoUploadDropzone.jsx b/SearchFrontend/search_ui/src/components/VideoUploadDropzone.jsx
new file mode 100644
index 0000000..2c9741f
--- /dev/null
+++ b/SearchFrontend/search_ui/src/components/VideoUploadDropzone.jsx
@@ -0,0 +1,129 @@
+import React, { useState, useRef } from 'react';
+import './VideoUploadDropzone.css';
+
+function VideoUploadDropzone({ onVideoUpload }) {
+ const [isDragOver, setIsDragOver] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setIsDragOver(false);
+
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ handleFileUpload(files[0]);
+ }
+ };
+
+ const handleFileSelect = (e) => {
+ const files = e.target.files;
+ if (files.length > 0) {
+ handleFileUpload(files[0]);
+ }
+ };
+
+ const handleFileUpload = async (file) => {
+ // Validate file type
+ if (!file.type.startsWith('video/')) {
+ alert('Please select a valid video file');
+ return;
+ }
+
+ // Validate file size (e.g., max 100MB)
+ const maxSize = 100 * 1024 * 1024; // 100MB
+ if (file.size > maxSize) {
+ alert('File size too large. Please select a file under 100MB');
+ return;
+ }
+
+ setIsUploading(true);
+
+ try {
+ const formData = new FormData();
+ formData.append('video', file);
+
+ // Get auth token
+ const token = localStorage.getItem('access_token');
+
+ const response = await fetch('/api/upload-video', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ },
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`Upload failed: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ // Call the callback with the uploaded video URL
+ if (onVideoUpload && result.video_url) {
+ onVideoUpload(result.video_url);
+ }
+
+ } catch (error) {
+ console.error('Upload error:', error);
+ alert('Upload failed. Please try again.');
+ } finally {
+ setIsUploading(false);
+ // Reset file input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ };
+
+ const handleClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ return (
+
+
+
+ {isUploading ? (
+
+ ) : (
+
+
📹
+
Drag and drop a video file here
+
or click to select
+
+ Supports: MP4, AVI, MOV, WebM
+
+
+ )}
+
+ );
+}
+
+export default VideoUploadDropzone;
\ No newline at end of file
diff --git a/SearchFrontend/search_ui/src/index.css b/SearchFrontend/search_ui/src/index.css
index d44f8ea..ffe89cf 100644
--- a/SearchFrontend/search_ui/src/index.css
+++ b/SearchFrontend/search_ui/src/index.css
@@ -2,12 +2,12 @@
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
-
+ height: 100%;
color-scheme: light dark ;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
-
+ overflow: auto;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
diff --git a/SearchFrontend/search_ui/src/main.jsx b/SearchFrontend/search_ui/src/main.jsx
index af4624b..aad0558 100644
--- a/SearchFrontend/search_ui/src/main.jsx
+++ b/SearchFrontend/search_ui/src/main.jsx
@@ -1,10 +1,23 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.jsx";
+import Explorer from "./Explorer.jsx"
+import {
+ BrowserRouter as Router,
+ Route,
+ Routes,
+ BrowserRouter,
+} from "react-router-dom";
-createRoot(document.getElementById('root')).render(
- //
-
- // ,
-)
+createRoot(document.getElementById("root")).render(
+ //
+
+
+ } />
+ } />
+
+
+
+ // ,
+);
diff --git a/SearchFrontend/search_ui/src/util/EventBus.jsx b/SearchFrontend/search_ui/src/util/EventBus.jsx
new file mode 100644
index 0000000..0c3b47d
--- /dev/null
+++ b/SearchFrontend/search_ui/src/util/EventBus.jsx
@@ -0,0 +1,118 @@
+class EventBus {
+ constructor() {
+ // initialize event list
+ this.eventObject = {};
+ // id of the callback function list
+ this.callbackId = 0;
+ }
+ // publish event
+ publish(eventName, ...args) {
+ // Get all the callback functions of the current event
+ if (!this.eventObject[eventName]) {
+ // Use object storage to improve the efficiency of deletion when logging out the callback function
+ this.eventObject[eventName] = {};
+ }
+ const callbackObject = this.eventObject[eventName];
+
+ if (!callbackObject) return console.warn(eventName + " not found!");
+
+ // execute each callback function
+ for (let id in callbackObject) {
+ // pass parameters when executing
+ callbackObject[id](...args);
+
+ // The callback function that is only subscribed once needs to be deleted
+ if (id[0] === "d") {
+ delete callbackObject[id];
+ }
+ }
+ }
+ // Subscribe to events
+ subscribe(eventName, callback) {
+ // initialize this event
+ if (!this.eventObject[eventName]) {
+ // Use object storage to improve the efficiency of deletion when logging out the callback function
+ this.eventObject[eventName] = {};
+ }
+
+ const id = this.callbackId++;
+
+ // store the callback function of the subscriber
+ // callbackId needs to be incremented after use for the next callback function
+ this.eventObject[eventName][id] = callback;
+
+ // Every time you subscribe to an event, a unique unsubscribe function is generated
+ const unSubscribe = () => {
+ // clear the callback function of this subscriber
+ if (!this.eventObject[eventName]) {
+ console.warn(`Event "${eventName}" no longer exists`);
+ return;
+ }
+ if (!this.eventObject[eventName][id]) {
+ console.warn(`Callback with id "${id}" not found for event "${eventName}"`);
+ return;
+ }
+ delete this.eventObject[eventName][id];
+
+ // If this event has no subscribers, also clear the entire event object
+ if (Object.keys(this.eventObject[eventName]).length === 0) {
+ delete this.eventObject[eventName];
+ }
+ };
+
+ return { unSubscribe };
+ }
+
+ // only subscribe once
+ subscribeOnce(eventName, callback) {
+ // initialize this event
+ if (!this.eventObject[eventName]) {
+ // Use object storage to improve the efficiency of deletion when logging out the callback function
+ this.eventObject[eventName] = {};
+ }
+
+ // Callback function marked as subscribe only once
+ const id = "d" + this.callbackId++;
+
+ // store the callback function of the subscriber
+ // callbackId needs to be incremented after use for the next callback function
+ this.eventObject[eventName][id] = callback;
+
+ // Every time you subscribe to an event, a unique unsubscribe function is generated
+ const unSubscribe = () => {
+ // clear the callback function of this subscriber
+ if (!this.eventObject[eventName]) {
+ console.warn(`Event "${eventName}" no longer exists`);
+ return;
+ }
+ if (!this.eventObject[eventName][id]) {
+ console.warn(`Callback with id "${id}" not found for event "${eventName}"`);
+ return;
+ }
+ delete this.eventObject[eventName][id];
+
+ // If this event has no subscribers, also clear the entire event object
+ // if (Object.keys(this.eventObject[eventName]).length === 0) {
+ // delete this.eventObject[eventName];
+ // }
+ };
+
+ return { unSubscribe };
+ }
+
+ // clear event
+ clear(eventName) {
+ // If no event name is provided, all events are cleared by default
+ if (!eventName) {
+ this.eventObject = {};
+ return;
+ }
+
+ // clear the specified event
+ delete this.eventObject[eventName];
+ }
+}
+
+const eventBus = new EventBus()
+
+export default eventBus
\ No newline at end of file
diff --git a/SearchInterface.code-workspace b/SearchInterface.code-workspace
index 7e17fe3..362d7c2 100644
--- a/SearchInterface.code-workspace
+++ b/SearchInterface.code-workspace
@@ -2,9 +2,6 @@
"folders": [
{
"path": "."
- },
- {
- "path": "../../../Seafile/Designs/Code/Python/CommonCode"
}
]
}
\ No newline at end of file
diff --git a/SearchScratch/out.jpg b/SearchScratch/out.jpg
new file mode 100644
index 0000000..35ff7da
Binary files /dev/null and b/SearchScratch/out.jpg differ
diff --git a/SearchScratch/seek_time_test.py b/SearchScratch/seek_time_test.py
new file mode 100644
index 0000000..5af48b0
--- /dev/null
+++ b/SearchScratch/seek_time_test.py
@@ -0,0 +1,40 @@
+from pprint import pprint
+import pickle
+import os
+
+with open('/home/thebears/Web/Nuggets/SearchInterface/SearchBackend/crap.p','rb') as ff:
+ cc = pickle.load(ff)
+
+
+tstamp, folder_scores = cc[1],cc[0]
+
+
+for i in range(100):
+ target_tstamp = tstamp + i
+
+ matching_file = None
+ next_file = None
+ for video_file in folder_scores['videos']:
+ start_time = video_file['start_time']
+ end_time = video_file['end_time']
+
+ if target_tstamp > start_time and target_tstamp < end_time:
+ matching_file = video_file
+ if start_time > target_tstamp and next_file is None:
+ next_file = video_file
+
+
+ if matching_file is not None:
+ fname = matching_file['file_name']
+ offset = target_tstamp - matching_file['start_time']
+ else:
+ fname = 'None Found'
+ offset = -1
+ if next_file is not None:
+ fname = next_file['file_name']
+ offset = 0
+
+ web_name = 'media/'+os.path.basename(fname)
+ ret_val = dict(full_path = fname, path=web_name, timeoffset = offset)
+ pprint(ret_val)
+ pprint('-----------------------------------')
diff --git a/SearchScratch/test_random_file.py b/SearchScratch/test_random_file.py
new file mode 100644
index 0000000..7818ed4
--- /dev/null
+++ b/SearchScratch/test_random_file.py
@@ -0,0 +1,65 @@
+import sys, os
+sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/SearchUtil")
+sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util")
+import embed_scores as ES
+import numpy as np
+import time
+from CommonCode import kwq
+
+# %%
+from CommonCode.video_meta import FTPVide
+o
+
+
+video_path = '/home/thebears/temp/dog.mp4'
+prompt = 'hello'
+
+video_embeds = ES.get_embeddings_for_a_file(video_path)
+prompt_embeds = ES.get_query_vector(prompt)
+video_norm_embeds = FTPVideo.vec_norm(video_embeds['embeds'])
+prompt_norm_embed = FTPVideo.vec_norm(prompt_embeds)
+scores = np.dot(video_norm_embeds, prompt_norm_embed.T).squeeze().tolist()
+
+
+
+ff = FTPVideo(file_path, ignore_filename = True)
+res = ff.embeddings
+results = np.asarray([res['frame_offsets'], scores])
+results.T.tolist()
+# %%
+def get_embed_cache_file_search_path(file_path):
+ return os.path.splitext(file_path)[0]+'.oclip_embeds.npz'
+
+
+
+
+file_search_path = get_embed_cache_file_search_path(file_path)
+force_score = False
+llvec = None
+if os.path.exists(file_search_path):
+ llvec = np.load(file_search_path)
+ frs = llvec['frame_numbers']
+ if set(np.unique(np.diff(frs))) != {1}:
+ force_score = True
+ llvec = None
+
+
+if not os.path.exists(file_search_path) or force_score:
+ kwq.publish(kwq.TOPICS.enter_60_videos_embed_priority, file_path, {'push_to_db':False, 'frame_interval':1, 'force_score':force_score})
+
+
+
+if llvec is None:
+ for i in range(120):
+ print('waiting')
+ if os.path.exists(file_search_path):
+ print('Found embedding path!')
+ llvec = np.load(file_search_path)
+ break
+ else:
+ time.sleep(1)
+
+
+
+
+
diff --git a/SearchScratch/test_recreate_cache.py b/SearchScratch/test_recreate_cache.py
index 7f8be44..b96d2e6 100644
--- a/SearchScratch/test_recreate_cache.py
+++ b/SearchScratch/test_recreate_cache.py
@@ -2,8 +2,11 @@ import sys, os
sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/SearchUtil")
sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util")
import embed_scores as ES
-
-cd = '/srv/ftp_tcc/leopards1/2025/09/13/'
+# %%
+#cd = '/srv/ftp_tcc/leopards1/2025/09/25/'
+cd = '/mnt/hdd_24tb_1/videos/ftp/leopards1/2025/09/24'
+xx = ES.get_vector_representation(cd)
+g
o = ES.calculate_embedding_score_in_folder(cd, 0.1, query='Two cats');
# %%
@@ -11,3 +14,15 @@ from CommonCode.video_meta import FTPVideo
f='/srv/ftp_tcc/leopards1/2025/09/13/Leopards1_00_20250913135952.mp4'
c = FTPVideo(f)
c.embeddings
+
+# %%
+cd = '/srv/ftp_tcc/leopards1/2025/08/15'
+o = ES.calculate_embedding_score_in_folder(cd, 0.1, query='Two cats');
+# %%
+
+cd = '/srv/ftp_tcc/leopards1/2025/10/01'
+vecreo = ES.get_vector_representation(cd, force_compute = True)
+print(vecreo)
+# %%
+o = ES.calculate_embedding_score_in_folder(cd, 0.1, query='Two cats');
+# %%
diff --git a/VectorService/util/embed_scores.py b/VectorService/util/embed_scores.py
index cc7964c..6aede47 100644
--- a/VectorService/util/embed_scores.py
+++ b/VectorService/util/embed_scores.py
@@ -1,11 +1,14 @@
from CommonCode.video_meta import FTPVideo
from CommonCode.settings import get_logger
+from CommonCode import kwq
import logging
import json
import datetime as dt
import functools
import requests
import numpy as np
+import time
+
from pqdm.processes import pqdm
from multiprocessing import Pool
import os
@@ -15,53 +18,118 @@ import redis
from hashlib import md5
-r = redis.Redis(host='localhost', port=6379, db=15)
-logger = get_logger(__name__,'/var/log/vector_search_logs/util_embed_scores', stdout=True, systemd=False, level = logging.INFO)
+r = redis.Redis(host="localhost", port=6379, db=15)
+
+logger = get_logger(
+ __name__,
+ "/var/log/vector_search_logs/util_embed_scores",
+ stdout=True,
+ systemd=False,
+ level=logging.INFO,
+)
+
+
+
+def get_embed_cache_file_search_path(file_path):
+ return os.path.splitext(file_path)[0] + ".oclip_embeds.npz"
+
+
+def get_embeddings_for_a_file(file_path, frame_interval=1):
+ if not os.path.exists(file_path):
+ return {"Error": f"No file exists: {file_path}"}
+ file_search_path = get_embed_cache_file_search_path(file_path)
+ force_score = False
+ llvec = None
+
+ logger.error(f"GETTING EMBEDDINGS FOR A FILE {file_path}")
+ if os.path.exists(file_search_path):
+ llvec = np.load(file_search_path)
+ frs = llvec["frame_numbers"]
+ if set(np.unique(np.diff(frs))) != {frame_interval}:
+ force_score = True
+ llvec = None
+
+ if not os.path.exists(file_search_path) or force_score:
+ kwq.publish(
+ kwq.TOPICS.enter_60_videos_embed_priority,
+ file_path,
+ {
+ "push_to_db": False,
+ "frame_interval": frame_interval,
+ "force_score": force_score,
+ },
+ )
+
+ if llvec is None:
+ for i in range(120):
+ print("waiting")
+ if os.path.exists(file_search_path):
+ print("Found embedding path!")
+ llvec = np.load(file_search_path)
+ break
+ else:
+ time.sleep(1)
+
+
+ return llvec
def get_matching_file_for_tstamp(target_tstamp, folder_scores):
matching_file = None
- for video_file in folder_scores['videos']:
- start_time = video_file['start_time']
- end_time = video_file['end_time']
+ for video_file in folder_scores["videos"]:
+ start_time = video_file["start_time"]
+ end_time = video_file["end_time"]
if target_tstamp > start_time and target_tstamp < end_time:
matching_file = video_file
if matching_file is not None:
- fname = matching_file['file_name']
- offset = target_tstamp - matching_file['start_time']
+ fname = matching_file["file_name"]
+ offset = target_tstamp - matching_file["start_time"]
else:
- fname = 'None Found'
+ fname = "None Found"
offset = -1
- web_name = 'media/'+os.path.basename(fname)
- return dict(full_path = fname, path=web_name, timeoffset = offset)
+ web_name = "media/" + os.path.basename(fname)
+ return dict(full_path=fname, path=web_name, timeoffset=offset)
+
def get_vec_rep_file_loc(c_dir):
- vec_rep_file = os.path.join(c_dir, 'vec_rep.npz')
+ vec_rep_file = os.path.join(c_dir, "vec_rep.npz")
return vec_rep_file
-def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute_log'):
- message = {'task':'VECTOR_CALC_IN_FOLDER_START', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
+
+def get_vector_representation(c_dir, force_compute=False, redis_key="compute_log"):
+ message = {
+ "task": "VECTOR_CALC_IN_FOLDER_START",
+ "when": str(c_dir),
+ "time": dt.datetime.now().timestamp(),
+ }
r.rpush(redis_key, json.dumps(message))
vec_rep_file = get_vec_rep_file_loc(c_dir)
if os.path.exists(vec_rep_file) and not force_compute:
try:
- result = dict(np.load(vec_rep_file))
- message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp(), 'precomputed':True}
+ result = dict(np.load(vec_rep_file, allow_pickle = True))
+ if result['embeds'].ndim == 0:
+ result['embeds'] = result['embeds'].tolist()
+
+ message = {
+ "task": "VECTOR_CALC_IN_FOLDER_DONE",
+ "when": str(c_dir),
+ "time": dt.datetime.now().timestamp(),
+ "precomputed": True,
+ }
r.rpush(redis_key, json.dumps(message))
return result
except:
os.remove(vec_rep_file)
-
-
+
ff = list()
for root, dirs, files in os.walk(c_dir):
for f in files:
- if f.endswith('.mp4') and '_reduced' not in f:
+ if f.endswith(".mp4") and "_reduced" not in f:
ff.append(os.path.join(root, f))
videos = list()
@@ -76,165 +144,265 @@ def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute
all_source = list()
all_tstamps = list()
enu = 0
+ ts_e = 0
+ id_e = 0
for idx, x in enumerate(sorted_videos):
-
try:
hh = x.embeddings
except Exception as e:
hh = None
if hh is not None:
- n_emb = FTPVideo.vec_norm(hh['embeds'])
- all_cat.append(n_emb)
- all_idx.append( enu * np.ones(n_emb.shape[0], dtype=np.int64) )
all_source.append(x.real_path)
- all_tstamps.append( [x.timestamp() for x in hh['frame_time']])
- enu +=1
+ enu += 1
+
+ if hh.get('embeds',None) is not None:
+ n_emb = FTPVideo.vec_norm(hh["embeds"])
+ all_cat.append(n_emb)
+ ts_e+= n_emb.shape[-2]
+ arr_app = (enu-1) * np.ones(n_emb.shape[-2], dtype=np.int64)
+ all_idx.append(arr_app)
+ id_e+= len(arr_app)
+
+ all_tstamps.append([x.timestamp() for x in hh["frame_time"]])
- message = {'task':'VECTOR_CALC_IN_FOLDER_BUMP', 'when': c_dir, 'progress': idx+1, 'how_many': len(sorted_videos), 'time': dt.datetime.now().timestamp()}
+
+
+ message = {
+ "task": "VECTOR_CALC_IN_FOLDER_BUMP",
+ "when": c_dir,
+ "progress": idx + 1,
+ "how_many": len(sorted_videos),
+ "time": dt.datetime.now().timestamp(),
+ }
r.rpush(redis_key, json.dumps(message))
if len(all_cat) == 0:
- return []
- all_embeds = np.vstack(all_cat)
- all_embeds = FTPVideo.vec_norm(all_embeds)
+ return None
+# all_embeds = np.vstack(all_cat)
+
+ all_embeds = {idx:x for idx,x in enumerate(all_cat)}
+
all_idces = np.hstack(all_idx)
all_times = np.hstack(all_tstamps)
-
- np.savez(vec_rep_file, embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
- message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
+
+ np.savez(
+ vec_rep_file,
+ embeds=all_embeds,
+ idces=all_idces,
+ timestamps=all_times,
+ source_files=all_source,
+ )
+ message = {
+ "task": "VECTOR_CALC_IN_FOLDER_DONE",
+ "when": str(c_dir),
+ "time": dt.datetime.now().timestamp(),
+ }
r.rpush(redis_key, json.dumps(message))
- return dict( embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
-
+ return dict(
+ embeds=all_embeds,
+ idces=all_idces,
+ timestamps=all_times,
+ source_files=all_source,
+ )
-
-
-def get_scores_embedding_c_dir(c_dir, query_vector, redis_key = 'compute_log'):
+def get_scores_embedding_c_dir(c_dir, query_vector, redis_key="compute_log"):
+ query_scores = None
vec_rep = get_vector_representation(c_dir, redis_key=redis_key)
- query_scores = (query_vector @ vec_rep['embeds'].T).squeeze()
+ if isinstance(vec_rep['embeds'], dict):
+ vec_rep['embeds'] = [y for x,y in vec_rep['embeds'].items()]
+
+ if vec_rep is None:
+ return None, None
+
+ if isinstance(vec_rep['embeds'], list):
+ q_scores = list()
+ for emb in vec_rep['embeds']:
+ d = len(query_vector)
+ similarity = (query_vector @ emb.reshape(-1, d).T).reshape(emb.shape[:-1])
+
+ if similarity.ndim == 2:
+ scores = np.max(similarity,axis=0)
+ else:
+ scores = similarity
+ q_scores.append(scores)
+ query_scores = np.hstack(q_scores)
+ else:
+ query_scores = (query_vector @ vec_rep["embeds"].T).squeeze()
return vec_rep, query_scores
+@functools.lru_cache
+def get_clip_scores(video_path: str, prompt: str):
+ if not os.path.exists(get_embed_cache_file_search_path(video_path)):
+ get_embeddings_for_a_file(video_path)
+ ff = FTPVideo(video_path, ignore_filename=True)
+ res = ff.embeddings
+
+ prompt_embeds = get_query_vector(prompt)
+ video_norm_embeds = FTPVideo.vec_norm(res["embeds"])
+ prompt_norm_embed = FTPVideo.vec_norm(prompt_embeds)
+ scores = ( prompt_norm_embed @ video_norm_embeds.T).squeeze().tolist()
+
+ results = np.asarray([res["frame_offsets"], scores])
+ return results
+
+
@functools.lru_cache
def get_query_vector(query):
- vec_form = requests.get('http://192.168.1.242:53004/encode',params={'query':query}).json()['vector'][0]
+ vec_form = requests.get(
+ "http://192.168.1.242:53004/encode", params={"query": query}
+ ).json()["vector"][0]
vec_search = np.asarray(vec_form)
- query_vector = FTPVideo.vec_norm(vec_search[None,:])
+ query_vector = FTPVideo.vec_norm(vec_search[None, :])
return query_vector
-
-def calculate_embedding_score_in_folders(c_dirs, threshold, query = None, query_vector = None, redis_key = 'compute_log'):
+def calculate_embedding_score_in_folders(
+ c_dirs, threshold, query=None, query_vector=None, redis_key="compute_log"
+):
result_list = list()
query_vector = None
if query_vector is None:
query_vector = get_query_vector(query)
-
-# kwargs = [{'c_dir':x, 'threshold':threshold, 'query': query} for x in c_dirs]
+ # kwargs = [{'c_dir':x, 'threshold':threshold, 'query': query} for x in c_dirs]
args = [(x, threshold, query, None, logger, redis_key) for x in c_dirs]
-
+
# logger.info(f"CALCULATING FOR {args}")
with Pool(processes=8) as pool:
out = pool.starmap(calculate_embedding_score_in_folder, args)
# logger.info(f"DONE CALCULATING FOR {args}")
- cache_files = list();
- for x, cache_file_loc in out:
- try:
- result_list.extend(x['videos'])
- cache_files.append(cache_file_loc);
- except Exception as e:
- print(e, x)
-
-
+ cache_files = list()
+ out = [x for x in out if x is not None]
+
- return {'videos':result_list, 'cache_file_locs': cache_files}
+ for result in out:
+ try:
+ x, cache_file_loc = result
+ result_list.extend(x["videos"])
+ cache_files.append(cache_file_loc)
+ except Exception as e:
+ print(e)
+
+ return {"videos": result_list, "cache_file_locs": cache_files}
def collapse_scores_to_maxmin_avg(folder_scores):
-
+
result = list()
- for c_data in folder_scores['videos']:
+ for c_data in folder_scores["videos"]:
new_d = c_data.copy()
- scores = new_d['embed_scores']['score']
+ scores = new_d["embed_scores"]["score"]
max_score = max(scores)
min_score = min(scores)
max_score_idx = scores.index(max_score)
min_score_idx = scores.index(min_score)
- max_score_time = new_d['embed_scores']['time'][max_score_idx]
- min_score_time = new_d['embed_scores']['time'][min_score_idx]
- new_d['embed_scores']['score'] = [min_score, max_score, max_score_time, min_score_time]
- new_d['embed_scores']['time'] = max(new_d['embed_scores']['time'])
+ max_score_time = new_d["embed_scores"]["time"][max_score_idx]
+ min_score_time = new_d["embed_scores"]["time"][min_score_idx]
+ new_d["embed_scores"]["score"] = [
+ min_score,
+ max_score,
+ max_score_time,
+ min_score_time,
+ ]
+ new_d["embed_scores"]["time"] = max(new_d["embed_scores"]["time"])
result.append(new_d)
return result
# c_data = {'file_name': str(s_file), 'start_time':start_time, 'end_time':end_time, 'embed_scores':{'time':frame_time, 'score':embed_scores}}
# video_json_info.append(c_data)
-
+
# to_write = {'source_files': vec_rep['source_files'], 'videos': video_json_info}
# with open(cache_file_loc, 'wb') as f:
# logger.info(f"WRITING EMBEDDING SCORE TO CACHE {cache_file_loc}")
- # pickle.dump(to_write, f)
+ # pickle.dump(to_write, f)
-def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_vector = None, logger = logger, redis_key = 'compute_log'):
- message = {'task':'SCORE_CALC_IN_FOLDER_START', 'when': str(og_dir), 'time': dt.datetime.now().timestamp()}
- r.rpush(redis_key, json.dumps(message))
+def calculate_embedding_score_in_folder(
+ og_dir,
+ threshold,
+ query=None,
+ query_vector=None,
+ logger=logger,
+ redis_key="compute_log",
+):
+ message = {
+ "task": "SCORE_CALC_IN_FOLDER_START",
+ "when": str(og_dir),
+ "time": dt.datetime.now().timestamp(),
+ }
+ r.rpush(redis_key, json.dumps(message))
if query_vector is None:
query_vector = get_query_vector(query)
-
+
candidate_dirs = list()
candidate_dirs.append(og_dir)
- candidate_dirs.append(og_dir.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp'))
- candidate_dirs.append(og_dir.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp'))
+ candidate_dirs.append(og_dir.replace("/srv/ftp_tcc", "/mnt/hdd_24tb_1/videos/ftp"))
+ candidate_dirs.append(og_dir.replace("/srv/ftp", "/mnt/hdd_24tb_1/videos/ftp"))
c_dir = None
for candidate in candidate_dirs:
if os.path.exists(candidate):
- c_dir = candidate
- break
+ if len([x for x in os.listdir(candidate) if x.endswith(".mp4")]) > 5:
+ c_dir = candidate
+ break
if c_dir is None:
return []
vec_cache_str = md5(query_vector).hexdigest()
- cache_file_loc = os.path.join(c_dir, 'embedding_scores@'+str(threshold)+'@'+vec_cache_str+'.pkl')
+ cache_file_loc = os.path.join(
+ c_dir, "embedding_scores@" + str(threshold) + "@" + vec_cache_str + ".pkl"
+ )
if os.path.exists(cache_file_loc):
logger.info(f"TRYING TO LOAD CACHE {cache_file_loc}")
try:
-
- with open(cache_file_loc, 'rb') as f:
+ with open(cache_file_loc, "rb") as f:
video_json_info = pickle.load(f)
- files_in_cache = {os.path.splitext(os.path.basename(x))[0] for x in video_json_info.get('source_files',[])}
+ files_in_cache = {
+ os.path.splitext(os.path.basename(x))[0]
+ for x in video_json_info.get("source_files", [])
+ }
lsd_dir = os.listdir(c_dir)
- files_on_disk = {x.split(".")[0] for x in lsd_dir if x.endswith('oclip_embeds.npz')}
-
-
+ files_on_disk = {
+ x.split(".")[0] for x in lsd_dir if x.endswith("oclip_embeds.npz")
+ }
if files_on_disk == files_in_cache:
logger.info(f"LOADED EMBEDDING SCORE FROM CACHE {cache_file_loc}")
- message = {'task':'SCORE_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp(), 'precomputed': True}
- r.rpush(redis_key, json.dumps(message))
+ message = {
+ "task": "SCORE_CALC_IN_FOLDER_DONE",
+ "when": str(c_dir),
+ "time": dt.datetime.now().timestamp(),
+ "precomputed": True,
+ }
+ r.rpush(redis_key, json.dumps(message))
return (video_json_info, cache_file_loc)
else:
- logger.info(f"CACHE FILE IS OLD, DELETING VEC REP FILE AND RECREATING {cache_file_loc}")
- os.remove( get_vec_rep_file_loc(c_dir))
+ logger.info(
+ f"CACHE FILE IS OLD, DELETING VEC REP FILE AND RECREATING {cache_file_loc}"
+ )
+ os.remove(get_vec_rep_file_loc(c_dir))
except Exception as e:
logger.info(f"CACHE FILE IS CORRUPT, RECREATING {cache_file_loc} {e}")
os.remove(cache_file_loc)
-
+
pass
-
# vec_rep = get_vector_representation(c_dir, redis_key = redis_key)
- vec_rep, query_scores = get_scores_embedding_c_dir(c_dir, tuple(query_vector.tolist()[0]), redis_key = redis_key)
+ vec_rep, query_scores = get_scores_embedding_c_dir(
+ c_dir, tuple(query_vector.tolist()[0]), redis_key=redis_key
+ )
+ if vec_rep is None:
+ return
+
video_json_info = list()
idces_keep = np.where(query_scores > threshold)[0]
-
- video_id = vec_rep['idces'][idces_keep]
+ video_id = vec_rep["idces"][idces_keep]
videos_that_match = np.unique(video_id)
# subset_timestampsF = list()
@@ -244,67 +412,82 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
# max_idces = idces_entry[-1]
# subset_timestampsF.append( [ vec_rep['timestamps'][min_idces], vec_rep['timestamps'][max_idces]])
- id_extract_video_level = np.where(np.isin(vec_rep['idces'], videos_that_match))[0]
- idces_split = np.where(np.diff(vec_rep['idces'][id_extract_video_level]) !=0)[0] + 1
- subset_timestampsF = np.split(vec_rep['timestamps'][id_extract_video_level], idces_split)
+ id_extract_video_level = np.where(np.isin(vec_rep["idces"], videos_that_match))[0]
+ idces_split = (
+ np.where(np.diff(vec_rep["idces"][id_extract_video_level]) != 0)[0] + 1
+ )
+ subset_timestampsF = np.split(
+ vec_rep["timestamps"][id_extract_video_level], idces_split
+ )
-
-
-
for subset_t in subset_timestampsF:
if len(subset_t) == 0:
continue
-
+
min_t = min(subset_t)
max_t = max(subset_t)
- idces_curr = np.where(np.logical_and(vec_rep['timestamps'] > min_t , vec_rep['timestamps'] < max_t))[0]
+ idces_curr = np.where(
+ np.logical_and(vec_rep["timestamps"] > min_t, vec_rep["timestamps"] < max_t)
+ )[0]
if len(idces_curr) == 0:
continue
-
- unq_vids = np.unique(vec_rep['idces'][idces_curr])
- subset_idx = np.where(np.isin(vec_rep['idces'],unq_vids))[0]
- subset_idces = vec_rep['idces'][subset_idx]
- subset_timestamps = vec_rep['timestamps'][subset_idx]
+ unq_vids = np.unique(vec_rep["idces"][idces_curr])
+ subset_idx = np.where(np.isin(vec_rep["idces"], unq_vids))[0]
+
+ subset_idces = vec_rep["idces"][subset_idx]
+ subset_timestamps = vec_rep["timestamps"][subset_idx]
subset_scores = query_scores[subset_idx]
- idx_split = np.where(np.diff(vec_rep['idces'][subset_idx]) !=0)[0]+1
+ idx_split = np.where(np.diff(vec_rep["idces"][subset_idx]) != 0)[0] + 1
split_idces = np.split(subset_idces, idx_split)
split_timestamps = np.split(subset_timestamps, idx_split)
split_scores = np.split(subset_scores, idx_split)
- split_files = [vec_rep['source_files'][x[0]] for x in split_idces]
-
- for s_file, s_scores, s_tstamps, s_idces in zip(split_files, split_scores, split_timestamps, split_idces):
+ split_files = [vec_rep["source_files"][x[0]] for x in split_idces]
+
+ for s_file, s_scores, s_tstamps, s_idces in zip(
+ split_files, split_scores, split_timestamps, split_idces
+ ):
start_time = float(min(s_tstamps))
end_time = float(max(s_tstamps))
-
+
frame_time = (s_tstamps - start_time).tolist()
embed_scores = s_scores.tolist()
-
- c_data = {'file_name': str(s_file), 'start_time':start_time, 'end_time':end_time, 'embed_scores':{'time':frame_time, 'score':embed_scores}}
+
+ c_data = {
+ "file_name": str(s_file),
+ "start_time": start_time,
+ "end_time": end_time,
+ "embed_scores": {"time": frame_time, "score": embed_scores},
+ }
video_json_info.append(c_data)
- message = {'task':'SCORE_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
- r.rpush(redis_key, json.dumps(message))
- to_write = {'source_files': vec_rep['source_files'], 'videos': video_json_info}
- with open(cache_file_loc, 'wb') as f:
+ message = {
+ "task": "SCORE_CALC_IN_FOLDER_DONE",
+ "when": str(c_dir),
+ "time": dt.datetime.now().timestamp(),
+ }
+ r.rpush(redis_key, json.dumps(message))
+ to_write = {"source_files": vec_rep["source_files"], "videos": video_json_info}
+ with open(cache_file_loc, "wb") as f:
logger.info(f"WRITING EMBEDDING SCORE TO CACHE {cache_file_loc}")
- pickle.dump(to_write, f)
+ pickle.dump(to_write, f)
logger.info(f"SAVED EMBEDDING SCORE TO CACHE {cache_file_loc}")
return (to_write, cache_file_loc)
def get_matching_file_given_filename(web_name, folder_scores):
file_name = None
- for x in folder_scores['videos']:
- if x['file_name'].endswith(web_name):
- file_name = x['file_name']
+ for x in folder_scores["videos"]:
+ if x["file_name"].endswith(web_name):
+ file_name = x["file_name"]
-
candidate_files = list()
candidate_files.append(file_name)
- candidate_files.append(file_name.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp'))
- candidate_files.append(file_name.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp'))
+ candidate_files.append(
+ file_name.replace("/srv/ftp_tcc", "/mnt/hdd_24tb_1/videos/ftp")
+ )
+ candidate_files.append(file_name.replace("/srv/ftp", "/mnt/hdd_24tb_1/videos/ftp"))
file_name = None
for candidate in candidate_files:
@@ -312,30 +495,27 @@ def get_matching_file_given_filename(web_name, folder_scores):
file_name = candidate
break
-
-
-
return file_name
+# c_dirs = ['/mnt/hdd_24tb_1/videos/ftp/leopards2/2025/08/26','/srv/ftp_tcc/leopards1/2025/08/27','/srv/ftp_tcc/leopards1/2025/08/28','/srv/ftp_tcc/leopards1/2025/08/29']
+# op = calculate_embedding_score_in_folders( tuple(c_dirs), 0.10, query = 'A cat and human')
-#c_dirs = ['/mnt/hdd_24tb_1/videos/ftp/leopards2/2025/08/26','/srv/ftp_tcc/leopards1/2025/08/27','/srv/ftp_tcc/leopards1/2025/08/28','/srv/ftp_tcc/leopards1/2025/08/29']
-#op = calculate_embedding_score_in_folders( tuple(c_dirs), 0.10, query = 'A cat and human')
-def add_breaks_between_videos(op, threshold_to_split_seconds = 30*60): # 30 minutes):
+def add_breaks_between_videos(op, threshold_to_split_seconds=30 * 60): # 30 minutes):
ranges = list()
- for vids in op['videos']:
- ranges.append( (vids['start_time'], vids['end_time']) )
+ for vids in op["videos"]:
+ ranges.append((vids["start_time"], vids["end_time"]))
breaks = list()
- for idx in range(len(ranges)-1):
+ for idx in range(len(ranges) - 1):
current_range = ranges[idx]
- next_range = ranges[idx+1]
+ next_range = ranges[idx + 1]
end_now = current_range[1]
start_next = next_range[0]
if (start_next - end_now) > threshold_to_split_seconds:
breaks.append((end_now, start_next))
-
+
return breaks
diff --git a/VectorService/vec_cropped.py b/VectorService/vec_cropped.py
new file mode 100644
index 0000000..59e3dde
--- /dev/null
+++ b/VectorService/vec_cropped.py
@@ -0,0 +1,27 @@
+from util import embed_scores as ES
+
+query_vector = ES.get_query_vector('A cat grooming their tail')
+c_dir = '/srv/ftp_tcc/leopards1/2026/02/28'
+
+vpath = '/srv/ftp_tcc/leopards2/2026/02/28/Leopards2_00_20260228210054.mp4'
+from CommonCode.video_meta import FTPVideo
+#vec_rep = ES.get_vector_representation('/srv/ftp_tcc/leopards2/2026/02/28')
+#if isinstance(vec_rep['embeds'], dict):
+# vec_rep['embeds'] = [y for x,y in vec_rep['embeds'].items()]
+# %%
+vec_rep = ES.get_vector_representation(c_dir)
+vec_rep['idces'].shape
+sum([x.shape[-2] for _,x in vec_rep['embeds'].items()])
+# %%
+#ou = ES.calculate_embedding_score_in_folder(
+ou = ES.calculate_embedding_score_in_folder(c_dir, threshold=0.04, query = 'A cat grooming')
+# %%
+vec_rep, query_scores = ES.get_scores_embedding_c_dir(c_dir, query_vector.squeeze())
+
+# results = ES.get_clip_scores(video_path, prompt)
+# %%
+
+
+import pickle
+with open('/home/thebears/crap.p','rb') as ff:
+ ou = pickle.load(ff)
diff --git a/VectorService/vector_service.py b/VectorService/vector_service.py
index d3bbd2e..2941fb4 100644
--- a/VectorService/vector_service.py
+++ b/VectorService/vector_service.py
@@ -1,11 +1,25 @@
+import uvicorn.protocols.http.httptools_impl
+
+# orig_data_received = uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received
+# def debug_data_received(self, data):
+# try:
+# print("RAW INVALID DATA RECEIVED: %r", data)
+# except Exception:
+# pass
+# return orig_data_received(self, data)
+# uvicorn.protocols.http.httptools_impl.HttpToolsProtocol.data_received = debug_data_received
from typing import Union, Optional, List
+from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel
from fastapi import FastAPI, Request, Depends
from CommonCode.settings import get_logger
+from CommonCode.video_meta import FTPVideo
import logging
-from fastapi.responses import StreamingResponse
+from fastapi.responses import JSONResponse, StreamingResponse
import os
+import numpy as np
+from fastapi import FastAPI, Request, status
import sys
import json
import time
@@ -20,15 +34,54 @@ session_manager = SessionManager(
interface=RedisSessionInterface(redis.from_url("redis://localhost"))
)
-logger = get_logger(__name__,'/var/log/vector_search_logs/main_embed_scores', stdout=True, systemd=False, level = logging.INFO)
-r = redis.Redis(host='localhost', port=6379, db=15)
+logger = get_logger(
+ __name__,
+ "/var/log/vector_search_logs/main_embed_scores",
+ stdout=True,
+ systemd=False,
+ level=logging.INFO,
+)
+r = redis.Redis(host="localhost", port=6379, db=15)
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request: Request, exc: RequestValidationError):
+ exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
+ logging.error(f"{request}: {exc_str}")
+ content = {"status_code": 10422, "message": exc_str, "data": None}
+ return JSONResponse(
+ content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
+ )
+
+
+@app.get("/video_embeddings")
+def get_video_embeddings(video_path: str):
+ llvec = ES.get_embeddings_for_a_file(video_path)
+ if llvec is not None:
+ llvec = dict(llvec)
+ llvec["embeds"] = llvec["embeds"].tolist()
+ llvec["frame_numbers"] = llvec["frame_numbers"].tolist()
+
+ return llvec
+
+
+@app.get("/prompt_embedding")
+def get_prompt_embedding(prompt: str):
+ return ES.get_query_vector(prompt)
+
+
+@app.get("/match_scores")
+def get_clip_scores(video_path: str, prompt: str):
+ results = ES.get_clip_scores(video_path, prompt)
+ return results.T.tolist()
class VideosPostRequest(BaseModel):
query: str = "A cat and a human"
threshold: float = 0.10
c_dirs: Optional[List[str]] = None
- task_id: str = 'compute_log'
+ task_id: str = "compute_log"
+
@app.post("/videos.json")
async def videos_json(
@@ -58,30 +111,27 @@ async def videos_json(
"/srv/ftp_tcc/leopards1/2025/09/08",
"/srv/ftp_tcc/leopards1/2025/09/09",
"/srv/ftp_tcc/leopards1/2025/09/10",
- "/srv/ftp_tcc/leopards1/2025/09/11",
+ "/srv/ftp_tcc/leopards1/2025/09/11",
]
-
# print(','.join([str(x) for x in c_dirs]))
# message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()}
# r.rpush(task_id, json.dumps(message))?
-
for x in c_dirs:
- message = {'task':'QUEUEING', 'when': str(x), 'time': time.time()}
+ message = {"task": "QUEUEING", "when": str(x), "time": time.time()}
r.rpush(task_id, json.dumps(message))
folder_scores = ES.calculate_embedding_score_in_folders(
- tuple(c_dirs), threshold=threshold, query=query, redis_key = task_id)
+ tuple(c_dirs), threshold=threshold, query=query, redis_key=task_id
+ )
# if p_hits != ES.calculate_embedding_score_in_folders.cache_info().hits:
# logger.info("FROM CACHE")
# else:pp
# logger.info("COMPUTED FROM SCRATCH")
-
-
folder_scores["breaks"] = ES.add_breaks_between_videos(folder_scores)
- folder_scores['videos'] = ES.collapse_scores_to_maxmin_avg(folder_scores)
+ folder_scores["videos"] = ES.collapse_scores_to_maxmin_avg(folder_scores)
return folder_scores