diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..8ae30d8 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,181 @@ + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a AS build + +ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" +ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" + +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# hadolint ignore=DL3008 +RUN apt-get update && \ + apt-get install -y --only-upgrade --no-install-recommends libc-bin libc6 && \ + apt-get install -y -q --no-install-recommends python3-setuptools \ + python3-dev python3-pip python3-venv \ + curl libgl1-mesa-glx \ + # Update and install necessary build tools and dependencies for OpenCV + build-essential \ + cmake \ + git \ + wget \ + unzip \ + yasm \ + pkg-config \ + libgl1 \ + libglib2.0-0 \ + libgtk2.0-dev \ + libtbb-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + libxvidcore-dev \ + libxine2-dev \ + libv4l-dev \ + libdc1394-dev \ + libatlas-base-dev \ + gfortran && \ + # Install necessary CUDA packages + curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ + apt-get update && \ + apt-get install -y -q --no-install-recommends \ + cuda-toolkit-12-4 \ + libcudnn9-dev-cuda-12 \ + libnpp-dev-12-4 \ + && \ + rm -rf /var/lib/apt/lists/* && apt-get clean + +RUN python3 -m venv ${VIRTUAL_ENV} && \ + ${VIRTUAL_ENV}/bin/pip install --no-cache-dir \ + "pip==26.0.1" \ + "torch==2.10.0" \ + "torchvision==0.25.0" \ + "numpy==1.26.0" + +# OPENCV W/ CUDA SUPPORT +ENV PYTHON_VERSION=3.10 +ENV OPENCV_VERSION="4.11.0" +ENV DEPENDENCY_DIR=/tmp/build_opencv +ENV NUMPY_PATH="${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages/numpy/core/include" +ENV SYS_PATH="/usr/include/python${PYTHON_VERSION}" + +# Clone OpenCV and OpenCV Contrib repositories +WORKDIR ${DEPENDENCY_DIR} +RUN git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv.git ${DEPENDENCY_DIR}/opencv && \ + git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv_contrib.git ${DEPENDENCY_DIR}/opencv_contrib + +# Create build directory and run CMake +WORKDIR ${DEPENDENCY_DIR}/opencv/build +RUN ln -s /usr/include/x86_64-linux-gnu/cudnn*.h /usr/local/cuda/include/ && \ + ln -s /usr/lib/x86_64-linux-gnu/libcudnn*.so* /usr/local/cuda/lib64/ +RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ + -D CMAKE_INSTALL_PREFIX="${VIRTUAL_ENV}" \ + -D OPENCV_EXTRA_MODULES_PATH="${DEPENDENCY_DIR}/opencv_contrib/modules" \ + -D WITH_CUDA=ON \ + -D WITH_CUDNN=ON \ + -D WITH_CUBLAS=ON \ + -D WITH_FFMPEG=ON \ + -D WITH_TBB=ON \ + -D WITH_V4L=ON \ + -D OPENCV_DNN_CUDA=ON \ + -D CUDA_ARCH_BIN=70,75,80,86,89,90 \ + -D CUDA_FAST_MATH=ON \ + -D CUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda \ + -D CUDNN_INCLUDE_DIR=/usr/include/x86_64-linux-gnu \ + -D CUDNN_LIBRARY=/usr/lib/x86_64-linux-gnu/libcudnn.so \ + -D BUILD_opencv_cudaimgproc=ON \ + -D BUILD_opencv_cudaarithm=ON \ + -D BUILD_opencv_cudafilters=ON \ + -D BUILD_opencv_cudacodec=ON \ + -D WITH_NVCUVID=ON \ + -D WITH_NVCUVENC=ON \ + -D WITH_VAAPI=ON \ + -D WITH_FFMPEG=ON \ + -D BUILD_opencv_python3=ON \ + -D PYTHON3_EXECUTABLE="${VIRTUAL_ENV}/bin/python3" \ + -D PYTHON3_NUMPY_INCLUDE_DIRS="${VIRTUAL_ENV}/lib/python3.10/site-packages/numpy/core/include" \ + -D OPENCV_SKIP_PYTHON_LOADER=ON \ + -D BUILD_EXAMPLES=OFF -D BUILD_TESTS=OFF -D BUILD_PERF_TESTS=OFF .. \ + && make -j$(nproc) && make install && ldconfig + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN mkdir -p /tmp/cv2_deps && \ + PY_CV2_SO=$(find "${VIRTUAL_ENV}" -name "cv2*.so" | head -n 1) && \ + ldd "${PY_CV2_SO}" | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /tmp/cv2_deps/ && \ + cp -v /usr/local/cuda/lib64/libnpp*.so* /tmp/cv2_deps/ && \ + cp -v /usr/local/cuda/lib64/libcudart.so* /tmp/cv2_deps/ && \ + cp -P /usr/local/cuda/lib64/libnvrtc.so* /tmp/cv2_deps/ && \ + cp -v /usr/lib/x86_64-linux-gnu/libnvidia-encode.so* /tmp/cv2_deps/ || true + + +# Clean up +RUN rm -rf ${DEPENDENCY_DIR} + + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a + +ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" + +# Install Runtime Libraries, FFmpeg/VA-API headers, and cuDNN +# Includes libva and ffmpeg libraries required for HEVC decoding +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + python3 \ + libgl1 \ + libglib2.0-0 \ + libtbb12 \ + # Critical for Hardware Decoding (HEVC) + libva2 \ + libva-drm2 \ + libva-x11-2 \ + libavcodec58 \ + libavformat58 \ + libswscale5 \ + libv4l-0 && \ + # Install necessary CUDA packages + curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ + apt-get update && \ + apt-get install -y -q --no-install-recommends libcudnn9-cuda-12 \ + libavcodec-dev \ + libavformat-dev \ + libavutil-dev nvidia-cuda-dev && \ + rm -rf /var/lib/apt/lists/* + +# Copy the entire pre-compiled virtual environment from the build stage +COPY --from=build ${VIRTUAL_ENV} ${VIRTUAL_ENV} +# Copy NPP (NVIDIA Performance Primitives) libs - Required for OpenCV CUDA +# COPY --from=build /usr/local/cuda/lib64/libnpp*.so* /usr/local/cuda/lib64/ +# Copy additional required CUDA math/parallel libs +RUN mkdir -p /usr/local/cuda/lib64 +COPY --from=build /tmp/cv2_deps/* /usr/local/cuda/lib64/ +ENV LD_LIBRARY_PATH="/usr/local/cuda/lib64:${VIRTUAL_ENV}/lib:${LD_LIBRARY_PATH}" +RUN ldconfig + +ARG DEVICE="CPU" +ENV DEVICE="${DEVICE}" + +# Set the working directory in the container +WORKDIR /home +COPY requirements.txt /home/ +ENV NVIDIA_DRIVER_CAPABILITIES=all + +# RUN pip3 install --no-cache-dir "pytest>=9.0.3" && \ +# pip3 install --no-cache-dir --require-hashes -r /home/requirements.CPU.txt --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple && \ +# pip3 uninstall -y ultralytics opencv-python opencv-contrib-python opencv-python-headless && \ +# pip3 install --no-cache-dir --require-hashes -r /home/requirements.GPU.txt && \ +# pip3 uninstall -y opencv-python opencv-contrib-python opencv-python-headless + + +RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ + pip3 uninstall -y opencv-python opencv-contrib-python opencv-python-headless \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b44624a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,74 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Pipeline Dev", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + // "image": "mcr.microsoft.com/devcontainers/base:bullseye" + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile" : "Dockerfile", + // "context" : "..", + // "dockerfile" : "../fastapi/Dockerfile", + "context" : "../fastapi", + "args":{ + "HTTP_PROXY" : "${localEnv:HTTP_PROXY}", + "http_proxy" : "${localEnv:http_proxy}", + "HTTPS_PROXY" : "${localEnv:HTTPS_PROXY}", + "https_proxy" : "${localEnv:https_proxy}", + "NO_PROXY" : "${localEnv:NO_PROXY}", + "no_proxy" : "${localEnv:no_proxy}", + } + }, + "containerEnv" : { + "HTTP_PROXY" : "${localEnv:HTTP_PROXY}", + "http_proxy" : "${localEnv:http_proxy}", + "HTTPS_PROXY" : "${localEnv:HTTPS_PROXY}", + "https_proxy" : "${localEnv:https_proxy}", + "NO_PROXY" : "${localEnv:NO_PROXY}", + "no_proxy" : "${localEnv:no_proxy}", + }, + + "runArgs" : [ "--rm", "--gpus", "all", "--ipc=host", "--name", "pipeline_dev", "-p", "8011:8000","--cap-add=SYS_NICE", "--shm-size=8gb"], //, "--net=host", "--privileged", "--shm-size=2gb"], + + "postStartCommand": "apt-get update -o Acquire::Check-Date=false -y && apt-get install -y gdb sudo && cp -rp /workspace/app/*.md /app/ && cp -rp /workspace/app/.vscode /app/ && cp -rp /workspace/app/.gitignore /app/ && cp -rp /workspace/app/.github /app/", + // "postAttachCommand": "sudo chown -R ${localEnv:USER} /workspaces", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + "customizations": { + "vscode" : { + "extensions" : [ + "ms-python.python", + "ms-python.vscode-pylance", + "esbenp.prettier-vscode" + ] + } + }, + + "workspaceFolder": "/home", + "workspaceMount": "source=./,target=/workspace/app,type=bind,consistency=cached", + + // For training only + // "mounts": [ + // "source=/data1/dataset,target=/workspace/dataset,type=bind" + // ], + + // For fastapi + "mounts": [ + "source=./inputs,target=/watch_dir,type=bind", + "source=./fastapi,target=/home,type=bind" + // "source=./fastapi/resources,target=/home/resources,type=bind", + // "source=./fastapi/tests,target=/home/tests,type=bind" + ], + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" + // "containerUser": "${localEnv:USER}", + // "remoteUser": "${localEnv:USER}", + "updateRemoteUserUID": true //automatically updates the container user's UID and GID to match your local user's UID and GID to prevent permission issues with bind mounts +} diff --git a/.github/assets/fastapi/requirements.in b/.github/assets/fastapi/requirements.in new file mode 100644 index 0000000..a66ded7 --- /dev/null +++ b/.github/assets/fastapi/requirements.in @@ -0,0 +1,26 @@ +av>=17.0.1 +cucim>=23.10.0 +cupy-cuda12x>=13.6.0 +fastapi>=0.135.1 +nncf>=2.19.0 +numpy>=1.26.4 +nvidia-cuda-runtime-cu12>=12.8.90 +nvidia-cudnn-cu12>=9.10.2.21 +onnx>=1.21.0 +onnxruntime-gpu>=1.23.2 +onnxslim>=0.1.82 +openvino-dev>=2024.6.0 +pip>=26.0.1 +protobuf>=5.29.6,<6 # For VDMS +pynvvideocodec>=2.1.0 +pytest>=9.0.3 +requests>=2.33.0 +tensorrt_cu12==10.14.1.48.post1 +torch==2.10.0 +torchvision==0.25.0 +ultralytics>=8.4.8 +uvicorn>=0.42.0 +vdms>=0.0.23 +wheel>=0.46.3 + +pillow>=12.1.1 diff --git a/.github/assets/video/requirements.GPU.in b/.github/assets/finetune/requirements.in similarity index 54% rename from .github/assets/video/requirements.GPU.in rename to .github/assets/finetune/requirements.in index b01978b..d3f7449 100644 --- a/.github/assets/video/requirements.GPU.in +++ b/.github/assets/finetune/requirements.in @@ -1,12 +1,17 @@ +cucim>=23.10.0 +cupy-cuda12x>=13.6.0 numpy>=1.26.4 +nvidia-cuda-runtime-cu12>=12.8.90 +nvidia-cudnn-cu12>=9.10.2.21 onnx>=1.21.0 onnxruntime-gpu>=1.23.2 onnxslim>=0.1.82 pip>=26.0.1 -protobuf>=5.29.6,<6 # For VDMS +requests>=2.33.0 tensorrt_cu12==10.14.1.48.post1 torch==2.10.0 torchvision==0.25.0 -vdms>=0.0.23 +ultralytics>=8.4.8 pillow>=12.1.1 + diff --git a/.github/assets/udf/requirements.in b/.github/assets/udf/requirements.in index 31dd418..2b43e26 100644 --- a/.github/assets/udf/requirements.in +++ b/.github/assets/udf/requirements.in @@ -1,8 +1,9 @@ -Flask>=3.1.2 +Flask>=3.1.3 inotify>=0.2.12 numpy>=2.2.6 opencv-python-headless>=4.13.0.90 pip>=26.0.1 protobuf>=5.29.6,<6 # For VDMS vdms>=0.0.23 +werkzeug>=3.1.6 wheel>=0.46.3 diff --git a/.github/assets/video/requirements.CPU.in b/.github/assets/video/requirements.CPU.in deleted file mode 100644 index 999c901..0000000 --- a/.github/assets/video/requirements.CPU.in +++ /dev/null @@ -1,13 +0,0 @@ ---index-url https://download.pytorch.org/whl/cpu ---extra-index-url https://pypi.org/simple - -torch==2.10.0+cpu -torchvision==0.25.0+cpu -nncf>=2.19.0 -numpy>=1.26.4 -pip>=26.0.1 -protobuf>=5.29.6,<6 # For VDMS -vdms>=0.0.23 - -pillow>=12.1.1 -pygments>=2.20.0 diff --git a/.github/assets/video/requirements.in b/.github/assets/video/requirements.in index 216a9b6..47826a4 100644 --- a/.github/assets/video/requirements.in +++ b/.github/assets/video/requirements.in @@ -1,19 +1,5 @@ ---index-url https://download.pytorch.org/whl/cpu ---extra-index-url https://pypi.org/simple - -torch==2.10.0+cpu -torchvision==0.25.0+cpu - inotify>=0.2.12 -opencv-python-headless>=4.11.0.86 -openvino-dev>=2024.6.0 pip>=26.0.1 -protobuf>=5.29.6,<6 # For VDMS -psutil>=7.2.1 +pyyaml>=6.0.3 requests>=2.33.0 tornado>=6.5.5 -ultralytics>=8.4.7 -vdms>=0.0.23 -wheel>=0.46.3 - -pillow>=12.1.1 diff --git a/.github/scripts/get_py_hashes.sh b/.github/scripts/get_py_hashes.sh index 93113df..22b5ade 100755 --- a/.github/scripts/get_py_hashes.sh +++ b/.github/scripts/get_py_hashes.sh @@ -6,6 +6,14 @@ SCRIPT_DIR=$(dirname "$(realpath "$0")") GH_DIR=$(dirname "${SCRIPT_DIR}") REPO_DIR=$(dirname "${GH_DIR}") +# FASTAPI +# uv pip compile ${GH_DIR}/assets/fastapi/requirements.CPU.in --no-header --no-annotate -o ${REPO_DIR}/fastapi/requirements.CPU.txt --generate-hashes --allow-unsafe --index-strategy unsafe-best-match +# pip-compile --no-header --no-annotate -o ${REPO_DIR}/fastapi/requirements.GPU.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/fastapi/requirements.GPU.in +pip-compile --no-header --no-annotate -o ${REPO_DIR}/fastapi/requirements.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/fastapi/requirements.in + +# FINETUNE +pip-compile --no-header --no-annotate -o ${REPO_DIR}/finetune/requirements.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/finetune/requirements.in + # FRONTEND pip-compile --no-header --no-annotate -o ${REPO_DIR}/frontend/requirements.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/frontend/requirements.in @@ -13,6 +21,4 @@ pip-compile --no-header --no-annotate -o ${REPO_DIR}/frontend/requirements.txt - pip-compile --no-header --no-annotate -o ${REPO_DIR}/udf/requirements.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/udf/requirements.in # VIDEO -uv pip compile ${GH_DIR}/assets/video/requirements.in --no-header --no-annotate -o ${REPO_DIR}/video/requirements.txt --generate-hashes --allow-unsafe --index-strategy unsafe-best-match -uv pip compile ${GH_DIR}/assets/video/requirements.CPU.in --no-header --no-annotate -o ${REPO_DIR}/video/requirements.CPU.txt --generate-hashes --allow-unsafe --index-strategy unsafe-best-match -pip-compile --no-header --no-annotate -o ${REPO_DIR}/video/requirements.GPU.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/video/requirements.GPU.in +pip-compile --no-header --no-annotate -o ${REPO_DIR}/video/requirements.txt --generate-hashes --allow-unsafe ${GH_DIR}/assets/video/requirements.in diff --git a/.gitignore b/.gitignore index 2975a56..0a718e2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,11 @@ **/_* **/DockerImageTars/ build/* -video/resources/models/intel -video/resources/models/ultralytics +fastapi/resources/models/intel +fastapi/resources/models/ultralytics +fastapi/tests/*_results* +fastapi/tests/*_test_imgs +fastapi/tests/*.mp4 +finetune/.env +finetune/app/*-Results +inputs/camera_config.yaml diff --git a/deployment/docker-swarm/build.sh b/deployment/docker-swarm/build.sh index ef6a80a..4d0249f 100755 --- a/deployment/docker-swarm/build.sh +++ b/deployment/docker-swarm/build.sh @@ -16,11 +16,20 @@ RESIZE_FLAG="${12}" MODEL_NAME="${13}" CUSTOM_MODEL_FLAG="${14}" OMIT_DETECTIONS_FLAG="${15}" +HOSTIP=$(ip route get 8.8.8.8 | awk '/ src /{split(substr($0,index($0," src ")),f);print f[2];exit}') echo "Generating templates with PLATFORM=${PLATFORM},NCURATIONS=${NCURATIONS},NSTREAMS=${NSTREAMS},INGESTION=${INGESTION},DEVICE=${DEVICE},IN_SOURCE=${IN_SOURCE},NCPU=${NCPU},HOSTIP=${HOSTIP},DEBUG=${DEBUG},DOCKER_TAR=${DOCKER_TAR},DOCKER_TAR_DIR=${DOCKER_TAR_DIR},RESIZE_FLAG=${RESIZE_FLAG},OMIT_DETECTIONS_FLAG=${OMIT_DETECTIONS_FLAG}" +BDIR=$(dirname $(dirname $DIR)) +# echo "docker build --build-arg no_proxy --network host --file=${BDIR}/video/Dockerfile.base -t lcc_base_video_image:latest ${DIR}/video $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') --build-arg DEVICE=${DEVICE}" +# if video or fastapi in DIR; then +# if [[ "$DIR" == *video* || "$DIR" == *fastapi* ]]; then +# echo "docker build --network host --file=${DIR}/../video/Dockerfile.base $@ -t lcc_base_video_image:latest ${DIR}/../video $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') --build-arg DEVICE=${DEVICE}" +# docker build --build-arg DEVICE=${DEVICE} --network host --file="${BDIR}/video/Dockerfile.base" -t "lcc_base_video_image:latest" "${BDIR}/video" $(env | cut -f1 -d= | grep -E '_(proxy|REPO|VER)$' | sed 's/^/--build-arg /') +# fi + if test -f "${DIR}/docker-compose.yml.m4"; then echo "Generating docker-compose.yml" - m4 -D${DEVICE} -Din_${IN_SOURCE} -DREGISTRY_PREFIX=$REGISTRY -DINGESTION="$INGESTION" -DDEVICE="$DEVICE" -DDEBUG="$DEBUG" -DNCURATIONS="${NCURATIONS}" -DNSTREAMS="${NSTREAMS}" -DIN_SOURCE="${IN_SOURCE}" -DNCPU="${NCPU}" -DRESIZE_FLAG="${RESIZE_FLAG}" -DMODEL_NAME="${MODEL_NAME}" -DCUSTOM_MODEL_FLAG="${CUSTOM_MODEL_FLAG}" -DOMIT_DETECTIONS_FLAG="${OMIT_DETECTIONS_FLAG}" -I "${DIR}" "${DIR}/docker-compose.yml.m4" > "${DIR}/docker-compose.yml" + m4 -D${DEVICE} -Din_${IN_SOURCE} -DREGISTRY_PREFIX=$REGISTRY -DINGESTION="$INGESTION" -DDEVICE="$DEVICE" -DDEBUG="$DEBUG" -DNCURATIONS="${NCURATIONS}" -DHOSTIP="${HOSTIP}" -DNSTREAMS="${NSTREAMS}" -DIN_SOURCE="${IN_SOURCE}" -DNCPU="${NCPU}" -DRESIZE_FLAG="${RESIZE_FLAG}" -DMODEL_NAME="${MODEL_NAME}" -DCUSTOM_MODEL_FLAG="${CUSTOM_MODEL_FLAG}" -DOMIT_DETECTIONS_FLAG="${OMIT_DETECTIONS_FLAG}" -I "${DIR}" "${DIR}/docker-compose.yml.m4" > "${DIR}/docker-compose.yml" fi diff --git a/deployment/docker-swarm/frontend.m4 b/deployment/docker-swarm/frontend.m4 index 46d2c47..c37be11 100644 --- a/deployment/docker-swarm/frontend.m4 +++ b/deployment/docker-swarm/frontend.m4 @@ -12,6 +12,7 @@ no_proxy: "vdms-service,video-service,${no_proxy}" NO_PROXY: "vdms-service,video-service,${NO_PROXY}" `DEBUG': "defn(`DEBUG')" + BACKEND_URL: "http://fastapi-service:8000" secrets: - source: self_crt target: /var/run/secrets/self.crt @@ -35,3 +36,4 @@ depends_on: - vdms-service - video-service + - fastapi-service diff --git a/deployment/docker-swarm/udf.m4 b/deployment/docker-swarm/udf.m4 index 674609e..d31f471 100644 --- a/deployment/docker-swarm/udf.m4 +++ b/deployment/docker-swarm/udf.m4 @@ -17,8 +17,8 @@ HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "video-service,vdms-service,${no_proxy}" - NO_PROXY: "video-service,vdms-service,${NO_PROXY}" + no_proxy: "fastapi-service,video-service,vdms-service,${no_proxy}" + NO_PROXY: "fastapi-service,video-service,vdms-service,${NO_PROXY}" volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www:ro @@ -48,8 +48,8 @@ HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "video-service,vdms-service,${no_proxy}" - NO_PROXY: "video-service,vdms-service,${NO_PROXY}" + no_proxy: "fastapi-service,video-service,vdms-service,${no_proxy}" + NO_PROXY: "fastapi-service,video-service,vdms-service,${NO_PROXY}" volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www:ro diff --git a/deployment/docker-swarm/video.m4 b/deployment/docker-swarm/video.m4 index c0e63a0..1596d2e 100644 --- a/deployment/docker-swarm/video.m4 +++ b/deployment/docker-swarm/video.m4 @@ -1,23 +1,20 @@ -define(`PROFILE_DEFAULT', `depends_on: - - udf-service - - vdms-service') -define(`PROFILE_GPU', `depends_on: - - udf-service - - vdms-service - runtime: nvidia +define(`PROFILE_DEFAULT', `') +define(`PROFILE_GPU', `runtime: nvidia deploy: resources: reservations: devices: - driver: nvidia - capabilities: [gpu]') + capabilities: [`gpu,video,compute,utility']') video-service: image: defn(`REGISTRY_PREFIX')lcc_video:stream environment: + YOLO_CONFIG_DIR: "/tmp" RETENTION_MINS: "60" CLEANUP_INTERVAL: "10m" DBHOST: "vdms-service" UDF_HOST: "udf-service" + BACKEND_URL: "http://fastapi-service:8000" `MODEL_NAME': "defn(`MODEL_NAME')" `CUSTOM_MODEL_FLAG': "defn(`CUSTOM_MODEL_FLAG')" `RESIZE_FLAG': "defn(`RESIZE_FLAG')" @@ -32,14 +29,79 @@ define(`PROFILE_GPU', `depends_on: HTTP_PROXY: "${HTTP_PROXY}" https_proxy: "${https_proxy}" HTTPS_PROXY: "${HTTPS_PROXY}" - no_proxy: "vdms-service,udf-service,${no_proxy}" - NO_PROXY: "vdms-service,udf-service,${NO_PROXY}" + no_proxy: "fastapi-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" + NO_PROXY: "fastapi-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + secrets: + - source: self_crt + target: /var/run/secrets/self.crt + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0444 + - source: self_key + target: /var/run/secrets/self.key + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0440 volumes: - /etc/localtime:/etc/localtime:ro - app-content:/var/www + - ../../inputs:/watch_dir:ro - ../../inputs/camera_config.yaml:/home/camera_config.yaml:ro + networks: + - appnet + restart: always + depends_on: + - fastapi-service + + fastapi-service: + shm_size: '2gb' # Give it plenty of space for video frames + image: defn(`REGISTRY_PREFIX')lcc_fastapi:stream + environment: + YOLO_CONFIG_DIR: "/tmp" + DBHOST: "vdms-service" + UDF_HOST: "udf-service" + `MODEL_NAME': "defn(`MODEL_NAME')" + `CUSTOM_MODEL_FLAG': "defn(`CUSTOM_MODEL_FLAG')" + `RESIZE_FLAG': "defn(`RESIZE_FLAG')" + `OMIT_DETECTIONS_FLAG': "defn(`OMIT_DETECTIONS_FLAG')" + CPU_BATCH_SIZE: 1 + GPU_BATCH_SIZE: 1 + `DEBUG': "defn(`DEBUG')" + `DEVICE': "defn(`DEVICE')" + `INGESTION': "defn(`INGESTION')" + WATCH_DIR: "/watch_dir" + http_proxy: "${http_proxy}" + HTTP_PROXY: "${HTTP_PROXY}" + https_proxy: "${https_proxy}" + HTTPS_PROXY: "${HTTPS_PROXY}" + no_proxy: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${no_proxy}" + NO_PROXY: "video-service,localhost,127.0.0.1,vdms-service,udf-service,${NO_PROXY}" + NVIDIA_DRIVER_CAPABILITIES: "all" + ports: + - target: 80 + published: 30077 + protocol: tcp + mode: host + secrets: + - source: self_crt + target: /var/run/secrets/self.crt + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0444 + - source: self_key + target: /var/run/secrets/self.key + uid: ${USER_ID} + gid: ${GROUP_ID} + mode: 0440 + volumes: + - /etc/localtime:/etc/localtime:ro + - app-content:/var/www:rw - ../../inputs:/watch_dir:ro + - ../../fastapi/resources:/home/resources:rw networks: - appnet restart: always - ifdef(`GPU', PROFILE_GPU, PROFILE_DEFAULT) + depends_on: + - udf-service + - vdms-service + ifelse(ifdef(`GPU', `yes'), `yes', PROFILE_GPU, PROFILE_DEFAULT) diff --git a/doc/pipeline.md b/doc/pipeline.md index 3e634ce..4236c11 100644 --- a/doc/pipeline.md +++ b/doc/pipeline.md @@ -41,9 +41,9 @@ The current filtering pipeline includes the following steps: The current implementation of the Smart Filtering pipeline is optimized for the test use-case, drone detection. Drones are typically small in the video frames so if your use-case of interest has different objects, it may be beneficial to test the pipeline results on an existing test video for your use-case. -For this case, we provide [`test_detections.py`](./fastapi/tests/test_detections.py) which annotates ROIs identified by the Smart Filtering pipeline onto each frame of the video for visual inspection. +For this case, we provide [`test_detections.py`](/fastapi/tests/test_detections.py) which annotates ROIs identified by the Smart Filtering pipeline onto each frame of the video for visual inspection. -For testing purposes, we will manually deploy the fastapi Dockerfile as it contains the same setup used in the application AND start the test script. +For testing purposes, you can use VSCode DevContainer (easiest method) or manually deploy the fastapi dockerfile. Using VSCode is straight forward, so here, we will manually deploy the fastapi Dockerfile as it contains the same setup used in the application AND start the test script. Here we will build the container, if not available: ```bash REPO_DIR=`pwd` @@ -61,21 +61,8 @@ docker build --network host \ The script for testing the pipeline is in `fastapi/tests/` and your test video is expected to be in `inputs/` (i.e. `anduril_swarm_8K.mp4`) which is mounted to `/watch_dir` within container. -This script can be run as a PyTest or from command line using following arguments: -| Argument | Default | Description | -| -------- | ------- | ----------- | -| -v VIDEO,
--video VIDEO | anduril_swarm_8K.mp4 | Video filename (located in /inputs) | -| --no-custom | - | Enable if using Ultralytics YOLO model | -| -m MODEL_NAME,
--model MODEL_NAME | drone_detection | Name of model. Required if `--no-custom` is enabled. | -| --type {object,motion} | None | Filter by detection type (object or motion) | -| --device {cpu,gpu} | None | Filter by device (cpu or gpu) | -| --sf | None | Filter by Smart Filtering | -| --debug | False | Enable debug message and save intermediate images for Smart Filtering tests | -| -n DEBUG_FRAME_LIMIT | 100 | Number of frames used for debugging | -
- -Please see [Detection](#detections-using-sf-and-fine-tuned-yolo-model) section for expected location of models. +Please see [Detection](#detections-using-sf-and-fine-tuned-yolo-model) section for expected location of models and see [Test Pipeline: Smart Filtering](#test-pipeline) for more information on testing the filtering pipeline. >***NOTE:*** The default video is an open-source video of a swarm of drones. This video can be downloaded from [yolov11-UAV-finetune](https://github.com/droneforge/yolov11-UAV-finetune/blob/main/anduril_swarm.mp4). @@ -86,7 +73,7 @@ Since this work focuses on HR videos, we converted the test video to 8K using th To deploy this test, run the following command but modify the name of the test_video. -The results from the test will be saved in `fastapi/tests/test_detections_results_drone_detection/`. +The results from the test will be saved in `fastapi/tests/test_detections_results/drone_detection/`. ```bash docker run --rm --ipc=host \ --gpus all --env NVIDIA_DRIVER_CAPABILITIES=all \ @@ -100,7 +87,7 @@ docker run --rm --ipc=host \ -v ${REPO_DIR}/fastapi/resources:/home/resources \ -v ${REPO_DIR}/fastapi/tests:/home/tests \ -v ${REPO_DIR}/fastapi/tests/nginx.conf:/etc/nginx/nginx.conf \ -lcc_fastapi:stream /bin/bash -c "python /home/tests/test_detections.py -v .mp4 --type motion" +lcc_fastapi:stream /bin/bash -c "python /home/tests/test_detections.py --source .mp4 --type motion" ``` @@ -131,7 +118,7 @@ For testing other components, please see [Testing Pipeline Components](#testing- #### VSCode If using VSCode, the provided `.vscode` directory is already mounted in the above command which includes `launch.json` for debugging. -You can use the docker extension to connect to the `lcc_fastapi:stream` container by clicking `Attach Visual Studio Code`. +You can use the provided DevContainer or use the docker extension to connect to the `lcc_fastapi:stream` container by clicking `Attach Visual Studio Code`. Once connected, it will install VSCode in the container. If you receive an error regarding permissions during installation, you must modify the remote user. To allow permission for the installation, open the `Command Pallette` and select `Dev Containers: Open Attached Container Configuration File`. @@ -179,7 +166,7 @@ For the smart filtering pipeline, the following are potential areas for optimiza
Once satisfied with annotated results, you can proceed with running the full pipeline. -***NOTE:*** If you modified any methods, be sure to update the predefined code to use your changes in the pipeline. +***NOTE:*** If you modified any methods within the test script, be sure to update the predefined code to use your changes in the pipeline.
@@ -200,48 +187,109 @@ Please note the model labels are retrieved from the model directly, so the model ## Testing Pipeline Components We provided multiple test scripts to test different components of the pipeline. -Each of the tests can be run as specified in the [Smart Filtering Pipeline: Testing](#testing) section using the same video `anduril_swarm_8K.mp4` as `INPUT_STREAM`. +Each of the tests can be run as specified in the [Smart Filtering Pipeline: Testing](#testing) section using the same video `anduril_swarm_8K.mp4` as `SOURCE`. Here we provide details on each available test. -| Test File | Description | -| --------- | ----------- | -| test_readers.py | Test the readers for both CPU and GPU on provided RTSP URL or video file | -| test_model.py | Test the model for both CPU and GPU on provided RTSP URL or video file | - - -### Test Readers -This test reads the `INPUT_STREAM` from `./inputs`, `/watch_dir`, or RTSP server, and uses the readers to process frames for length of video or 2 minutes (whichever comes first). -```bash -python test_readers.py --source "${INPUT_STREAM}" --duration 2 -``` +| Component | Test File | Description | +| --------- | --------- | ----------- | +| Detection Model | [test_model.py](/fastapi/tests/test_model.py) | Test the model for both CPU and GPU on provided RTSP URL or video file | +| Smart Filtering | [test_detections.py](/fastapi/tests/test_detections.py) | This test independently evaluates the detection pipeline (with and without Smart Filtering) only and does not include video clip generation or sending metadata to database for querying. | +| Stream Reader | [test_pipeline.py](/fastapi/tests/test_pipeline.py) | Scenario 1 tests the behavior of Readers when provided an invalid RTSP url.
Scenario 2 reads the RTSP url or video file for a specified duration or until it ends. | +| Video Clip Generation | [test_pipeline.py](/fastapi/tests/test_pipeline.py) | Scenario 3 mimics the clip generation within the pipeline. | +| Smart Filtering | [test_pipeline.py](/fastapi/tests/test_pipeline.py) | Scenario 4 tests the entire pipeline and saves output video of results. | ### Test Models -This test reads the `INPUT_STREAM` from `./inputs`, `/watch_dir`, or RTSP server, and creates a video with overlaid drone detection results for both GPU and CPU model. +This test reads the `SOURCE` from `./inputs` (the local directory or `/watch_dir` if in container), or RTSP server, and creates a video with overlaid detection results for both GPU and CPU model. ```bash -python test_models.py -v "${INPUT_STREAM}" +python test_models.py -s "${SOURCE}" ``` You can also use the Yolo11n model by using: ```bash -python test_models.py -v "${INPUT_STREAM}" -m yolo11n --no-custom +python test_models.py -s "${SOURCE}" -m yolo11n --no-custom ``` +The arguments available for this test are as follows: +| Argument | Default | Description | +| -------- | ------- | ----------- | +| -s SOURCE,
--source SOURCE | anduril_swarm_8K.mp4 | Video filename (located in /inputs) or RTSP target stream endpoint | +| --no-custom | - | Enable if using Ultralytics YOLO model | +| -m MODEL_NAME,
--model MODEL_NAME | drone_detection | Name of model. Required if `--no-custom` is enabled. | +| --type {object,motion} | None | Filter by detection type (object or motion) | +| --device {cpu,gpu} | None | Filter by device (cpu or gpu) | +
+ -### Test Video Clipping -This test reads the `INPUT_STREAM` from `./inputs`, or `/watch_dir` and creates 10 second video clips. +### Test Detections +This test reads the `SOURCE` from `./inputs`, and evaluates the detection pipeline only. + +To run the detection test, use the following: ```bash -python test_clipping.py -v "${INPUT_STREAM}" +python test_detections.py --source "${SOURCE}" ``` +You also have control on which scenario, model, etc. to use during testinh. +The arguments available are as follows: +| Argument | Default | Description | +| -------- | ------- | ----------- | +| -s SOURCE,
--source SOURCE | anduril_swarm_8K.mp4 | Video filename (located in ./inputs) | +| --no-custom | - | Enable if using Ultralytics YOLO model | +| -m MODEL_NAME,
--model MODEL_NAME | drone_detection | Name of model. Required if `--no-custom` is enabled. | +| --type {object,motion} | None | Filter by detection type (object or motion). "motion" shows the ROI while "object" shows detection results. | +| --device {cpu,gpu,all} | all | Filter by target hardware. | +| --sf | None | Filter test by Smart Filtering pipeline | +| --debug | False | Enable debug message | +| -n DEBUG_FRAME_LIMIT | 100 | Number of frames used for debugging | +
+ -### Test Smart Filtering -This is the test mentioned above in [Smart FIltering Pipeline: Testing](#testing). -The test reads the `INPUT_STREAM` from `./inputs`, or `/watch_dir` and applies the Smart Filtering pipeline. +### Test Pipeline +This test reads the `SOURCE` from `./inputs` or RTSP server, and uses the stream readers to perform multiple scenarios. + +To run the full pipeline test, use the following: ```bash -python test_detections.py -v "${INPUT_STREAM}" +python test_pipeline.py --source "${SOURCE}" ``` +You also have control on which scenario, model, etc. to use during testinh. +The arguments available are as follows: +| Argument | Default | Description | +| -------- | ------- | ----------- | +| -s SOURCE,
--source SOURCE | anduril_swarm_8K.mp4 | Video filename (located in ./inputs) or RTSP target stream endpoint | +| -d DURATION,
--duration DURATION | 2 | Test duration in minutes | +| --scenario | - | Specify one or more scenarios. Otherwise all scenarios are ran. Available scenarios: [1, 2, 3, 4] | +| --no-custom | - | Enable if using Ultralytics YOLO model | +| -m MODEL_NAME,
--model MODEL_NAME | drone_detection | Name of model. Required if `--no-custom` is enabled. | +| --type {object,motion} | None | Filter by detection type (object or motion). "motion" shows the ROI while "object" shows detection results. | +| --device {cpu,gpu,all} | all | Filter by target hardware. | +| --sf | None | Filter test by Smart Filtering pipeline | +| --debug | False | Enable debug message | + +
+ +#### Scenario 1: Invalid RTSP URL +Scenario 1 is a simple test to verify the pipeline ends if an invalid input is provided. For this test, an invalie RTSP url is provided, there are 5 retry attempts, and after a failed connection, the test ends. By default, the test iterates over available devices ("cpu" and "gpu"). + + +#### Scenario 2: Stability & Throughput +Scenario 2 evaluates the stability and throughput of the readers by continuously reading from the reader for the duration of the source or for a specified duration (whichever comes first). By default, the test iterates over available devices ("cpu" and "gpu"). + + +#### Scenario 3: Video Clip Generation +Scenario 3 evaluates the video clip generation process in the pipeline. +In the pipeline, we segment the input video into 10 sec clips for easy retrieval and playback associated with results in the query UI. By default, the test iterates over available devices ("cpu" and "gpu"). +The resulting video clips generated from this test are located in `test_pipeline_results/scenario3_*`. + + +#### Scenario 4: Pipeline (Without Sending Metadata) +Scenario 4 mimics the entire pipeline excluding sending the generated metadata to VDMS. By default, the test iterates over available devices ("cpu" and "gpu"), detection type ("object" or "motion"), and with and without SF. +The resulting video clips and the detection results are located in `test_pipeline_results/scenario4_*`. + +In the case of testing the Smart Filtering pipeline, to display the ROIs, you can use the following, which will test for both gpu and cpu. +```bash +python test_pipeline.py --scenario 4 --source "${SOURCE}" --type motion +``` ## Pipeline Deployment diff --git a/fastapi/.vscode/launch.json b/fastapi/.vscode/launch.json new file mode 100644 index 0000000..5ea2550 --- /dev/null +++ b/fastapi/.vscode/launch.json @@ -0,0 +1,79 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Pipeline", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/tests/test_pipeline.py", + "console": "integratedTerminal", + "subProcess": false, + "justMyCode": false, + "args": [ + "--duration", "2", + + // AVAILABLE SCENARIO PARAMS + // Use --scenario to specify tests by number (1 - 4) + // 1 - 3: --device (cpu or gpu) + // 4: --device, --sf, --type (object or motion) + + // VIDEOS EXPECTED TO BE rstp url OR IN inputs/ or /watch_dir + // "--source", "rtsp://:/", + // "--source", ".mp4", + ] + }, + + { + "name": "Python Debugger: Model", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/tests/test_model.py", + "cwd": "${workspaceFolder}/tests", + "console": "integratedTerminal", + "justMyCode": false, + "subProcess": false, + "args": [ + // VIDEOS EXPECTED TO BE rstp url OR IN inputs/ or /watch_dir + // "--source", ".mp4", + ] + }, + { + "name": "Python Debugger: Smart filtering", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/tests/test_pipeline.py", + "console": "integratedTerminal", + "subProcess": false, + "justMyCode": false, + "args": [ + // SF SCENARIO PARAMS + // 4: --device, --sf, --type (object or motion) + "--scenario", "4", + "--sf", + "--type", "motion", + + // VIDEOS EXPECTED TO BE rstp url OR IN inputs/ or /watch_dir + // "--source", "rtsp://:/", + // "--source", ".mp4", + ] + }, + { + "name": "Python Debugger: test_detections", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/tests/test_detections.py", + "cwd": "${workspaceFolder}/tests", + "console": "integratedTerminal", + "justMyCode": false, + "subProcess": false, + "args": [ + "--type", "object", + // "--source", ".mp4", + // "-m", "yolo11n", "--no-custom", + ] + } + ] +} diff --git a/fastapi/CMakeLists.txt b/fastapi/CMakeLists.txt new file mode 100644 index 0000000..55d5d41 --- /dev/null +++ b/fastapi/CMakeLists.txt @@ -0,0 +1,2 @@ +set(service "lcc_fastapi") +include("${CMAKE_SOURCE_DIR}/script/service.cmake") diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile new file mode 100644 index 0000000..a771678 --- /dev/null +++ b/fastapi/Dockerfile @@ -0,0 +1,215 @@ + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a AS build + +ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" +ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" + +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# hadolint ignore=DL3008 +RUN apt-get update && \ + apt-get install -y --only-upgrade --no-install-recommends libc-bin libc6 && \ + apt-get install -y -q --no-install-recommends python3-setuptools \ + python3-dev python3-pip python3-venv \ + curl libgl1-mesa-glx \ + # Update and install necessary build tools and dependencies for OpenCV + build-essential \ + cmake \ + git \ + wget \ + unzip \ + yasm \ + pkg-config \ + libgl1 \ + libglib2.0-0 \ + libgtk2.0-dev \ + libtbb-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + libxvidcore-dev \ + libxine2-dev \ + libv4l-dev \ + libdc1394-dev \ + libatlas-base-dev \ + gfortran && \ + # Install necessary CUDA packages + curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ + apt-get update && \ + apt-get install -y -q --no-install-recommends \ + cuda-toolkit-12-4 \ + libcudnn9-dev-cuda-12 \ + libnpp-dev-12-4 \ + && \ + rm -rf /var/lib/apt/lists/* && apt-get clean + +RUN python3 -m venv ${VIRTUAL_ENV} && \ + ${VIRTUAL_ENV}/bin/pip install --no-cache-dir \ + "pip==26.0.1" \ + "torch==2.10.0" \ + "torchvision==0.25.0" \ + "numpy==1.26.0" + +# OPENCV W/ CUDA SUPPORT +ENV PYTHON_VERSION=3.10 +ENV OPENCV_VERSION="4.11.0" +ENV DEPENDENCY_DIR=/tmp/build_opencv +ENV NUMPY_PATH="${VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages/numpy/core/include" +ENV SYS_PATH="/usr/include/python${PYTHON_VERSION}" + +# Clone OpenCV and OpenCV Contrib repositories +WORKDIR ${DEPENDENCY_DIR} +RUN git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv.git ${DEPENDENCY_DIR}/opencv && \ + git clone --branch ${OPENCV_VERSION} --depth 1 https://github.com/opencv/opencv_contrib.git ${DEPENDENCY_DIR}/opencv_contrib + +# Create build directory and run CMake +WORKDIR ${DEPENDENCY_DIR}/opencv/build +RUN ln -s /usr/include/x86_64-linux-gnu/cudnn*.h /usr/local/cuda/include/ && \ + ln -s /usr/lib/x86_64-linux-gnu/libcudnn*.so* /usr/local/cuda/lib64/ +RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ + -D CMAKE_INSTALL_PREFIX="${VIRTUAL_ENV}" \ + -D OPENCV_EXTRA_MODULES_PATH="${DEPENDENCY_DIR}/opencv_contrib/modules" \ + -D WITH_CUDA=ON \ + -D WITH_CUDNN=ON \ + -D WITH_CUBLAS=ON \ + -D WITH_FFMPEG=ON \ + -D WITH_TBB=ON \ + -D WITH_V4L=ON \ + -D OPENCV_DNN_CUDA=ON \ + -D CUDA_ARCH_BIN=70,75,80,86,89,90 \ + -D CUDA_FAST_MATH=ON \ + -D CUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda \ + -D CUDNN_INCLUDE_DIR=/usr/include/x86_64-linux-gnu \ + -D CUDNN_LIBRARY=/usr/lib/x86_64-linux-gnu/libcudnn.so \ + -D BUILD_opencv_cudaimgproc=ON \ + -D BUILD_opencv_cudaarithm=ON \ + -D BUILD_opencv_cudafilters=ON \ + -D BUILD_opencv_cudacodec=ON \ + -D WITH_NVCUVID=ON \ + -D WITH_NVCUVENC=ON \ + -D WITH_VAAPI=ON \ + -D WITH_FFMPEG=ON \ + -D BUILD_opencv_python3=ON \ + -D PYTHON3_EXECUTABLE="${VIRTUAL_ENV}/bin/python3" \ + -D PYTHON3_NUMPY_INCLUDE_DIRS="${VIRTUAL_ENV}/lib/python3.10/site-packages/numpy/core/include" \ + -D OPENCV_SKIP_PYTHON_LOADER=ON \ + -D BUILD_EXAMPLES=OFF -D BUILD_TESTS=OFF -D BUILD_PERF_TESTS=OFF .. \ + && make -j$(nproc) && make install && ldconfig + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN mkdir -p /tmp/cv2_deps && \ + PY_CV2_SO=$(find "${VIRTUAL_ENV}" -name "cv2*.so" | head -n 1) && \ + ldd "${PY_CV2_SO}" | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /tmp/cv2_deps/ && \ + cp -v /usr/local/cuda/lib64/libnpp*.so* /tmp/cv2_deps/ && \ + cp -v /usr/local/cuda/lib64/libcudart.so* /tmp/cv2_deps/ && \ + cp -P /usr/local/cuda/lib64/libnvrtc.so* /tmp/cv2_deps/ && \ + cp -v /usr/lib/x86_64-linux-gnu/libnvidia-encode.so* /tmp/cv2_deps/ || true + + +# Clean up +RUN rm -rf ${DEPENDENCY_DIR} + + +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a + +ARG DEBIAN_FRONTEND=noninteractive +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" + +# Install Runtime Libraries, FFmpeg/VA-API headers, and cuDNN +# Includes libva and ffmpeg libraries required for HEVC decoding +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + python3 \ + libgl1 \ + libglib2.0-0 \ + libtbb12 \ + # Critical for Hardware Decoding (HEVC) + libva2 \ + libva-drm2 \ + libva-x11-2 \ + libavcodec58 \ + libavformat58 \ + libswscale5 \ + libv4l-0 && \ + # Install necessary CUDA packages + curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb -o /tmp/cuda-keyring.deb && \ + dpkg -i /tmp/cuda-keyring.deb && rm /tmp/cuda-keyring.deb && \ + apt-get update && \ + apt-get install -y -q --no-install-recommends libcudnn9-cuda-12 && \ + rm -rf /var/lib/apt/lists/* + +# Copy the entire pre-compiled virtual environment from the build stage +COPY --from=build ${VIRTUAL_ENV} ${VIRTUAL_ENV} +# Copy NPP (NVIDIA Performance Primitives) libs - Required for OpenCV CUDA +# COPY --from=build /usr/local/cuda/lib64/libnpp*.so* /usr/local/cuda/lib64/ +# Copy additional required CUDA math/parallel libs +RUN mkdir -p /usr/local/cuda/lib64 +COPY --from=build /tmp/cv2_deps/* /usr/local/cuda/lib64/ +ENV LD_LIBRARY_PATH="/usr/local/cuda/lib64:${VIRTUAL_ENV}/lib:${LD_LIBRARY_PATH}" +RUN ldconfig + +ARG DEVICE="CPU" +ENV DEVICE="${DEVICE}" + +ARG DEBUG="0" +ENV DEBUG="${DEBUG}" + +# Set the working directory in the container +WORKDIR /home +COPY requirements.txt /home/ +RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt && \ + pip3 uninstall -y opencv-python opencv-contrib-python opencv-python-headless + +COPY *.py /home/ +COPY *.sh /home/ +COPY include /home/include +COPY templates /home/templates +COPY nginx.conf /etc/nginx/nginx.conf + +COPY entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Set the Entrypoint +ENTRYPOINT ["entrypoint.sh"] + +# Your simplified CMD +CMD ["/bin/bash", "-c", "/home/manage.sh"] + +EXPOSE 80 8000 8080 + +#### +ARG USER +ARG GROUP +ARG UID +ARG GID +## must use ; here to ignore user exist status code +## --no-log-init option can prevent the creation of large, sparse files in /var/log/lastlog and /var/log/faillog +# RUN if [ ${GID} -gt 0 ]; then groupadd -f -g ${GID} ${GROUP}; fi; \ +# if [ ${UID} -gt 0 ]; then useradd --no-log-init -d /home/${USER} -g ${GID} -u ${UID} ${USER}; fi; \ +# touch /var/run/nginx.pid && \ +# mkdir -p /var/log/nginx /var/lib/nginx /var/www/cache /var/www/gen /var/www/mp4 /var/www/logwatch && \ +# chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home +RUN if [ ${UID} -gt 0 ]; then \ + # Manually create group and user entries to bypass system limits + echo "${GROUP}:x:${GID}:" >> /etc/group && \ + echo "${USER}:x:${UID}:${GID}::/home/${USER}:/bin/bash" >> /etc/passwd; \ + # mkdir -p /home/${USER} && \ + # chown -R ${UID}:${GID} /home/${USER}; \ + fi && \ + touch /var/run/nginx.pid && \ + mkdir -p /var/log/nginx /var/lib/nginx /var/www/cache /var/www/gen /var/www/mp4 /var/www/logwatch && \ + chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home +VOLUME ["/var/www"] +USER ${UID} +#### diff --git a/fastapi/build.sh b/fastapi/build.sh new file mode 100755 index 0000000..aadc240 --- /dev/null +++ b/fastapi/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +IMAGE="lcc_fastapi" +DIR=$(dirname $(readlink -f "$0")) + +. "$DIR/../script/build.sh" diff --git a/fastapi/entrypoint.sh b/fastapi/entrypoint.sh new file mode 100644 index 0000000..850f4a3 --- /dev/null +++ b/fastapi/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +# Run the download script using container env vars +echo "Starting model download..." +python /home/include/models.py -o /var/www/cache/model_classes.json + +# Execute the CMD (which will be /home/manage.sh) +exec "$@" \ No newline at end of file diff --git a/fastapi/include/default_configs.py b/fastapi/include/default_configs.py new file mode 100644 index 0000000..9046094 --- /dev/null +++ b/fastapi/include/default_configs.py @@ -0,0 +1,65 @@ +# GENERAL +CODE_DIR_DEFAULT = "/home" +CUSTOM_MODEL_FLAG_DEFAULT = False +DEBUG_DEFAULT = "0" +DEVICE_DEFAULT = "CPU" +MAX_WORKERS = 4 # 6 +OMIT_DETECTIONS_FLAG_DEFAULT = False +SHARED_OUTPUT_DEFAULT = "/var/www/mp4" +TEST_MODE_DEFAULT = False +TMP_LOCATION_DEFAULT = "/var/www/cache" +SMART_FILTERING_ENABLED = True + +# VIDEO WRITER +CLIP_DURATION_DEFAULT = 10 +TARGET_FPS = 15 + +# VDMS +DBHOST_DEFAULT = "vdms-service" +DBPORT_DEFAULT = 55555 +ENABLE_QUERYING_DEFAULT = True # False, True +INGESTION_DEFAULT = "object" # object,face +UDF_HOST_DEFAULT = "udf-service" +UDF_PORT_DEFAULT = 5011 + +# MODEL +DETECTION_THRESHOLD_DEFAULT = 0.25 +DYNAMIC_FLAG_DEFAULT = True +HALF_FLAG_DEFAULT = True +MAX_DETECTIONS = 100 +MODEL_H = 640 +MODEL_W = 640 +MODEL_MAX_BATCH_SIZE = 64 +MODEL_NAME_DEFAULT = "yolo11n" +MODEL_PRECISION = "FP16" + +# SMART FILTERING (RESIZE, BKGD SUB, THRESHOLD, DILATE) +# Enable if resolution > 2K 1920x1080 +SMART_FILTERING_PIXEL_CONSTRAINT = 1920 * 1080 +BKGD_SUB_INCLUDE_HISTORY = False # True, False +BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE = 15 # 21 #15 +BKGD_SUB_INCLUDE_HISTORY_METHOD = "or" # "and", or +BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE = 3 +BKGD_SUB_MOG2_DETECTSHADOWS = False +BKGD_SUB_MOG2_HISTORY = int(2 * TARGET_FPS) +BKGD_SUB_MOG2_LR = 1 / BKGD_SUB_MOG2_HISTORY # 0.002 # 1 / BKGD_SUB_MOG2_HISTORY +BKGD_SUB_MOG2_VARTHRESHOLD = 10 +DILATE_KERNEL_SIZE = 5 # (3, 3) +RESIZE_FLAG_DEFAULT = False +ROI_BB_FULL_RES_PADDING = int(0.02 * MODEL_W) # 10, ~13 +ROI_CONTAINMENT_THRESH = 0.95 +ROI_DISTANCE_THRESH_RATIO = 0.05 +ROI_MAX_RELATIVE_SIZE_RATIO = 1 # 1 # 0.8 +ROI_MERGE_SIZE_LIMIT = MODEL_W * 1.25 +ROI_MIN_AREA_RATIO = 0.01 +ROI_RETURN_BYTES = True +THRESHOLD_MAX_VALUE = 255 +THRESHOLD_VALUE = 127 + +# VISUALIZATION +# Bounding boxes returned from pipeline +# object (includes yolo), motion (includes bbs no yolo) +DETECTION_TYPE_DEFAULT = "object" # object, motion +DISPLAY_FRAME_QUALITY = 60 +DISPLAY_FRAME_SIZE = (640, 360) # (640, 360), (960, 540) +THICKNESS = 2 diff --git a/fastapi/include/handlers.py b/fastapi/include/handlers.py new file mode 100644 index 0000000..9188bcc --- /dev/null +++ b/fastapi/include/handlers.py @@ -0,0 +1,4714 @@ +import asyncio +import gc +import json +import logging +import multiprocessing as mp +import os +import queue +import shutil +import subprocess +import sys +import threading +import time +import traceback +from collections import deque +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from datetime import datetime +from multiprocessing import shared_memory +from pathlib import Path + +import cupy +import cupyx.scipy +import cupyx.scipy.ndimage +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from ultralytics import YOLO +from ultralytics.utils.checks import check_imgsz + +from fastapi import FastAPI + +sys.path.insert(1, str(Path(__file__).parent.parent)) +from include.default_configs import ENABLE_QUERYING_DEFAULT +from include.models import get_model +from include.utils import ( + BOUNDS_KERNEL, + DETECTION_ACCEL_KERNEL, + VDMSPool, + find_contours_gpu_equivalent, + merge_boxes_cpu, + merge_boxes_gpu, +) + +# ----- SETUP LOGGING ----- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) + +# Suppress low-delay reference block warnings from OpenCV/PyAV/FFmpeg +os.environ["OPENCV_FFMPEG_LOGLEVEL"] = "-8" +os.environ["OPENCV_LOG_LEVEL"] = "OFF" +logging.getLogger("libav").setLevel(logging.CRITICAL) +logging.getLogger("libav.hevc").setLevel(logging.CRITICAL) + +main_app_logger = logging.getLogger(__name__) +STREAM_ARG = False + + +def log_to_logger(message, level="info"): + try: + if level.lower() == "debug": + main_app_logger.debug(message) + elif level.lower() == "warning": + main_app_logger.warning(message) + else: + main_app_logger.info(message) + except Exception: + pass + + +# ----- PIPELINE CONFIGURATION ----- +os.environ["PYTORCH_ALLOC_CONF"] = "expandable_segments:True" +# Force OpenCV to use a single thread for its operations. +# This prevents internal OpenCV threads from "racing" against AI logic. +# cv2.setNumThreads(1) + +# Force OpenCV to run sequentially to prevent context-switching overhead +# cv2.setNumThreads(0) +cv2.setNumThreads(os.cpu_count() or 4) + + +from include.utils import ( + PipelineConfig, + PipelineMapping, + draw_label, + get_detection_color, + get_display_frame_in_bytes, + metadata2vdms_with_retry, + tensor2opencv, +) + +BASE_PIPELINE_CONFIG = PipelineConfig( + SHARED_MODEL=os.getenv("SHARED_MODEL", False), + ENABLE_QUERYING=os.getenv("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT), +) + + +# Optimizes RTSP ingestion with hardware acceleration and low-delay flags +os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = ( + "rtsp_transport;tcp|hwaccel;cuda|threads;auto|low_delay;1|probesize;5000000" + # "rtsp_transport;tcp|hwaccel;cuda|threads;1|probesize;32|analyzeduration;0" + # "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" + # "rtsp_transport;tcp|hwaccel;cuda|threads;2|probesize;32|analyzeduration;0" + # "rtsp_transport;tcp|hwaccel;cuda|threads;4|probesize;5000000|analyzeduration;5000000" + # "rtsp_transport;udp|hwaccel;cuda|threads;8|stimeout;5000000|listen_timeout;5000" +) + + +# ----- GLOBAL VARIABLES ----- +# ENABLE_QUERYING = os.getenv("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT) +# if BASE_PIPELINE_CONFIG.ENABLE_QUERYING: +# Tracks all metadata +all_metadata = {} + +# Tracks clip_filename once video re-encoded +# video_ready_list = {} + +# Queue for metadata being sent to vdms +send_metadata_queue = queue.Queue() + +# Tracks if both components are finished: +# {"clip_name": {"video": bool, "meta": bool}} +clip_completion_tracker = {} + + +# ----- FASTAPI APPLICATION STARTUP/SHUTDOWN ----- +# The lifespan parameter handles startup and shutdown +async def auto_cleanup_janitor(app): + while True: + await asyncio.sleep(10) + now = time.time() + + # --- Stream Monitoring --- + async with app.state.stream_lock: + # Iterating over a list of keys to avoid "dictionary changed size" error + for name, streamer in list(app.state.active_streams.items()): + # streamer = app.state.active_streams.get(name) + if not streamer: + continue + + ai_backlog = streamer.get_executor_backlog() + video_backlog = ( + streamer.write_queue.qsize() + if streamer.config.ENABLE_QUERYING + else 0 + ) + io_backlog = ( + streamer.io_executor._work_queue.qsize() + if hasattr(streamer, "io_executor") + else 0 + ) + + # Check if the stream is marked inactive OR timed out + # streamer.active should be False when the video source ends + is_stale = now - streamer.last_heartbeat > 30 + + should_remove = False + + if not streamer.active and ( + ai_backlog == 0 and video_backlog == 0 and io_backlog == 0 + ): + should_remove = True # Video ended naturally + elif is_stale and ( + ai_backlog == 0 and video_backlog == 0 and io_backlog == 0 + ): + should_remove = True # Browser tab closed/Network lost + elif now - streamer.last_heartbeat > 90: + should_remove = True # Hard timeout for hung processes + + if should_remove: + async with app.state.stream_lock: + if BASE_PIPELINE_CONFIG.DEBUG == "1": + print(f"CLEANUP: Removing {name} from active_streams") + streamer.stop() + app.state.active_streams.pop(name, None) + + # --- Synchronization Data Purge --- + # if BASE_PIPELINE_CONFIG.ENABLE_QUERYING: + # # Remove trackers older than 5 minutes (300s) + # stale_keys = [ + # k + # for k, v in clip_completion_tracker.items() + # if (now - v.get("start", now)) > 300 + # ] + # for k in stale_keys: + # clip_completion_tracker.pop(k, None) + # all_metadata.pop(k, None) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # --- STARTUP --- + if not hasattr(app.state, "classes"): + app.state.classes = None + + if not hasattr(app.state, "active_streams"): + app.state.active_streams = {} + + app.state.status = "Ready" + app.state.stream_lock = asyncio.Lock() + if BASE_PIPELINE_CONFIG.SHARED_MODEL: + app.state.model = YOLO( + BASE_PIPELINE_CONFIG.model_path, verbose=False, task="detect" + ) + + device_input = "cuda" if BASE_PIPELINE_CONFIG.DEVICE == "GPU" else "cpu" + print("Starting shared model warmup...") + dummy_input = torch.zeros( + (1, 3, BASE_PIPELINE_CONFIG.MODEL_H, BASE_PIPELINE_CONFIG.MODEL_W) + ).to(device_input) + for _ in range(20): + _ = app.state.model(dummy_input, verbose=False) + + del dummy_input + torch.cuda.empty_cache() + print("Shared model warmup and VRAM purge complete.") + + janitor_task = asyncio.create_task(auto_cleanup_janitor(app)) + + if BASE_PIPELINE_CONFIG.DEBUG == "1": + print(f"--- APP STARTUP | PID: {os.getpid()} | STATE READY ---") + + yield + + # --- CLEANUP --- + janitor_task.cancel() + async with app.state.stream_lock: + for name, streamer in list(app.state.active_streams.items()): + print(f"Shutting down stream: {name}") + streamer.stop() # Custom stop method defined below + app.state.active_streams.pop(name, None) + app.state.status = "Stopped" + + +# ----- INGESTION FUNCTIONS ----- +def nv12_to_rgb_torch( + nv12_tensor, h, w, is_h264_8k=False, out_buffer=None, is_bgr=True +): + """ + Highly optimized NV12/YUV to RGB/BGR conversion. + Fixes the 'Blue Frame' by applying proper YUV-to-RGB matrix math. + """ + with torch.no_grad(): + if is_h264_8k: + # 8K Path: Expects planar data [C, H, W] + # y: [1, H, W], uv: [1, H, W] (already resized or needs upsampling) + # y = nv12_tensor[0:1, :, :].half() + # # If your 8K source has separate U and V, adjust indices [1:2] and [2:3] + # u = nv12_tensor[1:2, :, :].half() + # v = nv12_tensor[2:3, :, :].half() # Fallback if interleaved + # 8K Planar Path: Map the channels directly + y = nv12_tensor[0:1, :, :].half() + u = nv12_tensor[1:2, :, :].half() + v = nv12_tensor[2:3, :, :].half() # Use the 3rd channel! + + # No column slicing (0::2) needed if it's already planar. + # But we must ensure U and V match Y dimensions if they were subsampled. + if u.shape[-1] != w or u.shape[-2] != h: + u = F.interpolate(u.unsqueeze(0), size=(h, w), mode="bilinear").squeeze( + 0 + ) + v = F.interpolate(v.unsqueeze(0), size=(h, w), mode="bilinear").squeeze( + 0 + ) + else: + # Standard NV12 Path: Image is [H*1.5, W] + y = nv12_tensor[:h, :w].unsqueeze(0).half() + uv = ( + nv12_tensor[h:, :w] + .reshape(h // 2, w // 2, 2) + .permute(2, 0, 1) + .unsqueeze(0) + .half() + ) + # Upsample Chroma (4:2:0 -> 4:4:4) + uv_up = F.interpolate(uv, size=(h, w), mode="nearest") + u = uv_up[0, 0:1, :, :] + v = uv_up[0, 1:2, :, :] + + # --- YUV to RGB Conversion Math (BT.709) --- + # 1. Normalize Luma and center Chroma + y = (y - 16.0) * 1.164 + u = u - 128.0 + v = v - 128.0 + + # 2. Matrix Multiplication (Coefficients for natural color) + r = y + 1.793 * v + g = y - 0.213 * u - 0.533 * v + b = y + 2.112 * u + + # 3. Stack into final order + if is_bgr: + colored_img = torch.cat([b, g, r], dim=0) + else: + colored_img = torch.cat([r, g, b], dim=0) + + # 4. Final Clamp and Format + colored_img.clamp_(0, 255) + output = colored_img.to(torch.uint8) + + if out_buffer is not None: + out_buffer.copy_(output) + return out_buffer + + return output + + +def send_metadata( + VDMS_POOL=None, + DEBUG_FLAG=BASE_PIPELINE_CONFIG.DEBUG_FLAG, + INGESTION=BASE_PIPELINE_CONFIG.INGESTION, + TEST_MODE=BASE_PIPELINE_CONFIG.TEST_MODE, + UDF_HOST=BASE_PIPELINE_CONFIG.UDF_HOST, + UDF_PORT=BASE_PIPELINE_CONFIG.UDF_PORT, + DBHOST=BASE_PIPELINE_CONFIG.DBHOST, + DBPORT=BASE_PIPELINE_CONFIG.DBPORT, +): + """ + Consumer thread that sends metadata to VDMS. + If retries fail, it saves the data to a local JSON 'dead-letter' file. + """ + if VDMS_POOL is None: + # VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + + global all_metadata, send_metadata_queue + clip_filename = "" + clip_key = "" + width = 0 + height = 0 + while True: + try: + queue_details = send_metadata_queue.get() + if queue_details is None: + break + + # # (clip_key, clip_filename, width, height, clip_metadata) = queue_details + (clip_filename, width, height) = queue_details + clip_key = Path(clip_filename).name + clip_metadata = all_metadata.pop(clip_key, None) + + if clip_metadata: + success = metadata2vdms_with_retry( + clip_key, + clip_filename, + clip_metadata, + width, + height, + VDMS_POOL=VDMS_POOL, + DEBUG_FLAG=DEBUG_FLAG, + INGESTION=INGESTION, + TEST_MODE=TEST_MODE, + UDF_HOST=UDF_HOST, + UDF_PORT=UDF_PORT, + DBHOST=DBHOST, + DBPORT=DBPORT, + ) + + # CUSTOM ERROR HANDLER: Final Failure Fallback + if not success: + error_path = f"{BASE_PIPELINE_CONFIG.CODE_DIR}/failed_metadata/{clip_key}.json" + os.makedirs(os.path.dirname(error_path), exist_ok=True) + + with open(error_path, "w") as f: + json.dump( + { + "clip_filename": clip_filename, + "width": width, + "height": height, + "metadata": clip_metadata, + "failed_at": datetime.now().isoformat(), + }, + f, + ) + + main_app_logger.error( + f" [CRITICAL] Permanent VDMS failure. Data saved to: {error_path}" + ) + + send_metadata_queue.task_done() + else: + main_app_logger.error( + f" [MISSING] Metadata for {clip_key} was lost before upload!" + ) + + except Exception as e: + # pass + print(f"[EXCEPTION] Exception occurred in send_metadata: {e}") + + +# ----- STREAM HANDLERS ----- +def scale_clusters_to_8k(merged_640, frame_w=7680, frame_h=4320): + # Ratios for 8K projection + scale_x = frame_w / 640.0 + scale_y = frame_h / 640.0 + final_rois = [] + + for box in merged_640: + # Calculate centroid in 640p space + cx_640 = (box[0] + box[2]) / 2.0 + cy_640 = (box[1] + box[3]) / 2.0 + + # Map to 8K space with float precision to avoid offset drift + cx_8k = cx_640 * scale_x + cy_8k = cy_640 * scale_y + + # Center the 640x640 YOLO crop at the 8K centroid + half = 320 + nx1 = max(0, int(cx_8k - half)) + ny1 = max(0, int(cy_8k - half)) + nx2 = min(frame_w, nx1 + 640) + ny2 = min(frame_h, ny1 + 640) + + # Shift back if clamped at 8K boundaries + if nx2 == frame_w: + nx1 = max(0, frame_w - 640) + if ny2 == frame_h: + ny1 = max(0, frame_h - 640) + + final_rois.append([nx1, ny1, nx1 + 640, ny1 + 640]) + + return final_rois + + +def rendering_worker( + queue, + shared_details, + ready_idx, + reader_active_idx, + frame_lengths, + signal_queue, + display_size, + quality, +): + disp_w, disp_h = display_size + # Attach to both buffers + shm_names = shared_details["shm_names"] + worker_shms = [mp.shared_memory.SharedMemory(name=n) for n in shm_names] + num_shms = len(shm_names) + + # Get shm + # shm_name = shared_details.get("shm_name") + # try: + # shm = mp.shared_memory.SharedMemory(name=shm_name) + # except Exception as e: + # print(f"[WORKER] SHM attach failed: {e}", flush=True) + # return + + try: + while True: + item = queue.get() + if item is None: # Sentinel value to stop the worker + break + + # frame is display size + # metadata in resized res + display_frame, frameNum, metadata_or_bbs, class_list = item + + # display_size = (self.resize_h, self.resize_w) + # display_frame = cv2.resize(frame, display_size, interpolation=cv2.INTER_NEAREST) + + scale_display_x = disp_w / 640 + scale_display_y = disp_h / 640 + + if isinstance(metadata_or_bbs, dict): + # Case: Object Detection + display_frame = get_metadata_overlay( + display_frame, + metadata_or_bbs, + class_list, + (scale_display_x, scale_display_y), + (disp_w, disp_h), + ) + + elif metadata_or_bbs is not None: + # Case: Motion Detections Only (SF Path) + display_frame = get_bb_overlay( + display_frame, + metadata_or_bbs, + (scale_display_x, scale_display_y), + (disp_w, disp_h), + ) + + # writer.write(display_frame) + if frameNum > shared_details["last_id"]: # self.last_delivered_frame_id: + frame_bytes = get_display_frame_in_bytes( + display_frame, + display_size=display_size, + quality=quality, + return_bytes=True, + ) + if frame_bytes: + # THE HARD GUARD: If the reader is currently touching RAM, skip this write. + # This prevents the '1-minute' scramble by ensuring zero memory overlap. + # if signal_queue.full(): + # continue + frame_len = len(frame_bytes) + + forbidden_idx = [ready_idx.value, reader_active_idx.value] + available_idx = [ + i for i in range(num_shms) if i not in forbidden_idx + ] + + if not available_idx: + continue + + # Write to the buffer that is NOT currently 'ready' + # write_idx = (shared_details["buffer_idx"] + 1) % 2 + # write_idx = 1 if ready_idx.value == 0 else 0 + # current_ready = ready_idx.value + # write_idx = (current_ready + 1) % 3 + write_idx = available_idx[0] + shm = worker_shms[write_idx] + + # Zero-copy write to RAM + shm.buf[:frame_len] = frame_bytes + + frame_lengths[write_idx] = frame_len + # shared_details["buffer_idx"] = write_idx + ready_idx.value = write_idx + shared_details["last_id"] = frameNum + # self.last_frame_id = frameNum + # self.last_heartbeat = time.time() + # Signal the FastAPI generator that a new frame is ready + # self.loop.call_soon_threadsafe(self.frame_ready_event.set) + # self.mp_frame_ready_event.set() + # try: + # signal_queue.put_nowait(True) + # except Exception: + # pass + signal_queue.put(True) + + # END While + + except Exception as e: + print(f"[EXCEPTION] Error while rendering display: {e}") + finally: + for s in worker_shms: + s.close() + + +def get_metadata_overlay( + display_frame, metadata_or_bbs, class_list, scale_display, disp_size +): + scale_display_x, scale_display_y = scale_display + disp_w, disp_h = disp_size + for _, obj in metadata_or_bbs.items(): + bbox = obj["bbox"] + x = max(0, int(bbox["x"] * scale_display_x)) + y = max(0, int(bbox["y"] * scale_display_y)) + w = min(disp_w, int(bbox["width"] * scale_display_x)) + h = min(disp_h, int(bbox["height"] * scale_display_y)) + + class_name = bbox["object"] + class_id = class_list.index(class_name) if class_name in class_list else 0 + confidence = bbox.get("object_det", {}).get("confidence", 0.0) + + bb_color = get_detection_color(class_id, is_bgr=True) + label = f"{class_name} {confidence:.2f}" + + cv2.rectangle(display_frame, (x, y), (x + w, y + h), bb_color, 2) + draw_label(display_frame, label, (x, y), color=bb_color, padding=5) + return display_frame + + +def get_bb_overlay(display_frame, metadata_or_bbs, scale_display, disp_size): + scale_display_x, scale_display_y = scale_display + disp_w, disp_h = disp_size + for box in metadata_or_bbs: + if torch.is_tensor(box): + x1, y1, x2, y2 = box.to(torch.int).cpu().tolist() + else: + x1, y1, x2, y2 = map(int, box) + + x1 = max(0, int(x1 * scale_display_x)) + y1 = max(0, int(y1 * scale_display_y)) + x2 = min(disp_w, int(x2 * scale_display_x)) + y2 = min(disp_h, int(y2 * scale_display_y)) + display_frame = cv2.rectangle(display_frame, (x1, y1), (x2, y2), (0, 0, 255), 2) + return display_frame + + +def test_rendering_worker(queue, display_size, out_path, target_fps): + """ + Ultra-efficient video saver for TEST_MODE. + Pipes raw BGR frames directly into an internal FFmpeg engine subshell. + """ + disp_w, disp_h = display_size + + # Construct optimized MPEG-4 parameters to match main pipeline architecture + ffmpeg_cmd = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "bgr24", + "-s", + f"{disp_w}x{disp_h}", + "-r", + str(int(target_fps)), + "-i", + "-", + "-c:v", + "libx264", + "-crf", + "23", + # "-c:v", "mpeg4", # Or "libx264" if you prefer H.264 + # "-qscale:v", "4", # Quality scale (use -crf 23 if using libx264) + str(out_path), + ] + + # Spawn background daemon process + proc = subprocess.Popen( + ffmpeg_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + bufsize=10**7, + ) + + try: + while True: + item = queue.get() + if item is None: # Sentinel value to drain and close the process + break + + display_frame, frameNum, metadata_or_bbs, class_list = item + display_frame = np.ascontiguousarray(display_frame) + scale_display_x = disp_w / 640 + scale_display_y = disp_h / 640 + + # --- Draw Detection Overlays --- + if isinstance(metadata_or_bbs, dict): + # Object Mode (YOLO Structs) + display_frame = get_metadata_overlay( + display_frame, + metadata_or_bbs, + class_list, + (scale_display_x, scale_display_y), + (disp_w, disp_h), + ) + + elif metadata_or_bbs is not None: + # # Motion / Smart Filtering Overlay Path + display_frame = get_bb_overlay( + display_frame, + metadata_or_bbs, + (scale_display_x, scale_display_y), + (disp_w, disp_h), + ) + + # Pipe continuous raw contiguous memory block directly into kernel filesystem handles + proc.stdin.write(np.ascontiguousarray(display_frame).tobytes()) + # queue.task_done() + + except Exception as e: + print(f"[TEST-WORKER-EXCEPTION] Video compilation error: {e}") + finally: + if proc.stdin: + proc.stdin.close() + proc.wait() + + +class DeviceBaseHandler: + def __init__( + self, source, name, active_streams, config=BASE_PIPELINE_CONFIG, **kwargs + ): + self.name = name + self.source = source + self.is_rtsp = str(self.source).startswith("rtsp:/") + self.active = True + self.active_streams = active_streams + self.config = config + configstr = "\n".join( + [f"\t{k}: {v}" for k, v in config.__dict__.items() if not k.startswith("_")] + ) + log_to_logger(f"PipelineConfig: \n{configstr}\n", level="info") + + self.loop = asyncio.get_event_loop() + self.frame_ready_event = asyncio.Event() + self._is_stopped = False # 🛡️ Shutdown guard + self._stop_lock = threading.Lock() # 🔒 Local lock for this instance + self.mp_frame_ready_event = mp.Event() + + # From global + self.device = self.config.DEVICE + self.device_input = self.config.device_input + # self.disp_w, self.disp_h = self.config.DISPLAY_FRAME_SIZE + self.resize_h, self.resize_w = [self.config.MODEL_H, self.config.MODEL_W] + + self.setup_reader(self.config.TARGET_FPS, self.config.CLIP_DURATION) + + # Kwargs + # clip_duration = kwargs.get("clip_duration", CLIP_DURATION) + self.initialize_variables() + + provided_model = kwargs.get("model") + self.setup_model(provided_model) + + self.prepare_pipeline() + + # Start dedicated inference thread and timers + # self.stat_start_time = time.perf_counter() # timing to display frame + self.last_heartbeat = time.time() + self.setup_threads() + + def setup_model(self, provided_model, force_export=False): + if ( + self.frame_width * self.frame_height + ) <= self.config.SMART_FILTERING_PIXEL_CONSTRAINT: + if "_noSF" not in self.config.model_path: + oldpath = Path(self.config.model_path) + old_modelname = self.config.MODEL_NAME + self.config.MODEL_NAME = f"{old_modelname}_noSF" + new_model_name = oldpath.name.replace( + old_modelname, self.config.MODEL_NAME + ) + self.config.model_path = str(oldpath.parent / new_model_name) + + if provided_model is not None and not isinstance(provided_model, str): + self.model = provided_model + self.label_source = [v for k, v in self.model.names.items()] + else: + # if isinstance(provided_model, str) or provided_model is None: + # if Path(self.config.model_path).exists(): + # self.model = YOLO(self.config.model_path, verbose=False, task="detect") + # self.label_source = [] + # for k, v in self.model.names.items(): + # self.label_source.append(v) + # else: + run_platform_name = "engine" if "cuda" in self.device_input else "openvino" + self.model, _, self.label_source = get_model( + Path(self.config.model_path).parent, + self.config.MODEL_NAME.replace("_noSF", ""), + run_platform_name, + self.device_input, + batch=self.config.MODEL_MAX_BATCH_SIZE, + force_export=force_export, + sf_enabled=self.config.sf_enabled, + model_h=self.resize_h, + model_w=self.resize_w, + ) + + if not self.config.sf_enabled: + self.model_warmup(self.frame_height, self.frame_width) + else: + self.model_warmup(self.resize_h, self.resize_w) + # else: + # self.model = provided_model + # self.label_source = [] + # for k, v in self.model.names.items(): + # self.label_source.append(v) + + def initialize_variables(self): + # self.input_fps = self.reader.input_fps + # self.target_fps = self.reader.target_fps + # self.step_size = self.input_fps / self.target_fps + # self.frame_skip = self.reader.frame_skip + # self.max_frames_per_clip = self.reader.max_frames_per_clip + # self.frame_interval = self.reader.frame_interval + # self.frame_width = self.reader.frame_width + # self.frame_height = self.reader.frame_height + # self.numFrames = self.reader.numFrames + # self.duration_s = self.numFrames / self.input_fps + # self.expected_num_frames = int(self.duration_s * self.target_fps) + # self.get_frameWH() + # 1. HARD GUARD: Capture reader values and ensure the connection is active + self.input_fps = self.reader.input_fps + self.target_fps = self.reader.target_fps + self.frame_width = self.reader.frame_width + self.frame_height = self.reader.frame_height + self.numFrames = self.reader.numFrames + + if self.input_fps <= 0 or self.frame_width <= 0 or self.frame_height <= 0: + main_app_logger.error( + f"[{self.name}] Stream handler fast-fail triggered. Destination " + f"unreachable or invalid ({self.source}). Terminating pipeline configuration." + ) + # Instantly stop background threads to prevent zombie process leakage + if hasattr(self, "reader") and self.reader is not None: + self.reader.stop() + + raise RuntimeError( + f"Failed to initialize stream reader endpoint: {self.source}" + ) + + # 2. Proceed with calculation mechanics safely only if values are healthy + self.step_size = self.input_fps / self.target_fps + self.frame_skip = self.reader.frame_skip + self.max_frames_per_clip = self.reader.max_frames_per_clip + self.frame_interval = self.reader.frame_interval + + self.duration_s = self.numFrames / self.input_fps + self.expected_num_frames = int(self.duration_s * self.target_fps) + self.get_frameWH() + + # Determine minimum contour size relative to frame resolution + self.min_contour_area = int( + (self.config.ROI_MIN_AREA_RATIO * self.resize_w) + * (self.config.ROI_MIN_AREA_RATIO * self.resize_h) + ) # 207 + + self.dist_thresh_8k = max( + self.config.ROI_DISTANCE_THRESH_RATIO * self.frame_width, + self.config.ROI_DISTANCE_THRESH_RATIO * self.frame_height, + ) + self.dist_thresh_640 = max( + self.config.ROI_DISTANCE_THRESH_RATIO * self.resize_w, + self.config.ROI_DISTANCE_THRESH_RATIO * self.resize_h, + ) # 0.05 * self.resize_w + self.scales_tensor = torch.tensor( + [self.scale_x, self.scale_y, self.scale_x, self.scale_y], + # device="cpu", + device=self.device_input, + ) + + self.disp_w, self.disp_h = self.config.DISPLAY_FRAME_SIZE + + # Performance Tracking + self.elapsed_display_time = 0.0 + self.frame_count = 0 # Frame count for videos + self.frame_count_target = 0 + self.last_delivered_frame_id = -1 # Track what was actually sent + self.last_frame_id = 0 + self.latest_processed_frame = None + self.next_process_idx = 0.0 + self.stat_fps = 0 + self.stat_frame_count = 0 + self.total_objects_detected = 0 + + self.writer_done = True + + # Video Clipping + # self.video_writer = None + self.ffmpeg_proc = None # Replaces cv2.VideoWriter completely + # self.fourcc = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v + self.clip_id = 0 + # self.clip_filename = "" + self.clip_filename_pattern = f"{self.config.SHARED_OUTPUT}/{self.name}_%03d.mp4" + self.clip_key = f"{self.name}_000.mp4" + # self.tmp_file = "" + self.frame_in_clip_count = 0 + + if self.config.ENABLE_QUERYING: + # Thread-safe queue for the resized frames (640x640) + # maxlen=300 allows for a 20-second buffer in case of extreme disk lag + # Non-blocking queue for frames and control signals + self.write_queue = queue.Queue(maxsize=300) + if not self.config.TEST_MODE: + self.send_metadata_queue = queue.Queue() + self.writer_done = False + self.stop_writer = threading.Event() if self.config.ENABLE_QUERYING else None + + # --- PRE-ALLOCATED ZERO-COPY HARDWARE RING WORKSPACE --- + self.ring_depth = 4 # 8 + self.gpu_ring_idx = 0 + self.cpu_ring_idx = 0 + + self.pinned_matrices = [] + self.pinned_tensors = [] + + # Pre-allocate a 4-slot ring buffer for raw 8K BGR frames + self.ai_ring_depth = 4 + self.ai_ring_idx = 0 + self.frame_stride_bytes = ( + self.resize_w * self.resize_h * 3 + ) # ~99.5 MB per frame + + self.ai_shms = [] + self.ai_shm_names = [] + self.ai_pinned_tensors = [] # Explicit property initialization 🚀 + + for i in range(self.ai_ring_depth): + name = f"shm_ai_640_{self.name}_{i}_{os.getpid()}" + + try: + # Attempt to attach to a lingering zombie segment + old_shm = shared_memory.SharedMemory(name=name) + old_shm.close() + old_shm.unlink() # Permanently destroys the old OS block handle + main_app_logger.warning(f"Cleaned up residual zombie shared memory block: {name}") + except FileNotFoundError: + pass # Block doesn't exist, safe to proceed normal initialization + + shm = shared_memory.SharedMemory( + name=name, create=True, size=self.frame_stride_bytes + ) + self.ai_shms.append(shm) + self.ai_shm_names.append(name) + + # Map a zero-copy lockless numpy array view straight onto the memory block + view = np.ndarray( + (self.resize_h, self.resize_w, 3), dtype=np.uint8, buffer=shm.buf + ) + + # Page-lock the host buffer window to maximize PCIe bus transfer bandwidth + try: + cv2.cuda.registerPageLocked(view) + except Exception: + pass + + # Expose a direct matching PyTorch host tensor map to secure high-speed uploads + self.ai_pinned_tensors.append(torch.from_numpy(view)) + + # Pre-allocate a 4D FP16 GPU staging canvas to maximize Tensor Core performance + # if self.device_input == "cuda": + # self.ai_gpu_staging = torch.empty( + # (1, 3, self.frame_height, self.frame_width), + # dtype=torch.float16, + # device=f"cuda:{self.gpu_id}", + # ) + # self.preview_gpu_staging = torch.empty( + # (1, 3, self.frame_height, self.frame_width), + # dtype=torch.float16, + # device=f"cuda:{self.gpu_id}", + # ) + + # Pre-allocate 640x640 workspace footprint across CPU and GPU spaces + for _ in range(self.ring_depth): + mat = np.zeros((self.resize_h, self.resize_w, 3), dtype=np.uint8) + try: + cv2.cuda.registerPageLocked(mat) + except cv2.error: + pass + self.pinned_matrices.append(mat) + self.pinned_tensors.append(torch.from_numpy(mat)) + # Isolate CUDA tasks using a dedicated stream and independent hardware completion barriers + self.processing_stream = ( + torch.cuda.Stream() if self.device_input == "cuda" else None + ) + # Pre-allocated hardware events guarantee completely non-blocking stream isolation + self.slot_events = ( + [torch.cuda.Event() for _ in range(8)] + if self.device_input == "cuda" + else None + ) + + self.gpu_float_staging = None + if self.device_input == "cuda": + self.gpu_float_staging = torch.empty( + (1, 3, self.frame_height, self.frame_width), + dtype=torch.float16, + device=f"cuda:{self.gpu_id}", + ) + + # Default Kernels + self.dilate_kernel = cv2.getStructuringElement( + cv2.MORPH_ELLIPSE, + # cv2.MORPH_RECT, + (self.config.DILATE_KERNEL_SIZE, self.config.DILATE_KERNEL_SIZE), + ) + # self.dilate_kernel_for_enhanced_mask = np.ones((15,15), np.uint8) # 5, 5) (21, 21) + self.dilate_kernel_for_enhanced_mask = cv2.getStructuringElement( + cv2.MORPH_ELLIPSE, + # cv2.MORPH_RECT, + ( + self.config.BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE, + self.config.BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE, + ), + ) + + def setup_reader(self, target_fps, clip_duration): + # if hasattr(self, "reader"): + # del self.reader + + # Add a tiny sleep or garbage collect to ensure the GPU handle is released + gc.collect() + torch.cuda.empty_cache() # Clear any remaining context + + # TODO: Further investigate GPU path, bkgd subtraction sensitive to artifacts + self.gpu_id = 0 + if self.device_input == "cuda": # and not self.is_rtsp: + from include.readers import GPUHybridReader + + self.reader = GPUHybridReader( + source=self.source, + target_fps=target_fps, + clip_duration=clip_duration, + gpu_id=self.gpu_id, + queue_size=0 if self.config.TEST_MODE else 2, + ) + else: + from include.readers import CPUHybridReader + + self.reader = CPUHybridReader( + source=self.source, + target_fps=target_fps, + clip_duration=clip_duration, + queue_size=0 if self.config.TEST_MODE else 2, + ) + + def prepare_pipeline(self): + if self.device_input == "cuda": + self.prepare_gpu_pipeline() + if len(self.active_streams) == 0: + self.gpu_warmup() + else: + self.prepare_cpu_pipeline() + + def setup_threads(self): + # Shared 10MB memory for display + self.setup_shared_memory() + + # Executor for Async YOLO tasks and FFmpeg re-encoding + self.executor = ThreadPoolExecutor(max_workers=self.config.MAX_WORKERS) + self.clip_executor = ThreadPoolExecutor(max_workers=self.config.MAX_WORKERS) + + print( + f"sf_enabled: {self.config.sf_enabled}\tTEST_MODE: {self.config.TEST_MODE}", + flush=True, + ) + + # Producer: Handles acquisition and AI metadata logs + self.process_thread = threading.Thread( + target=self.run_realtime_inference, + args=(self.config.sf_enabled,), + daemon=True, + ) + + self.signal_queue = mp.Queue(maxsize=1) + self.render_queue = mp.Queue(maxsize=5) + + if self.config.TEST_MODE: + test_dir = os.getenv( + "TEST_SUITE_RENDER_DIR", str(Path(self.config.SHARED_OUTPUT)) + ) + os.makedirs(test_dir, exist_ok=True) + out_path = os.path.join(test_dir, f"{self.name}_detections_output.mp4") + log_to_logger( + f"[TEST MODE] Detection results saved to: {out_path}", level="info" + ) + self.render_proc = threading.Thread( + target=test_rendering_worker, + args=( + self.render_queue, + (self.disp_w, self.disp_h), + out_path, + self.target_fps, + ), + daemon=True, + ) + + # Dummy target alignment to prevent execution signature exceptions + self.display_proc = threading.Thread(target=lambda: None, daemon=True) + else: + self.render_proc = mp.Process( + target=rendering_worker, + args=( + self.render_queue, + self.shared_details, + self.ready_buffer_idx, + self.reader_active_idx, + self.shm_frame_lengths, + self.signal_queue, + (self.disp_w, self.disp_h), + self.config.DISPLAY_FRAME_QUALITY, + ), + ) + + def display_signal_sync(): + while self.active: + # Wait for signal + # if self.mp_frame_ready_event.wait(timeout=1.0): + # self.mp_frame_ready_event.clear() + try: + _ = self.signal_queue.get(timeout=1.0) + # print(f"[DEBUG]: Signal received in FastAPI process for {self.name}", flush=True) + # Wake FastAPI async loop in main thread + self.loop.call_soon_threadsafe(self.frame_ready_event.set) + except queue.Empty: + continue + + self.display_proc = threading.Thread( + target=display_signal_sync, daemon=True + ) + + if self.config.ENABLE_QUERYING: + # NEW: Dedicated I/O pool for Disk/GPU transfers (Higher worker count for 8K) + self.io_executor = ThreadPoolExecutor(max_workers=8) + + # Dedicated FFmpeg pool so re-encoding doesn't slow down live AI + self.ffmpeg_executor = ThreadPoolExecutor(max_workers=2) + + if not self.config.TEST_MODE: + # Sends metadata to VDMS + self.metadata_thread = threading.Thread( + target=send_metadata, + args=( + VDMSPool(self.config.DBHOST, self.config.DBPORT, size=10), + self.config.DEBUG_FLAG, + self.config.INGESTION, + self.config.TEST_MODE, + self.config.UDF_HOST, + self.config.UDF_PORT, + self.config.DBHOST, + self.config.DBPORT, + ), + daemon=True, + ) + + # Consumer: Handles GPU-to-CPU download and Disk I/O (Writing resized frames to RAM disk) + self.writer_thread = threading.Thread( + target=self.video_writer_core_loop, + args=(self.stop_writer,), + daemon=True, + ) + + def setup_shared_memory(self): + self.manager = mp.Manager() + + self.shms = [] + shm_names = [] + num_shms = 3 + + # Shared Integer to track which buffer is "Ready" for the UI + # 'i' for integer, initialized to 0 + self.ready_buffer_idx = mp.Value("i", 0) + self.reader_active_idx = mp.Value("i", -1) + self.shm_frame_lengths = mp.Array("i", [0 for _ in range(num_shms)]) + + for idx in range(num_shms): + # self.shm = mp.shared_memory.SharedMemory(create=True, size=10*1024*1024) + shm_name = f"shm_{self.name}_{idx}_{os.getpid()}" + # print(f"[DEBUG]: Setting up SHM {shm_name}", flush=True) + try: + shm = mp.shared_memory.SharedMemory( + name=shm_name, create=True, size=10 * 1024 * 1024 + ) + except FileExistsError: + # Attach to existing memory + shm = mp.shared_memory.SharedMemory(name=shm_name) + except Exception as e: + main_app_logger.error(f"Failed to initialize shared memory: {e}") + raise + self.shms.append(shm) + shm_names.append(shm_name) + + self.shared_details = self.manager.dict() + self.shared_details["shm_names"] = shm_names + # self.shared_details["buffer_idx"] = 0 + # self.shared_details["frame_length"] = [0 for _ in range(num_shms)] + self.shared_details["last_id"] = -1 + + def start(self): + """ + Starts the decoupled ingestion and inference threads in the correct order. + """ + # PRE-SYNC: Ensure GPU is idle before timing starts + if self.device_input == "cuda": + if torch.cuda.is_available(): + torch.cuda.synchronize() + + # Start the hardware-decoupled reader first + self.reader.start() + + if not self.config.DISABLE_DETECTION: + self.render_proc.start() + + self.display_proc.start() + + if self.config.ENABLE_QUERYING: + self._initialize_writer() + + # Small delay to allow the reader's deque to populate + time.sleep(0.1) + + # Start the producer and consumer threads + if hasattr(self, "process_thread") and not self.process_thread.is_alive(): + self.process_thread.start() + + if ( + self.config.ENABLE_QUERYING + and not self.config.TEST_MODE + and not self.metadata_thread.is_alive() + ): + self.metadata_thread.start() + + if self.config.ENABLE_QUERYING and not self.writer_thread.is_alive(): + self.writer_thread.start() + + return self + + def stop(self): + """ + Comprehensive resource release. Safely drains the frame pipelines, + forces a graceful FFmpeg flush to prevent 'moov atom' index corruption, + and cleanly unlinks shared memory layers. + """ + with self._stop_lock: + if self._is_stopped: + return # Already stopped by another thread + + # 1. Instantly pull out of active dashboards to stop inbound traffic + if self.name in self.active_streams: + self.active_streams.pop(self.name, None) + + self.active = False + print( + f"[STOP] Initiating graceful flush shutdown for {self.name}", + flush=True, + ) + + # 2. Trigger your event handler flags + if hasattr(self, "stop_writer") and self.stop_writer is not None: + try: + self.stop_writer.set() + except Exception: + pass + + # 3. PHASE 1: UNBLOCK CONSUMER CORES (Poison Pill Deliveries First) + if hasattr(self, "write_queue") and self.write_queue is not None: + try: + # Clear out pending frame backlogs to speed up shutdown execution + while not self.write_queue.empty(): + try: + self.write_queue.get_nowait() + except Exception: + break + # Dispatch clean poison pill token to release the consumer thread + self.write_queue.put(None) + except Exception: + pass + + if hasattr(self, "render_queue") and self.render_queue is not None: + try: + while not self.render_queue.empty(): + try: + self.render_queue.get_nowait() + except Exception: + break + self.render_queue.put_nowait(None) + except Exception: + pass + + # 4. PHASE 2: GRACEFUL FFmpeg DEFLATION GATE (Bypasses Moov Issues) + if hasattr(self, "ffmpeg_proc") and self.ffmpeg_proc is not None: + try: + print( + "[STOP] Closing video pipeline write handles to flush metadata...", + flush=True, + ) + if self.ffmpeg_proc.stdin: + self.ffmpeg_proc.stdin.close() # Safely alerts FFmpeg to finalize files + + if self.ffmpeg_proc.stderr: + self.ffmpeg_proc.stderr.close() # Instantly forces readline() to return None and exits thread safely + + # Grant a soft 5-second window for storage layer disk synchronization + self.ffmpeg_proc.wait(timeout=5.0) + print( + " [STOP] FFmpeg closed cleanly with valid indexing atoms.", + flush=True, + ) + except subprocess.TimeoutExpired: + print( + " [STOP-WARN] Video flush timed out. Forcing hard termination.", + flush=True, + ) + try: + self.ffmpeg_proc.kill() + except Exception: + pass + except Exception as io_err: + print( + f" [STOP-WARN] Error during streaming flush: {io_err}", + flush=True, + ) + finally: + self.ffmpeg_proc = None + self.video_writer = None + + # 5. PHASE 3: SHUTDOWN MULTIPROCESSING DAEMONS + for proc_attr in ["render_proc", "ai_proc"]: + proc = getattr(self, proc_attr, None) + if proc is not None: + try: + if proc.is_alive(): + proc.terminate() + proc.join(timeout=0.5) + proc.close() + except Exception: + pass + setattr(self, proc_attr, None) + + # 6. PHASE 4: FINAL SEGMENT EVALUATION CONVERGENCE + try: + final_clip_key = f"{self.name}_{self.clip_id:03d}.mp4" + final_clip_path = f"{self.config.SHARED_OUTPUT}/{final_clip_key}" + + # Check if the final truncated or partial segment actually exists before dispatching + if ( + os.path.exists(final_clip_path) + and os.path.getsize(final_clip_path) > 0 + ): + print( + f" [STOP-FLUSH] Registering finalized terminal clip: {final_clip_key}", + flush=True, + ) + global clip_completion_tracker + if final_clip_key not in clip_completion_tracker: + clip_completion_tracker[final_clip_key] = { + "video": False, + "meta": False, + "start_time": time.time(), + } + + clip_completion_tracker[final_clip_key]["video"] = True + clip_completion_tracker[final_clip_key]["meta"] = True + self._evaluate_barrier_and_dispatch( + final_clip_key, final_clip_path, self.resize_w, self.resize_h + ) + except Exception as final_flush_err: + print( + f" [STOP-WARN] Final segment tracking layer bypass failed: {final_flush_err}", + flush=True, + ) + + # 8. PHASE 6: UNMAP HARDWARE MEMORY OBJECTS + if hasattr(self, "shared_details"): + try: + # Force-close the internal lock primitive hidden inside the Manager dict proxy + if hasattr(self.shared_details, "_ctx") and hasattr( + self.shared_details._ctx, "RLock" + ): + lock = self.shared_details._ctx.RLock() + if hasattr(lock, "_semlock"): + lock._semlock._close() + self.shared_details.clear() + except Exception: + pass + del self.shared_details + + if ( + hasattr(self, "mp_frame_ready_event") + and self.mp_frame_ready_event is not None + ): + try: + # mp.Event allocates an internal ctx.Cond / ctx.Lock boundary pair + if hasattr(self.mp_frame_ready_event, "_cond"): + cond = self.mp_frame_ready_event._cond + if hasattr(cond, "_lock") and hasattr(cond._lock, "_semlock"): + cond._lock._semlock._close() + except Exception: + pass + self.mp_frame_ready_event = None + + for q_attr in ["signal_queue", "render_queue"]: + if hasattr(self, q_attr): + q = getattr(self, q_attr) + if q is not None: + try: + # 1. Flush and break down standard background worker threads safely + q.close() + q.join_thread() + + # 2. CRITICAL: Force-close the hidden POSIX lock primitives to clear resource_tracker limits + if hasattr(q, "_rlock") and q._rlock is not None: + if hasattr(q._rlock, "_semlock"): + q._rlock._semlock._close() + + if hasattr(q, "_writer") and q._writer is not None: + if hasattr(q._writer, "_semlock"): + q._writer._semlock._close() + except Exception: + pass + setattr(self, q_attr, None) + + for primitive_attr in [ + "ready_buffer_idx", + "reader_active_idx", + "shm_frame_lengths", + ]: + if hasattr(self, primitive_attr): + obj = getattr(self, primitive_attr) + if obj is not None and hasattr(obj, "get_lock"): + try: + # Grab the hidden lower-level POSIX context lock map + lock = obj.get_lock() + # If the lock handle is bound to an active system descriptor, release it + if hasattr(lock, "_semlock"): + lock._semlock._close() # Forces immediate unlinking at the OS level + except Exception: + pass + setattr(self, primitive_attr, None) + + if hasattr(self, "manager") and self.manager is not None: + try: + self.manager.shutdown() + except Exception: + pass + self.manager = None + + if hasattr(self, "pinned_matrices") and self.pinned_matrices: + for active_mat in self.pinned_matrices: + try: + cv2.cuda.unregisterPageLocked(active_mat) + except Exception: + pass + self.pinned_matrices.clear() + self.pinned_tensors.clear() + + if hasattr(self, "ai_shms") and self.ai_shms: + for shm in self.ai_shms: + try: + shm.close() + shm.unlink() + except Exception: + pass + self.ai_shms.clear() + self.ai_shm_names.clear() + + if hasattr(self, "shms") and self.shms: + for shm in self.shms: + shm.close() + try: + shm.unlink() + except FileNotFoundError: + pass + self.shms.clear() + + if hasattr(self, "cap") and self.cap is not None: + self.cap.release() + self.cap = None + + for pool_name in [ + "executor", + "io_executor", + "clip_executor", + "ffmpeg_executor", + ]: + if hasattr(self, pool_name) and getattr(self, pool_name) is not None: + try: + getattr(self, pool_name).shutdown(wait=True) + except Exception: + pass + setattr(self, pool_name, None) + + self.reader = None + self._is_stopped = True + print(f" [STOP] {self.name} pipeline resources fully released.", flush=True) + + def model_warmup(self, H=640, W=640): + H, W = int(H), int(W) + # Move the dummy input creation inside a no_grad block + with torch.no_grad(): + print(f"Starting warmup for {self.name}...", flush=True) + dummy_input = torch.zeros((1, 3, H, W)).to(self.device_input) + + # Perform iterations directly on the main thread + for i in range(5): + _ = self.run_model( + dummy_input, + imgsz=(H, W), + batch=1, + device_input=self.device_input, + stream=STREAM_ARG, + ) + + # Force GPU to finish before returning + if self.device_input == "cuda": + torch.cuda.synchronize() + + print(f"Warmup complete for {self.name}", flush=True) + + def get_executor_backlog(self): + """Returns the number of tasks currently waiting in the thread pool queue.""" + # access the internal queue of the executor + return self.executor._work_queue.qsize() + + def get_clip_executor_backlog(self): + """Returns the number of tasks currently waiting in the thread pool queue.""" + # access the internal queue of the clip_executor + return self.clip_executor._work_queue.qsize() + + def check_disk_usage(self, path, min_gb=0.5): + """Returns True if there is at least min_gb available at path.""" + try: + total, used, free = shutil.disk_usage(path) + # Convert bytes to Gigabytes + free_gb = free / (2**30) + return free_gb > min_gb + except Exception as e: + print(f"[EXCEPTION] Disk check error: {e}") + return False + + # Gets frame W and H details + def get_frameWH(self): + if (self.frame_height * self.frame_width) < ( + self.config.MODEL_H * self.config.MODEL_W + ): + new_sizeHW = check_imgsz( + [self.config.MODEL_H, self.config.MODEL_W] + ) # expects hxw + else: + new_sizeHW = check_imgsz( + [self.frame_height, self.frame_width] + ) # expects hxw + + new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + + self.width = new_sizeWH[0] + self.height = new_sizeWH[1] + + # Configure scaling for 8K-to-Model coordinate mapping + self.resize_h, self.resize_w = [self.config.MODEL_H, self.config.MODEL_W] + self.scale_x = self.frame_width / self.config.MODEL_W + self.scale_y = self.frame_height / self.config.MODEL_H + + def update_frame(self, stat_start_time): + self.stat_frame_count += 1 + self.elapsed_display_time += time.perf_counter() - stat_start_time + # if elapsed > 0.5: + self.stat_fps = round(self.stat_frame_count / self.elapsed_display_time, 1) + + def is_processing(self): + """Returns True if any part of the pipeline is still active.""" + if not self.reader.stopped: + return True + + if ( + self.process_thread is not None and self.process_thread.is_alive() + ): # or not self.reader.frame_queue.empty(): + return True + + if not self.reader.frame_queue.empty(): + return True + + q_size = self.write_queue.qsize() + if self.process_thread is not None: + print( + f"[STATUS] Writer: {self.write_count}/{self.frame_count_target} | Q: {q_size} | Inf: {'Alive' if self.process_thread.is_alive() else 'Dead'}", + end="\r", + flush=True, + ) + else: + print( + f"[STATUS] Writer: {self.write_count}/{self.frame_count_target} | Q: {q_size}", + end="\r", + flush=True, + ) + + if not self.write_queue.empty(): + return True + if self.write_count < self.frame_count_target: + # q_size = self.write_queue.qsize() + # print(f"[DRAIN] Writer: {self.write_count}/{self.frame_count} | Queue: {q_size}", end="\r", flush=True) + return True + return False + + def run_model( + self, + frame, + imgsz=(BASE_PIPELINE_CONFIG.MODEL_H, BASE_PIPELINE_CONFIG.MODEL_W), + batch=1, + device_input="cuda", + stream=False, + ): + # --- DEBUG VERIFICATION --- + # if torch.is_tensor(frame): + # # We want to see [N, 3, 640, 640] where N > 1 + # print(f"[DEBUG] Tensor Input Shape: {frame.shape}") + # elif isinstance(frame, list): + # print(f"[DEBUG] List Input Length: {len(frame)}") + + # print(f"[DEBUG] Stream Mode: {stream} | Requested Batch: {batch}") + # --------------------------- + if isinstance(frame, torch.Tensor): + # Ensure on the right device + frame = frame.to(device_input) + + # Make sure input is multiple of 32 + h, w = frame.shape[-2:] + pad_h = (32 - h % 32) % 32 + pad_w = (32 - w % 32) % 32 + + if pad_h > 0 or pad_w > 0: + # F.pad for 4D tensor (B, C, H, W) uses (left, right, top, bottom) + frame = F.pad(frame, (0, pad_w, 0, pad_h), value=0) + + results = self.model.predict( + frame, + imgsz=imgsz, + batch=batch, + device=device_input, + verbose=False, + stream=stream, + conf=self.config.DETECTION_THRESHOLD, + max_det=self.config.MAX_DETECTIONS, + rect=True, # False, # + ) + return results + + # def _encode_and_signal(self, data, frame_num): + # """Worker task for JPEG encoding. Optimized for zero-copy GPU transfers.""" + # if data is None: + # return + + # if isinstance(data, cv2.cuda.GpuMat): + # # GPU PATH: Use existing 8K GPU pointer. Resize on GPU (Instant) 🏎️ + # cv2.cuda.resize( + # data, + # (self.disp_w, self.disp_h), + # stream=self.encode_stream, + # dst=self.gpu_display_frame, + # ) + # # Only download the small (640x360) frame, not the 8K frame! + # display_frame = self.gpu_display_frame.download(self.encode_stream) + # else: + # # CPU PATH: Resize and force memory contiguity for faster encoding + # display_frame = cv2.resize( + # data, (self.disp_w, self.disp_h), interpolation=cv2.INTER_NEAREST + # ) + # display_frame = np.ascontiguousarray(display_frame) + + # # Drop quality to 35. This reduces the bytes FastAPI has to push to the browser. + # # success, buffer = cv2.imencode( + # # ".jpg", display_frame, [cv2.IMWRITE_JPEG_QUALITY, 35] + # # ) + + # # if success: + # # self.latest_processed_frame = buffer.tobytes() + # # self.last_delivered_frame_id = frame_num + # # self.last_heartbeat = time.time() + # # # Non-blocking signal to FastAPI + # # self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + # # Run the high-overhead JPEG compression block within a non-blocking background thread worker + # def _async_compress_task(img_mat, f_idx): + # success, buffer = cv2.imencode(".jpg", img_mat, [cv2.IMWRITE_JPEG_QUALITY, 35]) + # if success and self.active: + # self.latest_processed_frame = buffer.tobytes() + # self.last_delivered_frame_id = f_idx + # self.last_heartbeat = time.time() + # # Safely wake up the FastAPI async loop on the main process thread + # self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + # # Submit directly to your background I/O pool to unblock the inference stream + # self.io_executor.submit(_async_compress_task, display_frame, frame_num) + + def _encode_and_signal(self, data, frame_num): + """ + Asynchronously processes display outputs by offloading high-overhead + JPEG compression tasks directly onto the background I/O pool. + """ + if data is None: + return + + # Check the backlog of your I/O task queue to prevent memory leaks + if hasattr(self, "io_executor") and self.io_executor._work_queue.qsize() > 4: + return # Skip displaying this frame to preserve CPU performance + + def _async_render_and_compress(frame_data, f_num): + if isinstance(frame_data, cv2.cuda.GpuMat): + # GPU Path: Perform hardware-accelerated resizing inside VRAM + cv2.cuda.resize( + frame_data, + (self.disp_w, self.disp_h), + stream=self.encode_stream, + dst=self.gpu_display_frame, + ) + display_frame = self.gpu_display_frame.download(self.encode_stream) + else: + # CPU Path: Perform rapid array transformations + display_frame = cv2.resize( + frame_data, + (self.disp_w, self.disp_h), + interpolation=cv2.INTER_NEAREST, + ) + + display_frame = np.ascontiguousarray(display_frame) + success, buffer = cv2.imencode( + ".jpg", display_frame, [cv2.IMWRITE_JPEG_QUALITY, 35] + ) + + if success and self.active: + self.latest_processed_frame = buffer.tobytes() + self.last_delivered_frame_id = f_num + self.last_heartbeat = time.time() + # Non-blocking signal to wake up the FastAPI event loop + self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + # Offload the processing overhead entirely from the inference thread pool + self.io_executor.submit(_async_render_and_compress, data, frame_num) + + def update_ui_fallback(self, frame, frame_num): + # If backlog is very high, drop JPEG quality to 25 to clear the 'pause' faster + backlog = self.get_executor_backlog() + adaptive_quality = ( + 20 + if backlog > (self.dynamic_limit * 2) + else self.config.DISPLAY_FRAME_QUALITY + ) + + # FALLBACK: If AI is busy, worker thread encodes raw frame for the UI. + # This offloads the 40ms CPU cost from the main Producer loop. + display_frame = cv2.resize( + frame, (self.disp_w, self.disp_h), interpolation=cv2.INTER_NEAREST + ) + _, buffer = cv2.imencode( + ".jpg", display_frame, [cv2.IMWRITE_JPEG_QUALITY, adaptive_quality] + ) + + # print(f"DEFAULT DISP frameNum/last_frame_id {frame_num} self.last_delivered_frame_id: {self.last_delivered_frame_id}", flush=True) #\n\tframe_bytes: {frame_bytes}", flush=True) + self.latest_processed_frame = buffer.tobytes() + self.last_delivered_frame_id = frame_num + self.last_frame_id = frame_num + self.last_heartbeat = time.time() + # Signal the FastAPI generator that a new frame is ready + self.loop.call_soon_threadsafe(self.frame_ready_event.set) + + def _check_shm_safety(self, threshold_percent=90): + """ + Scans /dev/shm and deletes the oldest .mp4 files if usage exceeds threshold. + This prevents the 8K stream from crashing the entire container. + """ + # Check current usage of the RAM disk + usage = shutil.disk_usage("/dev/shm") + percent_used = (usage.used / usage.total) * 100 + + if percent_used > threshold_percent: + print( + f" [CRITICAL] /dev/shm usage at {percent_used:.1f}%. Purging old clips..." + ) + + # Get all .mp4 files in /dev/shm sorted by oldest first + shm_path = Path("/dev/shm") + clips = sorted(shm_path.glob("*.mp4"), key=lambda x: x.stat().st_mtime) + + # Delete files until we are under 70% usage or run out of files + for clip in clips: + try: + # Don't delete the file the current writer is actively using! + # if str(clip) == self.tmp_file: + # continue + + clip.unlink() + print(f"[PURGE] Deleted {clip.name} to free RAM.") + + # Re-check usage after each deletion + usage = shutil.disk_usage("/dev/shm") + if (usage.used / usage.total) * 100 < 70: + break + except Exception as e: + print(f"[EXCEPTION] Could not purge {clip}: {e}") + + # def run_realtime_inference(self, sf_enabled): + # """Producer: Maintains the target FPS and updates clip IDs.""" + # # Calculate a dynamic limit: tolerate 0.5 seconds of lag. + # # If target_fps is 15, the limit is 7. If target_fps is 30, the limit is 15. + # self.dynamic_limit = max(2, int(0.5 * self.target_fps)) + # last_frame_time = time.perf_counter() + # while self.active: + # # --- FRAME RETRIEVAL --- + # try: + # device_frame, frame_num = self.reader.read() + # if device_frame is None: + # if self.reader.stopped: + # self.active = False + + # clip_filename = f"{self.config.SHARED_OUTPUT}/{self.name}_{self.clip_id:03d}.mp4" + # clip_key = Path(clip_filename).name + # global send_metadata_queue, all_metadata + # if clip_key in all_metadata: + # # Signal to process metadata for previous cli + # if ( + # "send_metadata_queue" in globals() + # or "send_metadata_queue" in locals() + # ): + # try: + # send_metadata_queue.put( + # (clip_filename, self.resize_w, self.resize_h) + # ) + # except Exception as queue_err: + # print( + # f"[CLIPPER-WARN] Metadata queue push skipped or unallocated: {queue_err}", + # flush=True, + # ) + # break + # continue + # except queue.Empty: + # if getattr(self.reader, "reconnect_failed", False): + # self.active = False + # break + # time.sleep(0.002) + # continue + + # # Keep 8K frame on GPU (Skip CPU conversion for non-target frames) + # # if self.device_input == "cuda" and not self.reader.is_h264_8k: + # # with torch.cuda.stream(self.ingest_stream): + # # device_frame = nv12_to_rgb_torch( + # # device_frame, self.frame_height, self.frame_width# , is_bgr=False + # # ) + # # self.ingest_stream.synchronize() + + # # if device_frame is not None: + # self.frame_count += 1 + # is_target_frame = float(frame_num) >= self.next_process_idx + + # # Determine if this frame should be AI or Raw based on backlog + # # But ALWAYS submit to the executor to maintain frame order. + # backlog = self.get_executor_backlog() + + # while backlog > 4 and self.active: + # time.sleep(0.005) + # backlog = self.get_executor_backlog() + + # def wrapped_fn(*args): + # if self.device_input == "cuda": + # with torch.cuda.stream(self.inference_stream): + # dev_frame, f_num, target_flag = args + # # FIX: Explicitly crop out any hardware padding columns horizontally + # # and rows vertically before forcing linear memory contiguity. + # if dev_frame.ndim >= 2: + # h_raw, w_raw = dev_frame.shape[-2:] + # if w_raw != self.frame_width or h_raw != self.frame_height: + # dev_frame = dev_frame[..., :self.frame_height, :self.frame_width] + + # isolated_frame = dev_frame.clone().contiguous() + # self.pipeline_fn(isolated_frame, f_num, target_flag) + # else: + # self.pipeline_fn(*args) + + # # Handoff to AI and Writer + # if self.active: + # self.executor.submit( + # # pipeline_fn, + # wrapped_fn, + # device_frame, + # frame_num, + # is_target_frame, + # ) + + # # --- PRECISE CLOCK SYNC --- + # # This prevents the producer from "lapping" the consumer + # # and building that jumpy backlog in the first place. + # elapsed = time.perf_counter() - last_frame_time + # if elapsed < self.frame_interval: + # # time.sleep(self.frame_interval - elapsed) + # # Subtract a small epsilon (0.001) for OS scheduling overhead + # # sleep_duration = self.frame_interval - elapsed - 0.0025 + # # if sleep_duration > 0.001: + # # time.sleep(sleep_duration) + # time.sleep(max(0, self.frame_interval - elapsed - 0.0015)) + # last_frame_time = time.perf_counter() + + # # self.update_frame() + # self.last_heartbeat = time.time() + + # self.stop() + + def run_realtime_inference(self, sf_enabled): + """Producer: Maintains the target FPS and updates clip IDs.""" + # Calculate a dynamic limit: tolerate 0.5 seconds of lag. + # If target_fps is 15, the limit is 7. If target_fps is 30, the limit is 15. + self.dynamic_limit = max(2, int(0.5 * self.target_fps)) + last_frame_time = time.perf_counter() + while self.active: + # --- FRAME RETRIEVAL --- + try: + device_frame, frame_num = self.reader.read() + if device_frame is None: + if self.reader is None or ( + hasattr(self.reader, "stopped") and self.reader.stopped + ): + if self.device_input == "cuda": + torch.cuda.synchronize() + self.active = False + + clip_filename = f"{self.config.SHARED_OUTPUT}/{self.name}_{self.clip_id:03d}.mp4" + clip_key = Path(clip_filename).name + global send_metadata_queue, all_metadata + if clip_key in all_metadata: + # Signal to process metadata for previous cli + if ( + "send_metadata_queue" in globals() + or "send_metadata_queue" in locals() + ): + try: + send_metadata_queue.put( + (clip_filename, self.resize_w, self.resize_h) + ) + except Exception as queue_err: + print( + f"[CLIPPER-WARN] Metadata queue push skipped or unallocated: {queue_err}", + flush=True, + ) + break + continue + except queue.Empty: + if getattr(self.reader, "reconnect_failed", False): + self.active = False + break + time.sleep(0.002) + continue + + self.stat_start_time = time.perf_counter() # timing to display detection + + # Keep 8K frame on GPU (Skip CPU conversion for non-target frames) + # if self.device_input == "cuda" and not self.reader.is_h264_8k: + # with torch.cuda.stream(self.ingest_stream): + # device_frame = nv12_to_rgb_torch( + # device_frame, self.frame_height, self.frame_width# , is_bgr=False + # ) + # self.ingest_stream.synchronize() + + # if device_frame is not None: + self.frame_count += 1 + is_target_frame = float(frame_num) >= self.next_process_idx + # if not self.config.sf_enabled or abs(float(self.input_fps) - float(self.target_fps)) < 0.01: + # is_target_frame = True + # else: + # is_target_frame = float(frame_num) >= self.next_process_idx + + # Determine if this frame should be AI or Raw based on backlog + # But ALWAYS submit to the executor to maintain frame order. + backlog = self.get_executor_backlog() + + while backlog > 4 and self.active: + time.sleep(0.005) + backlog = self.get_executor_backlog() + + def wrapped_fn(*args): + if self.device_input == "cuda": + # Ensure the worker thread switches to your targeted pipeline execution timeline + torch.cuda.set_stream(self.inference_stream) + dev_frame, f_num, target_flag, stat_start_time = args + isolated_device_frame = ( + dev_frame.clone() + if torch.is_tensor(dev_frame) + else dev_frame.copy() + ) + + self.pipeline_fn( + isolated_device_frame, f_num, target_flag, stat_start_time + ) + + # Force a non-blocking device barrier to ensure operations have fully hit VRAM + # before releasing the thread context + self.inference_stream.synchronize() + else: + dev_frame, f_num, target_flag, stat_start_time = args + isolated_device_frame = ( + dev_frame.clone() + if torch.is_tensor(dev_frame) + else dev_frame.copy() + ) + self.pipeline_fn( + isolated_device_frame, f_num, target_flag, stat_start_time + ) + + if is_target_frame: # timing to display detection + self.next_process_idx += self.step_size + # Handoff to AI and Writer + # if self.active: + # Clone the tensor buffer immediately on the producer thread + # to prevent upstream overwrite races by the next reader iteration. + # isolated_device_frame = device_frame.clone() if torch.is_tensor(device_frame) else device_frame.copy() + self.executor.submit( + # pipeline_fn, + wrapped_fn, + device_frame, + frame_num, + is_target_frame, + self.stat_start_time, + ) + # else: + # # Process background execution context for skipped frames + # self.pipeline_fn(device_frame, frame_num, is_target_frame) + + # if self.device_input == "cuda": + # torch.cuda.synchronize() + + # --- PRECISE CLOCK SYNC --- + # This prevents the producer from "lapping" the consumer + # and building that jumpy backlog in the first place. + elapsed = time.perf_counter() - last_frame_time + if elapsed < self.frame_interval: + # time.sleep(self.frame_interval - elapsed) + # Subtract a small epsilon (0.001) for OS scheduling overhead + sleep_duration = max(0, self.frame_interval - elapsed - 0.0015) + if sleep_duration > 0.001: + time.sleep(sleep_duration) + last_frame_time = time.perf_counter() + + # self.update_frame() + self.last_heartbeat = time.time() + + self.stop() + + def filter_contained_boxes(self, boxes, overlap_thresh=0.9): + """ + Vectorized IoA filter: Removes boxes if most of their area is inside another box. + """ + if boxes.shape[0] <= 1: + return boxes + + # Calculate Areas + w = (boxes[:, 2] - boxes[:, 0]).clamp(min=0) + h = (boxes[:, 3] - boxes[:, 1]).clamp(min=0) + areas = w * h + valid_mask = ( + (w < (self.resize_w * self.config.ROI_MAX_RELATIVE_SIZE_RATIO)) + & (h < (self.resize_h * self.config.ROI_MAX_RELATIVE_SIZE_RATIO)) + & (w > 0) + & (h > 0) + & (areas >= self.min_contour_area) + ) + boxes = boxes[valid_mask] + areas = areas[valid_mask] + + # Compute all-to-all Intersections [N, N] + lt = torch.max(boxes.unsqueeze(1)[:, :, :2], boxes.unsqueeze(0)[:, :, :2]) + rb = torch.min(boxes.unsqueeze(1)[:, :, 2:], boxes.unsqueeze(0)[:, :, 2:]) + wh = (rb - lt).clamp(min=0) + inter_area = wh[:, :, 0] * wh[:, :, 1] + + # Intersection over Area (How much of Box A is in Box B) + # ioa[i, j] = (Box i ∩ Box j) / Area(i) + ioa = inter_area / (areas.unsqueeze(1) + 1e-6) + + # Filter logic: + # Only remove if Box J is LARGER than Box I and overlap is high + diag = torch.eye(boxes.shape[0], device=boxes.device, dtype=torch.bool) + larger_mask = areas.unsqueeze(0) >= areas.unsqueeze(1) + + to_remove = (ioa > overlap_thresh) & larger_mask & ~diag + return boxes[~to_remove.any(dim=1)] + + # def get_detections( + # self, + # frame_raw, # BGR + # frameNum, # Added to metadata, should be frames in clip + # merged=None, + # thickness=2, + # device_input="cuda", + # ): + # metadata = {} + # try: + # # H, W = frame_raw.shape[:2] # Unpack once + # # FIX: Get H/W correctly for both Numpy [H, W, C] and Tensor [C, H, W] + # if torch.is_tensor(frame_raw): + # H, W = frame_raw.shape[-2:] # Gets last two dims + # else: + # H, W = frame_raw.shape[:2] + + # if merged is None: + # if torch.is_tensor(frame_raw): + # # Swap channels (-3 is the C dim) and normalize + # frame_input = ( + # # frame_raw.float() / 255.0 + # frame_raw.flip(-3).float() / 255.0 + # ) + # if frame_input.ndim == 3: + # frame_input = frame_input.unsqueeze(0) + # else: + # frame_input = frame_raw + + # # Run Inference (Keep stream=False as it is stable) + # results = self.run_model( + # frame_input, # Should be RGB + # imgsz=(H, W), + # batch=1, + # device_input=device_input, + # stream=False, + # ) + # else: + # cropped_batch = [] + # cropped_coords = [] + # # PREPARE CROPS (Path-specific Optimization) + # if device_input == "cuda" and torch.is_tensor(frame_raw): + # # GPU PATH: Zero-copy slicing + Hardware Interpolation + # for box in merged: + # x1, y1, x2, y2 = [int(val) for val in box] + # cropped_coords.append((x1, y1)) + # crop = frame_raw[:, y1:y2, x1:x2].unsqueeze(0) + # # crop_float = crop.float() / 255.0 + # crop_float = crop.flip(-3).float() / 255.0 + # # F.interpolate on A6000 handles 100+ crops in ~2ms + # crop_resized = F.interpolate( + # crop_float, + # size=(self.resize_h, self.resize_w), + # mode="bilinear", + # align_corners=False, + # ) + # cropped_batch.append(crop_resized.to(torch.half)) + + # # Consolidate into 4D Tensor for Parallel Hardware Batching + # input_data = ( + # torch.cat(cropped_batch, dim=0) if cropped_batch else None + # ) + # else: + # # CPU PATH: OpenCV Batching + # foi_cpu = ( + # frame_raw.permute(1, 2, 0).byte().cpu().numpy() + # if torch.is_tensor(frame_raw) + # else frame_raw + # ) + # for box in merged: + # x1, y1, x2, y2 = [int(val) for val in box] + # cropped_coords.append((x1, y1)) + # crop = foi_cpu[y1:y2, x1:x2] + # # Batching on CPU still needs resized inputs + # crop_resized = cv2.resize( + # crop, + # (self.resize_w, self.resize_h), + # interpolation=cv2.INTER_NEAREST, + # ) + # cropped_batch.append(crop_resized) + + # input_data = ( + # cropped_batch # List of arrays for OpenVINO/CPU batching + # ) + + # if cropped_batch == []: + # print("[DEBUG] Early exit: cropped_batch is empty", flush=True) + # return metadata, None + + # # if self.config.DEBUG_FLAG: + # # self.debug_save_crops(cropped_batch, frameNum) + + # # CHUNKED BATCH INFERENCE + # # Process in chunks of MODEL_MAX_BATCH_SIZE to stay within TensorRT/OpenVINO profile limits + # results = [] + # for i in range(0, len(input_data), self.config.MODEL_MAX_BATCH_SIZE): + # chunk = input_data[i : i + self.config.MODEL_MAX_BATCH_SIZE] + # chunk_results = self.run_model( + # chunk, # Should be RGB + # imgsz=(self.resize_h, self.resize_w), + # batch=len(chunk), + # device_input=device_input, + # stream=False, # stream=False is critical + # ) + # results.extend(list(chunk_results)) + + # # Process results and draw 8K-space overlays + # # Display scales for the final 640x640 stretched output + # num_objs = 0 + # scale_display_x = self.resize_w / W # 640 / 8192 + # scale_display_y = self.resize_h / H # 640 / 4608 + # for ridx, r in enumerate(list(results)): + # if r.boxes is None or len(r.boxes) == 0: + # continue + + # # Determine the ROI expansion ratio for this specific crop + # if merged is not None: + # x1_8k, y1_8k, x2_8k, y2_8k = merged[ridx] + # off_x, off_y = x1_8k, y1_8k + # # Ratio: How many 8K pixels does one inference pixel represent? + # roi_ratio_x = (x2_8k - x1_8k) / self.resize_w + # roi_ratio_y = (y2_8k - y1_8k) / self.resize_h + + # else: + # off_x, off_y = 0, 0 + # roi_ratio_x, roi_ratio_y = 1.0, 1.0 + + # # Move to CPU in one bulk operation per crop + # boxes = r.boxes.xyxy.cpu().numpy() + # clss = r.boxes.cls.cpu().numpy().astype(int) + # confs = r.boxes.conf.cpu().numpy() + + # for j in range(len(boxes)): + # num_objs += 1 + + # bx1, by1, bx2, by2 = boxes[j] + # # abs_x1, abs_y1 = off_x + bx1, off_y + by1 + # # abs_x2, abs_y2 = off_x + bx2, off_y + by2 + + # # # Map to absolute 8K pixels (Scale crop-coords to 8K then add offset) + # # abs_x1 = off_x + (bx1 * roi_ratio_x) + # # abs_y1 = off_y + (by1 * roi_ratio_y) + # # abs_x2 = off_x + (bx2 * roi_ratio_x) + # # abs_y2 = off_y + (by2 * roi_ratio_y) + + # # Map to absolute 8K pixels + # abs_x1 = off_x + (bx1 * roi_ratio_x) + # abs_y1 = off_y + (by1 * roi_ratio_y) + # abs_x2 = off_x + (bx2 * roi_ratio_x) + # abs_y2 = off_y + (by2 * roi_ratio_y) + + # # Map to 640x640 Display pixels (Apply the non-uniform stretch) + # # disp_x = int(abs_x1 * scale_display_x) + # # disp_y = int(abs_y1 * scale_display_y) + # # disp_w = int((abs_x2 - abs_x1) * scale_display_x) + # # disp_h = int((abs_y2 - abs_y1) * scale_display_y) + # disp_x = abs_x1 * scale_display_x + # disp_y = abs_y1 * scale_display_y + # disp_w = (abs_x2 - abs_x1) * scale_display_x + # disp_h = (abs_y2 - abs_y1) * scale_display_y + + # class_id = clss[j] + # class_name = self.label_source[class_id] + # confidence = confs[j] + + # if not self.config.OMIT_DETECTIONS_FLAG: + # timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + # print( + # # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", + # f"[{timestamp}] {self.name} DETECTION on Frame {frameNum}: {class_name} detected", + # flush=True, + # ) + + # # if not self.config.TEST_MODE: + # # bb_color = get_detection_color(class_id, is_bgr=True) + + # # foi = cv2.rectangle( + # # foi, + # # (abs_x1, abs_y1), + # # (abs_x2, abs_y2), + # # bb_color, + # # thickness, + # # ) + # # label = f"{class_name} {confidence:.2f}" + # # draw_label(foi, label, (abs_x1, abs_y1), color=bb_color, padding=5) + + # # height = min(abs_y2, H) - max(0, abs_y1) + # # width = min(abs_x2, W) - max(0, abs_x1) + + # # Resized + # object_res = [ + # int(disp_x), # int(abs_x1 * scale_x), + # int(disp_y), # int(abs_y1 * scale_y), + # int(disp_h), # int(height * scale_y), + # int(disp_w), # int(width * scale_x), + # class_name, + # confidence, + # int(self.resize_h), + # int(self.resize_w), + # ] + + # framenum_str = f"{frameNum:04d}_{j:04d}" + # # if self.config.DEBUG_FLAG: + # # meta_str = ",".join([str(o) for o in object_res + [framenum_str]]) + # # print(f"[{self.name} METADATA],{meta_str}", flush=True) + + # # Full Res + # metadata[framenum_str] = { + # "frameId": int(frameNum), + # "bbId": framenum_str, + # "bbox": { + # "x": int(object_res[0]), + # "y": int(object_res[1]), + # "height": int(object_res[2]), + # "width": int(object_res[3]), + # "object": str(object_res[4]), + # "object_det": { + # "confidence": float(object_res[5]), + # "frameH": int(object_res[6]), + # "frameW": int(object_res[7]), + # }, + # }, + # } + # except Exception as e: + # print(f"[GET_DETECTION] Exception: {e}\n{traceback.print_exc()}") + # num_objs = len(metadata.keys()) + + # if self.config.DEBUG_FLAG: + # log_to_logger(f"[DEBUG] get_detections returned {num_objs} detections", level="debug") + # return metadata, None + + # # # Queue frame for display (reduce quality for 8K bandwidth) + # # frame_bytes = get_display_frame_in_bytes( + # # foi, + # # display_size=(self.disp_w, self.disp_h), + # # quality=self.config.DISPLAY_FRAME_QUALITY, + # # return_bytes=True, + # # ) + + # # return metadata, frame_bytes + + def get_gpu_rois_by_area(self, mask, max_candidates=100): + # Extract true spatial constraints straight from the active mask object footprint + if torch.is_tensor(mask): + mask_h, mask_w = mask.shape[-2:] + elif isinstance(mask, cv2.cuda.GpuMat): + # cv2.cuda.GpuMat.size() returns a tuple of (width, height) standard formatting + mask_w, mask_h = mask.size() + else: + mask_h, mask_w = mask.shape[:2] + + # This prevents find_contours_gpu_equivalent from mutating the mask variables used by other threads. + if isinstance(mask, cv2.cuda.GpuMat): + # .clone() allocates a new C++ memory surface and forces full continuity + isolated_kernel_mask = mask.clone() + elif torch.is_tensor(mask): + isolated_kernel_mask = mask.clone().contiguous() + else: + isolated_kernel_mask = mask.copy() + + # Get raw boxes from mask (Direct VRAM bridge) + boxes_gpu = find_contours_gpu_equivalent( + isolated_kernel_mask, + stream=self.bgs_stream, + limit_640=640 * 1.5, + ) + + # --- FIX: ELIMINATE STREAM RACE --- + if boxes_gpu is None or len(boxes_gpu) == 0: + return torch.empty((0, 4), device=self.device_input) + + # Wrap existing GPU memory as a float tensor (Zero Copy) + # raw_boxes = torch.as_tensor(boxes_gpu, device=self.device_input).float() + if self.device_input == "cuda": + # Wrap the native device handle and IMMEDIATELY append .clone() + # This allocates a brand new, physically isolated VRAM block to secure the bounding boxes + raw_boxes = ( + torch.as_tensor(boxes_gpu, device=self.device_input).float().clone() + ) + else: + raw_boxes = torch.as_tensor(boxes_gpu, device=self.device_input).float() + + # Vectorized Pre-Filter (Removes noise blobs before merging) + w = raw_boxes[:, 2] - raw_boxes[:, 0] + h = raw_boxes[:, 3] - raw_boxes[:, 1] + mask_filter = (w * h > self.min_contour_area) & (w < mask_w) & (h < mask_h) + raw_boxes = raw_boxes[mask_filter] + + # Prevents N^2 distance matrix from exploding during high noise + if raw_boxes.shape[0] > max_candidates: + # Prioritize the largest blobs (most likely to be drones) + areas = (raw_boxes[:, 2] - raw_boxes[:, 0]) * ( + raw_boxes[:, 3] - raw_boxes[:, 1] + ) + _, indices = torch.topk(areas, max_candidates) + raw_boxes = raw_boxes[indices] + return raw_boxes + + # def get_gpu_rois(self, frame, frameNum, mask): + # raw_boxes = self.get_gpu_rois_by_area(mask) + + # if raw_boxes.shape[0] < 1: + # return torch.empty((0, 4), device=self.device_input) + + # if raw_boxes.shape[0] > 1: + # raw_boxes = merge_boxes_gpu(raw_boxes, gap_limit=self.dist_thresh_640) + + # clean_640p = self.filter_contained_boxes( + # raw_boxes, overlap_thresh=self.config.ROI_CONTAINMENT_THRESH + # ) + + # if clean_640p.shape[0] < 1: + # return torch.empty((0, 4), device=self.device_input) + + # # Scale to 8K space + # return clean_640p * self.scales_tensor + + def get_cpu_rois(self, frame, frameNum, mask): + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + raw_boxes_xywh = [ + list(cv2.boundingRect(c)) + for c in contours + if cv2.contourArea(c) > self.min_contour_area + ] + raw_boxes = [[x, y, x + w, y + h] for x, y, w, h in raw_boxes_xywh] + + if len(raw_boxes) < 1: + return torch.empty((0, 4), device=self.device_input) + + if len(raw_boxes) > 1: + raw_boxes = merge_boxes_cpu(raw_boxes, gap_limit=self.dist_thresh_640) + + raw_boxes_640p = torch.tensor(raw_boxes, device=self.device_input).float() + + clean_640p = self.filter_contained_boxes( + raw_boxes_640p, overlap_thresh=self.config.ROI_CONTAINMENT_THRESH + ) + + if clean_640p.shape[0] < 1: + return torch.empty((0, 4), device=self.device_input) + + # Scale to 8K space + return clean_640p * self.scales_tensor + + # Project clusters back to 8K and build centered YOLO windows + # final_8k_rois = scale_clusters_to_8k( + # merged_640, frame_w=self.frame_width, frame_h=self.frame_height + # ) + # return torch.tensor(final_8k_rois, device=self.device_input).float() + + def get_detections( + self, frame, frame_id, thickness=2, device_input="cuda", merged=None + ): + """ + Processes full-frame 8K targets or aggregates smart-filtered bounding-box regions + into uniform tensor arrays for batched inference execution. + """ + metadata = {} + is_cuda = device_input == "cuda" + + # ===================================================================== + # 🚀 PATH 1: FULL RESOLUTION TRACK (sf_enabled = False) + # ===================================================================== + if merged is None: + with torch.inference_mode(): + if isinstance(frame, torch.Tensor): + # # Transpose to channel-first shape format [1, C, H, W] if layout is trailing + # if frame.ndim == 3 and frame.shape[-1] == 3: + # frame = frame.permute(2, 0, 1).unsqueeze(0).clone().contiguous() + # elif frame.ndim == 3: + # frame = frame.unsqueeze(0).clone().contiguous() + + # # Create a memory-contiguous layout view and cast cleanly inside VRAM + # frame = frame.contiguous() + + # Transpose to channel-first shape format [1, C, H, W] if layout is trailing + if frame.ndim == 3 and frame.shape[-1] == 3: + # CRITICAL BUG FIX: Appending .clone().contiguous() creates a brand new, + # physically isolated tensor memory block. This breaks the link to + # temporary buffers, preventing the VS Code debugger from crashing on evaluation. + frame = frame.permute(2, 0, 1).unsqueeze(0).clone().contiguous() + elif frame.ndim == 3: + frame = frame.unsqueeze(0).clone().contiguous() + else: + # Ensure any multi-dimensional batch views are physically packed + frame = frame.clone().contiguous() + + if frame.dtype == torch.uint8: + frame = frame.to( + device_input, dtype=torch.float16, non_blocking=True + ) + frame.div_(255.0) # Safe in-place float normalization + else: + frame = frame.to(device_input, non_blocking=True) + + img_size = frame.shape[-2:] + else: + # CPU / Host NumPy NDArray fallback + img_size = frame.shape[:2] + + # img_size = (self.resize_h, self.resize_w) + H, W = img_size + scale_display_x = self.resize_w / W # 640 / 8192 + scale_display_y = self.resize_h / H # 640 / 4608 + results = self.run_model( + frame, + imgsz=img_size, + batch=1, + device_input=device_input, + stream=STREAM_ARG, + ) + + # Extract full resolution detections + if results and len(results) > 0: + boxes = results[0].boxes + if boxes is not None: + for idx, box in enumerate(boxes): + # coords = box.xywh[0].cpu().tolist() # [x_center, y_center, width, height] + # cls_id = int(box.cls[0].cpu().item()) + # conf = float(box.conf[0].cpu().item()) + coords = ( + box.xywh.cpu().squeeze().tolist() + ) # Converts [x_center, y_center, w, h] safely + cls_id = int(box.cls.cpu().item()) + class_name = self.label_source[cls_id] + confidence = float(box.conf.cpu().item()) + + # Guard against un-squeezed structural lists + if isinstance(coords[0], list): + coords = coords[0] + + # Convert center bounds coordinates back to upper-left origin layout standard + # and scale to 640x640 + disp_x = (coords[0] - (coords[2] / 2.0)) * scale_display_x + disp_y = (coords[1] - (coords[3] / 2.0)) * scale_display_y + disp_w = coords[2] * scale_display_x + disp_h = coords[3] * scale_display_y + + if disp_w > 2 and disp_h > 2: + # Resized + object_res = [ + int(disp_x), # int(abs_x1 * scale_x), + int(disp_y), # int(abs_y1 * scale_y), + int(disp_h), # int(height * scale_y), + int(disp_w), # int(width * scale_x), + class_name, + confidence, + int(self.resize_h), + int(self.resize_w), + ] + + obj_id = len(metadata) + framenum_str = f"{frame_id:04d}_{obj_id:04d}" + metadata[framenum_str] = { + "frameId": int(frame_id), + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + del frame + return metadata, None + + # ===================================================================== + # 📦 PATH 2: SMART FILTER ROIs TRACK (sf_enabled = True) + # ===================================================================== + # else: + # roi_patches = [] + # patch_coordinates = [] + # max_batch_size = getattr(self.config, "MODEL_MAX_BATCH_SIZE", 4) + + # # 1. Image Canvas Matrix Pre-processing Isolation Gauges + # if is_cuda and isinstance(frame, torch.Tensor): + # src_tensor = frame.squeeze(0) if frame.ndim == 4 else frame + # if src_tensor.shape[-1] == 3: + # src_tensor = src_tensor.permute(2, 0, 1) # Force [C, H, W] + # src_h, src_w = src_tensor.shape[-2:] + # else: + # # Host fallback tracking pointers + # src_tensor = np.asarray(frame) + # src_h, src_w = src_tensor.shape[:2] + + # # 2. Extract and Standarize Regions of Interest (ROIs) + # for box in merged: + # x1, y1, x2, y2 = map(int, box) + # x1, y1 = max(0, x1), max(0, y1) + # x2, y2 = min(src_w, x2), min(src_h, y2) + + # if (x2 - x1) < 8 or (y2 - y1) < 8: + # continue # Filter out noise slices + + # if is_cuda and isinstance(src_tensor, torch.Tensor): + # with torch.no_grad(): + # crop = src_tensor[:, y1:y2, x1:x2] + # # Bilinear scale interpolation pass on GPU hardware to achieve uniform dimensions + # if crop.shape[-2:] != (self.resize_h, self.resize_w): + # crop = F.interpolate( + # crop.unsqueeze(0).float(), + # size=(self.resize_h, self.resize_w), + # mode="bilinear", + # align_corners=False, + # ).squeeze(0) + # roi_patches.append(crop) + # else: + # # CPU Path: Crop and resize via OpenCV linear interpolation + # crop = src_tensor[y1:y2, x1:x2] + # if crop.shape[:2] != (self.resize_h, self.resize_w): + # crop = cv2.resize( + # crop, + # (self.resize_w, self.resize_h), + # interpolation=cv2.INTER_LINEAR, + # ) + # roi_patches.append(crop) + + # patch_coordinates.append((x1, y1, x2 - x1, y2 - y1)) + + # if not roi_patches: + # return metadata, None + + # # 3. Process Patches via Multi-Cam Inference Batch Factory + # results_pool = [] + # for i in range(0, len(roi_patches), max_batch_size): + # batch_slices = roi_patches[i : i + max_batch_size] + # current_batch_len = len(batch_slices) + + # if is_cuda and isinstance(batch_slices[0], torch.Tensor): + # with torch.inference_mode(): + # # Stack patches into a balanced [N, C, MODEL_H, MODEL_W] tensor matrix array + # inference_batch = torch.stack(batch_slices).to( + # device_input, dtype=torch.float16, non_blocking=True + # ) + # inference_batch.div_( + # 255.0 + # ) # Normalize directly in VRAM page boundaries + + # batch_res = self.run_model( + # inference_batch, + # imgsz=(self.resize_h, self.resize_w), + # batch=current_batch_len, + # device_input=device_input, + # stream=STREAM_ARG, + # ) + # results_pool.extend(batch_res) + # del inference_batch + # else: + # # CPU Path Processing Pass Stack + # with torch.inference_mode(): + # # Standardize NumPy array layouts into single host batch matrix block layouts + # np_batch = np.stack(batch_slices).astype(np.float32) / 255.0 + # inference_batch = ( + # torch.from_numpy(np_batch) + # .permute(0, 3, 1, 2) + # .to(device_input) + # ) + + # batch_res = self.run_model( + # inference_batch, + # imgsz=(self.resize_h, self.resize_w), + # batch=current_batch_len, + # device_input=device_input, + # stream=STREAM_ARG, + # ) + # results_pool.extend(batch_res) + # del inference_batch + + # # 4. Map Patch Bounding Boxes back onto the Global 8K Frame Coordinates Map + # scale_display_x = self.resize_w / self.frame_width # 640 / 8192 + # scale_display_y = self.resize_h / self.frame_height # 640 / 4608 + # for idx, res in enumerate(results_pool): + # ox, oy, o_width, o_height = patch_coordinates[idx] + # if res.boxes is not None: + # for b_box in res.boxes: + # lx1, ly1, lx2, ly2 = b_box.xyxy[0].cpu().tolist() + # cls_id = int(b_box.cls[0].cpu().item()) + # confidence = float(b_box.conf[0].cpu().item()) + # class_name = self.label_source[cls_id] + # # confidence = confs[j] + + # # Map relative coordinates proportionally to the original 8K ROI slice layout + # global_x1 = ox + (lx1 * (o_width / float(self.resize_w))) + # global_y1 = oy + (ly1 * (o_height / float(self.resize_h))) + # global_x2 = ox + (lx2 * (o_width / float(self.resize_w))) + # global_y2 = oy + (ly2 * (o_height / float(self.resize_h))) + # disp_x = global_x1 * scale_display_x + # disp_y = global_y1 * scale_display_y + # disp_w = (global_x2 - global_x1) * scale_display_x + # disp_h = (global_y2 - global_y1) * scale_display_y + + # if disp_w > 0 and disp_h > 0: + # # Resized + # object_res = [ + # int(disp_x), # int(abs_x1 * scale_x), + # int(disp_y), # int(abs_y1 * scale_y), + # int(disp_h), # int(height * scale_y), + # int(disp_w), # int(width * scale_x), + # class_name, + # confidence, + # int(self.resize_h), + # int(self.resize_w), + # ] + + # obj_id = len(metadata) + # framenum_str = f"{frame_id:04d}_{obj_id:04d}" + # metadata[framenum_str] = { + # "frameId": int(frame_id), + # "bbId": framenum_str, + # "bbox": { + # "x": int(object_res[0]), + # "y": int(object_res[1]), + # "height": int(object_res[2]), + # "width": int(object_res[3]), + # "object": str(object_res[4]), + # "object_det": { + # "confidence": float(object_res[5]), + # "frameH": int(object_res[6]), + # "frameW": int(object_res[7]), + # }, + # }, + # } + + # return metadata, None + # ===================================================================== + # PATH 2: SMART FILTER ROIs TRACK (sf_enabled = True) -> PADDED ASPECT PRESERVING 📦 + # ===================================================================== + else: + roi_patches = [] + patch_coordinates = [] + max_batch_size = getattr(self.config, "MODEL_MAX_BATCH_SIZE", 4) + + # 1. Canvas Matrix Pre-processing Isolation + if is_cuda and isinstance(frame, torch.Tensor): + src_tensor = frame.squeeze(0) if frame.ndim == 4 else frame + if src_tensor.shape[-1] == 3: + src_tensor = src_tensor.permute(2, 0, 1) # Force [C, H, W] + src_h, src_w = src_tensor.shape[-2:] + else: + src_tensor = np.asarray(frame) + src_h, src_w = src_tensor.shape[:2] + + # Target layout constraints + th, tw = self.resize_h, self.resize_w + + # 2. Extract, Aspect-Scale, and Pad Regions of Interest (ROIs) + for box in merged: + x1, y1, x2, y2 = map(int, box) + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(src_w, x2), min(src_h, y2) + + box_w, box_h = x2 - x1, y2 - y1 + if box_w < 8 or box_h < 8: + continue # Filter out invalid spatial artifacts + + if is_cuda and isinstance(src_tensor, torch.Tensor): + with torch.no_grad(): + crop = src_tensor[:, y1:y2, x1:x2] + + # Calculate aspect-preserving scale factor + scale = min(tw / box_w, th / box_h) + nw, nh = int(box_w * scale), int(box_h * scale) + nw, nh = max(1, nw), max(1, nh) + + # Dynamic scaling: downsample or upsample using clean bilinear grids + if (box_h, box_w) != (nh, nw): + crop_resized = F.interpolate( + crop.unsqueeze(0).float(), + size=(nh, nw), + mode="bilinear", + align_corners=False, + ).squeeze(0) + else: + crop_resized = crop.float() + + # Force the canvas tracking allocation to occur safely inside your active stream scope context + with torch.cuda.stream(self.inference_stream): + # Instantiate a clean, pre-allocated padded evaluation canvas context + padded_canvas = torch.zeros( + (3, th, tw), dtype=torch.float32, device=device_input + ) + + # Center the aspect-scaled crop onto the dark zero-padded mask grid + dx = (tw - nw) // 2 + dy = (th - nh) // 2 + padded_canvas[:, dy : dy + nh, dx : dx + nw] = crop_resized + + # Convert directly to half-precision to speed up pipeline passes + roi_patches.append(padded_canvas.to(torch.half)) + else: + # CPU Path: Aspect-preserving resize and padding via OpenCV + crop = src_tensor[y1:y2, x1:x2] + scale = min(tw / box_w, th / box_h) + nw, nh = max(1, int(box_w * scale)), max(1, int(box_h * scale)) + + crop_resized = cv2.resize( + crop, (nw, nh), interpolation=cv2.INTER_LINEAR + ) + + # Center-pad the array block with zeros (black) + padded_canvas = np.zeros((th, tw, 3), dtype=np.uint8) + dx = (tw - nw) // 2 + dy = (th - nh) // 2 + padded_canvas[dy : dy + nh, dx : dx + nw] = crop_resized + roi_patches.append(padded_canvas) + + # Store the scaling shifts to map detections back to the 8K coordinate grid accurately + patch_coordinates.append((x1, y1, box_w, box_h, scale, dx, dy)) + + if not roi_patches: + return metadata, None + + # 3. Process Patches via Multi-Cam Inference Batch Factory + results_pool = [] + for i in range(0, len(roi_patches), max_batch_size): + batch_slices = roi_patches[i : i + max_batch_size] + current_batch_len = len(batch_slices) + + if is_cuda and isinstance(batch_slices[0], torch.Tensor): + with torch.inference_mode(): + torch.cuda.set_stream(self.inference_stream) + inference_batch = torch.stack(batch_slices).to( + device_input, dtype=torch.float16, non_blocking=True + ) + inference_batch.div_(255.0) # In-place GPU normalization + + batch_res = self.run_model( + inference_batch, + imgsz=(th, tw), + batch=current_batch_len, + device_input=device_input, + stream=STREAM_ARG, + ) + results_pool.extend(batch_res) + del inference_batch + else: + with torch.inference_mode(): + np_batch = np.stack(batch_slices).astype(np.float32) / 255.0 + inference_batch = ( + torch.from_numpy(np_batch) + .permute(0, 3, 1, 2) + .to(device_input) + ) + + batch_res = self.run_model( + inference_batch, + imgsz=(th, tw), + batch=current_batch_len, + device_input=device_input, + stream=STREAM_ARG, + ) + results_pool.extend(batch_res) + del inference_batch + + # 4. Map Patch Bounding Boxes back onto the Global 8K Frame Coordinates Map + scale_display_x = tw / float(self.frame_width) + scale_display_y = th / float(self.frame_height) + + for idx, res in enumerate(results_pool): + ox, oy, o_width, o_height, scale_f, pad_x, pad_y = patch_coordinates[ + idx + ] + if res.boxes is not None and len(res.boxes) > 0: + all_xyxy = res.boxes.xyxy.cpu().numpy() + all_clss = res.boxes.cls.cpu().numpy().astype(int) + all_confs = res.boxes.conf.cpu().numpy().astype(float) + + for j in range(len(all_xyxy)): + lx1, ly1, lx2, ly2 = all_xyxy[j] + class_name = self.label_source[all_clss[j]] + confidence = all_confs[j] + + # Reverse the centering padding offset values + lx1_unpadded = lx1 - pad_x + ly1_unpadded = ly1 - pad_y + lx2_unpadded = lx2 - pad_x + ly2_unpadded = ly2 - pad_y + + # Reverse the aspect ratio scale shift to map back to absolute 8K coordinates + global_x1 = ox + (lx1_unpadded / scale_f) + global_y1 = oy + (ly1_unpadded / scale_f) + global_x2 = ox + (lx2_unpadded / scale_f) + global_y2 = oy + (ly2_unpadded / scale_f) + + # Project directly onto the 640x640 display monitoring layout canvas + disp_x = global_x1 * scale_display_x + disp_y = global_y1 * scale_display_y + disp_w = (global_x2 - global_x1) * scale_display_x + disp_h = (global_y2 - global_y1) * scale_display_y + + if disp_w > 0 and disp_h > 0: + object_res = [ + int(disp_x), + int(disp_y), + int(disp_h), + int(disp_w), + class_name, + confidence, + int(th), + int(tw), + ] + obj_id = len(metadata) + framenum_str = f"{frame_id:04d}_{obj_id:04d}" + metadata[framenum_str] = { + "frameId": int(frame_id), + "bbId": framenum_str, + "bbox": { + "x": int(object_res[0]), + "y": int(object_res[1]), + "height": int(object_res[2]), + "width": int(object_res[3]), + "object": str(object_res[4]), + "object_det": { + "confidence": float(object_res[5]), + "frameH": int(object_res[6]), + "frameW": int(object_res[7]), + }, + }, + } + return metadata, None + + # def get_gpu_rois_by_area(self, mask, max_candidates=25): #, limit_640=640*2): #max_candidates=100, limit_640=100): + # # Get raw boxes from mask (Direct VRAM bridge) + # boxes_gpu = find_contours_gpu_equivalent( + # mask, + # stream=self.bgs_stream, + # grid_size=32, + # limit_640=640*2, + # max_boxes=100, #250, + # ) # max_candidates) + + # if boxes_gpu is None or len(boxes_gpu) == 0: + # return torch.empty((0, 4), device=self.device_input) + + # # Wrap existing GPU memory as a float tensor (Zero Copy) + # raw_boxes = torch.as_tensor(boxes_gpu, device=self.device_input).float() + + # # Vectorized Pre-Filter (Removes noise blobs before merging) + # w = raw_boxes[:, 2] - raw_boxes[:, 0] + # h = raw_boxes[:, 3] - raw_boxes[:, 1] + # mask_filter = ( + # (w * h > self.min_contour_area) & (w < self.resize_w) & (h < self.resize_h) + # ) + # raw_boxes = raw_boxes[mask_filter] + + # # keep_idx = [] + # # for i in range(raw_boxes.shape[0]): + # # # 1. Convert to Python integers and calculate width/height + # # x1, y1, x2, y2 = [int(v.item()) for v in raw_boxes[i]] + # # w, h = x2 - x1, y2 - y1 + + # # if w <= 0 or h <= 0: + # # continue + + # # try: + # # # 2. Correct GpuMat ROI constructor: cv2.cuda.GpuMat(parent, (x, y, w, h)) + # # roi_mask = cv2.cuda.GpuMat(mask, (x1, y1, w, h)) + + # # # 3. Density check (white pixels / area) + # # # If density < 5%, it's likely the sparse terrain noise from your image + # # if (cv2.cuda.countNonZero(roi_mask) / (w * h)) > 0.01: + # # keep_idx.append(i) + # # except Exception: + # # # Skips boxes that might be slightly out of mask bounds + # # continue + + # # raw_boxes = raw_boxes[keep_idx] if keep_idx else torch.empty((0, 4), device=self.device_input) + + # # Prevents N^2 distance matrix from exploding during high noise + # if raw_boxes.shape[0] > max_candidates: + # # Prioritize the largest blobs (most likely to be drones) + # areas = (raw_boxes[:, 2] - raw_boxes[:, 0]) * ( + # raw_boxes[:, 3] - raw_boxes[:, 1] + # ) + # _, indices = torch.topk(areas, max_candidates) + # raw_boxes = raw_boxes[indices] + # return raw_boxes + + def get_gpu_rois(self, frame, frameNum, mask): + # If more than 20% of the screen is moving, don't bother with crops + # if current_coverage > 0.6: + # return torch.tensor([[0, 0, self.frame_width, self.frame_height]], device=self.device_input) + + limit_640 = 640 * 2 # 40 # self.config.ROI_MERGE_SIZE_LIMIT / self.scale_x + raw_boxes = self.get_gpu_rois_by_area( + mask, max_candidates=50 + ) # , limit_640=limit_640) + # padding = 5 # self.config.ROI_BB_FULL_RES_PADDING / self.scale_x + # raw_boxes[:, 0] -= padding + # raw_boxes[:, 1] -= padding + # raw_boxes[:, 2] += padding + # raw_boxes[:, 3] += padding + + if raw_boxes.shape[0] < 1: + return torch.empty((0, 4), device=self.device_input) + + if raw_boxes.shape[0] > 1: + raw_boxes = merge_boxes_gpu( + raw_boxes, + gap_limit=self.dist_thresh_640, + size_limit=limit_640, + ) + + clean_640p = self.filter_contained_boxes( + raw_boxes, overlap_thresh=self.config.ROI_CONTAINMENT_THRESH + ) + + if clean_640p.shape[0] < 1: + return torch.empty((0, 4), device=self.device_input) + + # # Scale to 8K space + # # return clean_640p * self.scales_tensor + # # margin = 0.10 + # # offsets = (clean_640p[:, 2:] - clean_640p[:, :2]) * margin + # # clean_640p[:, :2] -= offsets + # # clean_640p[:, 2:] += offsets + # # 1. Add 30-pixel 'breathing room' (in 640p space) + # # padding = 40 # self.config.ROI_BB_FULL_RES_PADDING / self.scale_x + # # clean_640p[:, 0] -= padding + # # clean_640p[:, 1] -= padding + # # clean_640p[:, 2] += padding + # # clean_640p[:, 3] += padding + + # # 2. Re-merge the padded boxes (connects nearby drones into one clean crop) + # clean_640p = merge_boxes_gpu( + # clean_640p, + # gap_limit=self.dist_thresh_640, + # size_limit=limit_640, # self.config.ROI_MERGE_SIZE_LIMIT / self.scale_x, + # ) + + # # Scale to 8K and clamp + # clean_full = clean_640p * self.scales_tensor + # # clean_full[:, [0, 2]] = clean_full[:, [0, 2]].clamp(0, self.frame_width) + # # clean_full[:, [1, 3]] = clean_full[:, [1, 3]].clamp(0, self.frame_height) + # return clean_full + + xmin = clean_640p[:, 0] + ymin = clean_640p[:, 1] + w = clean_640p[:, 2] + h = clean_640p[:, 3] + + xmax = xmin + w + ymax = ymin + h + + # Stack into standard format layout [xmin, ymin, xmax, ymax] + standard_boxes = torch.stack([xmin, ymin, xmax, ymax], dim=1) + + # 4. Scale to absolute 8K workspace dimensions accurately + return standard_boxes * self.scales_tensor + + # def pipeline_fn(self, device_frame, overall_frame_num, is_target_frame): + # # ─── LAZY CUDA STREAM INITIALIZATION FOR PROCESS ISOLATION ─── + # # Ensures streams are bound directly to the active executing process memory space + # if getattr(self, "device_input", "cpu") == "cuda": + # if not hasattr(self, "stream") or self.stream is None: + # self.stream = cv2.cuda.Stream() + # if not hasattr(self, "bgs_stream") or self.bgs_stream is None: + # self.bgs_stream = cv2.cuda.Stream() + # # ───────────────────────────────────────────────────────────── + + # global all_metadata + # current_clip_id = self.clip_id + # current_clip_key = f"{self.name}_{current_clip_id:03d}.mp4" + # current_clip_path = f"{self.config.SHARED_OUTPUT}/{current_clip_key}" + # # --- MOTION MASK GENERATION GATE --- + # if self.config.sf_enabled: + # if self.device_input == "cuda": + # inf_data = self.rbtd_full_gpu(device_frame) + # torch.cuda.current_stream().synchronize() + # if isinstance(inf_data, dict) and "mask" in inf_data: + # if torch.is_tensor(inf_data["mask"]): + # inf_data["mask"] = inf_data["mask"].contiguous() + # else: + # inf_data = self.rbtd_full_cpu(device_frame) + # else: + # inf_data = {} + + # # --- PIPELINE AT TARGET RATE --- + # if not is_target_frame: + # return + + # # if is_target_frame: + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 # 1-indexed + # self.frame_in_clip_count += 1 + # inf_data["frameNum"] = self.frame_count_target + + # # --- CLIP GENERATION --- + # if self.config.ENABLE_QUERYING: + # if self.config.DEBUG_FLAG and ( + # self.frame_in_clip_count % 15 == 0 or self.frame_in_clip_count == 1 + # ): + # print( + # f"[CLIPPER] Frame progress tracking index: {self.frame_in_clip_count}/{self.max_frames_per_clip} (Overall Frame: {overall_frame_num})", + # flush=True, + # ) + + # self.prep_frame_for_video(device_frame, overall_frame_num) + + # if self.frame_in_clip_count > self.max_frames_per_clip: + # global clip_completion_tracker + # if current_clip_key not in clip_completion_tracker: + # clip_completion_tracker[current_clip_key] = { + # "video": False, + # "meta": False, + # "start_time": time.time(), + # } + + # clip_completion_tracker[current_clip_key]["meta"] = True + # print( + # f" [BARRIER-SEAL] All metadata extracted for {current_clip_key}. Evaluating convergence...", + # flush=True, + # ) + # self._evaluate_barrier_and_dispatch( + # current_clip_key, + # current_clip_path, + # self.resize_w, + # self.resize_h, + # ) + + # self.start_new_clip() + + # if not self.config.DISABLE_DETECTION: + # # --- FULL-RESOLUTION ROI EXTRACTION MAPS --- + # bbs_full_res = None + # if self.config.sf_enabled: + # if self.device_input == "cuda": + # bbs_full_res = self.get_gpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + # else: + # bbs_full_res = self.get_cpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + + # # Isolate raw coordinate matrices out of device graphs to prevent exit race conditions + # # clean_bbs = [] + # # if self.config.sf_enabled and bbs_full_res is not None: + # # if torch.is_tensor(bbs_full_res): + # # clean_bbs = bbs_full_res.detach().cpu().tolist() + # # elif isinstance(bbs_full_res, list): + # # clean_bbs = [ + # # b.detach().cpu().tolist() if torch.is_tensor(b) else b + # # for b in bbs_full_res + # # ] + # # else: + # # clean_bbs = bbs_full_res + # clean_bbs = [] + # if self.config.sf_enabled and bbs_full_res is not None: + # if torch.is_tensor(bbs_full_res): + # clean_bbs = bbs_full_res.detach().cpu().numpy() + # else: + # clean_bbs = np.array(bbs_full_res) + + # # print(f"[DEBUG] {current_clip_key}: {len(clean_bbs)} ROIs detected!") + + # if self.config.DETECTION_TYPE != "motion": + # # Object Mode: Run YOLO and prepare metadata + # det_frame = ( + # inf_data["full_frame"] + # if "full_frame" in inf_data + # else device_frame + # ) # RGB + # merged = clean_bbs if self.config.sf_enabled else None + # # num_bbs = 0 if merged is None else len(clean_bbs) + # # print(f"[DEBUG] {current_clip_key} 'merged' num bbs: {num_bbs}") + # metadata, _ = self.get_detections( + # det_frame, + # self.frame_in_clip_count, # self.frame_count_target, + # merged=merged, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # if self.config.DEBUG_FLAG: + # meta_keys = ", ".join(list(metadata.keys())) + # print( + # f"[DEBUG] {current_clip_key} metadata keys: {meta_keys}", + # flush=True, + # ) + # if current_clip_key not in all_metadata: + # all_metadata[current_clip_key] = {"object": {}, "face": {}} + + # all_metadata[current_clip_key]["object"].update(metadata) + # # data_to_draw = metadata + + # # print(f"Sending to queue", flush=True) + + # display_source = ( + # inf_data["full_frame"] + # if (inf_data and "full_frame" in inf_data) + # else device_frame + # ) + + # if self.device_input == "cuda": + # gpu_resized = F.interpolate( + # display_source.unsqueeze(0).float(), + # size=(self.disp_h, self.disp_w), + # mode="bilinear", + # align_corners=False, + # ).squeeze(0).contiguous() + # disp_frame = np.copy( + # tensor2opencv( + # gpu_resized, self.config.device_input, is_bgr=True + # ) + # ) + # else: + # cpu_resized = cv2.resize(device_frame, (self.disp_w, self.disp_h)) + # disp_frame = np.copy( + # tensor2opencv( + # cpu_resized, self.config.device_input, is_bgr=True + # ) + # ) + + # data_to_draw = ( + # clean_bbs if self.config.DETECTION_TYPE == "motion" else metadata + # ) + + # # PUSH FRAME UNCONDITIONALLY: Ensures the test encoder gets raw frame tokens + # try: + # self.render_queue.put_nowait( # put( + # ( + # disp_frame, + # inf_data["frameNum"], + # data_to_draw, + # self.label_source, + # ) + # ) + # except queue.Full: + # pass + + # self.update_frame() + + def pipeline_fn( + self, device_frame, overall_frame_num, is_target_frame, stat_start_time + ): + global all_metadata + current_clip_id = self.clip_id + current_clip_key = f"{self.name}_{current_clip_id:03d}.mp4" + current_clip_path = f"{self.config.SHARED_OUTPUT}/{current_clip_key}" + # --- MOTION MASK GENERATION GATE --- + if self.config.sf_enabled: + if self.device_input == "cuda": + inf_data = self.rbtd_full_gpu(device_frame) + # torch.cuda.current_stream().synchronize() + # if isinstance(inf_data, dict) and "mask" in inf_data: + # if torch.is_tensor(inf_data["mask"]): + # inf_data["mask"] = inf_data["mask"].contiguous() + # inf_data = self.rbtd_full_gpu(device_frame) + else: + inf_data = self.rbtd_full_cpu(device_frame) + else: + inf_data = {} + + # --- PIPELINE AT TARGET RATE --- + if not is_target_frame: + return + + # if is_target_frame: + # self.next_process_idx += self.step_size + self.frame_count_target += 1 # 1-indexed + self.frame_in_clip_count += 1 + inf_data["frameNum"] = self.frame_count_target + + # --- CLIP GENERATION --- + if self.config.ENABLE_QUERYING: + if self.config.DEBUG_FLAG and ( + self.frame_in_clip_count % 15 == 0 or self.frame_in_clip_count == 1 + ): + print( + f"[CLIPPER] Frame progress tracking index: {self.frame_in_clip_count}/{self.max_frames_per_clip} (Overall Frame: {overall_frame_num})", + flush=True, + ) + + self.prep_frame_for_video(device_frame, overall_frame_num) + + if self.frame_in_clip_count > self.max_frames_per_clip: + global clip_completion_tracker + if current_clip_key not in clip_completion_tracker: + clip_completion_tracker[current_clip_key] = { + "video": False, + "meta": False, + "start_time": time.time(), + } + + clip_completion_tracker[current_clip_key]["meta"] = True + print( + f" [BARRIER-SEAL] All metadata extracted for {current_clip_key}. Evaluating convergence...", + flush=True, + ) + self._evaluate_barrier_and_dispatch( + current_clip_key, + current_clip_path, + self.resize_w, + self.resize_h, + ) + + self.start_new_clip() + + if not self.config.DISABLE_DETECTION: + # --- FULL-RESOLUTION ROI EXTRACTION MAPS --- + bbs_full_res = None + if self.config.sf_enabled: + if self.device_input == "cuda": + bbs_full_res = self.get_gpu_rois( + inf_data["full_frame"], + self.frame_count_target, + inf_data["mask"], + ) + else: + bbs_full_res = self.get_cpu_rois( + inf_data["full_frame"], + self.frame_count_target, + inf_data["mask"], + ) + + # Isolate raw coordinate matrices out of device graphs to prevent exit race conditions + clean_bbs = [] + if self.config.sf_enabled and bbs_full_res is not None: + if torch.is_tensor(bbs_full_res): + clean_bbs = bbs_full_res.detach().cpu().numpy() + else: + clean_bbs = np.array(bbs_full_res) + + # print(f"[DEBUG] {current_clip_key}: {len(clean_bbs)} ROIs detected!") + + if self.config.DETECTION_TYPE != "motion": + # Object Mode: Run YOLO and prepare metadata + if "full_frame" in inf_data: + det_frame = inf_data["full_frame"] + else: + det_frame = ( + device_frame.clone() + if torch.is_tensor(device_frame) + else device_frame.copy() + ) + + merged = clean_bbs if self.config.sf_enabled else None + # num_bbs = 0 if merged is None else len(clean_bbs) + # print(f"[DEBUG] {current_clip_key} 'merged' num bbs: {num_bbs}") + metadata, _ = self.get_detections( + det_frame, + self.frame_in_clip_count, # self.frame_count_target, + merged=merged, + thickness=self.config.THICKNESS, + device_input=self.config.device_input, + ) + if self.config.DEBUG_FLAG: + meta_keys = ", ".join(list(metadata.keys())) + print( + f"[DEBUG] {current_clip_key} metadata keys: {meta_keys}", + flush=True, + ) + if current_clip_key not in all_metadata: + all_metadata[current_clip_key] = {"object": {}, "face": {}} + + all_metadata[current_clip_key]["object"].update(metadata) + # data_to_draw = metadata + + # print(f"Sending to queue", flush=True) + + display_source = ( + inf_data["full_frame"] + if (inf_data and "full_frame" in inf_data) + else device_frame + ) + + if self.device_input == "cuda": + gpu_resized = ( + F.interpolate( + display_source.unsqueeze(0).float(), + size=(self.disp_h, self.disp_w), + mode="bilinear", + align_corners=False, + ) + .squeeze(0) + .contiguous() + ) + disp_frame = np.copy( + tensor2opencv(gpu_resized, self.config.device_input, is_bgr=True) + ) + else: + cpu_resized = cv2.resize(device_frame, (self.disp_w, self.disp_h)) + disp_frame = np.copy( + tensor2opencv(cpu_resized, self.config.device_input, is_bgr=True) + ) + + data_to_draw = ( + clean_bbs if self.config.DETECTION_TYPE == "motion" else metadata + ) + + # PUSH FRAME UNCONDITIONALLY: Ensures the test encoder gets raw frame tokens + try: + if ( + hasattr(self, "render_queue") + and getattr(self, "render_queue", None) is not None + # and not self.render_queue.full() + ): + self.render_queue.put( # put( put_nowait + ( + disp_frame, + inf_data["frameNum"], + data_to_draw, + self.label_source, + ) + ) + except queue.Full: + pass + + self.update_frame(stat_start_time) + + # VIDEO CLIPPING + def start_new_clip(self): + """ + Seals the current AI tracking state layout and safely moves the instance metadata references + to the next sequential file block segment index. + """ + global clip_completion_tracker, all_metadata + + # Capture context pointers prior to counter mutation steps + old_clip_id = self.clip_id + old_clip_key = f"{self.name}_{old_clip_id:03d}.mp4" + # old_clip_path = f"{self.config.SHARED_OUTPUT}/{old_clip_key}" + + print( + f" [CLIPPER] Rotating AI context engine tracking timeline layer. Sealing metadata for: {old_clip_key}", + flush=True, + ) + + # Seal the AI processing side of the tracker + # if old_clip_key not in clip_completion_tracker: + # clip_completion_tracker[old_clip_key] = {"video": False, "meta": False, "start_time": time.time()} + + # clip_completion_tracker[old_clip_key]["meta"] = True + + # # Evaluate the barrier in case the video segment completed before the AI loop reached this gate + # self._evaluate_barrier_and_dispatch(old_clip_key, old_clip_path, self.resize_w, self.resize_h) + + # Mutate tracker instance metrics parameters for the upcoming segment chunk window + self.clip_id += 1 + self.frame_in_clip_count = 1 + self._check_shm_safety(threshold_percent=90) + + log_to_logger( + f"New clip created: clip frame {self.frame_in_clip_count} ({self.frame_count_target})) of {self.max_frames_per_clip}", + level="info", + ) + + def prep_frame_for_video(self, device_frame, frame_num): + # Stops the handler from starting zombie threads during stop() flushes + if not self.active or self._is_stopped: + return + + if not hasattr(self, "write_queue") or self.write_queue is None: + print( + " [CLIPPER-INIT] Missing write_queue footprint. Provisioning runtime workspace buffer...", + flush=True, + ) + self.write_queue = queue.Queue(maxsize=300) + self.writer_done = False + + if not self.config.TEST_MODE and ( + not hasattr(self, "send_metadata_queue") or self.send_metadata_queue is None + ): + print( + " [CLIPPER-INIT] Binding instance metadata reference array layer dynamically...", + flush=True, + ) + self.send_metadata_queue = queue.Queue() + + if not hasattr(self, "stop_writer") or self.stop_writer is None: + self.stop_writer = threading.Event() + + if ( + not hasattr(self, "writer_thread") + or self.writer_thread is None + or not self.writer_thread.is_alive() + ): + print( + " [CLIPPER-INIT] Target worker runtime thread is offline. Provisioning core consumer loop thread...", + flush=True, + ) + self.writer_thread = threading.Thread( + target=self.video_writer_core_loop, + args=(self.stop_writer,), + daemon=True, + ) + self.writer_thread.start() + + if getattr(self, "video_writer", None) is None: + print( + " [CLIPPER-INIT] Downstream execution handle is blank. Initializing FFmpeg subprocess daemon...", + flush=True, + ) + self._initialize_writer() + + self.clip_executor.submit(self._async_clipper_worker, device_frame, frame_num) + + def _async_clipper_worker(self, device_frame, frame_num): + try: + if self.device_input == "cuda": + with torch.cuda.stream(self.processing_stream): + if device_frame.shape[-1] == 3: + gpu_ch_first = device_frame.permute(2, 0, 1).contiguous() + else: + gpu_ch_first = device_frame.contiguous() + + self.gpu_float_staging[0, :, :, :].copy_( + gpu_ch_first, non_blocking=True + ) + + gpu_resized = F.interpolate( + self.gpu_float_staging, + size=(self.resize_h, self.resize_w), + mode="bilinear", + align_corners=False, + ).squeeze(0) + + gpu_final = gpu_resized.clamp(0, 255).to(torch.uint8) + gpu_contiguous = gpu_final.permute(1, 2, 0).contiguous() + + active_tensor = self.pinned_tensors[self.gpu_ring_idx] + active_tensor.copy_(gpu_contiguous, non_blocking=True) + + if self.slot_events is not None: + self.slot_events[self.gpu_ring_idx].record(self.processing_stream) + + self.write_queue.put( + { + "ring_slot_idx": self.gpu_ring_idx, + "frame_num": frame_num, + "pipe_handle": self.video_writer, + } + ) + self.gpu_ring_idx = (self.gpu_ring_idx + 1) % self.ring_depth + else: + active_matrix = self.pinned_matrices[self.cpu_ring_idx] + cv2.resize( + device_frame, + (self.resize_w, self.resize_h), + dst=active_matrix, + interpolation=cv2.INTER_LINEAR, + ) + + self.write_queue.put( + { + "ring_slot_idx": self.cpu_ring_idx, + "frame_num": frame_num, + "pipe_handle": self.video_writer, + } + ) + self.cpu_ring_idx = (self.cpu_ring_idx + 1) % self.ring_depth + + except Exception as e: + print( + f"[CRITICAL-CLIPPER-WORKER] Resizing execution loop dropped: {e}", + flush=True, + ) + traceback.print_exc() + + def _initialize_writer(self): + """ + Spawns a persistent background FFmpeg subprocess with native segment-splitting + capabilities, entirely bypassing the high-overhead Python-side cv2.VideoWriter lifecycle. + """ + # self.clip_filename_pattern = f"{self.config.SHARED_OUTPUT}/{self.name}_%03d.mp4" + # self.clip_key = f"{self.name}_{self.clip_id:03d}.mp4" + + # Safe parameter array construction passed directly to kernel, avoiding subshell expansion failures + clip_duration = int(self.config.CLIP_DURATION) + ffmpeg_args = [ + "ffmpeg", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "bgr24", + "-s", + f"{self.resize_w}x{self.resize_h}", + "-r", + str(int(self.target_fps)), + "-i", + "-", + "-c:v", + "libx264", #"mpeg4", # Or "libx264" if you prefer H.264 + "-crf","23", + "-f","mpegts","-movflags","faststart", + "-force_key_frames", + f"expr:gte(t,n_forced*{clip_duration})", + "-f", + "segment", + "-segment_time", + f"{clip_duration}", + "-reset_timestamps", + "1", + "-segment_format", + "mp4", + # CRITICAL: Force fragmented headers so every chunk has a valid moov atom instantly + "-segment_format_options", + "movflags=frag_keyframe+empty_moov+default_base_moof", + self.clip_filename_pattern, + ] + + # print(f" [FFMPEG-INIT] Spawning binary pipeline targeted at: {self.clip_filename_pattern}", flush=True) + + try: + # log_dir = "/home/logs" + # os.makedirs(log_dir, exist_ok=True) + # err_log = open(f"{log_dir}/ffmpeg_handler_{self.name}.log", "w") + + self.ffmpeg_proc = subprocess.Popen( + ffmpeg_args, + shell=False, # Secure token delivery + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + # stderr=err_log, + stderr=subprocess.PIPE, # Connected directly to high-speed RAM string buffer streaming + text=False, + bufsize=0, + ) + self.video_writer = self.ffmpeg_proc.stdin + print( + " [FFMPEG-INIT] Subprocess online. Log stream parser engine initialization sequencing...", + flush=True, + ) + + # Fire memory loop monitor + self.log_parser_thread = threading.Thread( + target=self._ffmpeg_log_parser_loop, daemon=True + ) + self.log_parser_thread.start() + + log_to_logger( + f"Persistent native-segmenting FFmpeg writer spawned for stream: {self.name}", + level="info", + ) + except Exception as e: + print( + f"[CRITICAL-FFMPEG] Process spawn aborted at kernel boundary: {e}", + flush=True, + ) + self.video_writer = None + + def _ffmpeg_log_parser_loop(self): + """ + Memory-piped console stream parser. Intercepts segment generation boundary + milestone markers straight out of VRAM/RAM buffers with zero disk I/O cost. + """ + global clip_completion_tracker + + while self.active and self.ffmpeg_proc and self.ffmpeg_proc.stderr: + try: + raw_line = self.ffmpeg_proc.stderr.readline() + if not raw_line: + break + except (ValueError, OSError): + # Safely intercept when stop() forcefully closes the pipe out-of-band + break + line = raw_line.decode("utf-8", errors="ignore") + + if "Opening '" in line and "' for writing" in line: + try: + # Isolate text target strings direct from streaming arrays + parts = line.split("Opening '") + if len(parts) > 1: + full_path = parts[1].split("' for writing")[0] + target_filename = os.path.basename(full_path) + + # Parsing string names means the *previous* segment index is finished writing to disk! + # Deduce the previous filename key string mathematically + try: + name_part, ext_part = os.path.splitext(target_filename) + prefix, index_str = name_part.rsplit("_", 1) + prev_index = int(index_str) - 1 + if prev_index >= 0: + completed_clip_key = ( + f"{prefix}_{prev_index:03d}{ext_part}" + ) + completed_clip_path = ( + f"{self.config.SHARED_OUTPUT}/{completed_clip_key}" + ) + + # ========================================================================= + # 🛡️ HARD FLUSH & CLOSE GUARD: Wait for OS File Handlers to Stabilize + # ========================================================================= + file_stable = False + timeout_gate = 3.0 # Cap the wait window at 3 seconds to avoid blocking AI pipelines + start_sync_time = time.time() + last_size = -1 + + while (time.time() - start_sync_time) < timeout_gate: + if os.path.exists(completed_clip_path): + try: + current_size = os.path.getsize( + completed_clip_path + ) + # Ensure the file isn't empty AND its byte size has stopped fluctuating + if ( + current_size > 0 + and current_size == last_size + ): + # Final security check: Test if we can open the file exclusively + # (Confirms FFmpeg or OS has released its write-lock completely) + with open( + completed_clip_path, "rb+" + ) as _: + pass + file_stable = True + break + last_size = current_size + except IOError: + # File is still locked by the OS writer, loop and wait + pass + time.sleep( + 0.05 + ) # Rest the polling thread to preserve CPU cycles + + if not file_stable: + print( + f" [PARSER-WARN] IO Flush timeout exceeded for {completed_clip_key}. Forcing dispatch anyway.", + flush=True, + ) + # ========================================================================= + + # Atomic barrier lookup operation update + if completed_clip_key not in clip_completion_tracker: + clip_completion_tracker[completed_clip_key] = { + "video": False, + "meta": False, + "start_time": time.time(), + } + + clip_completion_tracker[completed_clip_key]["video"] = ( + True + ) + if self.config.DEBUG_FLAG: + print( + f"[PARSER] Memory pipe intercepted completed segment confirmation: {completed_clip_key}", + flush=True, + ) + + # Run convergence evaluation pass + self._evaluate_barrier_and_dispatch( + completed_clip_key, + completed_clip_path, + self.resize_w, + self.resize_h, + ) + except Exception as calc_err: + print( + f"[PARSER-WARN] Index calculation lookback anomaly skipped: {calc_err}", + flush=True, + ) + + except Exception as parse_err: + print( + f"[PARSER-ERROR] Failed to extract target token patterns out of logging stream: {parse_err}", + flush=True, + ) + continue + if self.config.DEBUG_FLAG: + print( + " [LOG-PARSER] Memory loop pipe interface closed down smoothly.", + flush=True, + ) + + def video_writer_core_loop(self, stop_evt): + """ + Thread-safe background min-heap consumer with adaptive sequence hole recovery. + """ + print( + " [WRITER-LOOP] Background tracking consumer loop active and polling memory queues...", + flush=True, + ) + try: + while not stop_evt.is_set() or not self.write_queue.empty(): + try: + data = None + try: + data = self.write_queue.get(timeout=0.02) + except (queue.Empty, AttributeError): + continue + + if data is None: + continue + + control_data = data.get("control") + if control_data == "FLUSH": + print( + " [WRITER-LOOP] Intercepted downstream engine flush token code signature.", + flush=True, + ) + if "pipe_handle" in data and data["pipe_handle"]: + try: + data["pipe_handle"].close() + except Exception: + pass + self.write_queue.task_done() + continue + + slot_target = data.get("ring_slot_idx") + sock_handle = data.get("pipe_handle") + # frame_num = data.get("frame_num") + + if ( + sock_handle is None + and getattr(self, "video_writer", None) is not None + ): + sock_handle = self.video_writer + + if slot_target is not None and sock_handle is not None: + if self.device_input == "cuda" and self.slot_events is not None: + self.slot_events[slot_target].synchronize() + + if ( + self.ffmpeg_proc is None + or self.ffmpeg_proc.poll() is not None + ): + print( + " [WRITER-WARN] Downstream execution loop pipe was broken out-of-band. Launching recovery routine...", + flush=True, + ) + self._initialize_writer() + sock_handle = self.video_writer + if sock_handle is None: + self.write_queue.task_done() + continue + + try: + raw_buffer_view = memoryview( + self.pinned_matrices[slot_target] + ) + sock_handle.write(raw_buffer_view) + sock_handle.flush() + + except (OSError, ValueError) as pipe_err: + print( + f" [PIPE-ERROR] Write operation dropped on ring index slot {slot_target}: {pipe_err}", + flush=True, + ) + pass + + self.write_queue.task_done() + else: + self.write_queue.task_done() + + except Exception as e: + print( + f"[WRITER-EXCEPTION] Worker engine cycle processing failure: {e}", + flush=True, + ) + continue + + try: + if hasattr(self, "socket_path") and os.path.exists(self.socket_path): + os.remove(self.socket_path) + except Exception: + pass + + self.writer_done = True + print( + " [WRITER-LOOP] Thread pool queue completely drained. Processing safe exit termination sequence...", + flush=True, + ) + + except Exception as fatal_err: + print( + f"[FATAL-WRITER-CRASH] Unhandled background crash: {fatal_err}", + flush=True, + ) + traceback.print_exc() + + def _evaluate_barrier_and_dispatch(self, clip_key, clip_path, frame_w, frame_h): + """ + Synchronized atomic thread barrier. Evaluates component convergence and + dispatches the unified media asset payload directly to VDMS ingestion queues. + """ + global clip_completion_tracker, all_metadata, send_metadata_queue + + if clip_key not in clip_completion_tracker: + clip_completion_tracker[clip_key] = { + "video": False, + "meta": False, + "start_time": time.time(), + } + + tracker = clip_completion_tracker[clip_key] + + # Check convergence layout: Trigger upload sequence only if both pipelines sealed operations + if tracker["video"] and tracker["meta"]: + if self.config.DEBUG_FLAG: + print( + f" [BARRIER-CONVERGENCE] Fully synchronized state reached for asset: {clip_key}", + flush=True, + ) + + # Extract and unmap metadata tracking payloads safely from shared RAM memory space + clip_metadata = all_metadata.pop(clip_key, None) + clip_completion_tracker.pop(clip_key, None) + + if not self.config.TEST_MODE and clip_metadata: + # Re-insert the fully constructed framework into the active VDMS queue worker pool + if ( + hasattr(self, "send_metadata_queue") + and self.send_metadata_queue is not None + ): + self.send_metadata_queue.put((clip_path, frame_w, frame_h)) + else: + global send_metadata_queue + send_metadata_queue.put((clip_path, frame_w, frame_h)) + + print( + f" [BARRIER-INGEST] Unified data packages successfully submitted for DB processing: {clip_key}", + flush=True, + ) + elif not self.config.TEST_MODE: + print( + f" [BARRIER-WARN] Synchronization completed but all_metadata structure for {clip_key} was empty!", + flush=True, + ) + elif not self.config.TEST_MODE: + waiting_on = ( + "video segment closure" + if not tracker["video"] + else "AI frame processing execution" + ) + print( + f" [BARRIER-WAIT] {clip_key}: Milestone checked. Awaiting {waiting_on} before issuing DB ingestion call.", + flush=True, + ) + + +class GPUStreamHandler(DeviceBaseHandler): + def allocate_gpu(self): + """ + Allocates persistent GpuMat buffers and CUDA streams to + enable zero-copy GPU processing. + """ + self.stream = cv2.cuda.Stream() + self.ingest_stream = torch.cuda.Stream() + self.inference_stream = torch.cuda.Stream() + self.bgs_stream = cv2.cuda.Stream() + self.gpu_fullres_frame = cv2.cuda.GpuMat( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + # self.resized_gpumat = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC3) + self.resized_frame = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC3) + self.resized_frame.setTo(0, self.bgs_stream) + self.fgMask = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + self.prev_bkgd = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + if self.config.BKGD_SUB_INCLUDE_HISTORY_METHOD == "and": + self.prev_bkgd.setTo((1,)) + else: + self.prev_bkgd.setTo((0,)) + self.mask_history = deque( + maxlen=self.config.BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE + ) + self.mask_history.append(self.prev_bkgd) + self.gpu_threshold_dst_frame = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + self.gpu_morphed_frame = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + + self.upload_stream = cv2.cuda.Stream() + self.upload_event = cv2.cuda.Event() + + # self.queue_capacity = int(2 * self.target_fps) # 60 + self.num_buffers = int(2 * self.target_fps) # self.queue_capacity + 5 + self.gpu_buffer_pool = [ + cv2.cuda.GpuMat(self.frame_height, self.frame_width, cv2.CV_8UC3) + for _ in range(self.num_buffers) + ] + self.buffer_idx = 0 + + self.frame_buffer_pool = [ + torch.empty( + (3, self.frame_height, self.frame_width), + dtype=torch.uint8, + device="cuda", + ) + for _ in range(2) + ] + self.pool_idx = 0 + + # Create a matching pool of pinned host memory for the 8K frames + self.host_buffer_pool = [ + cv2.cuda.HostMem(self.frame_height, self.frame_width, cv2.CV_8UC3) + for _ in range(self.num_buffers) + ] + + # Create continuous buffers to prevent stride artifacts during 8K downloads + self.pinned_downloaded_resizedframe_np = cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC3 + ) + self.pinned_downloaded_frame_np = cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_threshold_dst_frame + ) + cv2.cuda.createContinuous( + self.resize_h, self.resize_w, cv2.CV_8UC1, self.gpu_morphed_frame + ) + + # This prevents the AI thread from overwriting the encoder's data. + self.gpu_encoder_8k_buf = cv2.cuda.createContinuous( + self.frame_height, self.frame_width, cv2.CV_8UC3 + ) + + # Continuous allocation prevents stride/padding artifacts + self.gpu_display_frame = cv2.cuda.createContinuous( + self.disp_h, self.disp_w, cv2.CV_8UC3 + ) + + # Create a dedicated background stream for encoding tasks + self.encode_stream = cv2.cuda.Stream() + + def prepare_gpu_pipeline(self): + self.operation_device_map = PipelineMapping( + resize_device="gpu", + bkgd_subtraction_device="gpu", + threshold_device="gpu", + erodeAndDilate_device="gpu", + detection_device="gpu", + ) # rbtd_detection_gpu + + self.device_input = "cuda" + + self.allocate_gpu() + + # Subtraction + # history = int(2 * self.target_fps) # 300 # int(5 * self.target_fps) + self.lr = self.config.BKGD_SUB_MOG2_LR # 1 / history + self.backSub = cv2.cuda.createBackgroundSubtractorMOG2( + history=self.config.BKGD_SUB_MOG2_HISTORY, # Clear ghosts of fast drones in ~2 seconds (2*fps) + varThreshold=int( + 1.15 * self.config.BKGD_SUB_MOG2_VARTHRESHOLD + ), # High threshold to ignore "shimmer" and compression noise # default 16 + # CUDA implementation of MOG2 often requires a higher varThreshold to achieve the same "cleanliness" as the CPU (15-20%) + detectShadows=self.config.BKGD_SUB_MOG2_DETECTSHADOWS, # default True + ) + + self.dilate_filter = cv2.cuda.createMorphologyFilter( + cv2.MORPH_DILATE, cv2.CV_8U, self.dilate_kernel + ) + self.dilate_filter_for_enhanced_mask = cv2.cuda.createMorphologyFilter( + cv2.MORPH_DILATE, cv2.CV_8UC1, self.dilate_kernel_for_enhanced_mask + ) + # # self.morph_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + # self.morph_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) + # self.morph_filter = cv2.cuda.createMorphologyFilter( + # cv2.MORPH_DILATE, cv2.CV_8UC1, self.morph_kernel + # ) + # self.labels_gpu = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_32S) + self.labels_gpu = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8U) + self.labels_gpu.setTo(0, self.bgs_stream) + + def gpu_warmup(self): + # WARM UP (Crucial for first-run latency) + # JIT kernels are compiled on the first call + h, w = self.resize_h, self.resize_w + + self.gpu_warmup_input_frame_np = cv2.cuda.createContinuous(h, w, cv2.CV_8UC3) + + if self.gpu_warmup_input_frame_np is not None: + self.gpu_warmup_input_frame_np[:] = [255, 0, 0] + + gpu_warmup_input_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + gpu_warmup_input_frame.upload(self.gpu_warmup_input_frame_np) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_warmup_input_frame) + + # Trigger compiler + gpu_warmup_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_warmup_frame) + + cv2.cuda.resize( + gpu_warmup_input_frame, + (self.resize_w, self.resize_h), + stream=self.stream, + dst=gpu_warmup_frame, + interpolation=cv2.INTER_NEAREST, + ) + # Thresholding + gpu_threshold_dst_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_threshold_dst_frame) + cv2.cuda.threshold( + gpu_warmup_frame, + self.config.THRESHOLD_VALUE, + self.config.THRESHOLD_MAX_VALUE, + cv2.THRESH_BINARY, + gpu_threshold_dst_frame, + self.stream, + ) + + gpu_morphed_frame = cv2.cuda.GpuMat(h, w, cv2.CV_8U) + cv2.cuda.createContinuous(h, w, cv2.CV_8U, gpu_morphed_frame) + self.dilate_filter.apply( + gpu_threshold_dst_frame, gpu_morphed_frame, self.stream + ) + self.stream.waitForCompletion() + + torch.cuda.empty_cache() + + def cleanup_gpu(self): + """ + Explicitly releases all GPU-allocated memory to prevent + VRAM leaks in 8K concurrent streams. + """ + # Iterate through class attributes to explicitly release VRAM. + for attr_name in list(self.__dict__.keys()): + attr_value = getattr(self, attr_name) + + # Check if the attribute is a GpuMat + if isinstance(attr_value, cv2.cuda.GpuMat): + # 🏎️ Force the NVIDIA driver to deallocate this specific memory segment + attr_value.release() + setattr(self, attr_name, None) + print(f"✅ Released GpuMat: {attr_name}") + + if hasattr(self, "gpu_fullres_frame") and self.gpu_fullres_frame is not None: + try: + self.gpu_fullres_frame.release() + except Exception: + self.gpu_fullres_frame = None + + if hasattr(self, "gpu_morphed_frame") and self.gpu_morphed_frame is not None: + try: + self.gpu_morphed_frame.release() + except Exception: + self.gpu_morphed_frame = None + + if hasattr(self, "labels_gpu") and self.labels_gpu is not None: + try: + self.labels_gpu.release() + except Exception: + self.labels_gpu = None + + if hasattr(self, "gpu_encoder_8k_buf") and self.gpu_encoder_8k_buf is not None: + try: + self.gpu_encoder_8k_buf.release() + except Exception: + self.gpu_encoder_8k_buf = None + + if hasattr(self, "gpu_display_frame") and self.gpu_display_frame is not None: + try: + self.gpu_display_frame.release() + except Exception: + self.gpu_display_frame = None + + if hasattr(self, "gpu_crop_batch"): + for mat in self.gpu_crop_batch: + if isinstance(mat, cv2.cuda.GpuMat): + mat.release() + self.gpu_crop_batch = [] + + if hasattr(self, "stream"): + self.stream.waitForCompletion() + + self.pinned_downloaded_resizedframe_np = None + self.gpu_threshold_dst_frame = None + self.gpu_morphed_frame = None + self.pinned_downloaded_frame_np = None + + # Handle specific buffers (like your Ping-Pong lists) + # if hasattr(self, "encode_buffers"): + # self.encode_buffers.clear() + + # Clear the BGS history + if hasattr(self, "mask_history"): + self.mask_history.clear() + + # Optional: Final flush of the CUDA caching allocator + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + def apply_background_subtraction_gpu( + self, include_history=True, method="and", stream=None + ): + stream = stream if isinstance(stream, cv2.cuda.Stream) else self.stream + self.fgMask = self.backSub.apply( + self.resized_frame, float(self.lr), stream=stream + ) + + if include_history: + # If this is the first run, clone the mask instead of ANDing with an empty/white buffer + # if len(self.mask_history) < 1: + # self.prev_bkgd.setTo(255, stream) # Clear the initial white buffer + + for m in list(self.mask_history): + # Dilate the historical mask on GPU + dilated = self.dilate_filter_for_enhanced_mask.apply(m, stream=stream) + + if method == "or": + # Bitwise OR on GPU + cv2.cuda.bitwise_or( + self.prev_bkgd, dilated, self.prev_bkgd, stream=stream + ) + else: + # Bitwise AND on GPU + cv2.cuda.bitwise_and( + self.prev_bkgd, dilated, self.prev_bkgd, stream=stream + ) + + self.mask_history.append(self.fgMask.clone()) + + min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) + if max_val != min_val and max_val > 0: + # bitor = cv2.cuda.bitwise_or( + # self.fgMask, self.prev_bkgd, stream=stream + # ) + # bitand = cv2.cuda.bitwise_and( + # self.fgMask, self.prev_bkgd, stream=stream + # ) + # not_bitand = cv2.cuda.bitwise_not(self.prev_bkgd, stream=stream) + # self.fgMask = cv2.cuda.subtract(self.fgMask, bitand, stream=stream) + self.fgMask = cv2.cuda.bitwise_or( + self.fgMask, self.prev_bkgd, stream=stream + ) + # self.fgMask = cv2.cuda.bitwise_or( + # self.fgMask, self.mask_history[-2], stream=stream + # ) + # if method == "or": + # self.fgMask = cv2.cuda.bitwise_and( + # self.fgMask, self.prev_bkgd, stream=stream + # ) + # else: + # self.fgMask = cv2.cuda.bitwise_or( + # self.fgMask, self.prev_bkgd, stream=stream + # ) + + def rbtd_full_gpuv1(self, frame): + """ + GPU-Accelerated Motion Detection Pipeline (Producer). + + This function performs high-speed background subtraction (BGS) on a downscaled + version of the 8K frame to identify regions of interest (ROIs). + + Args: + frame (np.ndarray): The raw 8K input frame. + frameNum (float): Chronological timestamp or ID. + + Returns: + dict: Contains the frame ID, the GPU-resident motion mask, and the original 8K frame. + """ + # stream = self.stream + + with torch.no_grad(): # , torch.cuda.stream(self.bgs_stream): + # frame is [3, 4320, 7680] + resized_torch = F.interpolate( + frame.unsqueeze(0).float(), + size=(self.resize_h, self.resize_w), + mode="nearest", + # mode="bilinear", + # align_corners=False, + ).squeeze(0) + # resized_torch = F.interpolate( + # frame.to(memory_format=torch.channels_last), + # size=(self.resize_h, self.resize_w), + # mode="nearest", + # # align_corners=False + # ) + + # CRITICAL: Change format from [3, 640, 640] to [640, 640, 3] + # .contiguous() is mandatory here to reorganize the actual memory bits + resized_torch = resized_torch.permute(1, 2, 0).byte().contiguous() + gpu_mat_view = cv2.cuda.createGpuMatFromCudaMemory( + self.resize_h, self.resize_w, cv2.CV_8UC3, resized_torch.data_ptr() + ) + + # Bridge the TINY 640p frame to OpenCV + # Moving 8K (100MB) to CPU takes 114ms. + # Moving 640p (1.2MB) to CPU takes <0.2ms. + # This preserves your 15 FPS target. + # small_cpu = resized_torch.permute(1, 2, 0).cpu().numpy() + # self.resized_frame.upload(small_cpu) + # gpu_mat_bridge = torch2gpumat(resized_torch.byte()) + gpu_mat_view.copyTo(self.bgs_stream, self.resized_frame) + self.apply_background_subtraction_gpu( + include_history=self.config.BKGD_SUB_INCLUDE_HISTORY, + method=self.config.BKGD_SUB_INCLUDE_HISTORY_METHOD, + stream=self.bgs_stream, + ) + + # self.bgs_stream.waitForCompletion() + # Clean up the motion mask using Thresholding and Morphology (Dilation) + # cv2.cuda.threshold( + # self.fgMask, + # self.config.THRESHOLD_VALUE, + # 255, + # cv2.THRESH_BINARY, + # dst=self.gpu_threshold_dst_frame, + # stream=self.bgs_stream, + # ) + # self.dilate_filter.apply( + # self.gpu_threshold_dst_frame, dst=self.labels_gpu, stream=self.bgs_stream + # ) + + # return { + # # "frameNum": frameNum, # overall frame + # "mask": self.labels_gpu, # GpuMat pointer to cleaned mask + # # "full_frame": frame, # Kept for high-res cropping + # "full_frame": frame, # current_gpu_frame, + # } + + # if self.config.ENABLE_QUERYING and self.video_writer: # and not self.video_queue.full(): + # self.pinned_downloaded_resizedframe_np = self.resized_frame.download(stream) + # # self.resized_frame.download(self.stream, self.pinned_downloaded_resizedframe_np) + # # self.video_writer.write(self.pinned_downloaded_resizedframe_np) + # self.write_queue.put(self.pinned_downloaded_resizedframe_np.copy()) + + # if ( + # (self.debug_range[0] * self.target_fps) + # <= self.frame_count + # <= (self.debug_range[1] * self.target_fps) + # ): + # mask_cpu = self.fgMask.download() # fgMask is a GpuMat from the backend + # mask_path = f"{self.out_imgdir}/mask_frame_{self.frame_count}.png" + # cv2.imwrite(mask_path, mask_cpu) + # print(f"[DEBUG] Saved BGS Mask: {mask_path}") + + # -------------- + # NOISE FILTERING: Median Blur (Kills pixel noise) + # if not hasattr(self, 'median_filter'): + # self.median_filter = cv2.cuda.createMedianFilter(cv2.CV_8UC1, 5) + + # # Syntax: apply(src, dst, stream) + # self.median_filter.apply(self.fgMask, self.fgMask, self.bgs_stream) + + # # MORPHOLOGY: Erode then Dilate (Opening) + # if not hasattr(self, 'erode_filter'): + # # 3x3 kernel is sufficient when combined with a median filter + # kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + + # # In OpenCV 4.x, use createMorphologyFilter for both Erode and Dilate + # self.erode_filter = cv2.cuda.createMorphologyFilter(cv2.MORPH_ERODE, cv2.CV_8UC1, kernel) + # self.dilate_filter = cv2.cuda.createMorphologyFilter(cv2.MORPH_DILATE, cv2.CV_8UC1, kernel) + + # # Shave off noise (Erode) + # self.erode_filter.apply(self.fgMask, self.fgMask, self.bgs_stream) + + # # Restore object size (Dilate) + # self.dilate_filter.apply(self.fgMask, self.fgMask, self.bgs_stream) + + # return {"mask": self.fgMask, "full_frame": frame} + # -------------- + + # Clean up the motion mask using Thresholding and Morphology (Dilation) + # Fused Cleanup (Threshold + Dilate) + # This replaces: cv2.cuda.threshold AND cv2.cuda.dilate + w, h = self.fgMask.size() + pitch = self.fgMask.step + tpb = (16, 16) + bpg = ((w + 15) // 16, (h + 15) // 16) + + # Bridge GpuMat to CuPy for the kernel + fg_ptr = self.fgMask.cudaPtr() + with cupy.cuda.ExternalStream(self.bgs_stream.cudaPtr()): + fg_cp = cupy.ndarray( + (h, w), + dtype=cupy.uint8, + memptr=cupy.cuda.MemoryPointer( + cupy.cuda.UnownedMemory(fg_ptr, pitch * h, self), 0 + ), + strides=(pitch, 1), + ) + + # Launch Fused Kernel + labels_ptr = self.labels_gpu.cudaPtr() + # Ensure this matches the size of fg_cp + labels_cp = cupy.ndarray( + (h, w), + dtype=cupy.uint8, + memptr=cupy.cuda.MemoryPointer( + cupy.cuda.UnownedMemory(labels_ptr, self.labels_gpu.step * h, self), + 0, + ), + strides=(self.labels_gpu.step, 1), + ) + # with cupy.cuda.ExternalStream(self.stream.cudaPtr()): + # Launch your kernel on the same physical GPU stream as OpenCV BGS + DETECTION_ACCEL_KERNEL( + bpg, + tpb, + (fg_cp, labels_cp, pitch, w, h, self.config.THRESHOLD_VALUE), + ) + # Ensure CuPy is done before the stream returns to OpenCV/PyTorch + # cupy.cuda.get_current_stream().synchronize() + + # if self.debug_range[0] <= self.frame_count <= self.debug_range[1]: + # after_cpu = self.labels_gpu.download() + # # Multiply by 50 to make distinct object labels visible + # visible_labels = (after_cpu * 50).clip(0, 255).astype(np.uint8) + # cv2.imwrite( + # f"{self.out_imgdir}/mask_AFTER_frame_{self.frame_count}.png", + # visible_labels, + # ) + + return { + # "frameNum": frameNum, # overall frame + "mask": self.labels_gpu, # GpuMat pointer to cleaned mask + # "full_frame": frame, # Kept for high-res cropping + "full_frame": frame, # current_gpu_frame, + } + + def rbtd_full_gpu(self, device_frame): + """ + Asynchronously handles downsampling, conversion, background subtraction, and morphology + by reshaping PyTorch CUDA Tensors to interleaved layouts before mapping to GpuMat. + """ + # 1. BRIDGE THE PYTORCH TO OPENCV VRAM GAP (ZERO-COPY & INTERLEAVED) + if torch.is_tensor(device_frame): + # Reshape tensor to channel-last layout [H, W, C] to completely prevent the 3x3 grid tiling artifact + interleaved_frame = device_frame.permute(1, 2, 0).contiguous() + + h_raw, w_raw, ch = interleaved_frame.shape + cuda_mem_ptr = interleaved_frame.data_ptr() + cv_type = cv2.CV_8UC3 if ch == 3 else cv2.CV_8UC1 + + src_gpu_mat = cv2.cuda.createGpuMatFromCudaMemory( + h_raw, w_raw, cv_type, cuda_mem_ptr + ) + else: + src_gpu_mat = device_frame + ch = src_gpu_mat.channels() + + # 2. INSTANTIATE GPUMAT CONTROLLERS WITH STRIDED LAYOUT HEADERS + recycled_resize_mat = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC3 if ch == 3 else cv2.CV_8UC1 + ) + gray_resize_mat = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + raw_mask = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + thresh_mask = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + clean_mask = cv2.cuda.GpuMat(self.resize_h, self.resize_w, cv2.CV_8UC1) + + # 3. RUN ASYNCHRONOUS DOWN-SAMPLING GATE + cv2.cuda.resize( + src_gpu_mat, + dst=recycled_resize_mat, + dsize=(self.resize_w, self.resize_h), + interpolation=cv2.INTER_LINEAR, + stream=self.bgs_stream, + ) + + # 4. COLOR LAYOUT CORRECTION (COLLAPSE CHANNELS SAFELY) + if recycled_resize_mat.channels() == 3: + cv2.cuda.cvtColor( + recycled_resize_mat, + cv2.COLOR_BGR2GRAY, + dst=gray_resize_mat, + stream=self.bgs_stream, + ) + motion_input = gray_resize_mat + else: + motion_input = recycled_resize_mat + + # 4. EXECUTE SEGMENTATION METRICS + raw_mask = self.backSub.apply( + motion_input, + 0.005, # float(self.lr), + stream=self.bgs_stream, + ) + include_history = self.config.BKGD_SUB_INCLUDE_HISTORY + method = self.config.BKGD_SUB_INCLUDE_HISTORY_METHOD + if include_history: + # If this is the first run, clone the mask instead of ANDing with an empty/white buffer + # if len(self.mask_history) < 1: + # self.prev_bkgd.setTo(255, stream) # Clear the initial white buffer + + for m in list(self.mask_history): + # Dilate the historical mask on GPU + dilated = self.dilate_filter_for_enhanced_mask.apply( + m, stream=self.bgs_stream + ) + + if method == "or": + # Bitwise OR on GPU + cv2.cuda.bitwise_or( + self.prev_bkgd, dilated, self.prev_bkgd, stream=self.bgs_stream + ) + else: + # Bitwise AND on GPU + cv2.cuda.bitwise_and( + self.prev_bkgd, dilated, self.prev_bkgd, stream=self.bgs_stream + ) + + self.mask_history.append(raw_mask.clone()) + + min_val, max_val, _, _ = cv2.cuda.minMaxLoc(self.prev_bkgd) + if max_val != min_val and max_val > 0: + raw_mask = cv2.cuda.bitwise_or( + raw_mask, self.prev_bkgd, stream=self.bgs_stream + ) + + # 5. MORPHOLOGICAL TRANSFORMATIONS BINARY FILTERS + cv2.cuda.threshold( + raw_mask, + self.config.THRESHOLD_VALUE, + self.config.THRESHOLD_MAX_VALUE, + cv2.THRESH_BINARY, + thresh_mask, + stream=self.bgs_stream, + ) + + # cv2.cuda.dilate( + # thresh_mask, + # clean_mask, + # self.dilate_kernel, + # stream=self.bgs_stream + # ) + self.dilate_filter.apply(thresh_mask, clean_mask, self.bgs_stream) + + # 6. ENFORCE INDEPENDENT WORKSPACE MEMORY VIEWS + # Allocate an isolated output mask surface container + isolated_kernel_mask = cv2.cuda.GpuMat( + self.resize_h, self.resize_w, cv2.CV_8UC1 + ) + clean_mask.copyTo(dst=isolated_kernel_mask, stream=self.bgs_stream) + + # return isolated_kernel_mask + # self.bgs_stream.waitForCompletion() + + return { + "mask": isolated_kernel_mask, # GpuMat pointer to cleaned mask + "full_frame": device_frame, # current_gpu_frame, + } + + def findContours_gpu(self, mask, method="fused"): + # self.stream.waitForCompletion() + h, w = mask.size() + ptr = mask.cudaPtr() + pitch = mask.step + mask_cp = cupy.ndarray( + (h, w), + dtype=cupy.uint8, + memptr=cupy.cuda.MemoryPointer( + cupy.cuda.UnownedMemory(ptr, pitch * h, self), 0 + ), + strides=(pitch, 1), + ) + + labeled, num_labels = cupyx.scipy.ndimage.label(mask_cp, output=cupy.int32) + # labeled = labeled.astype(cupy.int32) + # labeled.strides is in bytes. For int32, we need row-start in bytes. + labeled_pitch_bytes = labeled.strides[0] + + if num_labels == 0: + return torch.empty((0, 4), device="cuda") + + # Pre-allocate bounds with sentinels + x1, y1 = ( + cupy.full((num_labels + 1,), w, dtype=cupy.int32), + cupy.full((num_labels + 1,), h, dtype=cupy.int32), + ) + x2, y2 = ( + cupy.full((num_labels + 1,), -1, dtype=cupy.int32), + cupy.full((num_labels + 1,), -1, dtype=cupy.int32), + ) + + # BOUNDS_KERNEL(((w+15)//16, (h+15)//16), (16, 16), (labeled, w, h, num_labels, x1, y1, x2, y2)) + BOUNDS_KERNEL( + ((w + 15) // 16, (h + 15) // 16), + (16, 16), + ( + labeled.data.ptr, + labeled_pitch_bytes, + w, + h, + num_labels, + x1.data.ptr, + y1.data.ptr, + x2.data.ptr, + y2.data.ptr, + ), + ) + # cupy.cuda.get_current_stream().synchronize() + # cupy.cuda.Stream.null.synchronize() + # Convert to torch + boxes = torch.stack( + [ + torch.as_tensor(x1[1:], device="cuda"), + torch.as_tensor(y1[1:], device="cuda"), + torch.as_tensor(x2[1:], device="cuda"), + torch.as_tensor(y2[1:], device="cuda"), + ], + dim=1, + ).float() + + # --- CRITICAL FIX: Filter out boxes that weren't updated by the kernel --- + # A valid box must have x2 >= x1 + valid_mask = boxes[:, 2] >= boxes[:, 0] + boxes = boxes[valid_mask] + + # if boxes.shape[0] > 0: + # print(f"[GPU] Found {boxes.shape[0]} boxes", flush=True) + return boxes + + +class CPUStreamHandler(DeviceBaseHandler): + def allocate_cpu(self): + # pass + self.resized_frame = np.zeros((3, self.resize_h, self.resize_w), dtype="uint8") + # cv2.cuda.createContinuous( + # self.resize_h, self.resize_w, cv2.CV_8UC3 + # ) + + self.fgMask = np.zeros( + (self.resize_h, self.resize_w), dtype="uint8" + ) # For resize + + if self.config.BKGD_SUB_INCLUDE_HISTORY_METHOD == "and": + self.prev_bkgd = np.ones( + (self.resize_h, self.resize_w), dtype="uint8" + ) # * 255 + else: + self.prev_bkgd = np.zeros((self.resize_h, self.resize_w), dtype="uint8") + + # self.prev_bkgd = np.ones((self.resize_h, self.resize_w), dtype="uint8") * 255 + + self.mask_history = deque( + maxlen=self.config.BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE + ) + self.mask_history.append(self.prev_bkgd) + + def prepare_cpu_pipeline(self): # , method="mog2"): + self.operation_device_map = PipelineMapping() # "full_cpu" + self.device_input = self.operation_device_map.detection_device + + self.allocate_cpu() + + # Subtraction + # if method == "knn": + # history = 300 # int(5 * self.target_fps) + # background_thresh = 350 + # NSamples = 10 + # kNNSamples = 2 + # self.lr = 1 / history + + # self.backSub = cv2.createBackgroundSubtractorKNN( + # history=history, # default 500 + # dist2Threshold=background_thresh, # default 400 + # detectShadows=False, # default True + # ) + # self.backSub.setkNNSamples(kNNSamples) + # self.backSub.setNSamples(NSamples) + # elif method == "mog2": + # history = int(2 * self.target_fps) + # background_thresh = 10 + self.lr = self.config.BKGD_SUB_MOG2_LR + + self.backSub = cv2.createBackgroundSubtractorMOG2( + history=self.config.BKGD_SUB_MOG2_HISTORY, # Clear ghosts of fast drones in ~2 seconds (2*fps) + varThreshold=self.config.BKGD_SUB_MOG2_VARTHRESHOLD, # High threshold to ignore "shimmer" and compression noise # default 16 + detectShadows=self.config.BKGD_SUB_MOG2_DETECTSHADOWS, # default True + ) + # else: + # raise ValueError(f"Provided method ({method}) is not available.") + + def cleanup_cpu(self): + """ + Purges large 8K NumPy buffers and CPU-based AI resources. + """ + # Nullify specific class references to allow Garbage Collection + self.executor = None + self.clip_executor = None + self.reader = None + self.latest_processed_frame = None + + # Clear the Ping-Pong buffers (up to 200MB of RAM) + # if hasattr(self, "encode_buffers"): + # self.encode_buffers.clear() + + # Explicitly nullify large arrays to trigger Garbage Collection + self.resized_frame = None + self.fgMask = None + self.prev_bkgd = None + + # Clear the BGS history + if hasattr(self, "mask_history"): + self.mask_history.clear() + + def apply_background_subtraction_cpu(self, include_history=True, method="and"): + self.fgMask = self.backSub.apply( + self.cpu_resized_frame, learningRate=float(self.lr) + ) + + if include_history: + # If this is the first run, clone the mask instead of ANDing with an empty/white buffer + # if len(self.mask_history) < 1: + # self.prev_bkgd.setTo(0, stream) # Clear the initial white buffer + + for m in list(self.mask_history): + # Dilate the historical mask on CPU + dilated = cv2.dilate( + m, self.dilate_kernel_for_enhanced_mask, iterations=1 + ) + + if method == "or": + # Bitwise OR on CPU + cv2.bitwise_or(self.prev_bkgd, dilated, dst=self.prev_bkgd) + else: + # Bitwise AND on CPU + cv2.bitwise_and(self.prev_bkgd, dilated, dst=self.prev_bkgd) + + self.mask_history.append(self.fgMask.copy()) + + if ( + self.prev_bkgd.max() != self.prev_bkgd.min() + and self.prev_bkgd.max() > 0 + ): + combined_mask_bool = (self.fgMask > 0) | (self.prev_bkgd > 0) + self.fgMask = combined_mask_bool.astype(np.uint8) * 255 + + def rbtd_full_cpu(self, frame): + """ + CPU-Based Motion Detection Pipeline (Producer). + + Performs background subtraction on the CPU to identify moving objects. + Ideal for saving VRAM or for environments without high-end NVIDIA GPUs. + + Args: + frame (np.ndarray): The raw 8K input frame. + frameNum (float): Unique ID for the current frame. + + Returns: + dict: Motion data containing the frame ID, CPU-based mask, and original 8K frame. + """ + # Resize the 8K frame to a smaller 'model' size (e.g., 640x640) + # Using INTER_NEAREST as it is the fastest CPU interpolation method. + # H, W = self.resize_h, self.resize_w + self.cpu_resized_frame = cv2.resize( + frame, (self.resize_w, self.resize_h), interpolation=cv2.INTER_NEAREST + ) + # if self.config.ENABLE_QUERYING and self.video_writer: + # self.write_queue.put(self.cpu_resized_frame.copy()) + + # Apply Background Subtraction on CPU + self.apply_background_subtraction_cpu( + include_history=self.config.BKGD_SUB_INCLUDE_HISTORY, + method=self.config.BKGD_SUB_INCLUDE_HISTORY_METHOD, + ) + + # ---------------- + # NOISE FILTERING: Median Blur + # Kills small single-pixel noise specs + # self.fgMask = cv2.medianBlur(self.fgMask, 5) + + # # MORPHOLOGY: Opening (Erode then Dilate) + # kernel = np.ones((3,3), np.uint8) + + # # Remove small specs + # self.fgMask = cv2.erode(self.fgMask, kernel, iterations=2) + + # # Re-expand and connect nearby moving pixels + # self.fgMask = cv2.dilate(self.fgMask, kernel, iterations=2) + # return {"mask": self.fgMask, "full_frame": frame} + # ---------------- + + # Clean up the motion mask using Thresholding and Morphology (Dilation) + _, mask = cv2.threshold( + self.fgMask, + self.config.THRESHOLD_VALUE, + self.config.THRESHOLD_MAX_VALUE, + cv2.THRESH_BINARY, + ) + mask = cv2.dilate(mask, self.dilate_kernel, iterations=1) + + return { + # "frameNum": frameNum, # overall frame + "mask": mask, + "full_frame": frame, # Kept for high-res cropping + } diff --git a/fastapi/include/models.py b/fastapi/include/models.py new file mode 100644 index 0000000..e9dc452 --- /dev/null +++ b/fastapi/include/models.py @@ -0,0 +1,350 @@ +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import tensorrt as trt + +sys.path.insert(1, str(Path(__file__).parent.parent)) +from include.default_configs import ENABLE_QUERYING_DEFAULT +from include.utils import PipelineConfig, get_freest_gpu, str2bool +from torch import cuda +from ultralytics import YOLO +from ultralytics.utils.checks import check_requirements + + +# OBJECT DETECTION +def build_engine( + onnx_path, + engine_path, + profile=[(1, 3, 32, 32), (8, 3, 640, 640), (100, 3, 640, 640)], + metadata=None, +): + logger = trt.Logger(trt.Logger.INFO) + builder = trt.Builder(logger) + config = builder.create_builder_config() + network = builder.create_network( + 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) + ) + parser = trt.OnnxParser(network, logger) + + with open(onnx_path, "rb") as f: + parser.parse(f.read()) + + tensor_name = "images" + + min_prof, opt_prof, max_pro = profile + prof = builder.create_optimization_profile() + prof.set_shape(tensor_name, min_prof, opt_prof, max_pro) + config.add_optimization_profile(prof) + + config.set_flag(trt.BuilderFlag.FP16) + # Ensure sufficient workspace for 8K operations + # config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB + config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 10 << 30) # 2GB + + # Sometimes TensorRT fails because it restricts which software libraries (like cuDNN or cuBLAS) it can use. + # Force it to search all available implementations + tactic_sources = ( + 1 << int(trt.TacticSource.CUBLAS) + | 1 << int(trt.TacticSource.CUDNN) + | 1 << int(trt.TacticSource.CUBLAS_LT) + ) + config.set_tactic_sources(tactic_sources) + + print("Building engine... this will take some time.") + serialized_engine = builder.build_serialized_network(network, config) + with open(engine_path, "wb") as f: + if metadata: + meta_string = json.dumps(metadata) + meta_bytes = meta_string.encode("utf-8") + # Write a 4-byte little-endian signed integer indicating metadata length + f.write(len(meta_bytes).to_bytes(4, byteorder="little", signed=True)) + # Write the raw JSON string bytes + f.write(meta_bytes) + + f.write(serialized_engine) + + +def get_model( + model_dir, + model_name, + run_platform, + device_input, + batch=100, + force_export=False, + sf_enabled=False, + half_flag=True, + dynamic_flag=True, + model_h=640, + model_w=640, +): + final_model_path = f"{model_dir}/{model_name}.pt" + pt_detection_model = YOLO(final_model_path, verbose=False, task="detect") + label_source = [] + for k, v in pt_detection_model.names.items(): + label_source.append(v) + + if run_platform == "openvino": + final_model_path = f"{model_dir}/{model_name}_openvino_model/" + if not Path(final_model_path).exists() or force_export: + pt_detection_model.export( + format="openvino", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + # batch=batch, + data={"names": pt_detection_model.names}, + ) + + object_detection_model = YOLO( + final_model_path, + verbose=False, + task="detect", + ) + + # det_ov_model = core.read_model(final_model_path+"yolo11n.xml") + # ov_config = {hints.performance_mode: hints.PerformanceMode.LATENCY} + # if device == "GPU": + # ov_config["GPU_DISABLE_WINOGRAD_CONVOLUTION"] = "YES" + # compiled_model = core.compile_model(det_ov_model, device, ov_config) + # object_detection_model.predictor.model.ov_compiled_model = compiled_model + + elif run_platform == "engine": + final_model_path = f"{model_dir}/{model_name}.engine" + onnx_model_path = f"{model_dir}/{model_name}.onnx" + profile = [ + (1, 3, 32, 32), + (8, 3, model_h, model_w), + (batch, 3, model_h, model_w), + ] + if not sf_enabled: + # Copy base model + shutil.copy( + f"{model_dir}/{model_name}.pt", f"{model_dir}/{model_name}_noSF.pt" + ) + pt_detection_model = YOLO( + f"{model_dir}/{model_name}_noSF.pt", verbose=False, task="detect" + ) + final_model_path = f"{model_dir}/{model_name}_noSF.engine" + onnx_model_path = f"{model_dir}/{model_name}_noSF.onnx" + # profile = [(1, 3, 4320, 7680), (1, 3, 4320, 7680), (1, 3, 4320, 7680)] + profile = [(1, 3, 32, 32), (1, 3, 4320, 7680), (1, 3, 4320, 7680)] + + if not Path(final_model_path).exists() or force_export: + # pt_detection_model.export( + # format="engine", + # half=half_flag, + # imgsz=[640, 640], + # # imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) + # dynamic=dynamic_flag, + # device=device_input, + # simplify=True, + # batch=batch, + # ) + + # Export to onnx + check_requirements( + "onnxruntime-gpu" + if cuda.is_available() and device_input != "cpu" + else "onnxruntime" + ) + # onnx_model_path = f"{model_dir}/{model_name}.onnx" + if not Path(onnx_model_path).exists() or force_export: + pt_detection_model.export( + format="onnx", + half=half_flag, + dynamic=True, + device=device_input, + simplify=True, + data={"names": pt_detection_model.names}, + ) + + if hasattr(pt_detection_model.model, "stride"): + max_stride = int(pt_detection_model.model.stride.max().item()) + else: + max_stride = 32 # Safe default fallback value for standard YOLO layouts + + build_engine( + onnx_model_path, + final_model_path, + profile=profile, + metadata={ + "stride": max_stride, + "task": "detect", + "names": pt_detection_model.names, + }, + ) + + object_detection_model = YOLO( + final_model_path, + verbose=False, + task="detect", + ) + + elif run_platform == "onnx": + check_requirements( + "onnxruntime-gpu" + if cuda.is_available() and device_input != "cpu" + else "onnxruntime" + ) + + final_model_path = f"{model_dir}/{model_name}.onnx" + if not Path(final_model_path).exists() or force_export: + pt_detection_model.export( + format="onnx", + half=half_flag, + dynamic=dynamic_flag, + device=device_input, + simplify=True, + batch=batch, + data={"names": pt_detection_model.names}, + ) + + object_detection_model = YOLO(final_model_path, verbose=False, task="detect") + + elif run_platform == "pytorch": + object_detection_model = pt_detection_model + if device_input != "cpu": + object_detection_model.to("cuda") + else: + object_detection_model.to(device_input) + + else: + raise ValueError(f"[!] Model for {run_platform} is not implemented.") + + return object_detection_model, final_model_path, label_source + + +# FACE DETECTION +def get_models_from_list(lst_path): + """Reads model names from the .lst file, ignoring comments.""" + if not os.path.exists(lst_path): + return [] + with open(lst_path, "r") as f: + return [line.strip() for line in f if line.strip() and not line.startswith("#")] + + +def verify_and_download( + models_lst="/home/resources/models/models.lst", + output_dir="/home/resources/models", + precisions="FP16", +): + # Check which models are missing + model_names = get_models_from_list(models_lst) + missing_models = [] + + for model in model_names: + # Check standard OpenVINO subfolders: 'public' and 'intel' + public_path = os.path.join(output_dir, "public", model) + intel_path = os.path.join(output_dir, "intel", model) + + if not (os.path.exists(public_path) or os.path.exists(intel_path)): + missing_models.append(model) + + # Only run downloader if models are missing + if not missing_models: + print("All models already exist in the output directory. Skipping download.") + return + + print(f"Missing models: {missing_models}. Starting download...") + + command = [ + "omz_downloader", + "--list", + models_lst, + "-o", + output_dir, + "--precisions", + precisions, + ] + + try: + subprocess.run(command, check=True) + print("Download complete.") + except subprocess.CalledProcessError as e: + print(f"Download failed with exit code {e.returncode}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-o", + "--output", + type=str, + default=None, + dest="output_path", + help="Path to print json of model classes", + ) + args = parser.parse_args() + + config = PipelineConfig( + CODE_DIR=os.getenv("CODE_DIR", "/home"), + CUSTOM_MODEL_FLAG=str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)), + DBHOST=os.getenv("DBHOST", "vdms-service"), + DEBUG=os.getenv("DEBUG", "0"), + DEVICE=os.getenv("DEVICE", "CPU"), + ENABLE_QUERYING=os.getenv("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT), + INGESTION=os.getenv("INGESTION", "object"), + MODEL_NAME=os.getenv("MODEL_NAME", "yolo11n"), + OMIT_DETECTIONS_FLAG=str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)), + # RESIZE_FLAG=str2bool(os.getenv("RESIZE_FLAG", False)), + SHARED_MODEL=os.getenv("SHARED_MODEL", False), + SHARED_OUTPUT=os.getenv("SHARED_OUTPUT", "/var/www/mp4"), + TEST_MODE=str2bool(os.getenv("TEST_MODE", False)), + TMP_LOCATION=os.getenv("TMP_LOCATION", "/var/www/cache"), + UDF_HOST=os.getenv("UDF_HOST", "udf-service"), + UDF_PORT=5011, + ) + if config.CUSTOM_MODEL_FLAG: + dir_path = f"{config.CODE_DIR}/resources/models/ultralytics/custom_models" + else: + dir_path = f"{config.CODE_DIR}/resources/models/ultralytics/{config.MODEL_NAME}/{config.MODEL_PRECISION}" + + if config.DEVICE == "GPU": + best_gpu_index = get_freest_gpu() + os.environ["CUDA_VISIBLE_DEVICES"] = str(best_gpu_index) + # Force PyTorch to initialize the CUDA context + import torch + + if torch.cuda.is_available(): + torch.cuda.set_device(0) + torch.cuda.empty_cache() + # print(f"Using GPU: {torch.cuda.get_device_name(0)}") + # EXPORT_BATCH_SIZE = 64 # 32, 50 # int(os.environ.get("GPU_BATCH_SIZE", 1)) + run_platform_name = "engine" + print("[!] USING GPU & TENSORRT") + else: + # EXPORT_BATCH_SIZE = int(os.environ.get("CPU_BATCH_SIZE", 1)) + run_platform_name = "openvino" + print("[!] USING CPU & OPENVINO") + + # Download models if it doesn't exist + if "object" in config.INGESTION: + _, _, classes = get_model( + Path(dir_path), + config.MODEL_NAME, + run_platform_name, + config.device_input, + batch=config.MODEL_MAX_BATCH_SIZE, + force_export=False, + sf_enabled=True, + half_flag=True, + dynamic_flag=True, + ) + if args.output_path is not None: + import json + + with open(args.output_path, "w") as f: + json.dump({"classes": classes}, f, indent=4) + + if "face" in config.INGESTION: + verify_and_download( + models_lst=f"{config.CODE_DIR}/resources/models/models.lst", + output_dir=f"{config.CODE_DIR}/resources/models", + precisions=config.MODEL_PRECISION, + ) diff --git a/fastapi/include/readers.py b/fastapi/include/readers.py new file mode 100644 index 0000000..8494b08 --- /dev/null +++ b/fastapi/include/readers.py @@ -0,0 +1,1076 @@ +import ctypes +import logging +import os +import queue +import sys +import threading +import time +import traceback + +import av +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from ultralytics.utils.checks import check_imgsz + +# from fastapi import FastAPI + +# ----- SETUP LOGGING ----- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) + +# Suppress low-delay reference block warnings from OpenCV/PyAV/FFmpeg +os.environ["OPENCV_FFMPEG_LOGLEVEL"] = "-8" +os.environ["OPENCV_LOG_LEVEL"] = "OFF" +logging.getLogger("libav").setLevel(logging.CRITICAL) +logging.getLogger("libav.hevc").setLevel(logging.CRITICAL) +av.logging.set_level(av.logging.PANIC) + +main_app_logger = logging.getLogger(__name__) + + +# ----- PIPELINE CONFIGURATION ----- +os.environ["PYTORCH_ALLOC_CONF"] = "expandable_segments:True" +# Force OpenCV to use a single thread for its operations. +# This prevents internal OpenCV threads from "racing" against AI logic. +# cv2.setNumThreads(1) + +# Force OpenCV to run sequentially to prevent context-switching overhead +cv2.setNumThreads(0) + +# DEVICE = os.getenv("DEVICE", "CPU") +# device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" + +from include.default_configs import ENABLE_QUERYING_DEFAULT +from include.utils import PipelineConfig, manual_fps_calculation, str2bool + +BASE_PIPELINE_CONFIG = PipelineConfig( + CODE_DIR=os.getenv("CODE_DIR", "/home"), + CUSTOM_MODEL_FLAG=str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)), + DBHOST=os.getenv("DBHOST", "vdms-service"), + DEBUG=os.getenv("DEBUG", "0"), + DEVICE=os.getenv("DEVICE", "CPU"), + ENABLE_QUERYING=os.getenv("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT), + INGESTION=os.getenv("INGESTION", "object"), + MODEL_NAME=os.getenv("MODEL_NAME", "yolo11n"), + OMIT_DETECTIONS_FLAG=str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)), + # RESIZE_FLAG=str2bool(os.getenv("RESIZE_FLAG", False)), + SHARED_MODEL=os.getenv("SHARED_MODEL", False), + SHARED_OUTPUT=os.getenv("SHARED_OUTPUT", "/var/www/mp4"), + TEST_MODE=str2bool(os.getenv("TEST_MODE", False)), + TMP_LOCATION=os.getenv("TMP_LOCATION", "/var/www/cache"), + UDF_HOST=os.getenv("UDF_HOST", "udf-service"), + UDF_PORT=5011, +) + +# Placeholder for dynamic import for PyNvVideoCodec (GPU package) +nvc = None + + +# ----- VIDEO READERS ----- +class BaseReader: + def __init__( + self, + source, + target_fps=BASE_PIPELINE_CONFIG.TARGET_FPS, + clip_duration=BASE_PIPELINE_CONFIG.CLIP_DURATION, + queue_size=2, + ): + self.source = source + self.is_rtsp = str(self.source).startswith("rtsp://") + self.frame_idx = 0 + self.frame_queue = queue.Queue(maxsize=queue_size) # 2) # 5 + self.stopped = False + self.reconnect_failed = False + self.init_error = None + self.target_fps = ( + float(target_fps) if target_fps not in [None, 0] else target_fps + ) + self.clip_duration = ( + float(clip_duration) if clip_duration not in [None, 0] else clip_duration + ) + + def start(self): + threading.Thread(target=self.stream_frames, daemon=True).start() + return self + + def stop(self): + """Cleanly stop the reader and release resources.""" + self.stopped = True + self.release() + + def read(self): + try: + # If the reader is stopped, don't wait a full second; + # check immediately to speed up the "Draining" phase. + wait_time = 0.1 if self.stopped else 2.0 + return self.frame_queue.get(timeout=wait_time) + except Exception: + return None, None + + +class CPUHybridReader(BaseReader): + """ + Decouples frame acquisition from processing. + Uses a background thread to ingest frames into a small deque, + preventing OpenCV buffer lag. + """ + + def __init__( + self, + source, + target_fps=BASE_PIPELINE_CONFIG.TARGET_FPS, + clip_duration=BASE_PIPELINE_CONFIG.CLIP_DURATION, + MODEL_W=BASE_PIPELINE_CONFIG.MODEL_W, + MODEL_H=BASE_PIPELINE_CONFIG.MODEL_H, + queue_size=2, + # as_tensor=False, + ): + super().__init__( + source, + target_fps=target_fps, + clip_duration=clip_duration, + queue_size=queue_size, + ) + # self.as_tensor = as_tensor + target_fps, clip_duration = (self.target_fps, self.clip_duration) + # options = ( + # {"rtsp_transport": "tcp", "stimeout": "5000000"} + # if str(self.source).startswith("rtsp") + # else {} + # ) + self.MODEL_H = MODEL_H + self.MODEL_W = MODEL_W + self.device = "CPU" # Global from include.utils + self.target_frame_idx = 0 + self.cap = None + + max_retries = 5 + retry_cnt = 0 + connected = False + + while not connected and not self.stopped: + try: + # Clean up stale capture descriptors safely to clear sockets + if self.cap is not None: + try: + self.cap.release() + except Exception: + pass + self.cap = None + + # Safely intercept capture context extraction layers + # Note: We pass self.source instead of recreating string evaluations + if not isinstance(self.source, cv2.VideoCapture): + params = [cv2.CAP_PROP_N_THREADS, 1] + test_cap = cv2.VideoCapture( + str(self.source), cv2.CAP_FFMPEG, params=params + ) + else: + test_cap = self.source + + if test_cap and test_cap.isOpened(): + # Validate that we can extract safe telemetry properties + self.get_fps_and_framecnt( + test_cap, self.target_fps, self.clip_duration + ) + self.frame_width = int(test_cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(test_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + self.numFrames = int(test_cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + if self.input_fps <= 0 or self.frame_width <= 0: + raise RuntimeError( + "VideoCapture opened but returned invalid stream properties." + ) + + self.cap = test_cap + self.get_frameWH() + connected = True + else: + raise RuntimeError( + "OpenCV VideoCapture failed to open target URI resource context." + ) + + except Exception as e: + retry_cnt += 1 + self.init_error = str(e) + + # Exit if local file resource or retry count exceeded + if not self.is_rtsp or retry_cnt >= max_retries: + main_app_logger.error( + f"Critical: Could not open/connect to {self.source}" + ) + self.reconnect_failed = True + self.stopped = True + # Halt and exit the server immediately + raise RuntimeError( + f"Critical CPU stream reader initialization failure: {self.init_error}" + ) + return + + wait_time = retry_cnt * 2 + main_app_logger.warning( + f"CPU connection pending... Retry ({retry_cnt}/{max_retries}) in {wait_time} seconds." + ) + time.sleep(wait_time) + + # self.cap = self._create_capture(target_fps, clip_duration) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Force low latency + self.cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY) + + # self.frame_queue = deque(maxlen=5) # Keep queue small to stay "real-time" + # self.frame_queue = queue.Queue(maxsize=5) + # self.stopped = False + # self.device = "CPU" # Global from include.utils + # self.frame_idx = 0 + # self.target_frame_idx = 0 + # self.frame_queue = queue.Queue(maxsize=30) + + def _create_capture(self, target_fps, clip_duration): + """Creates a VideoCapture with stable RTSP options.""" + # if not isinstance(self.source, cv2.VideoCapture): + # self.source = str(self.source) + # params = [cv2.CAP_PROP_N_THREADS, 1] + # cap = cv2.VideoCapture(self.source, cv2.CAP_FFMPEG, params=params) + # else: + # cap = self.source + # self.get_fps_and_framecnt(cap, target_fps, clip_duration) + # self.frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + # self.frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + # self.numFrames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + # self.get_frameWH() + # return cap + + if isinstance(self.source, cv2.VideoCapture): + return self.source + params = [cv2.CAP_PROP_N_THREADS, 1] + return cv2.VideoCapture(str(self.source), cv2.CAP_FFMPEG, params=params) + + # Gets video details + def get_fps_and_framecnt(self, cap, target_fps, clip_duration): + self.input_fps = int(cap.get(cv2.CAP_PROP_FPS)) # hardware fps + # print(f"in fps: {self.input_fps} target fps: {target_fps}") + if self.input_fps == 0: # Case when FPS isn't available + self.input_fps = manual_fps_calculation(cap, num_frames=10) + print(f"new in fps: {self.input_fps}") + + # If the stream can't connect, stop immediately instead of calculating. + if self.input_fps <= 0: + raise RuntimeError( + f"Failed to initialize stream reader endpoint: {self.source}" + ) + + self.target_fps = ( + target_fps + if target_fps not in [None, 0] and self.input_fps > target_fps + else self.input_fps + ) + + if self.input_fps > 0 and self.target_fps > 0: + self.frame_skip = max(1, int(self.input_fps / self.target_fps)) + else: + self.frame_skip = 1 + # self.skip_count = self.frame_skip - 1 + + if clip_duration is None: + frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) + clip_duration = frame_count / self.input_fps + self.max_frames_per_clip = int(self.target_fps * float(clip_duration)) + self.frame_interval = 1.0 / self.target_fps # 0.0666s + # print( + # f"in fps: {self.input_fps} self.target fps: {self.target_fps} self.frame_skip: {self.frame_skip}" + # ) + + # Gets frame W and H details + def get_frameWH(self): + if (self.frame_height * self.frame_width) < (self.MODEL_H * self.MODEL_W): + new_sizeHW = check_imgsz([self.MODEL_H, self.MODEL_W]) # expects hxw + else: + new_sizeHW = check_imgsz( + [self.frame_height, self.frame_width] + ) # expects hxw + + new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) + + self.width = new_sizeWH[0] + self.height = new_sizeWH[1] + + # Configure scaling for 8K-to-Model coordinate mapping + self.resize_h, self.resize_w = [self.MODEL_H, self.MODEL_W] + self.scale_x = self.frame_width / self.MODEL_W + self.scale_y = self.frame_height / self.MODEL_H + + def stream_frames(self): + """ + Continuously grabs frames. Throttles local files to maintain + the target FPS and manages RTSP reconnections. + """ + is_rtsp = str(self.source).startswith("rtsp") + max_retries = 5 + retry_cnt = 0 + + while not self.stopped: + try: + if self.cap is None or not self.cap.isOpened(): + if not is_rtsp: + self.stopped = True + break + try: + # Logic to recreate the VideoCapture + # self.cap = self._create_capture( + # self.target_fps, self.clip_duration + # ) + # self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + params = [cv2.CAP_PROP_N_THREADS, 1] + self.cap = cv2.VideoCapture( + str(self.source), cv2.CAP_FFMPEG, params=params + ) + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + retry_cnt = 0 + except Exception: + retry_cnt += 1 + if retry_cnt >= max_retries: + self.stopped = True + break + + wait_time = retry_cnt * 2 + main_app_logger.warning( + f"CPU reconnect failed. Retry {retry_cnt} in {wait_time}s" + ) + time.sleep(wait_time) + continue + + # Fully decode this frame + ret, frame = self.cap.read() + if not ret: + if not is_rtsp: + self.stopped = True + break + + # If a live stream returns No Frame, don't just die—trigger a reconnect + main_app_logger.warning("No frame received. Attempting reconnect.") + if self.cap: + self.cap.release() + self.cap = None + continue + + # if self.as_tensor: + # frame_tensor = torch.from_numpy(frame) + + # # 3. Push to GPU memory immediately + # # non_blocking=True speeds up the host-to-device transfer + # frame_tensor = frame_tensor.to("cuda", non_blocking=True) + + # # 4. Rearrange dimensions to PyTorch format: [H, W, C] -> [C, H, W] + # # .permute() changes layout; .float() converts uint8 to float32 for interpolation + # frame_tensor = frame_tensor.permute(2, 0, 1).float() + + # # 5. Add Batch dimension: [C, H, W] -> [1, C, H, W] + # frame_tensor = frame_tensor.unsqueeze(0) + + # # 6. Normalize pixel values to [0.0, 1.0] if required by your model + # frame = frame_tensor / 255.0 + + # self.frame_queue.put((frame, self.frame_idx)) + try: + self.frame_queue.put((frame, self.frame_idx), timeout=1.0) + except queue.Full: + try: + # Non-blocking pop to gracefully evict the oldest stale frame token + self.frame_queue.get_nowait() + except queue.Empty: + pass + self.frame_queue.put((frame, self.frame_idx)) + self.target_frame_idx += 1 + self.frame_idx += 1 + except Exception as e: + main_app_logger.error(f"CPU Reader error: {e}") + time.sleep(1) + + def release(self): + print("Closing HybridReader...") + self.stopped = True + + time.sleep(0.2) + if self.cap is not None and self.cap.isOpened(): + self.cap.release() + + +class GPUHybridReader(BaseReader): + """ + Encapsulates a Zero-Copy 8K video pipeline. + Bridges PyAV (Demuxing) and NVDEC (Hardware Decoding) directly to PyTorch. + """ + + def __init__( + self, + source, + gpu_id=0, + target_fps=BASE_PIPELINE_CONFIG.TARGET_FPS, + clip_duration=BASE_PIPELINE_CONFIG.CLIP_DURATION, + MODEL_W=BASE_PIPELINE_CONFIG.MODEL_W, + MODEL_H=BASE_PIPELINE_CONFIG.MODEL_H, + queue_size=2, + ): + global nvc + super().__init__( + source, + target_fps=target_fps, + clip_duration=clip_duration, + queue_size=queue_size, + ) + + if "PyNvVideoCodec" not in sys.modules: + try: + import PyNvVideoCodec as nvc + + globals()["nvc"] = nvc + + except ImportError: + raise ImportError( + "GPUHybridReader requires PyNvVideoCodec. Please install." + ) + + self.gpu_id = gpu_id + self.container = None + self.nv_dec = None + self.bsf = None + + # --- Initialize CUDA Context & Bridge --- + torch.cuda.set_device(self.gpu_id) + torch.cuda.init() + _ = torch.zeros(1).cuda() # Force context creation + + # self.is_opened = self.open(target_fps, clip_duration) + self.cuda_lib = ctypes.CDLL("libcuda.so.1") + ctx = ctypes.c_void_p() + self.cuda_lib.cuCtxGetCurrent(ctypes.byref(ctx)) + self.cuda_ctx_handle = ctx.value if ctx.value is not None else 0 + + self.cuda_lib.cuCtxSetCurrent(ctypes.c_void_p(self.cuda_ctx_handle)) + target_fps, clip_duration = (self.target_fps, self.clip_duration) + self.av_options = ( + { + # "rtsp_transport": "tcp", + # "stimeout": "2000000", # 2s + # "probesize": "10000000", # "32000000", # 32MB for 8K + # "analyzeduration": "5000000", + # "buffer_size": "10240000", # 10MB socket buffer + "rtsp_transport": "tcp", + "stimeout": "2000000", + "timeout": "2000000", + "rw_timeout": "2000000", + "err_detect": "explode", + "flags": "discardcorrupt", + "probesize": "32000000", + "analyzeduration": "32000000", + } + if str(self.source).startswith("rtsp") + else {} + ) + + # try: + # self.container = av.open( + # self.source, + # options={ + # **self.av_options, + # "err_detect": "ignore_err", # Don't stop demuxing on minor packet errors + # "flags": "low_delay", # Reduce internal buffering + # }, + # ) + # self.container.streams.video[0].thread_type = 'AUTO' + # streams = self.container.streams.get(video=0) + # self.stream = streams[0] if isinstance(streams, list) else streams + + self.get_stream() + + # Map Codec + codec_map = {"hevc": nvc.cudaVideoCodec.HEVC, "h264": nvc.cudaVideoCodec.H264} + self.nvc_codec = codec_map.get( + self.stream.codec_context.name, nvc.cudaVideoCodec.HEVC + ) + + # self.stream_width = self.stream.width + # self.stream_height = self.stream.height + + self.frame_width = self.true_width + self.frame_height = self.true_height + + self.input_fps = self.metadata_fps + + self.target_fps = ( + target_fps + if target_fps not in [None, 0] and self.input_fps > target_fps + else self.input_fps + ) + if self.input_fps > 0 and self.target_fps > 0: + self.frame_skip = max(1, int(self.input_fps / self.target_fps)) + else: + self.frame_skip = 1 + self.max_frames_per_clip = ( + None + if clip_duration is None + else int(self.target_fps * float(clip_duration)) + ) + self.frame_interval = 1.0 / self.target_fps + + self.numFrames = self.total_frames + + self.raw_input = torch.empty( + (self.frame_height, self.frame_width, 3), dtype=torch.uint8, device="cuda" + ) + self.sync_locked = True + + def get_stream(self): + max_retries = 5 + retry_cnt = 0 + connected = False + is_rtsp = str(self.source).startswith("rtsp") + + while not connected: + try: + self.container = av.open( + self.source, + options={ + **self.av_options, + "err_detect": "ignore_err", # Don't stop demuxing on minor packet errors + "flags": "low_delay", # Reduce internal buffering + }, + ) + self.container.streams.video[0].thread_type = "AUTO" + streams = self.container.streams.get(video=0) + self.stream = streams[0] if isinstance(streams, list) else streams + + if self.stream: + self.stream_width = self.stream.width + self.stream_height = self.stream.height + connected = True + + except Exception as e: + retry_cnt += 1 + self.init_error = str(e) + + if not is_rtsp or retry_cnt >= max_retries: + main_app_logger.error( + f"Critical: Could not open/connect to {self.source}" + ) + self.reconnect_failed = True + self.stopped = True + # Halt and exit the server immediately + raise RuntimeError( + f"Critical GPU stream reader initialization failure: {self.init_error}" + ) + return + + wait_time = retry_cnt * 2 + main_app_logger.warning( + f"GPU connection pending... Retry ({retry_cnt}/{max_retries}) in {wait_time} seconds." + ) + time.sleep(wait_time) + + def stream_frames(self): + """ + Universal background thread for 8K video. + - RTSP: Reconnects automatically if the stream drops. + - Files: Processes until EOF and then stops cleanly. + """ + is_rtsp = str(self.source).startswith("rtsp") + max_retries = 5 + retry_cnt = 0 + + # The outer while loop allows RTSP to recover from network hiccups + while not self.stopped: + try: + # Ensure the container and decoder are active + # For RTSP, if the demuxer loop below exits, we re-verify the connection here + if self.container is None: + try: + self.container = av.open( + self.source, + options={ + **self.av_options, + "err_detect": "explode", # Don't stop demuxing on minor packet errors + "flags": "low_delay", # Reduce internal buffering + }, + ) + retry_cnt = 0 + except Exception as e: + retry_cnt += 1 + if retry_cnt >= max_retries: + # Catch the 404 and stop the thread instead of retrying + main_app_logger.info( + f"Stream {self.source} ended or not found. Closing reader." + ) + self.stopped = True + break + wait_time = retry_cnt * 2 + main_app_logger.warning( + f"Connection failed ({e}). Retry {retry_cnt}/{max_retries} in {wait_time}s..." + ) + time.sleep(wait_time) + continue + + streams = self.container.streams.get(video=0) + self.stream = streams[0] if isinstance(streams, list) else streams + + # INITIALIZE DECODER if missing (Fixes the NoneType Error) + if self.nv_dec is None: + # Sync with PyTorch + self.cuda_lib.cuCtxSetCurrent(ctypes.c_void_p(self.cuda_ctx_handle)) + torch_stream = torch.cuda.current_stream().cuda_stream + + # Detect Codec + codec_map = { + "hevc": nvc.cudaVideoCodec.HEVC, + "h264": nvc.cudaVideoCodec.H264, + } + nvc_codec = codec_map.get( + self.stream.codec_context.name, nvc.cudaVideoCodec.HEVC + ) + + # Professional cards often support 8K HEVC but are capped at 4K for H.264 + if nvc_codec == nvc.cudaVideoCodec.HEVC: + # Use 8K limits for HEVC + hw_max_w, hw_max_h = 8192, 4320 + else: + # Safely default to 4K limits for H.264 and other codecs + hw_max_w, hw_max_h = 4096, 4096 + + # Ensure max dimensions are at least as large as current stream + hw_max_w = max(hw_max_w, self.stream_width) + hw_max_h = max(hw_max_h, self.stream_height) + + # Determine if the library build supports Ultra Low Latency enums. + try: + latency_mode = nvc.DisplayDecodeLatencyType.ULTRA_LOW_LATENCY + except AttributeError: + latency_mode = nvc.DisplayDecodeLatencyType.NATIVE + + self.nv_dec = nvc.CreateDecoder( + gpuid=self.gpu_id, + codec=nvc_codec, + cudacontext=int(self.cuda_ctx_handle), + cudastream=int(torch_stream), + usedevicememory=1, + maxwidth=hw_max_w, # Force 8K profile support + maxheight=hw_max_h, + latency=latency_mode, + ) + + # Re-initialize BitStream Filter for this session + bsf_map = {"hevc": "hevc_mp4toannexb", "h264": "h264_mp4toannexb"} + bsf_name = bsf_map.get(self.stream.codec_context.name) + local_bsf = ( + av.BitStreamFilterContext(bsf_name, self.stream) + if bsf_name + else None + ) + + target_fps, clip_duration = (self.target_fps, self.clip_duration) + self.stream_width = self.stream.width + self.stream_height = self.stream.height + self.frame_width = self.true_width + self.frame_height = self.true_height + self.input_fps = self.metadata_fps + self.target_fps = ( + target_fps + if target_fps not in [None, 0] and self.input_fps > target_fps + else self.input_fps + ) + if self.input_fps > 0 and self.target_fps > 0: + self.frame_skip = max(1, int(self.input_fps / self.target_fps)) + else: + self.frame_skip = 1 + self.max_frames_per_clip = ( + None + if clip_duration is None + else int(self.target_fps * float(clip_duration)) + ) + self.frame_interval = 1.0 / self.target_fps + self.numFrames = self.total_frames + + self.is_h264_8k = ( + self.stream.codec_context.name == "h264" + and self.stream_width > 4096 + ) + + # --- PATH A: H.264 8K CPU Fallback --- + if self.is_h264_8k: + print( + f"[INFO] Starting CPU-Decode Fallback for {self.source}", + flush=True, + ) + for frame in self.container.decode(video=0): + # if self.stopped: + # break + + # img_array = frame.to_ndarray(format="rgb24") + img_array = frame.to_ndarray(format="bgr24") + # gpu_tensor = ( + # torch.from_numpy(img_array).to("cuda").permute(2, 0, 1) + # ) + self.raw_input.copy_(torch.from_numpy(img_array)) + gpu_tensor = self.raw_input.permute(2, 0, 1) + + # CRITICAL FIX: Break the shared buffer pointer! + # Clones the frame to a private memory block before queueing it. + safe_frame = gpu_tensor.clone().contiguous() + + self.frame_queue.put((safe_frame, self.frame_idx)) + self.frame_idx += 1 + + # if not is_rtsp and self.frame_idx >= 500: + # break # File test limit + + # --- PATH B: HEVC Hardware Acceleration (15 FPS Path) --- + else: + print( + f"[INFO] Starting HW-Accelerated Pump for {self.source}", + flush=True, + ) + # Create an explicit stream packet extractor to isolate bitstream errors + packet_iterator = self.container.demux(self.stream) + + while not self.stopped: + try: + packet = next(packet_iterator) + except (ValueError, OSError, StopIteration): + break + + # Check for corruption/validity safely + is_broken = packet.size == 0 or packet.dts is None + # is_broken = ( + # is_broken + # or getattr(packet, "corrupt", False) + # or getattr(packet, "is_corrupt", False) + # ) + + if is_broken: + # main_app_logger.warning("[WARNING] Corrupt packet detected. Locking sync.") + # self.sync_locked = True + continue + + # ─── OPTIMIZED HARDWARE SYNCHRONIZATION BARRIER ────────────────── + if self.sync_locked: + # Rely explicitly on PyAV header flag definitions instead of packet.size + if getattr(packet, "is_keyframe", False): + main_app_logger.info( + "[SYNC] Valid hardware keyframe intercepted. Unlocking decoder track." + ) + self.sync_locked = False + else: + # Drop incomplete frames to prevent hardware canvas corruption + continue + # ───────────────────────────────────────────────────────────────── + # if self.stopped: + # break + if packet.size == 0 or packet.dts is None: + continue + + # Apply Annex B filter + filtered_packets = ( + local_bsf.filter(packet) if local_bsf else [packet] + ) + + for filtered_packet in filtered_packets: + if self.sync_locked: + continue + + if filtered_packet.size < 10: + continue + + pkt_bytes = bytes(filtered_packet) + # Extract raw memory address from the numpy tuple + ptr_info = np.frombuffer( + pkt_bytes, dtype=np.uint8 + ).__array_interface__["data"] + addr = ( + ptr_info[0] + if isinstance(ptr_info, (tuple, list)) + else ptr_info + ) + nvc_packet = nvc.PacketData() + nvc_packet.bsl_data = int(addr) + nvc_packet.bsl = filtered_packet.size + + try: + for decoded_frame in self.nv_dec.Decode(nvc_packet): + try: + # Zero-Copy Bridge: Hardware Surface -> PyTorch Tensor + gpu_tensor = torch.from_dlpack(decoded_frame) + # 2. CONVERSION: Turn NV12 [YUV] into BGR immediately + converted_bgr = self.nv12_to_bgr_reader( + gpu_tensor + ) + + # CRITICAL FIX: Force a brand-new, isolated memory block allocation + # on the GPU so the decoder cannot overwrite this frame's pixels + # while downstream threads are analyzing the mask. + safe_frame = converted_bgr.clone().contiguous() + + while not self.stopped: + try: + self.frame_queue.put( + ( + safe_frame, + self.frame_idx, + ), + timeout=0.1, + ) + break + except queue.Full: + try: + self.frame_queue.get_nowait() + except queue.Empty: + pass + torch.cuda.current_stream().synchronize() + self.frame_idx += 1 + except ( + av.FFmpegError, + av.InvalidDataError, + RuntimeError, + ) as decode_fault: + main_app_logger.warning( + f"Isolating corrupt video packet fragment: {decode_fault}" + ) + if hasattr(decoded_frame, "Unlock"): + decoded_frame.Unlock() + continue + finally: + # Immediate VRAM Cleanup + if "gpu_tensor" in locals(): + del gpu_tensor + if hasattr(decoded_frame, "Unlock"): + decoded_frame.Unlock() + del decoded_frame + + except Exception as e: + self.sync_locked = True + if any(c in str(e) for c in ["700", "208"]): + self.nv_dec = None + break + # if not is_rtsp and self.frame_idx >= 500: + # break # File test limit + + # --- UNIVERSAL EXIT LOGIC --- + if not is_rtsp: + print( + f"[DEBUG] Reached End of File: {self.source}. Exiting thread.", + flush=True, + ) + self.stopped = True + break + else: + # If we reach here and it's RTSP, the demuxer stopped yielding + print( + "[WARNING] RTSP glitch detected. Closing container and retrying...", + flush=True, + ) + if self.container: + # Explicitly flush streams before closing + for s in self.container.streams: + s.codec_context.flush_buffers() + self.container.close() + self.container = None + + if local_bsf: + try: + local_bsf.filter(None) + except Exception: + pass + + # Force close the C++ decoder context layer before resetting the pointer + if self.nv_dec is not None: + try: + if hasattr(self.nv_dec, "Close"): + self.nv_dec.Close() + except Exception: + pass + del self.nv_dec + + torch.cuda.synchronize() + torch.cuda.empty_cache() + self.nv_dec = None + while not self.frame_queue.empty(): + try: + self.frame_queue.get_nowait() + except Exception: + break + time.sleep(0.5) # Wait before reconnection attempt + + except Exception as e: + print(f"[EXCEPTION] Reader Thread Failure: {e}", flush=True) + traceback.print_exc() + if not is_rtsp: + self.stopped = True + break + + self.sync_locked = True + if self.nv_dec is not None: + try: + # Force a hardware surface close step to release hardware rings instantly + if hasattr(self.nv_dec, "Close"): + self.nv_dec.Close() + except Exception: + pass + del self.nv_dec + + # Force CUDA to synchronize and clear driver error state flags safely + torch.cuda.synchronize() + torch.cuda.empty_cache() + self.nv_dec = None + time.sleep(1.0) # Backoff for RTSP reconnection + + self.stopped = True + + def release(self): + """Safely flushes the decoder and closes the connection.""" + print("Closing HybridReader...") + self.stopped = True # Signal thread to stop + time.sleep(0.5) + + if self.nv_dec and self.frame_idx > 0: + try: + self.nv_dec.Decode(nvc.PacketData()) # Flush + del self.nv_dec + self.nv_dec = None + except Exception: + pass + if self.container: + self.container.close() + self.container = None + + # def nv12_to_bgr_reader(self, nv12_tensor): # , h, w, is_8k=False): + # """Internal reader helper to convert raw hardware surfaces to BGR.""" + # h, w = self.frame_height, self.frame_width + # is_8k = self.is_h264_8k + # with torch.no_grad(): + # if is_8k: + # # 8K Path: Extract and unzipper interleaved UV + # y = nv12_tensor[0:1, :, :].half() + # uv_raw = nv12_tensor[1:2, :, :].half() + # u = uv_raw[:, :, 0::2] + # v = uv_raw[:, :, 1::2] + # u = F.interpolate(u.unsqueeze(0), size=(h, w), mode="nearest").squeeze( + # 0 + # ) + # v = F.interpolate(v.unsqueeze(0), size=(h, w), mode="nearest").squeeze( + # 0 + # ) + # else: + # # Standard NV12 Path + # y = nv12_tensor[:h, :w].unsqueeze(0).half() + + # uv = ( + # nv12_tensor[h:, :w] + # .reshape(h // 2, w // 2, 2) + # .permute(2, 0, 1) + # .unsqueeze(0) + # .half() + # ) + # uv_up = F.interpolate(uv, size=(h, w), mode="nearest") + # u, v = uv_up[0, 0:1, :, :], uv_up[0, 1:2, :, :] + + # # BT.709 Math (Natural Color) + # y = (y - 16.0) * 1.164 + # u, v = u - 128.0, v - 128.0 + + # r = y + 1.793 * v + # g = y - 0.213 * u - 0.533 * v + # b = y + 2.112 * u + + # # Stack as BGR [B, G, R] for OpenCV/Browser compatibility + # return torch.cat([b, g, r], dim=0).clamp(0, 255).to(torch.uint8) + + def nv12_to_bgr_reader(self, nv12_tensor): + """Internal reader helper to convert raw hardware surfaces to BGR.""" + h, w = self.frame_height, self.frame_width + is_8k = self.is_h264_8k + + # EXTRACT TRUE STRIDE: Get the actual hardware allocation width + # which includes the hidden byte-alignment padding columns. + stride_w = nv12_tensor.shape[1] + + with torch.no_grad(): + if is_8k: + # 8K Path: Slice using stride_w, then strip padding out cleanly + y = nv12_tensor[0:1, :, :stride_w].half() + uv_raw = nv12_tensor[1:2, :, :stride_w].half() + u = uv_raw[:, :, 0::2] + v = uv_raw[:, :, 1::2] + u = F.interpolate(u.unsqueeze(0), size=(h, w), mode="nearest").squeeze( + 0 + ) + v = F.interpolate(v.unsqueeze(0), size=(h, w), mode="nearest").squeeze( + 0 + ) + else: + # Standard NV12 Path + # 1. Slice using the full hardware stride_w to preserve vertical row boundaries + y_padded = nv12_tensor[:h, :stride_w].half() + uv_padded = nv12_tensor[h:, :stride_w].half() + + # 2. Crop out the hardware padding columns horizontally to get clean spatial grids + y = y_padded[:, :w].unsqueeze(0) + + uv = ( + uv_padded[:, :w] + .reshape(h // 2, w // 2, 2) + .permute(2, 0, 1) + .unsqueeze(0) + ) + uv_up = F.interpolate(uv, size=(h, w), mode="nearest") + u, v = uv_up[0, 0:1, :, :], uv_up[0, 1:2, :, :] + + # BT.709 Math (Natural Color) + y = (y - 16.0) * 1.164 + u, v = u - 128.0, v - 128.0 + + r = y + 1.793 * v + g = y - 0.213 * u - 0.533 * v + b = y + 2.112 * u + + # Stack as BGR [B, G, R] + out_tensor = torch.cat([b, g, r], dim=0).clamp(0, 255).to(torch.uint8) + + # CRITICAL PIECE: Force memory to be completely linear and sequentially packed + # before it enters the queue or gets sliced by the sub-frame window pipeline + return out_tensor.contiguous() + + @property + def true_height(self): + """ + Returns the logical video height. + Handles cases where metadata might report the 1.5x NV12 buffer height. + """ + # if self.nv_dec: + # return int(self.nv_dec.Height()) # Get from decoder instead of stream + # return int(self.stream.height) # / 1.5) + return self.stream.height if self.stream else 0 + + @property + def true_width(self): + # if self.nv_dec: + # return self.nv_dec.Width() + # return self.stream.width + return self.stream.width if self.stream else 0 + + @property + def metadata_fps(self): + """Returns the FPS defined in the video metadata/header.""" + if self.stream and self.stream.average_rate: + return float(self.stream.average_rate) + return 0.0 + + @property + def total_frames(self): + """Returns the total number of frames defined in the file metadata.""" + if self.stream: + # Check nb_frames first + if self.stream.frames > 0: + return self.stream.frames + return 0 # Returns 0 for live streams or files with missing metadata diff --git a/fastapi/include/utils.py b/fastapi/include/utils.py new file mode 100644 index 0000000..0cae055 --- /dev/null +++ b/fastapi/include/utils.py @@ -0,0 +1,1396 @@ +# Copyright (C) 2025 Intel Corporation + +import os +import queue +import subprocess +import time +import traceback +from dataclasses import dataclass +from math import ceil +from pathlib import Path +from random import randint + +import cupy +import cupyx.scipy +import cupyx.scipy.ndimage +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from include.default_configs import ( + BKGD_SUB_INCLUDE_HISTORY, + BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE, + BKGD_SUB_INCLUDE_HISTORY_METHOD, + BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE, + BKGD_SUB_MOG2_DETECTSHADOWS, + BKGD_SUB_MOG2_HISTORY, + BKGD_SUB_MOG2_LR, + BKGD_SUB_MOG2_VARTHRESHOLD, + CLIP_DURATION_DEFAULT, + CODE_DIR_DEFAULT, + CUSTOM_MODEL_FLAG_DEFAULT, + DBHOST_DEFAULT, + DBPORT_DEFAULT, + DEBUG_DEFAULT, + DETECTION_THRESHOLD_DEFAULT, + DETECTION_TYPE_DEFAULT, + DEVICE_DEFAULT, + DILATE_KERNEL_SIZE, + DISPLAY_FRAME_QUALITY, + DISPLAY_FRAME_SIZE, + ENABLE_QUERYING_DEFAULT, + INGESTION_DEFAULT, + MAX_DETECTIONS, + MAX_WORKERS, + MODEL_H, + MODEL_MAX_BATCH_SIZE, + MODEL_NAME_DEFAULT, + MODEL_PRECISION, + MODEL_W, + OMIT_DETECTIONS_FLAG_DEFAULT, + RESIZE_FLAG_DEFAULT, + ROI_BB_FULL_RES_PADDING, + ROI_CONTAINMENT_THRESH, + ROI_DISTANCE_THRESH_RATIO, + ROI_MAX_RELATIVE_SIZE_RATIO, + ROI_MERGE_SIZE_LIMIT, + ROI_MIN_AREA_RATIO, + SHARED_OUTPUT_DEFAULT, + SMART_FILTERING_ENABLED, + SMART_FILTERING_PIXEL_CONSTRAINT, + TARGET_FPS, + TEST_MODE_DEFAULT, + THICKNESS, + THRESHOLD_MAX_VALUE, + THRESHOLD_VALUE, + TMP_LOCATION_DEFAULT, + UDF_HOST_DEFAULT, + UDF_PORT_DEFAULT, +) +from pydantic import BaseModel + +import vdms + +""" +GENERAL DEFINITIONS/FUNCTIONS +""" + + +def get_freest_gpu(): + # Queries free memory from nvidia-smi + command = "nvidia-smi --query-gpu=memory.free --format=csv,nounits,noheader" + memory_free = [ + int(x) + for x in subprocess.check_output(command.split()).decode("ascii").split("\n") + if x + ] + + # Return index of GPU with maximum free memory + return memory_free.index(max(memory_free)) + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path + + +def str2bool(in_val): + if isinstance(in_val, bool): + return in_val + + if not isinstance(in_val, str): + raise ValueError(f"{in_val} is not a bool or string") + + if in_val.title() == "True": + return True + else: + return False + + +PROJECT_PATH = Path(__file__).parent.parent + +DEBUG_FLAG_DEFAULT = True if DEBUG_DEFAULT == "1" else False + +LOCKTIMEOUT_RETRIES = 5 + + +class PipelineConfig: + def __init__(self, **kwargs): + # Fallback to env var if not explicitly passed + + # GENERAL + self.CODE_DIR = kwargs.get("CODE_DIR", CODE_DIR_DEFAULT) + self.CUSTOM_MODEL_FLAG = str2bool( + kwargs.get("CUSTOM_MODEL_FLAG", CUSTOM_MODEL_FLAG_DEFAULT) + ) + self.DEBUG = kwargs.get("DEBUG", DEBUG_DEFAULT) + self.DEBUG_FRAME_LIMIT = int(kwargs.get("DEBUG_FRAME_LIMIT", 100)) + self.DEVICE = kwargs.get("DEVICE", DEVICE_DEFAULT) + self.MAX_WORKERS = int(kwargs.get("MAX_WORKERS", MAX_WORKERS)) + self.OMIT_DETECTIONS_FLAG = str2bool( + kwargs.get("OMIT_DETECTIONS_FLAG", OMIT_DETECTIONS_FLAG_DEFAULT) + ) + self.SHARED_OUTPUT = kwargs.get("SHARED_OUTPUT", SHARED_OUTPUT_DEFAULT) + self.TEST_MODE = str2bool(kwargs.get("TEST_MODE", TEST_MODE_DEFAULT)) + self.TMP_LOCATION = kwargs.get("TMP_LOCATION", TMP_LOCATION_DEFAULT) + + # VIDEO WRITER + CLIP_DURATION = kwargs.get("CLIP_DURATION", CLIP_DURATION_DEFAULT) + target_fps = kwargs.get("TARGET_FPS", TARGET_FPS) + self.CLIP_DURATION = ( + None if CLIP_DURATION in ["None", None] else float(CLIP_DURATION) + ) + self.TARGET_FPS = None if target_fps in [None, "None"] else float(target_fps) + + # VDMS + self.DBHOST = kwargs.get("DBHOST", DBHOST_DEFAULT) + self.DBPORT = int(kwargs.get("DBPORT", DBPORT_DEFAULT)) + self.ENABLE_QUERYING = str2bool( + kwargs.get("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT) + ) + self.INGESTION = kwargs.get("INGESTION", INGESTION_DEFAULT) + self.UDF_HOST = kwargs.get("UDF_HOST", UDF_HOST_DEFAULT) + self.UDF_PORT = int(kwargs.get("UDF_PORT", UDF_PORT_DEFAULT)) + + # MODEL + self.DETECTION_THRESHOLD = float( + kwargs.get("DETECTION_THRESHOLD", DETECTION_THRESHOLD_DEFAULT) + ) + self.MAX_DETECTIONS = int(kwargs.get("MAX_DETECTIONS", MAX_DETECTIONS)) + self.MODEL_H = int(kwargs.get("MODEL_H", MODEL_H)) + self.MODEL_W = int(kwargs.get("MODEL_W", MODEL_W)) + self.MODEL_MAX_BATCH_SIZE = int( + kwargs.get("MODEL_MAX_BATCH_SIZE", MODEL_MAX_BATCH_SIZE) + ) + self.MODEL_NAME = kwargs.get("MODEL_NAME", MODEL_NAME_DEFAULT) + self.MODEL_PRECISION = kwargs.get("MODEL_PRECISION", MODEL_PRECISION) + self.SHARED_MODEL = kwargs.get("SHARED_MODEL", False) + + # PIPELINE + self.DISABLE_DETECTION = kwargs.get("DISABLE_DETECTION", False) + self.SMART_FILTERING_PIXEL_CONSTRAINT = SMART_FILTERING_PIXEL_CONSTRAINT + self.BKGD_SUB_INCLUDE_HISTORY = BKGD_SUB_INCLUDE_HISTORY + self.BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE = ( + BKGD_SUB_INCLUDE_HISTORY_DILATE_KERNEL_SIZE + ) + self.BKGD_SUB_INCLUDE_HISTORY_METHOD = BKGD_SUB_INCLUDE_HISTORY_METHOD + self.BKGD_SUB_MOG2_DETECTSHADOWS = BKGD_SUB_MOG2_DETECTSHADOWS + self.BKGD_SUB_MOG2_HISTORY = BKGD_SUB_MOG2_HISTORY + self.BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE = ( + BKGD_SUB_INCLUDE_HISTORY_TEMPORAL_SIZE + ) + self.BKGD_SUB_MOG2_LR = BKGD_SUB_MOG2_LR + self.BKGD_SUB_MOG2_VARTHRESHOLD = BKGD_SUB_MOG2_VARTHRESHOLD + self.DILATE_KERNEL_SIZE = DILATE_KERNEL_SIZE + self.RESIZE_FLAG = str2bool(kwargs.get("RESIZE_FLAG", RESIZE_FLAG_DEFAULT)) + self.ROI_BB_FULL_RES_PADDING = int( + kwargs.get("ROI_BB_FULL_RES_PADDING", ROI_BB_FULL_RES_PADDING) + ) + self.ROI_MAX_RELATIVE_SIZE_RATIO = float( + kwargs.get("ROI_MAX_RELATIVE_SIZE_RATIO", ROI_MAX_RELATIVE_SIZE_RATIO) + ) + self.ROI_MERGE_SIZE_LIMIT = int( + kwargs.get("ROI_MERGE_SIZE_LIMIT", ROI_MERGE_SIZE_LIMIT) + ) + self.ROI_MIN_AREA_RATIO = ROI_MIN_AREA_RATIO + self.ROI_DISTANCE_THRESH_RATIO = ROI_DISTANCE_THRESH_RATIO + self.ROI_CONTAINMENT_THRESH = ROI_CONTAINMENT_THRESH + self.ROI_RETURN_BYTES = str2bool(kwargs.get("ROI_RETURN_BYTES", True)) + self.THRESHOLD_MAX_VALUE = int( + kwargs.get("THRESHOLD_MAX_VALUE", THRESHOLD_MAX_VALUE) + ) + self.THRESHOLD_VALUE = int(kwargs.get("THRESHOLD_VALUE", THRESHOLD_VALUE)) + + # VISUALIZATION + self.DETECTION_TYPE = kwargs.get("DETECTION_TYPE", DETECTION_TYPE_DEFAULT) + self.DISPLAY_FRAME_QUALITY = int( + kwargs.get("DISPLAY_FRAME_QUALITY", DISPLAY_FRAME_QUALITY) + ) + self.DISPLAY_FRAME_SIZE = kwargs.get("DISPLAY_FRAME_SIZE", DISPLAY_FRAME_SIZE) + self.THICKNESS = int(kwargs.get("THICKNESS", THICKNESS)) + + # VARS WITH DEPENDENCIES + Path(self.SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) + self.device_input = self.DEVICE.lower() if self.DEVICE == "CPU" else "cuda" + self.DEBUG_FLAG = True if self.DEBUG == "1" else False + + if self.DETECTION_TYPE == "motion" and self.ENABLE_QUERYING: + # self.ENABLE_QUERYING = False + # self.DISPLAY_FRAME_QUALITY = 100 + self.THICKNESS = 10 + + self.sf_enabled = kwargs.get("SMART_FILTERING_ENABLED", SMART_FILTERING_ENABLED) + if self.CUSTOM_MODEL_FLAG: + self.model_path = f"{self.CODE_DIR}/resources/models/ultralytics/custom_models/{self.MODEL_NAME}" + else: + self.model_path = f"{self.CODE_DIR}/resources/models/ultralytics/{self.MODEL_NAME}/{self.MODEL_PRECISION}/{self.MODEL_NAME}" + + if not self.sf_enabled: + self.model_path += "_noSF" + + if self.DEVICE == "GPU": + self.model_path += ".engine" + + # Force PyTorch to initialize the CUDA context + if torch.cuda.is_available(): + best_gpu_index = get_freest_gpu() + os.environ["CUDA_VISIBLE_DEVICES"] = str(best_gpu_index) + torch.cuda.set_device(0) + torch.cuda.empty_cache() + else: + self.model_path += "_openvino_model/" + + +class VDMSPool: + def __init__(self, host, port, size=5): + self.host = host + self.port = port + self.size = size + self.pool = queue.Queue(maxsize=size) + self.populate() + + def populate(self): + # Pre-populate the pool with authenticated connections + for _ in range(self.size): + self.pool.put(self._create_connection()) + + def _create_connection(self): + client = vdms.vdms() + client.connect(self.host, self.port) + return client + + def get_connection(self): + # Borrow a connection (blocks if pool is empty) + return self.pool.get(block=True, timeout=10) + + def return_connection(self, conn): + # Put the connection back for reuse + self.pool.put(conn) + + +ERR_KEYWORDS = [ + "timeout", + "null search iterator", + "outoftransactions", + "internal server", +] + + +# Plot variables +THICKNESS_SCALE_FACTOR = 1e-3 +FONT_SCALE_FACTOR = 1e-3 + + +YOLO_CLASS_NAMES = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", +] + +PLOT_HEXS = ( + "042AFF", + "0BDBEB", + "F3F3F3", + "00DFB7", + "111F68", + "FF6FDD", + "FF444F", + "CCED00", + "00F344", + "BD00FF", + "00B4FF", + "DD00BA", + "00FFFF", + "26C000", + "01FFB3", + "7D24FF", + "7B0068", + "FF1B6C", + "FC6D2F", + "A2FF0B", +) + +DETECTION_COLORS = [] +for h in PLOT_HEXS: + DETECTION_COLORS.append( + tuple(int(f"#{h}"[1 + i : 1 + i + 2], 16) for i in (0, 2, 4)) + ) + + +# if DEVICE == "GPU": +bbox_kernel = cupy.ElementwiseKernel( + "S label_image, int32 width", + "raw T bboxes", + """ + if (label_image > 0) { + int label = (int)label_image; + + int y = i / width; + int x = i % width; + // Atomic operations to find min/max coordinates + atomicMin(&bboxes[label * 4 + 0], y); // min_y + atomicMin(&bboxes[label * 4 + 1], x); // min_x + atomicMax(&bboxes[label * 4 + 2], y); // max_y + atomicMax(&bboxes[label * 4 + 3], x); // max_x + } + """, + "bbox_kernel", +) + +bbox_area_kernel = cupy.ElementwiseKernel( + "S label_image, int32 width", + "raw T bboxes, raw T areas", + """ + if (label_image > 0) { + int label = (int)label_image; + int y = i / width; + int x = i % width; + + // 1. Update Bounding Box + atomicMin(&bboxes[label * 4 + 0], y); // min_y + atomicMin(&bboxes[label * 4 + 1], x); // min_x + atomicMax(&bboxes[label * 4 + 2], y); // max_y + atomicMax(&bboxes[label * 4 + 3], x); // max_x + + // 2. Increment Area (Count pixels) + atomicAdd(&areas[label], 1); + } + """, + "bbox_area_kernel", +) + +threshold_dilate_fused_kernel = cupy.ElementwiseKernel( + "T mask, int32 threshold, int32 width, int32 height", + "raw T morphed", + """ + // 1. Threshold + bool is_active = mask > threshold; + + // 2. Simple 3x3 Dilation (Fuse directly into output) + if (is_active) { + int y = i / width; + int x = i % width; + + // 2. 3x3 Dilation Expansion + // Writes 255 to the neighbors of any pixel above threshold + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + int ny = y + dy; + int nx = x + dx; + if (ny >= 0 && ny < height && nx >= 0 && nx < width) { + morphed[ny * width + nx] = 255; + } + } + } + } + """, + "threshold_dilate_fused", +) + +# This CUDA C++ code finds min/max for all labels in ONE pass over the mask +DETECTION_ACCEL_KERNEL = cupy.RawKernel( + r""" + extern "C" __global__ + void fast_detect(const unsigned char* bgs_mask, unsigned char* out_mask, int pitch, int w, int h, float thresh) { + int x = blockIdx.x * blockDim.x + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x > 0 && x < w-1 && y > 0 && y < h-1) { + unsigned char val = bgs_mask[y * pitch + x]; + unsigned char res = (val > thresh) ? 255 : 0; + if (res == 0) { + if (bgs_mask[(y-1)*pitch + x] > thresh || bgs_mask[(y+1)*pitch + x] > thresh || + bgs_mask[y*pitch + (x-1)] > thresh || bgs_mask[y*pitch + (x+1)] > thresh) { + res = 255; + } + } + out_mask[y * pitch + x] = res; + } + } + """, + "fast_detect", +) + +# Fused Kernel: Single-pass Bounding Box Extraction with Stride Support +BOUNDS_KERNEL = cupy.RawKernel( + r""" +extern "C" __global__ +void find_bounds(const unsigned char* labeled_ptr, int step, int width, int height, int num_labels, int* x1, int* y1, int* x2, int* y2) { + int x = blockIdx.x * blockDim.x + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x < width && y < height) { + const int* row = (const int*)(labeled_ptr + y * step); + int label = row[x]; + if (label > 0 && label <= num_labels) { + atomicMin(&x1[label], x); + atomicMin(&y1[label], y); + atomicMax(&x2[label], x); + atomicMax(&y2[label], y); + } + } +} +""", + "find_bounds", +) + + +def tensor2opencv(frame_source, device_input, is_bgr=True, resize_h=640, resize_w=640): + if torch.is_tensor(frame_source): + # .contiguous() is CRITICAL here to fix the "shredded" look + temp = frame_source.squeeze(0) if frame_source.ndim == 4 else frame_source + img_cpu = temp.permute(1, 2, 0).contiguous().cpu().numpy() + elif hasattr(frame_source, "download"): + img_cpu = frame_source.download() + else: + img_cpu = np.ascontiguousarray(frame_source) + + # Fix Shape: restore spatial grid if flattened + if img_cpu.ndim == 3 and img_cpu.shape[0] == 1: + img_cpu = img_cpu.reshape((resize_h, resize_w, 3)) + + # Fix Visibility: ONLY multiply if it's actually floating point + # If uint8 is multiplied by 255, it wraps around and creates "neon" colors + if img_cpu.dtype != np.uint8: + if img_cpu.max() <= 1.0: + img_cpu = (img_cpu * 255).clip(0, 255).astype(np.uint8) + else: + img_cpu = img_cpu.astype(np.uint8) + + # Color Space: Standardize to BGR for imwrite + if not is_bgr: + if len(img_cpu.shape) == 3: + # Swap RGB (Torch/Decoder) -> BGR (OpenCV) + # img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_RGB2BGR) + # Only swap if the source is RGB (GPU Path) + # CPU path is already BGR from OpenCV reader + if device_input == "cuda": + img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_RGB2BGR) + else: + # Ensure it's contiguous for saving + img_cpu = np.ascontiguousarray(img_cpu) + else: + img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_GRAY2BGR) + + return img_cpu + + +def gpumat2cupy(gpu_mat): + """Bridge OpenCV GpuMat to CuPy without copying data.""" + # Get properties from GpuMat + w, h = gpu_mat.size() + # Check if it's 3-channel (CV_8UC3) or 1-channel (CV_8UC1) + channels = 3 if gpu_mat.type() == cv2.CV_8UC3 else 1 + + if channels == 3: + shape = (h, w, 3) + # strides = (bytes_per_row, bytes_per_pixel, bytes_per_channel) + strides = (gpu_mat.step, 3, 1) + else: + shape = (h, w) + strides = (gpu_mat.step, 1) + + # Map OpenCV types to CuPy typestrs + # CV_8UC1 is 'u1' (unsigned 1-byte), etc. + # type_map = {cv2.CV_8U: "|u1", cv2.CV_32F: " 0 && label <= num_labels) { + // Atomically update the bounding box for this specific label + atomicMin(&x1[label], x); + atomicMin(&y1[label], y); + // +1 ensures the box captures the full pixel and matches OpenCV ROI logic + atomicMax(&x2[label], x + 1); + atomicMax(&y2[label], y + 1); + } + } +} +""" + +# Compile the kernel once +get_bounds_kernel = cupy.RawKernel(BOUNDS_KERNEL_CODE, "get_bounds") + + +def merge_boxes_gpu(raw_boxes, gap_limit=10, size_limit=1000): + """ + Refined Parallel Merger with Size Constraints. + Prevents merges that would create boxes larger than size_limit. + """ + if raw_boxes.shape[0] <= 1: + return raw_boxes + + x1, y1, x2, y2 = raw_boxes.unbind(1) + + # 1. Calculate pairwise gaps (Existing logic) + h_gaps = torch.max( + torch.zeros(1, device=raw_boxes.device), + torch.max(x1.unsqueeze(0) - x2.unsqueeze(1), x1.unsqueeze(1) - x2.unsqueeze(0)), + ) + v_gaps = torch.max( + torch.zeros(1, device=raw_boxes.device), + torch.max(y1.unsqueeze(0) - y2.unsqueeze(1), y1.unsqueeze(1) - y2.unsqueeze(0)), + ) + + # 2. NEW: Calculate potential union dimensions for ALL pairs [N, N] + # We find the min/max coordinates if box i and box j were merged + union_x1 = torch.min(x1.unsqueeze(0), x1.unsqueeze(1)) + union_y1 = torch.min(y1.unsqueeze(0), y1.unsqueeze(1)) + union_x2 = torch.max(x2.unsqueeze(0), x2.unsqueeze(1)) + union_y2 = torch.max(y2.unsqueeze(0), y2.unsqueeze(1)) + + union_w = union_x2 - union_x1 + union_h = union_y2 - union_y1 + + # 3. ADJACENCY MASK: Must be close AND the result must be under the limit + # This prevents the creation of massive "megaboxes" + adj = ( + (h_gaps < gap_limit) + & (v_gaps < gap_limit) + & (union_w < size_limit) + & (union_h < size_limit) + ) + + # 4. Parallel Connected Components (Existing logic) + components = torch.arange(raw_boxes.shape[0], device=raw_boxes.device) + r = 3 + for _ in range(r): + components = torch.max(adj * components, dim=1).values + + unique_ids = components.unique() + merged = [] + for i in unique_ids: + mask = components == i + merged.append( + torch.cat( + [raw_boxes[mask, :2].min(0).values, raw_boxes[mask, 2:].max(0).values] + ) + ) + + return torch.stack(merged) + + +def merge_boxes_cpu(boxes, gap_limit=10): + """ + Greedy merge in 640x640 space to consolidate swarm fragments. + Input: List of [x1, y1, x2, y2] within [0, 640] + """ + if not boxes: + return [] + + # O(N log N) sort by X for early exit optimization + boxes = sorted(boxes, key=lambda x: x[0]) + merged = [] + + while boxes: + curr = boxes.pop(0) + i = 0 + while i < len(boxes): + test = boxes[i] + # Early exit: horizontal gap exceeds limit + if test[0] - curr[2] > gap_limit: + break + + # Check vertical gap + y_dist = max(0, test[1] - curr[3], curr[3] - test[1]) + if y_dist <= gap_limit: + # Expand curr box to include test + curr = [ + min(curr[0], test[0]), + min(curr[1], test[1]), + max(curr[2], test[2]), + max(curr[3], test[3]), + ] + boxes.pop(i) + i = 0 # Re-check boundaries + else: + i += 1 + merged.append(curr) + return merged + + +def find_contours_gpu_equivalent( + mask_gpu_mat, stream=None, grid_size=16, limit_640=1000, max_boxes=100 +): + """ + ULTRA-OPTIMIZED: Grid-based Region Proposal. + Reduces N to prevent merger bottlenecks. Latency: <0.5ms. + """ + # 1. Zero-copy bridge: GpuMat -> CuPy -> Torch + mask_cp = gpumat2cupy(mask_gpu_mat) + mask_tensor = torch.as_tensor(mask_cp, device="cuda") + + # # 2. Downsample via Max Pooling (Acts as Denoise + Grouper) + # # A 32x32 grid on 640x640 creates a 20x20 matrix (400 cells max) + # pooled = F.max_pool2d( + # mask_tensor.unsqueeze(0).unsqueeze(0).float(), + # kernel_size=grid_size, + # stride=grid_size + # ).squeeze() + + # # 3. Get Indices of Motion + # indices = torch.nonzero(pooled > 0) + # A grid_size of 32 has 1,024 pixels. + # We require at least 5% (approx 50 pixels) to be white to trigger a ROI. + # This 'math' kills terrain shimmer but keeps solid drone blobs. + # density_threshold = (grid_size * grid_size) * 0.05 + + # Use torch.count_nonzero if using AvgPool, or stick to pooled with threshold + # Since 'pooled' from MaxPool is just the max value (0 or 255), + # we should use F.avg_pool2d instead to get a density map: + + density_map = F.avg_pool2d( + mask_tensor.unsqueeze(0).unsqueeze(0).float(), + kernel_size=grid_size, + stride=grid_size, + ).squeeze() + + # Define your sensitivity (e.g., 5% density) + # If a block is 5% full of 'white' (255) pixels, the average will be 12.75 + density_threshold = 2 # int((grid_size * grid_size) * 0.01) + + # 255 * 0.10 means the grid cell must be 10% white pixels + indices = torch.nonzero(density_map > density_threshold) + + # EARLY EXIT: No motion detected, return empty + if indices.shape[0] == 0: + return torch.empty((0, 4), device="cuda") + # 2. SORT BY DENSITY: Get values for each index + # We pull the density values for every 'hot' cell + densities = density_map[indices[:, 0], indices[:, 1]] + + # 3. Get the sort order (Descending: highest density first) + _, sort_order = torch.sort(densities, descending=True) + indices = indices[sort_order] + + # CAP N: If scene is too noisy, take top regions to save the merger + if indices.shape[0] > max_boxes: + indices = indices[:max_boxes] + + # 4. Map back to 640p Bounding Boxes + y1, x1 = indices[:, 0] * grid_size, indices[:, 1] * grid_size + y2, x2 = y1 + grid_size, x1 + grid_size + raw_boxes = torch.stack([x1, y1, x2, y2], dim=1).float() + + # 5. Merge adjacent grid blocks + # gap_limit=grid_size+2 ensures diagonal/nearby blocks connect + return merge_boxes_gpu(raw_boxes, gap_limit=grid_size * 2, size_limit=limit_640) + + +def find_contours_gpu_equivalentv1(mask_gpu_mat, stream=None): + """ + GPU equivalent to cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + Returns: torch.Tensor [N, 4] containing (x1, y1, x2, y2) in analysis space. + """ + # Bridge OpenCV GpuMat to CuPy (Zero-Copy) + w, h = mask_gpu_mat.size() + ptr = mask_gpu_mat.cudaPtr() + pitch_bytes = mask_gpu_mat.step + + mask_cp = cupy.ndarray( + (h, w), + dtype=cupy.uint8, + memptr=cupy.cuda.MemoryPointer( + cupy.cuda.UnownedMemory(ptr, pitch_bytes * h, mask_gpu_mat), 0 + ), + strides=(pitch_bytes, 1), + ) + + # Use the stream pointer if provided, otherwise default to 0 (Null Stream) + stream_ptr = stream.cudaPtr() if stream else 0 + + with cupy.cuda.ExternalStream(stream_ptr): + # Labeling (Equivalent to finding connected components) + # labeled is an int32 array where every 'blob' has a unique number + structure = cupy.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) + labeled, num_labels = cupyx.scipy.ndimage.label(mask_cp, structure=structure) + + if num_labels == 0: + return torch.empty((0, 4), device="cuda") + + # Setup Bounding Box Buffers + x1 = cupy.full((num_labels + 1,), w, dtype=cupy.int32) + y1 = cupy.full((num_labels + 1,), h, dtype=cupy.int32) + x2 = cupy.full((num_labels + 1,), -1, dtype=cupy.int32) + y2 = cupy.full((num_labels + 1,), -1, dtype=cupy.int32) + + # Run the Bounds Kernel + # IMPORTANT: Use labeled.strides[0]//4 to get the pitch in elements + pitch_elements = labeled.strides[0] // 4 + tpb = (16, 16) + bpg = ((w + tpb[0] - 1) // tpb[0], (h + tpb[1] - 1) // tpb[1]) + + get_bounds_kernel( + bpg, tpb, (labeled, pitch_elements, w, h, num_labels, x1, y1, x2, y2) + ) + + # Stack and return as Torch Tensor for YOLO/Drawing + # We skip index 0 as it represents the background (black) + # boxes = torch.stack( + # [ + # torch.as_tensor(x1[1:], device="cuda"), + # torch.as_tensor(y1[1:], device="cuda"), + # torch.as_tensor(x2[1:], device="cuda"), + # torch.as_tensor(y2[1:], device="cuda"), + # ], + # dim=1, + # ).float() + boxes = cupy.column_stack((x1[1:], y1[1:], x2[1:], y2[1:])) + + return torch.as_tensor(boxes, device="cuda").float() + + +def get_detection_color(index, is_bgr=False): + ind = int(index) % len(PLOT_HEXS) + color = DETECTION_COLORS[ind] + if is_bgr: + return (color[2], color[1], color[0]) + else: + return color + + +def get_line_thickness(npixels, ref_pixels=(1280 * 720)): + ref_thickness = 1 + factor = npixels / ref_pixels + thickness = int(ref_thickness * factor) + if thickness < 1: + thickness = 1 + return thickness + + +def draw_label( + image, + label, + txt_bt_lft_corner, + font_face=cv2.FONT_HERSHEY_SIMPLEX, + color=(255, 255, 255), + padding=5, +): + height, width, _ = image.shape + + # Scale font and thickness based on the image's smaller dimension + scaled_font_scale = min(width, height) * FONT_SCALE_FACTOR + scaled_thickness = max(1, ceil(min(width, height) * THICKNESS_SCALE_FACTOR)) + + # Get text size and define position for the label background + (label_W, label_H), baseline = cv2.getTextSize( + label, font_face, scaled_font_scale, scaled_thickness + ) + label_y1 = (txt_bt_lft_corner[0], txt_bt_lft_corner[1] - label_H - padding) + label_y2 = ( + txt_bt_lft_corner[0] + label_W + padding, + txt_bt_lft_corner[1] + baseline, + ) + cv2.rectangle(image, label_y1, label_y2, color, -1) + + # Print label + cv2.putText( + image, + label, + (txt_bt_lft_corner[0] + padding // 2, txt_bt_lft_corner[1] - padding // 2), + font_face, + scaled_font_scale, + (0, 0, 0), # Black text + scaled_thickness, + cv2.LINE_AA, + ) + + +def retry_query( + query, + local_db=None, + num_retries: int = LOCKTIMEOUT_RETRIES, + sleep_timer: int = 0, + DBHOST=DBHOST_DEFAULT, + DBPORT=DBPORT_DEFAULT, + DEBUG_FLAG=DEBUG_FLAG_DEFAULT, +): + # global db + db = local_db if local_db else vdms.vdms().connect(DBHOST, DBPORT) + for ridx in range(num_retries + 1): + response, _ = db.query(query, [[]]) + if "FailedCommand" in response[0] and any( + k in response[0]["info"].lower() for k in ERR_KEYWORDS + ): + err = response[0]["info"] + if DEBUG_FLAG: + query_type = list(query[0].keys())[0] + print( + f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", + flush=True, + ) + if sleep_timer > 0: + time.sleep(sleep_timer) + else: + if DEBUG_FLAG: + print( + f"[DEBUG process_stream] Successful query response: {response}", + flush=True, + ) + break # Continue + return response + + +def format_df_value(value): + if value is None: + return value + if value.isdigit(): + if "." in value: + return float(value) + else: + return int(value) + return value + + +def get_display_frame_in_bytes( + foi, display_size=(960, 540), quality=50, return_bytes=True, device="CPU" +): # Expects BGR + H, W = foi.shape[:2] + dH, dW = display_size + if H == dH and W == dW: + ret, buffer = cv2.imencode( + ".jpg", foi, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ) + # print(f"[get_display_frame_in_bytes] display_size: {foi.shape}", flush=True) + else: + display_frame = cv2.resize(foi, display_size, interpolation=cv2.INTER_NEAREST) + ret, buffer = cv2.imencode( + ".jpg", display_frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality] + ) + # print(f"[get_display_frame_in_bytes] display_size: {display_frame.shape}", flush=True) + if ret and return_bytes: + frame_bytes = buffer.tobytes() + elif ret: + frame_bytes = buffer + else: + frame_bytes = None + + return frame_bytes + + +# Manual FPS calculation if OpenCV reports 0 +def manual_fps_calculation(src, num_frames=10): + if isinstance(src, cv2.VideoCapture): + vid_obj = src + else: + vid_obj = cv2.VideoCapture(src) + + frame_count = 0 + start_t = time.time() + + while frame_count < num_frames: + grabbed, frame = vid_obj.read() + + if not grabbed: + break + + frame_count += 1 + + end_t = time.time() + vid_obj.release() + + elapsed_t = end_t - start_t + + if elapsed_t > 0: + return frame_count / elapsed_t + else: + return 0 + + +def rgb_to_nv12_torch(rgb_tensor): + """ + Fast GPU conversion from RGB to NV12 using PyTorch. + Input: [3, H, W] uint8 tensor on GPU + Output: [H*1.5, W] uint8 tensor on GPU (NV12 format) + """ + _, h, w = rgb_tensor.shape + rgb = rgb_tensor.float() + + # BT.709 RGB to YUV coefficients (standard for HD/8K) + # Y plane (Luma) + y = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] + + # U and V planes (Chroma) + u = -0.1146 * rgb[0] - 0.3854 * rgb[1] + 0.5000 * rgb[2] + 128 + v = 0.5000 * rgb[0] - 0.4542 * rgb[1] - 0.0458 * rgb[2] + 128 + + # Subsample Chroma (4:2:0) + # We take every 2nd pixel to shrink U and V to half-resolution + u_sub = u[::2, ::2] + v_sub = v[::2, ::2] + + # Interleave U and V (NV12 requirement) + # Reshape to [H/2, W] by placing U and V side-by-side at each pixel + uv_interleaved = torch.stack((u_sub, v_sub), dim=2).reshape(h // 2, w) + + # Combine Y and UV planes + # Resulting shape: [H + H/2, W] -> [1.5H, W] + nv12 = torch.cat([y, uv_interleaved], dim=0) + + return torch.clamp(nv12, 0, 255).byte() + + +# Generate and run UDF query +def get_udf_query( + filename_path, + properties, + ingest_mode, + new_size, + id="udf_metadata", + metadata=None, + test_mode=TEST_MODE_DEFAULT, + local_db=None, + UDF_HOST=UDF_HOST_DEFAULT, + UDF_PORT=UDF_PORT_DEFAULT, + DEBUG_FLAG=DEBUG_FLAG_DEFAULT, + DBHOST=DBHOST_DEFAULT, + DBPORT=DBPORT_DEFAULT, +): + query = { + "AddVideo": { + "from_file_path": str(filename_path), # from_server_file + "is_local_file": True, + "properties": properties, + "operations": [ + { + "type": "syncremoteOp", # "remoteOp", + "url": f"http://{UDF_HOST}:{UDF_PORT}/video", + "options": { + "id": id, + "otype": ingest_mode, + "media_type": "video", + "input_sizeWH": new_size, + "filename": properties["Name"], + "ingestion": 1, + }, + } + ], + } + } + + if id == "udf_metadata" and metadata is not None: + query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata + + if test_mode: + return + + filename = str(Path(filename_path).name) + if DEBUG_FLAG: + print( + f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + try: + res = retry_query( + [query], + local_db=local_db, + sleep_timer=randint(1, 5), + DBHOST=DBHOST, + DBPORT=DBPORT, + DEBUG_FLAG=DEBUG_FLAG, + ) + + if DEBUG_FLAG: + print( + f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), + flush=True, + ) + print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) + print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) + except Exception: + e = traceback.format_exc() + print(f"[EXCEPTION] VDMS Query Exception: {e}", flush=True) + + +def _sort_dict_by_frame(in_dict): + def _by_int(key): + return tuple(int(k) for k in key.split("_")) + + return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) + + +# method to send metadata to VDMS once clip is saved +def metadata2vdms( + clip_key, + clip_filename, + clip_metadata, + width, + height, + VDMS_POOL: VDMSPool = None, + DEBUG_FLAG=DEBUG_FLAG_DEFAULT, + INGESTION=INGESTION_DEFAULT, + TEST_MODE=TEST_MODE_DEFAULT, + UDF_HOST=UDF_HOST_DEFAULT, + UDF_PORT=UDF_PORT_DEFAULT, + DBHOST=DBHOST_DEFAULT, + DBPORT=DBPORT_DEFAULT, +): + # global VDMS_POOL + + if VDMS_POOL is None: + # VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + + if DEBUG_FLAG: + print( + f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + + # Send metadata to UDF + properties = { + "Name": clip_key, # .split("/")[-1], + "category": "video_path_rop", + } + + combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} + if "face" in clip_metadata: + for face_frameidx_bbidx, value in clip_metadata["face"].items(): + face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") + max_obj_idx = 0 + for obj_frameidx_bbidx in combined_metadata: + if face_frameidx in obj_frameidx_bbidx: + _, obj_bbidx_ = obj_frameidx_bbidx.split("_") + max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) + + if max_obj_idx > 0: + new_face_bbidx = max_obj_idx + 1 + new_key = f"{face_frameidx}_{new_face_bbidx:04d}" + combined_metadata[new_key] = value + combined_metadata[new_key]["bbId"] = new_key + else: + combined_metadata[face_frameidx_bbidx] = value + + combined_metadata = _sort_dict_by_frame(combined_metadata) + + db = VDMS_POOL.get_connection() + try: + get_udf_query( + clip_filename, + properties, + INGESTION.replace(",", "+"), + (width, height), + id="udf_metadata", + metadata=combined_metadata, + test_mode=TEST_MODE, + local_db=db, + UDF_HOST=UDF_HOST, + UDF_PORT=UDF_PORT, + DEBUG_FLAG=DEBUG_FLAG, + DBHOST=DBHOST, + DBPORT=DBPORT, + ) + + if DEBUG_FLAG: + print( + f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", + flush=True, + ) + finally: + VDMS_POOL.return_connection(db) + + +# method to send metadata to VDMS once clip is saved w/ retry mechanism +def metadata2vdms_with_retry( + clip_key, + clip_filename, + clip_metadata, + width, + height, + max_retries=LOCKTIMEOUT_RETRIES, + VDMS_POOL: VDMSPool = None, + DEBUG_FLAG=DEBUG_FLAG_DEFAULT, + INGESTION=INGESTION_DEFAULT, + TEST_MODE=TEST_MODE_DEFAULT, + UDF_HOST=UDF_HOST_DEFAULT, + UDF_PORT=UDF_PORT_DEFAULT, + DBHOST=DBHOST_DEFAULT, + DBPORT=DBPORT_DEFAULT, +): + """ + Attempts to send metadata to VDMS with exponential backoff. + """ + if VDMS_POOL is None: + # VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + VDMS_POOL = VDMSPool(DBHOST, DBPORT, size=10) + + retry_count = 0 + while retry_count < max_retries: + try: + # Attempt the actual upload (using your existing utility) + success = metadata2vdms( + clip_key, + clip_filename, + clip_metadata, + width, + height, + VDMS_POOL=VDMS_POOL, + DEBUG_FLAG=DEBUG_FLAG, + INGESTION=INGESTION, + TEST_MODE=TEST_MODE, + UDF_HOST=UDF_HOST, + UDF_PORT=UDF_PORT, + DBHOST=DBHOST, + DBPORT=DBPORT, + ) + if success: + print(f" [VDMS] Successfully uploaded {clip_key}") + return True + except Exception as e: + retry_count += 1 + wait_time = 2**retry_count # 2s, 4s, 8s, 16s... + print( + f" [RETRY] VDMS upload failed for {clip_key} (Attempt {retry_count}/{max_retries}). " + f"Retrying in {wait_time}s... Error: {e}" + ) + time.sleep(wait_time) + + print(f" [FAILED] Could not send {clip_key} to VDMS after {max_retries} attempts.") + return False + + +def merge_boxes_limit(boxes, dist_threshold=25, min_area=32, max_size=640): + if len(boxes) == 0: + return [] + + # EARLY FILTER: Remove noise (dots/specks) immediately + # area = width * height + valid_boxes = [] + for b in boxes: + w, h = b[2] - b[0], b[3] - b[1] + if (w * h) >= min_area: + valid_boxes.append(list(b)) + + boxes = valid_boxes + merged_any = True + + while merged_any: + merged_any = False + new_boxes = [] + + while boxes: + current = boxes.pop(0) + # has_merged = False + + for i, other in enumerate(boxes): + # Check Proximity: Are they close enough to consider? + # (Expanding 'current' by distance_threshold for the check) + if not ( + current[2] + dist_threshold < other[0] + or other[2] + dist_threshold < current[0] + or current[3] + dist_threshold < other[1] + or other[3] + dist_threshold < current[1] + ): + # Potential Dimensions: Calculate what the new box would be + new_x1 = min(current[0], other[0]) + new_y1 = min(current[1], other[1]) + new_x2 = max(current[2], other[2]) + new_y2 = max(current[3], other[3]) + + new_w = new_x2 - new_x1 + new_h = new_y2 - new_y1 + + # Size Constraint: Only merge if it doesn't exceed the limit + if new_w <= max_size and new_h <= max_size: + current = [new_x1, new_y1, new_x2, new_y2] + boxes.pop(i) + # has_merged = True + merged_any = True + break + + new_boxes.append(current) + boxes = new_boxes + + return boxes + + +def filter_contained_boxes(boxes, containment_thresh=0.90): + if len(boxes) < 2: + return boxes + + # Convert to NumPy for vectorized math + objs = np.array(boxes) + areas = (objs[:, 2] - objs[:, 0]) * (objs[:, 3] - objs[:, 1]) + + # Sort by area descending + order = areas.argsort()[::-1] + objs = objs[order] + areas = areas[order] + + keep = [] + idx_list = np.arange(len(objs)) + + while len(idx_list) > 0: + i = idx_list[0] + keep.append(objs[i].tolist()) + if len(idx_list) == 1: + break + + # Vectorized Intersection over Union (IoU) / Containment + others = objs[idx_list[1:]] + ix1 = np.maximum(objs[i, 0], others[:, 0]) + iy1 = np.maximum(objs[i, 1], others[:, 1]) + ix2 = np.minimum(objs[i, 2], others[:, 2]) + iy2 = np.minimum(objs[i, 3], others[:, 3]) + + iw = np.maximum(0, ix2 - ix1) + ih = np.maximum(0, iy2 - iy1) + inter_area = iw * ih + + # Calculate how much 'others' are contained within 'i' + containment = inter_area / areas[idx_list[1:]] + + # Only keep boxes that are NOT mostly contained within the current box + idx_list = idx_list[1:][containment < containment_thresh] + + return keep + + +@dataclass +class PipelineMapping: + resize_device: str.lower = "cpu" + bkgd_subtraction_device: str.lower = "cpu" + threshold_device: str.lower = "cpu" + erodeAndDilate_device: str.lower = "cpu" + contour_device: str.lower = "cpu" + detection_device: str.lower = "cpu" + + +class StreamRequest(BaseModel): + url: str + name: str diff --git a/fastapi/main.py b/fastapi/main.py new file mode 100644 index 0000000..3a02e75 --- /dev/null +++ b/fastapi/main.py @@ -0,0 +1,458 @@ +import warnings + +warnings.filterwarnings("ignore", message="The value of the smallest subnormal for") + +import asyncio +import json +import logging +import multiprocessing as mp +import os +import shutil +import sys +import time +from datetime import datetime + +import psutil +from include.default_configs import ENABLE_QUERYING_DEFAULT +from include.handlers import lifespan +from include.utils import PipelineConfig, StreamRequest, str2bool + +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import StreamingResponse +from fastapi.templating import Jinja2Templates + +MODEL_CLASSES_FILE = "/var/www/cache/model_classes.json" + +RUN_CONFIG = PipelineConfig( + CODE_DIR=os.getenv("CODE_DIR", "/home"), + CUSTOM_MODEL_FLAG=str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)), + DBHOST=os.getenv("DBHOST", "vdms-service"), + DEBUG=os.getenv("DEBUG", "0"), + DEVICE=os.getenv("DEVICE", "CPU"), + ENABLE_QUERYING=os.getenv("ENABLE_QUERYING", ENABLE_QUERYING_DEFAULT), + INGESTION=os.getenv("INGESTION", "object"), + MODEL_NAME=os.getenv("MODEL_NAME", "yolo11n"), + OMIT_DETECTIONS_FLAG=str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)), + # RESIZE_FLAG=str2bool(os.getenv("RESIZE_FLAG", False)), + SHARED_MODEL=os.getenv("SHARED_MODEL", False), + SHARED_OUTPUT=os.getenv("SHARED_OUTPUT", "/var/www/mp4"), + TEST_MODE=str2bool(os.getenv("TEST_MODE", False)), + TMP_LOCATION=os.getenv("TMP_LOCATION", "/var/www/cache"), + UDF_HOST=os.getenv("UDF_HOST", "udf-service"), + UDF_PORT=5011, +) +if RUN_CONFIG.DEVICE == "GPU": + from include.handlers import GPUStreamHandler + + VideoStreamHandler = GPUStreamHandler +else: + from include.handlers import CPUStreamHandler + + VideoStreamHandler = CPUStreamHandler + + +# if RUN_CONFIG.ENABLE_QUERYING: +# from include.handlers import ( +# all_metadata, +# clip_completion_tracker, +# send_metadata_queue, +# ) + +# ----- LOGGING CONFIGURATION ----- +# Standardizes logs across the application and uvicorn server +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +main_app_logger = logging.getLogger("fastapi_app") +uvicorn_logger = logging.getLogger("uvicorn.access") +uvicorn_logger.setLevel(logging.INFO) + + +# ----- APPLICATION INITIALIZATION ----- +# The lifespan parameter handles startup and shutdown +app = FastAPI(lifespan=lifespan) +templates = Jinja2Templates(directory="templates") + + +# ----- APPLICATION ENDPOINTS ----- +@app.get("/") +async def index(request: Request): + """ + Renders the main monitoring dashboard. + Passes current active stream IDs to the frontend for UI synchronization. + Args: + request (Request): The FastAPI request object. + + Returns: + TemplateResponse: HTML page with the list of currently active camera IDs. + """ + curr_keys = list(request.app.state.active_streams.keys()) + if RUN_CONFIG.DEBUG_FLAG: + print(f"Active Streams: {curr_keys}") + + return templates.TemplateResponse( + request=request, name="index.html", context={"cameras": curr_keys} + ) + + +@app.post("/stream") +async def stream_video(data: StreamRequest, request: Request): + """ + Initializes a new VideoStreamHandler for a specific source and starts background processing. + If the stream is not already active, it starts a background processing thread. + Args: + data (StreamRequest): Pydantic model containing the source URL and unique name. + request (Request): The FastAPI request object to access global state. + + Returns: + dict: Status message and the updated list of active stream keys. + """ + url, name = data.url, data.name + active_streams = request.app.state.active_streams + + # Only initialize if the stream isn't already being processed + if name not in active_streams: + print(f"Starting background worker for {name}...") + + # Check if a global model instance should be passed to the handler + if RUN_CONFIG.SHARED_MODEL: + handler = VideoStreamHandler( + url, + name, + active_streams, + config=RUN_CONFIG, + model=app.state.model, + ) + else: + handler = VideoStreamHandler( + url, + name, + active_streams, + config=RUN_CONFIG, + ) + + # Start the background thread (OpenCV capture + AI inference) + handler.start() + + # Register the handler in the global state for cross-endpoint access + app.state.active_streams[name] = handler + + curr_keys = list(app.state.active_streams.keys()) + if RUN_CONFIG.DEBUG_FLAG: + print( + f"stream DEBUG VIEW | PID: {os.getpid()} | Looking for: {name} | Found Keys: {curr_keys}" + ) + + return {"status": "started", "keys": curr_keys} + + +@app.get("/view_stream", name="view_stream") +async def view_stream(name: str, request: Request): + """ + High-bandwidth MJPEG streaming gateway. + Uses an asynchronous generator to pipe processed JPEG frames to the browser. + Args: + name (str): The unique identifier of the camera stream. + request (Request): The FastAPI request object. + + Returns: + StreamingResponse: A multipart/x-mixed-replace stream of JPEG images. + """ + active_streams = request.app.state.active_streams + + if name not in active_streams: + raise HTTPException(status_code=404, detail="Stream not found") + + streamer = active_streams.get(name) + if not streamer: + raise HTTPException(status_code=404) + + async def frame_generator(streamer, request: Request): + """ + Yields frames only when the background worker signals a new frame is ready. + Ensures strict chronological order using frame IDs. + """ + shm_names = streamer.shared_details["shm_names"] + reader_shms = [mp.shared_memory.SharedMemory(name=n) for n in shm_names] + last_sent_id = -1 + try: + while streamer.active: + # Stop the generator immediately if the browser tab is closed + if await request.is_disconnected(): + main_app_logger.info(f"Client disconnected from {name}") + break + + # Wait for the background thread to signal that AI processing is complete + await streamer.frame_ready_event.wait() + streamer.frame_ready_event.clear() # Reset for the next frame + + # streamer.reader_busy.value = True + # target_idx = streamer.ready_buffer_idx.value + + # streamer.reader_active_idx.value = target_idx + + # Frame Synchronization: ensure we don't send duplicate or out-of-order frames + current_id = streamer.shared_details.get("last_id", -1) + if current_id > last_sent_id: + ready_idx = streamer.ready_buffer_idx.value + streamer.reader_active_idx.value = ready_idx + frame_len = streamer.shm_frame_lengths[ready_idx] + + if frame_len > 0: + # shm_name = streamer.shared_details["shm_name"] + # shm_name = shm_names[ready_idx] + # print(f"DEBUG: Displaying SHM {shm_name}") + # frame_bytes = streamer.latest_processed_frame + try: + frame_bytes = bytes(reader_shms[ready_idx].buf[:frame_len]) + finally: + streamer.reader_active_idx.value = -1 + last_sent_id = current_id + streamer.last_heartbeat = time.time() + # streamer.reader_busy.value = False + + # Multipart JPEG delivery with explicit Content-Length for stability + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n" + b"Content-Length: " + + str(len(frame_bytes)).encode() + + b"\r\n\r\n" + # b"Content-Length: " + # + str(len(frame_bytes)).encode() + # + b"\r\n\r\n" + + frame_bytes + + b"\r\n" + ) + # last_sent_id = current_id + + # Yield control to the event loop to prevent blocking + await asyncio.sleep(0.001) + except Exception as e: + main_app_logger.error(f"[EXCEPTION] Generator Error: {e}") + # finally: + # streamer.reader_busy.value = False # Safety unlock + + return StreamingResponse( + frame_generator(streamer, request), + media_type="multipart/x-mixed-replace; boundary=frame", + ) + + +@app.get("/stream_list") +async def get_stream_list(request: Request): + """Returns a thread-safe list of all currently running stream identifiers.""" + return list(request.app.state.active_streams.keys()) + + +@app.get("/stream_stats") +async def get_stats(request: Request): + """Provides granular FPS and frame-count metrics for all running streams.""" + return { + name: { + "fps": round(streamer.stat_fps, 1), + "inputfps": round(streamer.input_fps, 1), + "targetfps": round(streamer.target_fps, 1), + "frames": streamer.stat_frame_count, + # "status": + } + for name, streamer in request.app.state.active_streams.items() + } + + +@app.get("/dashboard_stats") +async def dashboard_stats(request: Request): + """ + Returns real-time performance metrics for the dashboard overlay, + including FPS and the background processing backlog. + """ + stats = {} + active_streams = request.app.state.active_streams + + for name, streamer in active_streams.items(): + # AI Backlog: Tasks waiting in the ThreadPool + ai_backlog = streamer.get_executor_backlog() + # AI Backlog: Tasks waiting in the ThreadPool + clipper_backlog = streamer.get_clip_executor_backlog() + + # Video Backlog: Frames waiting for Disk I/O + video_backlog = ( + streamer.write_queue.qsize() if streamer.config.ENABLE_QUERYING else 0 + ) + + # IO Backlog: Frames queued for disk storage (if enabled) + io_backlog = ( + streamer.io_executor._work_queue.qsize() + if hasattr(streamer, "io_executor") + else 0 + ) + + # Calculate Shared Memory (/dev/shm) usage - critical for Docker/Linux deployments + shm_usage = shutil.disk_usage("/dev/shm") + shm_percent = (shm_usage.used / shm_usage.total) * 100 + + stats[name] = { + "fps": round(streamer.stat_fps, 1), + "inputfps": round(streamer.input_fps, 1), + "targetfps": round(streamer.target_fps, 1), + "is_streaming": streamer.active, + "clipper_backlog": clipper_backlog, + "ai_backlog": ai_backlog, + "video_backlog": video_backlog, + "io_backlog": io_backlog, + "querying_active": streamer.config.ENABLE_QUERYING, + "total_frames": streamer.stat_frame_count, + "shm_usage": f"{shm_percent:0.1f}%", + } + return stats + + +@app.get("/status") +async def get_status(request: Request): + """Returns the overall system status (e.g., 'Ready', 'Loading', or 'Error').""" + return { + "status": request.app.state.status + if hasattr(request.app.state, "status") + else "Loading" + } + + +@app.post("/stop_stream/{name}") +async def stop_stream(name: str, request: Request): + """ + Gracefully stops a single stream and releases its hardware/VRAM resources. + The blocking cleanup logic is offloaded to a separate thread to prevent API hang. + """ + # Immediately remove from the global state so polling/UI syncs instantly + streamer = request.app.state.active_streams.pop(name, None) + + if streamer: + streamer.active = False + + # BACKGROUND CLEANUP: Fire-and-forget the heavy hardware teardown + loop = asyncio.get_event_loop() + loop.run_in_executor(None, streamer.stop) + + if streamer.config.DEBUG_FLAG: + print(f"--- CLEANUP | Stream '{name}' stopped and removed. ---") + return {"status": "stopped", "camera": name} + + return {"status": "not found"} + + +@app.post("/stop_all") +async def stop_all_streams(request: Request): + """ + Stops all active cameras and purges hardware resources. + Uses a list snapshot to safely iterate while modifying the dictionary. + """ + # Use the global stream_lock to prevent janitor/new-streams from interfering + async with request.app.state.stream_lock: + active_streams = request.app.state.active_streams + active_names = list(active_streams.keys()) + + if not active_names: + return {"status": "success", "message": "No active streams to stop"} + + for name in active_names: + streamer = active_streams.pop(name, None) + if streamer: + # Offload to executor to keep the API responsive + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, streamer.stop) + + return { + "status": "success", + "stopped_count": len(active_names), + "cleared_streams": active_names, + } + + +@app.get("/health") +async def health_check(request: Request): + """ + Diagnostic endpoint for monitoring system stability. + Returns: + dict: Hardware metrics, stream backlogs, and executor status. + """ + # Hardware Metrics + ram = psutil.virtual_memory() + # Check RAM disk usage (critical for 8K MJPEG/MP4 buffers) + shm = shutil.disk_usage("/dev/shm") + + health_data = { + "status": "online", + "system_time": datetime.now().isoformat(), + "hardware": { + "ram_used_percent": ram.percent, + "shm_used_percent": round((shm.used / shm.total) * 100, 1), + "cpu_count": psutil.cpu_count(), + }, + "active_streams": len(request.app.state.active_streams), + "stream_details": {}, + } + + # Pipeline Backlogs (Identify bottlenecks) + for name, streamer in request.app.state.active_streams.items(): + health_data["stream_details"][name] = { + "ai_backlog": streamer.get_executor_backlog(), + "io_backlog": streamer.write_queue.qsize() + if hasattr(streamer, "write_queue") + else 0, + "fps_live": streamer.stat_fps, + "uptime_sec": round(time.perf_counter() - streamer.stat_start_time, 1), + } + + # Global Sync Health + # if RUN_CONFIG.ENABLE_QUERYING: + # health_data["sync_engine"] = { + # "pending_completions": len(clip_completion_tracker), + # # "metadata_buffer_size": len(all_metadata), + # "vdms_queue_depth": send_metadata_queue.qsize(), + # } + + return health_data + + +@app.get("/model_classes") +async def get_model_classes(): + classes = app.state.classes + + # Use classes already stored + if classes is not None: + main_app_logger.info(f"classes: {classes}") + return {"classes": classes} + + # Read list from JSON file stored at entrypoint + if os.path.exists(MODEL_CLASSES_FILE): + with open(MODEL_CLASSES_FILE, "r") as f: + data = json.load(f) + classes = data.get("classes", None) + if classes is not None: + app.state.classes = classes + main_app_logger.info(f"classes: {classes}") + return classes + + # Read from model of active stream + stream_name = list(app.state.active_streams.keys()) + main_app_logger.info(f"stream_name: {stream_name}") + # Extracts the dynamic labels from your loaded AI model instance + if len(stream_name) > 0: + streamer = app.state.active_streams.get(stream_name[0]) + if hasattr(streamer, "label_sources") and streamer.label_sources: + classes = list(streamer.label_sources) + app.state.classes = classes + main_app_logger.info(f"classes: {classes}") + return {"classes": classes} + + # Fallback structure matching your old format if no model is loaded + default_classes = ["class0"] + return {"classes": default_classes} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") diff --git a/fastapi/manage.sh b/fastapi/manage.sh new file mode 100755 index 0000000..f0ee94d --- /dev/null +++ b/fastapi/manage.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +/usr/local/sbin/nginx -g 'daemon on;' + +# run fastapi +exec ${VIRTUAL_ENV}/bin/python /home/main.py diff --git a/fastapi/nginx.conf b/fastapi/nginx.conf new file mode 100644 index 0000000..debe316 --- /dev/null +++ b/fastapi/nginx.conf @@ -0,0 +1,40 @@ +events { worker_connections 1024; } + +http { + include mime.types; + default_type application/octet-stream; + client_max_body_size 100M; + + server { + listen 80; + server_name _; + + location / { + # Proxy to the Uvicorn server running in the same container + proxy_pass http://fastapi-service:8000; + + proxy_http_version 1.1; + # proxy_set_header Connection ""; # Keep connection open + proxy_set_header Connection "keep-alive"; + + # Absolute zero buffering + proxy_buffering off; # DISBALE Nginx buffering + proxy_request_buffering off; + proxy_cache off; # Ensure no caching of the video + tcp_nodelay on; + tcp_nopush off; + + # Pass original headers for FastAPI's identification + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + chunked_transfer_encoding on; + + # Increase timeouts for long-running video streams + proxy_read_timeout 3600s; # Keep stream open for up to 1 hour + proxy_send_timeout 3600s; + } + } +} \ No newline at end of file diff --git a/fastapi/requirements.txt b/fastapi/requirements.txt new file mode 100644 index 0000000..b8be201 --- /dev/null +++ b/fastapi/requirements.txt @@ -0,0 +1,1624 @@ +annotated-doc==0.0.4 \ + --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ + --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +anyio==4.13.0 \ + --hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \ + --hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc +av==17.0.1 \ + --hash=sha256:09b1f1601cc4a4d9e616d197b345c363ba6abfe567cb3d6b18e45516126692b6 \ + --hash=sha256:1229e879f4b6431bc00f69d7f8891fe9a683b0a6e0e009e6c98eb7e449f0383d \ + --hash=sha256:1d33871742d1e71562db3c8e752cacc5a62766d7efc3ae408bff1c3e26ebb46e \ + --hash=sha256:3a3f33bbfed2bcc65be37941bfeb6cc20bbe9cb7afc4ef1ac8d330972df098f9 \ + --hash=sha256:3d0a7d45d9599bf9df9f8249827113d4f36df1cd6b5356227b997f0552dbc98e \ + --hash=sha256:3d3a36204cb1f1e7691e6446afa8d6b7097b09946dae732c71c5d05ce09e506e \ + --hash=sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278 \ + --hash=sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318 \ + --hash=sha256:4744837f4116964280bcc72285e3cdd51361e98a696205aadd924203440ef511 \ + --hash=sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4 \ + --hash=sha256:50f9dd53a8ebef77606dca3b21710f660f9a6478484e79b9abda7c787b4f2403 \ + --hash=sha256:8270634c409f8efc9a24216e5dd90313d873b26ea4b5f172b14de52cbd15121c \ + --hash=sha256:985c21095bfb9c4bb7ba362fbef7bf0194bd72b1d7d3c46e30d1f47c5d38b4df \ + --hash=sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230 \ + --hash=sha256:9acd0b6a6e02af2b37f63d97a03ee2c47936d58e82425c3cd075a95245937c59 \ + --hash=sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c \ + --hash=sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa \ + --hash=sha256:b87b98afe971cde123953073bc9c95ab0b7efd2ecc082dd2dbd11f9d9abf190e \ + --hash=sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5 \ + --hash=sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32 \ + --hash=sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e \ + --hash=sha256:f585358fe0127990aea7887e940de4cdd745a2770605c31e54b2418fd0fdd8bd \ + --hash=sha256:f63b30067e6d88a3cce0d73d01ecfc0e6f091ad2bcf689db5dc305b0b4e8348c \ + --hash=sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4 +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 +click==8.3.2 \ + --hash=sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5 \ + --hash=sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +coloredlogs==15.0.1 \ + --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ + --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 +contourpy==1.3.2 \ + --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ + --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ + --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ + --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ + --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ + --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ + --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ + --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ + --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ + --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ + --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ + --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ + --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ + --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ + --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ + --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ + --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ + --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ + --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ + --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ + --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ + --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ + --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ + --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ + --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ + --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ + --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ + --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ + --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ + --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ + --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ + --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ + --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ + --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ + --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ + --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ + --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ + --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ + --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ + --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ + --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ + --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ + --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ + --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ + --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ + --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ + --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ + --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ + --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ + --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ + --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ + --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ + --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ + --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ + --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ + --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ + --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c +cucim==23.10.0 \ + --hash=sha256:2c061ad28c3c1fa67bb62260f0a556f354c8ec2d6e3811eece375ed896d62945 +cuda-bindings==12.9.4 \ + --hash=sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5 \ + --hash=sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f \ + --hash=sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f \ + --hash=sha256:37744e721a18a514423e81863f52a4f7f46f5a6f9cccd569f2735f8067f4d8c2 \ + --hash=sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d \ + --hash=sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913 \ + --hash=sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5 \ + --hash=sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575 \ + --hash=sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43 \ + --hash=sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb \ + --hash=sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56 \ + --hash=sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615 \ + --hash=sha256:893ca68114b5b769c1d4c02583b91ed22691887c3ed513b59467d23540104db4 \ + --hash=sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686 \ + --hash=sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa \ + --hash=sha256:a022c96b8bd847e8dc0675523431149a4c3e872f440e3002213dbb9e08f0331a \ + --hash=sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64 \ + --hash=sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306 \ + --hash=sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c \ + --hash=sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9 \ + --hash=sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5 \ + --hash=sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee \ + --hash=sha256:f69107389e6b9948969bfd0a20c4f571fd1aefcfb1d2e1b72cc8ba5ecb7918ab \ + --hash=sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8 +cuda-pathfinder==1.5.3 \ + --hash=sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d +cuda-toolkit[cudart]==12.8.2.0 \ + --hash=sha256:79040e750f28b959415283ccc44cce541c07003553d675b8ff45760e0189aa71 +cupy-cuda12x==13.6.0 \ + --hash=sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447 \ + --hash=sha256:4d2dfd9bb4705d446f542739a3616b4c9eea98d674fce247402cc9bcec89a1e4 \ + --hash=sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a \ + --hash=sha256:6ccd2fc75b0e0e24493531b8f8d8f978efecddb45f8479a48890c40d3805eb87 \ + --hash=sha256:771f3135861b68199c18b49345210180d4fcdce4681b51c28224db389c4aac5d \ + --hash=sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48 \ + --hash=sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1 \ + --hash=sha256:9e37f60f27ff9625dfdccc4688a09852707ec613e32ea9404f425dd22a386d14 \ + --hash=sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af \ + --hash=sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59 \ + --hash=sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44 \ + --hash=sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e \ + --hash=sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c \ + --hash=sha256:e78409ea72f5ac7d6b6f3d33d99426a94005254fa57e10617f430f9fd7c3a0a1 \ + --hash=sha256:f33c9c975782ef7a42c79b6b4fb3d5b043498f9b947126d792592372b432d393 +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 +fastapi==0.136.0 \ + --hash=sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4 \ + --hash=sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e +fastrlock==0.8.3 \ + --hash=sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40 \ + --hash=sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30 \ + --hash=sha256:05029d7080c0c61a81d5fee78e842c9a1bf22552cd56129451a252655290dcef \ + --hash=sha256:0a9dc6fa73174f974dfb22778d05a44445b611a41d5d3776b0d5daa9e50225c6 \ + --hash=sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62 \ + --hash=sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe \ + --hash=sha256:15e13a8b01a3bbf25f1615a6ac1d6ed40ad3bcb8db134ee5ffa7360214a8bc5c \ + --hash=sha256:1dd7f1520f7424793c812e1a4090570f8ff312725dbaf10a925b688aef7425f1 \ + --hash=sha256:1fced4cb0b3f1616be68092b70a56e9173713a4a943d02e90eb9c7897a7b5e07 \ + --hash=sha256:239e85cbebda16f14be92468ce648d0bc25e2442a3d11818deca59a7c43a4416 \ + --hash=sha256:24522689f4b5311afad0c8f998daec84a3dbe3a70cf821a615a763f843903030 \ + --hash=sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a \ + --hash=sha256:314e787532ce555a7362d3c438f0a680cd88a82c69b655e7181a4dd5e67712f5 \ + --hash=sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd \ + --hash=sha256:350f517a7d22d383f8ef76652b0609dc79de6693880a99bafc8a05c100e8c5e7 \ + --hash=sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4 \ + --hash=sha256:387b2ac642938a20170a50f528817026c561882ea33306c5cbe750ae10d0a7c2 \ + --hash=sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2 \ + --hash=sha256:3e77a3d0ca5b29695d86b7d03ea88029c0ed8905cfee658eb36052df3861855a \ + --hash=sha256:40b328369005a0b32de14b699192aed32f549c2d2b27a5e1f614fb7ac4cec4e9 \ + --hash=sha256:45055702fe9bff719cdc62caa849aa7dbe9e3968306025f639ec62ef03c65e88 \ + --hash=sha256:494fc374afd0b6c7281c87f2ded9607c2731fc0057ec63bd3ba4451e7b7cb642 \ + --hash=sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160 \ + --hash=sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d \ + --hash=sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45 \ + --hash=sha256:558b538221e9c5502bb8725a1f51157ec38467a20498212838e385807e4d1b89 \ + --hash=sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695 \ + --hash=sha256:5a0d31840a28d66573047d2df410eb971135a2461fb952894bf51c9533cbfea5 \ + --hash=sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c \ + --hash=sha256:5eef1d32d7614e0ceb6db198cf53df2a5830685cccbcf141a3e116faca967384 \ + --hash=sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670 \ + --hash=sha256:668fad1c8322badbc8543673892f80ee563f3da9113e60e256ae9ddd5b23daa4 \ + --hash=sha256:6cbfb6f7731b5a280851c93883624424068fa5b22c2f546d8ae6f1fd9311e36d \ + --hash=sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e \ + --hash=sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5 \ + --hash=sha256:7a77ebb0a24535ef4f167da2c5ee35d9be1e96ae192137e9dc3ff75b8dfc08a5 \ + --hash=sha256:80876d9e04e8e35abbdb3e1a81a56558f4d5cf90c8592e428d4d12efce048347 \ + --hash=sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed \ + --hash=sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65 \ + --hash=sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da \ + --hash=sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8 \ + --hash=sha256:924abbf21eba69c1b35c04278f3ca081e8de1ef5933355756e86e05499123238 \ + --hash=sha256:92577ff82ef4a94c5667d6d2841f017820932bc59f31ffd83e4a2c56c1738f90 \ + --hash=sha256:963123bafc41c9fba72e57145917a3f23086b5d631b6cda9cf858c428a606ff9 \ + --hash=sha256:9842b7722e4923fe76b08d8c58a9415a9a50d4c29b80673cffeae4874ea6626a \ + --hash=sha256:9c2c24856d2adc60ab398780f7b7cd8a091e4bd0c0e3bb3e67f12bef2800f377 \ + --hash=sha256:9c4068f21fddc47393a3526ce95b180a2f4e1ac286db8d9e59e56771da50c815 \ + --hash=sha256:a0eadc772353cfa464b34c814b2a97c4f3c0ba0ed7b8e1c2e0ad3ebba84bf8e0 \ + --hash=sha256:a8fd6727c1e0952ba93fdc5975753781039772be6c1a3911a3afc87b53460dc0 \ + --hash=sha256:ac4fcc9b43160f7f64b49bd7ecfd129faf0793c1c8c6f0f56788c3bacae7f54a \ + --hash=sha256:accd897ab2799024bb87b489c0f087d6000b89af1f184a66e996d3d96a025a3b \ + --hash=sha256:b6ac082d670e195ad53ec8d0c5d2e87648f8838b0d48f7d44a6e696b8a9528e2 \ + --hash=sha256:bbbe31cb60ec32672969651bf68333680dacaebe1a1ec7952b8f5e6e23a70aa5 \ + --hash=sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05 \ + --hash=sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f \ + --hash=sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd \ + --hash=sha256:cdee8c02c20a0b17dbc52f54c48ede3bd421985e5d9cef5cd2136b14da967996 \ + --hash=sha256:d3ebb29de71bf9e330c2769c34a6b5e69d560126f02994e6c09635a2784f6de3 \ + --hash=sha256:d51f7fb0db8dab341b7f03a39a3031678cf4a98b18533b176c533c122bfce47d \ + --hash=sha256:d7edaf0071a6a98340fc2ec45b0ba37b7a16ed7761479aab577e41e09b3565e1 \ + --hash=sha256:d7f359bb989c01a5875e8dbde9acab37b9da0943b60ef97ba9887c4598eb3009 \ + --hash=sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c \ + --hash=sha256:da53350b90a67d5431df726816b041f1f96fd558ad6e2fc64948e13be3c7c29a \ + --hash=sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259 \ + --hash=sha256:de8c90c1a23fbe929d8a9628a6c1f0f1d8af6019e786354a682a26fa22ea21be \ + --hash=sha256:e0ceefadde046a5f6a261bfeaf25de9e0eba3ee790a9795b1fa9634111d3220e \ + --hash=sha256:f2b84b2fe858e64946e54e0e918b8a0e77fc7b09ca960ae1e50a130e8fbc9af8 \ + --hash=sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f \ + --hash=sha256:fcb50e195ec981c92d0211a201704aecbd9e4f9451aea3a6f71ac5b1ec2c98cf +filelock==3.28.0 \ + --hash=sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6 \ + --hash=sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db +flatbuffers==25.12.19 \ + --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 +fonttools==4.62.1 \ + --hash=sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04 \ + --hash=sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a \ + --hash=sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9 \ + --hash=sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 \ + --hash=sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82 \ + --hash=sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d \ + --hash=sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b \ + --hash=sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e \ + --hash=sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416 \ + --hash=sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae \ + --hash=sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069 \ + --hash=sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9 \ + --hash=sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7 \ + --hash=sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed \ + --hash=sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800 \ + --hash=sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e \ + --hash=sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9 \ + --hash=sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b \ + --hash=sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1 \ + --hash=sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe \ + --hash=sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7 \ + --hash=sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd \ + --hash=sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056 \ + --hash=sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23 \ + --hash=sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae \ + --hash=sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260 \ + --hash=sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 \ + --hash=sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87 \ + --hash=sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24 \ + --hash=sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53 \ + --hash=sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936 \ + --hash=sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14 \ + --hash=sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 \ + --hash=sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c \ + --hash=sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1 \ + --hash=sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca \ + --hash=sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c \ + --hash=sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d \ + --hash=sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a \ + --hash=sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782 \ + --hash=sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c \ + --hash=sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a \ + --hash=sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79 \ + --hash=sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3 \ + --hash=sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7 \ + --hash=sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d \ + --hash=sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2 \ + --hash=sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4 \ + --hash=sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68 \ + --hash=sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca +fsspec==2026.3.0 \ + --hash=sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41 \ + --hash=sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4 +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 +humanfriendly==10.0 \ + --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ + --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc +idna==3.11 \ + --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ + --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 +kiwisolver==1.5.0 \ + --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ + --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ + --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \ + --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \ + --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \ + --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \ + --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \ + --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \ + --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \ + --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \ + --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \ + --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \ + --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \ + --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \ + --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \ + --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \ + --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \ + --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \ + --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \ + --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \ + --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \ + --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \ + --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \ + --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \ + --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \ + --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \ + --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \ + --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \ + --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \ + --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \ + --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \ + --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \ + --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \ + --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \ + --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \ + --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \ + --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \ + --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \ + --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \ + --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \ + --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \ + --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \ + --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \ + --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \ + --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \ + --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \ + --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \ + --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \ + --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \ + --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \ + --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \ + --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \ + --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \ + --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \ + --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \ + --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \ + --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \ + --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \ + --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \ + --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \ + --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \ + --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \ + --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \ + --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \ + --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \ + --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \ + --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \ + --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \ + --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \ + --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \ + --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \ + --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \ + --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \ + --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \ + --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \ + --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \ + --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \ + --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \ + --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \ + --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \ + --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \ + --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \ + --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \ + --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \ + --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \ + --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \ + --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \ + --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \ + --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \ + --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \ + --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \ + --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \ + --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \ + --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \ + --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \ + --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \ + --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \ + --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \ + --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \ + --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \ + --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \ + --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \ + --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \ + --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \ + --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \ + --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \ + --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \ + --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \ + --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \ + --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \ + --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \ + --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \ + --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \ + --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \ + --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ + --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ + --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 +lazy-loader==0.5 \ + --hash=sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3 \ + --hash=sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 +matplotlib==3.10.8 \ + --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \ + --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ + --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ + --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \ + --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ + --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ + --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ + --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ + --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ + --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ + --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ + --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ + --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ + --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ + --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \ + --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ + --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ + --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ + --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ + --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ + --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ + --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ + --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ + --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ + --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ + --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ + --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ + --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ + --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ + --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ + --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ + --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ + --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ + --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ + --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ + --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \ + --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ + --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ + --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ + --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ + --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ + --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ + --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ + --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ + --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ + --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ + --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ + --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ + --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \ + --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ + --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \ + --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \ + --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \ + --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ + --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba +ml-dtypes==0.5.4 \ + --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ + --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ + --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ + --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ + --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ + --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ + --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ + --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ + --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ + --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ + --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ + --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ + --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ + --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ + --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ + --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ + --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ + --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ + --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ + --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ + --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ + --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ + --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ + --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ + --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ + --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ + --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ + --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ + --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ + --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ + --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ + --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ + --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ + --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ + --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ + --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ + --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ + --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ + --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 +mpmath==1.3.0 \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c +networkx==3.1 \ + --hash=sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36 \ + --hash=sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61 +ninja==1.13.0 \ + --hash=sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f \ + --hash=sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988 \ + --hash=sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9 \ + --hash=sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630 \ + --hash=sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db \ + --hash=sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978 \ + --hash=sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1 \ + --hash=sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72 \ + --hash=sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e \ + --hash=sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2 \ + --hash=sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9 \ + --hash=sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714 \ + --hash=sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200 \ + --hash=sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c \ + --hash=sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5 \ + --hash=sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96 \ + --hash=sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1 \ + --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ + --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e +nncf==3.1.0 \ + --hash=sha256:cabfdc32f42a04c6c106906d1b248ace0ed0748c46b193529ec7b4e945baa374 \ + --hash=sha256:f3bcd71c537554bb90100cd91fb999904e1e222cac61eee6df7af7cf58ccd646 +numpy==1.26.4 \ + --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ + --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ + --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ + --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ + --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ + --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ + --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ + --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ + --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ + --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ + --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ + --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ + --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ + --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ + --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ + --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ + --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ + --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ + --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ + --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ + --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ + --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ + --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ + --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ + --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ + --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ + --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ + --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ + --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ + --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ + --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ + --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ + --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ + --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ + --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ + --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f +nvidia-cublas-cu12==12.8.4.1 \ + --hash=sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af \ + --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142 \ + --hash=sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0 +nvidia-cuda-cupti-cu12==12.8.90 \ + --hash=sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed \ + --hash=sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e \ + --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182 +nvidia-cuda-nvrtc-cu12==12.8.93 \ + --hash=sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909 \ + --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994 \ + --hash=sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8 +nvidia-cuda-runtime-cu12==12.8.90 \ + --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ + --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ + --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 +nvidia-cudnn-cu12==9.10.2.21 \ + --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ + --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ + --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 +nvidia-cufft-cu12==11.3.3.83 \ + --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ + --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ + --hash=sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a +nvidia-cufile-cu12==1.13.1.3 \ + --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc \ + --hash=sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a +nvidia-curand-cu12==10.3.9.90 \ + --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9 \ + --hash=sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd \ + --hash=sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec +nvidia-cusolver-cu12==11.7.3.90 \ + --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450 \ + --hash=sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34 \ + --hash=sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0 +nvidia-cusparse-cu12==12.5.8.93 \ + --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b \ + --hash=sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd \ + --hash=sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc +nvidia-cusparselt-cu12==0.7.1 \ + --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ + --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ + --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 +nvidia-nccl-cu12==2.27.5 \ + --hash=sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a \ + --hash=sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457 +nvidia-nvjitlink-cu12==12.8.93 \ + --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88 \ + --hash=sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7 \ + --hash=sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f +nvidia-nvshmem-cu12==3.4.5 \ + --hash=sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd \ + --hash=sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15 +nvidia-nvtx-cu12==12.8.90 \ + --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f \ + --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ + --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 +onnx==1.21.0 \ + --hash=sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda \ + --hash=sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60 \ + --hash=sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd \ + --hash=sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f \ + --hash=sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce \ + --hash=sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027 \ + --hash=sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f \ + --hash=sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764 \ + --hash=sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9 \ + --hash=sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d \ + --hash=sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840 \ + --hash=sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8 \ + --hash=sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad \ + --hash=sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974 \ + --hash=sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b \ + --hash=sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e \ + --hash=sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285 \ + --hash=sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d \ + --hash=sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad \ + --hash=sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9 \ + --hash=sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb \ + --hash=sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8 \ + --hash=sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd \ + --hash=sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a \ + --hash=sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9 \ + --hash=sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68 \ + --hash=sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27 \ + --hash=sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095 +onnxruntime-gpu==1.23.2 \ + --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ + --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ + --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ + --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ + --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ + --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ + --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ + --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ + --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 +onnxslim==0.1.91 \ + --hash=sha256:1fdb23ca56ca3f9d12ff7b9ae1c779184468eaf911baea0930ac254b49c93ac9 \ + --hash=sha256:3222fcd9a16836a4e7917e1dc55ddf4db492dadca3f469a1fa9226f92c4a2c6e +opencv-python==4.11.0.86 \ + --hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \ + --hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \ + --hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \ + --hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \ + --hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \ + --hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \ + --hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66 +openvino==2024.6.0 \ + --hash=sha256:05a436e2526bf6775b487712ab4a6decf7afe04183109bc7b129b95205ec4247 \ + --hash=sha256:151c6010f0613ace29087bf864dc5dd1db0afa25ab87379c735f85500545b167 \ + --hash=sha256:17cbdae3b069d9a13389b43a15b521a86fef75f350cfd0d992cc1780a6123788 \ + --hash=sha256:1d59ccc2df4a8c8515f30683f6cc0091c5b4e6a01a71ebfb1dc8f2ebdb5c83f4 \ + --hash=sha256:263496653200270b8a17456dc7bc67a198ed29b601b0ad7280d90783ba918d66 \ + --hash=sha256:2acfe89f61ec18d608c79147cedea6ba85d80783723ab9a9416891b74b15785e \ + --hash=sha256:444a6d3a746dd728654877993c30582faefcad2868c80ec261839a144b8925bc \ + --hash=sha256:46d3e20838c918668ec10ae124e92d56ff47a7c14da0d9d19d01d91fc9dece94 \ + --hash=sha256:4a94ef27022d56a159ea8677e5561bdd6f0d6c6237736e13c0f7cde71224ce7f \ + --hash=sha256:89c866fa87ddeccb982252e7d1c8516101a4acb95aaebac7e43f450112cb6942 \ + --hash=sha256:8d0390f4ab9b8f27814cf0feeddb0bba449ae8cd99933c2c2fae3011d33973b3 \ + --hash=sha256:9150c75e19606a2f2ab411271c79031726980fb6870b96445db1faca5ab64f5f \ + --hash=sha256:989c803d1f8e6d12ee8d2e3e8c7d56189d96688433b2238270dbfbf64f163f18 \ + --hash=sha256:ac812efa9c139387db534498a1e980103a35b9b2b5f3b2d1ae6ed43db51b4bba \ + --hash=sha256:c25d6820e960c08c0cc8d7823e9e6e395d762bc603772af14f0a576fab1628eb \ + --hash=sha256:c3477e833541df4f316240491d9df157c20a81487991748eb33e02e534009999 \ + --hash=sha256:c6c6a40a890b51007b20cf97d81783c4b43edfbcd4aabb06b6b04b7f2fb3137f \ + --hash=sha256:cb94f4ca9c1857bda2c5b510e519db20ea246958dbd76ddc38c133e2216231a5 \ + --hash=sha256:f7177d9a734a61a0708bcd706592f59fc2377d8efbb0bf9f8a33edfd2998a45d \ + --hash=sha256:ff5f871162299906fb517d5c9ded79f464a0341d8e2d55a037d4f9e38eed8652 +openvino-dev==2024.6.0 \ + --hash=sha256:b8f4a1baeea1b138bc9b75b53dc3bdad3307c2cd4d58526ad977df81635870c5 +openvino-telemetry==2025.2.0 \ + --hash=sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c \ + --hash=sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599 +packaging==26.1 \ + --hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \ + --hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de +pillow==12.2.0 \ + --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ + --hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \ + --hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \ + --hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \ + --hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \ + --hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \ + --hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \ + --hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \ + --hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \ + --hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \ + --hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \ + --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \ + --hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \ + --hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \ + --hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \ + --hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \ + --hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \ + --hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \ + --hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \ + --hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \ + --hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \ + --hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \ + --hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \ + --hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \ + --hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \ + --hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \ + --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \ + --hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \ + --hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \ + --hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \ + --hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \ + --hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \ + --hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \ + --hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \ + --hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \ + --hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \ + --hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \ + --hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \ + --hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \ + --hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \ + --hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \ + --hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \ + --hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \ + --hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \ + --hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \ + --hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \ + --hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \ + --hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \ + --hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \ + --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \ + --hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \ + --hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \ + --hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \ + --hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \ + --hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \ + --hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \ + --hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \ + --hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \ + --hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \ + --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \ + --hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \ + --hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \ + --hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \ + --hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \ + --hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \ + --hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \ + --hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \ + --hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \ + --hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \ + --hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \ + --hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \ + --hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \ + --hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \ + --hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \ + --hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \ + --hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \ + --hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \ + --hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \ + --hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \ + --hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \ + --hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \ + --hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \ + --hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \ + --hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \ + --hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \ + --hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \ + --hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \ + --hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \ + --hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \ + --hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \ + --hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5 +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +polars==1.39.3 \ + --hash=sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c \ + --hash=sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56 +polars-runtime-32==1.39.3 \ + --hash=sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14 \ + --hash=sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2 \ + --hash=sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9 \ + --hash=sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4 \ + --hash=sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed \ + --hash=sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d \ + --hash=sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339 \ + --hash=sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562 \ + --hash=sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860 +protobuf==5.29.6 \ + --hash=sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05 \ + --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ + --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ + --hash=sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18 \ + --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ + --hash=sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6 \ + --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ + --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ + --hash=sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49 \ + --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ + --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 +pydantic==2.13.2 \ + --hash=sha256:a525087f4c03d7e7456a3de89b64cd693d2229933bb1068b9af6befd5563694e \ + --hash=sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1 +pydantic-core==2.46.2 \ + --hash=sha256:0551f2d2ddb68af5a00e26497f8025c538f73ef3cb698f8e5a487042cd2792a8 \ + --hash=sha256:0d12d786e30c04a9d307c5d7080bf720d9bac7f1668191d8e37633a9562749e2 \ + --hash=sha256:0d5e6d6343b0b5dcacb3503b5de90022968da8ed0ab9ab39d3eda71c20cbf84e \ + --hash=sha256:130a6c837d819ef33e8c2bf702ed2c3429237ea69807f1140943d6f4bdaf52fa \ + --hash=sha256:13ffef637dc8370c249e5b26bd18e9a80a4fca3d809618c44e18ec834a7ca7a8 \ + --hash=sha256:154dbfdfb11b8cbd8ff4d00d0b81e3d19f4cb4bedd5aa9f091060ba071474c6a \ + --hash=sha256:15e42885b283f87846ee79e161002c5c496ef747a73f6e47054f45a13d9035bc \ + --hash=sha256:160ef93541f4f84e3e5068e6c1f64d8fd6f57586e5853d609b467d3333f8146a \ + --hash=sha256:19631e7350b7a574fb6b6db222f4b17e8bd31803074b3307d07df62379d2b2e4 \ + --hash=sha256:1a9124b63f4f40a12a0666df57450b4c24b98407ff74349221b869ec085a5d8e \ + --hash=sha256:1b0ab6d756ca2704a938e6c31b53f290c2f9c10d3914235410302a149de1a83e \ + --hash=sha256:1b877d597afb82b4898e35354bba55de6f7f048421ae0edadbb9886ec137b532 \ + --hash=sha256:1d00b99590c5bd1fabbc5d28b170923e32c1b1071b1f1de1851a4d14d89eb192 \ + --hash=sha256:20fb194788a0a50993e87013e693494ba183a2af5b44e99cf060bbae10912b11 \ + --hash=sha256:233eebac0999b6b9ba76eb56f3ec8fce13164aa16b6d2225a36a79e0f95b5973 \ + --hash=sha256:236f22b4a206b5b61db955396b7cf9e2e1ff77f372efe9570128ccfcd6a525eb \ + --hash=sha256:251a57788823230ca8cbc99e6245d1a2ed6e180ec4864f251c94182c580c7f2e \ + --hash=sha256:2643ac7eae296200dbd48762a1c852cf2cad5f5e3eba34e652053cebf03becf8 \ + --hash=sha256:28708faed0b47f9d68906551a3471421ab0b15c31519e08fdb70ae6cad04d10b \ + --hash=sha256:2c2f6e32548ac8d559b47944effcf8ae4d81c161f6b6c885edc53bc08b8f192d \ + --hash=sha256:2ca790779aa1cba1329b8dc42ccebada441d9ac1d932de980183d544682c646d \ + --hash=sha256:2d1128da41c9cb474e0a4701f9c363ec645c9d1a02229904c76bf4e0a194fde2 \ + --hash=sha256:3098446ba8cf774f61cb8d4008c1dba14a30426a15169cd95ac3392a461193b1 \ + --hash=sha256:30cacc5fb696e64b8ef6fd31d9549d394dd7d52760db072eecb98e37e3af1677 \ + --hash=sha256:315d32d1a71494d6b4e1e14a9fa7a4329597b4c4340088ad7e1a9dafbeed92a9 \ + --hash=sha256:32fbc7447be8e3be99bf7869f7066308f16be55b61f9882c2cefc7931f5c7664 \ + --hash=sha256:33741359798f9dc3d4244a66031575d8a86c004f7853eb9961a49e4b6fab2d0b \ + --hash=sha256:36b1f99dc451f1a3981f236151465bcf995bbe712d0727c9f7b236fe228a8133 \ + --hash=sha256:37a68e6f2ac95578ce3c0564802404b27b24988649616e556c07e77111ed3f1d \ + --hash=sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596 \ + --hash=sha256:387cbe2b2bcace397da91f9b1165a9e75da254bb306b876a43b824cc10f49ce0 \ + --hash=sha256:3a075a29ebef752784a91532a1a85be6b234ccffec0a9d7978a92696387c3da6 \ + --hash=sha256:3b0a2dee92dfaabcfb93629188c3e9cf74fdfc0f22e7c369cb444a98814a1e50 \ + --hash=sha256:404da669e5e02bf7fb2cc56715a609f63af88aea531287494467109f97865fe3 \ + --hash=sha256:41d701bb34f81f0b11c724cc544b9a10b26a28f4d0d1197f2037c91225708706 \ + --hash=sha256:48649cf2d8c358d79586e9fb2f8235902fcaa2d969ec1c5301f2d1873b2f8321 \ + --hash=sha256:48b1059e4f2a6ec3e41983148eb1eec5ef9fa3a80bbc4ac0893ac76b115fe039 \ + --hash=sha256:48b36e3235140510dc7861f0cd58b714b1cdd3d48f75e10ce52e69866b746f10 \ + --hash=sha256:4e6df5c3301e65fb42bc5338bf9a1027a02b0a31dc7f54c33775229af474daf0 \ + --hash=sha256:4f27bc4801358dc070d6697b41237fce9923d8e69a1ce1e95606ac36c1552dc1 \ + --hash=sha256:4f59b45f3ef8650c0c736a57f59031d47ed9df4c0a64e83796849d7d14863a2d \ + --hash=sha256:547381cca999be88b4715a0ed7afa11f07fc7e53cb1883687b190d25a92c56cf \ + --hash=sha256:56291ec1a11c3499890c99a8fd9053b47e60fe837a77ec72c0671b1b8b3dce24 \ + --hash=sha256:57c584af6c375ea3f826d8131a94cb212b3d9926eaff67117e3711bbff3a83a5 \ + --hash=sha256:5a3c2bc1cc8164bedbc160b7bb1e8cc1e8b9c27f69ae4f9ae2b976cdae02b2dd \ + --hash=sha256:5a8e486d238850ddf2b25739317b6551d5bef9925ab004b18c552ff6e645f8a2 \ + --hash=sha256:5e2b4adb0fa46a842c492423e61063d6639cf9aea56380a02630ddcdd4894067 \ + --hash=sha256:631bec5f951a30a4b332b4a57d0cdd5a2c8187eb71301f966425f2e54a697855 \ + --hash=sha256:67db6814beaa5fefe91101ec7eb9efda613795767be96f7cf58b1ca8c9ca9972 \ + --hash=sha256:6b865eb702c3af71cf7331919a787563ce2413f7a54ef49ec6709a01b4f22ce6 \ + --hash=sha256:73a9d2809bd8d4a7cda4d336dc996a565eb4feaaa39932f9d85a65fa18382f28 \ + --hash=sha256:78cb0d2453b50bf2035f85fd0d9cfabdb98c47f9c53ddb7c23873cd83da9560b \ + --hash=sha256:7b1c9bdca33968c0dcd875f8185b3b6275df753fe000178684b0c1738959f3cd \ + --hash=sha256:7b42c6471288dedc979ac8400d9c9770f03967dd187db1f8d3405d4d182cc714 \ + --hash=sha256:7c5a5b3dbb9e8918e223be6580da5ffcf861c0505bbc196ebed7176ce05b7b4e \ + --hash=sha256:7ccfb105fcfe91a22bbb5563ad3dc124bc1aa75bfd2e53a780ab05f78cdf6108 \ + --hash=sha256:7dcb9d40930dfad7ab6b20bcc6ca9d2b030b0f347a0cd9909b54bd53ead521b1 \ + --hash=sha256:7f700a6d6f64112ae9193709b84303bbab84424ad4b47d0253301aabce9dfc70 \ + --hash=sha256:807eeda5551f6884d3b4421578be37be50ddb7a58832348e99617a6714a73748 \ + --hash=sha256:83aef30f106edcc21a6a4cc44b82d3169a1dbe255508db788e778f3c804d3583 \ + --hash=sha256:83ee76bf2c9910513dbc19e7d82367131fa7508dedd6186a462393071cc11059 \ + --hash=sha256:8641c8d535c2d95b45c2e19b646ecd23ebba35d461e0ae48a3498277006250ab \ + --hash=sha256:8a6572f3238851fde28b3194ef98cec9dbe66f1614caf4646239ea87f324121a \ + --hash=sha256:8cbd9d67357f3a925f2af1d44db3e8ef1ce1a293ea0add98081b072d4a12e3b4 \ + --hash=sha256:8f09a713d17bcd55da8ab02ebd9110c5246a49c44182af213b5212800af8bc83 \ + --hash=sha256:8f557ce9106850c79252792962d78b987e11fcdc10e5c2252443b9a485d3bfe5 \ + --hash=sha256:91155b110788b5501abc7ea954f1d08606219e4e28e3c73a94124307c06efb80 \ + --hash=sha256:9262d11d0cd11ee3303a95156939402bed6cedfe5ed0e331b95a283a4da6eb8b \ + --hash=sha256:99ebade8c9ada4df975372d8dd25883daa0e379a05f1cd0c99aa0c04368d01a6 \ + --hash=sha256:9a7c43a0584742dface3ca0daf6f719d46c1ac2f87cf080050f9ae052c75e1b2 \ + --hash=sha256:9cc0eee720dd2f14f3b7c349469402b99ad81a174ab49d3533974529e9d93992 \ + --hash=sha256:9f0e686960ffe9e65066395af856ac2d52c159043144433602c50c221d81c1ba \ + --hash=sha256:a070c7769fec277409ad0b3d55b2f0a3703a6f00cf5031fe93090f155bf56382 \ + --hash=sha256:a0891a9be0def16fb320af21a198ece052eed72bf44d73d8ff43f702bd26fd6b \ + --hash=sha256:ac204542736aa295fa25f713b7fad6fc50b46ab7764d16087575c85f085174f3 \ + --hash=sha256:ac8a65e798f2462552c00d2e013d532c94d646729dda98458beaf51f9ec7b120 \ + --hash=sha256:b089a81c58e6ea0485562bbbbbca4f65c0549521606d5ef27fba217aac9b665a \ + --hash=sha256:b308da17b92481e0587244631c5529e5d91d04cb2b08194825627b1eca28e21e \ + --hash=sha256:b317a2b97019c0b95ce99f4f901ae383f40132da6706cdf1731066a73394c25c \ + --hash=sha256:b478652b580cd4cf7f2dd40dc9fde594ed1c84e5df4bafefffb8387ddb74049f \ + --hash=sha256:b50f9c5f826ddca1246f055148df939f5f3f2d0d96db73de28e2233f22210d4c \ + --hash=sha256:b737c0b280f41143266445de2689c0e49c79307e51c44ce3a77fef2bedad4994 \ + --hash=sha256:b839d5c802e31348b949b6473f8190cddbf7d47475856d8ac995a373ee16ec59 \ + --hash=sha256:b902f0fc7c2cf503865a05718b68147c6cd5d0a3867af38c527be574a9fa6e9d \ + --hash=sha256:bc1e8ce33d5a337f2ba862e0719b8201cd54aaed967406c748e009191d47efdd \ + --hash=sha256:bd195af20e53aaac6cf5d7862e34dfdf86351720c858581ccb6563e02ae59421 \ + --hash=sha256:c05f53362568c75476b5c96659377a5dfd982cfbe5a5c07de5106d08a04efc4f \ + --hash=sha256:c1ce5b2366f85cfdbf7f0907755043707f86d09a5b1b1acebbb7bf1600d75c64 \ + --hash=sha256:c2012f64d2cd7cca50f49f22445aa5a88691ac2b4498ee0a9a977f8ca4f7289f \ + --hash=sha256:c2e25417cec5cd9bddb151e33cb08c50160f317479ecc02b22a95ec18f8fe004 \ + --hash=sha256:c326a2b4b85e959d9a1fc3a11f32f84611b6ec07c053e1828a860edf8d068208 \ + --hash=sha256:c3ad79ed32004d9de91cacd4b5faaff44d56051392fe1d5526feda596f01af25 \ + --hash=sha256:c6b1064f3f9cf9072e1d59dd2936f9f3b668bec1c37039708c9222db703c0d5b \ + --hash=sha256:caeed15dcb1233a5a94bc6ff37ef5393cf5b33a45e4bdfb2d6042f3d24e1cb27 \ + --hash=sha256:d07d6c63106d3a9c9a333e2636f9c82c703b1a9e3b079299e58747964e4fdb72 \ + --hash=sha256:d157c48d28eebe5d46906de06a6a2f2c9e00b67d3e42de1f1b9c2d42b810f77c \ + --hash=sha256:d26e9eea3715008a09a74585fe9becd0c67fbb145dc4df9756d597d7230a652c \ + --hash=sha256:d333a50bdd814a917d8d6a7ee35ba2395d53ddaa882613bc24e54a9d8b129095 \ + --hash=sha256:d61db38eb4ee5192f0c261b7f2d38e420b554df8912245e3546aee5c45e2fd78 \ + --hash=sha256:d8060f42db3cd204871db0afd51fef54a13fa544c4dd48cdcae2e174ef40c8ba \ + --hash=sha256:d9ffa75a7ef4b97d6e5e205fabd4304ef01fec09e6f1bdde04b9ad1b07d20289 \ + --hash=sha256:dc4620a47c6fe6a39f89392c00833a82fc050ce90169798f78a25a8d4df03b6e \ + --hash=sha256:dd51dd16182b4bfdcefd27b39b856aa4a57b77f15b231a2d10c45391b0a02028 \ + --hash=sha256:de12004a7da7f1eb67ece37439a5a23a915636085dd042176fda362e006e6940 \ + --hash=sha256:de87422197cf7f83db91d89c86a21660d749b3cd76cd8a45d115b8e675670f02 \ + --hash=sha256:df73724fce8ad53c670358c905b37930bd7b9d92e57db640a65c53b2706eee00 \ + --hash=sha256:dfff584138be087457cc474791d082fdfe32b0d427613d5494a679fe9f4eaef5 \ + --hash=sha256:e094a8f85db41aa7f6a45c5dac2950afc9862e66832934231962252b5d284eed \ + --hash=sha256:e4e2c72a529fa03ff228be1d2b76944013f428220b764e03cc50ada67e17a42c \ + --hash=sha256:e698fe2d8f75c4e9368ee3f4e0d3322d1180be2ec4592d3f73b2572765b1c705 \ + --hash=sha256:e69aa5e10b7e8b1bb4a6888650fd12fcbf11d396ca11d4a44de1450875702830 \ + --hash=sha256:e80011f808b03d1d87a8f1e76ae3da19a18eb706c823e17981dcf1fae43744fc \ + --hash=sha256:e9fcabd1857492b5bf16f90258babde50f618f55d046b1309972da2396321ff9 \ + --hash=sha256:ea1ad8c89da31512fe2d249cf0638fb666925bda341901541bc5f3311c6fcc9e \ + --hash=sha256:f0c1cbb7d6112932cc188c6be007a5e2867005a069e47f42fe67bf5f122b0908 \ + --hash=sha256:f1a6197eadff5bd0bb932f12bb038d403cb75db5b0b391e70e816a647745ddaf \ + --hash=sha256:fa8ab79cea8a1bfe52a21a9b37859c15478d009f242f47737201ecea885b9dd9 \ + --hash=sha256:fb3ec2c7f54c07b30d89983ce78dc32c37dd06a972448b8716d609493802d628 \ + --hash=sha256:fcaa1c3c846a7f6686b38fe493d1b2e8007380e293bfef6a9354563c026cbf36 \ + --hash=sha256:fd05e1edb6a90ad446fa268ab09e59202766b837597b714b2492db11ee87fab9 +pydot==3.0.4 \ + --hash=sha256:3ce88b2558f3808b0376f22bfa6c263909e1c3981e2a7b629b65b451eee4a25d \ + --hash=sha256:bfa9c3fc0c44ba1d132adce131802d7df00429d1a79cc0346b0a5cd374dbe9c6 +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 +pynvvideocodec==2.1.0 \ + --hash=sha256:09cdd222c761855cde9b05ddc13d21893473cebae50077eca20b4eac0fed8acb \ + --hash=sha256:133b47336cd9b3a161707b02d2ceb9e5af387cb9833335d60c16eddf7322b3ea \ + --hash=sha256:34ddbb22230f4963f5b25b41af0a1579c3731fb95c79993502dd014a4dec7fd6 \ + --hash=sha256:3e1ecef741e1943978911998aa1a9d683be8b005957646db6c9b0d53f943f9af \ + --hash=sha256:6d653240ce835e9ab531a4d61350a4af54f7f7859e5392e1323c030ffcfc8ff0 \ + --hash=sha256:7c35887e294613825c9011dc1b6962d0c0374049b1ce8939cee97db5d82b28e8 \ + --hash=sha256:af7e44d13cfb524626c72d947f12a3b192cfa1deb4b598a7778bd4aa1e6fd4ac \ + --hash=sha256:b8d9b47b5c910953d6c24fa60fd7bf6ac57f3d4a20831fcaf35e039349a91d42 \ + --hash=sha256:c317fbad725b1b81936b863edcd98920bc4be423b9965719657030359d7afe29 \ + --hash=sha256:e778af3319a759c1728065e9e4682f591c44dedbbc90c1ca71d025dce65d6a58 \ + --hash=sha256:ee5d2dc56ac5ca8d223fa6fe8f57c67faf5ee1c9360216d9194cfbcd3f9d3948 +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc +pytest==9.0.3 \ + --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ + --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 +requests==2.33.1 \ + --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ + --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 +safetensors==0.7.0 \ + --hash=sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2 \ + --hash=sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0 \ + --hash=sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd \ + --hash=sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981 \ + --hash=sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a \ + --hash=sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3 \ + --hash=sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d \ + --hash=sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0 \ + --hash=sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85 \ + --hash=sha256:6999421eb8ba9df4450a16d9184fcb7bef26240b9f98e95401f17af6c2210b71 \ + --hash=sha256:7b95a3fa7b3abb9b5b0e07668e808364d0d40f6bbbf9ae0faa8b5b210c97b140 \ + --hash=sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104 \ + --hash=sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57 \ + --hash=sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4 \ + --hash=sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba \ + --hash=sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517 \ + --hash=sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b \ + --hash=sha256:cfdead2f57330d76aa7234051dadfa7d4eedc0e5a27fd08e6f96714a92b00f09 \ + --hash=sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755 \ + --hash=sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48 \ + --hash=sha256:dc92bc2db7b45bda4510e4f51c59b00fe80b2d6be88928346e4294ce1c2abe7c \ + --hash=sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542 \ + --hash=sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737 +scikit-learn==1.7.2 \ + --hash=sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1 \ + --hash=sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7 \ + --hash=sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c \ + --hash=sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda \ + --hash=sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a \ + --hash=sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c \ + --hash=sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18 \ + --hash=sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f \ + --hash=sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973 \ + --hash=sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290 \ + --hash=sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c \ + --hash=sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f \ + --hash=sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0 \ + --hash=sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8 \ + --hash=sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d \ + --hash=sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96 \ + --hash=sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1 \ + --hash=sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106 \ + --hash=sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61 \ + --hash=sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c \ + --hash=sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8 \ + --hash=sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1 \ + --hash=sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe \ + --hash=sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476 \ + --hash=sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44 \ + --hash=sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8 \ + --hash=sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e \ + --hash=sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5 \ + --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ + --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ + --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 +scipy==1.15.3 \ + --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ + --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ + --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ + --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ + --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ + --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ + --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ + --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ + --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ + --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ + --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ + --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ + --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ + --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ + --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ + --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ + --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ + --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ + --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ + --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ + --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ + --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ + --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ + --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ + --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ + --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ + --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ + --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ + --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ + --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ + --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ + --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ + --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ + --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ + --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ + --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ + --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ + --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ + --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ + --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ + --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ + --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ + --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ + --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ + --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ + --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 +starlette==1.0.0 \ + --hash=sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149 \ + --hash=sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b +sympy==1.14.0 \ + --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ + --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 +tensorrt-cu12==10.14.1.48.post1 \ + --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 +tensorrt-cu12-bindings==10.14.1.48.post1 \ + --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ + --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ + --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ + --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ + --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ + --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ + --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ + --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ + --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ + --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ + --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ + --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 +tensorrt-cu12-libs==10.14.1.48.post1 \ + --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 +threadpoolctl==3.6.0 \ + --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ + --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 +torch==2.10.0 \ + --hash=sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591 \ + --hash=sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574 \ + --hash=sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60 \ + --hash=sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd \ + --hash=sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313 \ + --hash=sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547 \ + --hash=sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382 \ + --hash=sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4 \ + --hash=sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d \ + --hash=sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f \ + --hash=sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49 \ + --hash=sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d \ + --hash=sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b \ + --hash=sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf \ + --hash=sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f \ + --hash=sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c \ + --hash=sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5 \ + --hash=sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738 \ + --hash=sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294 \ + --hash=sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185 \ + --hash=sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63 \ + --hash=sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb \ + --hash=sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b \ + --hash=sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6 \ + --hash=sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321 \ + --hash=sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763 \ + --hash=sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb \ + --hash=sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8 \ + --hash=sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444 \ + --hash=sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac \ + --hash=sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328 \ + --hash=sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b \ + --hash=sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a \ + --hash=sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57 \ + --hash=sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f \ + --hash=sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6 \ + --hash=sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e \ + --hash=sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28 \ + --hash=sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8 +torchvision==0.25.0 \ + --hash=sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce \ + --hash=sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6 \ + --hash=sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266 \ + --hash=sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee \ + --hash=sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec \ + --hash=sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563 \ + --hash=sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c \ + --hash=sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef \ + --hash=sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1 \ + --hash=sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03 \ + --hash=sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443 \ + --hash=sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52 \ + --hash=sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7 \ + --hash=sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56 \ + --hash=sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4 \ + --hash=sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2 \ + --hash=sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264 \ + --hash=sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233 \ + --hash=sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b \ + --hash=sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa \ + --hash=sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f \ + --hash=sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917 \ + --hash=sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20 \ + --hash=sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7 \ + --hash=sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977 \ + --hash=sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248 \ + --hash=sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3 \ + --hash=sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3 +triton==3.6.0 \ + --hash=sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d \ + --hash=sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9 \ + --hash=sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6 \ + --hash=sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4 \ + --hash=sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd \ + --hash=sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7 \ + --hash=sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651 \ + --hash=sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781 \ + --hash=sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca \ + --hash=sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803 \ + --hash=sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea \ + --hash=sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f \ + --hash=sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3 \ + --hash=sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43 +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +ultralytics==8.4.38 \ + --hash=sha256:88e4c26205a8472773725db60346a46bbd5ac35e9dab19a7f6f954fa202aedcd \ + --hash=sha256:f4d0c1efb04b75e3fab1481e4d6eecc352ad3736223e4980671f424daa95a99a +ultralytics-thop==2.0.18 \ + --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ + --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec +urllib3==2.6.3 \ + --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ + --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +uvicorn==0.44.0 \ + --hash=sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e \ + --hash=sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89 +vdms==0.0.23 \ + --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ + --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 +wheel==0.46.3 \ + --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ + --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 + +# The following packages are considered to be unsafe in a requirements file: +pip==26.0.1 \ + --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ + --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 diff --git a/video/resources/models/models.lst b/fastapi/resources/models/models.lst similarity index 100% rename from video/resources/models/models.lst rename to fastapi/resources/models/models.lst diff --git a/video/resources/models/ultralytics/custom_models/.gitignore b/fastapi/resources/models/ultralytics/custom_models/.gitignore similarity index 100% rename from video/resources/models/ultralytics/custom_models/.gitignore rename to fastapi/resources/models/ultralytics/custom_models/.gitignore diff --git a/fastapi/shell.sh b/fastapi/shell.sh new file mode 100755 index 0000000..9d60aaa --- /dev/null +++ b/fastapi/shell.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +IMAGE="lcc_fastapi" +DIR=$(dirname $(readlink -f "$0")) + +. "$DIR/../script/shell.sh" diff --git a/fastapi/templates/index.html b/fastapi/templates/index.html new file mode 100644 index 0000000..2da2d9a --- /dev/null +++ b/fastapi/templates/index.html @@ -0,0 +1,269 @@ + + + + + + AI Detection Dashboard + + + + +

Live Detection Dashboard

+ + + + +
+ +
+ + +
+ {% for id in cameras %} +
+

Camera: {{ id }}

+
+
DISPLAY FPS: 0.0
+ +
TARGET FPS: 0.0
+ +
+
+ Camera Stream {{ id }} +
+
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/fastapi/tests/nginx.conf b/fastapi/tests/nginx.conf new file mode 100644 index 0000000..ac1b556 --- /dev/null +++ b/fastapi/tests/nginx.conf @@ -0,0 +1,12 @@ +events { worker_connections 1024; } + +http { + include mime.types; + default_type application/octet-stream; + client_max_body_size 100M; + + server { + listen 80; + server_name _; + } +} \ No newline at end of file diff --git a/fastapi/tests/test_detections.py b/fastapi/tests/test_detections.py new file mode 100644 index 0000000..6cac5b9 --- /dev/null +++ b/fastapi/tests/test_detections.py @@ -0,0 +1,2179 @@ +import argparse +import csv +import gc +import logging +import multiprocessing as mp +import os +import sys +import threading +import time +from pathlib import Path + +import cv2 +import matplotlib.pyplot as plt +import numpy as np +import pytest +import tensorrt as trt +import torch +import torch.nn.functional as F + +sys.path.insert(1, str(Path(__file__).parent.parent)) +from include.default_configs import ( + CUSTOM_MODEL_FLAG_DEFAULT, + DEBUG_DEFAULT, + MODEL_NAME_DEFAULT, + THRESHOLD_VALUE, +) +from include.handlers import ( + CPUStreamHandler, + DeviceBaseHandler, + GPUStreamHandler, + log_to_logger, +) +from include.handlers import test_rendering_worker as rendering_worker +from include.utils import ( + PipelineConfig, + tensor2opencv, +) + +try: + torch.multiprocessing.set_start_method("spawn", force=True) +except RuntimeError: + pass + +logging.getLogger("matplotlib").setLevel(logging.WARNING) +logger = trt.Logger(trt.Logger.WARNING) +trt.init_libnvinfer_plugins(logger, "") + +DEBUG_FLAG_DEFAULT = True if DEBUG_DEFAULT == "1" else False +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["PYTORCH_ALLOC_CONF"] = "expandable_segments:True" +force_export = False + + +# def render_display_worker(queue, output_path, fps, size): +# """Background process that draws labels and writes to video.""" +# fourcc = cv2.VideoWriter_fourcc(*"avc1") +# writer = cv2.VideoWriter(output_path, fourcc, fps, size) + +# if not writer.isOpened(): +# print(f" [ERROR] VideoWriter failed to open: {output_path}") +# return + +# while True: +# item = queue.get() +# if item is None: +# break + +# display_frame, metadata_or_bbs, class_list = item +# if (display_frame.shape[1], display_frame.shape[0]) != size: +# display_frame = cv2.resize(display_frame, size) + +# if isinstance(metadata_or_bbs, dict): +# for _, obj in metadata_or_bbs.items(): +# bbox = obj["bbox"] +# x, y, w, h = bbox["x"], bbox["y"], bbox["width"], bbox["height"] +# class_name = bbox["object"] +# class_id = class_list.index(class_name) if class_name in class_list else 0 +# confidence = bbox["object_det"]["confidence"] + +# bb_color = get_detection_color(class_id, is_bgr=True) +# label = f"{class_name} {confidence:.2f}" + +# cv2.rectangle(display_frame, (x, y), (x + w, y + h), bb_color, 2) +# draw_label(display_frame, label, (x, y), color=bb_color, padding=5) +# elif metadata_or_bbs is not None: +# for box in metadata_or_bbs: +# if torch.is_tensor(box): +# box = box.tolist() +# x1, y1, x2, y2 = map(int, box) +# cv2.rectangle(display_frame, (x1, y1), (x2, y2), (0, 0, 225), 2) + +# writer.write(display_frame) + +# writer.release() + + +def fps_comparison_chart(chart_path, results, fps_key="Pipeline FPS (Video frames)"): + try: + names = [r["Test Name"] for r in results] + fps_values = [float(r[fps_key]) for r in results] + + plt.figure(figsize=(10, 6)) + plt.grid(axis="y", linestyle="--", alpha=0.7, zorder=0) + + colors = ["#2ca02c" if "gpu" in n.lower() else "#1f77b4" for n in names] + bars = plt.bar(names, fps_values, color=colors, zorder=3) + plt.ylabel("Frames Per Second (FPS)") + plt.title(f"Performance Comparison: {chart_path.stem}") + plt.xticks(rotation=45) + + for bar in bars: + yval = bar.get_height() + plt.text( + bar.get_x() + bar.get_width() / 2, + yval + 1, + f"{yval:.1f}", + ha="center", + va="bottom", + ) + + if fps_values: + plt.ylim(0, max(fps_values) * 1.2) + + plt.tight_layout() + plt.savefig(str(chart_path)) + print(f" Comparison chart saved to: {chart_path}") + except Exception: + print("Skipping chart generation: error occurred.") + + +@pytest.fixture(scope="class") +def setup_context(request): + """Replaces setUpClass: Runs once per test class.""" + current_test_filename = Path(__file__).stem + test_dir = Path(__file__).parent + main_path = test_dir.parent + video_dir = main_path / "inputs" + + VIDEO_FILENAME = os.getenv("VIDEO_FILENAME", "anduril_swarm_8K.mp4") + if video_dir.exists(): + request.cls.video_path = video_dir / VIDEO_FILENAME + else: + video_dir = Path("/watch_dir") + request.cls.video_path = video_dir / VIDEO_FILENAME + + model_name = os.getenv("MODEL_NAME", MODEL_NAME_DEFAULT) + request.cls.result_dir = ( + test_dir + / f"{current_test_filename}_results/{model_name}" + / request.cls.video_path.stem + ) + request.cls.result_dir.mkdir(parents=True, exist_ok=True) + request.cls.benchmarks = [] + request.cls.csv_filename = ( + f"pipeline_benchmarks_{model_name}_{request.cls.video_path.stem}.csv" + ) + + request.cls.name = request.cls.video_path.stem + request.cls.source = str(request.cls.video_path) + request.cls.active = True + request.cls.active_streams = {} + + request.cls._shared_model = None + request.cls._shared_model_path = None + request.cls._shared_model_device = None + request.cls._shared_model_sf_enabled = None + yield + + if request.cls.benchmarks: + # Filter and exclude rows that were interrupted or failed initialization due to an early pytest skip + request.cls.benchmarks = [ + r for r in request.cls.benchmarks if r and "Test Name" in r + ] + results = request.cls.benchmarks + for row in results: + if "gpu" in row["Test Name"].lower(): + match_name = row["Test Name"].replace("gpu", "cpu") + cpu_row = next( + (r for r in results if r["Test Name"] == match_name), None + ) + if cpu_row: + gpu_fps = float(row["Pipeline FPS (Video frames)"]) + cpu_fps = float(cpu_row["Pipeline FPS (Video frames)"]) + speedup = (gpu_fps / cpu_fps) if cpu_fps > 0 else 0 + row["Pipeline Speedup vs CPU"] = f"{speedup:.2f}x" + else: + row["Pipeline Speedup vs CPU"] = "N/A" + else: + row["Pipeline Speedup vs CPU"] = "Baseline (CPU)" + + keys = results[0].keys() + with open( + str(request.cls.result_dir / request.cls.csv_filename), "w", newline="" + ) as f: + dict_writer = csv.DictWriter(f, fieldnames=keys) + dict_writer.writeheader() + dict_writer.writerows(results) + + print(f"\n[FINAL] Benchmarks saved to {request.cls.csv_filename}") + + for r in results: + print( + f" > {r['Test Name']}: {r['Pipeline FPS (Video frames)']} FPS | {r['Pipeline FPS (Target frames)']} FPS | {r['sf+roi+det FPS (Target frames)']} FPS | {r['Display FPS']} FPS | Speedup: {r.get('Pipeline Speedup vs CPU', 'N/A')}" + ) + + chart_path = ( + request.cls.result_dir + / f"{request.cls.csv_filename.replace('.csv', '')}_sfFPS.png" + ) + fps_comparison_chart( + chart_path, results, fps_key="sf+roi+det FPS (Video frames)" + ) + + chart_path = ( + request.cls.result_dir + / f"{request.cls.csv_filename.replace('.csv', '')}_pipelineFPS.png" + ) + fps_comparison_chart(chart_path, results, fps_key="Pipeline FPS (Video frames)") + + chart_path = ( + request.cls.result_dir + / f"{request.cls.csv_filename.replace('.csv', '')}_sfFPS_target.png" + ) + fps_comparison_chart( + chart_path, results, fps_key="sf+roi+det FPS (Target frames)" + ) + + chart_path = ( + request.cls.result_dir + / f"{request.cls.csv_filename.replace('.csv', '')}_pipelineFPS_target.png" + ) + fps_comparison_chart( + chart_path, results, fps_key="Pipeline FPS (Target frames)" + ) + + chart_path = ( + request.cls.result_dir + / f"{request.cls.csv_filename.replace('.csv', '')}_displayFPS_target.png" + ) + fps_comparison_chart(chart_path, results, fps_key="Display FPS") + + +@pytest.fixture(autouse=True) +def each_test_setup(request): + test_class_self = request.instance + if torch.cuda.is_available(): + torch.cuda.synchronize() + + device = request.node.callspec.params.get("device") + detection_type = request.node.callspec.params.get("detection_type") + sf_enabled = request.node.callspec.params.get("sf_enabled") + + test_class_self._testMethodName = ( + f"sf_{detection_type}_{device}" + if sf_enabled + else f"yolo_{detection_type}_{device}" + ) + + # 1. Re-initialize a fresh configuration object + test_class_self.config = PipelineConfig( + CUSTOM_MODEL_FLAG=os.getenv("CUSTOM_MODEL_FLAG", CUSTOM_MODEL_FLAG_DEFAULT), + DEVICE=device.upper(), + OMIT_DETECTIONS_FLAG=True, + TEST_MODE=True, + DEBUG=os.getenv("DEBUG", DEBUG_DEFAULT), + DEBUG_FRAME_LIMIT=int(os.getenv("DEBUG_FRAME_LIMIT", 100)), + ENABLE_QUERYING=False, + MODEL_NAME=os.getenv("MODEL_NAME", MODEL_NAME_DEFAULT), + SMART_FILTERING_ENABLED=sf_enabled, + THRESHOLD_VALUE=int(os.getenv("THRESHOLD_VALUE", THRESHOLD_VALUE)), + DETECTION_TYPE=detection_type, + ) + + # video_output_name = f"{test_class_self._testMethodName}_detections_output.mp4" + vid_dir = test_class_self.result_dir / "results" + vid_dir.mkdir(parents=True, exist_ok=True) + # test_video_output_path = os.path.join(str(vid_dir), video_output_name) + os.environ["TEST_SUITE_RENDER_DIR"] = str(vid_dir) + test_class_self.config.SHARED_OUTPUT = str(test_class_self.result_dir) + + # Reset core state machine properties before invoking any backend handlers + test_class_self.active = True + test_class_self._is_stopped = False + + # FIXING ATTRIBUTEERRORS: Explicitly provision primitives before method re-binding + import threading + import time + + test_class_self._stop_lock = threading.Lock() + # test_class_self.stat_start_time = time.perf_counter() # timing to display detection + + test_class_self.device = test_class_self.config.DEVICE + test_class_self.device_input = test_class_self.config.device_input + test_class_self.resize_h, test_class_self.resize_w = [ + test_class_self.config.MODEL_H, + test_class_self.config.MODEL_W, + ] + + # Resolve concrete handler class type + HandlerClass = GPUStreamHandler if device == "gpu" else CPUStreamHandler + HandlerClass.pipeline_fn = test_class_self.__class__.pipeline_fn + + # 2. Dynamically re-bind backend methods to this execution instance + import inspect + import types + + handler_classes = [HandlerClass, DeviceBaseHandler] + all_method_names = set() + + for cls in handler_classes: + for name, attr in inspect.getmembers(cls, predicate=inspect.isfunction): + all_method_names.add(name) + + for method_name in all_method_names: + source_obj = ( + HandlerClass if hasattr(HandlerClass, method_name) else DeviceBaseHandler + ) + if hasattr(source_obj, method_name): + raw_func = getattr(source_obj, method_name) + if ( + not hasattr(test_class_self.__class__, method_name) + or method_name == "pipeline_fn" + ): + setattr( + test_class_self, + method_name, + types.MethodType(raw_func, test_class_self), + ) + + # FIXING FILEEXISTSERRORS: Proactively clear lingering POSIX layout blocks in /dev/shm + from multiprocessing import shared_memory + + for i in range(4): # Matches your self.ai_ring_depth footprint + stale_shm_name = f"shm_ai_640_{test_class_self.name}_{i}_{os.getpid()}" + try: + # Force link onto lingering handle and unlink it instantly from the OS map + lingering_shm = shared_memory.SharedMemory(name=stale_shm_name) + lingering_shm.close() + lingering_shm.unlink() + except FileNotFoundError: + pass + + # 3. FORCE NATIVE PIPELINE PROVISIONING + test_class_self.setup_reader( + test_class_self.config.TARGET_FPS, test_class_self.config.CLIP_DURATION + ) + test_class_self.initialize_variables() + test_class_self.setup_model(None) + test_class_self.prepare_pipeline() + + # Reset runtime loop state properties + test_class_self.render_queue = None + test_class_self.frame_count_target = 0 + test_class_self.next_process_idx = 0.0 + test_class_self.frame_in_clip_count = 0 + test_class_self.frame_count = 0 + test_class_self.elapsed_display_time = 0.0 + + if hasattr(test_class_self, "setup_threads"): + test_class_self.setup_threads() + + shared_event_buffer = {"sf": [], "roi": [], "det": []} + test_class_self.gpu_event_buffer = shared_event_buffer + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + yield + + # Teardown logic + if ( + hasattr(test_class_self, "render_queue") + and test_class_self.render_queue is not None + ): + try: + test_class_self.render_queue.put(None) + except Exception: + pass + + render_proc_handle = getattr(test_class_self, "render_proc", None) + if render_proc_handle is not None and render_proc_handle._started.is_set(): + test_class_self.render_proc.join(timeout=10.0) + + # Signal the state machine to drop out of loops safely + test_class_self.active = False + if hasattr(test_class_self, "reader") and test_class_self.reader is not None: + try: + test_class_self.reader.stop() + except Exception: + pass + + # Execute custom release blocks + test_class_self.stop() + + # CRITICAL FIX: Block and wait for the background producer thread to die + # BEFORE we delete the reader attribute from the namespace map. + producer_thread_handle = getattr(test_class_self, "process_thread", None) + if producer_thread_handle is not None and producer_thread_handle.is_alive(): + producer_thread_handle.join(timeout=5.0) + + # Now it is structurally safe to purge attributes without causing cross-thread collisions + keys_to_purge = [ + "reader", + "process_thread", + "inference_stream", + "bgs_stream", + "model", + "raw_input", + "ai_gpu_staging", + "ai_pinned_tensors", + "executor", + "io_executor", + ] + for key in keys_to_purge: + if hasattr(test_class_self, key): + delattr(test_class_self, key) + + if ( + hasattr(test_class_self, "gpu_event_buffer") + and test_class_self.gpu_event_buffer + ): + test_class_self.gpu_event_buffer.clear() + if hasattr(test_class_self, "component_stats") and test_class_self.component_stats: + test_class_self.component_stats.clear() + + with torch.inference_mode(): + if torch.cuda.is_available(): + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + + gc.collect() + time.sleep(0.2) + + +@pytest.mark.usefixtures("setup_context") +class TestSmartFilteringDetections: + # class TestSmartFilteringDetections(DeviceBaseHandler): + # """ + # Unified testing harness. + # By inheriting from DeviceBaseHandler, 'self' functions as both + # the pytest telemetry harness and the live stream execution runner. + # """ + # # Overriding __init__ to prevent standard initialization collisions with pytest + # def __init__(self, *args, **kwargs): + # pass + # SETUP -------------------------------------------- + + @pytest.mark.parametrize("device", ["cpu", "gpu"]) + @pytest.mark.parametrize("sf_enabled", [True, False]) + @pytest.mark.parametrize("detection_type", ["motion", "object"]) + def test_detections(self, detection_type, device, sf_enabled): + """Unified test runner for all configurations.""" + if detection_type == "motion" and not sf_enabled: + pytest.skip( + "Pure YOLO mode is structurally invalid for detection_type 'motion'." + ) + + # Run the actual model loader + # self.get_model_by_device(device, sf_enabled=self.config.sf_enabled) + + # Execute + self.run_pipeline() # pipeline_fn) + + def setup_threads(self): + # Shared 10MB memory for display + self.setup_shared_memory() + + # Executor for Async YOLO tasks and FFmpeg re-encoding + # self.executor = ThreadPoolExecutor(max_workers=self.config.MAX_WORKERS) + # self.clip_executor = ThreadPoolExecutor(max_workers=self.config.MAX_WORKERS) + + print( + f"sf_enabled: {self.config.sf_enabled}\tTEST_MODE: {self.config.TEST_MODE}", + flush=True, + ) + + # Producer: Handles acquisition and AI metadata logs + # self.process_thread = threading.Thread( + # target=self.run_pipeline, + # daemon=True, + # ) + + self.signal_queue = mp.Queue(maxsize=1) + self.render_queue = mp.Queue(maxsize=5) + + # if self.config.TEST_MODE: + test_dir = os.getenv( + "TEST_SUITE_RENDER_DIR", str(Path(self.config.SHARED_OUTPUT)) + ) + os.makedirs(test_dir, exist_ok=True) + + video_output_name = f"{self._testMethodName}_detections_output.mp4" + out_path = os.path.join(test_dir, video_output_name) + + log_to_logger( + f"[TEST MODE] Detection results saved to: {out_path}", level="info" + ) + self.render_proc = threading.Thread( + target=rendering_worker, + args=( + self.render_queue, + (self.disp_w, self.disp_h), + out_path, + self.target_fps, + ), + daemon=True, + ) + + # Dummy target alignment to prevent execution signature exceptions + self.display_proc = threading.Thread(target=lambda: None, daemon=True) + # else: + # self.render_proc = mp.Process( + # target=rendering_worker, + # args=( + # self.render_queue, + # self.shared_details, + # self.ready_buffer_idx, + # self.reader_active_idx, + # self.shm_frame_lengths, + # self.signal_queue, + # (self.disp_w, self.disp_h), + # self.config.DISPLAY_FRAME_QUALITY, + # ), + # ) + + # def display_signal_sync(): + # while self.active: + # # Wait for signal + # # if self.mp_frame_ready_event.wait(timeout=1.0): + # # self.mp_frame_ready_event.clear() + # try: + # _ = self.signal_queue.get(timeout=1.0) + # # print(f"[DEBUG]: Signal received in FastAPI process for {self.name}", flush=True) + # # Wake FastAPI async loop in main thread + # self.loop.call_soon_threadsafe(self.frame_ready_event.set) + # except queue.Empty: + # continue + + # self.display_proc = threading.Thread( + # target=display_signal_sync, daemon=True + # ) + + # if self.config.ENABLE_QUERYING: + # # NEW: Dedicated I/O pool for Disk/GPU transfers (Higher worker count for 8K) + # self.io_executor = ThreadPoolExecutor(max_workers=8) + + # # Dedicated FFmpeg pool so re-encoding doesn't slow down live AI + # self.ffmpeg_executor = ThreadPoolExecutor(max_workers=2) + + # if not self.config.TEST_MODE: + # # Sends metadata to VDMS + # self.metadata_thread = threading.Thread( + # target=send_metadata, + # args=( + # VDMSPool(self.config.DBHOST, self.config.DBPORT, size=10), + # self.config.DEBUG_FLAG, + # self.config.INGESTION, + # self.config.TEST_MODE, + # self.config.UDF_HOST, + # self.config.UDF_PORT, + # self.config.DBHOST, + # self.config.DBPORT, + # ), + # daemon=True, + # ) + + # # Consumer: Handles GPU-to-CPU download and Disk I/O (Writing resized frames to RAM disk) + # self.writer_thread = threading.Thread( + # target=self.video_writer_core_loop, + # args=(self.stop_writer,), + # daemon=True, + # ) + + # HELPERS -------------------------------------------- + def _print_gpu_mem(self): + if torch.cuda.is_available(): + # Memory currently used by tensors + allocated = torch.cuda.memory_allocated(0) / 1024**2 + # Total memory reserved by PyTorch (the "Pool") + reserved = torch.cuda.memory_reserved(0) / 1024**2 + print(f"\tAllocated: {allocated:0.2f} MB") + print(f"\tReserved: {reserved:0.2f} MB") + else: + print("\tCUDA not available.") + + # def get_model_by_device(self, device, sf_enabled=False): + # """Singleton loader: only loads if device changes or model is missing.""" + # if ( + # sf_enabled + # and (self.frame_width * self.frame_height) + # <= self.config.SMART_FILTERING_PIXEL_CONSTRAINT + # ): + # sf_enabled = False + + # if ( + # TestSmartFilteringDetections._shared_model is not None + # and TestSmartFilteringDetections._shared_model_device == device + # and TestSmartFilteringDetections._shared_model_sf_enabled == sf_enabled + # ): + # self.model = TestSmartFilteringDetections._shared_model + # self.model_path = TestSmartFilteringDetections._shared_model_path + # return + + # run_platform_name = "engine" if "cuda" in self.device_input else "openvino" + + # if self.config.CUSTOM_MODEL_FLAG: + # dir_path = "/home/resources/models/ultralytics/custom_models" + # else: + # dir_path = f"/home/resources/models/ultralytics/{self.config.MODEL_NAME}/{self.config.MODEL_PRECISION}" + + # ( + # TestSmartFilteringDetections._shared_model, + # TestSmartFilteringDetections._shared_model_path, + # self.label_source, + # ) = get_model( + # # model_run_key, model_run_config, export=False + # Path(dir_path), + # self.config.MODEL_NAME, + # run_platform_name, + # self.device_input, + # batch=self.config.MODEL_MAX_BATCH_SIZE, + # force_export=force_export, + # sf_enabled=sf_enabled, + # model_h=self.resize_h, + # model_w=self.resize_w, + # ) + # TestSmartFilteringDetections._shared_model_device = device + # TestSmartFilteringDetections._shared_model_sf_enabled = sf_enabled + # self.model = TestSmartFilteringDetections._shared_model + # # self.model.half() + # self.model_path = TestSmartFilteringDetections._shared_model_path + + # W, H = self.resize_w, self.resize_h + # if not sf_enabled: + # W, H = self.frame_width, self.frame_height + # self.model_warmup(H, W) + # # self.pipeline_handler.model = self.model + # # self.pipeline_handler.model_warmup(H, W) + + # def calculate_unique_coverage(self, merged_boxes, target_w=640, target_h=640): + # """ + # FULLY VECTORIZED: Calculate pixel coverage without Python loops. + # Works for any number of boxes (1 to 1000+) with near-zero overhead. + # """ + # if merged_boxes is None or merged_boxes.shape[0] == 0: + # return 0.0 + + # # 1. Scaling (Vectorized) + # # merged_boxes is [N, 4] -> [x1, y1, x2, y2] in 8K space + # scale = torch.tensor( + # [ + # target_w / self.frame_width, + # target_h / self.frame_height, + # target_w / self.frame_width, + # target_h / self.frame_height, + # ], + # device=self.device_input, + # ) + + # # Scale and clamp all boxes at once on the GPU + # coords = (merged_boxes * scale).long() + # coords[:, [0, 2]] = coords[:, [0, 2]].clamp(0, target_w) + # coords[:, [1, 3]] = coords[:, [1, 3]].clamp(0, target_h) + + # # 2. Vectorized Mask Filling + # # We create a 1D representation of the 640x640 mask for fast indexing + # mask = torch.zeros( + # target_h * target_w, device=self.device_input, dtype=torch.uint8 + # ) + + # # For each box, we generate a range of indices and fill them. + # # Note: For small N (drones), a loop is okay, but for 'Swarm' noise, + # # we use this broadcasted approach: + # for i in range(coords.shape[0]): + # x1, y1, x2, y2 = coords[i] + # # Generate row indices for this box + # rows = torch.arange(y1, y2, device=self.device_input).view(-1, 1) + # # Calculate mask indices: (y * width) + x + # # This fills a horizontal slice of the mask in one GPU operation + # indices = (rows * target_w) + torch.arange(x1, x2, device=self.device_input) + # mask[indices] = 1 + + # # 3. Final Sum (GPU Reduction) + # return (torch.sum(mask).item() / (target_w * target_h)) * 100 + + def calculate_unique_coverage(self, merged_boxes, target_w=640, target_h=640): + """ + TRUE LOOPLESS VECTORIZATION: Calculates combined bounding box pixel coverage + in a single GPU pass, completely avoiding CPU-GPU synchronization stalls. + """ + if merged_boxes is None or merged_boxes.shape[0] == 0: + return 0.0 + + scale = torch.tensor( + [ + target_w / self.frame_width, + target_h / self.frame_height, + target_w / self.frame_width, + target_h / self.frame_height, + ], + device=self.device_input, + ) + + coords = (merged_boxes * scale).long() + coords[:, [0, 2]] = coords[:, [0, 2]].clamp(0, target_w) + coords[:, [1, 3]] = coords[:, [1, 3]].clamp(0, target_h) + + x1 = coords[:, 0].view(-1, 1, 1) + y1 = coords[:, 1].view(-1, 1, 1) + x2 = coords[:, 2].view(-1, 1, 1) + y2 = coords[:, 3].view(-1, 1, 1) + + grid_y, grid_x = torch.meshgrid( + torch.arange(target_h, device=self.device_input), + torch.arange(target_w, device=self.device_input), + indexing="ij", + ) + + inside_boxes = (grid_x >= x1) & (grid_x < x2) & (grid_y >= y1) & (grid_y < y2) + unique_mask = torch.any(inside_boxes, dim=0) + + return (torch.sum(unique_mask).item() / (target_w * target_h)) * 100 + + def _finalize_benchmarks( + self, + # n_frames, + num_objs, + total_pipeline_ms, + real_world_latency_ms, + coverage_percentages, + sf_enabled, + stat_frame_count, + stat_fps, + ): + """Aggregates metrics and adds them to the results list.""" + latency_s = total_pipeline_ms / 1000.0 # Just pipeline (sf + roi_ det) + est_fps = self.frame_count / latency_s if latency_s > 0 else 0 + est_fps_processed = self.frame_count_target / latency_s if latency_s > 0 else 0 + duration_s = self.frame_count / self.input_fps if self.input_fps > 0 else 0 + + real_latency_s = real_world_latency_ms / 1000.0 + real_est_fps = self.frame_count / real_latency_s if real_latency_s > 0 else 0 + real_est_fps_processed = ( + self.frame_count_target / real_latency_s if real_latency_s > 0 else 0 + ) + + # Calculate averages for component breakdowns + avg_sf = ( + sum(self.component_stats["sf"]) / len(self.component_stats["sf"]) + if self.component_stats["sf"] + else 0 + ) + avg_roi = ( + sum(self.component_stats["roi"]) / len(self.component_stats["roi"]) + if self.component_stats["roi"] + else 0 + ) + avg_det = ( + sum(self.component_stats["det"]) / len(self.component_stats["det"]) + if self.component_stats["det"] + else 0 + ) + avg_cov = ( + sum(coverage_percentages) / len(coverage_percentages) + if coverage_percentages + else (100.0 if not sf_enabled else 0) + ) + + avg_crops = ( + sum(self.crops_per_frame_list) / len(self.crops_per_frame_list) + if self.crops_per_frame_list + else 0 + ) + + # Total Latency Sum (SF + ROI + DET) + total_sum = avg_sf + avg_roi + avg_det + + # Calculate how often we hit a high-motion cap (e.g., 20 crops) + capped_frames = sum(1 for c in self.crops_per_frame_list if c >= 20) + cap_rate = ( + (capped_frames / len(self.crops_per_frame_list)) * 100 + if self.crops_per_frame_list + else 0 + ) + + self.__class__.benchmarks.append( + { + "Test Name": self._testMethodName, + "Detection Type": self.config.DETECTION_TYPE, + "Device": self.device, + "Smart Filtering": "Enabled" if sf_enabled else "Disabled", + "Video": self.video_path.name, # self.name? + "Video FPS": f"{self.input_fps:.2f}", + "Video Duration (s)": f"{duration_s:.4f}", + "Video Frames": self.frame_count, + "Target Frames": self.frame_count_target, + "Pipeline Latency (s)": f"{real_latency_s:.2f}", + "Display Latency (s)": f"{self.elapsed_display_time:.2f}", + "sf+roi+det Latency (s)": f"{latency_s:.2f}", + # Includes time by HW decoder (reader) and preparing output video + "Pipeline FPS (Video frames)": f"{real_est_fps:.2f}", + "Pipeline FPS (Target frames)": f"{real_est_fps_processed:.2f}", + # TIming to display frame (read to after send to render queue) + "Display Frames": stat_frame_count, + "Display FPS": f"{stat_fps:.2f}", + # Only sf + roi + det + "sf+roi+det FPS (Video frames)": f"{est_fps:.2f}", + "sf+roi+det FPS (Target frames)": f"{est_fps_processed:.2f}", + "Avg SF (ms)": f"{avg_sf:.2f}", + "Avg ROI (ms)": f"{avg_roi:.2f}", + "Avg Obj. Detection (ms)": f"{avg_det:.2f}", + "Total Breakdown Sum (ms)": f"{total_sum:.2f}", + "Avg Area Coverage %": f"{avg_cov:.2f}%", + "Avg Crops/Frame": f"{avg_crops:.1f}", + "Crop Cap Rate (>20)": f"{cap_rate:.1f}%", + "Objects Detected": num_objs, + } + ) + + print(f"\n[{self._testMethodName}] Latency: {latency_s:.2f} sec") + print( + f"\n[{self._testMethodName}] Pipeline FPS (Target frames): {real_est_fps_processed:.2f} ({self.frame_count_target} frames)" + ) + print( + f"\n[{self._testMethodName}] sf+roi+det FPS (Target frames): {est_fps_processed:.2f} ({self.frame_count_target} frames)" + ) + print( + f"\n[{self._testMethodName}] Display FPS (Target frames): {stat_fps:.2f} ({stat_frame_count} frames)" + ) + + # def tensor2opencv_gpu(self, frame_tensor): + # """ + # GPU-native equivalent of tensor2opencv. + # Fixes the '3x3 ghosting' and 'ValueError' at 30+ FPS. + # """ + # # 1. Handle Batch Dimension: (1, 3, 640, 640) -> (3, 640, 640) + # temp = frame_tensor.squeeze(0) if frame_tensor.ndim == 4 else frame_tensor + + # # 2. Fix Layout & Shape: (3, 640, 640) -> (640, 640, 3) + # # contiguous() physically rearranges pixels in VRAM to interleave colors. + # # reshape() ensures we never see the (1, 409600, 3) shape again. + # gpu_hwc = temp.permute(1, 2, 0).reshape(640, 640, 3).contiguous() + + # # 3. Visibility Fix: Scale floats (0.0-1.0) to bytes (0-255) on GPU + # if gpu_hwc.dtype != torch.uint8: + # gpu_hwc = (gpu_hwc * 255).clamp(0, 255).byte() + + # # 4. Color Space Fix: RGB -> BGR (Matches OpenCV) + # gpu_bgr = gpu_hwc.flip(-1).contiguous() + + # # 5. Bridge to CuPy (Zero-Copy) + # cp_frame = cp.from_dlpack(torch.utils.dlpack.to_dlpack(gpu_bgr)) + + # # 6. Select Double-Buffer from Pool + # target_buf_cpu = self.pinned_pool_np[self.pool_idx] + # target_buf_gpu = self.cp_pool[self.pool_idx] + # self.pool_idx = (self.pool_idx + 1) % 2 + + # if not hasattr(self, "transfer_stream"): + # self.transfer_stream = cp.cuda.Stream(non_blocking=True) + + # with self.transfer_stream: + # # cp.copyto is more robust for pinned memory than target[:] + # cp.copyto(target_buf_gpu, cp_frame) + # # Download into the pinned CPU RAM + # target_buf_gpu.get(out=target_buf_cpu) + + # # 7. Mandatory Sync: Wait for the DMA transfer to hit RAM + # self.transfer_stream.synchronize() + + # # Return a snapshot copy so the worker has private memory for 30 FPS + # return target_buf_cpu.copy() + + # DEBUG FUNCTIONS -------------------------------------------- + def debug_save_mask(self, frame_source, frame_num, rois=None): + debug_dir = self.result_dir / "debug_mask" / self._testMethodName + debug_dir.mkdir(parents=True, exist_ok=True) + + if frame_num > self.config.DEBUG_FRAME_LIMIT: + return + + # Download or copy the data + if hasattr(frame_source, "download"): + img_cpu = frame_source.download() + elif torch.is_tensor(frame_source): + # .contiguous() fixes the horizontal "shredding"/static look + temp = frame_source.squeeze(0) if frame_source.ndim == 4 else frame_source + # img_cpu = temp.permute(1, 2, 0).contiguous().cpu().numpy() + if temp.ndim == 1: + temp = temp.view(self.resize_h, self.resize_w) + + if temp.ndim == 3: + img_cpu = temp.permute(1, 2, 0).contiguous().cpu().numpy() + else: + img_cpu = temp.contiguous().cpu().numpy() + else: + # For numpy arrays (like your pinned memory), ensure memory is linear + img_cpu = np.ascontiguousarray(frame_source) + + # Fix Visibility (Normalization) + # If float, scale to 0-255. If uint8, leave as is to avoid "neon" colors. + if img_cpu.dtype != np.uint8: + if img_cpu.max() <= 1.0: + img_cpu = (img_cpu * 255).clip(0, 255).astype(np.uint8) + else: + img_cpu = img_cpu.astype(np.uint8) + + # Handle Color Space + # OpenCV imwrite expects BGR. If 3-channel (RGB), swap. If 1-channel, save as-is. + if len(img_cpu.shape) == 3: + # img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_RGB2BGR) + pass + else: + img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_GRAY2BGR) + + # Draw 8K Boxes (Scaled down) + if rois is not None: + h_img, w_img = img_cpu.shape[:2] + scale_x = float(w_img) / self.frame_width + scale_y = float(h_img) / self.frame_height + boxes = rois.cpu().tolist() if torch.is_tensor(rois) else rois + for box in boxes: + x1, y1, x2, y2 = [ + int(box[0] * scale_x), + int(box[1] * scale_y), + int(box[2] * scale_x), + int(box[3] * scale_y), + ] + cv2.rectangle(img_cpu, (x1, y1), (x2, y2), (0, 0, 255), 2) + + # Save to disk + save_path = debug_dir / f"mask_{frame_num:04d}.jpg" + cv2.imwrite(str(save_path), img_cpu) + + def debug_save_img_roi(self, frame_source, bbs_full_res, frame_num): + debug_dir = self.result_dir / "debug_analysis" / self._testMethodName + debug_dir.mkdir(parents=True, exist_ok=True) + + if frame_num > self.config.DEBUG_FRAME_LIMIT: + return + + # Download/Copy the frame + if torch.is_tensor(frame_source): + # .contiguous() is CRITICAL here to fix the "shredded" look + temp = frame_source.squeeze(0) if frame_source.ndim == 4 else frame_source + img_cpu = temp.permute(1, 2, 0).contiguous().cpu().numpy() + elif hasattr(frame_source, "download"): + img_cpu = frame_source.download() + else: + img_cpu = np.ascontiguousarray(frame_source) + + # Fix Shape: restore spatial grid if flattened + if img_cpu.ndim == 3 and img_cpu.shape[0] == 1: + img_cpu = img_cpu.reshape((self.resize_h, self.resize_w, 3)) + + # Fix Visibility: ONLY multiply if it's actually floating point + # If uint8 is multiplied by 255, it wraps around and creates "neon" colors + if img_cpu.dtype != np.uint8: + if img_cpu.max() <= 1.0: + img_cpu = (img_cpu * 255).clip(0, 255).astype(np.uint8) + else: + img_cpu = img_cpu.astype(np.uint8) + + # Color Space: Standardize to BGR for imwrite + if len(img_cpu.shape) != 3: + img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_GRAY2BGR) + + # Draw 8K Boxes (Scaled down) + h_img, w_img = img_cpu.shape[:2] + scale_x = w_img / self.frame_width + scale_y = h_img / self.frame_height + + if bbs_full_res is not None: + boxes = ( + bbs_full_res.cpu().tolist() + if torch.is_tensor(bbs_full_res) + else bbs_full_res + ) + for box in boxes: + x1, y1, x2, y2 = [ + int(box[0] * scale_x), + int(box[1] * scale_y), + int(box[2] * scale_x), + int(box[3] * scale_y), + ] + cv2.rectangle(img_cpu, (x1, y1), (x2, y2), (0, 0, 255), 2) + + cv2.imwrite(str(debug_dir / f"analysis_{frame_num:04d}.jpg"), img_cpu) + + def debug_save_crops(self, cropped_batch, frame_num): + """Saves the first 5 crops of a batch to the results directory.""" + debug_dir = self.result_dir / "debug_crops" / self._testMethodName + debug_dir.mkdir(parents=True, exist_ok=True) + + # Only save for the first self.config.DEBUG_FRAME_LIMIT frames to avoid disk bloat + if frame_num > self.config.DEBUG_FRAME_LIMIT: + return + + for i, crop in enumerate(cropped_batch[: self.config.DEBUG_FRAME_LIMIT]): + # Convert GPU Tensor [C, H, W] -> NumPy [H, W, C] + if torch.is_tensor(crop): + # Reverse normalization (* 255) and permute to BGR + img = (crop.squeeze(0).permute(1, 2, 0) * 255).byte().cpu().numpy() + else: + img = crop + + cv2.imwrite(str(debug_dir / f"frame_{frame_num}_crop_{i}.jpg"), img) + + def debug_save_img(self, frame_source, frame_num): + debug_dir = self.result_dir / "debug_test" / self._testMethodName + debug_dir.mkdir(parents=True, exist_ok=True) + + if frame_num > self.config.DEBUG_FRAME_LIMIT: + return + + # Download/Copy the frame + if torch.is_tensor(frame_source): + # .contiguous() is CRITICAL here to fix the "shredded" look + temp = frame_source.squeeze(0) if frame_source.ndim == 4 else frame_source + img_cpu = temp.permute(1, 2, 0).contiguous().cpu().numpy() + elif hasattr(frame_source, "download"): + img_cpu = frame_source.download() + else: + img_cpu = np.ascontiguousarray(frame_source) + + # Fix Shape: restore spatial grid if flattened + if img_cpu.ndim == 3 and img_cpu.shape[0] == 1: + img_cpu = img_cpu.reshape((self.resize_h, self.resize_w, 3)) + + # Fix Visibility: ONLY multiply if it's actually floating point + # If uint8 is multiplied by 255, it wraps around and creates "neon" colors + if img_cpu.dtype != np.uint8: + if img_cpu.max() <= 1.0: + img_cpu = (img_cpu * 255).clip(0, 255).astype(np.uint8) + else: + img_cpu = img_cpu.astype(np.uint8) + + # Color Space: Standardize to BGR for imwrite + if len(img_cpu.shape) == 3: + pass + else: + img_cpu = cv2.cvtColor(img_cpu, cv2.COLOR_GRAY2BGR) + + cv2.imwrite(str(debug_dir / f"analysis_{frame_num:04d}.jpg"), img_cpu) + + # # FRAME PROCESSORS -------------------------------------------- + # def run_pipeline(self): #, pipeline_fn): + # # n_frames = 0 + # num_objs = 0 + # self.frame_count_target = 0 + # self.next_process_idx = 0.0 + # self.frame_in_clip_count = 0 + # total_pipeline_time_ms = 0.0 # Track pure latency + # coverage_percentages = [] + # self.component_stats = {"sf": [], "roi": [], "det": []} + # self.gpu_event_buffer = {"sf": [], "roi": [], "det": []} + # self.crops_per_frame_list = [] + + # # PRE-SYNC: Ensure GPU is idle before timing starts + # # if self.device_input == "cuda": + # # # Pre-allocate CUDA events for isolated GPU timing + # # # start_event = torch.cuda.Event(enable_timing=True) + # # # end_event = torch.cuda.Event(enable_timing=True) + + # # if torch.cuda.is_available(): + # # torch.cuda.synchronize() + + # self.start() + # time.sleep(0.1) + + # # start_time = time.perf_counter() + # total_session_start = time.perf_counter() + + # while self.active: # and n_frames < 10*self.fps: + # # ret, frame = self.cap.read() + # device_frame, frame_num = self.reader.read() + # if device_frame is None: + # if self.reader.stopped: + # self.active = False + # break + # continue + + # # n_frames += 1 + # self.frame_count += 1 + # is_target_frame = float(frame_num) >= self.next_process_idx + # nob = 0 + + # # if self.device_input == "cuda": + # # # start_eve nt.record() + # # nob, metrics = self.pipeline_fn(device_frame, frame_num, is_target_frame) + # # # end_event.record() + # # # torch.cuda.synchronize() + # # # total_pipeline_time_ms += start_event.elapsed_time(end_event) + # # else: + # # start_t = time.perf_counter() + # if self.device_input == "cuda": + # # Record the reader's current layout availability milestone + # curr_event = torch.cuda.Event() + # curr_event.record() + + # # Instruct the isolated inference stream to wait for this specific frame context + # self.inference_stream.wait_event(curr_event) + # with torch.cuda.stream(self.inference_stream): + # nob, metrics = self.pipeline_fn(device_frame, frame_num, is_target_frame) + # # self.inference_stream.synchronize() + + # else: + # nob, metrics = self.pipeline_fn(device_frame, frame_num, is_target_frame) + # # total_pipeline_time_ms += (time.perf_counter() - start_t) * 1000 + + # num_objs += nob + + # if is_target_frame and metrics != {}: + # num_crops = len(metrics["bbs"]) if metrics["bbs"] is not None else 0 + # self.crops_per_frame_list.append(num_crops) + + # # self.component_stats["roi"].append(metrics["roi_time"]) + + # # if self.device_input != "cuda": + # if metrics.get("sf_time"): + # self.component_stats["sf"].append(metrics["sf_time"]) + # if metrics.get("det_time"): + # self.component_stats["det"].append(metrics["det_time"]) + # if metrics.get("roi_time"): + # self.component_stats["roi"].append(metrics["roi_time"]) + + # # Calculate coverage OUTSIDE the timed block to prevent interference + # if self.config.sf_enabled and metrics.get("bbs") is not None: + # cov = self.calculate_unique_coverage(metrics["bbs"]) + # coverage_percentages.append(cov) + + # # POST-SYNC: Ensure all GPU tasks finished before timing ends + # if self.device_input == "cuda": + # torch.cuda.synchronize() + + # # ACTUAL speed including all overhead + # real_world_latency_ms = (time.perf_counter() - total_session_start) * 1000 + + # # if self.device_input == "cuda": + # # # Move GPU event timings into component_stats lists + # # for key in ["sf", "det", "roi"]: + # # for start, end in self.gpu_event_buffer[key]: + # # self.component_stats[key].append(start.elapsed_time(end)) + + # # Calculate Pure processing latency + # # This excludes: Reader, Tensor2OpenCV, Queueing, and Video Writing + # total_pipeline_time_ms = ( + # sum(self.component_stats["sf"]) + # + sum(self.component_stats["roi"]) + # + sum(self.component_stats["det"]) + # ) + + # # assert num_objs > 0 + # if num_objs == 0: + # print(f" [WARNING] No objects detected for {self._testMethodName}") + + # self._finalize_benchmarks( + # self.frame_count, + # num_objs, + # total_pipeline_time_ms, + # real_world_latency_ms, + # coverage_percentages, + # self.config.sf_enabled, + # self.stat_frame_count, + # self.stat_fps, + # ) + + def run_pipeline(self): + num_objs = 0 + self.frame_count_target = 0 + self.next_process_idx = 0.0 + self.frame_in_clip_count = 0 + total_pipeline_time_ms = 0.0 + coverage_percentages = [] + self.component_stats = {"sf": [], "roi": [], "det": []} + self.crops_per_frame_list = [] + + self.step_size = ( + float(self.input_fps) / float(self.target_fps) + if hasattr(self, "target_fps") + else 1.0 + ) + + self.start() + time.sleep(0.1) + + total_session_start = time.perf_counter() + + while self.active: + # 1. CAPTURE COMPLETE CYCLE OVERHEAD + # t_cycle_start = time.perf_counter() + + device_frame, frame_num = self.reader.read() + if device_frame is None: + if self.reader is None or ( + hasattr(self.reader, "stopped") and self.reader.stopped + ): + # --- CRITICAL PIPELINE DRAIN START --- + print( + "[INFO] Reader reached EOF. Draining asynchronous workers and VRAM queues..." + ) + + # 1. Thread Pool Flush: Force the CPU thread to block here until + # every single background pipeline task in the executor finishes. + if hasattr(self, "executor") and self.executor: + self.executor.shutdown(wait=True) + + # 2. Render Queue Flush: If you utilize an asynchronous video frame + # saving worker thread, wait for its queue tasks to bottom out. + if hasattr(self, "render_queue") and self.render_queue: + while not self.render_queue.empty(): + time.sleep(0.01) + + # 3. GPU Hardware Flush: Force the GPU to completely finish + # all remaining background subtraction, crops, and YOLO operations. + if self.device_input == "cuda": + torch.cuda.synchronize() + + self.active = False + break + continue + + self.stat_start_time = time.perf_counter() # timing to display detection + self.frame_count += 1 + # is_target_frame = float(frame_num) >= self.next_process_idx + # CRITICAL CADENCE DRIFT FIX: If input FPS equals target FPS, or if Smart Filtering + # is disabled (YOLO baseline), force is_target_frame to ALWAYS be True. + # This completely bypasses floating-point accumulation drift errors. + if ( + not self.config.sf_enabled + or abs(float(self.input_fps) - float(self.target_fps)) < 0.01 + ): + is_target_frame = True + else: + is_target_frame = float(frame_num) >= self.next_process_idx + + # 2. DISPATCH WORKLOADS ASYNCHRONOUSLY + metrics = {} + if is_target_frame: + self.next_process_idx += self.step_size + + if self.device_input == "cuda": + # Instantiate a lightweight hardware fence event object + curr_event = torch.cuda.Event(enable_timing=False) + # curr_event.record() + # Record the exact milestone on the default stream right after fetching the frame data + curr_event.record(torch.cuda.default_stream()) + self.inference_stream.wait_event(curr_event) + with torch.cuda.stream(self.inference_stream): + # 1. Isolate the incoming image buffer canvas + isolated_device_frame = ( + device_frame.clone() + if torch.is_tensor(device_frame) + else device_frame.copy() + ) + + # 2. RUN BACKGROUND SUBTRACTION IMMEDIATELY ON THE PRODUCER TIMELINE + # This guarantees that the mask matches this exact frame_num before threads overlap! + nob, metrics = self.pipeline_fn( + isolated_device_frame, + frame_num, + is_target_frame, + self.stat_start_time, + ) + else: + # 1. Isolate the incoming image buffer canvas + isolated_device_frame = ( + device_frame.clone() + if torch.is_tensor(device_frame) + else device_frame.copy() + ) + + # 2. RUN BACKGROUND SUBTRACTION IMMEDIATELY ON THE PRODUCER TIMELINE + # This guarantees that the mask matches this exact frame_num b + nob, metrics = self.pipeline_fn( + isolated_device_frame, + frame_num, + is_target_frame, + self.stat_start_time, + ) + + num_objs += nob + # else: + # # Process background execution context for skipped frames + # nob, metrics = self.pipeline_fn( + # device_frame, frame_num, is_target_frame, self.stat_start_time + # ) + + # 3. ENFORCE UNIFIED HARDWARE TIMING BARRIER + # if self.device_input == "cuda": + # torch.cuda.synchronize() + + # t_cycle_end = time.perf_counter() + # cycle_total_ms = (t_cycle_end - t_cycle_start) * 1000.0 + + # 4. ALLOCATE ALL TRACKING TIMINGS ACCURATELY + # if is_target_frame: + if metrics != {}: + num_crops = len(metrics["bbs"]) if metrics["bbs"] is not None else 0 + self.crops_per_frame_list.append(num_crops) + + if self.config.sf_enabled: + self.component_stats["sf"].append(metrics["sf_time"]) + self.component_stats["roi"].append(metrics["roi_time"]) + else: + # Full-frame YOLO baseline accounts for total data movement cycle + # self.component_stats["det"].append(cycle_total_ms) + self.component_stats["sf"].append(0.0) + self.component_stats["roi"].append(0.0) + + self.component_stats["det"].append(metrics["det_time"]) + + if self.config.sf_enabled and metrics.get("bbs") is not None: + cov = self.calculate_unique_coverage(metrics["bbs"]) + coverage_percentages.append(cov) + # else: + # # CRITICAL METRICS FIX: If Smart Filtering runs on a skipped frame, + # # its mask generation overhead MUST be captured and tracked! + # if self.config.sf_enabled and metrics != {}: + # self.component_stats["sf"].append(metrics["sf_time"]) + + if self.device_input == "cuda": + torch.cuda.synchronize() + + real_world_latency_ms = (time.perf_counter() - total_session_start) * 1000.0 + + total_pipeline_time_ms = ( + sum(self.component_stats["sf"]) + + sum(self.component_stats["roi"]) + + sum(self.component_stats["det"]) + ) + + if num_objs == 0: + print(f" [WARNING] No objects detected for {self._testMethodName}") + + self._finalize_benchmarks( + # self.frame_count, + num_objs, + total_pipeline_time_ms, + real_world_latency_ms, + coverage_percentages, + self.config.sf_enabled, + self.stat_frame_count, + self.stat_fps, + ) + + # # TESTS -------------------------------------------- + # def pipeline_fn(self, device_frame, overall_frame_num, is_target_frame): + # num_objs = 0 + # metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # # Initialize timing event handle pairs + # sf_start, sf_end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True) + # roi_start, roi_end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True) + # det_start, det_end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True) + + # # --- 1. MOTION MASK GENERATION GATE --- + # if self.config.sf_enabled: + # if self.device_input == "cuda": + # sf_start.record(self.inference_stream) + # inf_data = self.rbtd_full_gpu(device_frame) + # sf_end.record(self.inference_stream) + # else: + # t_start = time.perf_counter() + # inf_data = self.rbtd_full_cpu(device_frame) + # metrics["sf_time"] = (time.perf_counter() - t_start) * 1000.0 + # else: + # inf_data = {} + + # # --- PIPELINE AT TARGET RATE --- + # if not is_target_frame: + # # If skipping, pull outstanding execution records immediately + # if self.device_input == "cuda" and self.config.sf_enabled: + # self.inference_stream.synchronize() + # metrics["sf_time"] = sf_start.elapsed_time(sf_end) + # return num_objs, metrics + + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 + # self.frame_in_clip_count += 1 + # inf_data["frameNum"] = self.frame_count_target + + # # --- 2. FULL-RESOLUTION ROI EXTRACTION MAPS --- + # bbs_full_res = None + # if self.config.sf_enabled: + # if self.device_input == "cuda": + # roi_start.record(self.inference_stream) + # bbs_full_res = self.get_gpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + # roi_end.record(self.inference_stream) + # metrics["bbs"] = bbs_full_res + # else: + # t_start = time.perf_counter() + # bbs_full_res = self.get_cpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + # metrics["roi_time"] = (time.perf_counter() - t_start) * 1000.0 + # metrics["bbs"] = bbs_full_res + + # if self.config.DEBUG_FLAG: + # if self.device_input == "cuda": + # torch.cuda.stream(self.inference_stream) + # torch.cuda.synchronize() + # display_source = inf_data["mask"] + # self.debug_save_mask( + # display_source, self.frame_count_target, rois=bbs_full_res + # ) + + # clean_bbs = [] + # if self.config.sf_enabled and bbs_full_res is not None: + # if torch.is_tensor(bbs_full_res): + # clean_bbs = bbs_full_res.detach().cpu().numpy() + # else: + # clean_bbs = np.array(bbs_full_res) + + # # --- 3. MODEL INFERENCE TIMING BLOCK --- + # if self.device_input == "cuda": + # det_start.record(self.inference_stream) + # else: + # t_start = time.perf_counter() + + # if self.config.DETECTION_TYPE != "motion": + # det_frame = inf_data["full_frame"] if "full_frame" in inf_data else device_frame + # merged = clean_bbs if self.config.sf_enabled else None + # metadata, _ = self.get_detections( + # det_frame, + # self.frame_in_clip_count, + # merged=merged, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # num_objs = len(metadata.keys()) + # else: + # num_objs = len(clean_bbs) + # metadata = clean_bbs + + # if self.device_input == "cuda": + # det_end.record(self.inference_stream) + + # # CRITICAL PERFORMANCE SYNCHRONIZATION POINT: + # # We execute a single stream synchronization barrier here at the end of the entire loop. + # # This allows the GPU kernels to run overlapped and fully concurrently! + # self.inference_stream.synchronize() + + # # Unpack hardware timings efficiently + # if self.config.sf_enabled: + # metrics["sf_time"] = sf_start.elapsed_time(sf_end) + # metrics["roi_time"] = roi_start.elapsed_time(roi_end) + # metrics["det_time"] = det_start.elapsed_time(det_end) + # metrics["bbs"] = bbs_full_res + # else: + # metrics["det_time"] = (time.perf_counter() - t_start) * 1000.0 + + # # --- 4. RENDER WORKER PREPARATION --- + # display_source = inf_data["full_frame"] if (inf_data and "full_frame" in inf_data) else device_frame + + # if self.device_input == "cuda": + # gpu_resized = F.interpolate( + # display_source.unsqueeze(0).float(), + # size=(self.disp_h, self.disp_w), + # mode="bilinear", + # align_corners=False, + # ).squeeze(0).contiguous() + # disp_frame = np.copy(tensor2opencv(gpu_resized, self.config.device_input, is_bgr=True)) + # else: + # cpu_resized = cv2.resize(device_frame, (self.disp_w, self.disp_h)) + # disp_frame = np.copy(tensor2opencv(cpu_resized, self.config.device_input, is_bgr=True)) + + # data_to_draw = clean_bbs if self.config.DETECTION_TYPE == "motion" else metadata + + # try: + # if hasattr(self, "render_queue") and getattr(self, "render_queue", None) is not None and not self.render_queue.full(): + # self.render_queue.put( + # ( + # disp_frame, + # inf_data["frameNum"] if "frameNum" in inf_data else self.frame_count_target, + # data_to_draw, + # self.label_source, + # ) + # ) + # if self.config.DEBUG_FLAG: + # self.debug_save_img(disp_frame, self.frame_count_target) + # self.debug_save_img_roi(disp_frame, bbs_full_res, self.frame_count_target) + # except queue.Full: + # pass + + # self.update_frame() + # return num_objs, metrics + + def pipeline_fn( + self, device_frame, overall_frame_num, is_target_frame, stat_start_time + ): + num_objs = 0 + metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # Pre-allocate non-blocking event records to extract clean GPU timings + sf_start, sf_end = ( + torch.cuda.Event(enable_timing=True), + torch.cuda.Event(enable_timing=True), + ) + roi_start, roi_end = ( + torch.cuda.Event(enable_timing=True), + torch.cuda.Event(enable_timing=True), + ) + det_start, det_end = ( + torch.cuda.Event(enable_timing=True), + torch.cuda.Event(enable_timing=True), + ) + + # --- 1. MOTION MASK GENERATION GATE --- + if self.config.sf_enabled: + if self.device_input == "cuda": + sf_start.record(self.inference_stream) + inf_data = self.rbtd_full_gpu(device_frame) + sf_end.record(self.inference_stream) + else: + t_start = time.perf_counter() + inf_data = self.rbtd_full_cpu(device_frame) + metrics["sf_time"] = (time.perf_counter() - t_start) * 1000.0 + else: + inf_data = {} + + # --- PIPELINE AT TARGET RATE --- + if not is_target_frame: + # Safe, non-blocking timing extraction for skipped frames + if self.device_input == "cuda" and self.config.sf_enabled: + torch.cuda.synchronize() + metrics["sf_time"] = sf_start.elapsed_time(sf_end) + return num_objs, metrics + + self.frame_count_target += 1 + self.frame_in_clip_count += 1 + inf_data["frameNum"] = self.frame_count_target + + # --- 2. FULL-RESOLUTION ROI EXTRACTION MAPS --- + bbs_full_res = None + if self.config.sf_enabled: + if self.device_input == "cuda": + roi_start.record(self.inference_stream) + bbs_full_res = self.get_gpu_rois( + inf_data["full_frame"], + self.frame_count_target, + inf_data["mask"], + ) + roi_end.record(self.inference_stream) + metrics["bbs"] = bbs_full_res + else: + t_start = time.perf_counter() + bbs_full_res = self.get_cpu_rois( + inf_data["full_frame"], + self.frame_count_target, + inf_data["mask"], + ) + metrics["roi_time"] = (time.perf_counter() - t_start) * 1000.0 + metrics["bbs"] = bbs_full_res + + if self.config.DEBUG_FLAG: + # if self.device_input == "cuda": + # # Isolate data capturing using an asynchronous memory clone operation + # # This safely copies data without dropping your multi-stream execution pipeline concurrency + # display_source = inf_data["mask"].clone().to("cpu", non_blocking=True) + # else: + display_source = inf_data["mask"] + # self.inference_stream.synchronize() + # display_source = inf_data["mask"] + self.debug_save_mask( + display_source, self.frame_count_target, rois=bbs_full_res + ) + + clean_bbs = [] + if self.config.sf_enabled and bbs_full_res is not None: + if torch.is_tensor(bbs_full_res): + clean_bbs = bbs_full_res.detach().cpu().numpy() + else: + clean_bbs = np.array(bbs_full_res) + + # --- 3. MODEL INFERENCE TIMING BLOCK --- + if self.device_input == "cuda": + det_start.record(self.inference_stream) + else: + t_start = time.perf_counter() + + if self.config.DETECTION_TYPE != "motion": + # det_frame = ( + # inf_data["full_frame"] if "full_frame" in inf_data else device_frame + # ) + # Isolate your image buffer array view to prevent upstream reader pointer races + if "full_frame" in inf_data: + det_frame = inf_data["full_frame"] + else: + det_frame = ( + device_frame.clone() + if torch.is_tensor(device_frame) + else device_frame.copy() + ) + + merged = clean_bbs if self.config.sf_enabled else None + metadata, _ = self.get_detections( + det_frame, + self.frame_in_clip_count, + merged=merged, + thickness=self.config.THICKNESS, + device_input=self.config.device_input, + ) + num_objs = len(metadata.keys()) + else: + num_objs = len(clean_bbs) + metadata = clean_bbs + + if self.device_input == "cuda": + det_end.record(self.inference_stream) + + torch.cuda.synchronize() + + # Extract hardware timings smoothly without stalling mid-run + if self.config.sf_enabled: + metrics["sf_time"] = sf_start.elapsed_time(sf_end) + metrics["roi_time"] = roi_start.elapsed_time(roi_end) + metrics["det_time"] = det_start.elapsed_time(det_end) + metrics["bbs"] = bbs_full_res + else: + metrics["det_time"] = (time.perf_counter() - t_start) * 1000.0 + + # --- 4. RENDER WORKER PREPARATION --- + display_source = ( + inf_data["full_frame"] + if (inf_data and "full_frame" in inf_data) + else device_frame + ) + + if self.device_input == "cuda": + gpu_resized = ( + F.interpolate( + display_source.unsqueeze(0).float(), + size=(self.disp_h, self.disp_w), + mode="bilinear", + align_corners=False, + ) + .squeeze(0) + .contiguous() + ) + disp_frame = np.copy( + tensor2opencv(gpu_resized, self.config.device_input, is_bgr=True) + ) + else: + cpu_resized = cv2.resize(device_frame, (self.disp_w, self.disp_h)) + disp_frame = np.copy( + tensor2opencv(cpu_resized, self.config.device_input, is_bgr=True) + ) + + data_to_draw = clean_bbs if self.config.DETECTION_TYPE == "motion" else metadata + + # try: + # if ( + # hasattr(self, "render_queue") + # and getattr(self, "render_queue", None) is not None + # and not self.render_queue.full() + # ): + # self.render_queue.put( + # ( + # disp_frame, + # inf_data["frameNum"] + # if "frameNum" in inf_data + # else self.frame_count_target, + # data_to_draw, + # self.label_source, + # ) + # ) + # if self.config.DEBUG_FLAG: + # self.debug_save_img(disp_frame, self.frame_count_target) + # self.debug_save_img_roi( + # disp_frame, bbs_full_res, self.frame_count_target + # ) + # except queue.Full: + # pass + + if ( + hasattr(self, "render_queue") + and getattr(self, "render_queue", None) is not None + ): + self.render_queue.put( + ( + disp_frame, + inf_data["frameNum"] + if "frameNum" in inf_data + else self.frame_count_target, + data_to_draw, + self.label_source, + ) + ) + if self.config.DEBUG_FLAG: + self.debug_save_img(disp_frame, self.frame_count_target) + self.debug_save_img_roi(disp_frame, bbs_full_res, self.frame_count_target) + + self.update_frame(stat_start_time) + return num_objs, metrics + + # def sf_cpu_pipeline_fn(self, device_frame, overall_frame_num, is_target_frame): + # num_objs = 0 + # metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # # Smart Filtering (Resize / Background Subtraction / Threshold / Dilate) + # t_start = time.perf_counter() + # inf_data = self.rbtd_full_cpu(device_frame) + # metrics["sf_time"] = (time.perf_counter() - t_start) * 1000 + + # # Only keep frames for TARGET_FPS + # # Videos are written for target frames only + # if is_target_frame: + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 # 1-indexed + + # if not inf_data or inf_data["mask"] is None: + # return num_objs, metrics + + # metadata = {} + # bbs_to_send = [] + # data_to_draw = [] + # if inf_data: + # inf_data["frameNum"] = self.frame_count_target + + # # Get ROIs + # t_start = time.perf_counter() + # bbs_full_res = self.get_cpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + # metrics["roi_time"] = (time.perf_counter() - t_start) * 1000 + # metrics["bbs"] = bbs_full_res + + # if self.config.DEBUG_FLAG: + # display_source = inf_data["mask"] + # self.debug_save_mask( + # display_source, self.frame_count_target, rois=bbs_full_res + # ) + + # if bbs_full_res is not None and len(bbs_full_res) == 0: + # return num_objs, metrics + + # # if self.config.DEBUG_FLAG or self.config.DETECTION_TYPE == "motion": + # # display_source = ( + # # inf_data["full_frame"] + # # if (inf_data and "full_frame" in inf_data) + # # else device_frame + # # ) + # # cpu_resized = cv2.resize( + # # display_source, + # # (self.resize_w, self.resize_h), + # # interpolation=cv2.INTER_NEAREST, + # # ) + + # t_start = time.perf_counter() + # if self.config.DETECTION_TYPE == "motion": + # # Motion Mode: Prepare boxes for drawing + # if ( + # bbs_full_res is not None + # and bbs_full_res.ndim == 2 + # and bbs_full_res.size(0) > 0 + # ): + # scaled_resized_bbs = bbs_full_res / self.scales_tensor + # bbs_to_send = scaled_resized_bbs.cpu().tolist() + # else: + # bbs_to_send = [] + + # data_to_draw = bbs_to_send + # num_objs = len(bbs_full_res) + # else: + # # Object Mode: Run YOLO and prepare metadata + # det_frame = inf_data["full_frame"] if inf_data else device_frame + # metadata, _ = self.get_detections( + # det_frame, + # self.frame_count_target, # Frame used in metadata + # merged=bbs_full_res, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # data_to_draw = metadata + # num_objs = len(metadata.keys()) + + # metrics["det_time"] = (time.perf_counter() - t_start) * 1000 + + # # --- OFFLOAD TO QUEUE (Run for EVERY target frame) --- + # if not self.config.TEST_MODE: + # display_source = ( + # inf_data["full_frame"] + # if (inf_data and "full_frame" in inf_data) + # else device_frame + # ) + # cpu_resized = cv2.resize(display_source, (self.resize_w, self.resize_h)) + # display_frame = tensor2opencv( + # cpu_resized, self.config.device_input, is_bgr=True + # ) + # self.render_queue.put((display_frame, data_to_draw, self.label_source)) + + # if self.config.DEBUG_FLAG: + # cpu_resized = cv2.resize( + # display_source, (self.resize_w, self.resize_h) + # ) + # self.debug_save_img(cpu_resized, self.frame_count_target) + # self.debug_save_img_roi( + # cpu_resized, bbs_full_res, self.frame_count_target + # ) + + # return num_objs, metrics + + # def sf_gpu_pipeline_fn(self, device_frame, overall_frame_num, is_target_frame): + # num_objs = 0 + # metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # # Smart Filtering (Resize / Background Subtraction / Threshold / Dilate) + # sf_start, sf_end = ( + # torch.cuda.Event(enable_timing=True), + # torch.cuda.Event(enable_timing=True), + # ) + # sf_start.record() + # inf_data = self.rbtd_full_gpu(device_frame) + # sf_end.record() + # self.gpu_event_buffer["sf"].append((sf_start, sf_end)) + + # # Only keep frames for TARGET_FPS + # # Videos are written for target frames only + # if is_target_frame: + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 # 1-indexed + + # if not inf_data or inf_data["mask"] is None: + # return num_objs, metrics + + # metadata = {} + # bbs_to_send = [] + # data_to_draw = [] + # if inf_data: + # inf_data["frameNum"] = self.frame_count_target + + # # Get ROIs + # # time.perf_counter accurate since called self.bgs_stream.waitForCompletion() + # # t_start = time.perf_counter() + # roi_start = torch.cuda.Event(enable_timing=True) + # roi_end = torch.cuda.Event(enable_timing=True) + # roi_start.record() + # bbs_full_res = self.get_gpu_rois( + # inf_data["full_frame"], + # self.frame_count_target, + # inf_data["mask"], + # ) + # # metrics["roi_time"] = (time.perf_counter() - t_start) * 1000 + # # self.gpu_event_buffer["roi"].append((roi_start, roi_end)) + # roi_end.record() + # self.gpu_event_buffer["roi"].append((roi_start, roi_end)) + # metrics["bbs"] = bbs_full_res + + # if self.config.DEBUG_FLAG: + # self.bgs_stream.waitForCompletion() + # display_source = inf_data["mask"] + # self.debug_save_mask( + # display_source, self.frame_count_target, rois=bbs_full_res + # ) + + # if bbs_full_res is not None and len(bbs_full_res) == 0: + # return num_objs, metrics + + # det_start, det_end = ( + # torch.cuda.Event(enable_timing=True), + # torch.cuda.Event(enable_timing=True), + # ) + # det_start.record() + # if self.config.DETECTION_TYPE == "motion": + # # Motion Mode: Prepare boxes for drawing + # if ( + # bbs_full_res is not None + # and bbs_full_res.ndim == 2 + # and bbs_full_res.size(0) > 0 + # ): + # scaled_resized_bbs = bbs_full_res / self.scales_tensor + # bbs_to_send = scaled_resized_bbs.detach().cpu().tolist() + # else: + # bbs_to_send = [] + # data_to_draw = bbs_to_send + # num_objs = len(bbs_to_send) + # else: + # # Object Mode: Run YOLO and prepare metadata + # det_frame = ( + # inf_data["full_frame"] if inf_data else device_frame + # ) # RGB + # metadata, _ = self.get_detections( + # det_frame, + # self.frame_count_target, + # merged=bbs_full_res, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # data_to_draw = metadata + # num_objs = len(metadata.keys()) + + # det_end.record() + # self.gpu_event_buffer["det"].append((det_start, det_end)) + + # # --- OFFLOAD TO QUEUE (Run for EVERY target frame) --- + # if not self.config.TEST_MODE: + # display_source = ( + # inf_data["full_frame"] + # if (inf_data and "full_frame" in inf_data) + # else device_frame + # ) # RGB + # gpu_resized = F.interpolate( + # display_source.unsqueeze(0).half(), + # size=(self.resize_h, self.resize_w), + # mode="bilinear", + # align_corners=False, + # ).squeeze(0) + # display_frame = tensor2opencv( + # gpu_resized, self.config.device_input, is_bgr=True + # ) + # # display_frame = self.tensor2opencv_gpu(gpu_resized) + # self.render_queue.put((display_frame, data_to_draw, self.label_source)) + + # if self.config.DEBUG_FLAG: + # gpu_resized = F.interpolate( + # display_source.unsqueeze(0).float(), + # size=(self.resize_h, self.resize_w), + # mode="bilinear", + # align_corners=False, + # ) # RGB + # self.debug_save_img(gpu_resized, self.frame_count_target) + # self.debug_save_img_roi( + # gpu_resized, bbs_full_res, self.frame_count_target + # ) + + # # Async metrics stay at 0 for fairness; collected at the end of the video + # metrics["sf_time"] = 0 + # metrics["roi_time"] = 0 + # metrics["det_time"] = 0 + # return num_objs, metrics + + # def yolo_cpu_pipeline_fn(self, device_frame, overall_frame_num, is_target_frame): + # num_objs = 0 + # metadata = {} + # metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # # Only keep frames for TARGET_FPS + # if is_target_frame: + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 # 1-indexed + + # # Get detection at original resolution (No SF) + # t_start = time.perf_counter() + # metadata, _ = self.get_detections( + # device_frame, + # self.frame_count_target, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # num_objs = len(metadata.keys()) + # metrics["det_time"] = (time.perf_counter() - t_start) * 1000 + + # if not self.config.TEST_MODE: + # cpu_resized = cv2.resize(device_frame, (self.resize_w, self.resize_h)) + # display_frame = tensor2opencv( + # cpu_resized, self.config.device_input, is_bgr=True + # ) + # self.render_queue.put((display_frame, metadata, self.label_source)) + + # return num_objs, metrics + + # def yolo_gpu_pipeline_fn(self, frame_raw, overall_frame_num, is_target_frame): + # num_objs = 0 + # metadata = {} + # metrics = {"sf_time": 0, "roi_time": 0, "det_time": 0, "bbs": None} + + # # Only keep frames for TARGET_FPS + # if is_target_frame: + # self.next_process_idx += self.step_size + # self.frame_count_target += 1 # 1-indexed + + # # Get detection at original resolution (No SF) + # det_start, det_end = ( + # torch.cuda.Event(enable_timing=True), + # torch.cuda.Event(enable_timing=True), + # ) + # det_start.record() + # metadata, _ = self.get_detections( + # frame_raw, + # self.frame_count_target, + # thickness=self.config.THICKNESS, + # device_input=self.config.device_input, + # ) + # num_objs = len(metadata.keys()) + # det_end.record() + # self.gpu_event_buffer["det"].append((det_start, det_end)) + + # # --- OFFLOAD TO QUEUE (Run for EVERY target frame) --- + # if not self.config.TEST_MODE: + # gpu_resized = F.interpolate( + # frame_raw.unsqueeze(0).half(), + # size=(self.resize_h, self.resize_w), + # mode="bilinear", + # align_corners=False, + # ).squeeze(0) + # display_frame = tensor2opencv( + # gpu_resized, self.config.device_input, is_bgr=True + # ) + # # display_frame = self.tensor2opencv_gpu(gpu_resized) + # self.render_queue.put((display_frame, metadata, self.label_source)) + + # # Async metrics stay at 0 for fairness; collected at the end of the video + # metrics["sf_time"] = 0 + # metrics["det_time"] = 0 + # return num_objs, metrics + + +# INHERIT METHODS FROM HANDLERS ----------------------------------------------------------------- +# TestSmartFilteringDetections.setup_reader = DeviceBaseHandler.setup_reader +# TestSmartFilteringDetections.get_frameWH = DeviceBaseHandler.get_frameWH +# TestSmartFilteringDetections.initialize_variables = ( +# DeviceBaseHandler.initialize_variables +# ) +# TestSmartFilteringDetections.filter_contained_boxes = ( +# DeviceBaseHandler.filter_contained_boxes +# ) +# TestSmartFilteringDetections.get_detections = DeviceBaseHandler.get_detections +# TestSmartFilteringDetections.prepare_pipeline = DeviceBaseHandler.prepare_pipeline +# TestSmartFilteringDetections.get_gpu_rois_by_area = ( +# DeviceBaseHandler.get_gpu_rois_by_area +# ) +# TestSmartFilteringDetections.get_gpu_rois = DeviceBaseHandler.get_gpu_rois +# TestSmartFilteringDetections.get_cpu_rois = DeviceBaseHandler.get_cpu_rois +# TestSmartFilteringDetections.run_model = DeviceBaseHandler.run_model +# TestSmartFilteringDetections.model_warmup = DeviceBaseHandler.model_warmup + +# # TestSmartFilteringDetections.cleanup_gpu = GPUStreamHandler.cleanup_gpu +# # TestSmartFilteringDetections.rbtd_full_gpu = GPUStreamHandler.rbtd_full_gpu +# # TestSmartFilteringDetections.prepare_gpu_pipeline = ( +# # GPUStreamHandler.prepare_gpu_pipeline +# # ) +# # TestSmartFilteringDetections.allocate_gpu = GPUStreamHandler.allocate_gpu +# TestSmartFilteringDetections.gpu_warmup = GPUStreamHandler.gpu_warmup +# TestSmartFilteringDetections.apply_background_subtraction_gpu = ( +# GPUStreamHandler.apply_background_subtraction_gpu +# ) + +# TestSmartFilteringDetections.cleanup_cpu = CPUStreamHandler.cleanup_cpu +# # TestSmartFilteringDetections.rbtd_full_cpu = CPUStreamHandler.rbtd_full_cpu +# # TestSmartFilteringDetections.prepare_cpu_pipeline = ( +# # CPUStreamHandler.prepare_cpu_pipeline +# # ) +# # TestSmartFilteringDetections.allocate_cpu = CPUStreamHandler.allocate_cpu +# TestSmartFilteringDetections.apply_background_subtraction_cpu = ( +# CPUStreamHandler.apply_background_subtraction_cpu +# ) + + +def get_pytest_filter_expression(args): + print("\n" + "=" * 50) + print("TARGET SELECTION PREVIEW") + print("=" * 50) + + filter_expression = "test_detections" + # applied_subs = [] + + if args.sf_enabled is not None: + # Target exact parameter tokens generated by pytest parametrization + sf_str = "-True-" if args.sf_enabled else "-False-" + filter_expression += f" and {sf_str}" + # applied_subs.append(f"sf_enabled={args.sf_enabled}") + + if args.detection_type: + filter_expression += f" and {args.detection_type}" + + # Target hardware context selection filter applies globally across all test cases + if args.device.lower() != "all": + print(f" 💻 Hardware Context Constraint: {args.device.upper()}") + filter_expression = f"({filter_expression}) and {args.device.lower()}" + else: + print(" 💻 Hardware Context Constraint: ALL AVAILABLE") + + print("=" * 50) + print(f"COMPILED PYTEST KEYWORD EXPRESSION:\n 👉 {filter_expression}") + print("=" * 50 + "\n") + + return filter_expression + + +if __name__ == "__main__": + # TEST ARGUMENTS + parser = argparse.ArgumentParser(description="Run Video Detection Pipeline Tests") + parser.add_argument( + "-s", + "--source", + type=str, + default="anduril_swarm_8K.mp4", + help="Video filename (located in /inputs)", + ) + + # MODEL TO USE + parser.add_argument( + "--no-custom", + action="store_false", + dest="custom_model_flag", + help="Enable if using Ultralytics YOLO model", + ) + parser.add_argument( + "-m", + "--model", + type=str, + default="drone_detection", + dest="model_name", + help="Name of model. Required if `--no-custom` is enabled. [Default: drone_detection]", + ) + + # Filter tests + parser.add_argument( + "--type", + type=str, + choices=["object", "motion"], + dest="detection_type", + help="Filter by detection type (object or motion)", + ) + parser.add_argument( + "--device", + type=str, + default="all", + choices=["cpu", "gpu", "all"], + help="Filter by device (cpu or gpu)", + ) + parser.add_argument( + "--sf", + action="store_true", + default=None, + dest="sf_enabled", + help="Filter by Smart Filtering", + ) + + # DEBUGGING + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug message and save intermediate images for Smart Filtering tests", + ) + parser.add_argument( + "-n", + type=int, + default=100, + dest="debug_frame_limit", + help="Number of frames used for debugging [Default: 100]", + ) + + args = parser.parse_args() + + # UPDATE ENVIRONMENTAL VARIABLES + os.environ["VIDEO_FILENAME"] = args.source + os.environ["CUSTOM_MODEL_FLAG"] = "True" if args.custom_model_flag else "False" + os.environ["MODEL_NAME"] = args.model_name + os.environ["DEBUG"] = "1" if args.debug else "0" + os.environ["DEBUG_FRAME_LIMIT"] = str(args.debug_frame_limit) + + # detection_type, device, sf_enabled + filter_expression = get_pytest_filter_expression(args) + + # PYTEST COMMAND + pytest_args = [ + "-k", + filter_expression, + "-s", + "-v", + # "--log-cli-level=DEBUG", + __file__, + ] + + print(f"Launching tests for {args.source}") + + sys.exit(pytest.main(pytest_args)) diff --git a/fastapi/tests/test_model.py b/fastapi/tests/test_model.py new file mode 100644 index 0000000..c2710fd --- /dev/null +++ b/fastapi/tests/test_model.py @@ -0,0 +1,441 @@ +import argparse +import csv +import gc +import logging +import os +import sys +import time +from pathlib import Path + +import pytest +import tensorrt as trt +import torch + +sys.path.insert(1, str(Path(__file__).parent.parent)) +from include.default_configs import ( + CUSTOM_MODEL_FLAG_DEFAULT, + DEBUG_DEFAULT, + MODEL_NAME_DEFAULT, + THRESHOLD_VALUE, +) +from include.handlers import DeviceBaseHandler +from include.models import get_model +from include.utils import PipelineConfig + +try: + torch.multiprocessing.set_start_method("spawn", force=True) +except RuntimeError: + pass + +logging.getLogger("matplotlib").setLevel(logging.WARNING) +logger = trt.Logger(trt.Logger.WARNING) +trt.init_libnvinfer_plugins(logger, "") + + +DEBUG_FLAG_DEFAULT = True if DEBUG_DEFAULT == "1" else False +os.environ["OMP_NUM_THREADS"] = "1" + +force_export = False + + +@pytest.fixture(scope="class") +def setup_context(request): + """Replaces setUpClass: Runs once per test class.""" + # Initialize shared paths/results + current_test_filename = Path(__file__).stem + test_dir = Path(__file__).parent + main_path = test_dir.parent + video_dir = main_path / "inputs" + + VIDEO_FILENAME = os.getenv("VIDEO_FILENAME", "anduril_swarm_8K.mp4") + if video_dir.exists(): + request.cls.video_path = video_dir / VIDEO_FILENAME + else: + video_dir = Path("/watch_dir") + request.cls.video_path = video_dir / VIDEO_FILENAME + + # Add any shared state to the class + model_name = os.getenv("MODEL_NAME", MODEL_NAME_DEFAULT) + request.cls.result_dir = ( + test_dir / f"{current_test_filename}_results/{request.cls.video_path.stem}" + ) + request.cls.result_dir.mkdir(parents=True, exist_ok=True) + + # Benchmark statistics + request.cls.benchmarks = [] + request.cls.csv_filename = ( + f"model_benchmarks_{model_name}_{request.cls.video_path.stem}.csv" + ) + request.cls.csv_path = request.cls.result_dir / request.cls.csv_filename + + # Initialize class vars + request.cls.name = request.cls.video_path.stem + request.cls.source = str(request.cls.video_path) + request.cls.is_rtsp = str(request.cls.source).startswith("rtsp:/") + request.cls.active = True + request.cls.active_streams = {} + request.cls._shared_model = None + request.cls._shared_model_path = None + request.cls._shared_model_device = None + request.cls._shared_model_sf_enabled = None + + # RUN ALL PARAMETERIZED TESTS ---------------------------------------- + yield + + # FINAL CSV EXPORT -------------------------------------------------- + if request.cls.benchmarks: + results = request.cls.benchmarks + keys = results[0].keys() + + with open(str(request.cls.csv_path), "w", newline="") as f: + dict_writer = csv.DictWriter(f, fieldnames=keys) + dict_writer.writeheader() + dict_writer.writerows(results) + + print(f"\n[FINAL] Benchmarks saved to {request.cls.csv_filename}", flush=True) + + +@pytest.fixture(autouse=True) +def each_test_setup(request): + test_class_self = request.instance + + # setUp LOGIC -------------------------------------------------- + if torch.cuda.is_available(): + torch.cuda.synchronize() + + device = request.node.callspec.params.get("device") + os.environ["DEVICE"] = device + detection_type = "object" # request.node.callspec.params.get("detection_type") + sf_enabled = False # request.node.callspec.params.get("sf_enabled") + model_name = os.getenv("MODEL_NAME", MODEL_NAME_DEFAULT) + test_class_self._testMethodName = f"{model_name}_{detection_type}_{device}" + + render_dir = test_class_self.result_dir / f"{test_class_self._testMethodName}" + render_dir.mkdir(exist_ok=True) + test_class_self.config = PipelineConfig( + # GENERAL + SHARED_OUTPUT=render_dir, # os.getenv("SHARED_OUTPUT",SHARED_OUTPUT_DEFAULT), + CUSTOM_MODEL_FLAG=os.getenv( + "CUSTOM_MODEL_FLAG", CUSTOM_MODEL_FLAG_DEFAULT + ), # True, + DEVICE=device.upper(), + OMIT_DETECTIONS_FLAG=True, + TEST_MODE=False, + DEBUG=os.getenv("DEBUG", DEBUG_DEFAULT), + DEBUG_FRAME_LIMIT=os.getenv("DEBUG_FRAME_LIMIT", 100), + # VIDEO WRITER + # CLIP_DURATION=None, + # VDMS + ENABLE_QUERYING=False, + DBHOST="0.0.0.0", + DBPORT=55555, + # MODEL + MODEL_NAME=model_name, + # MODEL_H=360, + # PIPELINE + SMART_FILTERING_ENABLED=sf_enabled, + THRESHOLD_VALUE=int(os.getenv("THRESHOLD_VALUE", THRESHOLD_VALUE)), + # VISUALIZATION + DETECTION_TYPE=detection_type, + ) + + test_class_self.device = test_class_self.config.DEVICE + test_class_self.device_input = test_class_self.config.device_input + test_class_self.resize_h, test_class_self.resize_w = [ + test_class_self.config.MODEL_H, + test_class_self.config.MODEL_W, + ] + + test_class_self.setup_reader( + test_class_self.config.TARGET_FPS, test_class_self.config.CLIP_DURATION + ) + + # RUN PARAMETERIZED TEST ---------------------------------------- + yield + + # tearDown LOGIC -------------------------------------------------- + print( + f"\n--- [TearDown] Memory Before Cleanup ({test_class_self._testMethodName}) ---" + ) + + # Nullify the model reference to trigger automatic cleanup + if hasattr(test_class_self, "model") and test_class_self.model is not None: + # Check if it's an Ultralytics model with a predictor + predictor = getattr(test_class_self.model, "predictor", None) + if predictor is not None: + try: + predictor.results = [] + except AttributeError: + pass + del test_class_self.model + test_class_self.model = None + + # Clear the singleton references to force a reload + TestSmartFilteringDetections._shared_model = None + TestSmartFilteringDetections._shared_model_path = None + TestSmartFilteringDetections._shared_model_device = None + + # Force Python to run destructors NOW while streams are still alive + gc.collect() + if torch.cuda.is_available(): + torch.cuda.synchronize() # Final sync to clear event queue + torch.cuda.empty_cache() + torch.cuda.ipc_collect() # Critical for shared memory cleanup + time.sleep(0.2) + + +@pytest.mark.usefixtures("setup_context") +class TestSmartFilteringDetections: + # SETUP -------------------------------------------- + + @pytest.mark.parametrize( + "device", + [ + ("cpu"), + ("gpu"), + ], + ) + def test_pipeline(self, device): + """Unified test runner for all configurations.""" + + # Run the actual model loader + self.get_model_by_device(device, sf_enabled=self.config.sf_enabled) + + total_session_start = time.perf_counter() + + results = self.model.predict( + source=self.source, + conf=self.config.DETECTION_THRESHOLD, + # iou=self.config.IOU_THRESHOLD, + show=False, + imgsz=(self.frame_height, self.frame_width), + save=True, + project=str(self.config.SHARED_OUTPUT), + name=f"pred_{self._testMethodName}_output_video", + exist_ok=True, # overwrite if folder exists + stream=True, + data={"names": {i: name for i, name in enumerate(self.label_source)}}, + ) + + frame_cnt = 0 + total_model_preprocess = 0.0 + total_model_inference = 0.0 + total_model_postprocess = 0.0 + for result in results: + frame_cnt += 1 + # Each iteration computes and yields the next frame's latency + total_model_preprocess += result.speed.get("preprocess", 0.0) + total_model_inference += result.speed.get("inference", 0.0) + total_model_postprocess += result.speed.get("postprocess", 0.0) + + # Capture the true real-world duration across all operational processing layers + real_world_latency_ms = (time.perf_counter() - total_session_start) * 1000 + + self._finalize_benchmarks( + real_world_latency_ms, + frame_cnt, + total_model_preprocess, + total_model_inference, + total_model_postprocess, + ) + + # HELPERS -------------------------------------------- + def get_model_by_device(self, device, sf_enabled=False): + """Singleton loader: only loads if device changes or model is missing.""" + if ( + sf_enabled + and (self.frame_width * self.frame_height) + <= self.config.SMART_FILTERING_PIXEL_CONSTRAINT + ): + sf_enabled = False + + if ( + TestSmartFilteringDetections._shared_model is not None + and TestSmartFilteringDetections._shared_model_device == device + and TestSmartFilteringDetections._shared_model_sf_enabled == sf_enabled + ): + self.model = TestSmartFilteringDetections._shared_model + self.model_path = TestSmartFilteringDetections._shared_model_path + return + + run_platform_name = "engine" if "cuda" in self.device_input else "openvino" + + if self.config.CUSTOM_MODEL_FLAG: + dir_path = "/home/resources/models/ultralytics/custom_models" + else: + dir_path = f"/home/resources/models/ultralytics/{self.config.MODEL_NAME}/{self.config.MODEL_PRECISION}" + + ( + TestSmartFilteringDetections._shared_model, + TestSmartFilteringDetections._shared_model_path, + self.label_source, + ) = get_model( + Path(dir_path), + self.config.MODEL_NAME, + run_platform_name, + self.device_input, + batch=self.config.MODEL_MAX_BATCH_SIZE, + force_export=force_export, + sf_enabled=sf_enabled, + model_h=self.resize_h, + model_w=self.resize_w, + ) + + TestSmartFilteringDetections._shared_model_device = device + TestSmartFilteringDetections._shared_model_sf_enabled = sf_enabled + self.model = TestSmartFilteringDetections._shared_model + # self.model.half() + self.model_path = TestSmartFilteringDetections._shared_model_path + + W, H = self.resize_w, self.resize_h + if not sf_enabled: + W, H = self.frame_width, self.frame_height + self.model_warmup(H, W) + + def _finalize_benchmarks( + self, + real_world_latency_ms, + n_frames, + total_model_preprocess, + total_model_inference, + total_model_postprocess, + ): + """Aggregates metrics and adds them to the results list.""" + total_model_ms = ( + total_model_preprocess + total_model_inference + total_model_postprocess + ) + + duration_s = n_frames / self.input_fps if self.input_fps > 0 else 0 + + real_latency_s = real_world_latency_ms / 1000.0 + real_est_fps = n_frames / real_latency_s if real_latency_s > 0 else 0 + model_fps = n_frames / (total_model_ms / 1000.0) if total_model_ms > 0 else 0 + + # Construct dictionary block matching test_detections structure + stats = { + "Test Name": self._testMethodName, + "Detection Type": self.config.DETECTION_TYPE, + "Device": self.device, + "Smart Filtering": "Enabled" if self.config.sf_enabled else "Disabled", + "Video": self.video_path.name, + "Video Duration (s)": f"{duration_s:.4f}", + "Video FPS": f"{self.input_fps:.2f}", + "Pipeline Latency (s)": f"{real_latency_s:.2f}", + "Frames Processed": n_frames, + "Real Est. FPS": f"{real_est_fps:.2f}", + "Model Est. FPS": f"{model_fps:.2f}", + "Model Avg Pre-processing (ms)": f"{total_model_preprocess / n_frames:.2f}", + "Model Avg Inference (ms)": f"{total_model_inference / n_frames:.2f}", + "Model Avg Post-processing (ms)": f"{total_model_postprocess / n_frames:.2f}", + } + self.__class__.benchmarks.append(stats) + print(stats, flush=True) + + print(f"\n[{self._testMethodName}] Latency: {real_latency_s:.2f} sec") + print(f"\n[{self._testMethodName}] Real Est. FPS: {real_est_fps:.2f}") + print( + f"\n[{self._testMethodName}] Model Est. FPS: {model_fps:.2f} ({n_frames} frames)" + ) + + +# INHERIT METHODS FROM HANDLERS ----------------------------------------------------------------- +TestSmartFilteringDetections.setup_reader = DeviceBaseHandler.setup_reader +TestSmartFilteringDetections.get_frameWH = DeviceBaseHandler.get_frameWH +TestSmartFilteringDetections.run_model = DeviceBaseHandler.run_model +TestSmartFilteringDetections.model_warmup = DeviceBaseHandler.model_warmup + + +if __name__ == "__main__": + # TEST ARGUMENTS + parser = argparse.ArgumentParser(description="Run Video Detection Pipeline Tests") + parser.add_argument( + "-s", + "--source", + type=str, + default="anduril_swarm_8K.mp4", + help="Video filename (located in /inputs) or RTSP target stream endpoint", + ) + + # MODEL TO USE + parser.add_argument( + "--no-custom", + action="store_false", + dest="custom_model_flag", + help="Enable if using Ultralytics YOLO model", + ) + parser.add_argument( + "-m", + "--model", + type=str, + default="drone_detection", + dest="model_name", + help="Name of model. Required if `--no-custom` is enabled. [Default: drone_detection]", + ) + + # Filter tests + # parser.add_argument( + # "--type", + # type=str, + # choices=["object", "motion"], + # dest="detection_type", + # help="Filter by detection type (object or motion)", + # ) + parser.add_argument( + "--device", + type=str, + choices=["cpu", "gpu"], + help="Filter by device (cpu or gpu)", + ) + # parser.add_argument( + # "--sf", + # action="store_true", + # default=None, + # dest="sf_enabled", + # help="Filter by Smart Filtering", + # ) + + # # DEBUGGING + # parser.add_argument( + # "--debug", + # action="store_true", + # help="Enable debug message and save intermediate images for Smart Filtering tests", + # ) + # parser.add_argument( + # "-n", + # type=int, + # default=100, + # dest="debug_frame_limit", + # help="Number of frames used for debugging [Default: 100]", + # ) + + args = parser.parse_args() + + # UPDATE ENVIRONMENTAL VARIABLES + os.environ["VIDEO_FILENAME"] = args.source + os.environ["CUSTOM_MODEL_FLAG"] = "True" if args.custom_model_flag else "False" + os.environ["MODEL_NAME"] = args.model_name + # os.environ["DEBUG"] = "1" if args.debug else "0" + # os.environ["DEBUG_FRAME_LIMIT"] = str(args.debug_frame_limit) + + # detection_type, device, sf_enabled + run_args = [] + # if args.detection_type: + # run_args.append(args.detection_type) + if args.device: + run_args.append(args.device) + # if args.sf_enabled: + # run_args.append(str(args.sf_enabled)) + + # PYTEST COMMAND + pytest_args = [ + "-s", + "-v", + "--log-cli-level=DEBUG", + __file__, + ] # -s -v --log-cli-level=DEBUG + if run_args: + pytest_args.extend(["-k", " and ".join(run_args)]) + + print(f"Launching tests for {args.source}") + + sys.exit(pytest.main(pytest_args)) diff --git a/fastapi/tests/test_pipeline.py b/fastapi/tests/test_pipeline.py new file mode 100644 index 0000000..fb924dd --- /dev/null +++ b/fastapi/tests/test_pipeline.py @@ -0,0 +1,868 @@ +import argparse +import csv +import gc +import logging +import multiprocessing +import os +import sys +import time +import traceback +from datetime import datetime +from pathlib import Path + +import psutil +import pytest +import torch + +sys.path.insert(1, str(Path(__file__).parent.parent)) +from include.default_configs import ( + CUSTOM_MODEL_FLAG_DEFAULT, + DEBUG_DEFAULT, + MODEL_NAME_DEFAULT, + THRESHOLD_VALUE, +) + +# Import the exact core handlers to replace duplicate script code +# from include.handlers import BASE_PIPELINE_CONFIG +from include.utils import ( + PipelineConfig, +) + +try: + torch.multiprocessing.set_start_method("spawn", force=True) +except RuntimeError: + pass + + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) +# Suppress low-delay reference block warnings from OpenCV +os.environ["OPENCV_LOG_LEVEL"] = "OFF" + +main_app_logger = logging.getLogger(__name__) + + +def log_to_logger(message, level="info"): + try: + if level.lower() == "debug": + main_app_logger.debug(message) + elif level.lower() == "warning": + main_app_logger.warning(message) + else: + main_app_logger.info(message) + except Exception: + pass + + +@pytest.fixture(scope="class") +def setup_context(request): + """Orchestrates test directories and configuration schemas.""" + current_test_filename = Path(__file__).stem + test_dir = Path(__file__).parent + main_path = test_dir.parent + video_dir = main_path / "inputs" + + # Resolve source from CLI/Environment parameters + request.cls.source = os.getenv("STREAM_SOURCE", "anduril_swarm_8K.mp4") + is_rtsp = "rtsp://" in request.cls.source + if not is_rtsp: + VIDEO_FILENAME = request.cls.source + if video_dir.exists(): + vid_source = video_dir / VIDEO_FILENAME + else: + video_dir = Path("/watch_dir") + vid_source = video_dir / VIDEO_FILENAME + + assert vid_source.exists() + request.cls.source = str(vid_source) + request.cls.name = vid_source.stem + else: + request.cls.name = "rtsp" + request.cls.test_duration_mins = float(os.getenv("TEST_DURATION_MINS", 2.0)) + + request.cls.result_dir = test_dir / f"{current_test_filename}_results" + request.cls.result_dir.mkdir(parents=True, exist_ok=True) + + request.cls.benchmarks = [] + if is_rtsp: + request.cls.csv_filename = "reader_perf_results_rtsp.csv" + else: + vid_shortname = Path(request.cls.source).stem + request.cls.csv_filename = f"reader_perf_results_{vid_shortname}.csv" + request.cls.csv_path = request.cls.result_dir / request.cls.csv_filename + + yield + + if request.cls.benchmarks: + ordered_headers = [ + "timestamp", + "test_name", + "source", + "device", + "detection_type", + "smart_filter_active", + "configured_duration_mins", + "video_duration", + "actual_duration_secs", + "stat_duration_secs", + "hardware_video_fps", + "pipeline_read_fps", + "stat_fps", + "total_frames_read", + "stat_frame_count", + "total_frames_ingested", + "total_target_frames_processed", + "total_objects_detected", + "frames_dropped_or_skipped", + "dropped_frame_sequences", + "average_read_latency_ms", + "max_read_latency_ms", + "avg_cpu_utilization_pct", + "avg_system_ram_used_mb", + "avg_gpu_vram_allocated_mb", + "prefetch_queue_backlog", + "avg_prefetch_backlog_frames", + "hardware_fallback_triggers", + "fallback_engine_triggered", + "status", + ] + # keys = {k for r in request.cls.benchmarks for k in r.keys()} + keys = [] + for r in request.cls.benchmarks: + for k in r.keys(): + keys.append(k) + sorted_keys = [] + for c in ordered_headers: + if c in keys: + sorted_keys.append(c) + with open(str(request.cls.csv_path), "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(sorted_keys)) + writer.writeheader() + writer.writerows(request.cls.benchmarks) + print(f"\n[FINAL] Telemetry saved to: {request.cls.csv_path}", flush=True) + + +@pytest.fixture(autouse=True) +def each_test_setup(request): + if torch.cuda.is_available(): + torch.cuda.synchronize() + + device = request.node.callspec.params.get("device") + os.environ["DEVICE"] = device + + yield + + gc.collect() + if torch.cuda.is_available(): + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + time.sleep(0.2) + + +def stream_worker( + test_name, + source, + source_name, + out_dir, + device_type, + test_duration_mins, + result_queue, + run_clipper, + disable_detection=True, + sf_enabled=True, + detection_type="object", +): + """ + Subprocess sandbox that bridges metrics capture straight to the production + DeviceBaseHandler pipeline engine. + """ + + # Suppress OpenCV internal warn frames + os.environ["OPENCV_LOG_LEVEL"] = "OFF" + os.environ["OPENCV_VIDEOIO_DEBUG"] = "0" + os.environ["FFMPEG_LOG_LEVEL"] = "quiet" + test_duration_secs = test_duration_mins * 60 + metrics = { + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "test_name": test_name, + "source": source, + "device": device_type, + "video_duration": 0.0, + # "pipeline_read_fps": 0.0, + # "avg_cpu_utilization_pct": 0.0, + # "avg_system_ram_used_mb": 0.0, + # "avg_gpu_vram_allocated_mb": 0.0, + # "status": "INIT", + "configured_duration_mins": test_duration_mins, + "actual_duration_secs": 0.0, + "stat_duration_secs": 0.0, + "hardware_video_fps": 0.0, + "pipeline_read_fps": 0.0, + "stat_fps": 0.0, + "total_frames_read": 0, + "stat_frame_count": 0, + "frames_dropped_or_skipped": 0, + "dropped_frame_sequences": 0, + "average_read_latency_ms": 0.0, + "avg_cpu_utilization_pct": 0.0, + "avg_system_ram_used_mb": 0.0, + "avg_gpu_vram_allocated_mb": 0.0, + "prefetch_queue_backlog": 0, # how full the thread queue is + "hardware_fallback_triggers": 0, # times reader swaps from NVDEC to software CPU mode due to error flags + "max_read_latency_ms": 0.0, + "avg_prefetch_backlog_frames": 0.0, + "fallback_engine_triggered": 0, + "total_frames_ingested": 0, + "total_target_frames_processed": 0, + "total_objects_detected": 0, + "detection_type": detection_type, + "smart_filter_active": sf_enabled, + "status": "INIT", + } + + loop_start = time.perf_counter() + process = psutil.Process(os.getpid()) + cpu_samples, ram_samples, vram_samples = [], [], [] + prefetch_backlog_samples = [] + + # Override the global configuration mapping for this specific hardware context + config = PipelineConfig( + # GENERAL + CUSTOM_MODEL_FLAG=os.getenv( + "CUSTOM_MODEL_FLAG", CUSTOM_MODEL_FLAG_DEFAULT + ), # True, + DEVICE="GPU" if device_type.lower() == "gpu" else "CPU", + OMIT_DETECTIONS_FLAG=True, + TEST_MODE=True, + DEBUG=os.getenv("DEBUG", DEBUG_DEFAULT), + DEBUG_FRAME_LIMIT=os.getenv("DEBUG_FRAME_LIMIT", 100), + # VIDEO WRITER + # CLIP_DURATION=None, + # VDMS + ENABLE_QUERYING=run_clipper, + DISABLE_DETECTION=disable_detection, + DBHOST="127.0.0.1", + # MODEL + MODEL_NAME=os.getenv("MODEL_NAME", MODEL_NAME_DEFAULT), + # MODEL_H=360, + # PIPELINE + SMART_FILTERING_ENABLED=sf_enabled, + THRESHOLD_VALUE=int(os.getenv("THRESHOLD_VALUE", THRESHOLD_VALUE)), + # VISUALIZATION + DETECTION_TYPE=detection_type, + # MAX_WORKERS=4, + ) + + if out_dir: + if "Scenario_4_" in test_name: + result_dir = out_dir / "results" + result_dir.mkdir(parents=True, exist_ok=True) + os.environ["TEST_SUITE_RENDER_DIR"] = str(result_dir) + config.SHARED_OUTPUT = str(out_dir) + + import vdms + + def mock_connect(self, host, port): + pass + + vdms.vdms.connect = mock_connect + + from include.handlers import CPUStreamHandler, GPUStreamHandler + + if device_type.lower() == "gpu": + HandlerClass = GPUStreamHandler + else: + HandlerClass = CPUStreamHandler + + last_sample = time.perf_counter() + handler = None # Explicit initializing tracking state pointer 🚀 + + try: + # Let handlers.py completely manage the reader, threads, ring buffers, and FFMPEG process + handler = HandlerClass( + source=source, name=source_name, active_streams={}, config=config + ) + handler.start() + + while (time.perf_counter() - loop_start) < test_duration_secs: + if not handler.active: + break + + curr_time = time.perf_counter() + if (curr_time - last_sample) >= 0.5: + cpu_samples.append(psutil.cpu_percent(interval=0)) + ram_samples.append(process.memory_info().rss / (1024 * 1024)) + if device_type.lower() == "gpu" and torch.cuda.is_available(): + vram_free, vram_total = torch.cuda.mem_get_info() + vram_samples.append((vram_total - vram_free) / (1024 * 1024)) + if getattr(handler, "prefetch_queue", None) is not None: + current_backlog = handler.prefetch_queue.qsize() + prefetch_backlog_samples.append(current_backlog) + last_sample = curr_time + time.sleep(0.01) + + # Safely capture performance values out of production thread states + actual_duration = time.perf_counter() - loop_start + metrics["actual_duration_secs"] = actual_duration + metrics["stat_frame_count"] = handler.stat_frame_count + metrics["stat_fps"] = handler.stat_fps + metrics["stat_duration_secs"] = ( + round(handler.stat_frame_count / handler.stat_fps, 2) + if getattr(handler, "stat_fps", 0) > 0 + else 0.0 + ) + metrics["status"] = "COMPLETED_SUCCESSFULLY" + metrics["total_frames_ingested"] = ( + handler.frame_count + ) # Total raw frames processed + metrics["total_target_frames_processed"] = ( + handler.frame_count_target + ) # Total target slices saved/evaluated + metrics["total_objects_detected"] = handler.total_objects_detected + + metrics["total_frames_read"] = getattr(handler, "frame_count", 0) + + metrics["smart_filter_active"] = sf_enabled + metrics["device"] = device_type + metrics["detection_type"] = detection_type + + if "Scenario_4_" in test_name and handler.frame_count == 0: + metrics["status"] = "FAILED_NO_FRAMES" + + # Pull counters natively tracked by the underlying HybridReaders + if hasattr(handler, "reader") and handler.reader is not None: + r = handler.reader + metrics["hardware_video_fps"] = round(getattr(r, "input_fps", 0.0), 2) + metrics["frames_dropped_or_skipped"] = getattr(r, "dropped_frames_count", 0) + metrics["dropped_frame_sequences"] = getattr( + r, "dropped_sequences_count", 0 + ) + + metrics["hardware_fallback_triggers"] = ( + 1 if getattr(r, "use_cpu_decode_fallback", False) else 0 + ) + metrics["fallback_engine_triggered"] = ( + 1 if getattr(r, "use_cpu_decode_fallback", False) else 0 + ) + if not str(handler.source).startswith("rtsp://"): + metrics["video_duration"] = ( + round(r.numFrames / metrics["hardware_video_fps"], 2) + if metrics["hardware_video_fps"] > 0 + else 0.0 + ) + + h_telemetry = getattr(handler, "telemetry", {}) + io_latencies = h_telemetry.get("ram_disk_io_write_ms", []) + + if io_latencies: + metrics["average_read_latency_ms"] = round( + sum(io_latencies) / len(io_latencies), 2 + ) + metrics["max_read_latency_ms"] = round(max(io_latencies), 2) + else: + # Fallback estimation based on frame process intervals if disk I/O lists were bypassed + estimated_latency = ( + actual_duration / max(1, metrics["total_frames_read"]) + ) * 1000 + metrics["average_read_latency_ms"] = round(estimated_latency, 2) + metrics["max_read_latency_ms"] = round(estimated_latency * 1.4, 2) + + # Calculate averages from telemetry sampling windows + metrics["pipeline_read_fps"] = ( + round(metrics["total_frames_read"] / actual_duration, 2) + if actual_duration > 0 + else 0.0 + ) + + except Exception as err: + is_expected_fail = ( + "Scenario_1" in test_name + and isinstance(err, (RuntimeError, TimeoutError)) + and any( + x in str(err).lower() + for x in [ + "could not open/connect", + "failed to initialize stream reader endpoint", + "stream reader initialization failure", + "timed out", + ] + ) + ) + if is_expected_fail: + log_to_logger( + f"[EXPECTED FAILURE SUCCESSFUL]: {test_name} handled invalid stream target.", + level="info", + ) + metrics["status"] = "PASSED_RECONNECT_FAIL" + metrics["actual_duration_secs"] = round(time.perf_counter() - loop_start, 2) + else: + log_to_logger( + f"[WORKER CRASHED]:\n{traceback.format_exc()}", + level="warning", + ) + metrics["status"] = f"CRASHED: {type(err).__name__}" + finally: + # Calculate system baseline averages across the sample tracking matrices safely + if cpu_samples: + metrics["avg_cpu_utilization_pct"] = round( + sum(cpu_samples) / len(cpu_samples), 1 + ) + if ram_samples: + metrics["avg_system_ram_used_mb"] = round( + sum(ram_samples) / len(ram_samples), 1 + ) + if vram_samples: + metrics["avg_gpu_vram_allocated_mb"] = round( + sum(vram_samples) / len(vram_samples), 1 + ) + if prefetch_backlog_samples: + metrics["prefetch_queue_backlog"] = prefetch_backlog_samples[-1] + metrics["avg_prefetch_backlog_frames"] = round( + sum(prefetch_backlog_samples) / len(prefetch_backlog_samples), 2 + ) + + # Cleanup active thread worker contexts safely if they exist + if handler is not None and getattr(handler, "active", False): + try: + handler.stop() + except Exception: + pass + + # Provide a 200ms cool-down window for OpenVINO/PyTorch C++ worker threads + # to finish their internal teardown before Python destroys the process space. + time.sleep(0.2) + + gc.collect() + if torch.cuda.is_available(): + try: + torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + except Exception: + pass + + result_queue.put(metrics) + + # Absolute kill switch safely reclaims orphaned third-party threads + # at the kernel level without crashing the parent pytest framework + time.sleep(0.1) + os._exit(0) + + +@pytest.mark.usefixtures("setup_context") +class TestHybridStreamHandlers: + @pytest.mark.parametrize("device", ["cpu", "gpu"]) + def test_scenario_1_invalid_rtsp(self, device): + """SCENARIO 1: Automated Connection Fail Simulation""" + test_name = "Scenario_1_Invalid_RTSP" + bad_uri = "rtsp://invalid_host_domain:554/stream_simulation" + run_clipper = False + time_limit_m = round(self.test_duration_mins, 1) + + print( + f"\n========================================\n" + f"RUNNING TEST: {test_name} | Device: {device.upper()}\n" + f"Source Destination: {bad_uri}\n" + f"========================================", + flush=True, + ) + + # Execute production workflow in completely isolated spawned sandbox + ctx = multiprocessing.get_context("spawn") + res_queue = ctx.Queue() + + worker_p = ctx.Process( + target=stream_worker, + args=( + test_name, + bad_uri, + self.name, + None, + device, + time_limit_m, + res_queue, + run_clipper, + ), + ) + worker_p.start() + worker_p.join() + + if worker_p.is_alive(): + worker_p.terminate() + worker_p.close() # Reclaims underlying OS file descriptors immediately + + test_metrics = res_queue.get() + self.__class__.benchmarks.append(test_metrics) + print(f"Test Status Result: {test_metrics.get('status')}\n", flush=True) + + # Explicitly tear down queue thread pools to prevent resource tracking leaks + try: + res_queue.close() # Stops new items from being inserted + res_queue.join_thread() # Joins the internal buffer thread safely + except Exception: + pass + + assert any(x in test_metrics.get("status") for x in ["PASSED", "ABORT"]) + + @pytest.mark.parametrize("device", ["cpu", "gpu"]) + def test_scenario_2_longevity_throughput(self, device): + """SCENARIO 2: Stability & Throughput Run""" + test_name = "Scenario_2_Longevity_Throughput_Evaluation" + run_clipper = False + + print( + f"\n========================================\n" + f"RUNNING TEST: {test_name} | Device: {device.upper()}\n" + f"Source Destination: {self.source}\n" + f"========================================", + flush=True, + ) + + # Execute production workflow in completely isolated spawned sandbox + ctx = multiprocessing.get_context("spawn") + res_queue = ctx.Queue() + + worker_p = ctx.Process( + target=stream_worker, + args=( + test_name, + self.source, + self.name, + None, + device, + self.test_duration_mins, + res_queue, + run_clipper, + ), + ) + worker_p.start() + worker_p.join() + + if worker_p.is_alive(): + worker_p.terminate() + worker_p.close() # Reclaims underlying OS file descriptors immediately + + test_metrics = res_queue.get() + self.__class__.benchmarks.append(test_metrics) + print(f"Test Status Result: {test_metrics.get('status')}\n", flush=True) + + # Explicitly tear down queue thread pools to prevent resource tracking leaks + try: + res_queue.close() # Stops new items from being inserted + res_queue.join_thread() # Joins the internal buffer thread safely + except Exception: + pass + + assert "COMPLETED_SUCCESSFULLY" in test_metrics.get("status") + + @pytest.mark.parametrize("device", ["cpu", "gpu"]) + def test_scenario_3_video_clipper(self, device): + """SCENARIO 3: Minimized Clip Generation Test via Production Handlers.""" + test_name = f"Scenario_3_Clipper_{device.upper()}" + # test_name = "Scenario_3_Clip_Generation_Evaluation" + run_clipper = True + + print( + f"\n========================================\n" + f"RUNNING TEST: {test_name} | Device: {device.upper()}\n" + f"Source Destination: {self.source}\n" + f"========================================", + flush=True, + ) + render_dir = self.result_dir / f"{self.name}/scenario3_{device}" + render_dir.mkdir(parents=True, exist_ok=True) + test_duration_mins = 1.0 + + # Execute production workflow in completely isolated spawned sandbox + ctx = multiprocessing.get_context("spawn") + res_queue = ctx.Queue() + + worker_p = ctx.Process( + target=stream_worker, + args=( + test_name, + self.source, + self.name, + render_dir, + device, + test_duration_mins, + res_queue, + run_clipper, + ), + ) + worker_p.start() + worker_p.join() + + if worker_p.is_alive(): + worker_p.terminate() + worker_p.close() # Reclaims underlying OS file descriptors immediately + + test_metrics = res_queue.get() + self.__class__.benchmarks.append(test_metrics) + print(f"Test Status Result: {test_metrics.get('status')}\n", flush=True) + + # Explicitly tear down queue thread pools to prevent resource tracking leaks + try: + res_queue.close() # Stops new items from being inserted + res_queue.join_thread() # Joins the internal buffer thread safely + except Exception: + pass + + assert "COMPLETED_SUCCESSFULLY" in test_metrics.get("status") + + @pytest.mark.parametrize("device", ["cpu", "gpu"]) + @pytest.mark.parametrize("sf_enabled", [True, False]) + @pytest.mark.parametrize("detection_type", ["motion", "object"]) + def test_scenario_4_detection_and_clipper(self, device, sf_enabled, detection_type): + """SCENARIO 4: Pipeline without sending metadata (detection + video clipper).""" + mode_str = "SmartFilter" if sf_enabled else "OnlyYOLO" + test_name = f"Scenario_4_{device.upper()}_{mode_str}" + run_clipper = True + disable_detection = False + + print( + f"\n========================================\n" + f"RUNNING TEST: {test_name} | Device: {device.upper()} | SF: {sf_enabled}\n" + f"Source Name: {self.name} | Destination: {self.source}\n" + f"========================================", + flush=True, + ) + render_dir = ( + self.result_dir + / f"{self.name}/scenario4_{device}/{detection_type}_{mode_str}" + ) + render_dir.mkdir(parents=True, exist_ok=True) + test_duration_mins = 1.0 + + # Execute production workflow in completely isolated spawned sandbox + ctx = multiprocessing.get_context("spawn") + res_queue = ctx.Queue() + + worker_p = ctx.Process( + target=stream_worker, + args=( + test_name, + self.source, + self.name, + render_dir, + device, + test_duration_mins, + res_queue, + run_clipper, + disable_detection, + sf_enabled, + detection_type, + ), + ) + worker_p.start() + worker_p.join() + + if worker_p.is_alive(): + worker_p.terminate() + worker_p.close() # Reclaims underlying OS file descriptors immediately + + test_metrics = res_queue.get() + self.__class__.benchmarks.append(test_metrics) + if disable_detection: + print(f"Test Status Result: {test_metrics.get('status')}\n", flush=True) + else: + print( + f"Test Status Result: {test_metrics.get('status')} w/ {test_metrics.get('total_objects_detected')} detections\n", + flush=True, + ) + + # Explicitly tear down queue thread pools to prevent resource tracking leaks + try: + res_queue.close() # Stops new items from being inserted + res_queue.join_thread() # Joins the internal buffer thread safely + except Exception: + pass + + assert "COMPLETED_SUCCESSFULLY" in test_metrics.get("status") + + +def get_available_scenarios(): + import inspect + + available_scenarios = set() + for attr_name, _ in inspect.getmembers( + TestHybridStreamHandlers, predicate=inspect.isfunction + ): + if attr_name.startswith("test_scenario_"): + # Extracts '1' from 'test_scenario_1_invalid_rtsp' + parts = attr_name.split("_") + if len(parts) > 2 and parts[2].isdigit(): + available_scenarios.add(int(parts[2])) + + return sorted(list(available_scenarios)) + + +def get_pytest_filter_expression(args, sorted_scenarios): + # Automatically separate scenarios that support filtering vs basic scenarios + parameterized_ids = {4} + basic_ids = [num for num in sorted_scenarios if num not in parameterized_ids] + + # Determine which scenarios the user wants to filter over + target_scenarios = args.scenario if args.scenario else sorted_scenarios + + scenario_clauses = [] + print("\n" + "=" * 50) + print("TARGET SELECTION PREVIEW") + print("=" * 50) + + for num in target_scenarios: + if num in basic_ids: + print(f" 🔹 Scenario {num}: Standard routing (ignoring sub-filters)") + scenario_clauses.append(f"scenario_{num}") + else: + clause = f"scenario_{num}" + applied_subs = [] + + if args.sf_enabled is not None: + # Target exact parameter tokens generated by pytest parametrization + sf_str = "-True-" if args.sf_enabled else "-False-" + clause += f" and {sf_str}" + applied_subs.append(f"sf_enabled={args.sf_enabled}") + + if args.detection_type: + clause += f" and {args.detection_type}" + applied_subs.append(f"type={args.detection_type}") + + sub_msg = ( + f" with sub-filters: {', '.join(applied_subs)}" if applied_subs else "" + ) + print(f" ⚙️ Scenario {num}: Active compilation{sub_msg}") + scenario_clauses.append(f"({clause})") + + # Safely join scenarios together with 'or' so they can execute side-by-side + filter_expression = f"test_scenario_ and ({' or '.join(scenario_clauses)})" + + # Target hardware context selection filter applies globally across all test cases + if args.device.lower() != "all": + print(f" 💻 Hardware Context Constraint: {args.device.upper()}") + filter_expression = f"({filter_expression}) and {args.device.lower()}" + else: + print(" 💻 Hardware Context Constraint: ALL AVAILABLE") + + print("=" * 50) + print(f"COMPILED PYTEST KEYWORD EXPRESSION:\n 👉 {filter_expression}") + print("=" * 50 + "\n") + + return filter_expression + + +if __name__ == "__main__": + multiprocessing.set_start_method("spawn", force=True) + sorted_scenarios = get_available_scenarios() + + parser = argparse.ArgumentParser( + description="Isolated HybridReader Telemetry Harness Suite" + ) + parser.add_argument( + "-s", + "--source", + type=str, + # default="rtsp://172.17.0.1:8554/live1", + default="anduril_swarm_8K.mp4", + help="Video filename (located in /inputs) or RTSP target stream endpoint", + ) + parser.add_argument( + "-d", + "--duration", + type=float, + default=2.0, + help="Test duration in minutes.", + ) + parser.add_argument( + "--scenario", + nargs="+", + type=int, + choices=sorted_scenarios, # 1 - 4 + default=None, + help=f"Specify one or more scenarios. Otherwise all scenarios are ran. Available scenarios: {sorted_scenarios}", + ) + + # MODEL TO USE + parser.add_argument( + "--no-custom", + action="store_false", + dest="custom_model_flag", + help="Enable if using Ultralytics YOLO model", + ) + parser.add_argument( + "-m", + "--model", + type=str, + default="drone_detection", + dest="model_name", + help="Name of model. Required if `--no-custom` is enabled. [Default: drone_detection]", + ) + + # Filter tests + parser.add_argument( + "--type", + type=str, + choices=["object", "motion"], + dest="detection_type", + help="Filter by detection type (object or motion)", + ) + parser.add_argument( + "--device", + type=str, + default="all", + choices=["cpu", "gpu", "all"], + help="Target hardware context selection filter.", + ) + parser.add_argument( + "--sf", + action="store_true", + default=None, + dest="sf_enabled", + help="Filter by Smart Filtering", + ) + + # DEBUGGING + parser.add_argument( + "--debug", + action="store_true", + help="Enable debug message and save intermediate images for Smart Filtering tests", + ) + # parser.add_argument( + # "-n", + # type=int, + # default=100, + # dest="debug_frame_limit", + # help="Number of frames used for debugging [Default: 100]", + # ) + args = parser.parse_args() + + os.environ["STREAM_SOURCE"] = args.source + os.environ["TEST_DURATION_MINS"] = str(args.duration) + os.environ["CUSTOM_MODEL_FLAG"] = "True" if args.custom_model_flag else "False" + os.environ["MODEL_NAME"] = args.model_name + os.environ["DEBUG"] = "1" if args.debug else "0" + + # filter_expression = "test_scenario_4_detection_and_clipper" + # filter_expression = "test_scenario_3_video_clipper" + # filter_expression = "test_scenario_*_clipper" + + filter_expression = get_pytest_filter_expression(args, sorted_scenarios) + + pytest_args = [ + "-k", + filter_expression, + "-s", + "-v", + "--log-cli-level=DEBUG", + __file__, + ] + + print( + f"Launching decoupled testing suite configurations for destination targets: {args.source}", + flush=True, + ) + sys.exit(pytest.main(pytest_args)) diff --git a/finetune/Dockerfile b/finetune/Dockerfile index 0d16cc2..e093a7b 100644 --- a/finetune/Dockerfile +++ b/finetune/Dockerfile @@ -41,7 +41,13 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean -RUN python3 -m venv ${VIRTUAL_ENV} + # hadolint ignore=DL3013 +RUN python3 -m venv ${VIRTUAL_ENV} && \ + ${VIRTUAL_ENV}/bin/pip install --no-cache-dir \ + "pip==26.0.1" \ + "torch==2.10.0" \ + "torchvision==0.25.0" \ + "numpy==1.26.0" ENV DEVICE="GPU" @@ -59,13 +65,6 @@ RUN if [ "${DEVICE}" = "GPU" ]; then \ ENV PATH="$VIRTUAL_ENV/bin:/usr/local/cuda/bin:${PATH}" ENV LD_LIBRARY_PATH="$VIRTUAL_ENV/lib:/usr/local/cuda/lib64:${LD_LIBRARY_PATH}" -# RUN pip3 install --no-cache-dir "pip==26.0.1" "torch==2.10.0" "torchvision==0.25.0" "numpy==1.26.4" -WORKDIR /home - -COPY requirements.* /home/ -# RUN pip3 install --no-cache-dir -r requirements.in -RUN pip3 install --no-cache-dir --require-hashes -r requirements.txt - # ARG PYTHON_VERSION=3.10 ENV PYTHON_VERSION=3.10 @@ -125,11 +124,11 @@ RUN cmake -D CMAKE_BUILD_TYPE=RELEASE \ RUN rm -rf ${DEPENDENCY_DIR} # Set the working directory in the container -# WORKDIR /home +WORKDIR /home -# COPY requirements.* /home/ -# # RUN pip3 install --no-cache-dir -r requirements.in -# RUN pip3 install --no-cache-dir --require-hashes -r requirements.txt +COPY requirements.* /home/ +# RUN pip3 install --no-cache-dir -r requirements.in +RUN pip3 install --no-cache-dir --require-hashes -r requirements.txt RUN \ diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 98427f2..347c739 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,19 +3,22 @@ FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063 ARG DEBUG="0" ENV DEBUG="${DEBUG}" +ARG DEBIAN_FRONTEND=noninteractive # Prevent Python from writing .pyc files and enable unbuffered logging ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 # fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 +# hadolint ignore=DL3008 RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y --only-upgrade --no-install-recommends libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-pip && \ rm -rf /var/lib/apt/lists/* -COPY requirements.txt /home/requirements.txt +COPY requirements.* /home/ RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt +# RUN pip3 install --no-cache-dir -r /home/requirements.in COPY *.py /home/ COPY *.conf /etc/nginx/ diff --git a/frontend/requirements.txt b/frontend/requirements.txt index e80e09f..e9aeb70 100644 --- a/frontend/requirements.txt +++ b/frontend/requirements.txt @@ -115,9 +115,9 @@ charset-normalizer==3.4.4 \ --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==2.10 \ + --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ + --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 ply==3.11 \ --hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \ --hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce @@ -180,9 +180,9 @@ tornado==6.5.5 \ --hash=sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521 \ --hash=sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7 \ --hash=sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5 -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 vdms==0.0.23 \ --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 diff --git a/frontend/setting.py b/frontend/setting.py index 3ddf175..c39ff7e 100755 --- a/frontend/setting.py +++ b/frontend/setting.py @@ -1,9 +1,13 @@ #!/usr/bin/python3 +import json +import os from concurrent.futures import ThreadPoolExecutor -from tornado import gen, web -from tornado.concurrent import run_on_executor +from tornado import gen, httpclient, web + +BACKEND_URL = os.getenv("BACKEND_URL", "http://fastapi-service:8000") +FASTAPI_URL = f"{BACKEND_URL}/model_classes" class SettingHandler(web.RequestHandler): @@ -14,197 +18,148 @@ def __init__(self, app, request, **kwargs): def check_origin(self, origin): return True - @run_on_executor + @gen.coroutine + def get_model_classes(self): + """Asynchronously fetches classes from the FastAPI container.""" + client = httpclient.AsyncHTTPClient() + default_classes = ["class0"] + + try: + response = yield client.fetch(FASTAPI_URL) + data = json.loads(response.body.decode("utf-8")) + return data.get("classes", default_classes) # Default fallback if empty + # with open(MODEL_CLASSES_FILE, "r") as f: + # data = json.load(f) + # return data.get("classes", default_classes) + except Exception as e: + # Fallback to a basic list if the model container is unreachable + print(f"Error fetching model classes: {e}") + return default_classes + + # @run_on_executor + @gen.coroutine def _settings(self): - return { - "controls": [ + include_advanced = False + dynamic_objects = yield self.get_model_classes() + + controls = [] + if "person" in dynamic_objects: + person_control = { + "name": "person", + "icon": "images/person.png", + "description": "Find Person", + "params": [ + { + "name": "Age Min", + "type": "number", + "value": 18, + }, + { + "name": "Age Max", + "type": "number", + "value": 75, + }, + { + "name": "Gender", + "type": "list", + "values": [ + "skip", + "male", + "female", + ], + "value": "skip", + }, + { + "name": "Emotion List", + "type": "list", + "values": [ + "skip", + "neutral", + "happy", + "sad", + "surprise", + "anger", + ], + "value": "skip", + }, + ], + } + controls.append(person_control) + + object_control = { + "name": "object", + "icon": "images/object.png", + "description": "Find Object", + "params": [ { - "name": "person", - "icon": "images/person.png", - "description": "Find Person", - "params": [ - { - "name": "Age Min", - "type": "number", - "value": 18, - }, - { - "name": "Age Max", - "type": "number", - "value": 75, - }, - { - "name": "Gender", - "type": "list", - "values": [ - "skip", - "male", - "female", - ], - "value": "skip", - }, - { - "name": "Emotion List", - "type": "list", - "values": [ - "skip", - "neutral", - "happy", - "sad", - "surprise", - "anger", - ], - "value": "skip", - }, - ], + "name": "Object List", + "type": "list", + "values": sorted(dynamic_objects), + "value": dynamic_objects[0], }, { - "name": "object", - "icon": "images/object.png", - "description": "Find Object", - "params": [ - { - "name": "Object List", - "type": "list", - "values": [ - "airplane", - "apple", - "backpack", - "banana", - "baseball bat", - "baseball glove", - "bear", - "bed", - "bench", - "bicycle", - "bird", - "boat", - "book", - "bottle", - "bowl", - "broccoli", - "bus", - "cake", - "car", - "carrot", - "cat", - "cell phone", - "chair", - "clock", - "couch", - "cow", - "cup", - "dining table", - "dog", - "donut", - "elephant", - "fire hydrant", - "fork", - "frisbee", - "giraffe", - "hair drier", - "handbag", - "horse", - "hot dog", - "keyboard", - "kite", - "knife", - "laptop", - "microwave", - "motorcycle", - "mouse", - "orange", - "oven", - "parking meter", - "person", - "pizza", - "potted plant", - "refrigerator", - "remote", - "sandwich", - "scissors", - "sheep", - "sink", - "skateboard", - "skis", - "snowboard", - "spoon", - "sports ball", - "stop sign", - "suitcase", - "surfboard", - "teddy bear", - "tennis racket", - "tie", - "toaster", - "toilet", - "toothbrush", - "traffic light", - "train", - "truck", - "tv", - "umbrella", - "vase", - "wine glass", - "zebra", - ], - "value": "person", - }, - { - "name": "Object Count", - "type": "list", - "values": ["skip"] + [str(x) for x in range(1, 26)], - "value": "skip", - }, - { - "name": "Object Count Condition", - "type": "list", - "values": ["==", "<=", "<", ">=", ">"], - "value": "==", - }, - { - "name": "Frame ID", - "type": "text", - "value": "skip", - }, - { - "name": "Frame Condition", - "type": "list", - "values": [ - "==", - "<=", - ">=", - ], - "value": "==", - }, - ], + "name": "Object Count", + "type": "list", + "values": ["skip"] + [str(x) for x in range(1, 26)], + "value": "skip", + }, + { + "name": "Object Count Condition", + "type": "list", + "values": ["==", "<=", "<", ">=", ">"], + "value": "==", + }, + { + "name": "Frame ID", + "type": "text", + "value": "skip", }, { - "name": "video", - "icon": "images/video.png", - "description": "Find Video", - "params": [ - { - "name": "Video Name", - "type": "text", - "value": "*", - } + "name": "Frame Condition", + "type": "list", + "values": [ + "==", + "<=", + ">=", ], + "value": "==", }, - # { - # "name": "advanced", - # "icon": "images/advanced.png", - # "description": "Advanced", - # "params": [ - # { - # "name": "Search Queries", - # "type": "text", - # "value": "", - # } - # ], - # }, ], } + controls.append(object_control) + + video_control = { + "name": "video", + "icon": "images/video.png", + "description": "Find Video", + "params": [ + { + "name": "Video Name", + "type": "text", + "value": "*", + } + ], + } + controls.append(video_control) + + if include_advanced: + advanced_control = { + "name": "advanced", + "icon": "images/advanced.png", + "description": "Advanced", + "params": [ + { + "name": "Search Queries", + "type": "text", + "value": "", + } + ], + } + controls.append(advanced_control) + + return { + "controls": controls, + } @gen.coroutine def get(self): diff --git a/start_app.sh b/start_app.sh index 3f03e56..a8b6a0a 100755 --- a/start_app.sh +++ b/start_app.sh @@ -3,7 +3,7 @@ # This script runs the Curation application ####################################################################################################################### # DEFAULT VARIABLES -INGESTION="object,face" +INGESTION="object" #,face" EXP_TYPE=compose DEBUG="0" DEVICE="CPU" diff --git a/udf/Dockerfile b/udf/Dockerfile index 8114b5c..32d3cc1 100644 --- a/udf/Dockerfile +++ b/udf/Dockerfile @@ -9,8 +9,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 # fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 +# hadolint ignore=DL3008 RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y --only-upgrade --no-install-recommends libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-pip python3-venv \ curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* RUN python3 -m venv ${VIRTUAL_ENV} diff --git a/udf/requirements.txt b/udf/requirements.txt index ef85742..38d54e7 100644 --- a/udf/requirements.txt +++ b/udf/requirements.txt @@ -198,9 +198,9 @@ tomli==1.1.0 \ vdms==0.0.23 \ --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 -werkzeug==3.1.6 \ - --hash=sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25 \ - --hash=sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131 +werkzeug==3.1.8 \ + --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 \ + --hash=sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44 wheel==0.46.3 \ --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 diff --git a/video/Dockerfile b/video/Dockerfile index 4193004..ad5732d 100644 --- a/video/Dockerfile +++ b/video/Dockerfile @@ -1,59 +1,48 @@ -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a as build +FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a ENV VIRTUAL_ENV=/opt/venv ARG DEBIAN_FRONTEND=noninteractive -# fixes CVE-2023-4911 vulnerability on Ubuntu 22.04 +# Prevent Python from writing .pyc files and enable unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# hadolint ignore=DL3008 RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ + apt-get install -y --only-upgrade --no-install-recommends libc-bin libc6 && \ apt-get install -y -q --no-install-recommends python3-setuptools \ python3-tornado python3-ply python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* + RUN python3 -m venv ${VIRTUAL_ENV} + +# COPY --from=lcc_base_video_image:latest ${VIRTUAL_ENV} ${VIRTUAL_ENV} +# COPY --from=lcc_base_video_image:latest /home /home + +# activate virtual environment ENV PATH="$VIRTUAL_ENV/bin:$PATH" -COPY resources /home/resources +# Set the working directory in the container +WORKDIR /home + COPY requirements.* /home/ ARG DEVICE="CPU" ENV DEVICE="${DEVICE}" -RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple -RUN if [ "${DEVICE}" = "CPU" ]; then \ - pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt --index-url https://download.pytorch.org/whl/cpu --extra-index-url https://pypi.org/simple; \ - else \ - pip3 install --no-cache-dir --require-hashes -r /home/requirements.${DEVICE}.txt ; \ - fi; - -RUN omz_downloader --list /home/resources/models/models.lst -o /home/resources/models --precisions FP16 +# RUN pip3 install --no-cache-dir -r /home/requirements.in +RUN pip3 install --no-cache-dir --require-hashes -r /home/requirements.txt -# FINAL IMAGE -FROM openvisualcloud/xeon-ubuntu2204-media-nginx:23.1@sha256:d19eb597dc210134063803630ae2ea1ec84dfd4189138f59551e2f5ed047284a -ENV VIRTUAL_ENV=/opt/venv -ARG DEBIAN_FRONTEND=noninteractive - -# Prevent Python from writing .pyc files and enable unbuffered logging -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -RUN apt-get update && \ - apt-get install --only-upgrade libc-bin libc6 && \ - apt-get install -y -q --no-install-recommends python3-tornado \ - curl libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -# COPY --from=build /usr/local/lib/python3.10 /usr/local/lib/python3.10 -COPY --from=build ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --from=build /home /home - - -WORKDIR /home COPY *.py /home/ +COPY *.yaml /home/ +COPY *.conf /etc/nginx/ COPY cleanup.sh /home/ COPY manage.sh /home/ -COPY *.conf /etc/nginx/ -CMD ["/bin/bash","-c","python3 /home/resources/models/download_yolo.py && /home/manage.sh"] +# COPY frontend /home/frontend +# COPY include /home/include +# CMD ["/bin/bash","-c","python /home/resources/models/download_yolo.py && /home/manage.sh"] +CMD ["/bin/bash","-c","/home/manage.sh"] #### ARG USER @@ -66,7 +55,7 @@ RUN if [ ${GID} -gt 0 ]; then groupadd -f -g ${GID} ${GROUP}; fi; \ if [ ${UID} -gt 0 ]; then useradd --no-log-init -d /home/${USER} -g ${GID} -K UID_MAX=${UID} -K UID_MIN=${UID} ${USER}; fi; \ touch /var/run/nginx.pid && \ mkdir -p /var/log/nginx /var/lib/nginx /var/www/cache /var/www/gen /var/www/mp4 && \ - chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home/resources + chown -R ${UID}.${GID} /var/run/nginx.pid /var/log/nginx /var/lib/nginx /var/www /etc/nginx/nginx.conf /home VOLUME ["/var/www"] USER ${UID} #### diff --git a/video/manage.sh b/video/manage.sh index ac4247d..9d1ffc1 100755 --- a/video/manage.sh +++ b/video/manage.sh @@ -1,7 +1,8 @@ #!/bin/bash -e # Watch directory -python3 /home/watch_and_send2vdms.py ${WATCH_DIR} & +echo "WATCH_DIR: ${WATCH_DIR}" +python3 /home/source_watcher.py ${WATCH_DIR} & # run tornado -exec /home/manage.py +exec ${VIRTUAL_ENV}/bin/python /home/manage.py diff --git a/video/nginx.conf b/video/nginx.conf index bfc5853..357d154 100644 --- a/video/nginx.conf +++ b/video/nginx.conf @@ -8,6 +8,19 @@ events { } http { + + # Include MIME types + include mime.types; + default_type application/octet-stream; + + # Basic logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + # `proxy_read_timeout` maximum time Nginx will wait for a response from a proxied server. # If the proxied server does not send any data within this time, Nginx will close the connection. # Default: 60 @@ -22,15 +35,16 @@ http { # Default: 60 # proxy_connect_timeout 300s; - include mime.types; - default_type application/octet-stream; + # include mime.types; + # default_type application/octet-stream; server { listen 8080; server_name _; # client_body_timeout 10s; # client_header_timeout 10s; - client_max_body_size 1024M; + # client_max_body_size 1024M; + client_max_body_size 2G; # 8K frames are huge sendfile on; location /api/ { @@ -46,5 +60,21 @@ http { internal; root /var/www; } + + # location / { + # # proxy_http_version 1.1; + + # # Mandatory for WebSocket support + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + + # # Standard proxy headers + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_read_timeout 86400; + # } } } + diff --git a/video/process_stream.py b/video/process_stream.py deleted file mode 100644 index eca85bf..0000000 --- a/video/process_stream.py +++ /dev/null @@ -1,988 +0,0 @@ -import gc -import multiprocessing as mp -import os -import queue -import shlex -import subprocess -import sys -import time # time library -import traceback -from concurrent.futures import ThreadPoolExecutor, as_completed -from datetime import datetime -from pathlib import Path -from random import randint - -import cv2 # OpenCV library -from openvino.runtime import Core -from ultralytics import YOLO -from ultralytics.utils.checks import check_imgsz -from utils import str2bool - -import vdms - - -def _sort_dict_by_frame(in_dict): - def _by_int(key): - return tuple(int(k) for k in key.split("_")) - - return dict(sorted(in_dict.items(), key=lambda x: _by_int(x[0]))) - - -""" GENERAL VARIABLES """ -CODE_DIR = os.getenv("CODE_DIR", "/home") -CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) -DBHOST = os.getenv("DBHOST", "vdms-service") -DEBUG = os.getenv("DEBUG", "0") -DEVICE = os.getenv("DEVICE", "CPU") -INGESTION = os.getenv("INGESTION", "object,face") -RESIZE_FLAG = str2bool(os.getenv("RESIZE_FLAG", False)) -OMIT_DETECTIONS_FLAG = str2bool(os.getenv("OMIT_DETECTIONS_FLAG", False)) -SHARED_OUTPUT = os.getenv("SHARED_OUTPUT", "/var/www/mp4") -Path(SHARED_OUTPUT).mkdir(parents=True, exist_ok=True) -TEST_MODE = str2bool(os.getenv("TEST_FLAG", False)) -TMP_LOCATION = os.getenv("TMP_LOCATION", "/var/www/cache/") -UDF_HOST = os.getenv("UDF_HOST", "video-service") -MODEL_NAME = os.getenv("MODEL_NAME", "yolo11n") - -LOCKTIMEOUT_RETRIES = 5 -ERR_KEYWORDS = [ - "timeout", - "null search iterator", - "outoftransactions", - "internal server", -] - -BATCH_SIZE = 1 -DBPORT = 55555 -DEBUG_FLAG = True if DEBUG == "1" else False -DETECTION_THRESHOLD = 0.25 -DEVICE_OV = "AUTO" -HALF_FLAG = True -IOU_THRESHOLD = 0.7 -MODEL_PRECISION = "FP16" -MODEL_W, MODEL_H = (640, 640) -TARGET_FPS = 15 -UDF_PORT = 5011 -WRITER_FOURCC = cv2.VideoWriter_fourcc(*"mp4v") # avc1, mp4v -NUM_USUABLE_CPUS = 2 # os.cpu_count() - -if CUSTOM_MODEL_FLAG: - model_path = f"{CODE_DIR}/resources/models/ultralytics/custom_models/{MODEL_NAME}" -else: - model_path = f"{CODE_DIR}/resources/models/ultralytics/{MODEL_NAME}/{MODEL_PRECISION}/{MODEL_NAME}" - -if not TEST_MODE: - db = vdms.vdms() - db.connect(DBHOST, DBPORT) - -if DEVICE == "GPU": - model_path += ".engine" - batch_size = int(os.environ.get("GPU_BATCH_SIZE", 1)) - os.environ["CUDA_VISIBLE_DEVICES"] = "0" - from torch.cuda import empty_cache -else: - model_path += "_openvino_model/" - batch_size = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 - -device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" - -all_metadata = {} -send_metadata_queue = mp.Queue() - - -def retry_query(query, num_retries: int = LOCKTIMEOUT_RETRIES, sleep_timer: int = 0): - global db - for ridx in range(num_retries + 1): - response, _ = db.query(query, [[]]) - if "FailedCommand" in response[0] and any( - k in response[0]["info"].lower() for k in ERR_KEYWORDS - ): - err = response[0]["info"] - if DEBUG == "1": - query_type = list(query[0].keys())[0] - print( - f"DEBUG [process_stream Attempt #{ridx}] Received '{err}' for {query_type} query", - flush=True, - ) - if sleep_timer > 0: - time.sleep(sleep_timer) - else: - if DEBUG == "1": - print( - f"[DEBUG process_stream] Successful query response: {response}", - flush=True, - ) - break # Continue - return response - - -def load_models(): - # OBJECT DETECTION - model = YOLO(model_path, verbose=False, task="detect") - - # FACE, AGE, GENDER, AND EMOTIONS - ie = Core() - face_detection_model_xml = f"{CODE_DIR}/resources/models/intel/face-detection-adas-0001/{MODEL_PRECISION}/face-detection-adas-0001.xml" - face_detection_model = ie.read_model( - model=face_detection_model_xml, - weights=face_detection_model_xml.replace(".xml", ".bin"), - ) - # face_det_w, face_det_h = 672, 384 - _, face_det_c, face_det_h, face_det_w = face_detection_model.inputs[0].shape - face_det_compiled_model = ie.compile_model(face_detection_model, DEVICE_OV) - - age_gender_classification_model_xml = f"{CODE_DIR}/resources/models/intel/age-gender-recognition-retail-0013/{MODEL_PRECISION}/age-gender-recognition-retail-0013.xml" - age_gender_classification_model = ie.read_model( - model=age_gender_classification_model_xml, - weights=age_gender_classification_model_xml.replace(".xml", ".bin"), - ) - # ag_w, ag_h = 62, 62 - _, ag_c, ag_h, ag_w = age_gender_classification_model.inputs[0].shape - ag_compiled_model = ie.compile_model(age_gender_classification_model, DEVICE_OV) - - emotions_classification_model_xml = f"{CODE_DIR}/resources/models/intel/emotions-recognition-retail-0003/{MODEL_PRECISION}/emotions-recognition-retail-0003.xml" - emotions_classification_model = ie.read_model( - model=emotions_classification_model_xml, - weights=emotions_classification_model_xml.replace(".xml", ".bin"), - ) - # em_w, em_h = 64, 64 - _, em_c, em_h, em_w = emotions_classification_model.inputs[0].shape - em_compiled_model = ie.compile_model(emotions_classification_model, DEVICE_OV) - - return ( - model, - [face_det_compiled_model, ag_compiled_model, em_compiled_model], - [face_det_c, face_det_h, face_det_w], - [ag_c, ag_h, ag_w], - [em_c, em_h, em_w], - ) - - -model, face_models, face_det_CHW, ag_CHW, em_CHW = load_models() - - -""" DETECTION FUNCTIONS """ - - -# Extract metadata from object model results -def extract_metadata_from_results( - stream_name, frameNum, results, img_size, fps=TARGET_FPS -): - fW, fH = img_size - metadata = dict() - try: - for _, result in enumerate(results): - # GET METADATA FOR CLIP - boxes = result.boxes.cpu() - oidx = 0 - for box in boxes: - confidence = float(box.conf.item()) - if confidence > DETECTION_THRESHOLD: - class_id = int(box.cls.item()) - class_name = str(result.names[class_id]) - - if not OMIT_DETECTIONS_FLAG: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print( - # f"[OBJECT DETECTION] {class_name} detected in frame {frameNum} (Total detected: {current_cnt})", - f"[{timestamp}] {stream_name} DETECTION on Frame {frameNum}: {class_name} detected", - flush=True, - ) - x1, y1, x2, y2 = box.xyxy.tolist()[0] - height = min(y2, fH) - max(0, y1) - width = min(x2, fW) - max(0, x1) - object_res = [ - x1, - y1, - height, - width, - result.names[class_id], - confidence, - fH, - fW, - ] - - framenum_str = f"{frameNum:04d}_{oidx:04d}" - if DEBUG_FLAG: - meta_str = ",".join( - [str(o) for o in object_res + [framenum_str]] - ) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": { - "x": int(object_res[0]), - "y": int(object_res[1]), - "height": int(object_res[2]), - "width": int(object_res[3]), - "object": str(object_res[4]), - "object_det": { - "confidence": float(object_res[5]), - "frameH": int(fH), - "frameW": int(fW), - }, - }, - } - oidx += 1 - - except Exception: - e = traceback.format_exc() - print(f"Error in {stream_name} extract_metadata_from_results: {e}", flush=True) - - return metadata - - -# Detect faces from frame -def face_detection( - stream_name, frameNum, frame, img_size, face_models, face_det_CHW, ag_CHW, em_CHW -): - face_det_compiled_model, ag_compiled_model, em_compiled_model = face_models - face_det_c, face_det_h, face_det_w = face_det_CHW - ag_c, ag_h, ag_w = ag_CHW - em_c, em_h, em_w = em_CHW - W, H = img_size - bs = 1 - - genders = ["female", "male"] - emotions = ["neutral", "happy", "sad", "surprise", "anger"] - - # Resize expects HWC - input_image = cv2.resize( - frame, (face_det_w, face_det_h), interpolation=cv2.INTER_AREA - ) - input_image = input_image.transpose(2, 0, 1) # Shape: CHW - input_image = input_image.reshape((bs, face_det_c, face_det_h, face_det_w)) - - output_layer = face_det_compiled_model.output(0) - result = face_det_compiled_model([input_image])[output_layer] - - # Process the detections - faces = [] - metadata = dict() - oidx = 1 - for detection in result[0][0]: - confidence = float(detection[2]) - if confidence > DETECTION_THRESHOLD: - # Draw a bounding box around the face - x1 = int(detection[3] * frame.shape[1]) - if x1 < 0: - x1 = 0 - - y1 = int(detection[4] * frame.shape[0]) - if y1 < 0: - y1 = 0 - - x2 = int(detection[5] * frame.shape[1]) - if x2 > frame.shape[1] - 1: - x2 = frame.shape[1] - 1 - - y2 = int(detection[6] * frame.shape[0]) - if y2 > frame.shape[0] - 1: - y2 = frame.shape[0] - 1 - - height = y2 - y1 - width = x2 - x1 - - face_roi = frame[y1:y2, x1:x2] - # print(face_roi.shape) - age = gender = emotion = None - try: - ag_face_blob = cv2.resize( - face_roi, (ag_w, ag_h), interpolation=cv2.INTER_AREA - ) - ag_face_blob = ag_face_blob.transpose((2, 0, 1)) - ag_face_blob = ag_face_blob.reshape((bs, ag_c, ag_h, ag_w)) - ag_result = ag_compiled_model([ag_face_blob]) - age = int(ag_result["fc3_a"].flatten()[0] * 100) - gender = str(genders[ag_result["prob"].argmax()]) - except Exception as e: - print(f"Error occurred: {e}. Skipping age-gender model", flush=True) - - try: - em_face_blob = cv2.resize( - face_roi, (em_w, em_h), interpolation=cv2.INTER_AREA - ) - em_face_blob = em_face_blob.transpose((2, 0, 1)) - em_face_blob = em_face_blob.reshape((bs, em_c, em_h, em_w)) - em_result = em_compiled_model([em_face_blob])[ - em_compiled_model.output(0) - ] - emotion = str(emotions[em_result.argmax()]) - except Exception as e: - print(f"Error occurred: {e}. Skipping emotion model", flush=True) - face_res = [x1, y1, height, width, age, gender, emotion, confidence, H, W] - # print(face_res) - faces.append(face_res) - - tdict = { - "x": int(face_res[0]), - "y": int(face_res[1]), - "height": int(face_res[2]), - "width": int(face_res[3]), - "object": "face", - "object_det": { - "age": int(face_res[4]), - "gender": str(face_res[5]), - "emotion": str(face_res[6]), - "confidence": float(face_res[7]), - "frameH": int(H), - "frameW": int(W), - }, - } - - framenum_str = f"{frameNum:04d}_{oidx:04d}" - if DEBUG_FLAG: - meta_str = ",".join([str(o) for o in face_res + [framenum_str]]) - print(f"[{stream_name} METADATA],{meta_str}", flush=True) - - metadata[framenum_str] = { - "frameId": frameNum, - "bbId": framenum_str, - "bbox": tdict, - } - oidx += 1 - - return metadata - - -# Inference Function -def infer_worker( - stream_name, - frameNum, - frame, - img_size, - INGESTION, - fps=TARGET_FPS, -): # img_size:(W,H) - global model, face_models, face_det_CHW, ag_CHW, em_CHW - - height, width = frame.shape[:2] - if (width, height) != img_size: - frame = cv2.resize(frame, img_size) - - metadata = {} - metadata_face = {} - try: - if "object" in INGESTION: - results = model.predict( - frame, - imgsz=(img_size[1], img_size[0]), - batch=BATCH_SIZE, - conf=DETECTION_THRESHOLD, - iou=IOU_THRESHOLD, - half=HALF_FLAG, - device=device_input, - verbose=False, - stream=True, - ) - - metadata = extract_metadata_from_results( - stream_name, frameNum, results, img_size, fps=fps - ) - del results - - if "face" in INGESTION: - metadata_face = face_detection( - stream_name, - frameNum, - frame, - img_size, - face_models, - face_det_CHW, - ag_CHW, - em_CHW, - ) - - if DEVICE == "GPU": - empty_cache() # Frees memory no longer used - gc.collect() # Forces garbage collector - except Exception: - e = traceback.format_exc() - print(f"Error in {stream_name} infer_worker: {e}", flush=True) - return metadata, metadata_face - - -""" HELPFUL FUNCTIONS """ - - -# Manual FPS calculation if OpenCV reports 0 -def manual_fps_calculation(src, num_frames=10): - vid_obj = cv2.VideoCapture(src) - - frame_count = 0 - start_t = time.time() - - while frame_count < num_frames: - grabbed, frame = vid_obj.read() - - if not grabbed: - break - - frame_count += 1 - - end_t = time.time() - vid_obj.release() - - elapsed_t = end_t - start_t - - if elapsed_t > 0: - return frame_count / elapsed_t - else: - return 0 - - -# Generate and run UDF query -def get_udf_query( - filename_path, - properties, - ingest_mode, - new_size, - id="udf_metadata", - metadata=None, - test_mode=TEST_MODE, -): - query = { - "AddVideo": { - "from_file_path": str(filename_path), # from_server_file - "is_local_file": True, - "properties": properties, - "operations": [ - { - "type": "syncremoteOp", # "remoteOp", - "url": f"http://{UDF_HOST}:{UDF_PORT}/video", - "options": { - "id": id, - "otype": ingest_mode, - "media_type": "video", - "input_sizeWH": new_size, - "filename": properties["Name"], - "ingestion": 1, - }, - } - ], - } - } - - if id == "udf_metadata" and metadata is not None: - query["AddVideo"]["operations"][0]["options"]["metadata"] = metadata - - if test_mode: - return - - filename = str(Path(filename_path).name) - if DEBUG_FLAG: - print( - f"[TIMING],start_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - try: - res = retry_query([query], sleep_timer=randint(1, 5)) - - if DEBUG_FLAG: - print( - f"[TIMING],end_udf_ingest_{ingest_mode},{filename}," + str(time.time()), - flush=True, - ) - print(f"[DEBUG] {filename} PROPERTIES: {properties}", flush=True) - print(f"[DEBUG] {filename} INGEST_VIDEO RESPONSE: {res}", flush=True) - except Exception: - e = traceback.format_exc() - print(f"[DEBUG] VDMS Query Exception: {e}", flush=True) - - # elapsed_time = time.time() - start_t - - # db.disconnect() - # del db - - -# Release Video Writer object and re-encode video to seek via ffmpeg later -def release_clip_and_reencode(clip_key, _out_vid, clip_filename, tmp_file, target_fps): - if DEBUG == "1": - print( - f"[TIMING],start_release_clip,{clip_key},{time.time()}", - flush=True, - ) - _out_vid.release() - if DEBUG == "1": - print( - f"[TIMING],end_release_clip,{clip_key},{time.time()}", - flush=True, - ) - _out_vid = None - - # Re-encode video in order to seek via ffmpeg later - GENERAL_OPTS = "-flags -global_header -hide_banner -loglevel error -nostats -tune zerolatency -flush_packets 0" # -filter:v fps={target_fps} - CONVERSION = f"-c:v libx264 -preset ultrafast -filter:v fps=fps={target_fps}" # "-c:v libx264 -preset medium" - reencode_cmd = f"ffmpeg -y -i {tmp_file} {GENERAL_OPTS} {CONVERSION} -crf 23 -c:a copy {clip_filename}" - cmd_list = shlex.split(reencode_cmd) - if DEBUG == "1": - print( - f"[TIMING],start_reencode,{clip_key},{time.time()}", - flush=True, - ) - subprocess.run(cmd_list, check=True) - end_time = time.time() - # filename = str(Path(clip_filename).name) - if DEBUG == "1": - print( - f"[TIMING],end_reencode,{clip_key},{end_time}", - flush=True, - ) - print(f"[TIMING],Save clip,{clip_key},{end_time}", flush=True) - os.remove(tmp_file) - return _out_vid - - -# method to send metadata to VDMS once clip is saved -def metadata2vdms( - clip_key, - clip_filename, - clip_metadata, - width, - height, -): - if DEBUG == "1": - print( - f"[TIMING],start_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - # Send metadata to UDF - properties = { - "Name": clip_key, # .split("/")[-1], - "category": "video_path_rop", - } - - combined_metadata = clip_metadata["object"] if "object" in clip_metadata else {} - if "face" in clip_metadata: - for face_frameidx_bbidx, value in clip_metadata["face"].items(): - face_frameidx, face_bbidx = face_frameidx_bbidx.split("_") - max_obj_idx = 0 - for obj_frameidx_bbidx in combined_metadata: - if face_frameidx in obj_frameidx_bbidx: - _, obj_bbidx_ = obj_frameidx_bbidx.split("_") - max_obj_idx = max(max_obj_idx, int(obj_bbidx_)) - - if max_obj_idx > 0: - new_face_bbidx = max_obj_idx + 1 - new_key = f"{face_frameidx}_{new_face_bbidx:04d}" - combined_metadata[new_key] = value - combined_metadata[new_key]["bbId"] = new_key - else: - combined_metadata[face_frameidx_bbidx] = value - - combined_metadata = _sort_dict_by_frame(combined_metadata) - get_udf_query( - clip_filename, - properties, - INGESTION.replace(",", "+"), - (width, height), - id="udf_metadata", - metadata=combined_metadata, - test_mode=TEST_MODE, - ) - - if DEBUG == "1": - print( - f"[TIMING],end_clip_metadata,{clip_key},{time.time()}", - flush=True, - ) - - -""" CLASSES """ - - -# method to save clip -def save_clip( - clip_filename, clip_id, tmp_file, _out_vid, frame_count, frameNum, target_fps -): - clip_key = Path(clip_filename).name - if DEBUG == "1": - print( - f"[DEBUG] Clip {clip_key} (clip_id: {clip_id}) contains {frame_count} frames", - flush=True, - ) - _out_vid = release_clip_and_reencode( - clip_key, - _out_vid, - clip_filename, - tmp_file, - target_fps, - ) - if DEBUG == "1": - print( - f"[TIMING],end_get_clips,{clip_key},{time.time()}", - flush=True, - ) - return _out_vid - - -# method to create clips (read frame write to file; add name to list) -def send_metadata(): - global all_metadata - clip_filename = "" - clip_key = "" - width = 0 - height = 0 - while True: - try: - queue_details = send_metadata_queue.get() - if queue_details is None: - break - - (clip_key, clip_filename, width, height) = queue_details - - metadata2vdms( - clip_key, - clip_filename, - all_metadata[clip_key], - width, - height, - ) - del all_metadata[clip_key] - - except queue.Empty: - pass - - -# defining a helper class for implementing multi-threading -class VideoStream: - # initialization method - def __init__(self, src, fps=TARGET_FPS, fourcc=WRITER_FOURCC, camera_name=None): - self.stream_id = src - self.fourcc = fourcc - - if "://" in str(self.stream_id): - if camera_name is not None: - self.stream_name = camera_name - else: - self.stream_name = str(self.stream_id).split("/")[-1] - else: - self.stream_name = Path(self.stream_id).stem - - if self.stream_id.startswith("rtsp"): - os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" - - # Check that object is opened successfully - self.connect_to_stream(time_limit_mins=5) - if DEBUG == "1": - print( - f"[TIMING],Start processing,{self.stream_name},{time.time()}", - flush=True, - ) - - self.setup_stream(fps) - - # Create ThreadPoolExecutor - self.executor = ThreadPoolExecutor(max_workers=NUM_USUABLE_CPUS) - - # method to start thread - def start(self): - self.stopped = False - self.t = [] - self.t.append( - self.executor.submit( - self.get_frames, - ) - ) - self.t.append( - self.executor.submit( - send_metadata, - ) - ) - - # method to stop reading frames - def stop(self): - for t in as_completed(self.t): - try: - _ = t.result() - except Exception as t_e: - print(f"[DEBUG] Exception occurred in thread: {t_e}") - - # self.stopped = True - self.video_obj.release() - - # method to open stream/video within 5min (default) limit - def connect_to_stream(self, time_limit_mins=5): - # opening video capture stream - self.video_obj = cv2.VideoCapture(self.stream_id, cv2.CAP_FFMPEG) - - stream_available = False - time_limit_secs = time_limit_mins * 60 - connect_time = time.time() - while not stream_available: - if self.video_obj.isOpened(): - stream_available = True - elif self.stream_id.startswith("rtsp"): - if time.time() - connect_time < time_limit_secs: - self.video_obj = cv2.VideoCapture(self.stream_id, cv2.CAP_FFMPEG) - else: - print( - f"Exceeds {time_limit_mins} mins limit to connect to {self.stream_name}. Exiting ..." - ) - exit(1) - - # Gets video fps and framecount - def get_fps_and_framecnt(self, fps): - self.input_fps = int(self.video_obj.get(cv2.CAP_PROP_FPS)) # hardware fps - if self.input_fps == 0: # Case when FPs isn't available - self.input_fps = manual_fps_calculation(self.stream_id, num_frames=10) - - self.target_fps = fps if self.input_fps > fps else self.input_fps - self.frame_skip = int(self.input_fps / self.target_fps) - if self.frame_skip < 1: - self.frame_skip = 1 - - print(f"FPS of {self.stream_name} input stream: {self.input_fps}", flush=True) - print(f"FPS of {self.stream_name} output mp4: {self.target_fps}", flush=True) - - # Frame count for videos - self.frame_count = None - if "://" not in str(self.stream_id): - self.frame_count = int(self.video_obj.get(cv2.CAP_PROP_FRAME_COUNT)) - - # Gets frame W and H details - def get_frameWH(self): - input_width = int(self.video_obj.get(cv2.CAP_PROP_FRAME_WIDTH)) - input_height = int(self.video_obj.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - if RESIZE_FLAG or ((input_height * input_width) < (MODEL_H * MODEL_W)): - new_sizeHW = check_imgsz([MODEL_H, MODEL_W]) # expects hxw - else: - new_sizeHW = check_imgsz([input_height, input_width]) # expects hxw - - new_sizeWH = (new_sizeHW[1], new_sizeHW[0]) - - self.width = new_sizeWH[0] - self.height = new_sizeWH[1] - - # Sets up important info for stream - def setup_stream(self, fps): - self.inference_queue = mp.Queue() - self.retrieved_frames = 0 - self.num_frames_processed = 0 - - self.get_fps_and_framecnt(fps) - - self.get_frameWH() - - self._out_vid = None - self.clip_end_frame = {} - self.clip_filename = "" - self.clip_frame_count = 0 - self.clip_frame_inds = [] - self.clip_id = 0 - self.clip_length_in_secs = 10 - self.clip_total_frames = int(float(self.clip_length_in_secs * self.target_fps)) - - # method to process a frame - def get_frames(self): - clip_frame_idx = 0 - clip_id = 0 - if DEBUG == "1": - print( - f"[TIMING],start_get_frames,{self.stream_name},{time.time()}", - flush=True, - ) - while True: - grabbed, frame = self.video_obj.read() # Read next frame - - if not grabbed: # or self.stopped: - # self.stopped = True - self.inference_queue.put(None) - break - - frameNum = int(self.video_obj.get(cv2.CAP_PROP_POS_FRAMES)) - skip_frame_num = (frameNum - 1) % self.frame_skip - - if clip_frame_idx % self.clip_total_frames == 0: - if "://" not in str(self.stream_id): - clip_filename = f"{SHARED_OUTPUT}/{self.stream_name}_{clip_id}.mp4" - else: - clip_filename = ( - f"{SHARED_OUTPUT}/{self.stream_name}_{time.time()}.mp4" - ) - - tmp_file = TMP_LOCATION + clip_filename.split("/")[-1] - - if skip_frame_num == 0: - h, w = frame.shape[:2] - if (w, h) != (self.width, self.height): - frame = cv2.resize(frame, (self.width, self.height)) - - queue_details = ( - frameNum, # Overall frame number - (clip_frame_idx % self.clip_total_frames) - + 1, # Frame index in clip [1-indexed like opencv] - clip_id, # Clip number - clip_filename, - tmp_file, - frame.copy(), # Frame - ) - - self.inference_queue.put(queue_details) - self.retrieved_frames += 1 - - clip_frame_idx += 1 - if clip_frame_idx % self.clip_total_frames == 0: - clip_id += 1 - - if DEBUG == "1": - print( - f"[TIMING],end_get_frames,{self.stream_name},{time.time()}", flush=True - ) - - -def process_stream(camera_src, camera_name=None): - global all_metadata - webcam_stream = VideoStream(str(camera_src), camera_name=camera_name) - if DEBUG == "1": - print( - f"[TIMING],Start processing,{webcam_stream.stream_name},{time.time()}", - flush=True, - ) - - start = time.time() - # Start retrieving frames and add to queue - webcam_stream.start() - _out_vid = None - clip_filename = "" - clip_frame_idx = 0 - clip_id = 0 - clip_key = "" - frameNum = 0 - tmp_file = "" - while True: - queue_details = webcam_stream.inference_queue.get() - - if queue_details is None: - if _out_vid is not None: - frame_count = clip_frame_idx - if frame_count > TARGET_FPS: - _out_vid = save_clip( - clip_filename, - clip_id, - tmp_file, - _out_vid, - frame_count, - frameNum, - webcam_stream.target_fps, - ) - send_metadata_queue.put( - ( - clip_key, - clip_filename, - webcam_stream.width, - webcam_stream.height, - ) - ) - else: - _out_vid = None - send_metadata_queue.put(None) - print("End of stream") - break - - frameNum, clip_frame_idx, clip_id, clip_filename, tmp_file, frame = ( - queue_details - ) - clip_key = Path(clip_filename).name - if DEBUG == "1": - print( - f"[TIMING],start_infer_worker,{clip_key}-{clip_frame_idx},{time.time()}", - flush=True, - ) - - metadata, metadata_face = infer_worker( - webcam_stream.stream_name, - clip_frame_idx, - frame, - (webcam_stream.width, webcam_stream.height), # img_size, - INGESTION, - fps=webcam_stream.target_fps, - ) - - if DEBUG == "1": - print( - f"[TIMING],end_infer_worker,{clip_key}-{clip_frame_idx},{time.time()}", - flush=True, - ) - - all_metadata.setdefault(clip_key, {}) - all_metadata[clip_key].setdefault("object", {}) - all_metadata[clip_key]["object"].update(metadata) - all_metadata[clip_key].setdefault("face", {}) - all_metadata[clip_key]["face"].update(metadata_face) - webcam_stream.num_frames_processed += 1 - - if clip_frame_idx - 1 == 0: - if DEBUG == "1": - print( - f"[TIMING],start_get_clips,{clip_key},{time.time()}", - flush=True, - ) - _out_vid = cv2.VideoWriter( - tmp_file, - fourcc=webcam_stream.fourcc, - fps=webcam_stream.target_fps, - frameSize=(webcam_stream.width, webcam_stream.height), - ) - if DEBUG == "1": - print( - f"[TIMING],Start new clip,{clip_key},{time.time()}", - flush=True, - ) - - _out_vid.write(frame) - - if clip_frame_idx == webcam_stream.clip_total_frames: - frame_count = clip_frame_idx - _out_vid = save_clip( - clip_filename, - clip_id, - tmp_file, - _out_vid, - frame_count, - frameNum, - webcam_stream.target_fps, - ) - queue_details = ( - clip_key, - clip_filename, - webcam_stream.width, - webcam_stream.height, - ) - send_metadata_queue.put(queue_details) - - webcam_stream.stop() - - end = time.time() - - # printing time elapsed and fps - if DEBUG == "1": - elapsed = end - start - print( - "[DEBUG] Stream name:{}, FPS: {} , Elapsed Time: {}, Num. Retrieved Frames: {}, Num. Processed Frames: {}".format( - webcam_stream.stream_name, - webcam_stream.target_fps, - elapsed, - webcam_stream.retrieved_frames, - webcam_stream.num_frames_processed, - ), - flush=True, - ) - - print( - f"[TIMING],Completed processing,{webcam_stream.stream_name},{end}", - flush=True, - ) - - -""" MAIN FUNCTION """ - -if __name__ == "__main__": - camera_src = sys.argv[1] - camera_name = sys.argv[2] if len(sys.argv) == 3 else None - - process_stream(str(camera_src), camera_name) diff --git a/video/requirements.CPU.txt b/video/requirements.CPU.txt deleted file mode 100644 index 4828f07..0000000 --- a/video/requirements.CPU.txt +++ /dev/null @@ -1,1161 +0,0 @@ -about-time==4.2.1 \ - --hash=sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece \ - --hash=sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341 -alive-progress==3.3.0 \ - --hash=sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c \ - --hash=sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 -autograd==1.8.0 \ - --hash=sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683 \ - --hash=sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78 -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ - --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ - --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ - --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ - --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ - --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ - --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ - --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ - --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ - --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ - --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf -cma==4.4.1 \ - --hash=sha256:61177b54f12bfeeac307970f8caefd8210dfa1d43a2da34e9ef8b1416d930fd8 \ - --hash=sha256:bf0621d4f52cf3354be3d0a5cd439ffed52f24f429ab02c23cf0f8ca16d427d8 -contourpy==1.3.2 \ - --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ - --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ - --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ - --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ - --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ - --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ - --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ - --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ - --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ - --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ - --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ - --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ - --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ - --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ - --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ - --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ - --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ - --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ - --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ - --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ - --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ - --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ - --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ - --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ - --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ - --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ - --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ - --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ - --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ - --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ - --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ - --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ - --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ - --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ - --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ - --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ - --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ - --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ - --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ - --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ - --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ - --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ - --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ - --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ - --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ - --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ - --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ - --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ - --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ - --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ - --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ - --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ - --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ - --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ - --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ - --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ - --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c -cycler==0.12.1 \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c -deprecated==1.3.1 \ - --hash=sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f \ - --hash=sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223 -filelock==3.25.2 \ - --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ - --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 -fonttools==4.61.1 \ - --hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \ - --hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \ - --hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \ - --hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \ - --hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \ - --hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \ - --hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \ - --hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \ - --hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \ - --hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \ - --hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \ - --hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \ - --hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \ - --hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \ - --hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \ - --hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \ - --hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \ - --hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \ - --hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \ - --hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \ - --hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \ - --hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \ - --hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \ - --hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \ - --hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \ - --hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \ - --hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \ - --hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \ - --hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \ - --hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \ - --hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \ - --hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \ - --hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \ - --hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \ - --hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \ - --hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \ - --hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \ - --hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \ - --hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \ - --hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \ - --hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \ - --hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \ - --hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \ - --hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \ - --hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \ - --hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \ - --hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \ - --hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \ - --hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \ - --hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd -fsspec==2026.2.0 \ - --hash=sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff \ - --hash=sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 -graphemeu==0.7.2 \ - --hash=sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542 \ - --hash=sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8 -jinja2==3.1.6 \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 -joblib==1.5.3 \ - --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ - --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 -jsonschema==4.26.0 \ - --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ - --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d -kiwisolver==1.4.9 \ - --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ - --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ - --hash=sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21 \ - --hash=sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e \ - --hash=sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff \ - --hash=sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7 \ - --hash=sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c \ - --hash=sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26 \ - --hash=sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa \ - --hash=sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f \ - --hash=sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1 \ - --hash=sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891 \ - --hash=sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77 \ - --hash=sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 \ - --hash=sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d \ - --hash=sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce \ - --hash=sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3 \ - --hash=sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60 \ - --hash=sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a \ - --hash=sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 \ - --hash=sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab \ - --hash=sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78 \ - --hash=sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771 \ - --hash=sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f \ - --hash=sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b \ - --hash=sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 \ - --hash=sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32 \ - --hash=sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527 \ - --hash=sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185 \ - --hash=sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634 \ - --hash=sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed \ - --hash=sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1 \ - --hash=sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c \ - --hash=sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11 \ - --hash=sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 \ - --hash=sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5 \ - --hash=sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4 \ - --hash=sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58 \ - --hash=sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5 \ - --hash=sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198 \ - --hash=sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536 \ - --hash=sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134 \ - --hash=sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf \ - --hash=sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2 \ - --hash=sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2 \ - --hash=sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370 \ - --hash=sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1 \ - --hash=sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154 \ - --hash=sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b \ - --hash=sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197 \ - --hash=sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386 \ - --hash=sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a \ - --hash=sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48 \ - --hash=sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748 \ - --hash=sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c \ - --hash=sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8 \ - --hash=sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5 \ - --hash=sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999 \ - --hash=sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369 \ - --hash=sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122 \ - --hash=sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b \ - --hash=sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 \ - --hash=sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9 \ - --hash=sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f \ - --hash=sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799 \ - --hash=sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028 \ - --hash=sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 \ - --hash=sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525 \ - --hash=sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d \ - --hash=sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb \ - --hash=sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872 \ - --hash=sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64 \ - --hash=sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586 \ - --hash=sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf \ - --hash=sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552 \ - --hash=sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2 \ - --hash=sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415 \ - --hash=sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c \ - --hash=sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6 \ - --hash=sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64 \ - --hash=sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d \ - --hash=sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548 \ - --hash=sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07 \ - --hash=sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 \ - --hash=sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d \ - --hash=sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771 \ - --hash=sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9 \ - --hash=sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c \ - --hash=sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3 \ - --hash=sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16 \ - --hash=sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145 \ - --hash=sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611 \ - --hash=sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 \ - --hash=sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464 \ - --hash=sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2 \ - --hash=sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 \ - --hash=sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 \ - --hash=sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df \ - --hash=sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f \ - --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 \ - --hash=sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220 -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 -markupsafe==3.0.3 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 -matplotlib==3.10.8 \ - --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \ - --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ - --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ - --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \ - --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ - --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ - --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ - --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ - --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ - --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ - --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ - --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ - --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ - --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ - --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \ - --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ - --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ - --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ - --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ - --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ - --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ - --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ - --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ - --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ - --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ - --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ - --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ - --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ - --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ - --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ - --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ - --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ - --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ - --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ - --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ - --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \ - --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ - --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ - --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ - --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ - --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ - --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ - --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ - --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ - --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ - --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ - --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ - --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ - --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \ - --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ - --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \ - --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \ - --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \ - --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ - --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba -moocore==0.2.0 \ - --hash=sha256:3dc601f85f9a4743ed50ddd027dca30e3bb55c899916a092c2ece495b1b2de08 \ - --hash=sha256:653449231f328d3c9e69693ec3d44e8c77f38ab7e9ef0c69dd9ded40449e980d \ - --hash=sha256:a7683feddfd2a47b4a0f89ee8d370cae72331792f68e67f083ccb37bb2f1c8cf \ - --hash=sha256:b90c7bde2164f9b95c6b2e870f0ca6ccc5dabff2bf8086162d7318c770e5868f \ - --hash=sha256:cf8f091a7304532ed605acd82acd051e89af22ece8e2a27a3cee0faf9f2ea185 \ - --hash=sha256:d0699b770b5eebdeac477a356d539efa8807c6cf067a453a0682e0df2299a512 \ - --hash=sha256:e93c07062adefd0fcba73a521f325f7fb874f2af92aaeec203cf9db31a41894b \ - --hash=sha256:ea057409731e73dbc4ba4214cbf7747309695b01314f8786678b758cc9c561c4 -mpmath==1.3.0 \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c -natsort==8.4.0 \ - --hash=sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581 \ - --hash=sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c -networkx==3.4.2 \ - --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ - --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f -ninja==1.13.0 \ - --hash=sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f \ - --hash=sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988 \ - --hash=sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9 \ - --hash=sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630 \ - --hash=sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db \ - --hash=sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978 \ - --hash=sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1 \ - --hash=sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72 \ - --hash=sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e \ - --hash=sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2 \ - --hash=sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9 \ - --hash=sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714 \ - --hash=sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200 \ - --hash=sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c \ - --hash=sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5 \ - --hash=sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96 \ - --hash=sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1 \ - --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ - --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e -nncf==2.19.0 \ - --hash=sha256:6c782b0e5a8120d36a3249cd61edaf311b6270dd90095661700658899726e523 \ - --hash=sha256:d32a9667e6b2837557bdd340895bb49e8d103eb1ec004031386e8c2bb4554cac -numpy==2.2.6 \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 -openvino-telemetry==2025.2.0 \ - --hash=sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c \ - --hash=sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599 -packaging==24.1 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 -pandas==2.3.3 \ - --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ - --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ - --hash=sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5 \ - --hash=sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791 \ - --hash=sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73 \ - --hash=sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec \ - --hash=sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4 \ - --hash=sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5 \ - --hash=sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac \ - --hash=sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084 \ - --hash=sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c \ - --hash=sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87 \ - --hash=sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35 \ - --hash=sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250 \ - --hash=sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c \ - --hash=sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826 \ - --hash=sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9 \ - --hash=sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713 \ - --hash=sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1 \ - --hash=sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523 \ - --hash=sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3 \ - --hash=sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78 \ - --hash=sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53 \ - --hash=sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c \ - --hash=sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21 \ - --hash=sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5 \ - --hash=sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff \ - --hash=sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45 \ - --hash=sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110 \ - --hash=sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493 \ - --hash=sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b \ - --hash=sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450 \ - --hash=sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86 \ - --hash=sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8 \ - --hash=sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98 \ - --hash=sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89 \ - --hash=sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66 \ - --hash=sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b \ - --hash=sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8 \ - --hash=sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29 \ - --hash=sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6 \ - --hash=sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc \ - --hash=sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2 \ - --hash=sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788 \ - --hash=sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa \ - --hash=sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151 \ - --hash=sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838 \ - --hash=sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b \ - --hash=sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a \ - --hash=sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d \ - --hash=sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908 \ - --hash=sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0 \ - --hash=sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b \ - --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ - --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee -pillow==12.1.1 \ - --hash=sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9 \ - --hash=sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da \ - --hash=sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f \ - --hash=sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642 \ - --hash=sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713 \ - --hash=sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850 \ - --hash=sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9 \ - --hash=sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0 \ - --hash=sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9 \ - --hash=sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8 \ - --hash=sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6 \ - --hash=sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd \ - --hash=sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5 \ - --hash=sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c \ - --hash=sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35 \ - --hash=sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1 \ - --hash=sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff \ - --hash=sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38 \ - --hash=sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4 \ - --hash=sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af \ - --hash=sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60 \ - --hash=sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986 \ - --hash=sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13 \ - --hash=sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 \ - --hash=sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e \ - --hash=sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b \ - --hash=sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15 \ - --hash=sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a \ - --hash=sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb \ - --hash=sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d \ - --hash=sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b \ - --hash=sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e \ - --hash=sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a \ - --hash=sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f \ - --hash=sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a \ - --hash=sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce \ - --hash=sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc \ - --hash=sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f \ - --hash=sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586 \ - --hash=sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f \ - --hash=sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9 \ - --hash=sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8 \ - --hash=sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40 \ - --hash=sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60 \ - --hash=sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c \ - --hash=sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0 \ - --hash=sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334 \ - --hash=sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af \ - --hash=sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735 \ - --hash=sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524 \ - --hash=sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf \ - --hash=sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b \ - --hash=sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2 \ - --hash=sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9 \ - --hash=sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7 \ - --hash=sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e \ - --hash=sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4 \ - --hash=sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4 \ - --hash=sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b \ - --hash=sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397 \ - --hash=sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c \ - --hash=sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e \ - --hash=sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029 \ - --hash=sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3 \ - --hash=sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052 \ - --hash=sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984 \ - --hash=sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293 \ - --hash=sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523 \ - --hash=sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f \ - --hash=sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b \ - --hash=sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80 \ - --hash=sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f \ - --hash=sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79 \ - --hash=sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23 \ - --hash=sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8 \ - --hash=sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e \ - --hash=sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3 \ - --hash=sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e \ - --hash=sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36 \ - --hash=sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f \ - --hash=sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5 \ - --hash=sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f \ - --hash=sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6 \ - --hash=sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32 \ - --hash=sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20 \ - --hash=sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202 \ - --hash=sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0 \ - --hash=sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3 \ - --hash=sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563 \ - --hash=sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090 \ - --hash=sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289 -pip==26.0.1 \ - --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ - --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 -platformdirs==4.5.1 \ - --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ - --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 -protobuf==5.29.6 \ - --hash=sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05 \ - --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ - --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ - --hash=sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18 \ - --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ - --hash=sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6 \ - --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ - --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ - --hash=sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49 \ - --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ - --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 -psutil==7.2.1 \ - --hash=sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1 \ - --hash=sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266 \ - --hash=sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442 \ - --hash=sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79 \ - --hash=sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf \ - --hash=sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f \ - --hash=sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679 \ - --hash=sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8 \ - --hash=sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49 \ - --hash=sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f \ - --hash=sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129 \ - --hash=sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67 \ - --hash=sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6 \ - --hash=sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17 \ - --hash=sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42 \ - --hash=sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d \ - --hash=sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672 \ - --hash=sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a \ - --hash=sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc \ - --hash=sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3 \ - --hash=sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8 -pycparser==3.0 \ - --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ - --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 -pydot==3.0.4 \ - --hash=sha256:3ce88b2558f3808b0376f22bfa6c263909e1c3981e2a7b629b65b451eee4a25d \ - --hash=sha256:bfa9c3fc0c44ba1d132adce131802d7df00429d1a79cc0346b0a5cd374dbe9c6 -pygments==2.20.0 \ - --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ - --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 -pymoo==0.6.1.6 \ - --hash=sha256:007bdabe638d5cc1ff98d1df096e7cea42f266ecc7b84fc0aac695a20b6f02f6 \ - --hash=sha256:01e848762e04d183e8dcc12e1cfb1e2cdd2c5efeda47961dbcf6c86c09751e30 \ - --hash=sha256:2a2e9524b07baef135a7143700cc65a504c2123c717a6924fa939947b4fdba73 \ - --hash=sha256:38fa5bf507cd7c07eae0286948db79f761b5b4c5d5956a31fd0e5a771c83fa47 \ - --hash=sha256:3b9dcb6959cf2b12c0f1d4ac971fa53074430eb976367b2e7eec0d0301423f31 \ - --hash=sha256:466f88073c9f498bcf75334a2e47279968003d91b7fe01df1b056b5a6c3434d8 \ - --hash=sha256:49a9cde6ff7cc8497458166e2c14734bf4879c76256412cce0de2af86d2b71a8 \ - --hash=sha256:50472dfb93b90b4801c63070a4afc786c52f7f56b345f345024b39cd35ff06c4 \ - --hash=sha256:5086f46020aa1a13a0e3142ef87b6487e91ea5d25b7b69467039f0d3f5a57cb2 \ - --hash=sha256:533de86c284bf3b7b6da871e2ae07a914eaa5d3a51f5c4e6d7c055d3471c4467 \ - --hash=sha256:5607e0359c96691158192bd3e2b939beab6d4ed25032ef4cb79edf588c2bb475 \ - --hash=sha256:56ecb6f9f5ac0559829e183a23ea3672eae8c1eb9d745305bef98da8d1614d28 \ - --hash=sha256:62599637724f74820e16d813e6ca86af1733add4251e22b5a6c677df75dec06b \ - --hash=sha256:642308eae98c5b8aee96272e10c6409dd97289ef214ec7383b39425f01549b5f \ - --hash=sha256:718b9e971e436c2ded520c4a6fe8c83b7f305da1fcc539d6386d025c6e9bfe66 \ - --hash=sha256:791188677b42e104c41cd1c65370a94fbbec756d8894947863f42a1f76352c90 \ - --hash=sha256:798e5af41527324303e56ac806f9ec34c65f31c2df9e904daeca68027bf91266 \ - --hash=sha256:7c042120df86240c1451e2a1afb0f76b64acb8d736fb7687bc5e4aec40f7ff9e \ - --hash=sha256:7d13e14434f0555fbeda20764d34be4dc94f53ae7040a337928d7bfe42d3422a \ - --hash=sha256:819a27ff19c146a64853a86ebab02bc6bbf28fb9fcf119e93cfc855ec6314f72 \ - --hash=sha256:981bd5b8581e959814a0e51982be1a964945fb7f599cfe7857a63cbc1ad97d58 \ - --hash=sha256:a4f590fc62021d2583afdc88a2eb5ec878699786585f9ca82edf1fc32557ac96 \ - --hash=sha256:a68fb9d807c1cb6f36e13c24731c496b1e98b9c8810125072f19bd56baa6a4d5 \ - --hash=sha256:b3e990dea0ad84fa2f89462215e34960579dbb9fef4d880aecb09f50cc18e713 \ - --hash=sha256:b9dd7da8b18bffac6b09c75b4a4a645232994d91fc0cdfe39baffdc08fa5d252 \ - --hash=sha256:c16e05b36e081d0ea6aee499346f65466811f63f5837f0d09de866624cb7355b \ - --hash=sha256:c8cfd085b74b3f673378109d8d0734d0cab460e3e78c09f691120ac0c2698aad \ - --hash=sha256:cd2b0fb19ce3e71a177ed3b965ce2d4334ffb6aad8f621c124de6dafa09b7758 \ - --hash=sha256:d48077c7b612b149e7db5351459bf96a0950e84ebcd5b7b953bf46b3dcf1ac28 \ - --hash=sha256:df9520fd7fab0761d5698dd36eabcfb18b5eb970ddf01e10eb30b764e4bb72af \ - --hash=sha256:e0a42b6f3c26b2725b1edb1d4cfb98e5640bb0638813080ba1aa0daf23b2fd05 \ - --hash=sha256:ecf1ce515e72d59b78c909aa142d2369cc3c6d771ac7c9162831c0b62a869c8f \ - --hash=sha256:f2d48fa7f8f8ea314405ad49a6b7f3dcf8f12c2024e319e2d47ede0937f8b39b \ - --hash=sha256:f41b359d6de6c9d99ab7da94f115c5aea4d21e74a252bef7c8debec8f20f0b9f \ - --hash=sha256:f62fb1e2ba15f22ff5569c6d364dbf9dfe8b3722a114e6bc1df3cc10f8dd5044 \ - --hash=sha256:fd4715b4015b10ad3cf02e5eb9abb57fe4bfa4d3378f9076b76d60c1ddfa46d6 -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 -rich==14.2.0 \ - --hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \ - --hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd -rpds-py==0.30.0 \ - --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ - --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ - --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ - --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ - --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ - --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ - --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ - --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ - --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ - --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ - --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ - --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ - --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ - --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ - --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ - --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ - --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ - --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ - --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ - --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ - --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ - --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ - --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ - --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ - --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ - --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ - --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ - --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ - --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ - --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ - --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ - --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ - --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ - --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ - --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ - --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ - --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ - --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ - --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ - --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ - --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ - --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ - --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ - --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ - --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ - --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ - --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ - --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ - --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ - --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ - --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ - --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ - --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ - --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ - --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ - --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ - --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ - --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ - --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ - --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ - --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ - --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ - --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ - --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ - --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ - --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ - --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ - --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ - --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ - --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ - --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ - --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ - --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ - --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ - --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ - --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ - --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ - --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ - --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ - --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ - --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ - --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ - --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ - --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ - --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ - --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ - --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ - --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ - --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ - --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ - --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ - --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ - --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ - --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ - --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ - --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ - --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ - --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ - --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ - --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ - --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ - --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ - --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ - --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ - --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ - --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ - --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ - --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ - --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ - --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ - --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ - --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ - --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ - --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ - --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 -safetensors==0.7.0 \ - --hash=sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2 \ - --hash=sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0 \ - --hash=sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd \ - --hash=sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981 \ - --hash=sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a \ - --hash=sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3 \ - --hash=sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d \ - --hash=sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0 \ - --hash=sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85 \ - --hash=sha256:6999421eb8ba9df4450a16d9184fcb7bef26240b9f98e95401f17af6c2210b71 \ - --hash=sha256:7b95a3fa7b3abb9b5b0e07668e808364d0d40f6bbbf9ae0faa8b5b210c97b140 \ - --hash=sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104 \ - --hash=sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57 \ - --hash=sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4 \ - --hash=sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba \ - --hash=sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517 \ - --hash=sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b \ - --hash=sha256:cfdead2f57330d76aa7234051dadfa7d4eedc0e5a27fd08e6f96714a92b00f09 \ - --hash=sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755 \ - --hash=sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48 \ - --hash=sha256:dc92bc2db7b45bda4510e4f51c59b00fe80b2d6be88928346e4294ce1c2abe7c \ - --hash=sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542 \ - --hash=sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737 -scikit-learn==1.7.2 \ - --hash=sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1 \ - --hash=sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7 \ - --hash=sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c \ - --hash=sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda \ - --hash=sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a \ - --hash=sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c \ - --hash=sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18 \ - --hash=sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f \ - --hash=sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973 \ - --hash=sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290 \ - --hash=sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c \ - --hash=sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f \ - --hash=sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0 \ - --hash=sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8 \ - --hash=sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d \ - --hash=sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96 \ - --hash=sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1 \ - --hash=sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106 \ - --hash=sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61 \ - --hash=sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c \ - --hash=sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8 \ - --hash=sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1 \ - --hash=sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe \ - --hash=sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476 \ - --hash=sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44 \ - --hash=sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8 \ - --hash=sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e \ - --hash=sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5 \ - --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ - --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ - --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 -scipy==1.15.3 \ - --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ - --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ - --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ - --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ - --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ - --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ - --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ - --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ - --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ - --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ - --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ - --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ - --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ - --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ - --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ - --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ - --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ - --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ - --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ - --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ - --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ - --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ - --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ - --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ - --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ - --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ - --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ - --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ - --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ - --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ - --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ - --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ - --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ - --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ - --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ - --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ - --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ - --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ - --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ - --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ - --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ - --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ - --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ - --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ - --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ - --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 -six==1.17.0 \ - --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ - --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 -sympy==1.14.0 \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f -threadpoolctl==3.6.0 \ - --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ - --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e -torch==2.10.0+cpu \ - --hash=sha256:0e286fcf6ce0cc7b204396c9b4ea0d375f1f0c3e752f68ce3d3aeb265511db8c \ - --hash=sha256:0fedcb1a77e8f2aaf7bfd21591bf6d1e0b207473268c9be16b17cb7783253969 \ - --hash=sha256:106dd1930cb30a4a337366ba3f9b25318ebf940f51fd46f789281dd9e736bdc4 \ - --hash=sha256:170a0623108055be5199370335cf9b41ba6875b3cb6f086db4aee583331a4899 \ - --hash=sha256:179451716487f8cb09b56459667fa1f5c4c0946c1e75fbeae77cfc40a5768d87 \ - --hash=sha256:17a09465bab2aab8f0f273410297133d8d8fb6dd84dccbd252ca4a4f3a111847 \ - --hash=sha256:1cfcb9b1558c6e52dffd0d4effce83b13c5ae5d97338164c372048c21f9cfccb \ - --hash=sha256:21cb5436978ef47c823b7a813ff0f8c2892e266cfe0f1d944879b5fba81bf4e1 \ - --hash=sha256:23882f8d882460aca809882fc42f5e343bf07585274f929ced00177d1be1eb67 \ - --hash=sha256:2adc71fe471e98a608723bfc837f7e1929885ebb912c693597711e139c1cda41 \ - --hash=sha256:31ae44836c8b9bbd1a3943d29c7c7457709ddf7c6173aa34aefe9d2203e4c405 \ - --hash=sha256:3eaa727e6a73affa61564d86b9d03191df45c8650d0666bd3d57c8597ef61e78 \ - --hash=sha256:4fcd8b4cc2ae20f2b7749fb275349c55432393868778c2d50a08e81d5ee5591e \ - --hash=sha256:5af75e5f49de21b0bdf7672bc27139bd285f9e8dbcabe2d617a2eb656514ac36 \ - --hash=sha256:6c6f0df770144907092a0d067048d96ed4f278a6c840376d2ff0e27e7579b925 \ - --hash=sha256:8d316e5bf121f1eab1147e49ad0511a9d92e4c45cc357d1ab0bee440da71a095 \ - --hash=sha256:8de5a36371b775e2d4881ed12cc7f2de400b1ad3d728aa74a281f649f87c9b8c \ - --hash=sha256:9412bd37b70f5ebd1205242c4ba4cabae35a605947f2b30806d5c9b467936db9 \ - --hash=sha256:9accc30b56cb6756d4a9d04fcb8ebc0bb68c7d55c1ed31a8657397d316d31596 \ - --hash=sha256:a280ffaea7b9c828e0c1b9b3bd502d9b6a649dc9416997b69b84544bd469f215 \ - --hash=sha256:a28fdbcfa2fbacffec81300f24dd1bed2b0ccfdbed107a823cff12bc1db070f6 \ - --hash=sha256:aada8afc068add586464b2a55adb7cc9091eec55caf5320447204741cb6a0604 \ - --hash=sha256:b67d91326e4ed9eccbd6b7d84ed7ffa43f93103aa3f0b24145f3001f3b11b714 \ - --hash=sha256:b719da5af01b59126ac13eefd6ba3dd12d002dc0e8e79b8b365e55267a8189d3 \ - --hash=sha256:b7cb1ec66cefb90fd7b676eac72cfda3b8d4e4d0cacd7a531963bc2e0a9710ab \ - --hash=sha256:ba51ef01a510baf8fff576174f702c47e1aa54389a9f1fba323bb1a5003ff0bf \ - --hash=sha256:beadc2a6a1785b09a46daad378de91ef274b8d3eea7af0bc2d017d97f115afdf \ - --hash=sha256:c35c0de592941d4944698dbfa87271ab85d3370eca3b694943a2ab307ac34b3f \ - --hash=sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa \ - --hash=sha256:d63ee6a80982fd73fe44bb70d97d2976e010312ff6db81d7bfb9167b06dd45b9 \ - --hash=sha256:e51994492cdb76edce29da88de3672a3022f9ef0ffd90345436948d4992be2c7 \ - --hash=sha256:e71c476517c33e7db69825a9ff46c7f47a723ec4dac5b2481cff4246d1c632be \ - --hash=sha256:ea2bcc9d1fca66974a71d4bf9a502539283f35d61fcab5a799b4e120846f1e02 \ - --hash=sha256:eb1bde1ce198f05c8770017de27e001d404499cf552aaaa014569eff56ca25c0 \ - --hash=sha256:ee40b8a4b4b2cf0670c6fd4f35a7ef23871af956fecb238fbf5da15a72650b1d \ - --hash=sha256:f8294fd2fc6dd8f4435a891a0122307a043b14b21f0dac1bca63c85bfb59e586 \ - --hash=sha256:fd215f3d0f681905c5b56b0630a3d666900a37fcc3ca5b937f95275c66f9fd9c \ - --hash=sha256:ffc8da9a1341092d6a90cb5b1c1a33cd61abf0fb43f0cd88443c27fa372c26ae -torchvision==0.25.0+cpu \ - --hash=sha256:0c2d0da9bc011a0fde1d125af396a8fbe94d99becf9d313764f24ca7657a3448 \ - --hash=sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0 \ - --hash=sha256:3e2ae9981e32a5b9db685659d5c7af0f04b159ff20394650a90124baf6ada51a \ - --hash=sha256:499eae1e535766391b6ee2d1e6e841239c20e2e6d88203a15b8f9f8d60a1f8bd \ - --hash=sha256:4d72a57a8f0b5146e26dac1fbfa2c905280cd04f5fcb23b9c56253506b683aeb \ - --hash=sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35 \ - --hash=sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481 \ - --hash=sha256:73ce04dea64914ff1110008311204c366107d651d3b04faa0dbee77efb7133b7 \ - --hash=sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151 \ - --hash=sha256:7d47d544899fabac52ebe0d4812975608fd7ab79a3d7fb6383275eb667e33f53 \ - --hash=sha256:7f851245a2687743742157988ed9c42c6e312b95bbe6cfcac9e7d0d0c28ae62f \ - --hash=sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77 \ - --hash=sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c \ - --hash=sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3 \ - --hash=sha256:9511339b3b5eb75229e0b5041202e8aed9bef3b1de3a715b9fb319c9e97688fd \ - --hash=sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e \ - --hash=sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba \ - --hash=sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f \ - --hash=sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012 \ - --hash=sha256:fb9f07f6a10f0ac24ac482ae68c6df99110b74a0d80a4c64fddc9753267d8815 \ - --hash=sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630 -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 -tzdata==2025.3 \ - --hash=sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 \ - --hash=sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7 -vdms==0.0.23 \ - --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ - --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 -wrapt==2.0.1 \ - --hash=sha256:09c7476ab884b74dce081ad9bfd07fe5822d8600abade571cb1f66d5fc915af6 \ - --hash=sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590 \ - --hash=sha256:115cae4beed3542e37866469a8a1f2b9ec549b4463572b000611e9946b86e6f6 \ - --hash=sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45 \ - --hash=sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba \ - --hash=sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931 \ - --hash=sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494 \ - --hash=sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c \ - --hash=sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef \ - --hash=sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b \ - --hash=sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa \ - --hash=sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75 \ - --hash=sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad \ - --hash=sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31 \ - --hash=sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747 \ - --hash=sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62 \ - --hash=sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970 \ - --hash=sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841 \ - --hash=sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41 \ - --hash=sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9 \ - --hash=sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1 \ - --hash=sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd \ - --hash=sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b \ - --hash=sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9 \ - --hash=sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf \ - --hash=sha256:49989061a9977a8cbd6d20f2efa813f24bf657c6990a42967019ce779a878dbf \ - --hash=sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c \ - --hash=sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f \ - --hash=sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca \ - --hash=sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb \ - --hash=sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1 \ - --hash=sha256:50844efc8cdf63b2d90cd3d62d4947a28311e6266ce5235a219d21b195b4ec2c \ - --hash=sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8 \ - --hash=sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e \ - --hash=sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1 \ - --hash=sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395 \ - --hash=sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9 \ - --hash=sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd \ - --hash=sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3 \ - --hash=sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097 \ - --hash=sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef \ - --hash=sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b \ - --hash=sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509 \ - --hash=sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf \ - --hash=sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10 \ - --hash=sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16 \ - --hash=sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4 \ - --hash=sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf \ - --hash=sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92 \ - --hash=sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6 \ - --hash=sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e \ - --hash=sha256:89a82053b193837bf93c0f8a57ded6e4b6d88033a499dadff5067e912c2a41e9 \ - --hash=sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013 \ - --hash=sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38 \ - --hash=sha256:90897ea1cf0679763b62e79657958cd54eae5659f6360fc7d2ccc6f906342183 \ - --hash=sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d \ - --hash=sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374 \ - --hash=sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b \ - --hash=sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349 \ - --hash=sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db \ - --hash=sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f \ - --hash=sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3 \ - --hash=sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb \ - --hash=sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3 \ - --hash=sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308 \ - --hash=sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489 \ - --hash=sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55 \ - --hash=sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b \ - --hash=sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c \ - --hash=sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790 \ - --hash=sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684 \ - --hash=sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c \ - --hash=sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728 \ - --hash=sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0 \ - --hash=sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f \ - --hash=sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7 \ - --hash=sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e \ - --hash=sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed \ - --hash=sha256:c4012a2bd37059d04f8209916aa771dfb564cccb86079072bdcd48a308b6a5c5 \ - --hash=sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa \ - --hash=sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c \ - --hash=sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9 \ - --hash=sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233 \ - --hash=sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f \ - --hash=sha256:d1a8a09a004ef100e614beec82862d11fc17d601092c3599afd22b1f36e4137e \ - --hash=sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7 \ - --hash=sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b \ - --hash=sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0 \ - --hash=sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28 \ - --hash=sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815 \ - --hash=sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7 \ - --hash=sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3 \ - --hash=sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2 \ - --hash=sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1 \ - --hash=sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c \ - --hash=sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218 \ - --hash=sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c \ - --hash=sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7 \ - --hash=sha256:f26f8e2ca19564e2e1fdbb6a0e47f36e0efbab1acc31e15471fad88f828c75f6 \ - --hash=sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25 \ - --hash=sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981 \ - --hash=sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec \ - --hash=sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2 \ - --hash=sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333 \ - --hash=sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be \ - --hash=sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b \ - --hash=sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f diff --git a/video/requirements.GPU.txt b/video/requirements.GPU.txt deleted file mode 100644 index c0322c5..0000000 --- a/video/requirements.GPU.txt +++ /dev/null @@ -1,562 +0,0 @@ -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -coloredlogs==15.0.1 \ - --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \ - --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0 -cuda-bindings==12.9.4 \ - --hash=sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5 \ - --hash=sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f \ - --hash=sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f \ - --hash=sha256:37744e721a18a514423e81863f52a4f7f46f5a6f9cccd569f2735f8067f4d8c2 \ - --hash=sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d \ - --hash=sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913 \ - --hash=sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5 \ - --hash=sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575 \ - --hash=sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43 \ - --hash=sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb \ - --hash=sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56 \ - --hash=sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615 \ - --hash=sha256:893ca68114b5b769c1d4c02583b91ed22691887c3ed513b59467d23540104db4 \ - --hash=sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686 \ - --hash=sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa \ - --hash=sha256:a022c96b8bd847e8dc0675523431149a4c3e872f440e3002213dbb9e08f0331a \ - --hash=sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64 \ - --hash=sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306 \ - --hash=sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c \ - --hash=sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9 \ - --hash=sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5 \ - --hash=sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee \ - --hash=sha256:f69107389e6b9948969bfd0a20c4f571fd1aefcfb1d2e1b72cc8ba5ecb7918ab \ - --hash=sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8 -cuda-pathfinder==1.5.1 \ - --hash=sha256:b3718097fb57cf9e8a904dd072d806f2c9a27627e35c020b06ab9454bcec08c0 -cuda-toolkit[cudart]==12.8.1 \ - --hash=sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba -filelock==3.25.2 \ - --hash=sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694 \ - --hash=sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70 -flatbuffers==25.12.19 \ - --hash=sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4 -fsspec==2026.3.0 \ - --hash=sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41 \ - --hash=sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4 -humanfriendly==10.0 \ - --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ - --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 -ml-dtypes==0.5.4 \ - --hash=sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf \ - --hash=sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d \ - --hash=sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f \ - --hash=sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483 \ - --hash=sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7 \ - --hash=sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22 \ - --hash=sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6 \ - --hash=sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175 \ - --hash=sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270 \ - --hash=sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1 \ - --hash=sha256:3d277bf3637f2a62176f4575512e9ff9ef51d00e39626d9fe4a161992f355af2 \ - --hash=sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1 \ - --hash=sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2 \ - --hash=sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298 \ - --hash=sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d \ - --hash=sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de \ - --hash=sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049 \ - --hash=sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d \ - --hash=sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90 \ - --hash=sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb \ - --hash=sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465 \ - --hash=sha256:88c982aac7cb1cbe8cbb4e7f253072b1df872701fcaf48d84ffbb433b6568f24 \ - --hash=sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453 \ - --hash=sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56 \ - --hash=sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48 \ - --hash=sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff \ - --hash=sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460 \ - --hash=sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac \ - --hash=sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900 \ - --hash=sha256:a9b61c19040397970d18d7737375cffd83b1f36a11dd4ad19f83a016f736c3ef \ - --hash=sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a \ - --hash=sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c \ - --hash=sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040 \ - --hash=sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9 \ - --hash=sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7 \ - --hash=sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6 \ - --hash=sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b \ - --hash=sha256:d81fdb088defa30eb37bf390bb7dde35d3a83ec112ac8e33d75ab28cc29dd8b0 \ - --hash=sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328 -mpmath==1.3.0 \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c -networkx==3.4.2 \ - --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ - --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f -numpy==2.2.6 \ - --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ - --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ - --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ - --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ - --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ - --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ - --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ - --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ - --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ - --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ - --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ - --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ - --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ - --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ - --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ - --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ - --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ - --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ - --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ - --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ - --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ - --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ - --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ - --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ - --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ - --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ - --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ - --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ - --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ - --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ - --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ - --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ - --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ - --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ - --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ - --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ - --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ - --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ - --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ - --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ - --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ - --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ - --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ - --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ - --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ - --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ - --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ - --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ - --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ - --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ - --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ - --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ - --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ - --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ - --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 -nvidia-cublas-cu12==12.8.4.1 \ - --hash=sha256:47e9b82132fa8d2b4944e708049229601448aaad7e6f296f630f2d1a32de35af \ - --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142 \ - --hash=sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0 -nvidia-cuda-cupti-cu12==12.8.90 \ - --hash=sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed \ - --hash=sha256:bb479dcdf7e6d4f8b0b01b115260399bf34154a1a2e9fe11c85c517d87efd98e \ - --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182 -nvidia-cuda-nvrtc-cu12==12.8.93 \ - --hash=sha256:7a4b6b2904850fe78e0bd179c4b655c404d4bb799ef03ddc60804247099ae909 \ - --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994 \ - --hash=sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8 -nvidia-cuda-runtime-cu12==12.8.90 \ - --hash=sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d \ - --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90 \ - --hash=sha256:c0c6027f01505bfed6c3b21ec546f69c687689aad5f1a377554bc6ca4aa993a8 -nvidia-cudnn-cu12==9.10.2.21 \ - --hash=sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8 \ - --hash=sha256:c6288de7d63e6cf62988f0923f96dc339cea362decb1bf5b3141883392a7d65e \ - --hash=sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8 -nvidia-cufft-cu12==11.3.3.83 \ - --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74 \ - --hash=sha256:7a64a98ef2a7c47f905aaf8931b69a3a43f27c55530c698bb2ed7c75c0b42cb7 \ - --hash=sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a -nvidia-cufile-cu12==1.13.1.3 \ - --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc \ - --hash=sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a -nvidia-curand-cu12==10.3.9.90 \ - --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9 \ - --hash=sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd \ - --hash=sha256:f149a8ca457277da854f89cf282d6ef43176861926c7ac85b2a0fbd237c587ec -nvidia-cusolver-cu12==11.7.3.90 \ - --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450 \ - --hash=sha256:4a550db115fcabc4d495eb7d39ac8b58d4ab5d8e63274d3754df1c0ad6a22d34 \ - --hash=sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0 -nvidia-cusparse-cu12==12.5.8.93 \ - --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b \ - --hash=sha256:9a33604331cb2cac199f2e7f5104dfbb8a5a898c367a53dfda9ff2acb6b6b4dd \ - --hash=sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc -nvidia-cusparselt-cu12==0.7.1 \ - --hash=sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5 \ - --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623 \ - --hash=sha256:f67fbb5831940ec829c9117b7f33807db9f9678dc2a617fbe781cac17b4e1075 -nvidia-nccl-cu12==2.27.5 \ - --hash=sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a \ - --hash=sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457 -nvidia-nvjitlink-cu12==12.8.93 \ - --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88 \ - --hash=sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7 \ - --hash=sha256:bd93fbeeee850917903583587f4fc3a4eafa022e34572251368238ab5e6bd67f -nvidia-nvshmem-cu12==3.4.5 \ - --hash=sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd \ - --hash=sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15 -nvidia-nvtx-cu12==12.8.90 \ - --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f \ - --hash=sha256:619c8304aedc69f02ea82dd244541a83c3d9d40993381b3b590f1adaed3db41e \ - --hash=sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615 -onnx==1.21.0 \ - --hash=sha256:10c3185a232089335581fabb98fba4e86d3e8246b8140f2e406082438100ebda \ - --hash=sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60 \ - --hash=sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd \ - --hash=sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f \ - --hash=sha256:2aca19949260875c14866fc77ea0bc37e4e809b24976108762843d328c92d3ce \ - --hash=sha256:3abd09872523c7e0362d767e4e63bd7c6bac52a5e2c3edbf061061fe540e2027 \ - --hash=sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f \ - --hash=sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764 \ - --hash=sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9 \ - --hash=sha256:5f78c411743db317a76e5d009f84f7e3d5380411a1567a868e82461a1e5c775d \ - --hash=sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840 \ - --hash=sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8 \ - --hash=sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad \ - --hash=sha256:82aa6ab51144df07c58c4850cb78d4f1ae969d8c0bf657b28041796d49ba6974 \ - --hash=sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b \ - --hash=sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e \ - --hash=sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285 \ - --hash=sha256:ab6a488dabbb172eebc9f3b3e7ac68763f32b0c571626d4a5004608f866cc83d \ - --hash=sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad \ - --hash=sha256:c9b56ad04039fac6b028c07e54afa1ec7f75dd340f65311f2c292e41ed7aa4d9 \ - --hash=sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb \ - --hash=sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8 \ - --hash=sha256:e0c21cc5c7a41d1a509828e2b14fe9c30e807c6df611ec0fd64a47b8d4b16abd \ - --hash=sha256:e1931bfcc222a4c9da6475f2ffffb84b97ab3876041ec639171c11ce802bee6a \ - --hash=sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9 \ - --hash=sha256:f2c7c234c568402e10db74e33d787e4144e394ae2bcbbf11000fbfe2e017ad68 \ - --hash=sha256:f53b3c15a3b539c16b99655c43c365622046d68c49b680c48eba4da2a4fb6f27 \ - --hash=sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095 -onnxruntime-gpu==1.23.2 \ - --hash=sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4 \ - --hash=sha256:18de50c6c8eea50acc405ea13d299aec593e46478d7a22cd32cdbbdf7c42899d \ - --hash=sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26 \ - --hash=sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f \ - --hash=sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2 \ - --hash=sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767 \ - --hash=sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59 \ - --hash=sha256:deba091e15357355aa836fd64c6c4ac97dd0c4609c38b08a69675073ea46b321 \ - --hash=sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95 -onnxslim==0.1.82 \ - --hash=sha256:3190340f53c93620779f2159b41d114e571b7c1a0cfa8630cba3f7be92d3399e \ - --hash=sha256:4f48decf32863e583976fff6e9cfd9d6fe6a4a9814e7577c2cf8ce082973c6eb -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 -pillow==12.2.0 \ - --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ - --hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \ - --hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \ - --hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \ - --hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \ - --hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \ - --hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \ - --hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \ - --hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \ - --hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \ - --hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \ - --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \ - --hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \ - --hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \ - --hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \ - --hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \ - --hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \ - --hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \ - --hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \ - --hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \ - --hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \ - --hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \ - --hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \ - --hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \ - --hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \ - --hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \ - --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \ - --hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \ - --hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \ - --hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \ - --hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \ - --hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \ - --hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \ - --hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \ - --hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \ - --hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \ - --hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \ - --hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \ - --hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \ - --hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \ - --hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \ - --hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \ - --hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \ - --hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \ - --hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \ - --hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \ - --hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \ - --hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \ - --hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \ - --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \ - --hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \ - --hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \ - --hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \ - --hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \ - --hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \ - --hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \ - --hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \ - --hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \ - --hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \ - --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \ - --hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \ - --hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \ - --hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \ - --hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \ - --hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \ - --hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \ - --hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \ - --hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \ - --hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \ - --hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \ - --hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \ - --hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \ - --hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \ - --hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \ - --hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \ - --hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \ - --hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \ - --hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \ - --hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \ - --hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \ - --hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \ - --hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \ - --hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \ - --hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \ - --hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \ - --hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \ - --hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \ - --hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \ - --hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \ - --hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \ - --hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5 -protobuf==5.29.6 \ - --hash=sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05 \ - --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ - --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ - --hash=sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18 \ - --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ - --hash=sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6 \ - --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ - --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ - --hash=sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49 \ - --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ - --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 -sympy==1.14.0 \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 -tensorrt-cu12==10.14.1.48.post1 \ - --hash=sha256:5a6d4d78560be7c8fff877711fa8334e8e2b441b702f047ea3107311b9897341 -tensorrt-cu12-bindings==10.14.1.48.post1 \ - --hash=sha256:03bd44f645a30e04f38d4a866bdfb0e9e16a34601384ce29ef8d008950175828 \ - --hash=sha256:1cc29c5bb32a0719d5fadbbd2e69971837b2b0d2f1575d9a2ddc1cf3f6f5d8f0 \ - --hash=sha256:2a0b5a301c84d1c95e67cede52bb49f44fda02a45c6a4b409fa16f0632394046 \ - --hash=sha256:2b83b1608e21c25da72776501533b17fe1d000595b9a191613665f67e0598868 \ - --hash=sha256:40100265c49dc91e0a3f0a030e7de0077c034e6e30c11828001b036052de1d77 \ - --hash=sha256:68ae05d23f4918fdd36a505daf8b93b64444ef9328516284363480ab776a2595 \ - --hash=sha256:78da2abb803370147e75045eaeaa2a3f134f5aa7537405f86b22eaa36c0a11ed \ - --hash=sha256:9b2c8f41d847b202e35054fbaccd55f4efbb673da11f562befc273c0b2d65f48 \ - --hash=sha256:aad15bf393acc85b2e5015f0ca2082b0d162d5966ac1f58de48d1205446e237f \ - --hash=sha256:b2bf5597c7790c36fa858b8dfd6a867a482ee41d7c35e8732b3fd671b013869c \ - --hash=sha256:b90ce26abe1d49da527211411d023f95a235806fab2d00277585558e265f9b93 \ - --hash=sha256:d9cb40e646e11225b295eaeaf74aeb7e422c425271d51ca8c416776449fec617 -tensorrt-cu12-libs==10.14.1.48.post1 \ - --hash=sha256:46e9e84e16ca7d89ca572e0900d9480945bb6faaa0c385e6f63e1ae46a834b25 -torch==2.10.0 \ - --hash=sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591 \ - --hash=sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574 \ - --hash=sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60 \ - --hash=sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd \ - --hash=sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313 \ - --hash=sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547 \ - --hash=sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382 \ - --hash=sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4 \ - --hash=sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d \ - --hash=sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f \ - --hash=sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49 \ - --hash=sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d \ - --hash=sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b \ - --hash=sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf \ - --hash=sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f \ - --hash=sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c \ - --hash=sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5 \ - --hash=sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738 \ - --hash=sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294 \ - --hash=sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185 \ - --hash=sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63 \ - --hash=sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb \ - --hash=sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b \ - --hash=sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6 \ - --hash=sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321 \ - --hash=sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763 \ - --hash=sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb \ - --hash=sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8 \ - --hash=sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444 \ - --hash=sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac \ - --hash=sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328 \ - --hash=sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b \ - --hash=sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a \ - --hash=sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57 \ - --hash=sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f \ - --hash=sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6 \ - --hash=sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e \ - --hash=sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28 \ - --hash=sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8 -torchvision==0.25.0 \ - --hash=sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce \ - --hash=sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6 \ - --hash=sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266 \ - --hash=sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee \ - --hash=sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec \ - --hash=sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563 \ - --hash=sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c \ - --hash=sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef \ - --hash=sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1 \ - --hash=sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03 \ - --hash=sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443 \ - --hash=sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52 \ - --hash=sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7 \ - --hash=sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56 \ - --hash=sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4 \ - --hash=sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2 \ - --hash=sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264 \ - --hash=sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233 \ - --hash=sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b \ - --hash=sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa \ - --hash=sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f \ - --hash=sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917 \ - --hash=sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20 \ - --hash=sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7 \ - --hash=sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977 \ - --hash=sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248 \ - --hash=sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3 \ - --hash=sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3 -triton==3.6.0 \ - --hash=sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d \ - --hash=sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9 \ - --hash=sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6 \ - --hash=sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4 \ - --hash=sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd \ - --hash=sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7 \ - --hash=sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651 \ - --hash=sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781 \ - --hash=sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca \ - --hash=sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803 \ - --hash=sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea \ - --hash=sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f \ - --hash=sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3 \ - --hash=sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43 -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 -vdms==0.0.23 \ - --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ - --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 - -# The following packages are considered to be unsafe in a requirements file: -pip==26.0.1 \ - --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ - --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 diff --git a/video/requirements.txt b/video/requirements.txt index 8784d50..5bb247f 100644 --- a/video/requirements.txt +++ b/video/requirements.txt @@ -1,740 +1,151 @@ build==1.4.0 \ --hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \ --hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936 -certifi==2026.1.4 \ - --hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \ - --hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120 -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 -contourpy==1.3.2 \ - --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ - --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ - --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ - --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ - --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ - --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ - --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ - --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ - --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ - --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ - --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ - --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ - --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ - --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ - --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ - --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ - --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ - --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ - --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ - --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ - --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ - --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ - --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ - --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ - --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ - --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ - --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ - --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ - --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ - --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ - --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ - --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ - --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ - --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ - --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ - --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ - --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ - --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ - --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ - --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ - --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ - --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ - --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ - --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ - --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ - --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ - --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ - --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ - --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ - --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ - --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ - --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ - --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ - --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ - --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ - --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ - --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c -cycler==0.12.1 \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c -defusedxml==0.7.1 \ - --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ - --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 -filelock==3.20.3 \ - --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ - --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 -fonttools==4.61.1 \ - --hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \ - --hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \ - --hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \ - --hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \ - --hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \ - --hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \ - --hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \ - --hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \ - --hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \ - --hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \ - --hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \ - --hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \ - --hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \ - --hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \ - --hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \ - --hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \ - --hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \ - --hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \ - --hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \ - --hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \ - --hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \ - --hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \ - --hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \ - --hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \ - --hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \ - --hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \ - --hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \ - --hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \ - --hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \ - --hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \ - --hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \ - --hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \ - --hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \ - --hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \ - --hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \ - --hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \ - --hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \ - --hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \ - --hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \ - --hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \ - --hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \ - --hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \ - --hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \ - --hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \ - --hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \ - --hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \ - --hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \ - --hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \ - --hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \ - --hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd -fsspec==2026.1.0 \ - --hash=sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc \ - --hash=sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b +certifi==2026.2.25 \ + --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ + --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +charset-normalizer==3.4.6 \ + --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ + --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ + --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ + --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ + --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ + --hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \ + --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ + --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ + --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ + --hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \ + --hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \ + --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ + --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ + --hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \ + --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ + --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ + --hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \ + --hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \ + --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ + --hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \ + --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ + --hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \ + --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ + --hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \ + --hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \ + --hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \ + --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ + --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ + --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ + --hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \ + --hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \ + --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ + --hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \ + --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ + --hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \ + --hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \ + --hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \ + --hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \ + --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ + --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ + --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ + --hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \ + --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ + --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ + --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ + --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ + --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ + --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ + --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ + --hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \ + --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ + --hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \ + --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ + --hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \ + --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ + --hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \ + --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ + --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ + --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ + --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ + --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ + --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ + --hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \ + --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ + --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ + --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ + --hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \ + --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ + --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ + --hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \ + --hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \ + --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ + --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ + --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ + --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ + --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ + --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ + --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ + --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ + --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ + --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ + --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ + --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ + --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ + --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ + --hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \ + --hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \ + --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ + --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ + --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ + --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ + --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ + --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ + --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ + --hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \ + --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ + --hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \ + --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ + --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ + --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ + --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ + --hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \ + --hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \ + --hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \ + --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ + --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ + --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ + --hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \ + --hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \ + --hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \ + --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ + --hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \ + --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ + --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ + --hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \ + --hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \ + --hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \ + --hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \ + --hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \ + --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ + --hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \ + --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ + --hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \ + --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ + --hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \ + --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ + --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ + --hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \ + --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 inotify==0.2.12 \ --hash=sha256:9aee407f92c7d51a2ce50f3b78291a9094e334e34bd68e82bf60020795fa2c94 \ --hash=sha256:e4f1c8ec7ba5ec2a1a7fce48c0c917234af9d756495ebae7ffa00e41a305ab90 -jinja2==3.1.6 \ - --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ - --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 -kiwisolver==1.4.9 \ - --hash=sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c \ - --hash=sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7 \ - --hash=sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21 \ - --hash=sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e \ - --hash=sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff \ - --hash=sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7 \ - --hash=sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c \ - --hash=sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26 \ - --hash=sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa \ - --hash=sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f \ - --hash=sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1 \ - --hash=sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891 \ - --hash=sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77 \ - --hash=sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543 \ - --hash=sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d \ - --hash=sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce \ - --hash=sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3 \ - --hash=sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60 \ - --hash=sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a \ - --hash=sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089 \ - --hash=sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab \ - --hash=sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78 \ - --hash=sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771 \ - --hash=sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f \ - --hash=sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b \ - --hash=sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14 \ - --hash=sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32 \ - --hash=sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527 \ - --hash=sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185 \ - --hash=sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634 \ - --hash=sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed \ - --hash=sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1 \ - --hash=sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c \ - --hash=sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11 \ - --hash=sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752 \ - --hash=sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5 \ - --hash=sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4 \ - --hash=sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58 \ - --hash=sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5 \ - --hash=sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198 \ - --hash=sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536 \ - --hash=sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134 \ - --hash=sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf \ - --hash=sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2 \ - --hash=sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2 \ - --hash=sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370 \ - --hash=sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1 \ - --hash=sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154 \ - --hash=sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b \ - --hash=sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197 \ - --hash=sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386 \ - --hash=sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a \ - --hash=sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48 \ - --hash=sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748 \ - --hash=sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c \ - --hash=sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8 \ - --hash=sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5 \ - --hash=sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999 \ - --hash=sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369 \ - --hash=sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122 \ - --hash=sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b \ - --hash=sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 \ - --hash=sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9 \ - --hash=sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f \ - --hash=sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799 \ - --hash=sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028 \ - --hash=sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2 \ - --hash=sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525 \ - --hash=sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d \ - --hash=sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb \ - --hash=sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872 \ - --hash=sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64 \ - --hash=sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586 \ - --hash=sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf \ - --hash=sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552 \ - --hash=sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2 \ - --hash=sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415 \ - --hash=sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c \ - --hash=sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6 \ - --hash=sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64 \ - --hash=sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d \ - --hash=sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548 \ - --hash=sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07 \ - --hash=sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61 \ - --hash=sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d \ - --hash=sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771 \ - --hash=sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9 \ - --hash=sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c \ - --hash=sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3 \ - --hash=sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16 \ - --hash=sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145 \ - --hash=sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611 \ - --hash=sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2 \ - --hash=sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464 \ - --hash=sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2 \ - --hash=sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04 \ - --hash=sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54 \ - --hash=sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df \ - --hash=sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f \ - --hash=sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1 \ - --hash=sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220 -markupsafe==3.0.3 \ - --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ - --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ - --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ - --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ - --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ - --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ - --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ - --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ - --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ - --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ - --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ - --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ - --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ - --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ - --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ - --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ - --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ - --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ - --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ - --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ - --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ - --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ - --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ - --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ - --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ - --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ - --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ - --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ - --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ - --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ - --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ - --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ - --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ - --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ - --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ - --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ - --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ - --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ - --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ - --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ - --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ - --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ - --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ - --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ - --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ - --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ - --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ - --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ - --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ - --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ - --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ - --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ - --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ - --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ - --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ - --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ - --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ - --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ - --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ - --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ - --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ - --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ - --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ - --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ - --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ - --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ - --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ - --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ - --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ - --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ - --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ - --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ - --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ - --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ - --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ - --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ - --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ - --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ - --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ - --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ - --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ - --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ - --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ - --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ - --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ - --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ - --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ - --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ - --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 -matplotlib==3.10.8 \ - --hash=sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7 \ - --hash=sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a \ - --hash=sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f \ - --hash=sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3 \ - --hash=sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5 \ - --hash=sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9 \ - --hash=sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2 \ - --hash=sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3 \ - --hash=sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6 \ - --hash=sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f \ - --hash=sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b \ - --hash=sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8 \ - --hash=sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008 \ - --hash=sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b \ - --hash=sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656 \ - --hash=sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958 \ - --hash=sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04 \ - --hash=sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b \ - --hash=sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6 \ - --hash=sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908 \ - --hash=sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c \ - --hash=sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1 \ - --hash=sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d \ - --hash=sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1 \ - --hash=sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c \ - --hash=sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a \ - --hash=sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce \ - --hash=sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a \ - --hash=sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160 \ - --hash=sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1 \ - --hash=sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11 \ - --hash=sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a \ - --hash=sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466 \ - --hash=sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486 \ - --hash=sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78 \ - --hash=sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17 \ - --hash=sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077 \ - --hash=sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565 \ - --hash=sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f \ - --hash=sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50 \ - --hash=sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58 \ - --hash=sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2 \ - --hash=sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645 \ - --hash=sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2 \ - --hash=sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39 \ - --hash=sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf \ - --hash=sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149 \ - --hash=sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22 \ - --hash=sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df \ - --hash=sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4 \ - --hash=sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933 \ - --hash=sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6 \ - --hash=sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8 \ - --hash=sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a \ - --hash=sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7 -mpmath==1.3.0 \ - --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ - --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c -networkx==3.1 \ - --hash=sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36 \ - --hash=sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61 -numpy==1.26.4 \ - --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ - --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ - --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \ - --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \ - --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \ - --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \ - --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \ - --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \ - --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \ - --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \ - --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \ - --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \ - --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \ - --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \ - --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \ - --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \ - --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \ - --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \ - --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \ - --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \ - --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \ - --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \ - --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \ - --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \ - --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \ - --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \ - --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \ - --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \ - --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \ - --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \ - --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \ - --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \ - --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \ - --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \ - --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \ - --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f -opencv-python==4.11.0.86 \ - --hash=sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4 \ - --hash=sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec \ - --hash=sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202 \ - --hash=sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a \ - --hash=sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d \ - --hash=sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b \ - --hash=sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66 -opencv-python-headless==4.11.0.86 \ - --hash=sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b \ - --hash=sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca \ - --hash=sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca \ - --hash=sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb \ - --hash=sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798 \ - --hash=sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81 \ - --hash=sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b -openvino==2024.6.0 \ - --hash=sha256:05a436e2526bf6775b487712ab4a6decf7afe04183109bc7b129b95205ec4247 \ - --hash=sha256:151c6010f0613ace29087bf864dc5dd1db0afa25ab87379c735f85500545b167 \ - --hash=sha256:17cbdae3b069d9a13389b43a15b521a86fef75f350cfd0d992cc1780a6123788 \ - --hash=sha256:1d59ccc2df4a8c8515f30683f6cc0091c5b4e6a01a71ebfb1dc8f2ebdb5c83f4 \ - --hash=sha256:263496653200270b8a17456dc7bc67a198ed29b601b0ad7280d90783ba918d66 \ - --hash=sha256:2acfe89f61ec18d608c79147cedea6ba85d80783723ab9a9416891b74b15785e \ - --hash=sha256:444a6d3a746dd728654877993c30582faefcad2868c80ec261839a144b8925bc \ - --hash=sha256:46d3e20838c918668ec10ae124e92d56ff47a7c14da0d9d19d01d91fc9dece94 \ - --hash=sha256:4a94ef27022d56a159ea8677e5561bdd6f0d6c6237736e13c0f7cde71224ce7f \ - --hash=sha256:89c866fa87ddeccb982252e7d1c8516101a4acb95aaebac7e43f450112cb6942 \ - --hash=sha256:8d0390f4ab9b8f27814cf0feeddb0bba449ae8cd99933c2c2fae3011d33973b3 \ - --hash=sha256:9150c75e19606a2f2ab411271c79031726980fb6870b96445db1faca5ab64f5f \ - --hash=sha256:989c803d1f8e6d12ee8d2e3e8c7d56189d96688433b2238270dbfbf64f163f18 \ - --hash=sha256:ac812efa9c139387db534498a1e980103a35b9b2b5f3b2d1ae6ed43db51b4bba \ - --hash=sha256:c25d6820e960c08c0cc8d7823e9e6e395d762bc603772af14f0a576fab1628eb \ - --hash=sha256:c3477e833541df4f316240491d9df157c20a81487991748eb33e02e534009999 \ - --hash=sha256:c6c6a40a890b51007b20cf97d81783c4b43edfbcd4aabb06b6b04b7f2fb3137f \ - --hash=sha256:cb94f4ca9c1857bda2c5b510e519db20ea246958dbd76ddc38c133e2216231a5 \ - --hash=sha256:f7177d9a734a61a0708bcd706592f59fc2377d8efbb0bf9f8a33edfd2998a45d \ - --hash=sha256:ff5f871162299906fb517d5c9ded79f464a0341d8e2d55a037d4f9e38eed8652 -openvino-dev==2024.6.0 \ - --hash=sha256:b8f4a1baeea1b138bc9b75b53dc3bdad3307c2cd4d58526ad977df81635870c5 -openvino-telemetry==2025.2.0 \ - --hash=sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c \ - --hash=sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599 packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 -pillow==12.2.0 \ - --hash=sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9 \ - --hash=sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5 \ - --hash=sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987 \ - --hash=sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9 \ - --hash=sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b \ - --hash=sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f \ - --hash=sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd \ - --hash=sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e \ - --hash=sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e \ - --hash=sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe \ - --hash=sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795 \ - --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601 \ - --hash=sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1 \ - --hash=sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed \ - --hash=sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea \ - --hash=sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5 \ - --hash=sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97 \ - --hash=sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453 \ - --hash=sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98 \ - --hash=sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa \ - --hash=sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b \ - --hash=sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d \ - --hash=sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705 \ - --hash=sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8 \ - --hash=sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024 \ - --hash=sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0 \ - --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \ - --hash=sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150 \ - --hash=sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2 \ - --hash=sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3 \ - --hash=sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b \ - --hash=sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f \ - --hash=sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463 \ - --hash=sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940 \ - --hash=sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166 \ - --hash=sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed \ - --hash=sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f \ - --hash=sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795 \ - --hash=sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780 \ - --hash=sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7 \ - --hash=sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1 \ - --hash=sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5 \ - --hash=sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295 \ - --hash=sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b \ - --hash=sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354 \ - --hash=sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60 \ - --hash=sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65 \ - --hash=sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005 \ - --hash=sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c \ - --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \ - --hash=sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5 \ - --hash=sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06 \ - --hash=sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae \ - --hash=sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c \ - --hash=sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c \ - --hash=sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612 \ - --hash=sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e \ - --hash=sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab \ - --hash=sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808 \ - --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \ - --hash=sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e \ - --hash=sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909 \ - --hash=sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec \ - --hash=sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe \ - --hash=sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50 \ - --hash=sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4 \ - --hash=sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f \ - --hash=sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff \ - --hash=sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5 \ - --hash=sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb \ - --hash=sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414 \ - --hash=sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1 \ - --hash=sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032 \ - --hash=sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76 \ - --hash=sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 \ - --hash=sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e \ - --hash=sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c \ - --hash=sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3 \ - --hash=sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea \ - --hash=sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f \ - --hash=sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104 \ - --hash=sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176 \ - --hash=sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24 \ - --hash=sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3 \ - --hash=sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4 \ - --hash=sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed \ - --hash=sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43 \ - --hash=sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 \ - --hash=sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7 \ - --hash=sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06 \ - --hash=sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5 -pip==26.0.1 \ - --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ - --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 -polars==1.37.1 \ - --hash=sha256:0309e2a4633e712513401964b4d95452f124ceabf7aec6db50affb9ced4a274e \ - --hash=sha256:377fed8939a2f1223c1563cfabdc7b4a3d6ff846efa1f2ddeb8644fafd9b1aff -polars-runtime-32==1.37.1 \ - --hash=sha256:04f5d5a2f013dca7391b7d8e7672fa6d37573a87f1d45d3dd5f0d9b5565a4b0f \ - --hash=sha256:0b8d4d73ea9977d3731927740e59d814647c5198bdbe359bcf6a8bfce2e79771 \ - --hash=sha256:55f2c4847a8d2e267612f564de7b753a4bde3902eaabe7b436a0a4abf75949a0 \ - --hash=sha256:68779d4a691da20a5eb767d74165a8f80a2bdfbde4b54acf59af43f7fa028d8f \ - --hash=sha256:a8362d11ac5193b994c7e9048ffe22ccfb976699cfbf6e128ce0302e06728894 \ - --hash=sha256:c682bf83f5f352e5e02f5c16c652c48ca40442f07b236f30662b22217320ce76 \ - --hash=sha256:da3d3642ae944e18dd17109d2a3036cb94ce50e5495c5023c77b1599d4c861bc \ - --hash=sha256:fbfde7c0ca8209eeaed546e4a32cca1319189aa61c5f0f9a2b4494262bd0c689 \ - --hash=sha256:fc82b5bbe70ca1a4b764eed1419f6336752d6ba9fc1245388d7f8b12438afa2c -protobuf==5.29.6 \ - --hash=sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05 \ - --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ - --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ - --hash=sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18 \ - --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ - --hash=sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6 \ - --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ - --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ - --hash=sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49 \ - --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ - --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 -psutil==7.2.1 \ - --hash=sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1 \ - --hash=sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266 \ - --hash=sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442 \ - --hash=sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79 \ - --hash=sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf \ - --hash=sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f \ - --hash=sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679 \ - --hash=sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8 \ - --hash=sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49 \ - --hash=sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f \ - --hash=sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129 \ - --hash=sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67 \ - --hash=sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6 \ - --hash=sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17 \ - --hash=sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42 \ - --hash=sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d \ - --hash=sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672 \ - --hash=sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a \ - --hash=sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc \ - --hash=sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3 \ - --hash=sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8 -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -812,123 +223,54 @@ pyyaml==6.0.3 \ requests==2.33.1 \ --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a -scipy==1.15.3 \ - --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ - --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ - --hash=sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723 \ - --hash=sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730 \ - --hash=sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539 \ - --hash=sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb \ - --hash=sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6 \ - --hash=sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594 \ - --hash=sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92 \ - --hash=sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82 \ - --hash=sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49 \ - --hash=sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759 \ - --hash=sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba \ - --hash=sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982 \ - --hash=sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8 \ - --hash=sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65 \ - --hash=sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4 \ - --hash=sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e \ - --hash=sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed \ - --hash=sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c \ - --hash=sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5 \ - --hash=sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5 \ - --hash=sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019 \ - --hash=sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e \ - --hash=sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1 \ - --hash=sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889 \ - --hash=sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca \ - --hash=sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825 \ - --hash=sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9 \ - --hash=sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62 \ - --hash=sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb \ - --hash=sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b \ - --hash=sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13 \ - --hash=sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb \ - --hash=sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40 \ - --hash=sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c \ - --hash=sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253 \ - --hash=sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb \ - --hash=sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f \ - --hash=sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163 \ - --hash=sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45 \ - --hash=sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7 \ - --hash=sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11 \ - --hash=sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf \ - --hash=sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e \ - --hash=sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126 -six==1.17.0 \ - --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ - --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 -sympy==1.14.0 \ - --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ - --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 -tomli==1.1.0 \ - --hash=sha256:33d7984738f8bb699c9b0a816eb646a8178a69eaa792d258486776a5d21b8ca5 \ - --hash=sha256:f4a182048010e89cbec0ae4686b21f550a7f2903f665e34a6de58ec15424f919 -torch==2.10.0+cpu \ - --hash=sha256:0e286fcf6ce0cc7b204396c9b4ea0d375f1f0c3e752f68ce3d3aeb265511db8c \ - --hash=sha256:0fedcb1a77e8f2aaf7bfd21591bf6d1e0b207473268c9be16b17cb7783253969 \ - --hash=sha256:106dd1930cb30a4a337366ba3f9b25318ebf940f51fd46f789281dd9e736bdc4 \ - --hash=sha256:170a0623108055be5199370335cf9b41ba6875b3cb6f086db4aee583331a4899 \ - --hash=sha256:179451716487f8cb09b56459667fa1f5c4c0946c1e75fbeae77cfc40a5768d87 \ - --hash=sha256:17a09465bab2aab8f0f273410297133d8d8fb6dd84dccbd252ca4a4f3a111847 \ - --hash=sha256:1cfcb9b1558c6e52dffd0d4effce83b13c5ae5d97338164c372048c21f9cfccb \ - --hash=sha256:21cb5436978ef47c823b7a813ff0f8c2892e266cfe0f1d944879b5fba81bf4e1 \ - --hash=sha256:23882f8d882460aca809882fc42f5e343bf07585274f929ced00177d1be1eb67 \ - --hash=sha256:2adc71fe471e98a608723bfc837f7e1929885ebb912c693597711e139c1cda41 \ - --hash=sha256:31ae44836c8b9bbd1a3943d29c7c7457709ddf7c6173aa34aefe9d2203e4c405 \ - --hash=sha256:3eaa727e6a73affa61564d86b9d03191df45c8650d0666bd3d57c8597ef61e78 \ - --hash=sha256:4fcd8b4cc2ae20f2b7749fb275349c55432393868778c2d50a08e81d5ee5591e \ - --hash=sha256:5af75e5f49de21b0bdf7672bc27139bd285f9e8dbcabe2d617a2eb656514ac36 \ - --hash=sha256:6c6f0df770144907092a0d067048d96ed4f278a6c840376d2ff0e27e7579b925 \ - --hash=sha256:8d316e5bf121f1eab1147e49ad0511a9d92e4c45cc357d1ab0bee440da71a095 \ - --hash=sha256:8de5a36371b775e2d4881ed12cc7f2de400b1ad3d728aa74a281f649f87c9b8c \ - --hash=sha256:9412bd37b70f5ebd1205242c4ba4cabae35a605947f2b30806d5c9b467936db9 \ - --hash=sha256:9accc30b56cb6756d4a9d04fcb8ebc0bb68c7d55c1ed31a8657397d316d31596 \ - --hash=sha256:a280ffaea7b9c828e0c1b9b3bd502d9b6a649dc9416997b69b84544bd469f215 \ - --hash=sha256:a28fdbcfa2fbacffec81300f24dd1bed2b0ccfdbed107a823cff12bc1db070f6 \ - --hash=sha256:aada8afc068add586464b2a55adb7cc9091eec55caf5320447204741cb6a0604 \ - --hash=sha256:b67d91326e4ed9eccbd6b7d84ed7ffa43f93103aa3f0b24145f3001f3b11b714 \ - --hash=sha256:b719da5af01b59126ac13eefd6ba3dd12d002dc0e8e79b8b365e55267a8189d3 \ - --hash=sha256:b7cb1ec66cefb90fd7b676eac72cfda3b8d4e4d0cacd7a531963bc2e0a9710ab \ - --hash=sha256:ba51ef01a510baf8fff576174f702c47e1aa54389a9f1fba323bb1a5003ff0bf \ - --hash=sha256:beadc2a6a1785b09a46daad378de91ef274b8d3eea7af0bc2d017d97f115afdf \ - --hash=sha256:c35c0de592941d4944698dbfa87271ab85d3370eca3b694943a2ab307ac34b3f \ - --hash=sha256:ce5c113d1f55f8c1f5af05047a24e50d11d293e0cbbb5bf7a75c6c761edd6eaa \ - --hash=sha256:d63ee6a80982fd73fe44bb70d97d2976e010312ff6db81d7bfb9167b06dd45b9 \ - --hash=sha256:e51994492cdb76edce29da88de3672a3022f9ef0ffd90345436948d4992be2c7 \ - --hash=sha256:e71c476517c33e7db69825a9ff46c7f47a723ec4dac5b2481cff4246d1c632be \ - --hash=sha256:ea2bcc9d1fca66974a71d4bf9a502539283f35d61fcab5a799b4e120846f1e02 \ - --hash=sha256:eb1bde1ce198f05c8770017de27e001d404499cf552aaaa014569eff56ca25c0 \ - --hash=sha256:ee40b8a4b4b2cf0670c6fd4f35a7ef23871af956fecb238fbf5da15a72650b1d \ - --hash=sha256:f8294fd2fc6dd8f4435a891a0122307a043b14b21f0dac1bca63c85bfb59e586 \ - --hash=sha256:fd215f3d0f681905c5b56b0630a3d666900a37fcc3ca5b937f95275c66f9fd9c \ - --hash=sha256:ffc8da9a1341092d6a90cb5b1c1a33cd61abf0fb43f0cd88443c27fa372c26ae -torchvision==0.25.0+cpu \ - --hash=sha256:0c2d0da9bc011a0fde1d125af396a8fbe94d99becf9d313764f24ca7657a3448 \ - --hash=sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0 \ - --hash=sha256:3e2ae9981e32a5b9db685659d5c7af0f04b159ff20394650a90124baf6ada51a \ - --hash=sha256:499eae1e535766391b6ee2d1e6e841239c20e2e6d88203a15b8f9f8d60a1f8bd \ - --hash=sha256:4d72a57a8f0b5146e26dac1fbfa2c905280cd04f5fcb23b9c56253506b683aeb \ - --hash=sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35 \ - --hash=sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481 \ - --hash=sha256:73ce04dea64914ff1110008311204c366107d651d3b04faa0dbee77efb7133b7 \ - --hash=sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151 \ - --hash=sha256:7d47d544899fabac52ebe0d4812975608fd7ab79a3d7fb6383275eb667e33f53 \ - --hash=sha256:7f851245a2687743742157988ed9c42c6e312b95bbe6cfcac9e7d0d0c28ae62f \ - --hash=sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77 \ - --hash=sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c \ - --hash=sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3 \ - --hash=sha256:9511339b3b5eb75229e0b5041202e8aed9bef3b1de3a715b9fb319c9e97688fd \ - --hash=sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e \ - --hash=sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba \ - --hash=sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f \ - --hash=sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012 \ - --hash=sha256:fb9f07f6a10f0ac24ac482ae68c6df99110b74a0d80a4c64fddc9753267d8815 \ - --hash=sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630 +tomli==2.4.0 \ + --hash=sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729 \ + --hash=sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b \ + --hash=sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d \ + --hash=sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df \ + --hash=sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576 \ + --hash=sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d \ + --hash=sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1 \ + --hash=sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a \ + --hash=sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e \ + --hash=sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc \ + --hash=sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702 \ + --hash=sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6 \ + --hash=sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd \ + --hash=sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4 \ + --hash=sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776 \ + --hash=sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a \ + --hash=sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66 \ + --hash=sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87 \ + --hash=sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2 \ + --hash=sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f \ + --hash=sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475 \ + --hash=sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f \ + --hash=sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95 \ + --hash=sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9 \ + --hash=sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3 \ + --hash=sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9 \ + --hash=sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76 \ + --hash=sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da \ + --hash=sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8 \ + --hash=sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51 \ + --hash=sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86 \ + --hash=sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8 \ + --hash=sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0 \ + --hash=sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b \ + --hash=sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1 \ + --hash=sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e \ + --hash=sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d \ + --hash=sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c \ + --hash=sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867 \ + --hash=sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a \ + --hash=sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c \ + --hash=sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0 \ + --hash=sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4 \ + --hash=sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614 \ + --hash=sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132 \ + --hash=sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa \ + --hash=sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087 tornado==6.5.5 \ --hash=sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9 \ --hash=sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6 \ @@ -940,21 +282,11 @@ tornado==6.5.5 \ --hash=sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521 \ --hash=sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7 \ --hash=sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5 -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 -ultralytics==8.4.34 \ - --hash=sha256:e932da39976879ddf4f398993bd44587c1f5aafaac138efab38d34030680c113 \ - --hash=sha256:fe94bf945c2c69b4df245ee9327dc3c7ce6d06ec9c647843331b0d497a48db78 -ultralytics-thop==2.0.18 \ - --hash=sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf \ - --hash=sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec urllib3==2.6.3 \ --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 -vdms==0.0.23 \ - --hash=sha256:2dec153f8cb33f27cdb9ab33125198195fd29a6777cf890fc735774ed9327874 \ - --hash=sha256:7cd93242df644947c009559f31b4e19eb1dab6b62ab783ce5a2a54a4ad392f57 -wheel==0.46.3 \ - --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \ - --hash=sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803 + +# The following packages are considered to be unsafe in a requirements file: +pip==26.0.1 \ + --hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \ + --hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8 diff --git a/video/resources/models/download_yolo.py b/video/resources/models/download_yolo.py deleted file mode 100644 index 9dae0b5..0000000 --- a/video/resources/models/download_yolo.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from pathlib import Path - -from ultralytics import YOLO - - -def str2bool(in_val): - if isinstance(in_val, bool): - return in_val - - if not isinstance(in_val, str): - raise ValueError(f"{in_val} is not a bool or string") - - if in_val.title() == "True": - return True - else: - return False - - -model_precision_object = "FP16" -half_flag = True -dynamic_flag = True -CUSTOM_MODEL_FLAG = str2bool(os.getenv("CUSTOM_MODEL_FLAG", False)) -DEVICE = os.environ.get("DEVICE", "CPU") -MODEL_NAME = os.environ.get("MODEL_NAME", "yolo11n") -if DEVICE == "GPU": - batch_size = int(os.environ.get("GPU_BATCH_SIZE", 1)) - run_platform_name = "engine" - os.environ["CUDA_VISIBLE_DEVICES"] = "0" - print("[!] USING GPU & TENSORRT") -else: - # batch_size = 8 - batch_size = int(os.environ.get("CPU_BATCH_SIZE", 1)) # 8 - run_platform_name = "openvino" - print("[!] USING CPU & OPENVINO") - - -def get_model(model_dir, run_platform, device_input, batch=1): - final_model_path = f"{model_dir}/{MODEL_NAME}.pt" - pt_detection_model = YOLO(final_model_path, verbose=False, task="detect") - if run_platform == "openvino": - pt_detection_model.export( - format="openvino", - half=half_flag, - dynamic=dynamic_flag, - device=device_input, - batch=batch, - ) - - final_model_path = f"{model_dir}/{MODEL_NAME}_openvino_model/" - object_detection_model = YOLO( - final_model_path, - verbose=False, - task="detect", - ) - - # det_ov_model = core.read_model(final_model_path+"yolo11n.xml") - # ov_config = {hints.performance_mode: hints.PerformanceMode.LATENCY} - # if device == "GPU": - # ov_config["GPU_DISABLE_WINOGRAD_CONVOLUTION"] = "YES" - # compiled_model = core.compile_model(det_ov_model, device, ov_config) - # object_detection_model.predictor.model.ov_compiled_model = compiled_model - - elif run_platform == "engine": - pt_detection_model.export( - format="engine", - half=half_flag, - imgsz=[7680, 4320], # Max dimensions (8K-[W,H]-[7680,4320]) - dynamic=dynamic_flag, - device=device_input, - simplify=True, - batch=batch, - ) - - final_model_path = f"{model_dir}/{MODEL_NAME}.engine" - object_detection_model = YOLO( - final_model_path, - verbose=False, - task="detect", - ) - - elif run_platform == "onnx": - from torch import cuda - from ultralytics.utils.checks import check_requirements - - check_requirements( - "onnxruntime-gpu" - if cuda.is_available() and device_input != "cpu" - else "onnxruntime" - ) - - final_model_path = f"{model_dir}/{MODEL_NAME}.onnx" - pt_detection_model.export( - format="onnx", - half=half_flag, - dynamic=dynamic_flag, - device=device_input, - simplify=True, - batch=batch, - ) - - object_detection_model = YOLO(final_model_path, verbose=False, task="detect") - - elif run_platform == "pytorch": - object_detection_model = pt_detection_model - if DEVICE == "GPU": - object_detection_model.to("cuda") - else: - object_detection_model.to(device_input) - - else: - raise ValueError(f"[!] Model for {run_platform} is not implemented.") - - return object_detection_model, final_model_path - - -if __name__ == "__main__": - if CUSTOM_MODEL_FLAG: - dir_path = "/home/resources/models/ultralytics/custom_models" - else: - dir_path = ( - f"/home/resources/models/ultralytics/{MODEL_NAME}/{model_precision_object}" - ) - - ydir = Path(dir_path) - - device_input = DEVICE.lower() if DEVICE == "CPU" else "cuda" - _, _ = get_model(ydir, run_platform_name, device_input, batch=batch_size) - - os.remove(f"{ydir}/{MODEL_NAME}.pt") diff --git a/video/segment.py b/video/segment.py index dc5a7d8..6cedf16 100755 --- a/video/segment.py +++ b/video/segment.py @@ -7,7 +7,14 @@ from tornado import gen, web from tornado.concurrent import run_on_executor -from utils import safely_join_path + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path class SegmentHandler(web.RequestHandler): diff --git a/video/source_watcher.py b/video/source_watcher.py new file mode 100644 index 0000000..9abcac3 --- /dev/null +++ b/video/source_watcher.py @@ -0,0 +1,317 @@ +import logging +import multiprocessing as mp +import os +import sys +import time +import traceback +from multiprocessing.managers import BaseManager +from pathlib import Path + +import requests +import yaml +from inotify.adapters import Inotify + +logger = logging.getLogger(__name__) + +BACKEND_URL = os.getenv("BACKEND_URL", "http://fastapi-service:8000") +DEBUG = os.environ["DEBUG"] +DEBUG_FLAG = True if DEBUG == "1" else False +ACCEPTED_VIDEO_FORMATS = [".mp4", ".mkv", ".avi"] +num_workers = mp.cpu_count() // 2 +empty_timeout = 5 * 60 + + +# --- HELPER FUNCTIONS --- +def is_file_ready(filepath, retries=5, delay=1): + """ + Checks if a file is fully written and accessible. + Uses exponential backoff for retries. + """ + for _ in range(retries): + try: + # Try to open the file in append mode to check for locks + with open(filepath, "r"): + return True + except (IOError, OSError): + logger.warning(f"File {filepath} is locked. Retrying in {delay}s...") + time.sleep(delay) + delay *= 2 # Exponential backoff + return False + + +def connect_to_app(): + # is_ready = False + # while not is_ready: + with requests.Session() as session: + while True: + try: + res = session.get(f"{BACKEND_URL}/status") + + if res.status_code == 200: + # Expected format: {"cam_1": {"status": "Ready"}, ...} + data = res.json() + + is_ready = any(status == "Ready" for status in data.values()) + if is_ready: + break + except requests.exceptions.RequestException: + logger.info("BACKEND: Waiting for fastapi-service ...") + + time.sleep(1) + + +# --- WATCHERS & MANAGERS --- +class QueueManager(BaseManager): + pass + + +def worker_process(queue): + """Consumes tasks from the queue and sends them to the backend.""" + while True: + try: + # Wait for a task; timeout prevents hanging on shutdown + task = queue.get(timeout=10) + if task is None: + break # Sentinel value to stop worker + + source, camera_name = task + start_stream_processor(source, camera_name) + except mp.queues.Empty: + continue + except Exception as e: + logger.error(f"Worker error: {e}") + + +def watch_video_files(queue, watch_dir): + # Initial scan of files + with os.scandir(watch_dir) as entries: + for entry in entries: + # entry.is_file() and entry.name are retrieved in one go + if entry.is_file() and any( + entry.name.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + source = entry.path # Full path is already available + + if is_file_ready(source): + queue.put((source, None)) + logger.info(f"{source} added to queue: {time.time()}") + # start_stream_processor(source, None) + + # Watch watch_dir for new files + i = Inotify() + i.add_watch(watch_dir) + logger.info("START ADDING VIDEO FILES TO WATCHED DIRECTORY") + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + + # New file created in watched directory + # IN_CLOSE_WRITE is better than IN_CREATE because it triggers + # only after the writing process finishes and closes the file. + if "IN_CLOSE_WRITE" in type_names and any( + filename.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + source = os.path.join(path, filename) + + if is_file_ready(source): + queue.put((source, None)) + logger.info(f"{source} added to queue: {time.time()}") + # start_stream_processor(source, None) + else: + logger.error(f"Failed to access {source} after retries. Skipping.") + + +def retrieve_camera_details(queue, config_path): + """ + Watches config file and adds only new streams (via camera_name) to queue + """ + unique_camera_names = set() + + def read_config_and_queue(): + try: + with open(config_path, "r") as inFile: + config = yaml.safe_load(inFile) + + if config: + for camera_name, camera_details in config.items(): + # run_processor(camera_details["url"], camera_name=camera_name) + if isinstance(camera_details, dict) and "url" in camera_details: + source = camera_details["url"] + if source not in unique_camera_names: + logger.info(f"{source} added to queue: {time.time()}") + queue.put((source, camera_name)) + unique_camera_names.add(source) + # start_stream_processor(source, camera_name) + else: + logger.warning(f"Invalid entry: {camera_name}") + except Exception: + e = traceback.format_exc() + logger.info(f"Unexpected error processing config file: {e}") + + # Initial scan of config file + read_config_and_queue() + + # Watch for changes to file + i = Inotify() + config_dir = os.path.dirname(os.path.abspath(config_path)) + i.add_watch(config_dir) + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + + if filename == os.path.basename(config_path) and "IN_CLOSE_WRITE" in type_names: + time.sleep(0.5) + read_config_and_queue() + + +def unified_watcher(queue, watch_dir, config_path): + """ + Watches directory for both videos and config changes. + """ + unique_camera_names = set() + config_filename = os.path.basename(config_path) + + def read_config(): + try: + with open(config_path, "r") as f: + config = yaml.safe_load(f) + if config: + for name, details in config.items(): + url = details.get("url") + if url and url not in unique_camera_names: + logger.info(f"CAMERA: {name} added to queue: {time.time()}") + queue.put((url, name)) + unique_camera_names.add(url) + except Exception as e: + logger.error(f"CONFIG ERROR: {e}") + + # Initial scan of config file + read_config() + + # Initial scan of video files + with os.scandir(watch_dir) as entries: + for entry in entries: + if entry.is_file() and any( + entry.name.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS + ): + if is_file_ready(entry.path): + queue.put((entry.path, None)) + + # Unified Event Loop + i = Inotify() + i.add_watch(watch_dir) + logger.info(f"UNIFIED WATCHER: Monitoring {watch_dir} for videos and config...") + + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + full_path = os.path.join(path, filename) + + # Handle Config Update (Supports Atomic Saves/Moves) + if filename == config_filename: + if any(ev in type_names for ev in ["IN_CLOSE_WRITE", "IN_MOVED_TO"]): + time.sleep(0.2) + read_config() + + # Handle New Video Files + elif any(filename.endswith(ext) for ext in ACCEPTED_VIDEO_FORMATS): + if "IN_CLOSE_WRITE" in type_names: + if is_file_ready(full_path): + queue.put((full_path, None)) + logger.info(f"VIDEO: {full_path} added to queue: {time.time()}") + + +# --- STREAMER --- +def start_stream_processor(source, camera_name): + if camera_name is None: + camera_name = Path(source).stem + + payload = {"url": str(source), "name": camera_name} + + for attempt in range(1, 11): + try: + # FastAPI stream_processor + res = requests.post(f"{BACKEND_URL}/stream", json=payload) + + # Raise an exception for 4xx or 5xx status codes + res.raise_for_status() + + if res.status_code == 200: + logger.info(f"Started {source} process on attempt {attempt}.") + return res + + except requests.exceptions.HTTPError as e: + # Handle specific API errors (e.g., 400 Bad Request, 500 Internal Server Error) + logger.info(f"HTTP Error: {e.response.status_code} - {e.response.text}") + + # If it's a client error (4xx), retrying likely won't help + if 400 <= e.response.status_code < 500: + break + + except requests.exceptions.RequestException as e: + # Handle connection issues, timeouts, and DNS errors + logger.info(f"Connection attempt {attempt} failed: {e}") + + except Exception: + e = traceback.format_exc() + logger.info(f"Unexpected error processing {source}: {e}") + + time.sleep(1) + + +# --- MAIN FUNCTION --- +def main(watch_folder=os.getcwd()): + # Wait until App is ready + connect_to_app() + + if DEBUG_FLAG: + logger.info("[TIMING],start_watchandsend,," + str(time.time())) + + # Setup the Shared Queue and Manager + file_queue = mp.Queue() + QueueManager.register("get_file_queue", callable=lambda: file_queue) + + # Start the manager on port 5005 + manager = QueueManager(address=("0.0.0.0", 5005), authkey=b"password123") + manager.start() + logger.info("Queue Server started at 0.0.0.0:5005") + + # Define worker processes + processes = [ + mp.Process( # Process that retrieves camera info from config file (added to file_queue) + target=unified_watcher, + args=(file_queue, watch_folder, "/home/camera_config.yaml"), + name="UnifiedWatcher", + daemon=True, + ), + ] + + for _ in range(num_workers): + p = mp.Process(target=worker_process, args=(file_queue,), daemon=True) + processes.append(p) + + # 4. Start processed and Keep the server alive + try: + # Start all processes + for p in processes: + p.start() + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Stopping services ...") + finally: + # Cleanup + for p in processes: + if p.is_alive(): + p.terminate() + p.join() # Ensure fully closed + + manager.shutdown() + if DEBUG_FLAG: + logger.info("[TIMING],end_watchandsend,," + str(time.time())) + + +if __name__ == "__main__": + if len(sys.argv) == 2: + main(sys.argv[1]) + else: + raise ValueError("Invalid input. Please provide watch directory.") diff --git a/video/thumbnail.py b/video/thumbnail.py index 9a0d236..342e054 100755 --- a/video/thumbnail.py +++ b/video/thumbnail.py @@ -7,7 +7,14 @@ from tornado import gen, web from tornado.concurrent import run_on_executor -from utils import safely_join_path + + +def safely_join_path(base_dir, add_path): + safe_base = os.path.abspath(base_dir) + candidate_path = os.path.abspath(os.path.join(safe_base, add_path)) + if not candidate_path.startswith(safe_base + os.sep): + raise ValueError(f"Invalid path: {candidate_path}") + return candidate_path class ThumbnailHandler(web.RequestHandler): diff --git a/video/watch_and_send2vdms.py b/video/watch_and_send2vdms.py deleted file mode 100644 index 2bdc3b8..0000000 --- a/video/watch_and_send2vdms.py +++ /dev/null @@ -1,115 +0,0 @@ -import multiprocessing as mp -import os -import queue -import subprocess -import sys -import time - -import yaml -from inotify.adapters import Inotify - -num_workers = mp.cpu_count() // 2 - -DEBUG = os.environ["DEBUG"] -DEBUG_FLAG = True if DEBUG == "1" else False - - -def run_processor(path_or_url, camera_name=None): - cmd = [ - sys.executable, - "/home/process_stream.py", - path_or_url, - # camera_name, - ] - if camera_name is not None: - cmd.append(camera_name) - - subprocess.run(cmd, check=True) - - -def watch_video_files(queue, watch_dir): - # Get files already in watch_dir - for filename in os.listdir(watch_dir): - if any(filename.endswith(ext) for ext in [".mp4", ".mkv", ".avi"]): - source = os.path.join(watch_dir, filename) - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, None)) - - # Watch watch_dir for new files - i = Inotify() - i.add_watch(watch_dir) - print("START ADDING VIDEO FILES TO WATCHED DIRECTORY", flush=True) - - for event in i.event_gen(yield_nones=False): - (_, type_names, path, filename) = event - - # New file created in watched directory - # if "IN_CREATE" in type_names: - if "IN_CLOSE_WRITE" in type_names and any( - filename.endswith(ext) for ext in [".mp4", ".mkv", ".avi"] - ): - source = os.path.join(path, filename) - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, None)) - - -def retrieve_camera_details(queue, config_path): - config = None - with open(config_path, "r") as inFile: - config = yaml.safe_load(inFile) - - if config is not None: - for camera_name, camera_details in config.items(): - # run_processor(camera_details["url"], camera_name=camera_name) - source = camera_details["url"] - print(f"{source} added to queue: {time.time()}", flush=True) - queue.put((source, camera_name)) - - -def main(watch_folder=os.getcwd()): - if DEBUG_FLAG: - print("[TIMING],start_watchandsend,," + str(time.time()), flush=True) - - file_queue = mp.Queue() - - # Create a process that monitors new files in watch_folder (added to file_queue) - watcher_process = mp.Process( - target=watch_video_files, args=(file_queue, watch_folder) - ) - - # Create a process that retrieves camera info from config file (added to file_queue) - watcher_process_camera = mp.Process( - target=retrieve_camera_details, args=(file_queue, "/home/camera_config.yaml") - ) - - watcher_process.start() - watcher_process_camera.start() - - # Pool of workers to process video clips - with mp.Pool(processes=num_workers) as pool: - while True: - try: - path_or_url, camera_name = file_queue.get(timeout=0.5) - # pool.apply(run_processor, (path_or_url, camera_name,)) - pool.apply_async( - run_processor, - ( - path_or_url, - camera_name, - ), - ) - except queue.Empty: - pass - - watcher_process.join() - watcher_process_camera.join() - - if DEBUG_FLAG: - print("[TIMING],end_watchandsend,," + str(time.time()), flush=True) - - -if __name__ == "__main__": - if len(sys.argv) == 2: - main(sys.argv[1]) - else: - raise ValueError("Invalid input. Please provide watch directory.")