YACWC
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 4,
|
||||||
"useTabs": false
|
"useTabs": false,
|
||||||
|
"experimentalOperatorPosition": "start"
|
||||||
}
|
}
|
||||||
|
|||||||
11
SearchFrontend/search_ui/package-lock.json
generated
11
SearchFrontend/search_ui/package-lock.json
generated
@@ -1753,9 +1753,9 @@
|
|||||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="
|
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="
|
||||||
},
|
},
|
||||||
"error-ex": {
|
"error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-arrayish": "^0.2.1"
|
"is-arrayish": "^0.2.1"
|
||||||
}
|
}
|
||||||
@@ -2561,6 +2561,11 @@
|
|||||||
"prop-types": "^15.5.4"
|
"prop-types": "^15.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-table": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA=="
|
||||||
|
},
|
||||||
"react-transition-group": {
|
"react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-flexbox-grid": "^2.1.2",
|
"react-flexbox-grid": "^2.1.2",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
|
"react-table": "^7.8.0",
|
||||||
"rsuite": "^5.83.3",
|
"rsuite": "^5.83.3",
|
||||||
"timelines-chart": "^2.14.2",
|
"timelines-chart": "^2.14.2",
|
||||||
"uplot": "^1.6.32",
|
"uplot": "^1.6.32",
|
||||||
|
|||||||
@@ -9,283 +9,334 @@ import "./App.css";
|
|||||||
import StatusesDisplayHUD from "./components/StatusDisplay";
|
import StatusesDisplayHUD from "./components/StatusDisplay";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const original_data = useRef(null);
|
const original_data = useRef(null);
|
||||||
const chartRef = useRef(null);
|
const chartRef = useRef(null);
|
||||||
const [dataResults, setDataResults] = useState(null);
|
const [dataResults, setDataResults] = useState(null);
|
||||||
const [statusMessages, setStatusMessages] = useState([]);
|
const [statusMessages, setStatusMessages] = useState([]);
|
||||||
const [markerTime, setMarkerTime] = useState(0);
|
const [markerTime, setMarkerTime] = useState(0);
|
||||||
const playerRef = useRef(null);
|
const playerRef = useRef(null);
|
||||||
const playerInstanceRef = useRef(null);
|
const playerInstanceRef = useRef(null);
|
||||||
// State for the values
|
// State for the values
|
||||||
window.chartRef = chartRef;
|
window.chartRef = chartRef;
|
||||||
window.playerRef = playerRef;
|
window.playerRef = playerRef;
|
||||||
window.playerInstanceRef = playerInstanceRef;
|
window.playerInstanceRef = playerInstanceRef;
|
||||||
// Slider states
|
// Slider states
|
||||||
|
|
||||||
const [sliderMin, setSliderMin] = useState(0.0);
|
const [sliderMin, setSliderMin] = useState(0.0);
|
||||||
const [sliderMax, setSliderMax] = useState(1.0);
|
const [sliderMax, setSliderMax] = useState(1.0);
|
||||||
// Date range states
|
// Date range states
|
||||||
//
|
//
|
||||||
|
|
||||||
const [startRange, setStartRange] = useState(
|
const [startRange, setStartRange] = useState(
|
||||||
new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
|
new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
);
|
|
||||||
const [endRange, setEndRange] = useState(new Date());
|
|
||||||
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
|
|
||||||
const [queryText, setQueryText] = useState("A clouded leopard and a human");
|
|
||||||
const [sliderValue, setSliderValue] = useState(0);
|
|
||||||
|
|
||||||
// State to track last submitted values
|
|
||||||
const [lastSubmitted, setLastSubmitted] = useState({
|
|
||||||
startRange,
|
|
||||||
endRange,
|
|
||||||
sliderValue,
|
|
||||||
queryText,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if any value has changed
|
|
||||||
const hasChanged =
|
|
||||||
startRange !== lastSubmitted.startRange ||
|
|
||||||
endRange !== lastSubmitted.endRange ||
|
|
||||||
sliderValue !== lastSubmitted.sliderValue ||
|
|
||||||
queryText !== lastSubmitted.queryText;
|
|
||||||
|
|
||||||
// Function to resubmit fetch
|
|
||||||
const handleResubmit = () => {
|
|
||||||
// Start streaming status updates
|
|
||||||
fetch("api/return_status")
|
|
||||||
.then((response) => {
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = ""; // Accumulate partial text
|
|
||||||
|
|
||||||
function read() {
|
|
||||||
reader.read().then(({ done, value }) => {
|
|
||||||
if (done) {
|
|
||||||
if (buffer) {
|
|
||||||
// console.log("Status:", buffer); // Log any remaining text
|
|
||||||
}
|
|
||||||
setStatusMessages([]);
|
|
||||||
// console.log("Status stream finished");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Decode only the new chunk
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
// If your server sends lines, split and log only complete lines:
|
|
||||||
let lines = buffer.split("\n");
|
|
||||||
buffer = lines.pop(); // Save incomplete line for next chunk
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim()) {
|
|
||||||
// console.log("Status:", line);
|
|
||||||
console.log(line)
|
|
||||||
setStatusMessages((msgs) => [...msgs, JSON.parse(line)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
read();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error while streaming status:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("startRange", startRange.toISOString());
|
|
||||||
params.append("endRange", endRange.toISOString());
|
|
||||||
params.append("threshold", 0.0);
|
|
||||||
params.append("query", queryText);
|
|
||||||
setDataResults({ videos: [], breaks: [] });
|
|
||||||
|
|
||||||
fetch("api/videos.json?" + params.toString())
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
const max_value = Math.max(
|
|
||||||
...data["videos"].map((vid) => vid["embed_scores"]["score"][1])
|
|
||||||
);
|
|
||||||
setSliderMax(max_value);
|
|
||||||
original_data.current = data;
|
|
||||||
window.original_data = original_data;
|
|
||||||
setDataResults(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
setLastSubmitted({ startRange, endRange, sliderValue, queryText });
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateDataAndValue(newValue) {
|
|
||||||
const floatValue = parseFloat(newValue);
|
|
||||||
setSliderValue(floatValue);
|
|
||||||
var newData = JSON.parse(JSON.stringify(original_data.current));
|
|
||||||
newData["videos"] = newData["videos"].filter(
|
|
||||||
(vid) => vid["embed_scores"]["score"][1] >= floatValue
|
|
||||||
);
|
);
|
||||||
setDataResults(newData);
|
const [endRange, setEndRange] = useState(new Date());
|
||||||
}
|
// const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000));
|
||||||
|
const [queryText, setQueryText] = useState("A clouded leopard and a human");
|
||||||
|
const [sliderValue, setSliderValue] = useState(0);
|
||||||
|
|
||||||
function setMarkerValueNonReactive(inputValue) {
|
// State to track last submitted values
|
||||||
let chart = chartRef.current.getEchartsInstance();
|
const [lastSubmitted, setLastSubmitted] = useState({
|
||||||
let options = chart.getOption();
|
startRange,
|
||||||
let mappers = options["mappers"];
|
endRange,
|
||||||
|
sliderValue,
|
||||||
|
queryText,
|
||||||
|
});
|
||||||
|
|
||||||
let vv = {
|
// Check if any value has changed
|
||||||
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
const hasChanged =
|
||||||
lineStyle: { type: "solid", color: "#FF0000", width: 2 },
|
startRange !== lastSubmitted.startRange ||
|
||||||
label: {
|
endRange !== lastSubmitted.endRange ||
|
||||||
show: false,
|
sliderValue !== lastSubmitted.sliderValue ||
|
||||||
formatter: "Break",
|
queryText !== lastSubmitted.queryText;
|
||||||
position: "bottom",
|
|
||||||
color: "#888",
|
const streamComputeStatus = () => {
|
||||||
fontSize: 10,
|
fetch("api/return_status")
|
||||||
},
|
.then((response) => {
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = ""; // Accumulate partial text
|
||||||
|
|
||||||
|
function read() {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
if (buffer) {
|
||||||
|
}
|
||||||
|
setStatusMessages([]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Decode only the new chunk
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// If your server sends lines, split and log only complete lines:
|
||||||
|
let lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop(); // Save incomplete line for next chunk
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
let c_line = JSON.parse(line);
|
||||||
|
|
||||||
|
if (c_line["task"] !== "DONE_QUIT") {
|
||||||
|
console.log(c_line);
|
||||||
|
setStatusMessages((msgs) => [
|
||||||
|
...msgs,
|
||||||
|
c_line,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
read();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error while streaming status:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Function to resubmit fetch
|
||||||
|
const handleResubmit = (doTestMode = false) => {
|
||||||
|
console.log("startRange, endRange:", startRange, endRange);
|
||||||
|
console.log("test mode:", doTestMode);
|
||||||
|
let startRangeUse;
|
||||||
|
let endRangeUse;
|
||||||
|
if (doTestMode == true) {
|
||||||
|
startRangeUse = new Date(
|
||||||
|
new Date().getTime() - 2 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
endRangeUse = new Date(
|
||||||
|
new Date().getTime() - 1 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
startRangeUse = startRange;
|
||||||
|
endRangeUse = endRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Using date range:", startRangeUse, endRangeUse);
|
||||||
|
setStartRange(startRangeUse);
|
||||||
|
setEndRange(endRangeUse);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("startRange", startRangeUse.toISOString());
|
||||||
|
params.append("endRange", endRangeUse.toISOString());
|
||||||
|
params.append("threshold", 0.0);
|
||||||
|
params.append("query", queryText);
|
||||||
|
setDataResults({ videos: [], breaks: [] });
|
||||||
|
|
||||||
|
fetch("api/videos.json?" + params.toString())
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
streamComputeStatus();
|
||||||
|
// Don't setDataResults here, since it's just {"status": ...}
|
||||||
|
pollForResult(); // Start polling for the real result
|
||||||
|
});
|
||||||
|
|
||||||
|
setLastSubmitted({
|
||||||
|
startRangeUse,
|
||||||
|
endRangeUse,
|
||||||
|
sliderValue,
|
||||||
|
queryText,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let markLine = {
|
function updateDataAndValue(newValue) {
|
||||||
symbol: ["none", "none"],
|
const floatValue = parseFloat(newValue);
|
||||||
data: [vv],
|
setSliderValue(floatValue);
|
||||||
lineStyle: { type: "dashed", color: "#FF0000", width: 2 },
|
var newData = JSON.parse(JSON.stringify(original_data.current));
|
||||||
silent: true,
|
newData["videos"] = newData["videos"].filter(
|
||||||
animation: false,
|
(vid) => vid["embed_scores"]["score"][1] >= floatValue
|
||||||
};
|
);
|
||||||
|
setDataResults(newData);
|
||||||
// if ("markLine" in options["series"][1]) {
|
|
||||||
if (false) {
|
|
||||||
let vv_new = {
|
|
||||||
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
|
||||||
};
|
|
||||||
let markLine_new = {
|
|
||||||
data: [vv_new],
|
|
||||||
};
|
|
||||||
|
|
||||||
chart.setOption(
|
|
||||||
{
|
|
||||||
series: [{}, { markLine: { data: [vv_new] } }],
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
["series.markLine"]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
chart.setOption(
|
|
||||||
{
|
|
||||||
series: [{}, { markLine: markLine }],
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
["series.markLine"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Memoize the timeline click handler
|
function setMarkerValueNonReactive(inputValue) {
|
||||||
const handleTimelineClick = useCallback(
|
let chart = chartRef.current.getEchartsInstance();
|
||||||
(path, timeoffset) => {
|
let options = chart.getOption();
|
||||||
console.log("Timeline clicked:", path, timeoffset);
|
let mappers = options["mappers"];
|
||||||
|
|
||||||
if (playerRef.current && playerInstanceRef.current) {
|
let vv = {
|
||||||
console.log("Seeking video player to:", path, timeoffset);
|
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
||||||
playerInstanceRef.current.src({
|
lineStyle: { type: "solid", color: "#FF0000", width: 2 },
|
||||||
src: "api/" + path,
|
label: {
|
||||||
type: "video/mp4",
|
show: false,
|
||||||
});
|
formatter: "Break",
|
||||||
playerInstanceRef.current.on("loadedmetadata", () => {
|
position: "bottom",
|
||||||
playerInstanceRef.current.currentTime(timeoffset);
|
color: "#888",
|
||||||
});
|
fontSize: 10,
|
||||||
}
|
},
|
||||||
},
|
};
|
||||||
[] // Empty dependency array since it only uses playerRef
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
let markLine = {
|
||||||
const params = new URLSearchParams(window.location.search); // id=123
|
symbol: ["none", "none"],
|
||||||
|
data: [vv],
|
||||||
|
lineStyle: { type: "dashed", color: "#FF0000", width: 2 },
|
||||||
|
silent: true,
|
||||||
|
animation: false,
|
||||||
|
};
|
||||||
|
|
||||||
if (params.get("test_mode") == "true") {
|
// if ("markLine" in options["series"][1]) {
|
||||||
setStartRange(new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1000));
|
if (false) {
|
||||||
setEndRange(new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000));
|
let vv_new = {
|
||||||
|
xAxis: mappers["real_to_virtual"](new Date(inputValue)),
|
||||||
|
};
|
||||||
|
let markLine_new = {
|
||||||
|
data: [vv_new],
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.setOption(
|
||||||
|
{
|
||||||
|
series: [{}, { markLine: { data: [vv_new] } }],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["series.markLine"]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
chart.setOption(
|
||||||
|
{
|
||||||
|
series: [{}, { markLine: markLine }],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
["series.markLine"]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleResubmit();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
// Memoize the timeline click handler
|
||||||
<div className="app-container">
|
const handleTimelineClick = useCallback(
|
||||||
<div className="section-box-horiz">
|
(path, timeoffset) => {
|
||||||
<div className="flex-group">
|
if (playerRef.current && playerInstanceRef.current) {
|
||||||
<CustomDateRangePicker
|
playerInstanceRef.current.src({
|
||||||
startDate={startRange}
|
src: "api/" + path,
|
||||||
endDate={endRange}
|
type: "video/mp4",
|
||||||
setStartRange={setStartRange}
|
});
|
||||||
setEndRange={setEndRange}
|
playerInstanceRef.current.on("loadedmetadata", () => {
|
||||||
/>
|
playerInstanceRef.current.currentTime(timeoffset);
|
||||||
</div>
|
});
|
||||||
<div className="flex-group">
|
}
|
||||||
<input
|
},
|
||||||
type="text"
|
[] // Empty dependency array since it only uses playerRef
|
||||||
placeholder="Enter query"
|
);
|
||||||
value={queryText}
|
|
||||||
onChange={(e) => setQueryText(e.target.value)}
|
|
||||||
style={{
|
|
||||||
marginLeft: "16px",
|
|
||||||
marginRight: "16px",
|
|
||||||
padding: "8px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #343a40",
|
|
||||||
color: "#fff", // Text white
|
|
||||||
backgroundColor: "#23272f", // Optional: dark background for contrast
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-group">
|
|
||||||
<label
|
|
||||||
style={{ marginLeft: "8px", marginRight: "8px", color: "#fff" }}
|
|
||||||
>
|
|
||||||
Threshold:
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex-group">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={sliderMin}
|
|
||||||
max={sliderMax}
|
|
||||||
step={0.001}
|
|
||||||
value={sliderValue}
|
|
||||||
onChange={(e) => updateDataAndValue(e.target.value)}
|
|
||||||
style={{
|
|
||||||
width: "120px",
|
|
||||||
color: "#fff", // Text white
|
|
||||||
backgroundColor: "#23272f", // Optional: dark background for contrast
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-group">
|
|
||||||
<span style={{ marginLeft: "8px", color: "#fff" }}>
|
|
||||||
{sliderValue.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-group">
|
|
||||||
<button onClick={handleResubmit}>Resubmit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<StatusesDisplayHUD statusMessages={statusMessages} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="timeline-container">
|
useEffect(() => {
|
||||||
<EmbedTimeline
|
const params = new URLSearchParams(window.location.search);
|
||||||
chartRef={chartRef}
|
|
||||||
data_in={dataResults}
|
handleResubmit(params.get("test_mode") === "true");
|
||||||
onTimelineClick={handleTimelineClick}
|
}, []);
|
||||||
/>
|
|
||||||
</div>
|
// useEffect(() => {
|
||||||
<div className="section-box">
|
// if (startRange && endRange) {
|
||||||
<VideoPlayer
|
// handleResubmit();
|
||||||
videoRef={playerRef}
|
// }
|
||||||
playerInstanceRef={playerInstanceRef}
|
// }, [startRange, endRange]);
|
||||||
setMarkerTimeFunc={setMarkerValueNonReactive}
|
|
||||||
/>
|
function pollForResult() {
|
||||||
</div>
|
const poll = () => {
|
||||||
</div>
|
fetch("api/videos_result.json")
|
||||||
);
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.status === "processing") {
|
||||||
|
setTimeout(poll, 250); // Try again in 1 second
|
||||||
|
} else {
|
||||||
|
const max_value = Math.max(
|
||||||
|
...data["videos"].map(
|
||||||
|
(vid) => vid["embed_scores"]["score"][1]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSliderMax(max_value);
|
||||||
|
|
||||||
|
original_data.current = data;
|
||||||
|
setDataResults(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="section-box-horiz">
|
||||||
|
<div className="flex-group">
|
||||||
|
<CustomDateRangePicker
|
||||||
|
startDate={startRange}
|
||||||
|
endDate={endRange}
|
||||||
|
setStartRange={setStartRange}
|
||||||
|
setEndRange={setEndRange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter query"
|
||||||
|
value={queryText}
|
||||||
|
onChange={(e) => setQueryText(e.target.value)}
|
||||||
|
style={{
|
||||||
|
marginLeft: "16px",
|
||||||
|
marginRight: "16px",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1px solid #343a40",
|
||||||
|
color: "#fff", // Text white
|
||||||
|
backgroundColor: "#23272f", // Optional: dark background for contrast
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-group">
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
marginLeft: "8px",
|
||||||
|
marginRight: "8px",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Threshold:
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex-group">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={sliderMin}
|
||||||
|
max={sliderMax}
|
||||||
|
step={0.001}
|
||||||
|
value={sliderValue}
|
||||||
|
onChange={(e) => updateDataAndValue(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: "120px",
|
||||||
|
color: "#fff", // Text white
|
||||||
|
backgroundColor: "#23272f", // Optional: dark background for contrast
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-group">
|
||||||
|
<span style={{ marginLeft: "8px", color: "#fff" }}>
|
||||||
|
{sliderValue.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-group">
|
||||||
|
<button onClick={handleResubmit}>Resubmit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StatusesDisplayHUD statusMessages={statusMessages} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="timeline-container">
|
||||||
|
<EmbedTimeline
|
||||||
|
chartRef={chartRef}
|
||||||
|
data_in={dataResults}
|
||||||
|
onTimelineClick={handleTimelineClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="section-box">
|
||||||
|
<VideoPlayer
|
||||||
|
videoRef={playerRef}
|
||||||
|
playerInstanceRef={playerInstanceRef}
|
||||||
|
setMarkerTimeFunc={setMarkerValueNonReactive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
90
SearchFrontend/search_ui/src/components/StatusDisplay.css
Normal file
90
SearchFrontend/search_ui/src/components/StatusDisplay.css
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/* Container */
|
||||||
|
.table-container {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #23272f;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.10);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: #23272f;
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Header */
|
||||||
|
th {
|
||||||
|
background: linear-gradient(90deg, #2c313c 80%, #23272f 100%);
|
||||||
|
color: #a9b7c6;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 2px solid #3a7afe;
|
||||||
|
text-align: left;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Body */
|
||||||
|
td {
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-bottom: 1px solid #343a40;
|
||||||
|
background: #23272f;
|
||||||
|
color: #e0e6ed;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row hover */
|
||||||
|
tr:hover td {
|
||||||
|
background: #2c313c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First column highlight */
|
||||||
|
td:first-child, th:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3a7afe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Last row no border */
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table column widths */
|
||||||
|
table th:last-child,
|
||||||
|
table td:last-child {
|
||||||
|
width: 30%;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 400px;
|
||||||
|
/* Prevent shrinking below 30% on wide screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
table th:not(:last-child),
|
||||||
|
table td:not(:last-child) {
|
||||||
|
width: calc(70% / (var(--col-count, 1)));
|
||||||
|
/* --col-count should be set to (number of columns - 1) in your table element via JS or React */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive tweaks */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.table-container {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,159 @@
|
|||||||
|
import { useTable } from "react-table";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import "./StatusDisplay.css";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function StatusesDisplayHUD({ statusMessages }) {
|
export default function StatusesDisplayHUD({ statusMessages }) {
|
||||||
|
|
||||||
|
|
||||||
const msg = {};
|
const msg = {};
|
||||||
|
const dataPre = {};
|
||||||
|
const columns = [
|
||||||
|
{ Header: "When", accessor: "WHEN" },
|
||||||
|
{ Header: "Scheduled", accessor: "SCHEDULED" },
|
||||||
|
{ Header: "Queued", accessor: "QUEUED" },
|
||||||
|
{ Header: "Processing", accessor: "VECTOR_CALC" },
|
||||||
|
{ Header: "Calculating", accessor: "SCORE_CALC" },
|
||||||
|
{ Header: "Status", accessor: "STATUS" },
|
||||||
|
];
|
||||||
|
|
||||||
statusMessages.forEach((m) => {
|
statusMessages.forEach((m) => {
|
||||||
let when_key = 'other'
|
let when_key = "other";
|
||||||
if (m['task'] == 'SCHEDULED')
|
if (m["task"] == "SCHEDULED")
|
||||||
m['when'].forEach(( w ) => { msg[w] = 'Scheduled' })
|
m["when"].forEach((w) => {
|
||||||
|
msg[w] = "Scheduled";
|
||||||
|
dataPre[w] = {
|
||||||
|
SCHEDULED: "✓",
|
||||||
|
QUEUED: "",
|
||||||
|
VECTOR_CALC: "",
|
||||||
|
SCORE_CALC: "",
|
||||||
|
STATUS: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
else {
|
else {
|
||||||
if ('when' in m)
|
if ("when" in m) when_key = m["when"];
|
||||||
when_key = m['when']
|
let c_task = m["task"];
|
||||||
msg[when_key] = m['task']
|
|
||||||
|
let msg_show;
|
||||||
|
switch (c_task) {
|
||||||
|
case "SCHEDULED":
|
||||||
|
msg_show = "Scheduled";
|
||||||
|
dataPre[when_key]["SCHEDULED"] = "✓";
|
||||||
|
break;
|
||||||
|
case "QUEUEING":
|
||||||
|
msg_show = "In compute queue";
|
||||||
|
dataPre[when_key]["QUEUED"] = "✓";
|
||||||
|
break;
|
||||||
|
case "SCORE_CALC_IN_FOLDER_START":
|
||||||
|
msg_show = "Calculating Scores";
|
||||||
|
dataPre[when_key]["VECTOR_CALC"] = "...";
|
||||||
|
break;
|
||||||
|
case "VECTOR_CALC_IN_FOLDER_START":
|
||||||
|
msg_show = "Started processing videos";
|
||||||
|
dataPre[when_key]["VECTOR_CALC"] = "...";
|
||||||
|
break;
|
||||||
|
case "VECTOR_CALC_IN_FOLDER_BUMP":
|
||||||
|
msg_show =
|
||||||
|
"Processing videos: " +
|
||||||
|
m["progress"] +
|
||||||
|
"/" +
|
||||||
|
m["how_many"];
|
||||||
|
dataPre[when_key]["VECTOR_CALC"] =
|
||||||
|
m["progress"] + "/" + m["how_many"];
|
||||||
|
break;
|
||||||
|
case "VECTOR_CALC_IN_FOLDER_DONE":
|
||||||
|
msg_show = "Finished processing videos";
|
||||||
|
dataPre[when_key]["VECTOR_CALC"] = "✓";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SCORE_CALC_IN_FOLDER_DONE":
|
||||||
|
msg_show = "Finished calculating scores";
|
||||||
|
dataPre[when_key]["VECTOR_CALC"] = "✓";
|
||||||
|
dataPre[when_key]["SCORE_CALC"] = "✓";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
msg_show = c_task;
|
||||||
|
}
|
||||||
|
msg[when_key] = msg_show;
|
||||||
|
console.log(when_key);
|
||||||
|
dataPre[when_key]["STATUS"] = msg_show;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.entries(dataPre).forEach(([k, v]) => {
|
||||||
|
v["WHEN"] = k;
|
||||||
|
});
|
||||||
|
const data = useMemo(() => Object.values(dataPre), [dataPre]);
|
||||||
|
const columnsMemo = useMemo(() => columns, []);
|
||||||
|
|
||||||
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
|
||||||
|
useTable({ columns: columnsMemo, data });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="table-container">
|
||||||
{Object.entries(msg).map(([when, messages], idx) => (
|
<table
|
||||||
<StatusDisplay key={when} when={when} message={messages} />
|
{...getTableProps()}
|
||||||
))}
|
style={{ "--col-count": columns.length - 1 }}
|
||||||
</div>
|
>
|
||||||
);
|
{rows.length > 0 && (
|
||||||
|
<thead>
|
||||||
|
{headerGroups.map((headerGroup) => (
|
||||||
|
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||||
|
{headerGroup.headers.map((column) => (
|
||||||
|
<th {...column.getHeaderProps()}>
|
||||||
|
{column.render("Header")}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
)}
|
||||||
|
<tbody {...getTableBodyProps()}>
|
||||||
|
{rows.map((row) => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<tr {...row.getRowProps()}>
|
||||||
|
{row.cells.map((cell) => (
|
||||||
|
<td {...cell.getCellProps()}>
|
||||||
|
{cell.render("Cell")}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{Object.entries(msg).map(([when, messages], idx) => (
|
||||||
|
<StatusDisplay key={when} when={when} message={messages} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StatusDisplay({ when, message }) {
|
||||||
|
let msg_show = "";
|
||||||
|
|
||||||
export function StatusDisplay({when, message }) {
|
msg_show = when + ": " + message;
|
||||||
let msg_show = ''
|
|
||||||
|
|
||||||
msg_show = when + ': ' + message
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
return (
|
className="status-message"
|
||||||
<div
|
style={{
|
||||||
className="status-message"
|
color: "#fff",
|
||||||
style={{
|
background: "#23272f",
|
||||||
color: "#fff",
|
padding: "8px",
|
||||||
background: "#23272f",
|
margin: "4px 0",
|
||||||
padding: "8px",
|
borderRadius: "4px",
|
||||||
margin: "4px 0",
|
minHeight: "20px",
|
||||||
borderRadius: "4px",
|
}}
|
||||||
minHeight: "20px",
|
>
|
||||||
}}
|
{msg_show}
|
||||||
>
|
</div>
|
||||||
{msg_show}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// <div
|
// <div
|
||||||
// className="status-messages"
|
// className="status-messages"
|
||||||
// style={{
|
// style={{
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ def get_matching_file_for_tstamp(target_tstamp, folder_scores):
|
|||||||
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
|
||||||
@@ -92,7 +91,7 @@ def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute
|
|||||||
all_tstamps.append( [x.timestamp() for x in hh['frame_time']])
|
all_tstamps.append( [x.timestamp() for x in hh['frame_time']])
|
||||||
enu +=1
|
enu +=1
|
||||||
|
|
||||||
message = {'task':'VECTOR_CALC_IN_FOLDER_BUMP', 'progress': idx+1, 'how_many': len(sorted_videos), 'time': dt.datetime.now().timestamp()}
|
message = {'task':'VECTOR_CALC_IN_FOLDER_BUMP', 'when': c_dir, 'progress': idx+1, 'how_many': len(sorted_videos), 'time': dt.datetime.now().timestamp()}
|
||||||
r.rpush(redis_key, json.dumps(message))
|
r.rpush(redis_key, json.dumps(message))
|
||||||
|
|
||||||
if len(all_cat) == 0:
|
if len(all_cat) == 0:
|
||||||
@@ -104,6 +103,7 @@ def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute
|
|||||||
|
|
||||||
np.savez(vec_rep_file, embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
|
np.savez(vec_rep_file, embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
|
||||||
message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
|
message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()}
|
||||||
|
r.rpush(redis_key, json.dumps(message))
|
||||||
return dict( embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
|
return dict( embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute
|
|||||||
def get_scores_embedding_c_dir(c_dir, query_vector, redis_key = 'compute_log'):
|
def get_scores_embedding_c_dir(c_dir, query_vector, redis_key = 'compute_log'):
|
||||||
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()
|
query_scores = (query_vector @ vec_rep['embeds'].T).squeeze()
|
||||||
return query_scores
|
return vec_rep, query_scores
|
||||||
|
|
||||||
@functools.lru_cache
|
@functools.lru_cache
|
||||||
def get_query_vector(query):
|
def get_query_vector(query):
|
||||||
@@ -226,8 +226,8 @@ def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_v
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
vec_rep = get_vector_representation(c_dir, redis_key = redis_key)
|
# vec_rep = get_vector_representation(c_dir, redis_key = redis_key)
|
||||||
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)
|
||||||
|
|
||||||
video_json_info = list()
|
video_json_info = list()
|
||||||
idces_keep = np.where(query_scores > threshold)[0]
|
idces_keep = np.where(query_scores > threshold)[0]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
from typing import Union, Optional, List
|
from typing import Union, Optional, List
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import FastAPI, Request, Depends
|
from fastapi import FastAPI, Request, Depends
|
||||||
@@ -24,9 +25,9 @@ r = redis.Redis(host='localhost', port=6379, db=15)
|
|||||||
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -61,9 +62,9 @@ async def videos_json(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user