YACWC
This commit is contained in:
75
SearchFrontend/search_ui/package-lock.json
generated
75
SearchFrontend/search_ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
background: "#181a20",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login onLoginSuccess={handleLoginSuccess} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
{/* {drawerOpen && (
|
||||
<div
|
||||
className="drawer-backdrop"
|
||||
onClick={() => 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 && (
|
||||
<div
|
||||
className={`controls-section ${
|
||||
drawerOpen ? "drawer-open" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="control-group"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: "auto",
|
||||
minWidth: "80px",
|
||||
marginLeft: "20px",
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<div
|
||||
className="radio-group"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
flexShrink: 0,
|
||||
width: "auto",
|
||||
border: "2px solid #4a5568",
|
||||
borderRadius: "8px",
|
||||
padding: "12px",
|
||||
backgroundColor: "#2d3748",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
color: "white",
|
||||
justifyContent: "flex-start",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="camera_choice"
|
||||
value="Leopards 1"
|
||||
checked={selectedCamera === "Leopards 1"}
|
||||
onChange={(e) =>
|
||||
setSelectedCamera(e.target.value)
|
||||
}
|
||||
/>
|
||||
Leopards 1
|
||||
</label>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
color: "white",
|
||||
justifyContent: "flex-start",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="camera_choice"
|
||||
value="Leopards 2"
|
||||
checked={selectedCamera === "Leopards 2"}
|
||||
onChange={(e) =>
|
||||
setSelectedCamera(e.target.value)
|
||||
}
|
||||
/>
|
||||
Leopards 2
|
||||
</label>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
color: "white",
|
||||
justifyContent: "flex-start",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="camera_choice"
|
||||
value="Leopards 3"
|
||||
checked={selectedCamera === "Leopards 3"}
|
||||
onChange={(e) =>
|
||||
setSelectedCamera(e.target.value)
|
||||
}
|
||||
/>
|
||||
Leopards 3
|
||||
</label>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
color: "white",
|
||||
justifyContent: "flex-start",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="camera_choice"
|
||||
value="Leopards 4"
|
||||
checked={selectedCamera === "Leopards 4"}
|
||||
onChange={(e) =>
|
||||
setSelectedCamera(e.target.value)
|
||||
}
|
||||
/>
|
||||
Leopards 4
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="controls-section">
|
||||
<div className="control-group">
|
||||
<button
|
||||
className="drawer-toggle"
|
||||
onClick={() => setDrawerOpen(!drawerOpen)}
|
||||
>
|
||||
{drawerOpen ? "✕" : "Options"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="control-group">
|
||||
<CustomDateRangePicker
|
||||
startDate={startRange}
|
||||
@@ -321,21 +629,36 @@ const handleTimelineClick = useCallback(
|
||||
if (e.key === "Enter") handleResubmit();
|
||||
}}
|
||||
style={{
|
||||
// padding: "8px",
|
||||
// borderRadius: "4px",
|
||||
// border: "1px solid #343a40",
|
||||
// color: "#fff",
|
||||
// backgroundColor: "#23272f",
|
||||
// width: "100%",
|
||||
// minWidth: 0,
|
||||
// boxSizing: "border-box",
|
||||
// padding: "8px",
|
||||
// borderRadius: "4px",
|
||||
// border: "1px solid #343a40",
|
||||
// color: "#fff",
|
||||
// backgroundColor: "#23272f",
|
||||
// width: "100%",
|
||||
// minWidth: 0,
|
||||
// boxSizing: "border-box",
|
||||
fontSize: "1.1em",
|
||||
// transition: "width 0.2s",
|
||||
// transition: "width 0.2s",
|
||||
}}
|
||||
ref={inputRef}
|
||||
size={Math.max(queryText.length, 25)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="control-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="do_high_res"
|
||||
checked={selectedHighRes}
|
||||
onChange={(e) =>
|
||||
selectHighResFunc(
|
||||
selectedHighRes,
|
||||
setSelectedHighRes,
|
||||
)
|
||||
}
|
||||
/>
|
||||
HD Video
|
||||
</div>
|
||||
<div
|
||||
className="control-group"
|
||||
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
||||
@@ -371,7 +694,9 @@ const handleTimelineClick = useCallback(
|
||||
className="control-group"
|
||||
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
||||
>
|
||||
<span style={{ color: "#fff" }}>{sliderValue.toFixed(2)}</span>
|
||||
<span style={{ color: "#fff" }}>
|
||||
{sliderValue.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="control-group"
|
||||
@@ -389,6 +714,16 @@ const handleTimelineClick = useCallback(
|
||||
Resubmit
|
||||
</button>
|
||||
</div>
|
||||
{videoPlaying && (
|
||||
<div className="control-group">
|
||||
<button onClick={() => window.open("api/media_download/low")}>Download Low-Res</button>
|
||||
</div>
|
||||
)}
|
||||
{videoPlaying && (
|
||||
<div className="control-group">
|
||||
<button onClick={() => window.open("api/media_download/high")}>Download High-Res</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="status-section">
|
||||
@@ -400,14 +735,16 @@ const handleTimelineClick = useCallback(
|
||||
chartRef={chartRef}
|
||||
data_in={dataResults}
|
||||
onTimelineClick={handleTimelineClick}
|
||||
authenticatedFetch={authenticatedFetch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="video-section vjs-16-9 vjs-fluid">
|
||||
<VideoPlayer
|
||||
videoRef={playerRef}
|
||||
playerInstanceRef={playerInstanceRef}
|
||||
setMarkerTimeFunc={setMarkerValueNonReactive}
|
||||
authenticatedFetch={authenticatedFetch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
31
SearchFrontend/search_ui/src/Explorer.css
Normal file
31
SearchFrontend/search_ui/src/Explorer.css
Normal file
@@ -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;
|
||||
}
|
||||
80
SearchFrontend/search_ui/src/Explorer.jsx
Normal file
80
SearchFrontend/search_ui/src/Explorer.jsx
Normal file
@@ -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 (
|
||||
<div className="explorer-container">
|
||||
{/* Video Players */}
|
||||
<div className="multiple-timelines-videos">
|
||||
{uploadedVideos.map((videoSrc, index) => (
|
||||
<VideoPlayerTimelineStack
|
||||
key={index}
|
||||
video_src={videoSrc}
|
||||
onDelete={() => handleVideoDelete(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<VideoUploadDropzone onVideoUpload={handleVideoUpload} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Explorer;
|
||||
@@ -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 (
|
||||
<DatePicker
|
||||
selectsRange
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={([start, end]) => {
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
if (end && onChange) onChange({ startDate: start, endDate: end });
|
||||
}}
|
||||
isClearable
|
||||
maxDate={new Date()}
|
||||
placeholderText="Select date range"
|
||||
withPortal
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
76
SearchFrontend/search_ui/src/components/Login.css
Normal file
76
SearchFrontend/search_ui/src/components/Login.css
Normal file
@@ -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;
|
||||
}
|
||||
70
SearchFrontend/search_ui/src/components/Login.jsx
Normal file
70
SearchFrontend/search_ui/src/components/Login.jsx
Normal file
@@ -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 (
|
||||
<div className="login-container">
|
||||
<div className="login-box">
|
||||
<h2>Login Required</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="multiple-timelines-interface">
|
||||
<div className="timelines-container">
|
||||
{timelines.map((timeline) => (
|
||||
<div key={timeline.key} className="timeline-item">
|
||||
<TimelineStacked
|
||||
prompt={timeline.prompt}
|
||||
data={timeline.data}
|
||||
msg_bus_topic={video_path}
|
||||
onDelete={() => handleDeleteTimeline(timeline.key)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<br></br>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="item-input"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={`submit-button ${isLoading ? 'loading' : ''}`}
|
||||
disabled={!value.trim() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="spinner"></span>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
'Submit'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultipleTimelinesInterface;
|
||||
36
SearchFrontend/search_ui/src/components/TimelineStacked.css
Normal file
36
SearchFrontend/search_ui/src/components/TimelineStacked.css
Normal file
@@ -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);
|
||||
}
|
||||
152
SearchFrontend/search_ui/src/components/TimelineStacked.jsx
Normal file
152
SearchFrontend/search_ui/src/components/TimelineStacked.jsx
Normal file
@@ -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 (
|
||||
<div className="timeline-stacked-container">
|
||||
<button
|
||||
className="delete-timeline-btn"
|
||||
onClick={handleDelete}
|
||||
title="Delete this timeline"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<ReactECharts
|
||||
ref={chartRef}
|
||||
option={option}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineStacked;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<div data-vjs-player style={{ width: "100%", height: "100%" }}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="video-js vjs-big-play-centered"
|
||||
playsInline
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
objectFit: "contain"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<div data-vjs-player style={{ width: "100%", height: "100%" }}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="video-js vjs-big-play-centered"
|
||||
playsInline
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayer;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import videojs from "video.js";
|
||||
|
||||
|
||||
|
||||
function VideoPlayerMultiple()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export default VideoPlayerMultiple
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="timelines-and-video-parent">
|
||||
<button
|
||||
className="delete-video-btn"
|
||||
onClick={handleDelete}
|
||||
title="Delete this video player"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<div className="video-parent">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="video-js vjs-default-skin"
|
||||
controls
|
||||
preload="auto"
|
||||
data-setup="{}"
|
||||
/>
|
||||
</div>
|
||||
<div className="timelines-parent">
|
||||
<MultipleTimelinesInterface video_path={video_src} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayerTimelineStack;
|
||||
@@ -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;
|
||||
}
|
||||
129
SearchFrontend/search_ui/src/components/VideoUploadDropzone.jsx
Normal file
129
SearchFrontend/search_ui/src/components/VideoUploadDropzone.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={`video-dropzone ${isDragOver ? 'drag-over' : ''} ${isUploading ? 'uploading' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{isUploading ? (
|
||||
<div className="upload-status">
|
||||
<div className="spinner"></div>
|
||||
<p>Uploading video...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="upload-prompt">
|
||||
<div className="upload-icon">📹</div>
|
||||
<p>Drag and drop a video file here</p>
|
||||
<p className="upload-subtitle">or click to select</p>
|
||||
<div className="supported-formats">
|
||||
Supports: MP4, AVI, MOV, WebM
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoUploadDropzone;
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
// <StrictMode>
|
||||
<App />
|
||||
// </StrictMode>,
|
||||
)
|
||||
createRoot(document.getElementById("root")).render(
|
||||
// <StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/explorer" element={<Explorer />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
// </StrictMode>,
|
||||
);
|
||||
|
||||
118
SearchFrontend/search_ui/src/util/EventBus.jsx
Normal file
118
SearchFrontend/search_ui/src/util/EventBus.jsx
Normal file
@@ -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
|
||||
@@ -2,9 +2,6 @@
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../../../Seafile/Designs/Code/Python/CommonCode"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
SearchScratch/out.jpg
Normal file
BIN
SearchScratch/out.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
40
SearchScratch/seek_time_test.py
Normal file
40
SearchScratch/seek_time_test.py
Normal file
@@ -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('-----------------------------------')
|
||||
65
SearchScratch/test_random_file.py
Normal file
65
SearchScratch/test_random_file.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
# %%
|
||||
|
||||
@@ -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
|
||||
|
||||
27
VectorService/vec_cropped.py
Normal file
27
VectorService/vec_cropped.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user