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",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"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": {
|
"cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||||
@@ -2066,6 +2071,11 @@
|
|||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||||
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -2199,6 +2209,14 @@
|
|||||||
"type-check": "~0.4.0"
|
"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": {
|
"lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"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"
|
"@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": {
|
"locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
|
||||||
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw=="
|
"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": {
|
"memoize": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
|
"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": {
|
"optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz",
|
||||||
"integrity": "sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw=="
|
"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": {
|
"react-split-pane": {
|
||||||
"version": "0.1.92",
|
"version": "0.1.92",
|
||||||
"resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz",
|
"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"
|
"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": {
|
"require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="
|
"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": {
|
"shallow-equal": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz",
|
||||||
"integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="
|
"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": {
|
"source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
|
|||||||
@@ -22,15 +22,19 @@
|
|||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"flex-layout-system": "^2.0.3",
|
"flex-layout-system": "^2.0.3",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
|
"match-sorter": "^8.1.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-calendar": "^6.0.0",
|
"react-calendar": "^6.0.0",
|
||||||
"react-date-range": "^2.0.1",
|
"react-date-range": "^2.0.1",
|
||||||
"react-datepicker": "^8.7.0",
|
"react-datepicker": "^8.7.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-flexbox-grid": "^2.1.2",
|
"react-flexbox-grid": "^2.1.2",
|
||||||
|
"react-router-dom": "^7.9.3",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
"rsuite": "^5.83.3",
|
"rsuite": "^5.83.3",
|
||||||
|
"sort-by": "^1.2.0",
|
||||||
"timelines-chart": "^2.14.2",
|
"timelines-chart": "^2.14.2",
|
||||||
"uplot": "^1.6.32",
|
"uplot": "^1.6.32",
|
||||||
"uplot-react": "^1.2.4"
|
"uplot-react": "^1.2.4"
|
||||||
|
|||||||
@@ -119,3 +119,58 @@
|
|||||||
padding: 8px 0;
|
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";
|
"use client";
|
||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import Login from "./components/Login";
|
||||||
import EmbedTimeline from "./components/EmbedTimeline";
|
import EmbedTimeline from "./components/EmbedTimeline";
|
||||||
import VideoPlayer from "./components/VideoPlayer";
|
import VideoPlayer from "./components/VideoPlayer";
|
||||||
// import ModernDateRangeSelector from './components/ModernDateRangeSelector';
|
|
||||||
import CompactDateRangePicker from "./components/CompactDateRangePicker";
|
|
||||||
import CustomDateRangePicker from "./components/DateRangePicker";
|
import CustomDateRangePicker from "./components/DateRangePicker";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import StatusesDisplayHUD from "./components/StatusDisplay";
|
import StatusesDisplayHUD from "./components/StatusDisplay";
|
||||||
@@ -17,6 +16,9 @@ function App() {
|
|||||||
const playerRef = useRef(null);
|
const playerRef = useRef(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const playerInstanceRef = useRef(null);
|
const playerInstanceRef = useRef(null);
|
||||||
|
const [selectedCamera, setSelectedCamera] = useState("Leopards 1");
|
||||||
|
const [selectedHighRes, setSelectedHighRes] = useState(false);
|
||||||
|
const [videoPlaying, setVideoPlaying] = useState(true);
|
||||||
// State for the values
|
// State for the values
|
||||||
window.chartRef = chartRef;
|
window.chartRef = chartRef;
|
||||||
window.playerRef = playerRef;
|
window.playerRef = playerRef;
|
||||||
@@ -31,11 +33,11 @@ function App() {
|
|||||||
//
|
//
|
||||||
|
|
||||||
const [startRange, setStartRange] = useState(
|
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());
|
||||||
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
|
// 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);
|
const [sliderValue, setSliderValue] = useState(0);
|
||||||
|
|
||||||
// State to track last submitted values
|
// State to track last submitted values
|
||||||
@@ -43,6 +45,7 @@ function App() {
|
|||||||
startRange,
|
startRange,
|
||||||
endRange,
|
endRange,
|
||||||
queryText,
|
queryText,
|
||||||
|
selectedCamera,
|
||||||
});
|
});
|
||||||
|
|
||||||
// // Check if any value has changed
|
// // Check if any value has changed
|
||||||
@@ -60,7 +63,10 @@ function App() {
|
|||||||
const endChanged =
|
const endChanged =
|
||||||
endRange.getTime() !== new Date(lastSubmitted.endRange).getTime();
|
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 = () => {
|
const streamComputeStatus = () => {
|
||||||
fetch("api/return_status")
|
fetch("api/return_status")
|
||||||
@@ -109,14 +115,17 @@ function App() {
|
|||||||
};
|
};
|
||||||
// Function to resubmit fetch
|
// Function to resubmit fetch
|
||||||
const handleResubmit = (doTestMode = false) => {
|
const handleResubmit = (doTestMode = false) => {
|
||||||
|
setVideoPlaying(false);
|
||||||
let startRangeUse;
|
let startRangeUse;
|
||||||
let endRangeUse;
|
let endRangeUse;
|
||||||
|
setDrawerOpen(false);
|
||||||
|
|
||||||
if (doTestMode == true) {
|
if (doTestMode == true) {
|
||||||
startRangeUse = new Date(
|
startRangeUse = new Date(
|
||||||
new Date().getTime() - 2 * 24 * 60 * 60 * 1000
|
new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
endRangeUse = new Date(
|
endRangeUse = new Date(
|
||||||
new Date().getTime() - 1 * 24 * 60 * 60 * 1000
|
new Date().getTime() - 1 * 24 * 60 * 60 * 1000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
startRangeUse = startRange;
|
startRangeUse = startRange;
|
||||||
@@ -131,9 +140,10 @@ function App() {
|
|||||||
params.append("endRange", endRangeUse.toISOString());
|
params.append("endRange", endRangeUse.toISOString());
|
||||||
params.append("threshold", 0.0);
|
params.append("threshold", 0.0);
|
||||||
params.append("query", queryText);
|
params.append("query", queryText);
|
||||||
|
params.append("camera", selectedCamera);
|
||||||
setDataResults({ videos: [], breaks: [] });
|
setDataResults({ videos: [], breaks: [] });
|
||||||
|
|
||||||
fetch("api/videos.json?" + params.toString())
|
authenticatedFetch("api/videos.json?" + params.toString())
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
streamComputeStatus();
|
streamComputeStatus();
|
||||||
@@ -145,18 +155,36 @@ function App() {
|
|||||||
startRange: startRangeUse,
|
startRange: startRangeUse,
|
||||||
endRange: endRangeUse,
|
endRange: endRangeUse,
|
||||||
queryText,
|
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) {
|
function updateDataAndValue(newValue) {
|
||||||
const floatValue = parseFloat(newValue);
|
const floatValue = parseFloat(newValue);
|
||||||
setSliderValue(floatValue);
|
setSliderValue(floatValue);
|
||||||
var newData = JSON.parse(JSON.stringify(original_data.current));
|
var newData = JSON.parse(JSON.stringify(original_data.current));
|
||||||
newData["videos"] = newData["videos"].filter(
|
newData["videos"] = newData["videos"].filter(
|
||||||
(vid) => vid["embed_scores"]["score"][1] >= floatValue
|
(vid) => vid["embed_scores"]["score"][1] >= floatValue,
|
||||||
);
|
);
|
||||||
newData['threshold'] = floatValue;
|
newData["threshold"] = floatValue;
|
||||||
console.log(newData['threshold'])
|
|
||||||
setDataResults(newData);
|
setDataResults(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +227,7 @@ function App() {
|
|||||||
series: [{}, { markLine: { data: [vv_new] } }],
|
series: [{}, { markLine: { data: [vv_new] } }],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
["series.markLine"]
|
["series.markLine"],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
chart.setOption(
|
chart.setOption(
|
||||||
@@ -207,24 +235,47 @@ function App() {
|
|||||||
series: [{}, { markLine: markLine }],
|
series: [{}, { markLine: markLine }],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
["series.markLine"]
|
["series.markLine"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleTimelineClick = useCallback(
|
const handleTimelineClick = useCallback((path, timeoffset) => {
|
||||||
(path, timeoffset) => {
|
|
||||||
if (playerRef.current && playerInstanceRef.current) {
|
if (playerRef.current && playerInstanceRef.current) {
|
||||||
const player = playerInstanceRef.current;
|
const player = playerInstanceRef.current;
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
console.log("Setting video source:", "api/" + path);
|
const authenticatedUrl = `api/${path}?token=${token}`;
|
||||||
console.log("Target time offset:", timeoffset);
|
|
||||||
|
|
||||||
// Clear any existing source first
|
|
||||||
player.reset();
|
player.reset();
|
||||||
|
|
||||||
|
// Configure player for range request support
|
||||||
|
const videoElement = player.el().querySelector("video");
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.crossOrigin = "anonymous";
|
||||||
|
videoElement.preload = "metadata";
|
||||||
|
}
|
||||||
|
|
||||||
player.src({
|
player.src({
|
||||||
src: "api/" + path,
|
src: authenticatedUrl,
|
||||||
type: "video/mp4",
|
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();
|
player.load();
|
||||||
@@ -233,17 +284,40 @@ const handleTimelineClick = useCallback(
|
|||||||
player.one("loadedmetadata", () => {
|
player.one("loadedmetadata", () => {
|
||||||
console.log("Video metadata loaded");
|
console.log("Video metadata loaded");
|
||||||
console.log("Video duration:", player.duration());
|
console.log("Video duration:", player.duration());
|
||||||
player.currentTime(timeoffset);
|
console.log("Time offset:", timeoffset);
|
||||||
|
});
|
||||||
|
|
||||||
// Try to play after setting time
|
// Wait for the video to be ready for seeking
|
||||||
const playPromise = player.play();
|
player.one("canplay", () => {
|
||||||
if (playPromise !== undefined) {
|
console.log("Video can start playing - setting time");
|
||||||
playPromise.then(() => {
|
|
||||||
console.log("Video started playing");
|
const duration = player.duration();
|
||||||
}).catch(error => {
|
|
||||||
console.error("Error playing video:", error);
|
// 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) => {
|
player.one("error", (e) => {
|
||||||
@@ -261,13 +335,15 @@ const handleTimelineClick = useCallback(
|
|||||||
} else {
|
} else {
|
||||||
console.error("Player ref not available");
|
console.error("Player ref not available");
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
selectHighResFunc(
|
||||||
|
selectedHighRes,
|
||||||
|
setSelectedHighRes,
|
||||||
|
false
|
||||||
|
)
|
||||||
handleResubmit(params.get("test_mode") === "true");
|
handleResubmit(params.get("test_mode") === "true");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -279,7 +355,7 @@ const handleTimelineClick = useCallback(
|
|||||||
|
|
||||||
function pollForResult() {
|
function pollForResult() {
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
fetch("api/videos_result.json")
|
authenticatedFetch("api/videos_result.json")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.status === "processing") {
|
if (data.status === "processing") {
|
||||||
@@ -287,22 +363,254 @@ const handleTimelineClick = useCallback(
|
|||||||
} else {
|
} else {
|
||||||
const max_value = Math.max(
|
const max_value = Math.max(
|
||||||
...data["videos"].map(
|
...data["videos"].map(
|
||||||
(vid) => vid["embed_scores"]["score"][1]
|
(vid) => vid["embed_scores"]["score"][1],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
setSliderMax(max_value);
|
setSliderMax(max_value);
|
||||||
|
|
||||||
original_data.current = data;
|
original_data.current = data;
|
||||||
setDataResults(data);
|
setDataResults(data);
|
||||||
|
updateDataAndValue(sliderValue)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
poll();
|
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 (
|
return (
|
||||||
<div className="app-container">
|
<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="controls-section">
|
||||||
|
<div className="control-group">
|
||||||
|
<button
|
||||||
|
className="drawer-toggle"
|
||||||
|
onClick={() => setDrawerOpen(!drawerOpen)}
|
||||||
|
>
|
||||||
|
{drawerOpen ? "✕" : "Options"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
<CustomDateRangePicker
|
<CustomDateRangePicker
|
||||||
startDate={startRange}
|
startDate={startRange}
|
||||||
@@ -321,21 +629,36 @@ const handleTimelineClick = useCallback(
|
|||||||
if (e.key === "Enter") handleResubmit();
|
if (e.key === "Enter") handleResubmit();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
// padding: "8px",
|
// padding: "8px",
|
||||||
// borderRadius: "4px",
|
// borderRadius: "4px",
|
||||||
// border: "1px solid #343a40",
|
// border: "1px solid #343a40",
|
||||||
// color: "#fff",
|
// color: "#fff",
|
||||||
// backgroundColor: "#23272f",
|
// backgroundColor: "#23272f",
|
||||||
// width: "100%",
|
// width: "100%",
|
||||||
// minWidth: 0,
|
// minWidth: 0,
|
||||||
// boxSizing: "border-box",
|
// boxSizing: "border-box",
|
||||||
fontSize: "1.1em",
|
fontSize: "1.1em",
|
||||||
// transition: "width 0.2s",
|
// transition: "width 0.2s",
|
||||||
}}
|
}}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
size={Math.max(queryText.length, 25)}
|
size={Math.max(queryText.length, 25)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="control-group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="do_high_res"
|
||||||
|
checked={selectedHighRes}
|
||||||
|
onChange={(e) =>
|
||||||
|
selectHighResFunc(
|
||||||
|
selectedHighRes,
|
||||||
|
setSelectedHighRes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
HD Video
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="control-group"
|
className="control-group"
|
||||||
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
||||||
@@ -371,7 +694,9 @@ const handleTimelineClick = useCallback(
|
|||||||
className="control-group"
|
className="control-group"
|
||||||
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
style={{ visibility: queryChanged ? "hidden" : "visible" }}
|
||||||
>
|
>
|
||||||
<span style={{ color: "#fff" }}>{sliderValue.toFixed(2)}</span>
|
<span style={{ color: "#fff" }}>
|
||||||
|
{sliderValue.toFixed(2)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="control-group"
|
className="control-group"
|
||||||
@@ -389,6 +714,16 @@ const handleTimelineClick = useCallback(
|
|||||||
Resubmit
|
Resubmit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="status-section">
|
<div className="status-section">
|
||||||
@@ -400,6 +735,7 @@ const handleTimelineClick = useCallback(
|
|||||||
chartRef={chartRef}
|
chartRef={chartRef}
|
||||||
data_in={dataResults}
|
data_in={dataResults}
|
||||||
onTimelineClick={handleTimelineClick}
|
onTimelineClick={handleTimelineClick}
|
||||||
|
authenticatedFetch={authenticatedFetch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -408,6 +744,7 @@ const handleTimelineClick = useCallback(
|
|||||||
videoRef={playerRef}
|
videoRef={playerRef}
|
||||||
playerInstanceRef={playerInstanceRef}
|
playerInstanceRef={playerInstanceRef}
|
||||||
setMarkerTimeFunc={setMarkerValueNonReactive}
|
setMarkerTimeFunc={setMarkerValueNonReactive}
|
||||||
|
authenticatedFetch={authenticatedFetch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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,
|
chartRef,
|
||||||
data_in,
|
data_in,
|
||||||
onTimelineClick,
|
onTimelineClick,
|
||||||
|
authenticatedFetch,
|
||||||
markerTime,
|
markerTime,
|
||||||
}) {
|
}) {
|
||||||
// Use useRef instead of props/state to avoid re-renders
|
// Use useRef instead of props/state to avoid re-renders
|
||||||
@@ -19,13 +20,14 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
const BREAK_GAP = 0;
|
const BREAK_GAP = 0;
|
||||||
const ZOOM_THRESHOLD = 4 * 60 * 60 * 1000; // 1 hour in ms
|
const ZOOM_THRESHOLD = 4 * 60 * 60 * 1000; // 1 hour in ms
|
||||||
|
const BREAK_THRESHOLD_SECONDS = 60 * 15;
|
||||||
|
|
||||||
const fetchLineData = async (startTime, endTime) => {
|
const fetchLineData = async (startTime, endTime) => {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("start_time", new Date(startTime).toISOString());
|
params.append("start_time", new Date(startTime).toISOString());
|
||||||
params.append("end_time", new Date(endTime).toISOString());
|
params.append("end_time", new Date(endTime).toISOString());
|
||||||
const response = await fetch(
|
const response = await authenticatedFetch(
|
||||||
"/api/line_data.json?" + params.toString()
|
"/api/line_data.json?" + params.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
const handleDataZoom = async (params, chart) => {
|
const handleDataZoom = async (params, chart) => {
|
||||||
if (!params || !chart) return;
|
if (!params || !chart) return;
|
||||||
|
|
||||||
console.log(zoomed_range.current);
|
// console.log(zoomed_range.current);
|
||||||
let plot_start_time = zoomed_range.current[0];
|
let plot_start_time = zoomed_range.current[0];
|
||||||
let plot_end_time = zoomed_range.current[1];
|
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_start = dataZoom.startValue - plot_start_time;
|
||||||
let offset_end = dataZoom.endValue - plot_end_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) {
|
if (offset_start < 0 || offset_end > 0) {
|
||||||
console.log("Do force getting data");
|
|
||||||
isZoomedInRef.current = false;
|
isZoomedInRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +110,11 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
const fetchedLineData = await fetchLineData(startTime, endTime);
|
const fetchedLineData = await fetchLineData(startTime, endTime);
|
||||||
lineDataRef.current = fetchedLineData;
|
lineDataRef.current = fetchedLineData;
|
||||||
|
|
||||||
console.log("Fetched updated data");
|
// console.log("Fetched updated data");
|
||||||
// Add line directly to chart without React re-render
|
// Add line directly to chart without React re-render
|
||||||
addLineToChart(chart, fetchedLineData);
|
addLineToChart(chart, fetchedLineData);
|
||||||
} else if (!shouldShowLine && isZoomedInRef.current) {
|
} else if (!shouldShowLine && isZoomedInRef.current) {
|
||||||
console.log("zoomed out");
|
// console.log("zoomed out");
|
||||||
isZoomedInRef.current = false;
|
isZoomedInRef.current = false;
|
||||||
lineDataRef.current = null;
|
lineDataRef.current = null;
|
||||||
zoomed_range.current = [null, 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]));
|
).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) {
|
function calculateBreaks(videos) {
|
||||||
const breaks = [];
|
const breaks = [];
|
||||||
if (videos.length < 3) {
|
if (videos.length < 3) {
|
||||||
@@ -202,9 +226,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
for (let i = 0; i < videos.length - 1; i++) {
|
for (let i = 0; i < videos.length - 1; i++) {
|
||||||
let end_now = videos[i]["end_time"];
|
let end_now = videos[i]["end_time"];
|
||||||
let start_next = videos[i + 1]["start_time"];
|
let start_next = videos[i + 1]["start_time"];
|
||||||
if (start_next - end_now > 60 * 60) {
|
if (start_next - end_now > BREAK_THRESHOLD_SECONDS) {
|
||||||
// still in unix timestamp. break only if spaces of 60 minutes
|
breaks.push([end_now*1000, start_next*1000, (end_now/2 + start_next/2)*1000]);
|
||||||
breaks.push([end_now, start_next]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +252,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
|
|
||||||
function prepareBreaks(breaksRaw) {
|
function prepareBreaks(breaksRaw) {
|
||||||
return breaksRaw.map(([start, end]) => ({
|
return breaksRaw.map(([start, end]) => ({
|
||||||
start: new Date(1000 * start),
|
start: new Date(start),
|
||||||
end: new Date(1000 * end),
|
end: new Date( end),
|
||||||
gap: BREAK_GAP,
|
gap: BREAK_GAP,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
}));
|
}));
|
||||||
@@ -322,8 +345,8 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
symbol: "none",
|
symbol: "none",
|
||||||
lineStyle: { width: 100, opacity: 0 },
|
lineStyle: { width: 100, opacity: 0 },
|
||||||
data: [],
|
data: [],
|
||||||
smooth: true,
|
smooth: true,
|
||||||
sampling: 'lttb',
|
sampling: "lttb",
|
||||||
z: 4,
|
z: 4,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -332,9 +355,12 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
const videoData = prepareVideoData(data_in["videos"]);
|
const videoData = prepareVideoData(data_in["videos"]);
|
||||||
const withNulls = videoData;
|
const withNulls = videoData;
|
||||||
data_in["calc_breaks"] = calculateBreaks(data_in["videos"]);
|
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 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 virtualTime = buildVirtualTimeMapper(breaks);
|
||||||
|
|
||||||
const breaks_split = data_in["calc_breaks"].flat(1).map(function (x) {
|
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.unshift(new Date(videoData[0][0]).getTime())
|
||||||
// breaks_split.push(new Date(videoData.at(-1)[0]).getTime())
|
// breaks_split.push(new Date(videoData.at(-1)[0]).getTime())
|
||||||
// }
|
// }
|
||||||
const paired_splits = [];
|
const paired_splits = data_in['calc_breaks']
|
||||||
for (let i = 0; i < breaks_split.length; i += 2) {
|
// for (let i = 0; i < breaks_split.length; i += 2) {
|
||||||
paired_splits.push([
|
// paired_splits.push([
|
||||||
breaks_split[i],
|
// breaks_split[i],
|
||||||
breaks_split[i + 1],
|
// breaks_split[i + 1],
|
||||||
breaks_split[i] / 2 + breaks_split[i + 1] / 2,
|
// breaks_split[i] / 2 + breaks_split[i + 1] / 2,
|
||||||
]);
|
// ]);
|
||||||
}
|
// }
|
||||||
const split_centers = paired_splits.map((d) => new Date(d[2]));
|
const split_centers = paired_splits.map((d) => new Date(d[2]));
|
||||||
const splitCenterVirtualTimes = split_centers.map((d) => virtualTime(d));
|
const splitCenterVirtualTimes = split_centers.map((d) => virtualTime(d));
|
||||||
const splitCenterLabels = split_centers.map((d) =>
|
const splitCenterLabels = split_centers.map((d) =>
|
||||||
@@ -416,12 +442,12 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
// console.log(breakMarkLines)
|
||||||
// Attach break mark lines to the first series
|
// Attach break mark lines to the first series
|
||||||
if (seriesNormal[0]) {
|
if (seriesNormal[0]) {
|
||||||
seriesNormal[0].markLine = {
|
seriesNormal[0].markLine = {
|
||||||
symbol: ["none", "none"],
|
symbol: ["none", "none"],
|
||||||
data: [...(breakMarkLines || []), ...(splitCenterMarkLines || [])],
|
data: [...(breakMarkLines)],
|
||||||
lineStyle: { type: "dashed", color: "#888", width: 2 },
|
lineStyle: { type: "dashed", color: "#888", width: 2 },
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -437,7 +463,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
(virtualData.length > 0 ? virtualData[0][0] : 0) - 100000;
|
(virtualData.length > 0 ? virtualData[0][0] : 0) - 100000;
|
||||||
const virtual_x_max =
|
const virtual_x_max =
|
||||||
(virtualData.length > 0 ? virtualData[virtualData.length - 1][0] : 1) +
|
(virtualData.length > 0 ? virtualData[virtualData.length - 1][0] : 1) +
|
||||||
4100000;
|
100000;
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -547,7 +573,7 @@ const EmbedTimeline = React.memo(function EmbedTimeline({
|
|||||||
|
|
||||||
const dataCoord = echarts.convertFromPixel({ seriesIndex: 0 }, pixel);
|
const dataCoord = echarts.convertFromPixel({ seriesIndex: 0 }, pixel);
|
||||||
|
|
||||||
const res = await fetch("/api/events/click", {
|
const res = await authenticatedFetch("/api/events/click", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
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 videojs from "video.js";
|
||||||
import "video.js/dist/video-js.css";
|
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
|
||||||
|
|
||||||
|
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 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() }),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
|
||||||
// Prevent double init in StrictMode
|
let js_ = await res.json()
|
||||||
if (!playerInstanceRef.current && videoRef.current) {
|
|
||||||
playerInstanceRef.current = videojs(videoRef.current, {
|
|
||||||
controls: true,
|
|
||||||
preload: "auto",
|
|
||||||
autoplay: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
playerInstanceRef.current.on('timeupdate', async function (event) {
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
const res = await fetch('api/events/video_step', {
|
// Seek after metadata is loaded
|
||||||
method: "POST",
|
playerInstanceRef.current.on("loadedmetadata", () => {
|
||||||
headers: {
|
playerInstanceRef.current.currentTime(timeoffset);
|
||||||
"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" });
|
|
||||||
|
|
||||||
// Seek after metadata is loaded
|
|
||||||
playerInstanceRef.current.on("loadedmetadata", () => {
|
|
||||||
playerInstanceRef.current.currentTime(timeoffset);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
return () => {
|
ref={videoRef}
|
||||||
if (playerInstanceRef.current) {
|
className="video-js vjs-big-play-centered"
|
||||||
playerInstanceRef.current.dispose();
|
playsInline
|
||||||
playerInstanceRef.current = null;
|
style={{
|
||||||
}
|
width: "100%",
|
||||||
};
|
height: "100%",
|
||||||
}, []);
|
backgroundColor: "black",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
return (
|
/>
|
||||||
<div style={{ width: "100%", height: "100%" }}>
|
</div>
|
||||||
<div data-vjs-player style={{ width: "100%", height: "100%" }}>
|
</div>
|
||||||
<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;
|
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;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
height: 100%;
|
||||||
color-scheme: light dark ;
|
color-scheme: light dark ;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
color: rgba(255, 255, 255, 0.87);
|
||||||
background-color: #242424;
|
background-color: #242424;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import App from './App.jsx'
|
import App from "./App.jsx";
|
||||||
|
import Explorer from "./Explorer.jsx"
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
BrowserRouter,
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
// <StrictMode>
|
// <StrictMode>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
// </StrictMode>,
|
<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": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"path": "."
|
"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/SearchUtil")
|
||||||
sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util")
|
sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util")
|
||||||
import embed_scores as ES
|
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');
|
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'
|
f='/srv/ftp_tcc/leopards1/2025/09/13/Leopards1_00_20250913135952.mp4'
|
||||||
c = FTPVideo(f)
|
c = FTPVideo(f)
|
||||||
c.embeddings
|
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.video_meta import FTPVideo
|
||||||
from CommonCode.settings import get_logger
|
from CommonCode.settings import get_logger
|
||||||
|
from CommonCode import kwq
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import functools
|
import functools
|
||||||
import requests
|
import requests
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import time
|
||||||
|
|
||||||
from pqdm.processes import pqdm
|
from pqdm.processes import pqdm
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
import os
|
import os
|
||||||
@@ -15,53 +18,118 @@ import redis
|
|||||||
from hashlib import md5
|
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):
|
def get_matching_file_for_tstamp(target_tstamp, folder_scores):
|
||||||
matching_file = None
|
matching_file = None
|
||||||
for video_file in folder_scores['videos']:
|
for video_file in folder_scores["videos"]:
|
||||||
start_time = video_file['start_time']
|
start_time = video_file["start_time"]
|
||||||
end_time = video_file['end_time']
|
end_time = video_file["end_time"]
|
||||||
|
|
||||||
if target_tstamp > start_time and target_tstamp < end_time:
|
if target_tstamp > start_time and target_tstamp < end_time:
|
||||||
matching_file = video_file
|
matching_file = video_file
|
||||||
|
|
||||||
if matching_file is not None:
|
if matching_file is not None:
|
||||||
fname = matching_file['file_name']
|
fname = matching_file["file_name"]
|
||||||
offset = target_tstamp - matching_file['start_time']
|
offset = target_tstamp - matching_file["start_time"]
|
||||||
else:
|
else:
|
||||||
fname = 'None Found'
|
fname = "None Found"
|
||||||
offset = -1
|
offset = -1
|
||||||
|
|
||||||
web_name = 'media/'+os.path.basename(fname)
|
web_name = "media/" + os.path.basename(fname)
|
||||||
return dict(full_path = fname, path=web_name, timeoffset = offset)
|
return dict(full_path=fname, path=web_name, timeoffset=offset)
|
||||||
|
|
||||||
|
|
||||||
def get_vec_rep_file_loc(c_dir):
|
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
|
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))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
|
|
||||||
vec_rep_file = get_vec_rep_file_loc(c_dir)
|
vec_rep_file = get_vec_rep_file_loc(c_dir)
|
||||||
if os.path.exists(vec_rep_file) and not force_compute:
|
if os.path.exists(vec_rep_file) and not force_compute:
|
||||||
try:
|
try:
|
||||||
result = dict(np.load(vec_rep_file))
|
result = dict(np.load(vec_rep_file, allow_pickle = True))
|
||||||
message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp(), 'precomputed':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))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
return result
|
return result
|
||||||
except:
|
except:
|
||||||
os.remove(vec_rep_file)
|
os.remove(vec_rep_file)
|
||||||
|
|
||||||
|
|
||||||
ff = list()
|
ff = list()
|
||||||
for root, dirs, files in os.walk(c_dir):
|
for root, dirs, files in os.walk(c_dir):
|
||||||
for f in files:
|
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))
|
ff.append(os.path.join(root, f))
|
||||||
|
|
||||||
videos = list()
|
videos = list()
|
||||||
@@ -76,62 +144,129 @@ def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute
|
|||||||
all_source = list()
|
all_source = list()
|
||||||
all_tstamps = list()
|
all_tstamps = list()
|
||||||
enu = 0
|
enu = 0
|
||||||
|
ts_e = 0
|
||||||
|
id_e = 0
|
||||||
for idx, x in enumerate(sorted_videos):
|
for idx, x in enumerate(sorted_videos):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hh = x.embeddings
|
hh = x.embeddings
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
hh = None
|
hh = None
|
||||||
|
|
||||||
if hh is not 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_source.append(x.real_path)
|
||||||
all_tstamps.append( [x.timestamp() for x in hh['frame_time']])
|
enu += 1
|
||||||
enu +=1
|
|
||||||
|
|
||||||
message = {'task':'VECTOR_CALC_IN_FOLDER_BUMP', 'when': c_dir, 'progress': idx+1, 'how_many': len(sorted_videos), 'time': dt.datetime.now().timestamp()}
|
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(),
|
||||||
|
}
|
||||||
r.rpush(redis_key, json.dumps(message))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
|
|
||||||
if len(all_cat) == 0:
|
if len(all_cat) == 0:
|
||||||
return []
|
return None
|
||||||
all_embeds = np.vstack(all_cat)
|
# all_embeds = np.vstack(all_cat)
|
||||||
all_embeds = FTPVideo.vec_norm(all_embeds)
|
|
||||||
|
all_embeds = {idx:x for idx,x in enumerate(all_cat)}
|
||||||
|
|
||||||
all_idces = np.hstack(all_idx)
|
all_idces = np.hstack(all_idx)
|
||||||
all_times = np.hstack(all_tstamps)
|
all_times = np.hstack(all_tstamps)
|
||||||
|
|
||||||
np.savez(vec_rep_file, embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
|
np.savez(
|
||||||
message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
|
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))
|
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"):
|
||||||
|
query_scores = None
|
||||||
|
|
||||||
def get_scores_embedding_c_dir(c_dir, query_vector, redis_key = 'compute_log'):
|
|
||||||
vec_rep = get_vector_representation(c_dir, redis_key=redis_key)
|
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
|
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
|
@functools.lru_cache
|
||||||
def get_query_vector(query):
|
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)
|
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
|
return query_vector
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_embedding_score_in_folders(
|
||||||
def calculate_embedding_score_in_folders(c_dirs, threshold, query = None, query_vector = None, redis_key = 'compute_log'):
|
c_dirs, threshold, query=None, query_vector=None, redis_key="compute_log"
|
||||||
|
):
|
||||||
result_list = list()
|
result_list = list()
|
||||||
query_vector = None
|
query_vector = None
|
||||||
if query_vector is None:
|
if query_vector is None:
|
||||||
query_vector = get_query_vector(query)
|
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]
|
args = [(x, threshold, query, None, logger, redis_key) for x in c_dirs]
|
||||||
|
|
||||||
# logger.info(f"CALCULATING FOR {args}")
|
# logger.info(f"CALCULATING FOR {args}")
|
||||||
@@ -139,34 +274,41 @@ def calculate_embedding_score_in_folders(c_dirs, threshold, query = None, query_
|
|||||||
out = pool.starmap(calculate_embedding_score_in_folder, args)
|
out = pool.starmap(calculate_embedding_score_in_folder, args)
|
||||||
# logger.info(f"DONE CALCULATING FOR {args}")
|
# logger.info(f"DONE CALCULATING FOR {args}")
|
||||||
|
|
||||||
cache_files = list();
|
cache_files = list()
|
||||||
for x, cache_file_loc in out:
|
out = [x for x in out if x is not None]
|
||||||
|
|
||||||
|
|
||||||
|
for result in out:
|
||||||
try:
|
try:
|
||||||
result_list.extend(x['videos'])
|
x, cache_file_loc = result
|
||||||
cache_files.append(cache_file_loc);
|
result_list.extend(x["videos"])
|
||||||
|
cache_files.append(cache_file_loc)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e, x)
|
print(e)
|
||||||
|
|
||||||
|
return {"videos": result_list, "cache_file_locs": cache_files}
|
||||||
|
|
||||||
return {'videos':result_list, 'cache_file_locs': cache_files}
|
|
||||||
|
|
||||||
|
|
||||||
def collapse_scores_to_maxmin_avg(folder_scores):
|
def collapse_scores_to_maxmin_avg(folder_scores):
|
||||||
|
|
||||||
result = list()
|
result = list()
|
||||||
for c_data in folder_scores['videos']:
|
for c_data in folder_scores["videos"]:
|
||||||
new_d = c_data.copy()
|
new_d = c_data.copy()
|
||||||
|
|
||||||
scores = new_d['embed_scores']['score']
|
scores = new_d["embed_scores"]["score"]
|
||||||
max_score = max(scores)
|
max_score = max(scores)
|
||||||
min_score = min(scores)
|
min_score = min(scores)
|
||||||
max_score_idx = scores.index(max_score)
|
max_score_idx = scores.index(max_score)
|
||||||
min_score_idx = scores.index(min_score)
|
min_score_idx = scores.index(min_score)
|
||||||
max_score_time = new_d['embed_scores']['time'][max_score_idx]
|
max_score_time = new_d["embed_scores"]["time"][max_score_idx]
|
||||||
min_score_time = new_d['embed_scores']['time'][min_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"]["score"] = [
|
||||||
new_d['embed_scores']['time'] = max(new_d['embed_scores']['time'])
|
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)
|
result.append(new_d)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -179,8 +321,19 @@ def collapse_scores_to_maxmin_avg(folder_scores):
|
|||||||
# 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'):
|
def calculate_embedding_score_in_folder(
|
||||||
message = {'task':'SCORE_CALC_IN_FOLDER_START', 'when': str(og_dir), 'time': dt.datetime.now().timestamp()}
|
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))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
|
|
||||||
if query_vector is None:
|
if query_vector is None:
|
||||||
@@ -188,53 +341,68 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
|
|||||||
|
|
||||||
candidate_dirs = list()
|
candidate_dirs = list()
|
||||||
candidate_dirs.append(og_dir)
|
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_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", "/mnt/hdd_24tb_1/videos/ftp"))
|
||||||
|
|
||||||
c_dir = None
|
c_dir = None
|
||||||
for candidate in candidate_dirs:
|
for candidate in candidate_dirs:
|
||||||
if os.path.exists(candidate):
|
if os.path.exists(candidate):
|
||||||
c_dir = candidate
|
if len([x for x in os.listdir(candidate) if x.endswith(".mp4")]) > 5:
|
||||||
break
|
c_dir = candidate
|
||||||
|
break
|
||||||
if c_dir is None:
|
if c_dir is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
vec_cache_str = md5(query_vector).hexdigest()
|
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):
|
if os.path.exists(cache_file_loc):
|
||||||
logger.info(f"TRYING TO LOAD CACHE {cache_file_loc}")
|
logger.info(f"TRYING TO LOAD CACHE {cache_file_loc}")
|
||||||
try:
|
try:
|
||||||
|
with open(cache_file_loc, "rb") as f:
|
||||||
with open(cache_file_loc, 'rb') as f:
|
|
||||||
video_json_info = pickle.load(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)
|
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:
|
if files_on_disk == files_in_cache:
|
||||||
logger.info(f"LOADED EMBEDDING SCORE FROM CACHE {cache_file_loc}")
|
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}
|
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))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
return (video_json_info, cache_file_loc)
|
return (video_json_info, cache_file_loc)
|
||||||
else:
|
else:
|
||||||
logger.info(f"CACHE FILE IS OLD, DELETING VEC REP FILE AND RECREATING {cache_file_loc}")
|
logger.info(
|
||||||
os.remove( get_vec_rep_file_loc(c_dir))
|
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:
|
except Exception as e:
|
||||||
logger.info(f"CACHE FILE IS CORRUPT, RECREATING {cache_file_loc} {e}")
|
logger.info(f"CACHE FILE IS CORRUPT, RECREATING {cache_file_loc} {e}")
|
||||||
os.remove(cache_file_loc)
|
os.remove(cache_file_loc)
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# vec_rep = get_vector_representation(c_dir, redis_key = redis_key)
|
# 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()
|
video_json_info = list()
|
||||||
idces_keep = np.where(query_scores > threshold)[0]
|
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)
|
videos_that_match = np.unique(video_id)
|
||||||
|
|
||||||
# subset_timestampsF = list()
|
# subset_timestampsF = list()
|
||||||
@@ -244,12 +412,13 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
|
|||||||
# max_idces = idces_entry[-1]
|
# max_idces = idces_entry[-1]
|
||||||
# subset_timestampsF.append( [ vec_rep['timestamps'][min_idces], vec_rep['timestamps'][max_idces]])
|
# 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]
|
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
|
idces_split = (
|
||||||
subset_timestampsF = np.split(vec_rep['timestamps'][id_extract_video_level], 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:
|
for subset_t in subset_timestampsF:
|
||||||
if len(subset_t) == 0:
|
if len(subset_t) == 0:
|
||||||
@@ -257,37 +426,50 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
|
|||||||
|
|
||||||
min_t = min(subset_t)
|
min_t = min(subset_t)
|
||||||
max_t = max(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:
|
if len(idces_curr) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
unq_vids = np.unique(vec_rep['idces'][idces_curr])
|
unq_vids = np.unique(vec_rep["idces"][idces_curr])
|
||||||
subset_idx = np.where(np.isin(vec_rep['idces'],unq_vids))[0]
|
subset_idx = np.where(np.isin(vec_rep["idces"], unq_vids))[0]
|
||||||
|
|
||||||
subset_idces = vec_rep['idces'][subset_idx]
|
subset_idces = vec_rep["idces"][subset_idx]
|
||||||
subset_timestamps = vec_rep['timestamps'][subset_idx]
|
subset_timestamps = vec_rep["timestamps"][subset_idx]
|
||||||
subset_scores = query_scores[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_idces = np.split(subset_idces, idx_split)
|
||||||
split_timestamps = np.split(subset_timestamps, idx_split)
|
split_timestamps = np.split(subset_timestamps, idx_split)
|
||||||
split_scores = np.split(subset_scores, idx_split)
|
split_scores = np.split(subset_scores, idx_split)
|
||||||
split_files = [vec_rep['source_files'][x[0]] for x in 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):
|
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))
|
start_time = float(min(s_tstamps))
|
||||||
end_time = float(max(s_tstamps))
|
end_time = float(max(s_tstamps))
|
||||||
|
|
||||||
frame_time = (s_tstamps - start_time).tolist()
|
frame_time = (s_tstamps - start_time).tolist()
|
||||||
embed_scores = s_scores.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)
|
video_json_info.append(c_data)
|
||||||
|
|
||||||
message = {'task':'SCORE_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
|
message = {
|
||||||
|
"task": "SCORE_CALC_IN_FOLDER_DONE",
|
||||||
|
"when": str(c_dir),
|
||||||
|
"time": dt.datetime.now().timestamp(),
|
||||||
|
}
|
||||||
r.rpush(redis_key, json.dumps(message))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
to_write = {'source_files': vec_rep['source_files'], 'videos': video_json_info}
|
to_write = {"source_files": vec_rep["source_files"], "videos": video_json_info}
|
||||||
with open(cache_file_loc, 'wb') as f:
|
with open(cache_file_loc, "wb") as f:
|
||||||
logger.info(f"WRITING EMBEDDING SCORE TO CACHE {cache_file_loc}")
|
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}")
|
logger.info(f"SAVED EMBEDDING SCORE TO CACHE {cache_file_loc}")
|
||||||
@@ -296,15 +478,16 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
|
|||||||
|
|
||||||
def get_matching_file_given_filename(web_name, folder_scores):
|
def get_matching_file_given_filename(web_name, folder_scores):
|
||||||
file_name = None
|
file_name = None
|
||||||
for x in folder_scores['videos']:
|
for x in folder_scores["videos"]:
|
||||||
if x['file_name'].endswith(web_name):
|
if x["file_name"].endswith(web_name):
|
||||||
file_name = x['file_name']
|
file_name = x["file_name"]
|
||||||
|
|
||||||
|
|
||||||
candidate_files = list()
|
candidate_files = list()
|
||||||
candidate_files.append(file_name)
|
candidate_files.append(file_name)
|
||||||
candidate_files.append(file_name.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp'))
|
candidate_files.append(
|
||||||
candidate_files.append(file_name.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp'))
|
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
|
file_name = None
|
||||||
for candidate in candidate_files:
|
for candidate in candidate_files:
|
||||||
@@ -312,25 +495,22 @@ def get_matching_file_given_filename(web_name, folder_scores):
|
|||||||
file_name = candidate
|
file_name = candidate
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return file_name
|
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()
|
ranges = list()
|
||||||
for vids in op['videos']:
|
for vids in op["videos"]:
|
||||||
ranges.append( (vids['start_time'], vids['end_time']) )
|
ranges.append((vids["start_time"], vids["end_time"]))
|
||||||
|
|
||||||
breaks = list()
|
breaks = list()
|
||||||
for idx in range(len(ranges)-1):
|
for idx in range(len(ranges) - 1):
|
||||||
current_range = ranges[idx]
|
current_range = ranges[idx]
|
||||||
next_range = ranges[idx+1]
|
next_range = ranges[idx + 1]
|
||||||
|
|
||||||
end_now = current_range[1]
|
end_now = current_range[1]
|
||||||
start_next = next_range[0]
|
start_next = next_range[0]
|
||||||
|
|||||||
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 typing import Union, Optional, List
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import FastAPI, Request, Depends
|
from fastapi import FastAPI, Request, Depends
|
||||||
from CommonCode.settings import get_logger
|
from CommonCode.settings import get_logger
|
||||||
|
from CommonCode.video_meta import FTPVideo
|
||||||
import logging
|
import logging
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
import os
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from fastapi import FastAPI, Request, status
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
@@ -20,15 +34,54 @@ session_manager = SessionManager(
|
|||||||
interface=RedisSessionInterface(redis.from_url("redis://localhost"))
|
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)
|
logger = get_logger(
|
||||||
r = redis.Redis(host='localhost', port=6379, db=15)
|
__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):
|
class VideosPostRequest(BaseModel):
|
||||||
query: str = "A cat and a human"
|
query: str = "A cat and a human"
|
||||||
threshold: float = 0.10
|
threshold: float = 0.10
|
||||||
c_dirs: Optional[List[str]] = None
|
c_dirs: Optional[List[str]] = None
|
||||||
task_id: str = 'compute_log'
|
task_id: str = "compute_log"
|
||||||
|
|
||||||
|
|
||||||
@app.post("/videos.json")
|
@app.post("/videos.json")
|
||||||
async def videos_json(
|
async def videos_json(
|
||||||
@@ -61,27 +114,24 @@ async def videos_json(
|
|||||||
"/srv/ftp_tcc/leopards1/2025/09/11",
|
"/srv/ftp_tcc/leopards1/2025/09/11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# print(','.join([str(x) for x in c_dirs]))
|
# print(','.join([str(x) for x in c_dirs]))
|
||||||
# message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()}
|
# message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()}
|
||||||
# r.rpush(task_id, json.dumps(message))?
|
# r.rpush(task_id, json.dumps(message))?
|
||||||
|
|
||||||
|
|
||||||
for x in c_dirs:
|
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))
|
r.rpush(task_id, json.dumps(message))
|
||||||
|
|
||||||
folder_scores = ES.calculate_embedding_score_in_folders(
|
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:
|
# if p_hits != ES.calculate_embedding_score_in_folders.cache_info().hits:
|
||||||
# logger.info("FROM CACHE")
|
# logger.info("FROM CACHE")
|
||||||
# else:pp
|
# else:pp
|
||||||
# logger.info("COMPUTED FROM SCRATCH")
|
# logger.info("COMPUTED FROM SCRATCH")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
folder_scores["breaks"] = ES.add_breaks_between_videos(folder_scores)
|
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
|
return folder_scores
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user