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 ( +
+
+

Login Required

+
+
+ setUsername(e.target.value)} + required + /> +
+
+ setPassword(e.target.value)} + required + /> +
+ {error &&
{error}
} + +
+
+
+ ); +} \ 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 ? ( +
+
+

Uploading video...

+
+ ) : ( +
+
📹
+

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