Spaces:
Running on RTX PRO 6000
Running on RTX PRO 6000
Deploy simready-assets run 20260928-ed4eb73 (GPU variant)
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .gitattributes +4 -0
- Dockerfile +297 -0
- README.md +17 -8
- docker/hf-space/logging.yml +80 -0
- docker/hf-space/nginx.conf.template +259 -0
- docker/hf-space/search_fields.yaml +599 -0
- docker/hf-space/siglip2-model/PACKAGE-LICENSES/siglip2-giant-opt-patch16-384.txt +202 -0
- docker/hf-space/siglip2-model/SOURCE.txt +4 -0
- docker/hf-space/siglip2-model/model_repo/ensemble_image_model/1/_.txt +0 -0
- docker/hf-space/siglip2-model/model_repo/ensemble_image_model/config.pbtxt +48 -0
- docker/hf-space/siglip2-model/model_repo/ensemble_text_model/1/_.txt +0 -0
- docker/hf-space/siglip2-model/model_repo/ensemble_text_model/config.pbtxt +48 -0
- docker/hf-space/siglip2-model/model_repo/image_preprocessing/1/model.py +101 -0
- docker/hf-space/siglip2-model/model_repo/image_preprocessing/1/preprocessor_config/preprocessor_config.json +24 -0
- docker/hf-space/siglip2-model/model_repo/image_preprocessing/config.pbtxt +31 -0
- docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt +67 -0
- docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt +65 -0
- docker/hf-space/siglip2-model/model_repo/tokenizer/1/model.py +80 -0
- docker/hf-space/siglip2-model/model_repo/tokenizer/1/tokenizer/tokenizer.json +3 -0
- docker/hf-space/siglip2-model/model_repo/tokenizer/config.pbtxt +35 -0
- docker/hf-space/siglip2-model/pyproject.toml +14 -0
- docker/hf-space/siglip2-model/uv.lock +597 -0
- docker/hf-space/supervisord-gpu.conf +113 -0
- docker/hf-space/supervisord.conf +130 -0
- docker/hf-space/wheels/SOURCE.txt +3 -0
- docker/hf-space/wheels/asset_graph_client-0.1.0-py3-none-any.whl +0 -0
- docker/hf-space/wheels/asset_graph_service-0.1.0-py3-none-any.whl +0 -0
- docker/hf-space/wheels/base_logging-0.1.0-py3-none-any.whl +0 -0
- docker/hf-space/wheels/constraints.txt +160 -0
- docker/hf-space/wheels/deepsearch_api-0.1.0-py3-none-any.whl +3 -0
- docker/hf-space/wheels/llm_client-0.1.0-py3-none-any.whl +0 -0
- docker/hf-space/wheels/search_utils-0.1.0-py3-none-any.whl +3 -0
- docker/hf-space/wheels/siglip2_triton_client-0.6.1-py3-none-any.whl +3 -0
- docker/hf-space/wheels/storage_api-0.1.0-py3-none-any.whl +0 -0
- pipeline/__init__.py +1 -0
- pipeline/artifact_store.py +139 -0
- pipeline/bake.py +631 -0
- pipeline/clear.py +101 -0
- pipeline/config.py +512 -0
- pipeline/corpus.py +370 -0
- pipeline/enumerate_assets.py +331 -0
- pipeline/export.py +296 -0
- pipeline/hf_addressing.py +133 -0
- pipeline/hf_catalog.py +109 -0
- pipeline/hf_space.py +314 -0
- pipeline/hf_space_seed.py +245 -0
- pipeline/queues.py +59 -0
- pipeline/restore.py +266 -0
- pipeline/run_pipeline.py +311 -0
- pipeline/status.py +381 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docker/hf-space/siglip2-model/model_repo/tokenizer/1/tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docker/hf-space/wheels/deepsearch_api-0.1.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
docker/hf-space/wheels/search_utils-0.1.0-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
docker/hf-space/wheels/siglip2_triton_client-0.6.1-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# deploy: 20260928-ed4eb73 (simready-assets)
|
| 2 |
+
# GPU variant of the single-container MVP image for Hugging Face Spaces
|
| 3 |
+
# (Docker SDK): everything docker/Dockerfile.hf-space has (OpenSearch + Neo4j
|
| 4 |
+
# + Redis + deepsearch-api + asset-graph-service + nginx under supervisord),
|
| 5 |
+
# plus a *local* siglip2-triton process instead of routing SigLIP2 embeddings
|
| 6 |
+
# to NVCF β see docs/plans/siglip2-embedded-hf-space.md for the investigation
|
| 7 |
+
# and decision behind this file. Meant to be deployed to an HF Space running
|
| 8 |
+
# on a GPU hardware tier (T4 medium β see pipeline/hf_space.py's
|
| 9 |
+
# request_space_hardware call for the gpu variant); docker/Dockerfile.hf-space
|
| 10 |
+
# remains the free/cpu-basic, NVCF-backed variant β this is an additional
|
| 11 |
+
# deploy target, not a replacement (`SIGLIP2_BACKEND=nvcf` is unaffected).
|
| 12 |
+
#
|
| 13 |
+
# Builds the Triton half of this image the same way
|
| 14 |
+
# ../usdsearch/docker/Dockerfile.siglip2-triton does β FROM the bare, public
|
| 15 |
+
# nvcr.io/nvidia/tritonserver image, model_repo/ COPYed in from
|
| 16 |
+
# docker/hf-space/siglip2-model/ (vendored into *this* repo by
|
| 17 |
+
# scripts/extract_siglip2_model.sh, since HF's remote builder can reach
|
| 18 |
+
# neither ../usdsearch nor any private registry) β rather than FROM the
|
| 19 |
+
# prepackaged nvcr.io/nvidia/usdsearch/siglip2-triton image an earlier
|
| 20 |
+
# revision of this file used. Verified live why that prepackaged image was
|
| 21 |
+
# dropped: its published 1.4.0 tag bakes **FP32** weights (vision 4.4 GB +
|
| 22 |
+
# text 2.7 GB, inspected inside a pulled copy) β at this model's baked
|
| 23 |
+
# `instance_group: 2`, that's ~14 GB of resident GPU weight memory alone,
|
| 24 |
+
# and ~18 GB observed live in `nvidia-smi`, blowing past a T4's 16 GB VRAM.
|
| 25 |
+
# ../usdsearch's *current* services/siglip2-triton/model_repo checkout is
|
| 26 |
+
# already FP16-exported (vision 2.2 GB + text 1.4 GB β half the size, ~7 GB
|
| 27 |
+
# at instance_group: 2, comfortably under 16 GB) but was never republished
|
| 28 |
+
# under that NGC tag β vendoring this checkout's own model_repo directly
|
| 29 |
+
# sidesteps waiting on a new publish. See
|
| 30 |
+
# scripts/extract_siglip2_model.sh's own header for the full mechanism and
|
| 31 |
+
# docs/plans/siglip2-embedded-hf-space.md's progress log for when this was
|
| 32 |
+
# last refreshed.
|
| 33 |
+
#
|
| 34 |
+
# nvcr.io/nvidia/tritonserver itself (the base, not the prepackaged image)
|
| 35 |
+
# is on the same *public* NGC catalog path already verified live this
|
| 36 |
+
# project's other public pulls (opensearchproject/opensearch, neo4j) use β
|
| 37 |
+
# no credentials needed, so Hugging Face's own remote builder can pull it.
|
| 38 |
+
#
|
| 39 |
+
# VERIFIED LIVE on a real GPU (RTX 4090, local β not a T4, but same
|
| 40 |
+
# execution path): full build, `docker run --gpus all`, all 6 Triton models
|
| 41 |
+
# READY, OpenSearch/Neo4j restore from S3, deepsearch-api + asset-graph-service
|
| 42 |
+
# healthy through nginx. GPU memory settled at ~10-11 GB (tritonserver
|
| 43 |
+
# ~10.3 GB + a smaller deepsearch-api CUDA-context allocation) β comfortably
|
| 44 |
+
# under a T4's 16 GB, confirming the FP32->FP16 switch actually fixed the
|
| 45 |
+
# budget problem, not just on paper. A real `/v3/deepsearch/search` call
|
| 46 |
+
# came back with genuine `search_type: text_to_vector` /
|
| 47 |
+
# `field: siglip2-embedding` hits (query "a red chair" correctly ranked
|
| 48 |
+
# office-chair assets top) β the local siglip2-triton round trip is real,
|
| 49 |
+
# not just "the process started". LLM query parsing
|
| 50 |
+
# (`/v3/deepsearch/llm_parse/query`) and VLM validation
|
| 51 |
+
# (`/v3/deepsearch/vlm_validate/search_result`) were also exercised live
|
| 52 |
+
# against the already-deployed NVCF Qwen endpoint
|
| 53 |
+
# (config/pipeline.env.srp-staging-controlled's LLM_ENDPOINT_BASE_URL) with
|
| 54 |
+
# $NVCF_GENAI_RUN_KEY as USDSEARCH_LLM_API_KEY β both returned real model
|
| 55 |
+
# output (a structured filter parse; a logprob-based
|
| 56 |
+
# `P(Yes)=... from first-token log-probs.` verdict), confirming this
|
| 57 |
+
# variant's NVCF-backed LLM/VLM path works the same as the NVCF/cpu-basic
|
| 58 |
+
# variant's. See docs/plans/siglip2-embedded-hf-space.md's progress log for
|
| 59 |
+
# the full session notes, including the earlier FP32 attempt this
|
| 60 |
+
# superseded.
|
| 61 |
+
|
| 62 |
+
ARG OPENSEARCH_VERSION=3.8.0
|
| 63 |
+
ARG NEO4J_VERSION=5.23.0-community
|
| 64 |
+
ARG TRITONSERVER_BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3
|
| 65 |
+
|
| 66 |
+
FROM opensearchproject/opensearch:${OPENSEARCH_VERSION} AS opensearch-src
|
| 67 |
+
|
| 68 |
+
FROM neo4j:${NEO4J_VERSION} AS neo4j-src
|
| 69 |
+
|
| 70 |
+
FROM ${TRITONSERVER_BASE_IMAGE}
|
| 71 |
+
|
| 72 |
+
WORKDIR /workspace
|
| 73 |
+
RUN mkdir -p /models
|
| 74 |
+
|
| 75 |
+
# Vendored model repo (see header comment) β ensembles, the Python-backend
|
| 76 |
+
# tokenizer/preprocessing models, and the two ONNX encoders' *config.pbtxt*
|
| 77 |
+
# only. The encoders' actual weight files (~3.6 GB combined β the ONNX
|
| 78 |
+
# externa-data blobs under each encoder's own 1/ subdirectory) are
|
| 79 |
+
# deliberately NOT baked here: a real `deploy-space-gpu` attempt 403'd with
|
| 80 |
+
# "Repository storage limit reached (Max: 1 GB)" β HF Space git repos have a
|
| 81 |
+
# default 1 GB storage quota, well under what baking these in needs. Instead
|
| 82 |
+
# they're restored from the artifact S3 bucket at container startup
|
| 83 |
+
# (pipeline/hf_space_seed.py --component=siglip2-model, run by
|
| 84 |
+
# supervisord-gpu.conf's [program:siglip2-triton] before tritonserver
|
| 85 |
+
# itself execs) β the same "runtime restore, not baked" pattern this image
|
| 86 |
+
# already uses for the OpenSearch index and Neo4j graph, just for a
|
| 87 |
+
# corpus-independent static artifact instead of per-run data. Publish
|
| 88 |
+
# weights to that S3 path with scripts/publish_siglip2_model_to_s3.sh
|
| 89 |
+
# whenever scripts/extract_siglip2_model.sh refreshes the vendored copy.
|
| 90 |
+
COPY docker/hf-space/siglip2-model/model_repo/ensemble_image_model /models/ensemble_image_model
|
| 91 |
+
COPY docker/hf-space/siglip2-model/model_repo/ensemble_text_model /models/ensemble_text_model
|
| 92 |
+
COPY docker/hf-space/siglip2-model/model_repo/image_preprocessing /models/image_preprocessing
|
| 93 |
+
COPY docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt /models/siglip2_vision_encoder_onnx/config.pbtxt
|
| 94 |
+
COPY docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt /models/siglip2_text_encoder_onnx/config.pbtxt
|
| 95 |
+
RUN mkdir -p /models/siglip2_vision_encoder_onnx/1 /models/siglip2_text_encoder_onnx/1
|
| 96 |
+
COPY docker/hf-space/siglip2-model/model_repo/tokenizer /models/tokenizer
|
| 97 |
+
|
| 98 |
+
# Python deps for the tokenizer + image_preprocessing Python-backend models β
|
| 99 |
+
# resolved from pyproject.toml + uv.lock (the source of truth) at build time,
|
| 100 |
+
# same as ../usdsearch/docker/Dockerfile.siglip2-triton itself does, so this
|
| 101 |
+
# doesn't need a separately-maintained requirements.txt that can drift.
|
| 102 |
+
COPY docker/hf-space/siglip2-model/pyproject.toml docker/hf-space/siglip2-model/uv.lock /workspace/
|
| 103 |
+
RUN pip install --no-cache-dir uv \
|
| 104 |
+
&& uv export --frozen --no-hashes --directory /workspace -o /workspace/requirements.txt \
|
| 105 |
+
&& pip install --no-cache-dir -r /workspace/requirements.txt \
|
| 106 |
+
&& rm /workspace/requirements.txt
|
| 107 |
+
|
| 108 |
+
COPY docker/hf-space/siglip2-model/PACKAGE-LICENSES /PACKAGE-LICENSES
|
| 109 |
+
|
| 110 |
+
# Same two size/vulnerability trims ../usdsearch/docker/Dockerfile.siglip2-triton
|
| 111 |
+
# itself applies to this exact base image β carried over verbatim rather than
|
| 112 |
+
# dropped, since they're cheap and free up real space (~1 GB+ for Nsight
|
| 113 |
+
# Systems alone) on top of the FP16 savings above.
|
| 114 |
+
RUN rm -rf /usr/local/cuda-13.2/NsightSystems-cli-2026.2.1
|
| 115 |
+
|
| 116 |
+
USER root
|
| 117 |
+
|
| 118 |
+
# Non-root runtime uid/gid 1000, same convention docker/Dockerfile.hf-space
|
| 119 |
+
# uses for every data-writing process in that image (matches
|
| 120 |
+
# opensearchproject/opensearch's own "opensearch" user, 1000:1000). This
|
| 121 |
+
# base image is Ubuntu 24.04 ("noble") β not verified live whether uid 1000
|
| 122 |
+
# is already taken by a user the NGC tritonserver image itself creates, so
|
| 123 |
+
# this reuses it (rather than erroring) if it is: --chown=1000:1000 below
|
| 124 |
+
# only cares that the numeric id resolves consistently, not which username
|
| 125 |
+
# owns it.
|
| 126 |
+
RUN if id -u 1000 >/dev/null 2>&1; then \
|
| 127 |
+
echo "uid 1000 already exists as $(getent passwd 1000 | cut -d: -f1) β reusing it"; \
|
| 128 |
+
else \
|
| 129 |
+
groupadd -g 1000 nvs && useradd -u 1000 -g 1000 -m -d /home/nvs -s /usr/sbin/nologin nvs; \
|
| 130 |
+
fi
|
| 131 |
+
|
| 132 |
+
# AWS CLI v2 via its official self-contained installer, not apt/pip β same
|
| 133 |
+
# reasoning as docker/Dockerfile.hf-space. python3.12-venv is requested
|
| 134 |
+
# explicitly (Ubuntu splits the venv module out of the base python3.12
|
| 135 |
+
# package on some images) since this base is noble rather than bookworm and
|
| 136 |
+
# hasn't been checked for what's preinstalled.
|
| 137 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 138 |
+
redis-server supervisor nginx curl unzip python3.12-venv openssl \
|
| 139 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 140 |
+
&& rm -f /etc/nginx/sites-enabled/default \
|
| 141 |
+
&& curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
|
| 142 |
+
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
| 143 |
+
&& /tmp/aws/install \
|
| 144 |
+
&& rm -rf /tmp/awscliv2.zip /tmp/aws
|
| 145 |
+
|
| 146 |
+
# The app venv: same /app/.venv path docker/Dockerfile.hf-space and
|
| 147 |
+
# docker/hf-space/supervisord*.conf both hardcode. Installed from the same
|
| 148 |
+
# repacked usdsearch wheels + constraints.txt as the NVCF variant β see that
|
| 149 |
+
# Dockerfile's header comment for how wheels/ is produced and refreshed.
|
| 150 |
+
# Upstream's own docker/Dockerfile.usdsearch compiles these wheels in a
|
| 151 |
+
# python:3.12-bookworm builder stage but runs them in a
|
| 152 |
+
# nvcr.io/nvidia/base/ubuntu:noble-20260217 *runtime* stage β the same OS
|
| 153 |
+
# family as this Triton image's own base β so installing them here follows
|
| 154 |
+
# upstream's own production direction (older-glibc build -> newer-glibc
|
| 155 |
+
# run), not a new cross-distro risk. Not yet verified live in this repo,
|
| 156 |
+
# though (see header comment).
|
| 157 |
+
COPY docker/hf-space/wheels/ /tmp/usdsearch-wheels/
|
| 158 |
+
RUN python3.12 -m venv /app/.venv \
|
| 159 |
+
&& /app/.venv/bin/pip install --no-cache-dir \
|
| 160 |
+
--find-links /tmp/usdsearch-wheels \
|
| 161 |
+
--constraint /tmp/usdsearch-wheels/constraints.txt \
|
| 162 |
+
/tmp/usdsearch-wheels/*.whl \
|
| 163 |
+
&& rm -rf /tmp/usdsearch-wheels \
|
| 164 |
+
&& chown -R 1000:1000 /app/.venv
|
| 165 |
+
|
| 166 |
+
COPY --from=opensearch-src --chown=1000:1000 /usr/share/opensearch /usr/share/opensearch
|
| 167 |
+
|
| 168 |
+
RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-performance-analyzer
|
| 169 |
+
|
| 170 |
+
# See docker/Dockerfile.hf-space's own (much longer) comment on this same
|
| 171 |
+
# block for the full k-NN/faiss library-path story β copied verbatim here
|
| 172 |
+
# since it's the identical opensearchproject/opensearch source image and the
|
| 173 |
+
# identical cross-image-copy cause.
|
| 174 |
+
ENV OPENSEARCH_HOME=/usr/share/opensearch \
|
| 175 |
+
OPENSEARCH_PATH_CONF=/usr/share/opensearch/config \
|
| 176 |
+
PATH="/usr/share/opensearch/bin:/app/.venv/bin:${PATH}" \
|
| 177 |
+
LD_LIBRARY_PATH=/usr/share/opensearch/plugins/opensearch-knn/lib \
|
| 178 |
+
discovery.type=single-node \
|
| 179 |
+
DISABLE_SECURITY_PLUGIN=true \
|
| 180 |
+
DISABLE_INSTALL_DEMO_CONFIG=true \
|
| 181 |
+
bootstrap.memory_lock=false \
|
| 182 |
+
OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g -Djava.library.path=/usr/share/opensearch/plugins/opensearch-knn/lib"
|
| 183 |
+
|
| 184 |
+
COPY --from=neo4j-src /opt/java/openjdk /opt/java/openjdk
|
| 185 |
+
COPY --from=neo4j-src /var/lib/neo4j /var/lib/neo4j
|
| 186 |
+
|
| 187 |
+
RUN mkdir -p /data /logs && chmod 777 /data /logs
|
| 188 |
+
|
| 189 |
+
RUN cp /var/lib/neo4j/labs/apoc-*-core.jar /var/lib/neo4j/plugins/
|
| 190 |
+
|
| 191 |
+
RUN { \
|
| 192 |
+
echo "dbms.security.auth_enabled=false"; \
|
| 193 |
+
echo "server.memory.heap.initial_size=1G"; \
|
| 194 |
+
echo "server.memory.heap.max_size=1G"; \
|
| 195 |
+
echo "server.memory.pagecache.size=512M"; \
|
| 196 |
+
} >> /var/lib/neo4j/conf/neo4j.conf
|
| 197 |
+
|
| 198 |
+
ENV NEO4J_HOME=/var/lib/neo4j \
|
| 199 |
+
PATH="/var/lib/neo4j/bin:/opt/java/openjdk/bin:${PATH}"
|
| 200 |
+
|
| 201 |
+
COPY docker/hf-space/logging.yml /logging.yml
|
| 202 |
+
COPY docker/hf-space/search_fields.yaml /app/search_fields.yaml
|
| 203 |
+
|
| 204 |
+
# The GPU-variant supervisord config β same six programs as
|
| 205 |
+
# docker/hf-space/supervisord.conf plus [program:siglip2-triton], and
|
| 206 |
+
# deepsearch-api's own command additionally waits on Triton's readiness
|
| 207 |
+
# before execing uvicorn. See that file's header for the port-collision
|
| 208 |
+
# reasoning (Triton's default HTTP port 8000 clashes with deepsearch-api's
|
| 209 |
+
# own uvicorn; remapped to 8003 here).
|
| 210 |
+
COPY docker/hf-space/supervisord-gpu.conf /etc/supervisor/conf.d/hf-space.conf
|
| 211 |
+
|
| 212 |
+
COPY docker/hf-space/nginx.conf.template /etc/nginx/hf-space.conf.template
|
| 213 |
+
|
| 214 |
+
COPY services/explorer/dist /usr/share/nginx/explorer
|
| 215 |
+
|
| 216 |
+
COPY --chown=1000:1000 pipeline /app/hf-space-pipeline/pipeline
|
| 217 |
+
|
| 218 |
+
ARG ARTIFACT_S3_URI
|
| 219 |
+
ARG ARTIFACT_S3_REGION=us-east-2
|
| 220 |
+
ARG CORPUS_NAME
|
| 221 |
+
ENV ARTIFACT_S3_URI=${ARTIFACT_S3_URI} \
|
| 222 |
+
ARTIFACT_S3_REGION=${ARTIFACT_S3_REGION} \
|
| 223 |
+
CORPUS_NAME=${CORPUS_NAME}
|
| 224 |
+
|
| 225 |
+
# βββ deepsearch-api config baked for this corpus/run ββββββββββββββββββββββββ
|
| 226 |
+
# Same as docker/Dockerfile.hf-space's equivalent block, minus every
|
| 227 |
+
# NVCF-specific ARG/env (SIGLIP2_NVCF_ENDPOINT/FUNCTION_ID and their
|
| 228 |
+
# downstream SIGLIP2_TRITON_SERVER_* values) β this variant never talks to
|
| 229 |
+
# NVCF at all. SIGLIP2_TRITON_SERVER_URL instead points at the local
|
| 230 |
+
# siglip2-triton process's gRPC port (8001, loopback-only β nginx remains
|
| 231 |
+
# the sole externally-proxied port, same as every other backend process in
|
| 232 |
+
# this image), with no SSL and no auth headers, since nothing external ever
|
| 233 |
+
# reaches it.
|
| 234 |
+
ARG ES_NAME
|
| 235 |
+
ARG S3_STORAGE_BUCKET_NAME
|
| 236 |
+
ARG S3_STORAGE_REGION_NAME=us-west-2
|
| 237 |
+
ARG STORAGE_BACKEND_TYPE=s3
|
| 238 |
+
# URL mapping (hf:// -> https://huggingface.co/.../blob/<rev>/), derived per
|
| 239 |
+
# run from the corpus's own HF repos by pipeline/bake.py's
|
| 240 |
+
# hf_url_mapping_rules() and passed in by bake (--build-arg) and
|
| 241 |
+
# deploy-space (Space variables). Defaults to off: an s3:// corpus needs no
|
| 242 |
+
# rewrite, and a storage_api build without these args would otherwise map
|
| 243 |
+
# to some other dataset's prefix.
|
| 244 |
+
ARG USDSEARCH_URL_MAPPING_MODE=off
|
| 245 |
+
ARG USDSEARCH_URL_MAPPING_RULES=[]
|
| 246 |
+
ENV STORAGE_BACKEND_TYPE=${STORAGE_BACKEND_TYPE} \
|
| 247 |
+
S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} \
|
| 248 |
+
S3_STORAGE_REGION_NAME=${S3_STORAGE_REGION_NAME} \
|
| 249 |
+
opensearch_host=http://localhost:9200 \
|
| 250 |
+
opensearch_index_name=${ES_NAME}-ver5.0 \
|
| 251 |
+
opensearch_image_cache_index_name=${ES_NAME}-ver4.0-image-cache \
|
| 252 |
+
opensearch_use_ssl=false \
|
| 253 |
+
REDIS_URL=redis://localhost:6379 \
|
| 254 |
+
DEEPSEARCH_BACKEND_ASSET_GRAPH_SERVICE_URL=http://127.0.0.1:8010 \
|
| 255 |
+
ASSET_GRAPH_SERVICE_ENDPOINT=http://127.0.0.1:8010 \
|
| 256 |
+
ENABLE_ACCESS_VERIFICATION=False \
|
| 257 |
+
STORAGE_REQUIRE_AUTH=False \
|
| 258 |
+
VERIFY_ASSET_EXISTENCE=False \
|
| 259 |
+
verify_access=false \
|
| 260 |
+
ASSERT_ADMIN_USER=false \
|
| 261 |
+
USDSEARCH_VISION_VALIDATION_ENABLED=false \
|
| 262 |
+
USDSEARCH_LLM_PARSING_ENABLED=false \
|
| 263 |
+
USDSEARCH_LLM_PARSING_FIELDS_FILEPATH=/app/search_fields.yaml \
|
| 264 |
+
OTEL_SDK_DISABLED=true \
|
| 265 |
+
BACKEND_CONFIG_STR="backend_name: es-sample" \
|
| 266 |
+
DEFAULT_CUTOFF_THRESHOLD=0 \
|
| 267 |
+
HYBRID_SEARCH_SCORING_CONFIG_VECTOR_FIELDS="{\"siglip2-embedding.embedding\":{\"enabled\":true,\"weight\":1,\"field_name\":\"siglip2-embedding.embedding\",\"dimension\":1536}}" \
|
| 268 |
+
USDSEARCH_URL_MAPPING_MODE=${USDSEARCH_URL_MAPPING_MODE} \
|
| 269 |
+
USDSEARCH_URL_MAPPING_RULES="${USDSEARCH_URL_MAPPING_RULES}" \
|
| 270 |
+
SIGLIP2_TRITON_SERVER_URL=localhost:8001 \
|
| 271 |
+
TRITON_SERVER_URL=localhost:8001 \
|
| 272 |
+
SIGLIP2_TRITON_SERVER_SSL=false \
|
| 273 |
+
PORT=7860
|
| 274 |
+
|
| 275 |
+
ARG RUN_ID
|
| 276 |
+
ARG CORPUS_SHA256
|
| 277 |
+
ARG USDSEARCH_GIT_SHA
|
| 278 |
+
LABEL org.opencontainers.image.title="usdsearch-hf-space-simready-central-gpu" \
|
| 279 |
+
org.opencontainers.image.description="Single-container MVP (OpenSearch+Neo4j+Redis+deepsearch-api, SigLIP2 via local Triton on GPU) for Hugging Face Spaces" \
|
| 280 |
+
com.nvidia.usdsearch.run_id="${RUN_ID}" \
|
| 281 |
+
com.nvidia.usdsearch.corpus_sha256="${CORPUS_SHA256}" \
|
| 282 |
+
com.nvidia.usdsearch.usdsearch_git_sha="${USDSEARCH_GIT_SHA}"
|
| 283 |
+
|
| 284 |
+
# Longer start-period than docker/Dockerfile.hf-space's 10m: this variant
|
| 285 |
+
# additionally waits on Triton loading ~3.6 GB of ONNX weights onto the GPU
|
| 286 |
+
# before deepsearch-api's own uvicorn execs (see supervisord-gpu.conf).
|
| 287 |
+
# Verified live against a small (1488-asset) dev corpus: Triton model load
|
| 288 |
+
# took ~10s and the whole stack was healthy within ~20s total β 15m is
|
| 289 |
+
# deliberately still a generous upper bound, not a tight one, since a larger
|
| 290 |
+
# production corpus's OpenSearch bulk load / Neo4j offline load (the same
|
| 291 |
+
# restore path docker/Dockerfile.hf-space already budgets 10m for) can take
|
| 292 |
+
# much longer than this dev corpus did.
|
| 293 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15m --retries=3 \
|
| 294 |
+
CMD curl -sf "http://localhost:${PORT}/health" || exit 1
|
| 295 |
+
|
| 296 |
+
EXPOSE 7860
|
| 297 |
+
ENTRYPOINT ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/hf-space.conf"]
|
README.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
short_description: SimReady Central
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SimReady Central USD Search
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
hardware: t4-medium
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# SimReady Central USD Search
|
| 12 |
+
|
| 13 |
+
SimReady Central USD Search β single-container demo (OpenSearch + Neo4j +
|
| 14 |
+
Redis + deepsearch-api + asset-graph-service, SigLIP2 embeddings via a local
|
| 15 |
+
siglip2-triton process β see docker/Dockerfile.hf-space-gpu). Runs on a GPU
|
| 16 |
+
hardware tier (t4-medium), unlike the free-tier
|
| 17 |
+
docker/Dockerfile.hf-space variant, which routes SigLIP2 to NVCF instead.
|
| 18 |
+
The index itself is pulled from the artifact S3 bucket at container
|
| 19 |
+
startup, not baked in. Deployed by
|
| 20 |
+
github.com/NVIDIA-dev/simready-central-indexing-internal's
|
| 21 |
+
`python -m pipeline.run_pipeline deploy-space-gpu`.
|
docker/hf-space/logging.yml
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copied verbatim from ../usdsearch's own logging.yml (root of that repo,
|
| 2 |
+
# COPY'd to /logging.yml by its docker/Dockerfile.usdsearch) β the wheels in
|
| 3 |
+
# docker/hf-space/wheels/ carry the app code but not this repo-level config
|
| 4 |
+
# file, and packages/base-logging's get_logging_config() hardcodes
|
| 5 |
+
# /logging.yml as its default path ($LOGGING_CONFIG overrides it), so it has
|
| 6 |
+
# to be shipped here too or every service falls back to Python's bare
|
| 7 |
+
# default logging config (harmless β base_logging.setup_logging() catches
|
| 8 |
+
# the FileNotFoundError and just warns β but loses uvicorn's access-log
|
| 9 |
+
# formatting and the per-logger level tuning below, e.g. quieting
|
| 10 |
+
# opensearch/aiohttp.access to WARNING). Re-copy from ../usdsearch's
|
| 11 |
+
# logging.yml if it changes upstream; nothing here is HF-Space-specific.
|
| 12 |
+
#
|
| 13 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 14 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 15 |
+
#
|
| 16 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 17 |
+
# you may not use this file except in compliance with the License.
|
| 18 |
+
# You may obtain a copy of the License at
|
| 19 |
+
#
|
| 20 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 21 |
+
#
|
| 22 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 23 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 24 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 25 |
+
# See the License for the specific language governing permissions and
|
| 26 |
+
# limitations under the License.
|
| 27 |
+
|
| 28 |
+
version: 1
|
| 29 |
+
disable_existing_loggers: False
|
| 30 |
+
formatters:
|
| 31 |
+
default:
|
| 32 |
+
"()": uvicorn.logging.DefaultFormatter
|
| 33 |
+
format: '%(levelprefix)s %(asctime)s %(name)s - %(message)s'
|
| 34 |
+
access:
|
| 35 |
+
"()": uvicorn.logging.AccessFormatter
|
| 36 |
+
format: '%(levelprefix)s %(asctime)s %(name)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
|
| 37 |
+
handlers:
|
| 38 |
+
default:
|
| 39 |
+
formatter: default
|
| 40 |
+
class: logging.StreamHandler
|
| 41 |
+
stream: ext://sys.stderr
|
| 42 |
+
access:
|
| 43 |
+
formatter: access
|
| 44 |
+
class: logging.StreamHandler
|
| 45 |
+
stream: ext://sys.stdout
|
| 46 |
+
loggers:
|
| 47 |
+
root:
|
| 48 |
+
level: INFO
|
| 49 |
+
handlers:
|
| 50 |
+
- default
|
| 51 |
+
idl.service:
|
| 52 |
+
level: WARNING
|
| 53 |
+
opensearch:
|
| 54 |
+
level: WARNING
|
| 55 |
+
aiohttp.access:
|
| 56 |
+
level: WARNING
|
| 57 |
+
uvicorn.error:
|
| 58 |
+
level: INFO
|
| 59 |
+
handlers:
|
| 60 |
+
- default
|
| 61 |
+
propagate: no
|
| 62 |
+
uvicorn.access:
|
| 63 |
+
level: INFO
|
| 64 |
+
handlers:
|
| 65 |
+
- access
|
| 66 |
+
propagate: no
|
| 67 |
+
search_backend_observability:
|
| 68 |
+
level: INFO
|
| 69 |
+
handlers:
|
| 70 |
+
- default
|
| 71 |
+
propagate: no
|
| 72 |
+
websockets.client:
|
| 73 |
+
level: INFO
|
| 74 |
+
opentelemetry.sdk.trace:
|
| 75 |
+
level: WARNING
|
| 76 |
+
deepsearch_api.request_duration_logger:
|
| 77 |
+
level: INFO
|
| 78 |
+
handlers:
|
| 79 |
+
- default
|
| 80 |
+
propagate: no
|
docker/hf-space/nginx.conf.template
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Friendly external paths remapped to deepsearch-api's/asset-graph-service's
|
| 2 |
+
# actual routes β same mapping as
|
| 3 |
+
# usdsearch/helm/usdsearch/templates/api_gateway_config_map.yaml.
|
| 4 |
+
# deepsearch-api listens on 127.0.0.1:8000, asset-graph-service on
|
| 5 |
+
# 127.0.0.1:8010 (see docker/hf-space/supervisord.conf) β nginx is the sole
|
| 6 |
+
# process bound to the externally proxied port, so it can front every route
|
| 7 |
+
# with these rewrites plus CORS.
|
| 8 |
+
#
|
| 9 |
+
# The Explorer UI is served at `/` (root), not proxied to deepsearch-api at
|
| 10 |
+
# all β served directly from the static bundle baked into this image at
|
| 11 |
+
# build time (see the Dockerfile). Real difference from the multi-service
|
| 12 |
+
# deployment, which proxies /ui and /static/ to a *separate*
|
| 13 |
+
# deepsearch-explorer container: that bundle is just static files with
|
| 14 |
+
# same-origin relative API calls (REACT_APP_API_URL baked empty) and its
|
| 15 |
+
# own assets already use root-absolute paths (/static/js/..., /ds.png), so
|
| 16 |
+
# serving it directly at `/` β where it already expects to live β needs no
|
| 17 |
+
# extra process and no /ui prefix-stripping trick. The tradeoff: unlike the
|
| 18 |
+
# old single catch-all proxy, deepsearch-api's own root-level routes
|
| 19 |
+
# (/health, /docs, /redoc, /openapi.json, /metrics) now need to be listed
|
| 20 |
+
# explicitly below, since the final `location /` falls through to the SPA
|
| 21 |
+
# instead. Also sidesteps a real problem seen live: a Space deployed with
|
| 22 |
+
# `/` returning a 301 (redirecting to `/ui`, the previous approach) broke β
|
| 23 |
+
# root cause not fully confirmed, but consistent with Hugging Face's own
|
| 24 |
+
# readiness check wanting a real 200 at `/`, not a redirect.
|
| 25 |
+
#
|
| 26 |
+
# __PORT__ is substituted with $PORT at container start (supervisord.conf),
|
| 27 |
+
# matching the Spaces convention of an operator-configurable app_port.
|
| 28 |
+
|
| 29 |
+
# >>> simready-central slim explorer (http) >>>
|
| 30 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 31 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 32 |
+
# Included from nginx.conf's http{} (conf.d/*.conf) β an http-level directive is legal here.
|
| 33 |
+
# ONE level under /tmp on purpose: nginx mkdir()s only the last path component (no mkdir -p), and
|
| 34 |
+
# nothing in this image pre-creates a parent β /tmp/nginx-cache/slim-stats fails `nginx -t`. The
|
| 35 |
+
# `levels=` subdirectories are different: nginx creates those itself on the first write.
|
| 36 |
+
proxy_cache_path /tmp/slim-stats-cache levels=1 keys_zone=slim_stats_cache:2m max_size=256m inactive=1h use_temp_path=off;
|
| 37 |
+
# Thumbnails (/images): ~1,500 JPEGs of 27-52 KB (~75 MB) fit well inside 1g. Lives in the container's
|
| 38 |
+
# /tmp, so every deploy, restart or sleep starts it cold.
|
| 39 |
+
proxy_cache_path /tmp/slim-images-cache levels=1:2 keys_zone=slim_images_cache:10m max_size=1g inactive=7d use_temp_path=off;
|
| 40 |
+
# <<< simready-central slim explorer (http) <<<
|
| 41 |
+
server {
|
| 42 |
+
listen __PORT__;
|
| 43 |
+
|
| 44 |
+
proxy_http_version 1.1;
|
| 45 |
+
proxy_set_header Connection "";
|
| 46 |
+
proxy_connect_timeout 5s;
|
| 47 |
+
proxy_send_timeout 60s;
|
| 48 |
+
proxy_read_timeout 60s;
|
| 49 |
+
proxy_set_header Host $host;
|
| 50 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 51 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 52 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 53 |
+
|
| 54 |
+
add_header Access-Control-Allow-Origin "*" always;
|
| 55 |
+
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
| 56 |
+
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
| 57 |
+
|
| 58 |
+
location /search {
|
| 59 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 60 |
+
rewrite ^/search(.*)$ /v2/deepsearch/search$1 break;
|
| 61 |
+
proxy_pass http://127.0.0.1:8000;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
location /search_hybrid {
|
| 65 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 66 |
+
rewrite ^/search_hybrid(.*)$ /v3/deepsearch/search$1 break;
|
| 67 |
+
proxy_pass http://127.0.0.1:8000;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
location /vlm_validate/search_result {
|
| 71 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 72 |
+
rewrite ^/vlm_validate/search_result(.*)$ /v3/deepsearch/vlm_validate/search_result$1 break;
|
| 73 |
+
proxy_pass http://127.0.0.1:8000;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
location /llm_parse/ {
|
| 77 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 78 |
+
rewrite ^/llm_parse/(.*)$ /v3/deepsearch/llm_parse/$1 break;
|
| 79 |
+
proxy_pass http://127.0.0.1:8000;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
location /download {
|
| 83 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 84 |
+
rewrite ^/download(.*)$ /v3/download$1 break;
|
| 85 |
+
proxy_pass http://127.0.0.1:8000;
|
| 86 |
+
# Bundling an asset + all its dependencies can take longer than a single thumbnail.
|
| 87 |
+
proxy_read_timeout 300s;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
location /images {
|
| 91 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 92 |
+
rewrite ^/images(.*)$ /v3/images$1 break;
|
| 93 |
+
proxy_pass http://127.0.0.1:8000;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
location /static/ {
|
| 97 |
+
alias /usr/share/nginx/explorer/static/;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
# asset-graph-service routes (query-time graph reads only β see
|
| 101 |
+
# docker/Dockerfile.hf-space's header for why there's no graph-builder/
|
| 102 |
+
# asset_graph_generation worker in this image). Same rewrites as the
|
| 103 |
+
# real gateway config's asset_graph_service locations, just proxying to
|
| 104 |
+
# our own internal port instead of a separate service/Space.
|
| 105 |
+
location /asset_graph/usd/scene_summary/ {
|
| 106 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 107 |
+
rewrite ^/asset_graph/usd/scene_summary/(.*)$ /asset_graph/usd/scene_summary/$1 break;
|
| 108 |
+
proxy_pass http://127.0.0.1:8010;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
location /asset_graph/usd/scene_summary {
|
| 112 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 113 |
+
rewrite ^/asset_graph/usd/scene_summary(.*)$ /asset_graph/usd/scene_summary/$1 break;
|
| 114 |
+
proxy_pass http://127.0.0.1:8010;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
location /asset_graph/usd/prims {
|
| 118 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 119 |
+
rewrite ^/asset_graph/usd/prims(.*)$ /asset_graph/usd/prims$1 break;
|
| 120 |
+
proxy_pass http://127.0.0.1:8010;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
location /dependency_graph {
|
| 124 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 125 |
+
rewrite ^/dependency_graph(.*)$ /dependency_graph$1 break;
|
| 126 |
+
proxy_pass http://127.0.0.1:8010;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
# deepsearch-api's own root-level routes β explicit now that `location /`
|
| 130 |
+
# below serves the Explorer SPA instead of proxying everything
|
| 131 |
+
# unmatched. /docs and /redoc are prefix matches, not exact: Swagger
|
| 132 |
+
# UI/ReDoc can request sub-resources under their own path.
|
| 133 |
+
location = /health { proxy_pass http://127.0.0.1:8000; }
|
| 134 |
+
location = /openapi.json { proxy_pass http://127.0.0.1:8000; }
|
| 135 |
+
location = /metrics { proxy_pass http://127.0.0.1:8000; }
|
| 136 |
+
location /docs { proxy_pass http://127.0.0.1:8000; }
|
| 137 |
+
location /redoc { proxy_pass http://127.0.0.1:8000; }
|
| 138 |
+
|
| 139 |
+
# The Explorer SPA (see header comment) at root: try_files serves any of
|
| 140 |
+
# its own literal files (index.html, ds.png, favicon.ico, manifest.json,
|
| 141 |
+
# ...) directly, and falls back to index.html for anything else
|
| 142 |
+
# (client-side routing) β anything actually meant for the API above was
|
| 143 |
+
# already matched by a more specific location before nginx gets here.
|
| 144 |
+
# >>> simready-central slim explorer >>>
|
| 145 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 146 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 147 |
+
# services/explorer/dist is SimReady Central's search-only Explorer (repo simready-central,
|
| 148 |
+
# branch slim, slim/devspace/). The locations here are additive β exact matches, longer
|
| 149 |
+
# prefixes or a regex β and none of his locations is redefined. This block carries: /config.js,
|
| 150 |
+
# a no-cache /index.html, a 10-min
|
| 151 |
+
# proxy cache on /search/stats/usd_properties, a shared proxy cache on /images,
|
| 152 |
+
# /wikidata_hierarchy.json and JSON 404s for the routes this build does not have. What DOES
|
| 153 |
+
# reach beyond it: client_max_body_size 32m is server-wide (it applies to ALL his locations
|
| 154 |
+
# too); `= /images` takes the requests his `/images` prefix served before (same rewrite +
|
| 155 |
+
# proxy_pass, plus the cache); and an
|
| 156 |
+
# add_header inside one of our locations replaces his server-level headers for THOSE responses
|
| 157 |
+
# β /images and the stats cache re-declare his three CORS lines, /config.js and /index.html
|
| 158 |
+
# (same-origin UI files) do not. Applied by slim/devspace/deploy.sh.
|
| 159 |
+
client_max_body_size 32m; # query-image searches POST a base64 JPEG (nginx default 1 MB)
|
| 160 |
+
gzip_types text/css application/javascript application/json image/svg+xml; # stock gzip covers text/html only
|
| 161 |
+
|
| 162 |
+
# Runtime index registry (blocking <script src="/config.js">). The CORPUS_NAME marker is
|
| 163 |
+
# rendered by the same sed that renders the PORT marker (supervisord.conf). download:false β
|
| 164 |
+
# /download/asset needs the Storage API this image does not run. seedDefaultScope:false β
|
| 165 |
+
# boot browses the corpus.
|
| 166 |
+
# railFilters ["profiles","license","category"] + railDefaults ["profiles","license"]: rail = Profiles + License on,
|
| 167 |
+
# the Wikidata Category tree an OPT-IN row in Preferences βΊ Select Filters (owner, 2026-09-21); the parser catalog
|
| 168 |
+
# docker/hf-space/search_fields.yaml stays curated to profile + license. propertyAliases: this API stores the
|
| 169 |
+
# Wikidata code as semantic:wikidata_qcode:params:semanticData (wikidata_qcode= matches nothing here), so the
|
| 170 |
+
# Explorer translates its canonical name at the wire and renames the stats key back on arrival.
|
| 171 |
+
# propertyFilterCombine "and": the API ORs every filter_by_properties_include_any token globally, so with License
|
| 172 |
+
# pre-checked a Category pick would change nothing β a property with ONE checked value rides filter_by_properties
|
| 173 |
+
# (AND) instead; a multi-valued one (the tree's qcodes) stays the OR bucket. Measured: license 1488, one qcode 124,
|
| 174 |
+
# both in include_any 1488, license AND qcode 124.
|
| 175 |
+
location = /config.js {
|
| 176 |
+
default_type application/javascript;
|
| 177 |
+
add_header Cache-Control "no-cache, must-revalidate" always;
|
| 178 |
+
return 200 '(function(){var c="__CORPUS_NAME__";if(!c||c.indexOf("__")===0){c="usdsearch-dev";}window.__SERVER_MAPPING__={"usdsearch-dev":{"name":c+" (Hugging Face)","apiUrl":"","embedding_config":{"field_name":"siglip2-embedding.embedding","dimension":1536},"features":{"download":false,"s3UrlRemap":false,"seedDefaultScope":false,"railFilters":["profiles","license","category"],"railDefaults":["profiles","license"],"propertyAliases":{"wikidata_qcode":"semantic:wikidata_qcode:params:semanticData"},"propertyFilterCombine":"and","defaultFilters":{"simready_metadata_asset_license":["CC-BY-4.0"]}}}};window.__DEFAULT_SERVER__="usdsearch-dev";})();';
|
| 179 |
+
}
|
| 180 |
+
# index.html must always revalidate (covers the try_files fallback too): a swap or rollback
|
| 181 |
+
# must not leave a browser holding an index.html that names deleted chunks.
|
| 182 |
+
location = /index.html {
|
| 183 |
+
root /usr/share/nginx/explorer;
|
| 184 |
+
add_header Cache-Control "no-cache" always;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
# Property inventory (rail facets + parser grounding): multi-MB aggregation β cached 10 min.
|
| 188 |
+
location = /search/stats/usd_properties {
|
| 189 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 190 |
+
rewrite ^/search(.*)$ /v2/deepsearch/search$1 break;
|
| 191 |
+
proxy_pass http://127.0.0.1:8000;
|
| 192 |
+
proxy_ignore_headers Cache-Control Expires;
|
| 193 |
+
proxy_cache slim_stats_cache;
|
| 194 |
+
proxy_cache_key "$request_uri";
|
| 195 |
+
proxy_cache_valid 200 10m;
|
| 196 |
+
proxy_cache_lock on;
|
| 197 |
+
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
| 198 |
+
proxy_read_timeout 120s;
|
| 199 |
+
add_header X-Cache-Status $upstream_cache_status always;
|
| 200 |
+
# add_header above drops his server-level headers β re-declared as in his template
|
| 201 |
+
add_header Access-Control-Allow-Origin "*" always;
|
| 202 |
+
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
| 203 |
+
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
| 204 |
+
}
|
| 205 |
+
# Thumbnails: his `location /images` rewrite + proxy_pass (this exact match wins over his prefix)
|
| 206 |
+
# plus a SHARED cache β every cold /images costs the API ~150 CPU-ms on its one event loop.
|
| 207 |
+
# Keyed by the full URI (image_key, or asset_url + img_offset); the API's `Cache-Control: private`
|
| 208 |
+
# is ignored on purpose. Valid ONLY while the image runs with STORAGE_REQUIRE_AUTH=False (deploy.sh
|
| 209 |
+
# refuses a hub Dockerfile without it): with per-user storage auth, add
|
| 210 |
+
# `proxy_no_cache $arg_asset_url; proxy_cache_bypass $arg_asset_url;` so the access-checked
|
| 211 |
+
# asset_url path is never shared (image_key is content-addressed and unchecked). add_header here
|
| 212 |
+
# drops his server-level headers, so his three CORS lines are re-declared; proxy_hide_header
|
| 213 |
+
# keeps a chained nginx (check.sh --replica) to its own X-Cache-Status.
|
| 214 |
+
location = /images {
|
| 215 |
+
if ($request_method = OPTIONS) { add_header Content-Length 0 always; add_header Content-Type "text/plain" always; return 204; }
|
| 216 |
+
rewrite ^/images(.*)$ /v3/images$1 break;
|
| 217 |
+
proxy_pass http://127.0.0.1:8000;
|
| 218 |
+
proxy_cache slim_images_cache;
|
| 219 |
+
proxy_cache_key "$request_uri";
|
| 220 |
+
proxy_ignore_headers Cache-Control Expires;
|
| 221 |
+
proxy_hide_header X-Cache-Status;
|
| 222 |
+
proxy_cache_valid 200 7d;
|
| 223 |
+
proxy_cache_lock on;
|
| 224 |
+
proxy_cache_lock_timeout 15s;
|
| 225 |
+
proxy_cache_use_stale error timeout updating;
|
| 226 |
+
add_header X-Cache-Status $upstream_cache_status always;
|
| 227 |
+
add_header Access-Control-Allow-Origin "*" always;
|
| 228 |
+
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
| 229 |
+
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
| 230 |
+
}
|
| 231 |
+
# Our 404s are JSON like the API's own (a bare `return 404;` would be nginx's text/html error
|
| 232 |
+
# page): reachable only from the locations below, so nothing of his changes.
|
| 233 |
+
location @slim_not_found {
|
| 234 |
+
default_type application/json;
|
| 235 |
+
return 404 '{"detail":"Not Found"}';
|
| 236 |
+
}
|
| 237 |
+
# Per-corpus Wikidata hierarchy β the Category tree's ancestry: the bundle ships the corpus's file at its root
|
| 238 |
+
# (slim/scripts/build-explorer-public.sh, since 2026-09-21); absent β a real 404 and the Explorer falls back.
|
| 239 |
+
location = /wikidata_hierarchy.json {
|
| 240 |
+
root /usr/share/nginx/explorer;
|
| 241 |
+
try_files $uri @slim_not_found;
|
| 242 |
+
}
|
| 243 |
+
# The streaming validate shim does not exist here: a real 404 pins per-image validation through
|
| 244 |
+
# /vlm_validate/search_result (longer prefix, still his); a 502 or index.html would disable it.
|
| 245 |
+
location /vlm_validate/ {
|
| 246 |
+
default_type application/json;
|
| 247 |
+
return 404 '{"detail":"Not Found"}';
|
| 248 |
+
}
|
| 249 |
+
# Central's per-user surface is absent in this build: hard 404s, never the SPA's index.html.
|
| 250 |
+
location ~ ^/(auth|packs|prefs|chats|created_assets|concept_images|meshy_quota|info|process|folder_hierarchy\.json|api/create-chat|api/cad2simready)(/|$) {
|
| 251 |
+
default_type application/json;
|
| 252 |
+
return 404 '{"detail":"Not Found"}';
|
| 253 |
+
}
|
| 254 |
+
# <<< simready-central slim explorer <<<
|
| 255 |
+
location / {
|
| 256 |
+
root /usr/share/nginx/explorer;
|
| 257 |
+
try_files $uri $uri/ /index.html;
|
| 258 |
+
}
|
| 259 |
+
}
|
docker/hf-space/search_fields.yaml
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Trimmed to the two fields the SimReady Central slim Explorer's rail actually
|
| 5 |
+
# exposes (features.railFilters in docker/hf-space/nginx.conf.template's
|
| 6 |
+
# /config.js: Profiles + License) β see main@8c555a9dc920 ("Serve SimReady
|
| 7 |
+
# Central's slim Explorer at / β Profiles + License filtering..."), which
|
| 8 |
+
# replaced the full auto-derived catalog below with exactly these two for the
|
| 9 |
+
# same reason: deepsearch_api.llm_parse.fields loads this file for BOTH the
|
| 10 |
+
# LLM query parser and the catalog-driven rail facets, so a wider catalog here
|
| 11 |
+
# widens the parser AND the rail. `label:` on each field is preserved from
|
| 12 |
+
# that commit for the same reason (Explorer rail display name).
|
| 13 |
+
#
|
| 14 |
+
# `simready_metadata_mass`/`simready_metadata_rigid_body_count` added
|
| 15 |
+
# parser-only (no `label:`) β neither is in nginx.conf.template's
|
| 16 |
+
# railFilters allow-list, so they're invisible to the rail regardless of
|
| 17 |
+
# being registered here; only the LLM query parser picks them up. Verified
|
| 18 |
+
# live via /search/stats/usd_properties: mass is numeric (kg), continuous
|
| 19 |
+
# (1407 distinct values across 1484 of 1488 assets, range 0.001-217652.74,
|
| 20 |
+
# median ~1.77); rigid_body_count is numeric (integer), 37 distinct values
|
| 21 |
+
# across 1484 of 1488 assets, range 1-106, most common value 1 (904
|
| 22 |
+
# assets).
|
| 23 |
+
#
|
| 24 |
+
# The rest of the auto-generated catalog (40 fields total; regenerate via
|
| 25 |
+
# whatever produced this file's original derivation from the corpus's USD
|
| 26 |
+
# properties) is kept below, commented out, in case LLM-query-parsing over
|
| 27 |
+
# the full property set is wanted again later β see the header this replaces
|
| 28 |
+
# for how to regenerate it.
|
| 29 |
+
fields:
|
| 30 |
+
- name: simready_metadata_validation_profile
|
| 31 |
+
type: string
|
| 32 |
+
operators:
|
| 33 |
+
- eq
|
| 34 |
+
- contains
|
| 35 |
+
description: 'Auto-generated from USD property ''simready_metadata_validation__profile'' (1488 assets).
|
| 36 |
+
Common values: ''Robotics-Prop'' (1484), ''Prop-Robotics-Physx'' (4).'
|
| 37 |
+
examples:
|
| 38 |
+
- '"simready_metadata_validation_profile is Robotics-Prop" -> simready_metadata_validation_profile eq
|
| 39 |
+
"Robotics-Prop"'
|
| 40 |
+
label: Profiles
|
| 41 |
+
map:
|
| 42 |
+
kind: property_match
|
| 43 |
+
property: simready_metadata_validation__profile
|
| 44 |
+
- name: simready_metadata_asset_license
|
| 45 |
+
type: string
|
| 46 |
+
operators:
|
| 47 |
+
- eq
|
| 48 |
+
- contains
|
| 49 |
+
description: 'Auto-generated from USD property ''simready_metadata_asset_license'' (1488 assets). Common
|
| 50 |
+
values: ''CC-BY-4.0''.'
|
| 51 |
+
examples:
|
| 52 |
+
- '"simready_metadata_asset_license is CC-BY-4.0" -> simready_metadata_asset_license eq "CC-BY-4.0"'
|
| 53 |
+
label: License
|
| 54 |
+
map:
|
| 55 |
+
kind: property_match
|
| 56 |
+
property: simready_metadata_asset_license
|
| 57 |
+
- name: simready_metadata_mass
|
| 58 |
+
type: number
|
| 59 |
+
operators:
|
| 60 |
+
- gt
|
| 61 |
+
- gte
|
| 62 |
+
- lt
|
| 63 |
+
- lte
|
| 64 |
+
- eq
|
| 65 |
+
description: 'Auto-generated from USD property ''simready_metadata_mass'' (1484 of 1488 assets). Mass
|
| 66 |
+
in kilograms. Range 0.001-217652.74, median ~1.77.'
|
| 67 |
+
examples:
|
| 68 |
+
- '"simready_metadata_mass over 10" -> simready_metadata_mass gt 10'
|
| 69 |
+
map:
|
| 70 |
+
kind: property_numeric
|
| 71 |
+
property: simready_metadata_mass
|
| 72 |
+
- name: simready_metadata_rigid_body_count
|
| 73 |
+
type: number
|
| 74 |
+
operators:
|
| 75 |
+
- gt
|
| 76 |
+
- gte
|
| 77 |
+
- lt
|
| 78 |
+
- lte
|
| 79 |
+
- eq
|
| 80 |
+
description: 'Auto-generated from USD property ''simready_metadata_rigid_body_count'' (1484 of 1488
|
| 81 |
+
assets). Number of rigid bodies in the physics simulation. Integer, range 1-106, most common value 1
|
| 82 |
+
(904 assets).'
|
| 83 |
+
examples:
|
| 84 |
+
- '"simready_metadata_rigid_body_count over 5" -> simready_metadata_rigid_body_count gt 5'
|
| 85 |
+
map:
|
| 86 |
+
kind: property_numeric
|
| 87 |
+
property: simready_metadata_rigid_body_count
|
| 88 |
+
|
| 89 |
+
# --- auto-derived fields below, commented out (see header) ---
|
| 90 |
+
# - name: polygon_count_436
|
| 91 |
+
# type: number
|
| 92 |
+
# operators:
|
| 93 |
+
# - gt
|
| 94 |
+
# - gte
|
| 95 |
+
# - lt
|
| 96 |
+
# - lte
|
| 97 |
+
# - eq
|
| 98 |
+
# description: Auto-generated from USD property '__polygon_count' (1492 assets).
|
| 99 |
+
# examples:
|
| 100 |
+
# - '"polygon_count_436 over 10" -> polygon_count_436 gt 10'
|
| 101 |
+
# map:
|
| 102 |
+
# kind: property_numeric
|
| 103 |
+
# property: __polygon_count
|
| 104 |
+
# - name: simready_metadata_variants_physics_mu_jo_co_activate_option
|
| 105 |
+
# type: string
|
| 106 |
+
# operators:
|
| 107 |
+
# - eq
|
| 108 |
+
# - contains
|
| 109 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__MuJoCo__activateOption''
|
| 110 |
+
# (1492 assets). Common values: ''Enabled''.'
|
| 111 |
+
# examples:
|
| 112 |
+
# - '"simready_metadata_variants_physics_mu_jo_co_activate_option is Enabled" -> simready_metadata_variants_physics_mu_jo_co_activate_option
|
| 113 |
+
# eq "Enabled"'
|
| 114 |
+
# map:
|
| 115 |
+
# kind: property_match
|
| 116 |
+
# property: simready_metadata_Variants__Physics__MuJoCo__activateOption
|
| 117 |
+
# - name: simready_metadata_variants_physics_mu_jo_co_prim
|
| 118 |
+
# type: string
|
| 119 |
+
# operators:
|
| 120 |
+
# - eq
|
| 121 |
+
# - contains
|
| 122 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__MuJoCo__prim''
|
| 123 |
+
# (1492 assets). Common values: ''/RootNode''.'
|
| 124 |
+
# examples:
|
| 125 |
+
# - '"simready_metadata_variants_physics_mu_jo_co_prim is /RootNode" -> simready_metadata_variants_physics_mu_jo_co_prim
|
| 126 |
+
# eq "/RootNode"'
|
| 127 |
+
# map:
|
| 128 |
+
# kind: property_match
|
| 129 |
+
# property: simready_metadata_Variants__Physics__MuJoCo__prim
|
| 130 |
+
# - name: simready_metadata_variants_physics_mu_jo_co_variant_set_name
|
| 131 |
+
# type: string
|
| 132 |
+
# operators:
|
| 133 |
+
# - eq
|
| 134 |
+
# - contains
|
| 135 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__MuJoCo__variantSetName''
|
| 136 |
+
# (1492 assets). Common values: ''MuJoCo''.'
|
| 137 |
+
# examples:
|
| 138 |
+
# - '"simready_metadata_variants_physics_mu_jo_co_variant_set_name is MuJoCo" -> simready_metadata_variants_physics_mu_jo_co_variant_set_name
|
| 139 |
+
# eq "MuJoCo"'
|
| 140 |
+
# map:
|
| 141 |
+
# kind: property_match
|
| 142 |
+
# property: simready_metadata_Variants__Physics__MuJoCo__variantSetName
|
| 143 |
+
# - name: simready_metadata_variants_physics_newton_activate_option
|
| 144 |
+
# type: string
|
| 145 |
+
# operators:
|
| 146 |
+
# - eq
|
| 147 |
+
# - contains
|
| 148 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__Newton__activateOption''
|
| 149 |
+
# (1492 assets). Common values: ''Enabled''.'
|
| 150 |
+
# examples:
|
| 151 |
+
# - '"simready_metadata_variants_physics_newton_activate_option is Enabled" -> simready_metadata_variants_physics_newton_activate_option
|
| 152 |
+
# eq "Enabled"'
|
| 153 |
+
# map:
|
| 154 |
+
# kind: property_match
|
| 155 |
+
# property: simready_metadata_Variants__Physics__Newton__activateOption
|
| 156 |
+
# - name: simready_metadata_variants_physics_newton_prim
|
| 157 |
+
# type: string
|
| 158 |
+
# operators:
|
| 159 |
+
# - eq
|
| 160 |
+
# - contains
|
| 161 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__Newton__prim''
|
| 162 |
+
# (1492 assets). Common values: ''/RootNode''.'
|
| 163 |
+
# examples:
|
| 164 |
+
# - '"simready_metadata_variants_physics_newton_prim is /RootNode" -> simready_metadata_variants_physics_newton_prim
|
| 165 |
+
# eq "/RootNode"'
|
| 166 |
+
# map:
|
| 167 |
+
# kind: property_match
|
| 168 |
+
# property: simready_metadata_Variants__Physics__Newton__prim
|
| 169 |
+
# - name: simready_metadata_variants_physics_newton_variant_set_name
|
| 170 |
+
# type: string
|
| 171 |
+
# operators:
|
| 172 |
+
# - eq
|
| 173 |
+
# - contains
|
| 174 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__Newton__variantSetName''
|
| 175 |
+
# (1492 assets). Common values: ''Newton''.'
|
| 176 |
+
# examples:
|
| 177 |
+
# - '"simready_metadata_variants_physics_newton_variant_set_name is Newton" -> simready_metadata_variants_physics_newton_variant_set_name
|
| 178 |
+
# eq "Newton"'
|
| 179 |
+
# map:
|
| 180 |
+
# kind: property_match
|
| 181 |
+
# property: simready_metadata_Variants__Physics__Newton__variantSetName
|
| 182 |
+
# - name: simready_metadata_variants_physics_phys_x_activate_option
|
| 183 |
+
# type: string
|
| 184 |
+
# operators:
|
| 185 |
+
# - eq
|
| 186 |
+
# - contains
|
| 187 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__PhysX__activateOption''
|
| 188 |
+
# (1492 assets). Common values: ''Enabled''.'
|
| 189 |
+
# examples:
|
| 190 |
+
# - '"simready_metadata_variants_physics_phys_x_activate_option is Enabled" -> simready_metadata_variants_physics_phys_x_activate_option
|
| 191 |
+
# eq "Enabled"'
|
| 192 |
+
# map:
|
| 193 |
+
# kind: property_match
|
| 194 |
+
# property: simready_metadata_Variants__Physics__PhysX__activateOption
|
| 195 |
+
# - name: simready_metadata_variants_physics_phys_x_prim
|
| 196 |
+
# type: string
|
| 197 |
+
# operators:
|
| 198 |
+
# - eq
|
| 199 |
+
# - contains
|
| 200 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__PhysX__prim''
|
| 201 |
+
# (1492 assets). Common values: ''/RootNode''.'
|
| 202 |
+
# examples:
|
| 203 |
+
# - '"simready_metadata_variants_physics_phys_x_prim is /RootNode" -> simready_metadata_variants_physics_phys_x_prim
|
| 204 |
+
# eq "/RootNode"'
|
| 205 |
+
# map:
|
| 206 |
+
# kind: property_match
|
| 207 |
+
# property: simready_metadata_Variants__Physics__PhysX__prim
|
| 208 |
+
# - name: simready_metadata_variants_physics_phys_x_variant_set_name
|
| 209 |
+
# type: string
|
| 210 |
+
# operators:
|
| 211 |
+
# - eq
|
| 212 |
+
# - contains
|
| 213 |
+
# description: 'Auto-generated from USD property ''simready_metadata_Variants__Physics__PhysX__variantSetName''
|
| 214 |
+
# (1492 assets). Common values: ''PhysX''.'
|
| 215 |
+
# examples:
|
| 216 |
+
# - '"simready_metadata_variants_physics_phys_x_variant_set_name is PhysX" -> simready_metadata_variants_physics_phys_x_variant_set_name
|
| 217 |
+
# eq "PhysX"'
|
| 218 |
+
# map:
|
| 219 |
+
# kind: property_match
|
| 220 |
+
# property: simready_metadata_Variants__Physics__PhysX__variantSetName
|
| 221 |
+
# - name: simready_metadata_asset_type
|
| 222 |
+
# type: string
|
| 223 |
+
# operators:
|
| 224 |
+
# - eq
|
| 225 |
+
# - contains
|
| 226 |
+
# description: 'Auto-generated from USD property ''simready_metadata_asset_type'' (1492 assets). Common
|
| 227 |
+
# values: ''prop''.'
|
| 228 |
+
# examples:
|
| 229 |
+
# - '"simready_metadata_asset_type is prop" -> simready_metadata_asset_type eq "prop"'
|
| 230 |
+
# map:
|
| 231 |
+
# kind: property_match
|
| 232 |
+
# property: simready_metadata_asset_type
|
| 233 |
+
# - name: simready_metadata_validation_profile_version
|
| 234 |
+
# type: string
|
| 235 |
+
# operators:
|
| 236 |
+
# - eq
|
| 237 |
+
# - contains
|
| 238 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__profile_version'' (1492
|
| 239 |
+
# assets). Common values: ''3.1.0'', ''1.0.0'', ''3.2.0''.'
|
| 240 |
+
# examples:
|
| 241 |
+
# - '"simready_metadata_validation_profile_version is 3.1.0" -> simready_metadata_validation_profile_version
|
| 242 |
+
# eq "3.1.0"'
|
| 243 |
+
# map:
|
| 244 |
+
# kind: property_match
|
| 245 |
+
# property: simready_metadata_validation__profile_version
|
| 246 |
+
# - name: simready_metadata_author
|
| 247 |
+
# type: string
|
| 248 |
+
# operators:
|
| 249 |
+
# - eq
|
| 250 |
+
# - contains
|
| 251 |
+
# description: 'Auto-generated from USD property ''simready_metadata_author'' (1488 assets). Common values:
|
| 252 |
+
# ''NVIDIA''.'
|
| 253 |
+
# examples:
|
| 254 |
+
# - '"simready_metadata_author is NVIDIA" -> simready_metadata_author eq "NVIDIA"'
|
| 255 |
+
# map:
|
| 256 |
+
# kind: property_match
|
| 257 |
+
# property: simready_metadata_author
|
| 258 |
+
# - name: simready_metadata_category
|
| 259 |
+
# type: string
|
| 260 |
+
# operators:
|
| 261 |
+
# - eq
|
| 262 |
+
# - contains
|
| 263 |
+
# description: 'Auto-generated from USD property ''simready_metadata_category'' (1488 assets). Common
|
| 264 |
+
# values: ''prop'', ''candle'', ''book'', ''glasses''.'
|
| 265 |
+
# examples:
|
| 266 |
+
# - '"simready_metadata_category is prop" -> simready_metadata_category eq "prop"'
|
| 267 |
+
# map:
|
| 268 |
+
# kind: property_match
|
| 269 |
+
# property: simready_metadata_category
|
| 270 |
+
# - name: simready_metadata_mass
|
| 271 |
+
# type: number
|
| 272 |
+
# operators:
|
| 273 |
+
# - gt
|
| 274 |
+
# - gte
|
| 275 |
+
# - lt
|
| 276 |
+
# - lte
|
| 277 |
+
# - eq
|
| 278 |
+
# description: Auto-generated from USD property 'simready_metadata_mass' (1488 assets).
|
| 279 |
+
# examples:
|
| 280 |
+
# - '"simready_metadata_mass over 10" -> simready_metadata_mass gt 10'
|
| 281 |
+
# map:
|
| 282 |
+
# kind: property_numeric
|
| 283 |
+
# property: simready_metadata_mass
|
| 284 |
+
# - name: simready_metadata_rigid_body_count
|
| 285 |
+
# type: number
|
| 286 |
+
# operators:
|
| 287 |
+
# - gt
|
| 288 |
+
# - gte
|
| 289 |
+
# - lt
|
| 290 |
+
# - lte
|
| 291 |
+
# - eq
|
| 292 |
+
# description: Auto-generated from USD property 'simready_metadata_rigid_body_count' (1488 assets).
|
| 293 |
+
# examples:
|
| 294 |
+
# - '"simready_metadata_rigid_body_count over 10" -> simready_metadata_rigid_body_count gt 10'
|
| 295 |
+
# map:
|
| 296 |
+
# kind: property_numeric
|
| 297 |
+
# property: simready_metadata_rigid_body_count
|
| 298 |
+
# - name: semantic_wikidata_class_params_semantic_type
|
| 299 |
+
# type: string
|
| 300 |
+
# operators:
|
| 301 |
+
# - eq
|
| 302 |
+
# - contains
|
| 303 |
+
# description: 'Auto-generated from USD property ''semantic:wikidata_class:params:semanticType'' (1264
|
| 304 |
+
# assets). Common values: ''wikidata_class''.'
|
| 305 |
+
# examples:
|
| 306 |
+
# - '"semantic_wikidata_class_params_semantic_type is wikidata_class" -> semantic_wikidata_class_params_semantic_type
|
| 307 |
+
# eq "wikidata_class"'
|
| 308 |
+
# map:
|
| 309 |
+
# kind: property_match
|
| 310 |
+
# property: semantic:wikidata_class:params:semanticType
|
| 311 |
+
# - name: semantic_wikidata_qcode_params_semantic_type
|
| 312 |
+
# type: string
|
| 313 |
+
# operators:
|
| 314 |
+
# - eq
|
| 315 |
+
# - contains
|
| 316 |
+
# description: 'Auto-generated from USD property ''semantic:wikidata_qcode:params:semanticType'' (1264
|
| 317 |
+
# assets). Common values: ''wikidata_qcode''.'
|
| 318 |
+
# examples:
|
| 319 |
+
# - '"semantic_wikidata_qcode_params_semantic_type is wikidata_qcode" -> semantic_wikidata_qcode_params_semantic_type
|
| 320 |
+
# eq "wikidata_qcode"'
|
| 321 |
+
# map:
|
| 322 |
+
# kind: property_match
|
| 323 |
+
# property: semantic:wikidata_qcode:params:semanticType
|
| 324 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_dependencies
|
| 325 |
+
# type: string
|
| 326 |
+
# operators:
|
| 327 |
+
# - eq
|
| 328 |
+
# - contains
|
| 329 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__dependencies''
|
| 330 |
+
# (1000 assets). Common values: "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]".'
|
| 331 |
+
# examples:
|
| 332 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_dependencies is [{''FET_000_STANDARD'':
|
| 333 |
+
# {''version'': ''0.1.0''}}]" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_dependencies
|
| 334 |
+
# eq "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]"'
|
| 335 |
+
# map:
|
| 336 |
+
# kind: property_match
|
| 337 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__dependencies
|
| 338 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_optional_requirements
|
| 339 |
+
# type: string
|
| 340 |
+
# operators:
|
| 341 |
+
# - eq
|
| 342 |
+
# - contains
|
| 343 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__optional
|
| 344 |
+
# requirements'' (1000 assets). Common values: "[''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.008'']",
|
| 345 |
+
# "[''com.nvidia.simready.RV.008'', ''com.nvidia.simready.RV.010'']".'
|
| 346 |
+
# examples:
|
| 347 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_optional_requirements
|
| 348 |
+
# is [''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.008'']" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_optional_requirements
|
| 349 |
+
# eq "[''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.008'']"'
|
| 350 |
+
# map:
|
| 351 |
+
# kind: property_match
|
| 352 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__optional requirements
|
| 353 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_passed
|
| 354 |
+
# type: boolean
|
| 355 |
+
# operators:
|
| 356 |
+
# - eq
|
| 357 |
+
# - exists
|
| 358 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__passed'
|
| 359 |
+
# (1000 assets).
|
| 360 |
+
# examples:
|
| 361 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_passed
|
| 362 |
+
# eq true'
|
| 363 |
+
# map:
|
| 364 |
+
# kind: property_exists
|
| 365 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__passed
|
| 366 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_version
|
| 367 |
+
# type: string
|
| 368 |
+
# operators:
|
| 369 |
+
# - eq
|
| 370 |
+
# - contains
|
| 371 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__version''
|
| 372 |
+
# (1000 assets). Common values: ''0.1.0''.'
|
| 373 |
+
# examples:
|
| 374 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_version is 0.1.0" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_mujoco_version
|
| 375 |
+
# eq "0.1.0"'
|
| 376 |
+
# map:
|
| 377 |
+
# kind: property_match
|
| 378 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_MUJOCO__version
|
| 379 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_dependencies
|
| 380 |
+
# type: string
|
| 381 |
+
# operators:
|
| 382 |
+
# - eq
|
| 383 |
+
# - contains
|
| 384 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__dependencies''
|
| 385 |
+
# (1000 assets). Common values: "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]".'
|
| 386 |
+
# examples:
|
| 387 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_dependencies is [{''FET_000_STANDARD'':
|
| 388 |
+
# {''version'': ''0.1.0''}}]" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_dependencies
|
| 389 |
+
# eq "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]"'
|
| 390 |
+
# map:
|
| 391 |
+
# kind: property_match
|
| 392 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__dependencies
|
| 393 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_optional_requirements
|
| 394 |
+
# type: string
|
| 395 |
+
# operators:
|
| 396 |
+
# - eq
|
| 397 |
+
# - contains
|
| 398 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__optional
|
| 399 |
+
# requirements'' (1000 assets). Common values: "[''com.nvidia.simready.RV.005'', ''com.nvidia.simready.RV.010'']",
|
| 400 |
+
# "[''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.005'']".'
|
| 401 |
+
# examples:
|
| 402 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_optional_requirements
|
| 403 |
+
# is [''com.nvidia.simready.RV.005'', ''com.nvidia.simready.RV.010'']" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_optional_requirements
|
| 404 |
+
# eq "[''com.nvidia.simready.RV.005'', ''com.nvidia.simready.RV.010'']"'
|
| 405 |
+
# map:
|
| 406 |
+
# kind: property_match
|
| 407 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__optional requirements
|
| 408 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_passed
|
| 409 |
+
# type: boolean
|
| 410 |
+
# operators:
|
| 411 |
+
# - eq
|
| 412 |
+
# - exists
|
| 413 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__passed'
|
| 414 |
+
# (1000 assets).
|
| 415 |
+
# examples:
|
| 416 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_passed
|
| 417 |
+
# eq true'
|
| 418 |
+
# map:
|
| 419 |
+
# kind: property_exists
|
| 420 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__passed
|
| 421 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_version
|
| 422 |
+
# type: string
|
| 423 |
+
# operators:
|
| 424 |
+
# - eq
|
| 425 |
+
# - contains
|
| 426 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__version''
|
| 427 |
+
# (1000 assets). Common values: ''0.1.0''.'
|
| 428 |
+
# examples:
|
| 429 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_version is 0.1.0" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_newton_version
|
| 430 |
+
# eq "0.1.0"'
|
| 431 |
+
# map:
|
| 432 |
+
# kind: property_match
|
| 433 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_NEWTON__version
|
| 434 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_dependencies
|
| 435 |
+
# type: string
|
| 436 |
+
# operators:
|
| 437 |
+
# - eq
|
| 438 |
+
# - contains
|
| 439 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__dependencies''
|
| 440 |
+
# (1000 assets). Common values: "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]".'
|
| 441 |
+
# examples:
|
| 442 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_dependencies is [{''FET_000_STANDARD'':
|
| 443 |
+
# {''version'': ''0.1.0''}}]" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_dependencies
|
| 444 |
+
# eq "[{''FET_000_STANDARD'': {''version'': ''0.1.0''}}]"'
|
| 445 |
+
# map:
|
| 446 |
+
# kind: property_match
|
| 447 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__dependencies
|
| 448 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_optional_requirements
|
| 449 |
+
# type: string
|
| 450 |
+
# operators:
|
| 451 |
+
# - eq
|
| 452 |
+
# - contains
|
| 453 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__optional
|
| 454 |
+
# requirements'' (1000 assets). Common values: "[''com.nvidia.simready.RV.001'', ''com.nvidia.simready.RV.002'',
|
| 455 |
+
# ''com.nvidia.simready.RV.010'']", "[''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.001'',
|
| 456 |
+
# ''com.nvidia.simready.RV.002'']", "[''com.nvidia.simready.RV.002'', ''com.nvidia.simready.RV.001'',
|
| 457 |
+
# ''com.nvidia.simready.RV.010'']", "[''com.nvidia.simready.RV.001'', ''com.nvidia.simready.RV.010'',
|
| 458 |
+
# ''com.nvidia.simready.RV.002'']", "[''com.nvidia.simready.RV.002'', ''com.nvidia.simready.RV.010'',
|
| 459 |
+
# ''com.nvidia.simready.RV.001'']", "[''com.nvidia.simready.RV.010'', ''com.nvidia.simready.RV.002'',
|
| 460 |
+
# ''com.nvidia.simready.RV.001'']".'
|
| 461 |
+
# examples:
|
| 462 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_optional_requirements is
|
| 463 |
+
# [''com.nvidia.simready.RV.001'', ''com.nvidia.simready.RV.002'', ''com.nvidia.simready.RV.010'']"
|
| 464 |
+
# -> simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_optional_requirements
|
| 465 |
+
# eq "[''com.nvidia.simready.RV.001'', ''com.nvidia.simready.RV.002'', ''com.nvidia.simready.RV.010'']"'
|
| 466 |
+
# map:
|
| 467 |
+
# kind: property_match
|
| 468 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__optional requirements
|
| 469 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_passed
|
| 470 |
+
# type: boolean
|
| 471 |
+
# operators:
|
| 472 |
+
# - eq
|
| 473 |
+
# - exists
|
| 474 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__passed'
|
| 475 |
+
# (1000 assets).
|
| 476 |
+
# examples:
|
| 477 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_passed
|
| 478 |
+
# eq true'
|
| 479 |
+
# map:
|
| 480 |
+
# kind: property_exists
|
| 481 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__passed
|
| 482 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_version
|
| 483 |
+
# type: string
|
| 484 |
+
# operators:
|
| 485 |
+
# - eq
|
| 486 |
+
# - contains
|
| 487 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__version''
|
| 488 |
+
# (1000 assets). Common values: ''0.1.0''.'
|
| 489 |
+
# examples:
|
| 490 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_version is 0.1.0" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_physx_version
|
| 491 |
+
# eq "0.1.0"'
|
| 492 |
+
# map:
|
| 493 |
+
# kind: property_match
|
| 494 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_PHYSX__version
|
| 495 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_dependencies
|
| 496 |
+
# type: string
|
| 497 |
+
# operators:
|
| 498 |
+
# - eq
|
| 499 |
+
# - contains
|
| 500 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__dependencies''
|
| 501 |
+
# (1000 assets). Common values: ''[]''.'
|
| 502 |
+
# examples:
|
| 503 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_dependencies is []"
|
| 504 |
+
# -> simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_dependencies eq "[]"'
|
| 505 |
+
# map:
|
| 506 |
+
# kind: property_match
|
| 507 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__dependencies
|
| 508 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_passed
|
| 509 |
+
# type: boolean
|
| 510 |
+
# operators:
|
| 511 |
+
# - eq
|
| 512 |
+
# - exists
|
| 513 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__passed'
|
| 514 |
+
# (1000 assets).
|
| 515 |
+
# examples:
|
| 516 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_passed
|
| 517 |
+
# eq true'
|
| 518 |
+
# map:
|
| 519 |
+
# kind: property_exists
|
| 520 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__passed
|
| 521 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_version
|
| 522 |
+
# type: string
|
| 523 |
+
# operators:
|
| 524 |
+
# - eq
|
| 525 |
+
# - contains
|
| 526 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__version''
|
| 527 |
+
# (1000 assets). Common values: ''0.1.0''.'
|
| 528 |
+
# examples:
|
| 529 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_version is 0.1.0" ->
|
| 530 |
+
# simready_metadata_validation_validated_features_2026_08_18_fet_000_standard_version eq "0.1.0"'
|
| 531 |
+
# map:
|
| 532 |
+
# kind: property_match
|
| 533 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_000_STANDARD__version
|
| 534 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_dependencies
|
| 535 |
+
# type: string
|
| 536 |
+
# operators:
|
| 537 |
+
# - eq
|
| 538 |
+
# - contains
|
| 539 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__dependencies''
|
| 540 |
+
# (1000 assets). Common values: ''[]''.'
|
| 541 |
+
# examples:
|
| 542 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_dependencies is []"
|
| 543 |
+
# -> simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_dependencies eq "[]"'
|
| 544 |
+
# map:
|
| 545 |
+
# kind: property_match
|
| 546 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__dependencies
|
| 547 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_passed
|
| 548 |
+
# type: boolean
|
| 549 |
+
# operators:
|
| 550 |
+
# - eq
|
| 551 |
+
# - exists
|
| 552 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__passed'
|
| 553 |
+
# (1000 assets).
|
| 554 |
+
# examples:
|
| 555 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_passed
|
| 556 |
+
# eq true'
|
| 557 |
+
# map:
|
| 558 |
+
# kind: property_exists
|
| 559 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__passed
|
| 560 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_version
|
| 561 |
+
# type: string
|
| 562 |
+
# operators:
|
| 563 |
+
# - eq
|
| 564 |
+
# - contains
|
| 565 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__version''
|
| 566 |
+
# (1000 assets). Common values: ''1.0.1''.'
|
| 567 |
+
# examples:
|
| 568 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_version is 1.0.1" ->
|
| 569 |
+
# simready_metadata_validation_validated_features_2026_08_18_fet_001_standard_version eq "1.0.1"'
|
| 570 |
+
# map:
|
| 571 |
+
# kind: property_match
|
| 572 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_001_STANDARD__version
|
| 573 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_dependencies
|
| 574 |
+
# type: string
|
| 575 |
+
# operators:
|
| 576 |
+
# - eq
|
| 577 |
+
# - contains
|
| 578 |
+
# description: 'Auto-generated from USD property ''simready_metadata_validation__validated_features__2026-08-18__FET_003_MUJOCO__dependencies''
|
| 579 |
+
# (1000 assets). Common values: ''[]''.'
|
| 580 |
+
# examples:
|
| 581 |
+
# - '"simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_dependencies is []" ->
|
| 582 |
+
# simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_dependencies eq "[]"'
|
| 583 |
+
# map:
|
| 584 |
+
# kind: property_match
|
| 585 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_003_MUJOCO__dependencies
|
| 586 |
+
# - name: simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_passed
|
| 587 |
+
# type: boolean
|
| 588 |
+
# operators:
|
| 589 |
+
# - eq
|
| 590 |
+
# - exists
|
| 591 |
+
# description: Auto-generated from USD property 'simready_metadata_validation__validated_features__2026-08-18__FET_003_MUJOCO__passed'
|
| 592 |
+
# (1000 assets).
|
| 593 |
+
# examples:
|
| 594 |
+
# - '"with simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_passed" -> simready_metadata_validation_validated_features_2026_08_18_fet_003_mujoco_passed
|
| 595 |
+
# eq true'
|
| 596 |
+
# map:
|
| 597 |
+
# kind: property_exists
|
| 598 |
+
# property: simready_metadata_validation__validated_features__2026-08-18__FET_003_MUJOCO__passed
|
| 599 |
+
#
|
docker/hf-space/siglip2-model/PACKAGE-LICENSES/siglip2-giant-opt-patch16-384.txt
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
docker/hf-space/siglip2-model/SOURCE.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by scripts/extract_siglip2_model.sh β do not hand-edit.
|
| 2 |
+
source_repo=../usdsearch
|
| 3 |
+
source_commit=555bee64e6f4d12995ab49313d8a1487cae97c84
|
| 4 |
+
extracted_at=2026-09-23T08:29:31Z
|
docker/hf-space/siglip2-model/model_repo/ensemble_image_model/1/_.txt
ADDED
|
File without changes
|
docker/hf-space/siglip2-model/model_repo/ensemble_image_model/config.pbtxt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "ensemble_image_model"
|
| 2 |
+
platform: "ensemble"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "raw_image"
|
| 8 |
+
data_type: TYPE_UINT8
|
| 9 |
+
dims: [-1, -1, 3]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "image_embeds"
|
| 16 |
+
data_type: TYPE_FP32
|
| 17 |
+
dims: [1536]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
ensemble_scheduling {
|
| 22 |
+
step [
|
| 23 |
+
{
|
| 24 |
+
model_name: "image_preprocessing"
|
| 25 |
+
model_version: -1
|
| 26 |
+
input_map {
|
| 27 |
+
key: "raw_image"
|
| 28 |
+
value: "raw_image"
|
| 29 |
+
}
|
| 30 |
+
output_map {
|
| 31 |
+
key: "pixel_values"
|
| 32 |
+
value: "pixel_values"
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
model_name: "siglip2_vision_encoder_onnx"
|
| 37 |
+
model_version: -1
|
| 38 |
+
input_map {
|
| 39 |
+
key: "pixel_values"
|
| 40 |
+
value: "pixel_values"
|
| 41 |
+
}
|
| 42 |
+
output_map {
|
| 43 |
+
key: "image_embeds"
|
| 44 |
+
value: "image_embeds"
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
]
|
| 48 |
+
}
|
docker/hf-space/siglip2-model/model_repo/ensemble_text_model/1/_.txt
ADDED
|
File without changes
|
docker/hf-space/siglip2-model/model_repo/ensemble_text_model/config.pbtxt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "ensemble_text_model"
|
| 2 |
+
platform: "ensemble"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "text"
|
| 8 |
+
data_type: TYPE_STRING
|
| 9 |
+
dims: [1]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "embeddings"
|
| 16 |
+
data_type: TYPE_FP32
|
| 17 |
+
dims: [1536]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
ensemble_scheduling {
|
| 22 |
+
step [
|
| 23 |
+
{
|
| 24 |
+
model_name: "tokenizer"
|
| 25 |
+
model_version: -1
|
| 26 |
+
input_map {
|
| 27 |
+
key: "text"
|
| 28 |
+
value: "text"
|
| 29 |
+
}
|
| 30 |
+
output_map {
|
| 31 |
+
key: "input_ids"
|
| 32 |
+
value: "input_ids"
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
model_name: "siglip2_text_encoder_onnx"
|
| 37 |
+
model_version: -1
|
| 38 |
+
input_map {
|
| 39 |
+
key: "input_ids"
|
| 40 |
+
value: "input_ids"
|
| 41 |
+
}
|
| 42 |
+
output_map {
|
| 43 |
+
key: "embeddings"
|
| 44 |
+
value: "embeddings"
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
]
|
| 48 |
+
}
|
docker/hf-space/siglip2-model/model_repo/image_preprocessing/1/model.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
# standard modules
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
# third party modules
|
| 22 |
+
import numpy as np
|
| 23 |
+
import triton_python_backend_utils as pb_utils
|
| 24 |
+
from PIL import Image
|
| 25 |
+
|
| 26 |
+
# PIL resampling enum values
|
| 27 |
+
_RESAMPLE_MAP = {
|
| 28 |
+
0: Image.NEAREST,
|
| 29 |
+
1: Image.LANCZOS,
|
| 30 |
+
2: Image.BILINEAR,
|
| 31 |
+
3: Image.BICUBIC,
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class TritonPythonModel:
|
| 36 |
+
"""Image preprocessor using PIL resize + vectorized numpy rescale/normalize.
|
| 37 |
+
|
| 38 |
+
Uses direct PIL + numpy operations to avoid torch dependency in the
|
| 39 |
+
Triton server container (CPU-only torch conflicts with ONNX Runtime CUDA).
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def initialize(self, args: dict) -> None:
|
| 43 |
+
model_config = json.loads(args["model_config"])
|
| 44 |
+
|
| 45 |
+
current_dir = Path(__file__).parent
|
| 46 |
+
preprocessor_config_path = os.path.join(current_dir, "preprocessor_config", "preprocessor_config.json")
|
| 47 |
+
with open(preprocessor_config_path, "r") as f:
|
| 48 |
+
config = json.load(f)
|
| 49 |
+
|
| 50 |
+
self.height = config["size"]["height"]
|
| 51 |
+
self.width = config["size"]["width"]
|
| 52 |
+
self.rescale_factor = np.float32(config.get("rescale_factor", 1.0 / 255.0))
|
| 53 |
+
self.image_mean = np.array(config.get("image_mean", [0.5, 0.5, 0.5]), dtype=np.float32).reshape(3, 1, 1)
|
| 54 |
+
self.image_std = np.array(config.get("image_std", [0.5, 0.5, 0.5]), dtype=np.float32).reshape(3, 1, 1)
|
| 55 |
+
self.do_resize = config.get("do_resize", True)
|
| 56 |
+
self.do_rescale = config.get("do_rescale", True)
|
| 57 |
+
self.do_normalize = config.get("do_normalize", True)
|
| 58 |
+
self.resample = _RESAMPLE_MAP.get(config.get("resample", 2), Image.BILINEAR)
|
| 59 |
+
|
| 60 |
+
data_type = model_config["output"][0]["data_type"]
|
| 61 |
+
self.output_dtype = pb_utils.triton_string_to_numpy(data_type)
|
| 62 |
+
|
| 63 |
+
def _process_single(self, img_hwc: np.ndarray) -> np.ndarray:
|
| 64 |
+
"""Process a single HWC uint8 image to CHW float32."""
|
| 65 |
+
if self.do_resize:
|
| 66 |
+
pil_img = Image.fromarray(img_hwc)
|
| 67 |
+
pil_img = pil_img.resize((self.width, self.height), self.resample)
|
| 68 |
+
img_hwc = np.asarray(pil_img)
|
| 69 |
+
|
| 70 |
+
# HWC -> CHW
|
| 71 |
+
img = img_hwc.transpose(2, 0, 1).astype(np.float32)
|
| 72 |
+
|
| 73 |
+
if self.do_rescale:
|
| 74 |
+
img *= self.rescale_factor
|
| 75 |
+
|
| 76 |
+
if self.do_normalize:
|
| 77 |
+
img = (img - self.image_mean) / self.image_std
|
| 78 |
+
|
| 79 |
+
return img
|
| 80 |
+
|
| 81 |
+
def execute(self, requests: list) -> list:
|
| 82 |
+
responses = []
|
| 83 |
+
for request in requests:
|
| 84 |
+
image_tensor = pb_utils.get_input_tensor_by_name(request, "raw_image")
|
| 85 |
+
images = image_tensor.as_numpy()
|
| 86 |
+
|
| 87 |
+
batch_size = images.shape[0]
|
| 88 |
+
pixel_values = np.empty((batch_size, 3, self.height, self.width), dtype=self.output_dtype)
|
| 89 |
+
|
| 90 |
+
for i in range(batch_size):
|
| 91 |
+
pixel_values[i] = self._process_single(images[i])
|
| 92 |
+
|
| 93 |
+
inference_response = pb_utils.InferenceResponse(
|
| 94 |
+
output_tensors=[pb_utils.Tensor("pixel_values", pixel_values)]
|
| 95 |
+
)
|
| 96 |
+
responses.append(inference_response)
|
| 97 |
+
|
| 98 |
+
return responses
|
| 99 |
+
|
| 100 |
+
def finalize(self) -> None:
|
| 101 |
+
print("Cleaning up image preprocessor...")
|
docker/hf-space/siglip2-model/model_repo/image_preprocessing/1/preprocessor_config/preprocessor_config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_convert_rgb": null,
|
| 3 |
+
"do_normalize": true,
|
| 4 |
+
"do_rescale": true,
|
| 5 |
+
"do_resize": true,
|
| 6 |
+
"image_mean": [
|
| 7 |
+
0.5,
|
| 8 |
+
0.5,
|
| 9 |
+
0.5
|
| 10 |
+
],
|
| 11 |
+
"image_processor_type": "SiglipImageProcessor",
|
| 12 |
+
"image_std": [
|
| 13 |
+
0.5,
|
| 14 |
+
0.5,
|
| 15 |
+
0.5
|
| 16 |
+
],
|
| 17 |
+
"processor_class": "SiglipProcessor",
|
| 18 |
+
"resample": 2,
|
| 19 |
+
"rescale_factor": 0.00392156862745098,
|
| 20 |
+
"size": {
|
| 21 |
+
"height": 384,
|
| 22 |
+
"width": 384
|
| 23 |
+
}
|
| 24 |
+
}
|
docker/hf-space/siglip2-model/model_repo/image_preprocessing/config.pbtxt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "image_preprocessing"
|
| 2 |
+
backend: "python"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "raw_image"
|
| 8 |
+
data_type: TYPE_UINT8
|
| 9 |
+
dims: [-1, -1, 3]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "pixel_values"
|
| 16 |
+
data_type: TYPE_FP32
|
| 17 |
+
dims: [3, 384, 384]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
dynamic_batching {
|
| 22 |
+
preferred_batch_size: [4, 8, 16]
|
| 23 |
+
max_queue_delay_microseconds: 50000
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
instance_group [
|
| 27 |
+
{
|
| 28 |
+
count: 4
|
| 29 |
+
kind: KIND_CPU
|
| 30 |
+
}
|
| 31 |
+
]
|
docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "siglip2_text_encoder_onnx"
|
| 2 |
+
backend: "onnxruntime"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "input_ids"
|
| 8 |
+
data_type: TYPE_INT64
|
| 9 |
+
dims: [64]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "embeddings"
|
| 16 |
+
data_type: TYPE_FP32
|
| 17 |
+
dims: [1536]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
dynamic_batching {
|
| 22 |
+
preferred_batch_size: [8, 16, 32]
|
| 23 |
+
max_queue_delay_microseconds: 50000
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
instance_group [
|
| 27 |
+
{
|
| 28 |
+
count: 2
|
| 29 |
+
kind: KIND_AUTO
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
parameters {
|
| 34 |
+
key: "execution_accelerators"
|
| 35 |
+
value: { string_value: "cuda:0" }
|
| 36 |
+
}
|
| 37 |
+
parameters {
|
| 38 |
+
key: "enable_mem_pattern"
|
| 39 |
+
value: { string_value: "1" }
|
| 40 |
+
}
|
| 41 |
+
parameters {
|
| 42 |
+
key: "enable_mem_reuse"
|
| 43 |
+
value: { string_value: "1" }
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
optimization {
|
| 47 |
+
graph {
|
| 48 |
+
level: 0
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
model_warmup [
|
| 53 |
+
{
|
| 54 |
+
name: "warmup_batch_1"
|
| 55 |
+
batch_size: 1
|
| 56 |
+
inputs [
|
| 57 |
+
{
|
| 58 |
+
key: "input_ids"
|
| 59 |
+
value: {
|
| 60 |
+
data_type: TYPE_INT64
|
| 61 |
+
dims: [64]
|
| 62 |
+
zero_data: true
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
]
|
docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "siglip2_vision_encoder_onnx"
|
| 2 |
+
backend: "onnxruntime"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "pixel_values"
|
| 8 |
+
data_type: TYPE_FP32
|
| 9 |
+
dims: [3, 384, 384]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "image_embeds"
|
| 16 |
+
data_type: TYPE_FP32
|
| 17 |
+
dims: [1536]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
dynamic_batching {
|
| 22 |
+
preferred_batch_size: [4, 8, 16]
|
| 23 |
+
max_queue_delay_microseconds: 50000
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
instance_group [
|
| 27 |
+
{
|
| 28 |
+
count: 2
|
| 29 |
+
kind: KIND_AUTO
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
parameters {
|
| 34 |
+
key: "execution_accelerators"
|
| 35 |
+
value: { string_value: "cuda:0" }
|
| 36 |
+
}
|
| 37 |
+
parameters {
|
| 38 |
+
key: "enable_mem_pattern"
|
| 39 |
+
value: { string_value: "1" }
|
| 40 |
+
}
|
| 41 |
+
parameters {
|
| 42 |
+
key: "enable_mem_reuse"
|
| 43 |
+
value: { string_value: "1" }
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
optimization {
|
| 47 |
+
graph {
|
| 48 |
+
level: 0
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
model_warmup [
|
| 53 |
+
{
|
| 54 |
+
name: "warmup_batch_1"
|
| 55 |
+
batch_size: 1
|
| 56 |
+
inputs {
|
| 57 |
+
key: "pixel_values"
|
| 58 |
+
value: {
|
| 59 |
+
data_type: TYPE_FP32
|
| 60 |
+
dims: [3, 384, 384]
|
| 61 |
+
zero_data: true
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
]
|
docker/hf-space/siglip2-model/model_repo/tokenizer/1/model.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
# standard modules
|
| 17 |
+
import json
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
# third party modules
|
| 21 |
+
import numpy as np
|
| 22 |
+
import triton_python_backend_utils as pb_utils
|
| 23 |
+
from tokenizers import Tokenizer
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TritonPythonModel:
|
| 27 |
+
"""Triton Python backend model for SigLIP2 tokenizer."""
|
| 28 |
+
|
| 29 |
+
def initialize(self, args):
|
| 30 |
+
model_config = json.loads(args["model_config"])
|
| 31 |
+
|
| 32 |
+
current_dir = Path(__file__).parent
|
| 33 |
+
tokenizer_file = current_dir / "tokenizer" / "tokenizer.json"
|
| 34 |
+
|
| 35 |
+
# Load tokenizer directly from tokenizer.json using tokenizers library
|
| 36 |
+
self.tokenizer = Tokenizer.from_file(str(tokenizer_file))
|
| 37 |
+
|
| 38 |
+
# Enable padding and truncation
|
| 39 |
+
self.tokenizer.enable_padding(length=64, pad_id=0, pad_token="<pad>")
|
| 40 |
+
self.tokenizer.enable_truncation(max_length=64)
|
| 41 |
+
|
| 42 |
+
# Resolve output dtypes from model config
|
| 43 |
+
self.output_dtypes = {}
|
| 44 |
+
for output_config in model_config["output"]:
|
| 45 |
+
name = output_config["name"]
|
| 46 |
+
data_type = output_config["data_type"]
|
| 47 |
+
self.output_dtypes[name] = pb_utils.triton_string_to_numpy(data_type)
|
| 48 |
+
|
| 49 |
+
def execute(self, requests: list) -> list:
|
| 50 |
+
responses = []
|
| 51 |
+
for request in requests:
|
| 52 |
+
# Get input text
|
| 53 |
+
text_tensor = pb_utils.get_input_tensor_by_name(request, "text")
|
| 54 |
+
text_data = text_tensor.as_numpy()
|
| 55 |
+
|
| 56 |
+
# Decode bytes to strings if necessary
|
| 57 |
+
if text_data.dtype == np.object_:
|
| 58 |
+
texts = [t.decode("utf-8") if isinstance(t, bytes) else t for t in text_data.flatten()]
|
| 59 |
+
else:
|
| 60 |
+
texts = text_data.flatten().tolist()
|
| 61 |
+
|
| 62 |
+
# Tokenize using tokenizers library
|
| 63 |
+
encoded = self.tokenizer.encode_batch(texts)
|
| 64 |
+
|
| 65 |
+
# Extract input IDs from encodings
|
| 66 |
+
input_ids_dtype = self.output_dtypes.get("input_ids", np.int64)
|
| 67 |
+
input_ids = np.array([enc.ids for enc in encoded], dtype=input_ids_dtype)
|
| 68 |
+
|
| 69 |
+
# Create output tensors
|
| 70 |
+
input_ids_tensor = pb_utils.Tensor("input_ids", input_ids)
|
| 71 |
+
|
| 72 |
+
# Create response
|
| 73 |
+
inference_response = pb_utils.InferenceResponse(output_tensors=[input_ids_tensor])
|
| 74 |
+
responses.append(inference_response)
|
| 75 |
+
|
| 76 |
+
return responses
|
| 77 |
+
|
| 78 |
+
def finalize(self):
|
| 79 |
+
"""Clean up resources."""
|
| 80 |
+
print("Cleaning up tokenizer...")
|
docker/hf-space/siglip2-model/model_repo/tokenizer/1/tokenizer/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fc1870feba58b2d0cbcc75bfcb5124ee5c1202c5a64fdde45d473d228f1f35ae
|
| 3 |
+
size 34363171
|
docker/hf-space/siglip2-model/model_repo/tokenizer/config.pbtxt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "tokenizer"
|
| 2 |
+
backend: "python"
|
| 3 |
+
max_batch_size: 32
|
| 4 |
+
|
| 5 |
+
input [
|
| 6 |
+
{
|
| 7 |
+
name: "text"
|
| 8 |
+
data_type: TYPE_STRING
|
| 9 |
+
dims: [1]
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
output [
|
| 14 |
+
{
|
| 15 |
+
name: "input_ids"
|
| 16 |
+
data_type: TYPE_INT64
|
| 17 |
+
dims: [64]
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
dynamic_batching {
|
| 22 |
+
preferred_batch_size: [8, 16, 32]
|
| 23 |
+
max_queue_delay_microseconds: 50000
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
instance_group [
|
| 27 |
+
{
|
| 28 |
+
count: 4
|
| 29 |
+
kind: KIND_CPU
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
response_cache {
|
| 34 |
+
enable: true
|
| 35 |
+
}
|
docker/hf-space/siglip2-model/pyproject.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "siglip2-triton-server"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Server dependencies for SigLIP2 Triton"
|
| 5 |
+
requires-python = ">=3.11,<3.14"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"tokenizers>=0.22.2",
|
| 8 |
+
"transformers>=4.57.1",
|
| 9 |
+
"pillow>=12.3.0",
|
| 10 |
+
"numpy",
|
| 11 |
+
"wheel>=0.46.3",
|
| 12 |
+
"urllib3>=2.7.0",
|
| 13 |
+
"starlette>=1.3.1",
|
| 14 |
+
]
|
docker/hf-space/siglip2-model/uv.lock
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version = 1
|
| 2 |
+
revision = 3
|
| 3 |
+
requires-python = ">=3.11, <3.14"
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "annotated-doc"
|
| 7 |
+
version = "0.0.4"
|
| 8 |
+
source = { registry = "https://pypi.org/simple" }
|
| 9 |
+
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
| 10 |
+
wheels = [
|
| 11 |
+
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[[package]]
|
| 15 |
+
name = "anyio"
|
| 16 |
+
version = "4.13.0"
|
| 17 |
+
source = { registry = "https://pypi.org/simple" }
|
| 18 |
+
dependencies = [
|
| 19 |
+
{ name = "idna" },
|
| 20 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
| 21 |
+
]
|
| 22 |
+
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
| 23 |
+
wheels = [
|
| 24 |
+
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
[[package]]
|
| 28 |
+
name = "certifi"
|
| 29 |
+
version = "2026.4.22"
|
| 30 |
+
source = { registry = "https://pypi.org/simple" }
|
| 31 |
+
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
| 32 |
+
wheels = [
|
| 33 |
+
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
[[package]]
|
| 37 |
+
name = "click"
|
| 38 |
+
version = "8.3.3"
|
| 39 |
+
source = { registry = "https://pypi.org/simple" }
|
| 40 |
+
dependencies = [
|
| 41 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 42 |
+
]
|
| 43 |
+
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
| 44 |
+
wheels = [
|
| 45 |
+
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
[[package]]
|
| 49 |
+
name = "colorama"
|
| 50 |
+
version = "0.4.6"
|
| 51 |
+
source = { registry = "https://pypi.org/simple" }
|
| 52 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
| 53 |
+
wheels = [
|
| 54 |
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
[[package]]
|
| 58 |
+
name = "filelock"
|
| 59 |
+
version = "3.29.0"
|
| 60 |
+
source = { registry = "https://pypi.org/simple" }
|
| 61 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
|
| 62 |
+
wheels = [
|
| 63 |
+
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
[[package]]
|
| 67 |
+
name = "fsspec"
|
| 68 |
+
version = "2026.4.0"
|
| 69 |
+
source = { registry = "https://pypi.org/simple" }
|
| 70 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" }
|
| 71 |
+
wheels = [
|
| 72 |
+
{ url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
[[package]]
|
| 76 |
+
name = "h11"
|
| 77 |
+
version = "0.16.0"
|
| 78 |
+
source = { registry = "https://pypi.org/simple" }
|
| 79 |
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
| 80 |
+
wheels = [
|
| 81 |
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
[[package]]
|
| 85 |
+
name = "hf-xet"
|
| 86 |
+
version = "1.5.0"
|
| 87 |
+
source = { registry = "https://pypi.org/simple" }
|
| 88 |
+
sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
|
| 89 |
+
wheels = [
|
| 90 |
+
{ url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
|
| 91 |
+
{ url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
|
| 92 |
+
{ url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
|
| 93 |
+
{ url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
|
| 94 |
+
{ url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
|
| 95 |
+
{ url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
|
| 96 |
+
{ url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
|
| 97 |
+
{ url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
|
| 98 |
+
{ url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
|
| 99 |
+
{ url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
|
| 100 |
+
{ url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
|
| 101 |
+
{ url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
|
| 102 |
+
{ url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
|
| 103 |
+
{ url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
|
| 104 |
+
{ url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
|
| 105 |
+
{ url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
[[package]]
|
| 109 |
+
name = "httpcore"
|
| 110 |
+
version = "1.0.9"
|
| 111 |
+
source = { registry = "https://pypi.org/simple" }
|
| 112 |
+
dependencies = [
|
| 113 |
+
{ name = "certifi" },
|
| 114 |
+
{ name = "h11" },
|
| 115 |
+
]
|
| 116 |
+
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
| 117 |
+
wheels = [
|
| 118 |
+
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
[[package]]
|
| 122 |
+
name = "httpx"
|
| 123 |
+
version = "0.28.1"
|
| 124 |
+
source = { registry = "https://pypi.org/simple" }
|
| 125 |
+
dependencies = [
|
| 126 |
+
{ name = "anyio" },
|
| 127 |
+
{ name = "certifi" },
|
| 128 |
+
{ name = "httpcore" },
|
| 129 |
+
{ name = "idna" },
|
| 130 |
+
]
|
| 131 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
| 132 |
+
wheels = [
|
| 133 |
+
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
| 134 |
+
]
|
| 135 |
+
|
| 136 |
+
[[package]]
|
| 137 |
+
name = "huggingface-hub"
|
| 138 |
+
version = "1.14.0"
|
| 139 |
+
source = { registry = "https://pypi.org/simple" }
|
| 140 |
+
dependencies = [
|
| 141 |
+
{ name = "filelock" },
|
| 142 |
+
{ name = "fsspec" },
|
| 143 |
+
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
| 144 |
+
{ name = "httpx" },
|
| 145 |
+
{ name = "packaging" },
|
| 146 |
+
{ name = "pyyaml" },
|
| 147 |
+
{ name = "tqdm" },
|
| 148 |
+
{ name = "typer" },
|
| 149 |
+
{ name = "typing-extensions" },
|
| 150 |
+
]
|
| 151 |
+
sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" }
|
| 152 |
+
wheels = [
|
| 153 |
+
{ url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" },
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
[[package]]
|
| 157 |
+
name = "idna"
|
| 158 |
+
version = "3.18"
|
| 159 |
+
source = { registry = "https://pypi.org/simple" }
|
| 160 |
+
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
| 161 |
+
wheels = [
|
| 162 |
+
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
[[package]]
|
| 166 |
+
name = "markdown-it-py"
|
| 167 |
+
version = "4.2.0"
|
| 168 |
+
source = { registry = "https://pypi.org/simple" }
|
| 169 |
+
dependencies = [
|
| 170 |
+
{ name = "mdurl" },
|
| 171 |
+
]
|
| 172 |
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
| 173 |
+
wheels = [
|
| 174 |
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
[[package]]
|
| 178 |
+
name = "mdurl"
|
| 179 |
+
version = "0.1.2"
|
| 180 |
+
source = { registry = "https://pypi.org/simple" }
|
| 181 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
| 182 |
+
wheels = [
|
| 183 |
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
[[package]]
|
| 187 |
+
name = "numpy"
|
| 188 |
+
version = "2.4.6"
|
| 189 |
+
source = { registry = "https://pypi.org/simple" }
|
| 190 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
| 191 |
+
wheels = [
|
| 192 |
+
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
| 193 |
+
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
| 194 |
+
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
| 195 |
+
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
| 196 |
+
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
|
| 197 |
+
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
|
| 198 |
+
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
|
| 199 |
+
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
|
| 200 |
+
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
| 201 |
+
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
| 202 |
+
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
| 203 |
+
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
| 204 |
+
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
| 205 |
+
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
| 206 |
+
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
| 207 |
+
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
| 208 |
+
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
| 209 |
+
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
| 210 |
+
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
| 211 |
+
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
| 212 |
+
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
| 213 |
+
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
| 214 |
+
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
| 215 |
+
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
| 216 |
+
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
| 217 |
+
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
| 218 |
+
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
| 219 |
+
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
| 220 |
+
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
| 221 |
+
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
| 222 |
+
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
| 223 |
+
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
| 224 |
+
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
| 225 |
+
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
| 226 |
+
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
| 227 |
+
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
| 228 |
+
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
| 229 |
+
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
| 230 |
+
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
| 231 |
+
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
| 232 |
+
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
| 233 |
+
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
| 234 |
+
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
| 235 |
+
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
| 236 |
+
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
| 237 |
+
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
| 238 |
+
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
| 239 |
+
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
|
| 240 |
+
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
|
| 241 |
+
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
| 242 |
+
]
|
| 243 |
+
|
| 244 |
+
[[package]]
|
| 245 |
+
name = "packaging"
|
| 246 |
+
version = "26.2"
|
| 247 |
+
source = { registry = "https://pypi.org/simple" }
|
| 248 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
| 249 |
+
wheels = [
|
| 250 |
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
| 251 |
+
]
|
| 252 |
+
|
| 253 |
+
[[package]]
|
| 254 |
+
name = "pillow"
|
| 255 |
+
version = "12.3.0"
|
| 256 |
+
source = { registry = "https://pypi.org/simple" }
|
| 257 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
| 258 |
+
wheels = [
|
| 259 |
+
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
|
| 260 |
+
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
|
| 261 |
+
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
|
| 262 |
+
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
|
| 263 |
+
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
|
| 264 |
+
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
|
| 265 |
+
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
|
| 266 |
+
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
|
| 267 |
+
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
|
| 268 |
+
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
| 269 |
+
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
| 270 |
+
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
| 271 |
+
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
| 272 |
+
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
| 273 |
+
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
| 274 |
+
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
| 275 |
+
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
| 276 |
+
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
| 277 |
+
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
|
| 278 |
+
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
|
| 279 |
+
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
|
| 280 |
+
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
|
| 281 |
+
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
|
| 282 |
+
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
|
| 283 |
+
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
|
| 284 |
+
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
|
| 285 |
+
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
|
| 286 |
+
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
|
| 287 |
+
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
|
| 288 |
+
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
|
| 289 |
+
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
|
| 290 |
+
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
|
| 291 |
+
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
|
| 292 |
+
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
|
| 293 |
+
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
[[package]]
|
| 297 |
+
name = "pygments"
|
| 298 |
+
version = "2.20.0"
|
| 299 |
+
source = { registry = "https://pypi.org/simple" }
|
| 300 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
| 301 |
+
wheels = [
|
| 302 |
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
| 303 |
+
]
|
| 304 |
+
|
| 305 |
+
[[package]]
|
| 306 |
+
name = "pyyaml"
|
| 307 |
+
version = "6.0.3"
|
| 308 |
+
source = { registry = "https://pypi.org/simple" }
|
| 309 |
+
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
| 310 |
+
wheels = [
|
| 311 |
+
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
| 312 |
+
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
| 313 |
+
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
| 314 |
+
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
| 315 |
+
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
| 316 |
+
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
| 317 |
+
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
| 318 |
+
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
| 319 |
+
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
| 320 |
+
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
| 321 |
+
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
| 322 |
+
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
| 323 |
+
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
| 324 |
+
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
| 325 |
+
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
| 326 |
+
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
| 327 |
+
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
| 328 |
+
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
| 329 |
+
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
| 330 |
+
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
| 331 |
+
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
| 332 |
+
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
| 333 |
+
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
| 334 |
+
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
| 335 |
+
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
| 336 |
+
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
| 337 |
+
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
| 338 |
+
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
| 339 |
+
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
| 340 |
+
]
|
| 341 |
+
|
| 342 |
+
[[package]]
|
| 343 |
+
name = "regex"
|
| 344 |
+
version = "2026.5.9"
|
| 345 |
+
source = { registry = "https://pypi.org/simple" }
|
| 346 |
+
sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" }
|
| 347 |
+
wheels = [
|
| 348 |
+
{ url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" },
|
| 349 |
+
{ url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" },
|
| 350 |
+
{ url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" },
|
| 351 |
+
{ url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" },
|
| 352 |
+
{ url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" },
|
| 353 |
+
{ url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" },
|
| 354 |
+
{ url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" },
|
| 355 |
+
{ url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" },
|
| 356 |
+
{ url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" },
|
| 357 |
+
{ url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" },
|
| 358 |
+
{ url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" },
|
| 359 |
+
{ url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" },
|
| 360 |
+
{ url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" },
|
| 361 |
+
{ url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" },
|
| 362 |
+
{ url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" },
|
| 363 |
+
{ url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" },
|
| 364 |
+
{ url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" },
|
| 365 |
+
{ url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" },
|
| 366 |
+
{ url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" },
|
| 367 |
+
{ url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" },
|
| 368 |
+
{ url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" },
|
| 369 |
+
{ url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" },
|
| 370 |
+
{ url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" },
|
| 371 |
+
{ url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" },
|
| 372 |
+
{ url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" },
|
| 373 |
+
{ url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" },
|
| 374 |
+
{ url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" },
|
| 375 |
+
{ url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" },
|
| 376 |
+
{ url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" },
|
| 377 |
+
{ url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" },
|
| 378 |
+
{ url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" },
|
| 379 |
+
{ url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" },
|
| 380 |
+
{ url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" },
|
| 381 |
+
{ url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" },
|
| 382 |
+
{ url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" },
|
| 383 |
+
{ url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" },
|
| 384 |
+
{ url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" },
|
| 385 |
+
{ url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" },
|
| 386 |
+
{ url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" },
|
| 387 |
+
{ url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" },
|
| 388 |
+
{ url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" },
|
| 389 |
+
{ url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" },
|
| 390 |
+
{ url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" },
|
| 391 |
+
{ url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" },
|
| 392 |
+
{ url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" },
|
| 393 |
+
{ url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" },
|
| 394 |
+
{ url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" },
|
| 395 |
+
{ url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" },
|
| 396 |
+
{ url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" },
|
| 397 |
+
{ url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" },
|
| 398 |
+
{ url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" },
|
| 399 |
+
{ url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" },
|
| 400 |
+
{ url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" },
|
| 401 |
+
{ url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" },
|
| 402 |
+
{ url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" },
|
| 403 |
+
{ url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" },
|
| 404 |
+
{ url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" },
|
| 405 |
+
{ url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" },
|
| 406 |
+
{ url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" },
|
| 407 |
+
{ url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" },
|
| 408 |
+
{ url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" },
|
| 409 |
+
{ url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" },
|
| 410 |
+
{ url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" },
|
| 411 |
+
{ url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" },
|
| 412 |
+
]
|
| 413 |
+
|
| 414 |
+
[[package]]
|
| 415 |
+
name = "rich"
|
| 416 |
+
version = "15.0.0"
|
| 417 |
+
source = { registry = "https://pypi.org/simple" }
|
| 418 |
+
dependencies = [
|
| 419 |
+
{ name = "markdown-it-py" },
|
| 420 |
+
{ name = "pygments" },
|
| 421 |
+
]
|
| 422 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
| 423 |
+
wheels = [
|
| 424 |
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
| 425 |
+
]
|
| 426 |
+
|
| 427 |
+
[[package]]
|
| 428 |
+
name = "safetensors"
|
| 429 |
+
version = "0.7.0"
|
| 430 |
+
source = { registry = "https://pypi.org/simple" }
|
| 431 |
+
sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
|
| 432 |
+
wheels = [
|
| 433 |
+
{ url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
|
| 434 |
+
{ url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
|
| 435 |
+
{ url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
|
| 436 |
+
{ url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
|
| 437 |
+
{ url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
|
| 438 |
+
{ url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
|
| 439 |
+
{ url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
|
| 440 |
+
{ url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
|
| 442 |
+
{ url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
|
| 443 |
+
{ url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
|
| 444 |
+
{ url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
|
| 445 |
+
{ url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
|
| 446 |
+
{ url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
|
| 447 |
+
]
|
| 448 |
+
|
| 449 |
+
[[package]]
|
| 450 |
+
name = "shellingham"
|
| 451 |
+
version = "1.5.4"
|
| 452 |
+
source = { registry = "https://pypi.org/simple" }
|
| 453 |
+
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
| 454 |
+
wheels = [
|
| 455 |
+
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
| 456 |
+
]
|
| 457 |
+
|
| 458 |
+
[[package]]
|
| 459 |
+
name = "siglip2-triton-server"
|
| 460 |
+
version = "0.1.0"
|
| 461 |
+
source = { virtual = "." }
|
| 462 |
+
dependencies = [
|
| 463 |
+
{ name = "numpy" },
|
| 464 |
+
{ name = "pillow" },
|
| 465 |
+
{ name = "starlette" },
|
| 466 |
+
{ name = "tokenizers" },
|
| 467 |
+
{ name = "transformers" },
|
| 468 |
+
{ name = "urllib3" },
|
| 469 |
+
{ name = "wheel" },
|
| 470 |
+
]
|
| 471 |
+
|
| 472 |
+
[package.metadata]
|
| 473 |
+
requires-dist = [
|
| 474 |
+
{ name = "numpy" },
|
| 475 |
+
{ name = "pillow", specifier = ">=12.3.0" },
|
| 476 |
+
{ name = "starlette", specifier = ">=1.3.1" },
|
| 477 |
+
{ name = "tokenizers", specifier = ">=0.22.2" },
|
| 478 |
+
{ name = "transformers", specifier = ">=4.57.1" },
|
| 479 |
+
{ name = "urllib3", specifier = ">=2.7.0" },
|
| 480 |
+
{ name = "wheel", specifier = ">=0.46.3" },
|
| 481 |
+
]
|
| 482 |
+
|
| 483 |
+
[[package]]
|
| 484 |
+
name = "starlette"
|
| 485 |
+
version = "1.3.1"
|
| 486 |
+
source = { registry = "https://pypi.org/simple" }
|
| 487 |
+
dependencies = [
|
| 488 |
+
{ name = "anyio" },
|
| 489 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
| 490 |
+
]
|
| 491 |
+
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
|
| 492 |
+
wheels = [
|
| 493 |
+
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
|
| 494 |
+
]
|
| 495 |
+
|
| 496 |
+
[[package]]
|
| 497 |
+
name = "tokenizers"
|
| 498 |
+
version = "0.22.2"
|
| 499 |
+
source = { registry = "https://pypi.org/simple" }
|
| 500 |
+
dependencies = [
|
| 501 |
+
{ name = "huggingface-hub" },
|
| 502 |
+
]
|
| 503 |
+
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
|
| 504 |
+
wheels = [
|
| 505 |
+
{ url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
|
| 506 |
+
{ url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
|
| 507 |
+
{ url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
|
| 508 |
+
{ url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
|
| 509 |
+
{ url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
|
| 510 |
+
{ url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
|
| 511 |
+
{ url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
|
| 512 |
+
{ url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
|
| 513 |
+
{ url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
|
| 514 |
+
{ url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
|
| 515 |
+
{ url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
|
| 516 |
+
{ url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
|
| 517 |
+
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
|
| 518 |
+
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
|
| 519 |
+
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
|
| 520 |
+
]
|
| 521 |
+
|
| 522 |
+
[[package]]
|
| 523 |
+
name = "tqdm"
|
| 524 |
+
version = "4.67.3"
|
| 525 |
+
source = { registry = "https://pypi.org/simple" }
|
| 526 |
+
dependencies = [
|
| 527 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
| 528 |
+
]
|
| 529 |
+
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
| 530 |
+
wheels = [
|
| 531 |
+
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
| 532 |
+
]
|
| 533 |
+
|
| 534 |
+
[[package]]
|
| 535 |
+
name = "transformers"
|
| 536 |
+
version = "5.8.1"
|
| 537 |
+
source = { registry = "https://pypi.org/simple" }
|
| 538 |
+
dependencies = [
|
| 539 |
+
{ name = "huggingface-hub" },
|
| 540 |
+
{ name = "numpy" },
|
| 541 |
+
{ name = "packaging" },
|
| 542 |
+
{ name = "pyyaml" },
|
| 543 |
+
{ name = "regex" },
|
| 544 |
+
{ name = "safetensors" },
|
| 545 |
+
{ name = "tokenizers" },
|
| 546 |
+
{ name = "tqdm" },
|
| 547 |
+
{ name = "typer" },
|
| 548 |
+
]
|
| 549 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" }
|
| 550 |
+
wheels = [
|
| 551 |
+
{ url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" },
|
| 552 |
+
]
|
| 553 |
+
|
| 554 |
+
[[package]]
|
| 555 |
+
name = "typer"
|
| 556 |
+
version = "0.25.1"
|
| 557 |
+
source = { registry = "https://pypi.org/simple" }
|
| 558 |
+
dependencies = [
|
| 559 |
+
{ name = "annotated-doc" },
|
| 560 |
+
{ name = "click" },
|
| 561 |
+
{ name = "rich" },
|
| 562 |
+
{ name = "shellingham" },
|
| 563 |
+
]
|
| 564 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
|
| 565 |
+
wheels = [
|
| 566 |
+
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
|
| 567 |
+
]
|
| 568 |
+
|
| 569 |
+
[[package]]
|
| 570 |
+
name = "typing-extensions"
|
| 571 |
+
version = "4.15.0"
|
| 572 |
+
source = { registry = "https://pypi.org/simple" }
|
| 573 |
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
| 574 |
+
wheels = [
|
| 575 |
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
| 576 |
+
]
|
| 577 |
+
|
| 578 |
+
[[package]]
|
| 579 |
+
name = "urllib3"
|
| 580 |
+
version = "2.7.0"
|
| 581 |
+
source = { registry = "https://pypi.org/simple" }
|
| 582 |
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
| 583 |
+
wheels = [
|
| 584 |
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
| 585 |
+
]
|
| 586 |
+
|
| 587 |
+
[[package]]
|
| 588 |
+
name = "wheel"
|
| 589 |
+
version = "0.47.0"
|
| 590 |
+
source = { registry = "https://pypi.org/simple" }
|
| 591 |
+
dependencies = [
|
| 592 |
+
{ name = "packaging" },
|
| 593 |
+
]
|
| 594 |
+
sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" }
|
| 595 |
+
wheels = [
|
| 596 |
+
{ url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" },
|
| 597 |
+
]
|
docker/hf-space/supervisord-gpu.conf
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
; GPU variant of supervisord.conf (docker/Dockerfile.hf-space-gpu): the same
|
| 2 |
+
; six programs as the NVCF/cpu-basic variant, plus [program:siglip2-triton]
|
| 3 |
+
; (restores its ONNX weights from S3, then execs the local siglip2-triton
|
| 4 |
+
; server β see its own comment), and deepsearch-api's own command
|
| 5 |
+
; additionally waits on Triton's readiness before execing uvicorn β same
|
| 6 |
+
; "poll a real endpoint the process only starts once ready" idiom already
|
| 7 |
+
; used below for neo4j/opensearch. See
|
| 8 |
+
; docs/plans/siglip2-embedded-hf-space.md for the decision behind this file.
|
| 9 |
+
; Verified live end to end (2026-09-23), including through a real bake.py
|
| 10 |
+
; run and its own --gpus all smoke test.
|
| 11 |
+
|
| 12 |
+
[supervisord]
|
| 13 |
+
nodaemon=true
|
| 14 |
+
logfile=/dev/stdout
|
| 15 |
+
logfile_maxbytes=0
|
| 16 |
+
pidfile=/tmp/supervisord.pid
|
| 17 |
+
|
| 18 |
+
[program:opensearch]
|
| 19 |
+
command=/usr/share/opensearch/opensearch-docker-entrypoint.sh opensearch
|
| 20 |
+
directory=/usr/share/opensearch
|
| 21 |
+
user=1000
|
| 22 |
+
autorestart=true
|
| 23 |
+
startsecs=5
|
| 24 |
+
stdout_logfile=/dev/stdout
|
| 25 |
+
stdout_logfile_maxbytes=0
|
| 26 |
+
stderr_logfile=/dev/stderr
|
| 27 |
+
stderr_logfile_maxbytes=0
|
| 28 |
+
|
| 29 |
+
[program:redis]
|
| 30 |
+
command=/usr/bin/redis-server --port 6379 --databases 32 --save "" --stop-writes-on-bgsave-error no --dir /tmp
|
| 31 |
+
user=1000
|
| 32 |
+
autorestart=true
|
| 33 |
+
startsecs=3
|
| 34 |
+
stdout_logfile=/dev/stdout
|
| 35 |
+
stdout_logfile_maxbytes=0
|
| 36 |
+
stderr_logfile=/dev/stderr
|
| 37 |
+
stderr_logfile_maxbytes=0
|
| 38 |
+
|
| 39 |
+
[program:siglip2-triton]
|
| 40 |
+
; This program's command *is* pipeline/hf_space_seed.py --component=siglip2-model,
|
| 41 |
+
; not tritonserver itself β same "restore then exec into the real process"
|
| 42 |
+
; shape as [program:neo4j] below, for the same reason: the ~3.6 GB SigLIP2
|
| 43 |
+
; ONNX encoder weights aren't baked into the image (HF Space git repos cap
|
| 44 |
+
; storage at 1 GB by default β verified live, a real deploy blew through it)
|
| 45 |
+
; so they're synced from the artifact S3 bucket here before tritonserver
|
| 46 |
+
; execs β see that script's own docstring. The exec'd tritonserver command
|
| 47 |
+
; (SIGLIP2_TRITON_CMD in hf_space_seed.py) --http-ports off Triton's own
|
| 48 |
+
; default (8000) because deepsearch-api's own uvicorn already binds
|
| 49 |
+
; 127.0.0.1:8000 in this same container β only the gRPC port (8001,
|
| 50 |
+
; Triton's default, unchanged) is actually used by the siglip2-triton-client
|
| 51 |
+
; wheel deepsearch-api talks through; the HTTP port is kept (not disabled)
|
| 52 |
+
; only so this program's own readiness can be polled the same curl-based
|
| 53 |
+
; way every other program here is. Nothing here is bound beyond localhost
|
| 54 |
+
; in effect anyway β nginx is the sole externally-proxied process in this
|
| 55 |
+
; image, same as every other backend.
|
| 56 |
+
command=/bin/sh -c "cd /app/hf-space-pipeline && exec /app/.venv/bin/python -m pipeline.hf_space_seed --component=siglip2-model"
|
| 57 |
+
directory=/app/hf-space-pipeline
|
| 58 |
+
user=root
|
| 59 |
+
autorestart=true
|
| 60 |
+
startsecs=10
|
| 61 |
+
stdout_logfile=/dev/stdout
|
| 62 |
+
stdout_logfile_maxbytes=0
|
| 63 |
+
stderr_logfile=/dev/stderr
|
| 64 |
+
stderr_logfile_maxbytes=0
|
| 65 |
+
|
| 66 |
+
[program:neo4j]
|
| 67 |
+
command=/bin/sh -c "cd /app/hf-space-pipeline && exec /app/.venv/bin/python -m pipeline.hf_space_seed --component=neo4j"
|
| 68 |
+
directory=/app/hf-space-pipeline
|
| 69 |
+
user=1000
|
| 70 |
+
autorestart=true
|
| 71 |
+
startsecs=10
|
| 72 |
+
stdout_logfile=/dev/stdout
|
| 73 |
+
stdout_logfile_maxbytes=0
|
| 74 |
+
stderr_logfile=/dev/stderr
|
| 75 |
+
stderr_logfile_maxbytes=0
|
| 76 |
+
|
| 77 |
+
[program:asset-graph-service]
|
| 78 |
+
command=/bin/sh -c "until curl -sf http://localhost:7474 >/dev/null 2>&1; do sleep 2; done; exec /app/.venv/bin/python -m uvicorn asset_graph_service.api:app --host 127.0.0.1 --port 8010"
|
| 79 |
+
environment=DB_URI="neo4j://localhost:7687",DB_USERNAME="neo4j",DB_PASSWORD="none",ACCEPT_EULA="Y",OTEL_SDK_DISABLED="true"
|
| 80 |
+
directory=/app
|
| 81 |
+
user=1000
|
| 82 |
+
autorestart=true
|
| 83 |
+
startsecs=10
|
| 84 |
+
stdout_logfile=/dev/stdout
|
| 85 |
+
stdout_logfile_maxbytes=0
|
| 86 |
+
stderr_logfile=/dev/stderr
|
| 87 |
+
stderr_logfile_maxbytes=0
|
| 88 |
+
|
| 89 |
+
[program:deepsearch-api]
|
| 90 |
+
; Waits on siglip2-triton's own readiness (via its remapped HTTP port,
|
| 91 |
+
; 8003 β see [program:siglip2-triton] above) in addition to running
|
| 92 |
+
; pipeline/hf_space_seed.py, same "don't exec uvicorn until every backend
|
| 93 |
+
; this process depends on is actually ready" rule the cpu-basic/NVCF
|
| 94 |
+
; variant already follows for OpenSearch.
|
| 95 |
+
command=/bin/sh -c "until curl -sf http://localhost:8003/v2/health/ready >/dev/null 2>&1; do sleep 2; done; cd /app/hf-space-pipeline && /app/.venv/bin/python -m pipeline.hf_space_seed && cd /app && exec /app/.venv/bin/python -m uvicorn deepsearch_api.main:app --host 127.0.0.1 --port 8000"
|
| 96 |
+
directory=/app
|
| 97 |
+
user=1000
|
| 98 |
+
autorestart=true
|
| 99 |
+
startsecs=10
|
| 100 |
+
stdout_logfile=/dev/stdout
|
| 101 |
+
stdout_logfile_maxbytes=0
|
| 102 |
+
stderr_logfile=/dev/stderr
|
| 103 |
+
stderr_logfile_maxbytes=0
|
| 104 |
+
|
| 105 |
+
[program:nginx]
|
| 106 |
+
command=/bin/sh -c "until curl -sf http://localhost:8000/health >/dev/null 2>&1 && curl -sf http://localhost:8010/docs >/dev/null 2>&1; do sleep 2; done; sed -e 's/__PORT__/%(ENV_PORT)s/' -e 's|__CORPUS_NAME__|%(ENV_CORPUS_NAME)s|g' /etc/nginx/hf-space.conf.template > /etc/nginx/conf.d/hf-space.conf && exec nginx -g 'daemon off;'"
|
| 107 |
+
user=root
|
| 108 |
+
autorestart=true
|
| 109 |
+
startsecs=3
|
| 110 |
+
stdout_logfile=/dev/stdout
|
| 111 |
+
stdout_logfile_maxbytes=0
|
| 112 |
+
stderr_logfile=/dev/stderr
|
| 113 |
+
stderr_logfile_maxbytes=0
|
docker/hf-space/supervisord.conf
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
; Runs the MVP single-container stack (OpenSearch + Neo4j + Redis +
|
| 2 |
+
; deepsearch-api + asset-graph-service) for a Hugging Face Docker Space,
|
| 3 |
+
; which proxies exactly one port and has no docker-compose support β see
|
| 4 |
+
; docker/Dockerfile.hf-space.
|
| 5 |
+
;
|
| 6 |
+
; No local siglip2-triton β SigLIP2 embeddings always go to NVCF in this
|
| 7 |
+
; image. No graph-builder/asset_graph_generation worker either: both are
|
| 8 |
+
; indexing-time-only (building the graph from scratch), not needed to serve
|
| 9 |
+
; an already-built one β deepsearch-api's asset-graph calls are wired up as
|
| 10 |
+
; a dependency-injected per-request client (see routers_v2/dependencies.py),
|
| 11 |
+
; never a hard startup dependency, so /v2/deepsearch/search and
|
| 12 |
+
; /v3/deepsearch/search work regardless of asset-graph-service's own
|
| 13 |
+
; readiness.
|
| 14 |
+
|
| 15 |
+
[supervisord]
|
| 16 |
+
nodaemon=true
|
| 17 |
+
logfile=/dev/stdout
|
| 18 |
+
logfile_maxbytes=0
|
| 19 |
+
pidfile=/tmp/supervisord.pid
|
| 20 |
+
|
| 21 |
+
[program:opensearch]
|
| 22 |
+
command=/usr/share/opensearch/opensearch-docker-entrypoint.sh opensearch
|
| 23 |
+
directory=/usr/share/opensearch
|
| 24 |
+
user=1000
|
| 25 |
+
autorestart=true
|
| 26 |
+
startsecs=5
|
| 27 |
+
stdout_logfile=/dev/stdout
|
| 28 |
+
stdout_logfile_maxbytes=0
|
| 29 |
+
stderr_logfile=/dev/stderr
|
| 30 |
+
stderr_logfile_maxbytes=0
|
| 31 |
+
|
| 32 |
+
[program:redis]
|
| 33 |
+
command=/usr/bin/redis-server --port 6379 --databases 32 --save "" --stop-writes-on-bgsave-error no --dir /tmp
|
| 34 |
+
user=1000
|
| 35 |
+
autorestart=true
|
| 36 |
+
startsecs=3
|
| 37 |
+
stdout_logfile=/dev/stdout
|
| 38 |
+
stdout_logfile_maxbytes=0
|
| 39 |
+
stderr_logfile=/dev/stderr
|
| 40 |
+
stderr_logfile_maxbytes=0
|
| 41 |
+
|
| 42 |
+
[program:neo4j]
|
| 43 |
+
; This program's command *is* pipeline/hf_space_seed.py --component=neo4j,
|
| 44 |
+
; not neo4j itself: neo4j-admin's offline load has to finish before the
|
| 45 |
+
; real neo4j process starts (unlike OpenSearch's live bulk-load), so the
|
| 46 |
+
; seed step execs into the actual `neo4j console` as its last step once
|
| 47 |
+
; restore is done β see that module's docstring. A restore failure (e.g.
|
| 48 |
+
; bad AWS creds, no published run) correctly counts as this program failing
|
| 49 |
+
; to start, same as every other startup error here.
|
| 50 |
+
command=/bin/sh -c "cd /app/hf-space-pipeline && exec /app/.venv/bin/python -m pipeline.hf_space_seed --component=neo4j"
|
| 51 |
+
directory=/app/hf-space-pipeline
|
| 52 |
+
user=1000
|
| 53 |
+
autorestart=true
|
| 54 |
+
startsecs=10
|
| 55 |
+
stdout_logfile=/dev/stdout
|
| 56 |
+
stdout_logfile_maxbytes=0
|
| 57 |
+
stderr_logfile=/dev/stderr
|
| 58 |
+
stderr_logfile_maxbytes=0
|
| 59 |
+
|
| 60 |
+
[program:asset-graph-service]
|
| 61 |
+
; Waits for neo4j's own HTTP port rather than depending on it through
|
| 62 |
+
; supervisord (which has no real dependency ordering) β same shape as
|
| 63 |
+
; deepsearch-api waiting on hf_space_seed.py's own OpenSearch poll, just
|
| 64 |
+
; inline here since it's a one-line curl loop rather than needing a Python
|
| 65 |
+
; script. Bound to localhost only, same as deepsearch-api β nginx is the
|
| 66 |
+
; sole externally-proxied process.
|
| 67 |
+
command=/bin/sh -c "until curl -sf http://localhost:7474 >/dev/null 2>&1; do sleep 2; done; exec /app/.venv/bin/python -m uvicorn asset_graph_service.api:app --host 127.0.0.1 --port 8010"
|
| 68 |
+
environment=DB_URI="neo4j://localhost:7687",DB_USERNAME="neo4j",DB_PASSWORD="none",ACCEPT_EULA="Y",OTEL_SDK_DISABLED="true"
|
| 69 |
+
directory=/app
|
| 70 |
+
user=1000
|
| 71 |
+
autorestart=true
|
| 72 |
+
startsecs=10
|
| 73 |
+
stdout_logfile=/dev/stdout
|
| 74 |
+
stdout_logfile_maxbytes=0
|
| 75 |
+
stderr_logfile=/dev/stderr
|
| 76 |
+
stderr_logfile_maxbytes=0
|
| 77 |
+
|
| 78 |
+
[program:deepsearch-api]
|
| 79 |
+
; Bound to localhost only β nginx (below) is the sole process bound to the
|
| 80 |
+
; externally proxied port, so it can front every route with the friendly-URL
|
| 81 |
+
; rewrites and CORS headers in nginx.conf.template.
|
| 82 |
+
;
|
| 83 |
+
; Runs pipeline/hf_space_seed.py first and only execs uvicorn once it
|
| 84 |
+
; succeeds: that script waits for opensearch (started in parallel by
|
| 85 |
+
; supervisord, below) and bulk-loads this corpus's indexed run from S3
|
| 86 |
+
; before deepsearch-api has anything to search β see that module's
|
| 87 |
+
; docstring for why this happens at container startup, not `docker build`
|
| 88 |
+
; time. A seeding failure (e.g. no published run yet, or bad AWS creds)
|
| 89 |
+
; correctly counts as this program failing to start, same as any other
|
| 90 |
+
; startup error here.
|
| 91 |
+
command=/bin/sh -c "cd /app/hf-space-pipeline && /app/.venv/bin/python -m pipeline.hf_space_seed && cd /app && exec /app/.venv/bin/python -m uvicorn deepsearch_api.main:app --host 127.0.0.1 --port 8000"
|
| 92 |
+
directory=/app
|
| 93 |
+
user=1000
|
| 94 |
+
autorestart=true
|
| 95 |
+
startsecs=10
|
| 96 |
+
stdout_logfile=/dev/stdout
|
| 97 |
+
stdout_logfile_maxbytes=0
|
| 98 |
+
stderr_logfile=/dev/stderr
|
| 99 |
+
stderr_logfile_maxbytes=0
|
| 100 |
+
|
| 101 |
+
[program:nginx]
|
| 102 |
+
; nginx is the *only* process bound to the externally-proxied port, and
|
| 103 |
+
; Hugging Face's own Space "ready" detection is just "does something answer
|
| 104 |
+
; on app_port" β so starting nginx immediately (as this used to) flipped the
|
| 105 |
+
; Space to Running as soon as the container booted, while deepsearch-api and
|
| 106 |
+
; asset-graph-service were still behind it restoring their data (a
|
| 107 |
+
; multi-hundred-MB S3 bulk load / offline neo4j-admin load, routinely taking
|
| 108 |
+
; minutes β see bake.py's own verify_baked comment on the same point): the
|
| 109 |
+
; Explorer UI loaded instantly but every search/graph call 502'd or came
|
| 110 |
+
; back empty until restore finished. Waiting for both backends' own readiness
|
| 111 |
+
; first β same "poll a real endpoint the process only starts once restore
|
| 112 |
+
; finished" idiom asset-graph-service already uses above for neo4j, not a
|
| 113 |
+
; new pattern β means the external port stays closed, and HF keeps the Space
|
| 114 |
+
; in "building"/"starting" rather than Running, until the stack actually
|
| 115 |
+
; works. /health / /docs are plain unauthenticated GETs, so no extra config
|
| 116 |
+
; needed to poll them from inside the container.
|
| 117 |
+
;
|
| 118 |
+
; nginx reads $PORT if HF sets it (Spaces convention), else the baked
|
| 119 |
+
; default matches the Space's declared app_port (7860) β same convention
|
| 120 |
+
; deepsearch-api used to follow directly before nginx sat in front of it.
|
| 121 |
+
; %(ENV_PORT)s is substituted by supervisord itself, so no extra templating
|
| 122 |
+
; tool (envsubst etc.) is needed in the image.
|
| 123 |
+
command=/bin/sh -c "until curl -sf http://localhost:8000/health >/dev/null 2>&1 && curl -sf http://localhost:8010/docs >/dev/null 2>&1; do sleep 2; done; sed -e 's/__PORT__/%(ENV_PORT)s/' -e 's|__CORPUS_NAME__|%(ENV_CORPUS_NAME)s|g' /etc/nginx/hf-space.conf.template > /etc/nginx/conf.d/hf-space.conf && exec nginx -g 'daemon off;'"
|
| 124 |
+
user=root
|
| 125 |
+
autorestart=true
|
| 126 |
+
startsecs=3
|
| 127 |
+
stdout_logfile=/dev/stdout
|
| 128 |
+
stdout_logfile_maxbytes=0
|
| 129 |
+
stderr_logfile=/dev/stderr
|
| 130 |
+
stderr_logfile_maxbytes=0
|
docker/hf-space/wheels/SOURCE.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by scripts/extract_usdsearch_wheels.sh β do not hand-edit.
|
| 2 |
+
source_image=nvcr.io/m3sujtetvf5w/usdsearch/usdsearch:1.5.0-a1b3d0fa
|
| 3 |
+
extracted_at=2026-09-24T14:15:22Z
|
docker/hf-space/wheels/asset_graph_client-0.1.0-py3-none-any.whl
ADDED
|
Binary file (89.1 kB). View file
|
|
|
docker/hf-space/wheels/asset_graph_service-0.1.0-py3-none-any.whl
ADDED
|
Binary file (43.6 kB). View file
|
|
|
docker/hf-space/wheels/base_logging-0.1.0-py3-none-any.whl
ADDED
|
Binary file (3.04 kB). View file
|
|
|
docker/hf-space/wheels/constraints.txt
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aioboto3==15.5.0
|
| 2 |
+
aiobotocore==2.25.1
|
| 3 |
+
aiofiles==25.1.0
|
| 4 |
+
aiohappyeyeballs==2.6.1
|
| 5 |
+
aiohttp==3.14.3
|
| 6 |
+
aiohttp-retry==2.9.1
|
| 7 |
+
aioitertools==0.13.0
|
| 8 |
+
aiosignal==1.4.0
|
| 9 |
+
annotated-doc==0.0.4
|
| 10 |
+
annotated-types==0.7.0
|
| 11 |
+
anyio==4.13.0
|
| 12 |
+
asgi-correlation-id==4.3.4
|
| 13 |
+
asgiref==3.11.1
|
| 14 |
+
asset-graph-builder==0.1.0
|
| 15 |
+
async-lru==2.3.0
|
| 16 |
+
attrs==26.1.0
|
| 17 |
+
Authlib==1.7.2
|
| 18 |
+
backoff==2.2.1
|
| 19 |
+
boto3==1.40.61
|
| 20 |
+
botocore==1.40.61
|
| 21 |
+
certifi==2026.4.22
|
| 22 |
+
cffi==2.0.0
|
| 23 |
+
charset-normalizer==3.4.7
|
| 24 |
+
click==8.3.2
|
| 25 |
+
cryptography==50.0.0
|
| 26 |
+
deepsearch-crawler==0.1.0
|
| 27 |
+
deepsearch-monitor==0.1.0
|
| 28 |
+
deepsearch-utils==0.1.0
|
| 29 |
+
Deprecated==1.3.1
|
| 30 |
+
distro==1.9.0
|
| 31 |
+
elastic-transport==9.2.1
|
| 32 |
+
elasticsearch==9.3.0
|
| 33 |
+
envyaml==1.10.211231
|
| 34 |
+
Events==0.5
|
| 35 |
+
fastapi==0.136.0
|
| 36 |
+
fastapi-swagger==0.4.48
|
| 37 |
+
filelock==3.29.0
|
| 38 |
+
fire==0.7.1
|
| 39 |
+
frozenlist==1.8.0
|
| 40 |
+
fsspec==2026.3.0
|
| 41 |
+
googleapis-common-protos==1.74.0
|
| 42 |
+
greenlet==3.4.0
|
| 43 |
+
grpcio==1.80.0
|
| 44 |
+
h11==0.16.0
|
| 45 |
+
hf-xet==1.4.3
|
| 46 |
+
hiredis==3.3.1
|
| 47 |
+
httpcore==1.0.9
|
| 48 |
+
httptools==0.7.1
|
| 49 |
+
httpx==0.28.1
|
| 50 |
+
huggingface_hub==1.11.0
|
| 51 |
+
humanize==4.15.0
|
| 52 |
+
idna==3.18
|
| 53 |
+
importlib_metadata==8.7.1
|
| 54 |
+
Jinja2==3.1.6
|
| 55 |
+
jiter==0.14.0
|
| 56 |
+
jmespath==1.1.0
|
| 57 |
+
joserfc==1.7.2
|
| 58 |
+
jsonpatch==1.33
|
| 59 |
+
jsonpointer==3.1.1
|
| 60 |
+
kubernetes_asyncio==35.0.1
|
| 61 |
+
langchain==1.3.15
|
| 62 |
+
langchain-classic==1.0.7
|
| 63 |
+
langchain-core==1.5.6
|
| 64 |
+
langchain-openai==1.1.16
|
| 65 |
+
langchain-protocol==0.0.18
|
| 66 |
+
langchain-text-splitters==1.1.2
|
| 67 |
+
langgraph==1.2.11
|
| 68 |
+
langgraph-checkpoint==4.1.1
|
| 69 |
+
langgraph-prebuilt==1.1.0
|
| 70 |
+
langgraph-sdk==0.4.2
|
| 71 |
+
langsmith==0.8.18
|
| 72 |
+
markdown-it-py==4.0.0
|
| 73 |
+
MarkupSafe==3.0.3
|
| 74 |
+
mdurl==0.1.2
|
| 75 |
+
multidict==6.7.1
|
| 76 |
+
neo4j==6.1.0
|
| 77 |
+
ngsearch-backend==0.1.0
|
| 78 |
+
ngsearch-cache==0.1.0
|
| 79 |
+
ngsearch-crawlers==0.1.0
|
| 80 |
+
ngsearch-info-endpoint==0.1.0
|
| 81 |
+
ngsearch-plugins==0.1.0
|
| 82 |
+
ngsearch-storage==0.1.0
|
| 83 |
+
numpy==2.4.6
|
| 84 |
+
openai==2.32.0
|
| 85 |
+
opensearch-protobufs==0.19.0
|
| 86 |
+
opensearch-py==3.1.0
|
| 87 |
+
opentelemetry-api==1.41.0
|
| 88 |
+
opentelemetry-exporter-otlp==1.41.0
|
| 89 |
+
opentelemetry-exporter-otlp-proto-common==1.41.0
|
| 90 |
+
opentelemetry-exporter-otlp-proto-grpc==1.41.0
|
| 91 |
+
opentelemetry-exporter-otlp-proto-http==1.41.0
|
| 92 |
+
opentelemetry-instrumentation==0.62b0
|
| 93 |
+
opentelemetry-instrumentation-aiohttp-client==0.62b0
|
| 94 |
+
opentelemetry-instrumentation-asgi==0.62b0
|
| 95 |
+
opentelemetry-instrumentation-fastapi==0.62b0
|
| 96 |
+
opentelemetry-instrumentation-grpc==0.62b0
|
| 97 |
+
opentelemetry-instrumentation-httpx==0.62b0
|
| 98 |
+
opentelemetry-proto==1.41.0
|
| 99 |
+
opentelemetry-sdk==1.41.0
|
| 100 |
+
opentelemetry-semantic-conventions==0.62b0
|
| 101 |
+
opentelemetry-util-http==0.62b0
|
| 102 |
+
orjson==3.11.8
|
| 103 |
+
ormsgpack==1.12.2
|
| 104 |
+
packaging==26.1
|
| 105 |
+
pillow==12.3.0
|
| 106 |
+
prometheus_client==0.25.0
|
| 107 |
+
prometheus-fastapi-instrumentator==8.0.0
|
| 108 |
+
propcache==0.4.1
|
| 109 |
+
protobuf==6.33.6
|
| 110 |
+
psutil==7.2.2
|
| 111 |
+
pycparser==3.0
|
| 112 |
+
pydantic==2.13.3
|
| 113 |
+
pydantic_core==2.46.3
|
| 114 |
+
pydantic-settings==2.14.2
|
| 115 |
+
Pygments==2.20.0
|
| 116 |
+
pyinstrument==5.1.2
|
| 117 |
+
pyparsing==3.3.2
|
| 118 |
+
python-dateutil==2.9.0.post0
|
| 119 |
+
python-dotenv==1.2.2
|
| 120 |
+
python-json-logger==4.1.0
|
| 121 |
+
python-multipart==0.0.32
|
| 122 |
+
python-rapidjson==1.23
|
| 123 |
+
pytinyexr==0.9.1
|
| 124 |
+
pytz==2026.1.post1
|
| 125 |
+
PyYAML==6.0.3
|
| 126 |
+
redis==7.4.0
|
| 127 |
+
regex==2026.4.4
|
| 128 |
+
requests==2.33.1
|
| 129 |
+
requests-toolbelt==1.0.0
|
| 130 |
+
rich==15.0.0
|
| 131 |
+
s3transfer==0.14.0
|
| 132 |
+
safetensors==0.7.0
|
| 133 |
+
sentry-sdk==2.58.0
|
| 134 |
+
setuptools==82.0.1
|
| 135 |
+
shellingham==1.5.4
|
| 136 |
+
six==1.17.0
|
| 137 |
+
sniffio==1.3.1
|
| 138 |
+
SQLAlchemy==2.0.49
|
| 139 |
+
starlette==1.3.1
|
| 140 |
+
tenacity==9.1.4
|
| 141 |
+
termcolor==3.3.0
|
| 142 |
+
tiktoken==0.12.0
|
| 143 |
+
tokenizers==0.22.2
|
| 144 |
+
tqdm==4.67.3
|
| 145 |
+
transformers==5.5.4
|
| 146 |
+
tritonclient==2.41.0
|
| 147 |
+
typer==0.24.1
|
| 148 |
+
typing_extensions==4.15.0
|
| 149 |
+
typing-inspection==0.4.2
|
| 150 |
+
urllib3==2.7.0
|
| 151 |
+
usd-core==25.11
|
| 152 |
+
usdsearch==0.1.0
|
| 153 |
+
uuid_utils==0.14.1
|
| 154 |
+
uvicorn==0.45.0
|
| 155 |
+
websockets==15.0.1
|
| 156 |
+
wrapt==1.17.3
|
| 157 |
+
xxhash==3.6.0
|
| 158 |
+
yarl==1.23.0
|
| 159 |
+
zipp==3.23.1
|
| 160 |
+
zstandard==0.25.0
|
docker/hf-space/wheels/deepsearch_api-0.1.0-py3-none-any.whl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:16406fa180ff1ecac8b48bbface39243eeb471bfcee62af8f5b3cccb7891985e
|
| 3 |
+
size 212789
|
docker/hf-space/wheels/llm_client-0.1.0-py3-none-any.whl
ADDED
|
Binary file (39.4 kB). View file
|
|
|
docker/hf-space/wheels/search_utils-0.1.0-py3-none-any.whl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f81e65e56543e8393c4bae7aecab58b4c66c5cb8034e450ca88993efde4aaca9
|
| 3 |
+
size 286603
|
docker/hf-space/wheels/siglip2_triton_client-0.6.1-py3-none-any.whl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f8405481eb33cd4bc22401bf7a9391d5ba32342caa3206ce990854d7ca15df0
|
| 3 |
+
size 5482420
|
docker/hf-space/wheels/storage_api-0.1.0-py3-none-any.whl
ADDED
|
Binary file (77.1 kB). View file
|
|
|
pipeline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""USD Search index re-generation pipeline."""
|
pipeline/artifact_store.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared plumbing for talking to the dedicated artifact S3 bucket.
|
| 2 |
+
|
| 3 |
+
Used by both `upload` (stage 3, write) and `restore` (read, to continue
|
| 4 |
+
indexing on top of a previous run's data). Deliberately separate credentials
|
| 5 |
+
from `S3_STORAGE_*` (the source corpus bucket), so a mistake in one credential
|
| 6 |
+
set can't touch the other bucket β see the plan, Β§7.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import functools
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
import subprocess
|
| 15 |
+
import time
|
| 16 |
+
from typing import Dict, List
|
| 17 |
+
|
| 18 |
+
from .config import Config
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@functools.lru_cache(maxsize=1)
|
| 22 |
+
def _aws_cli_major_version() -> int:
|
| 23 |
+
"""1 or 2 β memoized, one `aws --version` subprocess call per process.
|
| 24 |
+
|
| 25 |
+
Defaults to 1 (the more defensive branch in aws_cmd(), below) if
|
| 26 |
+
detection fails for any reason, rather than assuming the newer,
|
| 27 |
+
less-defensive CLI is present."""
|
| 28 |
+
try:
|
| 29 |
+
result = subprocess.run(["aws", "--version"], capture_output=True, text=True, timeout=10)
|
| 30 |
+
match = re.search(r"aws-cli/(\d+)\.", result.stdout + result.stderr)
|
| 31 |
+
if match:
|
| 32 |
+
return int(match.group(1))
|
| 33 |
+
except Exception: # noqa: BLE001 - detection failure just falls through to the default below
|
| 34 |
+
pass
|
| 35 |
+
return 1
|
| 36 |
+
|
| 37 |
+
ARTIFACT_AWS_ACCESS_KEY_ID_VAR = "SIMREADYCENTRAL_INDEX_STORE_AWS_ACCESS_KEY_ID"
|
| 38 |
+
ARTIFACT_AWS_SECRET_ACCESS_KEY_VAR = "SIMREADYCENTRAL_INDEX_STORE_AWS_SECRET_ACCESS_KEY"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def artifact_env() -> Dict[str, str]:
|
| 42 |
+
key = os.environ.get(ARTIFACT_AWS_ACCESS_KEY_ID_VAR, "").strip()
|
| 43 |
+
secret = os.environ.get(ARTIFACT_AWS_SECRET_ACCESS_KEY_VAR, "").strip()
|
| 44 |
+
if not key or not secret:
|
| 45 |
+
raise SystemExit(
|
| 46 |
+
f"{ARTIFACT_AWS_ACCESS_KEY_ID_VAR} / {ARTIFACT_AWS_SECRET_ACCESS_KEY_VAR} "
|
| 47 |
+
"must be set in the environment to reach the artifact bucket."
|
| 48 |
+
)
|
| 49 |
+
env = dict(os.environ)
|
| 50 |
+
env["AWS_ACCESS_KEY_ID"] = key
|
| 51 |
+
env["AWS_SECRET_ACCESS_KEY"] = secret
|
| 52 |
+
env.pop("AWS_PROFILE", None)
|
| 53 |
+
return env
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def aws_cmd(cfg: Config, read_timeout: int = 300) -> List[str]:
|
| 57 |
+
cmd = ["aws", "s3"]
|
| 58 |
+
# aws-cli v1's `s3` subcommands mis-sign SigV4 requests against a bucket
|
| 59 |
+
# outside the CLI's configured default region (`SignatureDoesNotMatch`,
|
| 60 |
+
# even though `aws s3api` and plain `--region` alone are unaffected) β
|
| 61 |
+
# pin the endpoint explicitly to the artifact bucket's actual region.
|
| 62 |
+
# v1-only: verified live (2026-09-23) that aws-cli v2 (what both HF Space
|
| 63 |
+
# Dockerfiles bake, via the official installer β see
|
| 64 |
+
# docker/Dockerfile.hf-space-gpu) signs correctly with just `--region`,
|
| 65 |
+
# no `--endpoint-url` override needed at all. Kept conditional rather
|
| 66 |
+
# than dropped outright: this function is also called by this pipeline's
|
| 67 |
+
# own local `enumerate`/`upload`/`restore` stages, run on whatever `aws`
|
| 68 |
+
# an operator's own machine happens to have on PATH β and at least one
|
| 69 |
+
# (this session's own dev machine) is still v1.
|
| 70 |
+
region = cfg.get("ARTIFACT_S3_REGION", "us-east-2").strip()
|
| 71 |
+
if _aws_cli_major_version() >= 2:
|
| 72 |
+
cmd += ["--region", region]
|
| 73 |
+
else:
|
| 74 |
+
cmd += ["--endpoint-url", f"https://s3.{region}.amazonaws.com"]
|
| 75 |
+
# Bounded, not left to whatever the network happens to do: a CI run
|
| 76 |
+
# hung for 45 minutes (the whole job's ceiling) with no error and no
|
| 77 |
+
# log, right around where restore's `aws s3 cp`/`sync` calls run β those
|
| 78 |
+
# had no timeout anywhere, unlike the httpx calls elsewhere in this
|
| 79 |
+
# pipeline, which all do. Not confirmed as the cause (the actual hang
|
| 80 |
+
# point is unrecoverable β see the plan, Β§11), but a real, previously
|
| 81 |
+
# unprotected gap either way: any of these calls could hang forever on a
|
| 82 |
+
# stalled connection with zero backstop. `subprocess.run`'s own
|
| 83 |
+
# `timeout=` (used at every call site below) is the hard kill if even
|
| 84 |
+
# these don't fire.
|
| 85 |
+
#
|
| 86 |
+
# connect_timeout/read_timeout were originally 10s/60s β read_timeout is
|
| 87 |
+
# per-socket-read (resets on every chunk received, not a total-transfer
|
| 88 |
+
# deadline), so 60s looked plenty for this pipeline's normal restore
|
| 89 |
+
# payloads (MBs). Verified live it wasn't: a real deploy to a T4 HF
|
| 90 |
+
# Space hit `aws s3 sync ... exit status 255` (no further detail in the
|
| 91 |
+
# logs β --only-show-errors swallows it) on *both* the ~3.6 GB
|
| 92 |
+
# siglip2-model sync (raised to 300s there first) *and*, on a later
|
| 93 |
+
# deploy, the ordinary ~30 MB neo4j restore that had worked moments
|
| 94 |
+
# earlier on the same Space β i.e. this is a real, apparently
|
| 95 |
+
# intermittent characteristic of that Space's network path to S3, not
|
| 96 |
+
# specific to large-payload transfers, so the safer fix is raising the
|
| 97 |
+
# default here for every caller rather than special-casing one.
|
| 98 |
+
cmd += ["--cli-connect-timeout", "30", "--cli-read-timeout", str(read_timeout)]
|
| 99 |
+
return cmd
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def run_aws_with_retry(
|
| 103 |
+
cmd: List[str], *, env: Dict[str, str], timeout: int, max_attempts: int = 4, base_delay: float = 5.0,
|
| 104 |
+
) -> subprocess.CompletedProcess:
|
| 105 |
+
"""Run an `aws s3 ...` command, retrying on failure with exponential
|
| 106 |
+
backoff (5s/10s/20s between attempts by default).
|
| 107 |
+
|
| 108 |
+
Raising aws_cmd()'s own CLI timeouts (see that function's comment) alone
|
| 109 |
+
didn't fix the intermittent `aws s3 sync ... exit status 255` a real T4
|
| 110 |
+
HF Space deploy hit repeatedly, on both a small (~30 MB neo4j) and large
|
| 111 |
+
(~3.6 GB siglip2-model) payload, with no more detail than a bare
|
| 112 |
+
"aws: [ERROR]:" even with output captured β consistent with something
|
| 113 |
+
transient in that Space's network path to S3 (e.g. an egress connection
|
| 114 |
+
limit under concurrent load β OpenSearch, Neo4j, and siglip2-model all
|
| 115 |
+
restore in parallel at container startup) rather than a single
|
| 116 |
+
reproducible cause worth chasing further from here. A short backoff-retry
|
| 117 |
+
loop is the standard mitigation for exactly this class of transient
|
| 118 |
+
failure and costs nothing when the first attempt just succeeds (the
|
| 119 |
+
common case, verified live outside this specific Space).
|
| 120 |
+
"""
|
| 121 |
+
result = None
|
| 122 |
+
for attempt in range(1, max_attempts + 1):
|
| 123 |
+
result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=timeout)
|
| 124 |
+
if result.returncode == 0:
|
| 125 |
+
return result
|
| 126 |
+
print(f"==> aws command failed (attempt {attempt}/{max_attempts}, exit {result.returncode})")
|
| 127 |
+
if result.stdout.strip():
|
| 128 |
+
print(f" stdout: {result.stdout.strip()}")
|
| 129 |
+
if result.stderr.strip():
|
| 130 |
+
print(f" stderr: {result.stderr.strip()}")
|
| 131 |
+
if attempt < max_attempts:
|
| 132 |
+
delay = base_delay * (2 ** (attempt - 1))
|
| 133 |
+
print(f"==> retrying in {delay:.0f}s")
|
| 134 |
+
time.sleep(delay)
|
| 135 |
+
raise SystemExit(f"aws command failed after {max_attempts} attempts: {' '.join(str(c) for c in cmd[:8])} ...")
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def bucket_base(cfg: Config) -> str:
|
| 139 |
+
return cfg.require("ARTIFACT_S3_URI").rstrip("/")
|
pipeline/bake.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 4 β build (and optionally push) images with the data pre-loaded.
|
| 2 |
+
|
| 3 |
+
Staging happens here, on the host, so the Dockerfiles stay simple:
|
| 4 |
+
|
| 5 |
+
* settings + mappings are merged into one `<index>.create.json` per index,
|
| 6 |
+
with `index.knn.derived_source` stripped β the same transform upstream's
|
| 7 |
+
loader.sh applies, carried over verbatim because that setting is rejected
|
| 8 |
+
on restore.
|
| 9 |
+
* the elasticdump data file is converted to `_bulk` chunks (to_bulk.py),
|
| 10 |
+
since the OpenSearch image has no Node to run elasticdump with.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import gzip
|
| 16 |
+
import json
|
| 17 |
+
import shutil
|
| 18 |
+
import subprocess
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Dict, List
|
| 21 |
+
|
| 22 |
+
from . import hf_addressing
|
| 23 |
+
from .artifact_store import artifact_env
|
| 24 |
+
from .config import (
|
| 25 |
+
BUILD_DIR,
|
| 26 |
+
REPO_ROOT,
|
| 27 |
+
Config,
|
| 28 |
+
SIGLIP2_NVCF_ENDPOINT_DEFAULT,
|
| 29 |
+
SIGLIP2_NVCF_FUNCTION_ID_DEFAULT,
|
| 30 |
+
TRITONSERVER_BASE_IMAGE_DEFAULT,
|
| 31 |
+
)
|
| 32 |
+
from .enumerate_assets import load_manifest, save_manifest
|
| 33 |
+
from .export import NEO4J_ARTIFACTS, OS_ARTIFACTS
|
| 34 |
+
from .to_bulk import convert
|
| 35 |
+
|
| 36 |
+
BAKE_DIR = BUILD_DIR / "bake"
|
| 37 |
+
DOCKER_DIR = REPO_ROOT / "docker"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _run(cmd: List[str], timeout: int = 900, **kwargs) -> subprocess.CompletedProcess:
|
| 41 |
+
print(" $ " + " ".join(cmd[:8]) + (" ..." if len(cmd) > 8 else ""))
|
| 42 |
+
return subprocess.run(cmd, check=True, timeout=timeout, **kwargs)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _read_json_gz(path: Path) -> Dict:
|
| 46 |
+
"""Load a gzipped elasticdump artifact, undoing double JSON encoding.
|
| 47 |
+
|
| 48 |
+
elasticdump is inconsistent about this: `--type=settings` writes the body as
|
| 49 |
+
a JSON *string* containing JSON, while `--type=mapping` writes a plain
|
| 50 |
+
object. Upstream loader.sh normalises both with a `try fromjson catch .`
|
| 51 |
+
step; this is the same defence, applied to whichever form we get.
|
| 52 |
+
"""
|
| 53 |
+
with gzip.open(path, "rt") as fh:
|
| 54 |
+
doc = json.load(fh)
|
| 55 |
+
for _ in range(3):
|
| 56 |
+
if not isinstance(doc, str):
|
| 57 |
+
break
|
| 58 |
+
try:
|
| 59 |
+
doc = json.loads(doc)
|
| 60 |
+
except json.JSONDecodeError:
|
| 61 |
+
break
|
| 62 |
+
if not isinstance(doc, dict):
|
| 63 |
+
raise SystemExit(f"{path.name}: expected a JSON object, got {type(doc).__name__}")
|
| 64 |
+
return doc
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _unwrap(doc: Dict, key: str) -> Dict:
|
| 68 |
+
"""elasticdump writes {"<index>": {"settings": ...}}; loader.sh also accepts
|
| 69 |
+
the bare {"settings": ...} form. Handle both, like loader.sh does."""
|
| 70 |
+
if key in doc:
|
| 71 |
+
return doc[key]
|
| 72 |
+
if len(doc) == 1:
|
| 73 |
+
inner = next(iter(doc.values()))
|
| 74 |
+
if isinstance(inner, dict) and key in inner:
|
| 75 |
+
return inner[key]
|
| 76 |
+
raise SystemExit(f"could not find {key!r} in dump: keys={list(doc)[:5]}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def build_create_body(settings_path: Path, mapping_path: Path) -> Dict:
|
| 80 |
+
settings = _unwrap(_read_json_gz(settings_path), "settings")
|
| 81 |
+
mappings = _unwrap(_read_json_gz(mapping_path), "mappings")
|
| 82 |
+
|
| 83 |
+
index_settings = settings.get("index")
|
| 84 |
+
if isinstance(index_settings, dict):
|
| 85 |
+
# Present in the dump, rejected on create. loader.sh strips it too.
|
| 86 |
+
# The dump spells it as a literal dotted key mapping to an object:
|
| 87 |
+
# "knn.derived_source": {"enabled": "false"}
|
| 88 |
+
# but tolerate the nested spelling as well.
|
| 89 |
+
index_settings.pop("knn.derived_source", None)
|
| 90 |
+
index_settings.pop("knn.derived_source.enabled", None)
|
| 91 |
+
if isinstance(index_settings.get("knn"), dict):
|
| 92 |
+
index_settings["knn"].pop("derived_source", None)
|
| 93 |
+
# Cluster-assigned values that cannot be supplied on create.
|
| 94 |
+
for key in ("uuid", "creation_date", "creation_date_string", "provided_name",
|
| 95 |
+
"version", "resize", "routing", "history"):
|
| 96 |
+
index_settings.pop(key, None)
|
| 97 |
+
return {"settings": settings, "mappings": mappings}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def stage_opensearch(cfg: Config, manifest: Dict) -> Path:
|
| 101 |
+
seed_dir = BAKE_DIR / "opensearch" / "seed"
|
| 102 |
+
if seed_dir.exists():
|
| 103 |
+
shutil.rmtree(seed_dir)
|
| 104 |
+
seed_dir.mkdir(parents=True)
|
| 105 |
+
|
| 106 |
+
artifacts = manifest.get("opensearch_artifacts") or {}
|
| 107 |
+
if not artifacts:
|
| 108 |
+
raise SystemExit("manifest has no opensearch_artifacts β run `export` first.")
|
| 109 |
+
|
| 110 |
+
ordered: List[str] = []
|
| 111 |
+
for index in sorted(artifacts):
|
| 112 |
+
print(f" staging {index}")
|
| 113 |
+
body = build_create_body(
|
| 114 |
+
OS_ARTIFACTS / f"{index}.settings.json.gz",
|
| 115 |
+
OS_ARTIFACTS / f"{index}.mapping.json.gz",
|
| 116 |
+
)
|
| 117 |
+
(seed_dir / f"{index}.create.json").write_text(json.dumps(body))
|
| 118 |
+
chunks = convert(
|
| 119 |
+
OS_ARTIFACTS / f"{index}.data.json.gz",
|
| 120 |
+
seed_dir,
|
| 121 |
+
index,
|
| 122 |
+
chunk_docs=cfg.get_int("BULK_CHUNK_DOCS", 500),
|
| 123 |
+
)
|
| 124 |
+
print(f" {len(chunks)} bulk chunk(s)")
|
| 125 |
+
ordered.append(index)
|
| 126 |
+
|
| 127 |
+
(seed_dir / "indexes.txt").write_text("\n".join(ordered) + "\n")
|
| 128 |
+
|
| 129 |
+
context = BAKE_DIR / "opensearch"
|
| 130 |
+
for name in ("Dockerfile.opensearch-baked", "opensearch-seed.sh", "check_bulk.py"):
|
| 131 |
+
shutil.copy2(DOCKER_DIR / name, context / name)
|
| 132 |
+
return context
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def stage_neo4j(manifest: Dict) -> Path:
|
| 136 |
+
context = BAKE_DIR / "neo4j"
|
| 137 |
+
dumps = context / "dumps"
|
| 138 |
+
if dumps.exists():
|
| 139 |
+
shutil.rmtree(dumps)
|
| 140 |
+
dumps.mkdir(parents=True)
|
| 141 |
+
|
| 142 |
+
artifact = manifest.get("neo4j_artifact")
|
| 143 |
+
if not artifact:
|
| 144 |
+
raise SystemExit("manifest has no neo4j_artifact β run `export` first.")
|
| 145 |
+
|
| 146 |
+
src = NEO4J_ARTIFACTS / artifact["file"]
|
| 147 |
+
dest = dumps / "neo4j.dump"
|
| 148 |
+
with gzip.open(src, "rb") as fh, dest.open("wb") as out:
|
| 149 |
+
shutil.copyfileobj(fh, out)
|
| 150 |
+
print(f" staged {dest.name} ({dest.stat().st_size:,} bytes)")
|
| 151 |
+
|
| 152 |
+
for name in ("Dockerfile.neo4j-baked", "neo4j-seeded-entrypoint.sh"):
|
| 153 |
+
shutil.copy2(DOCKER_DIR / name, context / name)
|
| 154 |
+
return context
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _image_names(cfg: Config) -> Dict[str, str]:
|
| 158 |
+
registry = cfg.get("REGISTRY", "nvcr.io/m3sujtetvf5w/usdsearch").rstrip("/")
|
| 159 |
+
names = {
|
| 160 |
+
"opensearch": f"{registry}/{cfg.get('OPENSEARCH_IMAGE_NAME', 'opensearch-simready-central')}",
|
| 161 |
+
"neo4j": f"{registry}/{cfg.get('NEO4J_IMAGE_NAME', 'neo4j-simready-central')}",
|
| 162 |
+
}
|
| 163 |
+
# The combined single-container MVP image (docker/Dockerfile.hf-space)
|
| 164 |
+
# always routes siglip2 to NVCF β it has no local Triton β so it's only
|
| 165 |
+
# buildable when the run itself is already NVCF-backed. cfg is the same
|
| 166 |
+
# object bake/verify/push all load from pipeline.env, so this stays
|
| 167 |
+
# consistent across those stages without threading a separate flag.
|
| 168 |
+
if cfg.siglip2_nvcf:
|
| 169 |
+
names["hf_space"] = f"{registry}/{cfg.get('HF_SPACE_IMAGE_NAME', 'hf-space-simready-central')}"
|
| 170 |
+
# The GPU variant (docker/Dockerfile.hf-space-gpu, local siglip2-triton
|
| 171 |
+
# instead of NVCF) is a separate, optional deploy target β independent
|
| 172 |
+
# of SIGLIP2_BACKEND/siglip2_nvcf above, since it serves the same
|
| 173 |
+
# already-indexed corpus regardless of how the indexing stack itself
|
| 174 |
+
# computed embeddings. Off by default (HF_SPACE_BUILD_GPU_VARIANT):
|
| 175 |
+
# existing pipelines shouldn't pay for an extra ~15 GB build/push they
|
| 176 |
+
# didn't ask for.
|
| 177 |
+
if cfg.hf_space_gpu_variant_enabled:
|
| 178 |
+
names["hf_space_gpu"] = f"{registry}/{cfg.get('HF_SPACE_GPU_IMAGE_NAME', 'hf-space-simready-central-gpu')}"
|
| 179 |
+
return names
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def hf_url_mapping_rules(manifest: Dict) -> List[Dict[str, str]]:
|
| 183 |
+
"""USDSEARCH_URL_MAPPING_RULES for a storage_api corpus: one rule per HF
|
| 184 |
+
repo the manifest records, rewriting the indexed hf:// prefix to that
|
| 185 |
+
repo's browsable file-viewer URL (.../blob/<revision>/). The internal
|
| 186 |
+
prefix comes from hf_addressing.build(), so it matches the indexed
|
| 187 |
+
addresses exactly, pinned-sha `/resolve/<sha>/` segment included.
|
| 188 |
+
Empty for an S3 corpus. Per-repo prefixes never nest, so the library's
|
| 189 |
+
_validate_rules() ambiguity check can't trip on a multi-repo corpus.
|
| 190 |
+
"""
|
| 191 |
+
rules = []
|
| 192 |
+
for host, repo_type, repo_id, revision in manifest.get("hf_repos") or []:
|
| 193 |
+
type_prefix = hf_addressing.REPO_TYPE_PREFIX.get(repo_type, "")
|
| 194 |
+
public_repo = "/".join(p for p in (type_prefix, repo_id) if p)
|
| 195 |
+
rules.append({
|
| 196 |
+
"internal": hf_addressing.build(
|
| 197 |
+
host=host, repo_type=repo_type, repo_id=repo_id, revision=revision, is_folder=True
|
| 198 |
+
),
|
| 199 |
+
"public": f"https://{host}/{public_repo}/blob/{revision}/",
|
| 200 |
+
})
|
| 201 |
+
return rules
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def hf_space_build_args(cfg: Config, manifest: Dict) -> Dict[str, str]:
|
| 205 |
+
"""The `docker/Dockerfile.hf-space` ARGs this run needs, shared between
|
| 206 |
+
the local/CI `docker build` (bake.run, as --build-arg) and pipeline/
|
| 207 |
+
hf_space.py's deploy stage (as HF Space "Variables" β Hugging Face
|
| 208 |
+
passes any Space Variable whose name matches a Dockerfile ARG through as
|
| 209 |
+
a build-arg on its own build, same mechanism, different trigger). One
|
| 210 |
+
dict so the two build-arg lists can't silently drift apart.
|
| 211 |
+
"""
|
| 212 |
+
storage_backend = manifest.get("storage_backend", "s3")
|
| 213 |
+
args = {
|
| 214 |
+
"OPENSEARCH_VERSION": cfg.get("OPENSEARCH_VERSION", "3.8.0"),
|
| 215 |
+
"ARTIFACT_S3_URI": cfg.require("ARTIFACT_S3_URI"),
|
| 216 |
+
"ARTIFACT_S3_REGION": cfg.get("ARTIFACT_S3_REGION", "us-east-2"),
|
| 217 |
+
"CORPUS_NAME": manifest["corpus_name"],
|
| 218 |
+
"ES_NAME": manifest["es_name"],
|
| 219 |
+
# Search never makes a live storage-backend call at serve time (the
|
| 220 |
+
# Dockerfile bakes verification off β see its own comment), but the
|
| 221 |
+
# backend *type* still has to match what the corpus was indexed
|
| 222 |
+
# under: it gates a client-side URL/result code path unrelated to
|
| 223 |
+
# verification, and getting it wrong silently returns zero search
|
| 224 |
+
# results rather than erroring. Verified live.
|
| 225 |
+
"STORAGE_BACKEND_TYPE": storage_backend,
|
| 226 |
+
"SIGLIP2_NVCF_ENDPOINT": cfg.get("SIGLIP2_NVCF_ENDPOINT", SIGLIP2_NVCF_ENDPOINT_DEFAULT),
|
| 227 |
+
"SIGLIP2_NVCF_FUNCTION_ID": cfg.get("SIGLIP2_NVCF_FUNCTION_ID", SIGLIP2_NVCF_FUNCTION_ID_DEFAULT),
|
| 228 |
+
}
|
| 229 |
+
# S3_STORAGE_* is only ever meaningful for an S3-sourced corpus, and
|
| 230 |
+
# threading a source bucket name through for an HF-sourced one would be
|
| 231 |
+
# actively misleading.
|
| 232 |
+
if storage_backend != "storage_api":
|
| 233 |
+
args["S3_STORAGE_BUCKET_NAME"] = cfg.get("S3_STORAGE_BUCKET_NAME", "")
|
| 234 |
+
args["S3_STORAGE_REGION_NAME"] = cfg.get("S3_STORAGE_REGION_NAME", "us-west-2")
|
| 235 |
+
# Derived from this run's own corpus rather than hardcoded in the
|
| 236 |
+
# Dockerfile, so a Space follows its corpus when the dataset moves.
|
| 237 |
+
rules = hf_url_mapping_rules(manifest)
|
| 238 |
+
args["USDSEARCH_URL_MAPPING_MODE"] = "rules" if rules else "off"
|
| 239 |
+
args["USDSEARCH_URL_MAPPING_RULES"] = json.dumps(rules)
|
| 240 |
+
return args
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def hf_space_gpu_build_args(cfg: Config, manifest: Dict) -> Dict[str, str]:
|
| 244 |
+
"""The `docker/Dockerfile.hf-space-gpu` ARGs this run needs β same shape
|
| 245 |
+
as hf_space_build_args() above, minus every NVCF-only key (that
|
| 246 |
+
Dockerfile has no SIGLIP2_NVCF_* ARGs at all: SigLIP2 comes from the
|
| 247 |
+
local siglip2-triton process it bakes in, not NVCF), plus the bare
|
| 248 |
+
Triton base image, overridable in case a newer NGC tritonserver tag
|
| 249 |
+
needs picking up without a Dockerfile edit. The SigLIP2 model weights
|
| 250 |
+
themselves are not a build-arg β they're vendored source
|
| 251 |
+
(docker/hf-space/siglip2-model/, see scripts/extract_siglip2_model.sh),
|
| 252 |
+
committed like every other file this Dockerfile COPYs.
|
| 253 |
+
"""
|
| 254 |
+
args = hf_space_build_args(cfg, manifest)
|
| 255 |
+
args.pop("SIGLIP2_NVCF_ENDPOINT", None)
|
| 256 |
+
args.pop("SIGLIP2_NVCF_FUNCTION_ID", None)
|
| 257 |
+
args["TRITONSERVER_BASE_IMAGE"] = cfg.get("TRITONSERVER_BASE_IMAGE", TRITONSERVER_BASE_IMAGE_DEFAULT)
|
| 258 |
+
return args
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def image_tags(cfg: Config, manifest: Dict) -> Dict[str, str]:
|
| 262 |
+
# <corpus name>-<run_id> β in CI, run_id is itself
|
| 263 |
+
# <branch slug>-<workflow run id>-<short commit sha> (see
|
| 264 |
+
# nvcf-pipeline.yml's "Compute version" step), so the full tag traces
|
| 265 |
+
# a published image back to the exact corpus, branch, CI run and commit
|
| 266 |
+
# that built it. Locally, run_id falls back to enumerate_assets.py's
|
| 267 |
+
# `YYYYMMDD-<corpus sha prefix>`.
|
| 268 |
+
version = f"{manifest['corpus_name']}-{manifest['run_id']}"
|
| 269 |
+
return {name: f"{image}:{version}" for name, image in _image_names(cfg).items()}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def latest_tags(cfg: Config, manifest: Dict) -> Dict[str, str]:
|
| 273 |
+
"""`<corpus name>-latest` β updated only when `push` actually publishes."""
|
| 274 |
+
tag = f"{manifest['corpus_name']}-latest"
|
| 275 |
+
return {name: f"{image}:{tag}" for name, image in _image_names(cfg).items()}
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def run(cfg: Config, push: bool = False) -> int:
|
| 279 |
+
manifest = load_manifest()
|
| 280 |
+
tags = image_tags(cfg, manifest)
|
| 281 |
+
common = [
|
| 282 |
+
"--build-arg", f"RUN_ID={manifest['run_id']}",
|
| 283 |
+
"--build-arg", f"CORPUS_SHA256={manifest['corpus_sha256']}",
|
| 284 |
+
"--build-arg", f"USDSEARCH_GIT_SHA={manifest.get('usdsearch_git_sha', 'unknown')}",
|
| 285 |
+
]
|
| 286 |
+
|
| 287 |
+
print("Staging OpenSearch seed ...")
|
| 288 |
+
os_context = stage_opensearch(cfg, manifest)
|
| 289 |
+
print(f"\nBuilding {tags['opensearch']}")
|
| 290 |
+
_run([
|
| 291 |
+
"docker", "build",
|
| 292 |
+
"-f", str(os_context / "Dockerfile.opensearch-baked"),
|
| 293 |
+
"-t", tags["opensearch"],
|
| 294 |
+
"--build-arg", f"OPENSEARCH_VERSION={cfg.get('OPENSEARCH_VERSION', '3.8.0')}",
|
| 295 |
+
"--build-arg", f"MAIN_INDEX={cfg.main_index}",
|
| 296 |
+
"--build-arg", f"DOC_COUNT={manifest.get('base_docs_indexed', 0)}",
|
| 297 |
+
*common, str(os_context),
|
| 298 |
+
])
|
| 299 |
+
|
| 300 |
+
if "hf_space" in tags:
|
| 301 |
+
print(f"\nBuilding {tags['hf_space']} (single-container MVP: opensearch+redis+deepsearch-api, "
|
| 302 |
+
f"for Hugging Face Spaces)")
|
| 303 |
+
# Fails fast here, before the Neo4j build, if no NVCF token is in the
|
| 304 |
+
# environment β same fallback chain as the indexing/runtime nvcf
|
| 305 |
+
# overlays (config.py's siglip2_nvcf_env), so a missing token is
|
| 306 |
+
# reported the same way everywhere rather than surfacing later as an
|
| 307 |
+
# opaque docker build failure. No AWS-credential check here: this
|
| 308 |
+
# image no longer bakes the index in at build time (see the
|
| 309 |
+
# Dockerfile's header) β the artifact-bucket keys are only needed at
|
| 310 |
+
# `verify_baked`'s smoke-test `docker run`, and at the real Space's
|
| 311 |
+
# own runtime.
|
| 312 |
+
cfg.siglip2_nvcf_env()
|
| 313 |
+
# Build context is REPO_ROOT here (not DOCKER_DIR, unlike the
|
| 314 |
+
# opensearch/neo4j builds above): this Dockerfile COPYs pipeline/ in
|
| 315 |
+
# directly (its runtime seed step needs it) rather than reusing
|
| 316 |
+
# another already-built image's tag, since HF's own builder must be
|
| 317 |
+
# able to build it standalone with only public base images.
|
| 318 |
+
build_arg_flags = [
|
| 319 |
+
flag
|
| 320 |
+
for key, value in hf_space_build_args(cfg, manifest).items()
|
| 321 |
+
for flag in ("--build-arg", f"{key}={value}")
|
| 322 |
+
]
|
| 323 |
+
_run([
|
| 324 |
+
"docker", "build",
|
| 325 |
+
"-f", str(DOCKER_DIR / "Dockerfile.hf-space"),
|
| 326 |
+
"-t", tags["hf_space"],
|
| 327 |
+
*build_arg_flags,
|
| 328 |
+
*common, str(REPO_ROOT),
|
| 329 |
+
])
|
| 330 |
+
else:
|
| 331 |
+
print("\nSIGLIP2_BACKEND=local β skipping the HF Space image (it is NVCF-only by "
|
| 332 |
+
"design; set SIGLIP2_BACKEND=nvcf to build it β see docker/Dockerfile.hf-space)")
|
| 333 |
+
|
| 334 |
+
if "hf_space_gpu" in tags:
|
| 335 |
+
print(f"\nBuilding {tags['hf_space_gpu']} (single-container MVP + local siglip2-triton, "
|
| 336 |
+
f"for a GPU-tier Hugging Face Space β see docker/Dockerfile.hf-space-gpu)")
|
| 337 |
+
# No NVCF token needed here (this variant never calls NVCF) β only
|
| 338 |
+
# the same build-arg dict shape as the NVCF image, minus its
|
| 339 |
+
# NVCF-only keys. Build context is REPO_ROOT, same reasoning as the
|
| 340 |
+
# NVCF hf_space build above.
|
| 341 |
+
build_arg_flags = [
|
| 342 |
+
flag
|
| 343 |
+
for key, value in hf_space_gpu_build_args(cfg, manifest).items()
|
| 344 |
+
for flag in ("--build-arg", f"{key}={value}")
|
| 345 |
+
]
|
| 346 |
+
_run([
|
| 347 |
+
"docker", "build",
|
| 348 |
+
"-f", str(DOCKER_DIR / "Dockerfile.hf-space-gpu"),
|
| 349 |
+
"-t", tags["hf_space_gpu"],
|
| 350 |
+
*build_arg_flags,
|
| 351 |
+
*common, str(REPO_ROOT),
|
| 352 |
+
])
|
| 353 |
+
|
| 354 |
+
print("\nStaging Neo4j dump ...")
|
| 355 |
+
neo_context = stage_neo4j(manifest)
|
| 356 |
+
print(f"\nBuilding {tags['neo4j']}")
|
| 357 |
+
_run([
|
| 358 |
+
"docker", "build",
|
| 359 |
+
"-f", str(neo_context / "Dockerfile.neo4j-baked"),
|
| 360 |
+
"-t", tags["neo4j"],
|
| 361 |
+
"--build-arg", f"NEO4J_VERSION={cfg.get('NEO4J_VERSION', '5.23.0-community')}",
|
| 362 |
+
"--build-arg", f"NODE_COUNT={manifest.get('neo4j_node_count', 0)}",
|
| 363 |
+
"--build-arg", f"RELATIONSHIP_COUNT={manifest.get('neo4j_relationship_count', 0)}",
|
| 364 |
+
*common, str(neo_context),
|
| 365 |
+
])
|
| 366 |
+
|
| 367 |
+
manifest["images"] = tags
|
| 368 |
+
(BUILD_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
| 369 |
+
|
| 370 |
+
print("\nVerifying the baked data from a fresh container ...")
|
| 371 |
+
if verify_baked(cfg, tags, manifest) != 0:
|
| 372 |
+
return 1
|
| 373 |
+
|
| 374 |
+
if push:
|
| 375 |
+
# Only reached after `verify` has passed; run_pipeline never pushes
|
| 376 |
+
# before the smoke test.
|
| 377 |
+
for name, tag in tags.items():
|
| 378 |
+
print(f"\nPushing {tag}")
|
| 379 |
+
_run(["docker", "push", tag])
|
| 380 |
+
|
| 381 |
+
print("\nbuilt:")
|
| 382 |
+
for name, tag in tags.items():
|
| 383 |
+
print(f" {name}: {tag}")
|
| 384 |
+
if not push:
|
| 385 |
+
print("\nNot pushed. `run_pipeline.py verify` runs the smoke test; push with")
|
| 386 |
+
print("`run_pipeline.py push` once it passes.")
|
| 387 |
+
return 0
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def verify_baked(cfg: Config, tags: Dict[str, str], manifest: Dict) -> int:
|
| 391 |
+
"""Check the data from a fresh `docker run`, not from the build log.
|
| 392 |
+
|
| 393 |
+
This is what catches the VOLUME trap: a Dockerfile that writes into a
|
| 394 |
+
declared VOLUME builds green and ships an empty database, and only starting
|
| 395 |
+
the built image reveals it.
|
| 396 |
+
"""
|
| 397 |
+
ok = True
|
| 398 |
+
|
| 399 |
+
print(" opensearch ...")
|
| 400 |
+
result = subprocess.run(
|
| 401 |
+
["docker", "run", "--rm", "-d", "-p", "19200:9200", tags["opensearch"]],
|
| 402 |
+
capture_output=True, text=True, check=True, timeout=60,
|
| 403 |
+
)
|
| 404 |
+
container = result.stdout.strip()
|
| 405 |
+
try:
|
| 406 |
+
import time
|
| 407 |
+
import httpx
|
| 408 |
+
expected = manifest.get("base_docs_indexed", 0)
|
| 409 |
+
count = -1
|
| 410 |
+
shards_ready = False
|
| 411 |
+
for _ in range(90):
|
| 412 |
+
try:
|
| 413 |
+
response = httpx.get(f"http://localhost:19200/{cfg.main_index}/_count", timeout=5.0)
|
| 414 |
+
if response.status_code < 400:
|
| 415 |
+
body = response.json()
|
| 416 |
+
shards = body.get("_shards", {})
|
| 417 |
+
shards_ready = shards.get("successful") == shards.get("total")
|
| 418 |
+
count = int(body["count"])
|
| 419 |
+
# A fresh container recovers its shards from the baked
|
| 420 |
+
# segments over several seconds. OpenSearch answers
|
| 421 |
+
# `_count` as soon as any shard is open, and
|
| 422 |
+
# `allow_partial_search_results` defaults to true, so a
|
| 423 |
+
# response with `shards_ready=False` can silently
|
| 424 |
+
# undercount rather than error β keep polling instead of
|
| 425 |
+
# trusting the first HTTP 200.
|
| 426 |
+
if shards_ready and count == expected:
|
| 427 |
+
break
|
| 428 |
+
except httpx.HTTPError:
|
| 429 |
+
pass
|
| 430 |
+
time.sleep(2)
|
| 431 |
+
print(f" {cfg.main_index}: {count} docs (expected {expected}, shards ready: {shards_ready})")
|
| 432 |
+
if count != expected or count == 0:
|
| 433 |
+
print(" FAIL: baked OpenSearch image does not hold the expected documents")
|
| 434 |
+
ok = False
|
| 435 |
+
finally:
|
| 436 |
+
subprocess.run(["docker", "rm", "-f", container], capture_output=True, check=False, timeout=30)
|
| 437 |
+
|
| 438 |
+
print(" neo4j ...")
|
| 439 |
+
result = subprocess.run(
|
| 440 |
+
["docker", "run", "--rm", "-d", "-p", "17687:7687", tags["neo4j"]],
|
| 441 |
+
capture_output=True, text=True, check=True, timeout=60,
|
| 442 |
+
)
|
| 443 |
+
container = result.stdout.strip()
|
| 444 |
+
try:
|
| 445 |
+
import time
|
| 446 |
+
from neo4j import GraphDatabase
|
| 447 |
+
expected = manifest.get("neo4j_node_count", 0)
|
| 448 |
+
nodes = -1
|
| 449 |
+
# A bounded wall-clock deadline, not a bare iteration count: the
|
| 450 |
+
# driver is given no timeouts below by default, so a single stuck
|
| 451 |
+
# connection attempt (e.g. the server accepting TCP but stalling
|
| 452 |
+
# mid-handshake) can block far longer than the intended ~3 minutes
|
| 453 |
+
# of polling β 90 iterations at an unbounded per-attempt cost was
|
| 454 |
+
# exactly the shape of a run that hung here for 44 minutes and
|
| 455 |
+
# blew straight through this step's own 10-minute CI timeout
|
| 456 |
+
# without ever failing cleanly.
|
| 457 |
+
deadline = time.monotonic() + 180
|
| 458 |
+
while time.monotonic() < deadline:
|
| 459 |
+
try:
|
| 460 |
+
driver = GraphDatabase.driver(
|
| 461 |
+
"bolt://localhost:17687", auth=None,
|
| 462 |
+
connection_timeout=5.0, connection_acquisition_timeout=5.0,
|
| 463 |
+
)
|
| 464 |
+
with driver.session() as session:
|
| 465 |
+
nodes = session.run("MATCH (n) RETURN count(n) AS c").single()["c"]
|
| 466 |
+
driver.close()
|
| 467 |
+
break
|
| 468 |
+
except Exception: # noqa: BLE001 - the server is simply not up yet
|
| 469 |
+
time.sleep(2)
|
| 470 |
+
print(f" nodes: {nodes} (expected {expected})")
|
| 471 |
+
if expected > 0 and nodes != expected:
|
| 472 |
+
print(" FAIL: baked Neo4j image does not hold the expected graph")
|
| 473 |
+
ok = False
|
| 474 |
+
finally:
|
| 475 |
+
subprocess.run(["docker", "rm", "-f", container], capture_output=True, check=False, timeout=30)
|
| 476 |
+
|
| 477 |
+
if "hf_space" in tags:
|
| 478 |
+
print(" hf-space (combined MVP image) ...")
|
| 479 |
+
# The AWS keys here exercise the same runtime restore
|
| 480 |
+
# (pipeline/hf_space_seed.py, via supervisord) a real Space does β
|
| 481 |
+
# this smoke test is what actually proves that path works, not just
|
| 482 |
+
# that the image builds. A longer deadline than the other images'
|
| 483 |
+
# health checks: startup now includes a real S3 download + bulk
|
| 484 |
+
# load, not just a process coming up.
|
| 485 |
+
token = cfg.siglip2_nvcf_env()["SIGLIP2_TRITON_SERVER_AUTH_TOKEN"]
|
| 486 |
+
aws = artifact_env()
|
| 487 |
+
result = subprocess.run(
|
| 488 |
+
["docker", "run", "--rm", "-d", "-p", "17860:7860",
|
| 489 |
+
"-e", f"SIGLIP2_TRITON_SERVER_AUTH_TOKEN={token}",
|
| 490 |
+
"-e", f"SIMREADYCENTRAL_INDEX_STORE_AWS_ACCESS_KEY_ID={aws['AWS_ACCESS_KEY_ID']}",
|
| 491 |
+
"-e", f"SIMREADYCENTRAL_INDEX_STORE_AWS_SECRET_ACCESS_KEY={aws['AWS_SECRET_ACCESS_KEY']}",
|
| 492 |
+
tags["hf_space"]],
|
| 493 |
+
capture_output=True, text=True, check=True, timeout=60,
|
| 494 |
+
)
|
| 495 |
+
container = result.stdout.strip()
|
| 496 |
+
try:
|
| 497 |
+
import time
|
| 498 |
+
import httpx
|
| 499 |
+
healthy = False
|
| 500 |
+
deadline = time.monotonic() + 300
|
| 501 |
+
while time.monotonic() < deadline:
|
| 502 |
+
try:
|
| 503 |
+
if httpx.get("http://localhost:17860/health", timeout=5.0).status_code == 200:
|
| 504 |
+
healthy = True
|
| 505 |
+
break
|
| 506 |
+
except httpx.HTTPError:
|
| 507 |
+
pass
|
| 508 |
+
time.sleep(2)
|
| 509 |
+
print(f" /health responded: {healthy}")
|
| 510 |
+
if not healthy:
|
| 511 |
+
print(" FAIL: hf-space image never became healthy")
|
| 512 |
+
ok = False
|
| 513 |
+
|
| 514 |
+
# /health only proves deepsearch-api/OpenSearch came up β it says
|
| 515 |
+
# nothing about neo4j/asset-graph-service, which have their own
|
| 516 |
+
# offline-load startup path (pipeline/hf_space_seed.py
|
| 517 |
+
# --component=neo4j) that can fail silently as far as /health is
|
| 518 |
+
# concerned, and which routinely takes longer than deepsearch-api's
|
| 519 |
+
# own startup (a multi-hundred-MB neo4j-admin load, run offline,
|
| 520 |
+
# before that program even execs into `neo4j console`) β so this
|
| 521 |
+
# has to poll, not fire once right after /health passes. Exercise
|
| 522 |
+
# a real graph read through nginx, the same way a client actually
|
| 523 |
+
# would: search for a real asset url, then ask
|
| 524 |
+
# asset-graph-service for its prims.
|
| 525 |
+
graph_ok = False
|
| 526 |
+
if healthy:
|
| 527 |
+
graph_deadline = time.monotonic() + 180
|
| 528 |
+
while time.monotonic() < graph_deadline and not graph_ok:
|
| 529 |
+
try:
|
| 530 |
+
search_resp = httpx.get(
|
| 531 |
+
"http://localhost:17860/search", params={"query": "box", "limit": 1}, timeout=10.0,
|
| 532 |
+
)
|
| 533 |
+
results = search_resp.json()
|
| 534 |
+
asset_url = results[0]["url"] if results else None
|
| 535 |
+
if asset_url:
|
| 536 |
+
prims_resp = httpx.get(
|
| 537 |
+
"http://localhost:17860/asset_graph/usd/prims",
|
| 538 |
+
params={"asset_url": asset_url}, timeout=10.0,
|
| 539 |
+
)
|
| 540 |
+
prims = prims_resp.json()
|
| 541 |
+
graph_ok = prims_resp.status_code == 200 and isinstance(prims, list) and len(prims) > 0
|
| 542 |
+
except (httpx.HTTPError, ValueError):
|
| 543 |
+
pass
|
| 544 |
+
if not graph_ok:
|
| 545 |
+
time.sleep(3)
|
| 546 |
+
print(f" asset-graph-service responded with real prims: {graph_ok}")
|
| 547 |
+
if not graph_ok:
|
| 548 |
+
print(" FAIL: hf-space image's neo4j/asset-graph-service restore did not produce queryable data")
|
| 549 |
+
ok = False
|
| 550 |
+
|
| 551 |
+
# Recorded independently of `ok`/ the other images' checks β
|
| 552 |
+
# deploy-space (pipeline/hf_space.py) gates on this specifically,
|
| 553 |
+
# not on the separate full-stack `verify` stage's smoke_passed,
|
| 554 |
+
# since that stage also exercises asset-graph-service and other
|
| 555 |
+
# components the hf_space image doesn't even include.
|
| 556 |
+
manifest["hf_space_smoke_passed"] = healthy and graph_ok
|
| 557 |
+
save_manifest(manifest)
|
| 558 |
+
finally:
|
| 559 |
+
subprocess.run(["docker", "rm", "-f", container], capture_output=True, check=False, timeout=30)
|
| 560 |
+
|
| 561 |
+
if "hf_space_gpu" in tags:
|
| 562 |
+
print(" hf-space-gpu (local siglip2-triton variant) ...")
|
| 563 |
+
# Unlike every other smoke test in this function, this one needs an
|
| 564 |
+
# actual GPU on the machine running `bake` β `--gpus all` fails fast
|
| 565 |
+
# and loudly if there isn't one, rather than hanging, which is the
|
| 566 |
+
# right failure mode here: this is why this variant is opt-in
|
| 567 |
+
# (HF_SPACE_BUILD_GPU_VARIANT) and why the existing nightly CI
|
| 568 |
+
# workflow (no-GPU-runner, per docs/plans/index-regeneration-
|
| 569 |
+
# pipeline.md Β§12) doesn't enable it. No NVCF token needed. AWS keys
|
| 570 |
+
# still are β the index restore path is identical to the NVCF
|
| 571 |
+
# variant's.
|
| 572 |
+
aws = artifact_env()
|
| 573 |
+
result = subprocess.run(
|
| 574 |
+
["docker", "run", "--rm", "-d", "-p", "17861:7860", "--gpus", "all",
|
| 575 |
+
"-e", f"SIMREADYCENTRAL_INDEX_STORE_AWS_ACCESS_KEY_ID={aws['AWS_ACCESS_KEY_ID']}",
|
| 576 |
+
"-e", f"SIMREADYCENTRAL_INDEX_STORE_AWS_SECRET_ACCESS_KEY={aws['AWS_SECRET_ACCESS_KEY']}",
|
| 577 |
+
tags["hf_space_gpu"]],
|
| 578 |
+
capture_output=True, text=True, check=True, timeout=60,
|
| 579 |
+
)
|
| 580 |
+
container = result.stdout.strip()
|
| 581 |
+
try:
|
| 582 |
+
import time
|
| 583 |
+
import httpx
|
| 584 |
+
healthy = False
|
| 585 |
+
# Longer deadline than the NVCF variant's: this one also has to
|
| 586 |
+
# load ~3.6 GB of ONNX weights onto the GPU before deepsearch-api
|
| 587 |
+
# execs uvicorn (see supervisord-gpu.conf) β not measured live
|
| 588 |
+
# yet (see docker/Dockerfile.hf-space-gpu's header), budgeted
|
| 589 |
+
# generously rather than guessed tight.
|
| 590 |
+
deadline = time.monotonic() + 600
|
| 591 |
+
while time.monotonic() < deadline:
|
| 592 |
+
try:
|
| 593 |
+
if httpx.get("http://localhost:17861/health", timeout=5.0).status_code == 200:
|
| 594 |
+
healthy = True
|
| 595 |
+
break
|
| 596 |
+
except httpx.HTTPError:
|
| 597 |
+
pass
|
| 598 |
+
time.sleep(2)
|
| 599 |
+
print(f" /health responded: {healthy}")
|
| 600 |
+
if not healthy:
|
| 601 |
+
print(" FAIL: hf-space-gpu image never became healthy")
|
| 602 |
+
ok = False
|
| 603 |
+
|
| 604 |
+
embedding_ok = False
|
| 605 |
+
if healthy:
|
| 606 |
+
try:
|
| 607 |
+
search_resp = httpx.get(
|
| 608 |
+
"http://localhost:17861/search", params={"query": "box", "limit": 1}, timeout=15.0,
|
| 609 |
+
)
|
| 610 |
+
results = search_resp.json()
|
| 611 |
+
# A non-empty, non-erroring hybrid-search response through
|
| 612 |
+
# nginx is proof the local siglip2-triton text-embedding
|
| 613 |
+
# round trip actually worked, not just that the process
|
| 614 |
+
# answered /health β same "exercise a real call, not just
|
| 615 |
+
# readiness" reasoning as the NVCF variant's asset-graph
|
| 616 |
+
# check above.
|
| 617 |
+
embedding_ok = search_resp.status_code == 200 and isinstance(results, list) and len(results) > 0
|
| 618 |
+
except (httpx.HTTPError, ValueError):
|
| 619 |
+
pass
|
| 620 |
+
print(f" local siglip2-triton search round trip: {embedding_ok}")
|
| 621 |
+
if not embedding_ok:
|
| 622 |
+
print(" FAIL: hf-space-gpu image's local siglip2-triton embedding path did not "
|
| 623 |
+
"return real results")
|
| 624 |
+
ok = False
|
| 625 |
+
|
| 626 |
+
manifest["hf_space_gpu_smoke_passed"] = healthy and embedding_ok
|
| 627 |
+
save_manifest(manifest)
|
| 628 |
+
finally:
|
| 629 |
+
subprocess.run(["docker", "rm", "-f", container], capture_output=True, check=False, timeout=30)
|
| 630 |
+
|
| 631 |
+
return 0 if ok else 1
|
pipeline/clear.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 1b (pre-submit) β clear stale data before delta submission.
|
| 2 |
+
|
| 3 |
+
Two OpenSearch/Neo4j write paths have different idempotency on resubmission
|
| 4 |
+
(verified against upstream source, not assumed):
|
| 5 |
+
|
| 6 |
+
* OpenSearch base documents are safe: the doc `_id` is a deterministic hash
|
| 7 |
+
of the URL (`OpenSearchCache.update_meta`,
|
| 8 |
+
packages/search-utils/.../elasticsearch.py:90-91), so `/process/asset`
|
| 9 |
+
overwrites the same document. No clearing needed for a resubmission.
|
| 10 |
+
* Neo4j asset *nodes* are safe (`MERGE (n:Asset {url: ...})`,
|
| 11 |
+
asset_graph_service/db/neo4j.py:244-253) but *edges* are not β they are
|
| 12 |
+
written with `apoc.create.relationship`, which always inserts
|
| 13 |
+
(neo4j.py:262-274). Reprocessing an asset that already has edges in the
|
| 14 |
+
graph duplicates every one of them.
|
| 15 |
+
|
| 16 |
+
`DETACH DELETE` on the asset node removes the node *and every edge touching
|
| 17 |
+
it* in one statement, which fixes the edge case and doubles as exactly what a
|
| 18 |
+
`removed` asset needs (gone from both stores, never resubmitted). So one
|
| 19 |
+
`clear()` step covers both:
|
| 20 |
+
|
| 21 |
+
* a "repeating" asset β present in the restored baseline AND in this run's
|
| 22 |
+
`assets.jsonl` β gets Neo4j-cleared, then resubmitted by `submit`. The
|
| 23 |
+
OpenSearch doc does not need clearing; the resubmission overwrites it.
|
| 24 |
+
* a "removed" asset (corpus.json's `removed` list) β gets Neo4j-cleared
|
| 25 |
+
*and* its OpenSearch doc deleted, and is never resubmitted (enumerate
|
| 26 |
+
already drops it from assets.jsonl).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
from typing import Dict, List, Set
|
| 32 |
+
|
| 33 |
+
import httpx
|
| 34 |
+
|
| 35 |
+
from .config import BUILD_DIR, Config
|
| 36 |
+
from .corpus import Corpus
|
| 37 |
+
from .enumerate_assets import load_assets
|
| 38 |
+
from .restore import load_baseline_urls
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def compute_targets(corpus: Corpus) -> Dict[str, Set[str]]:
|
| 42 |
+
current = {a.url for a in load_assets()}
|
| 43 |
+
baseline = load_baseline_urls()
|
| 44 |
+
removed = corpus.removed_set
|
| 45 |
+
|
| 46 |
+
repeating = current & baseline
|
| 47 |
+
return {
|
| 48 |
+
"neo4j_clear": repeating | removed,
|
| 49 |
+
"opensearch_delete": set(removed),
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def clear_neo4j(cfg: Config, urls: List[str]) -> int:
|
| 54 |
+
if not urls:
|
| 55 |
+
return 0
|
| 56 |
+
from neo4j import GraphDatabase
|
| 57 |
+
|
| 58 |
+
driver = GraphDatabase.driver(cfg.neo4j_bolt_url, auth=None)
|
| 59 |
+
try:
|
| 60 |
+
with driver.session() as session:
|
| 61 |
+
result = session.run(
|
| 62 |
+
"UNWIND $urls AS u MATCH (n:Asset {url: u}) DETACH DELETE n RETURN count(n) AS c",
|
| 63 |
+
urls=urls,
|
| 64 |
+
)
|
| 65 |
+
return int(result.single()["c"])
|
| 66 |
+
finally:
|
| 67 |
+
driver.close()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def clear_opensearch(cfg: Config, urls: List[str]) -> int:
|
| 71 |
+
if not urls:
|
| 72 |
+
return 0
|
| 73 |
+
response = httpx.post(
|
| 74 |
+
f"{cfg.opensearch_url}/{cfg.main_index}/_delete_by_query?refresh=true",
|
| 75 |
+
json={"query": {"terms": {"base_key": urls}}},
|
| 76 |
+
timeout=60.0,
|
| 77 |
+
)
|
| 78 |
+
response.raise_for_status()
|
| 79 |
+
return int(response.json().get("deleted", 0))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def run(cfg: Config) -> int:
|
| 83 |
+
corpus = Corpus.load(BUILD_DIR / "corpus.json")
|
| 84 |
+
targets = compute_targets(corpus)
|
| 85 |
+
|
| 86 |
+
neo4j_urls = sorted(targets["neo4j_clear"])
|
| 87 |
+
opensearch_urls = sorted(targets["opensearch_delete"])
|
| 88 |
+
|
| 89 |
+
if not neo4j_urls and not opensearch_urls:
|
| 90 |
+
print("nothing to clear β no repeating or removed assets against the baseline")
|
| 91 |
+
return 0
|
| 92 |
+
|
| 93 |
+
print(f"clearing {len(neo4j_urls)} Neo4j node(s) (repeating + removed) ...")
|
| 94 |
+
cleared = clear_neo4j(cfg, neo4j_urls)
|
| 95 |
+
print(f" detached and deleted {cleared} node(s)")
|
| 96 |
+
|
| 97 |
+
print(f"deleting {len(opensearch_urls)} OpenSearch doc(s) (removed only) ...")
|
| 98 |
+
deleted = clear_opensearch(cfg, opensearch_urls)
|
| 99 |
+
print(f" deleted {deleted} doc(s)")
|
| 100 |
+
|
| 101 |
+
return 0
|
pipeline/config.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pipeline configuration, loaded from config/pipeline.env.
|
| 2 |
+
|
| 3 |
+
Deliberately reads only this file (plus the process environment as an
|
| 4 |
+
override), never CI_* variables: stage 1 runs locally today and on a CI runner
|
| 5 |
+
later, and that later change should be a wrapper exporting the same names
|
| 6 |
+
rather than a second configuration path.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
# The default NVCF function backing the siglip2-triton embedding model.
|
| 18 |
+
# Fixed, not looked up by name: NVCF function-name resolution needs an extra
|
| 19 |
+
# API call the smoke tooling in ../usdsearch (ci/siglip2-triton/smoke_test_nvcf.py)
|
| 20 |
+
# only falls back to when no id is given. We always have the id, so nothing in
|
| 21 |
+
# this repo ever needs to resolve a name.
|
| 22 |
+
SIGLIP2_NVCF_FUNCTION_ID_DEFAULT = "af53131c-7416-40b4-b652-4a8b4c82a6e6"
|
| 23 |
+
SIGLIP2_NVCF_ENDPOINT_DEFAULT = "grpc.nvcf.nvidia.com:443"
|
| 24 |
+
|
| 25 |
+
# Same fallback chain and order as smoke_test_nvcf.py:106-107 in ../usdsearch
|
| 26 |
+
# (`NVCF_RUN_KEY or NGC_GENAI_API_KEY or NVCF_GENAI_RUN_KEY`), so whichever of
|
| 27 |
+
# the three an operator already has exported just works, matching upstream's
|
| 28 |
+
# own tooling rather than picking one name and hoping it matches.
|
| 29 |
+
SIGLIP2_NVCF_AUTH_TOKEN_VARS_DEFAULT = ("NVCF_RUN_KEY", "NGC_GENAI_API_KEY", "NVCF_GENAI_RUN_KEY")
|
| 30 |
+
|
| 31 |
+
# The HF Space GPU variant's Triton base image (bare, public NGC catalog
|
| 32 |
+
# tritonserver β verified live via the nvcr.io v2 registry API with an
|
| 33 |
+
# anonymous guest-client token, no docker login needed; model_repo/ is
|
| 34 |
+
# vendored in separately by scripts/extract_siglip2_model.sh, not baked into
|
| 35 |
+
# this base β see that script's header for why the prepackaged
|
| 36 |
+
# nvcr.io/nvidia/usdsearch/siglip2-triton image was dropped: its published
|
| 37 |
+
# tag bakes FP32 weights, too large for a T4's 16 GB VRAM) and the HF Space
|
| 38 |
+
# hardware tier it's meant to run on. See docker/Dockerfile.hf-space-gpu and
|
| 39 |
+
# docs/plans/siglip2-embedded-hf-space.md.
|
| 40 |
+
TRITONSERVER_BASE_IMAGE_DEFAULT = "nvcr.io/nvidia/tritonserver:26.05-py3"
|
| 41 |
+
HF_SPACE_GPU_HARDWARE_DEFAULT = "t4-medium"
|
| 42 |
+
|
| 43 |
+
# The two source-content backends the indexing stack (stage 1) can point at.
|
| 44 |
+
# "s3" reads S3_STORAGE_* directly; "storage_api" runs `hf-storage-api`
|
| 45 |
+
# (https://gitlab-master.nvidia.com/omniverse/gen-ai/hf-storage-api-service)
|
| 46 |
+
# locally, backed by a HuggingFace dataset β see docs/plans/
|
| 47 |
+
# index-regeneration-pipeline.md's decision log. Matches the corpus JSON's own
|
| 48 |
+
# `storage_backend` field (pipeline/corpus.py); enumerate_assets.py checks the
|
| 49 |
+
# two agree, since a corpus written for one backend indexed against the other
|
| 50 |
+
# would silently list zero assets rather than error clearly.
|
| 51 |
+
STORAGE_BACKENDS = ("s3", "storage_api")
|
| 52 |
+
DEFAULT_STORAGE_BACKEND = "s3"
|
| 53 |
+
|
| 54 |
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
| 55 |
+
BUILD_DIR = REPO_ROOT / ".build"
|
| 56 |
+
DEFAULT_ENV_FILE = REPO_ROOT / "config" / "pipeline.env"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def parse_env_file(path: Path) -> Dict[str, str]:
|
| 60 |
+
"""Parse a KEY=VALUE env file. Blank lines and # comments are ignored.
|
| 61 |
+
|
| 62 |
+
Values are taken literally: no shell expansion, no quote stripping beyond a
|
| 63 |
+
single matched pair, because these values end up in URLs and S3 keys where a
|
| 64 |
+
stray transformation is hard to notice and expensive to debug.
|
| 65 |
+
"""
|
| 66 |
+
out: Dict[str, str] = {}
|
| 67 |
+
if not path.exists():
|
| 68 |
+
return out
|
| 69 |
+
for raw in path.read_text().splitlines():
|
| 70 |
+
line = raw.strip()
|
| 71 |
+
if not line or line.startswith("#"):
|
| 72 |
+
continue
|
| 73 |
+
if "=" not in line:
|
| 74 |
+
continue
|
| 75 |
+
key, _, value = line.partition("=")
|
| 76 |
+
key = key.strip()
|
| 77 |
+
value = value.strip()
|
| 78 |
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
| 79 |
+
value = value[1:-1]
|
| 80 |
+
out[key] = value
|
| 81 |
+
return out
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@dataclass
|
| 85 |
+
class Config:
|
| 86 |
+
values: Dict[str, str] = field(default_factory=dict)
|
| 87 |
+
env_file: Path = DEFAULT_ENV_FILE
|
| 88 |
+
|
| 89 |
+
@classmethod
|
| 90 |
+
def load(cls, env_file: Optional[Path] = None) -> "Config":
|
| 91 |
+
path = Path(env_file) if env_file else DEFAULT_ENV_FILE
|
| 92 |
+
values = parse_env_file(path)
|
| 93 |
+
# Process environment wins, so a one-off override needs no file edit.
|
| 94 |
+
for key in list(values) + [
|
| 95 |
+
"ARTIFACT_S3_URI",
|
| 96 |
+
"RUN_ID",
|
| 97 |
+
"RESTORE_RUN_ID",
|
| 98 |
+
"ES_NAME",
|
| 99 |
+
"CORPUS_FILE",
|
| 100 |
+
"INFO_ENDPOINT_URL",
|
| 101 |
+
"OPENSEARCH_URL",
|
| 102 |
+
"SIGLIP2_BACKEND",
|
| 103 |
+
"SIGLIP2_NVCF_ENDPOINT",
|
| 104 |
+
"STORAGE_BACKEND_TYPE",
|
| 105 |
+
"SIGLIP2_NVCF_FUNCTION_ID",
|
| 106 |
+
"SIGLIP2_NVCF_FUNCTION_VERSION_ID",
|
| 107 |
+
"HF_SPACE_REPO_ID",
|
| 108 |
+
"HF_SPACE_TITLE",
|
| 109 |
+
"HF_SPACE_REPO_ID_GPU",
|
| 110 |
+
"HF_SPACE_TITLE_GPU",
|
| 111 |
+
"HF_SPACE_BUILD_GPU_VARIANT",
|
| 112 |
+
"HF_SPACE_GPU_HARDWARE",
|
| 113 |
+
"TRITONSERVER_BASE_IMAGE",
|
| 114 |
+
"LLM_ENDPOINT_BASE_URL",
|
| 115 |
+
"LLM_PARSING_MODEL",
|
| 116 |
+
"VISION_VALIDATION_MODEL",
|
| 117 |
+
"VLM_METADATA_ENABLED",
|
| 118 |
+
]:
|
| 119 |
+
if os.environ.get(key):
|
| 120 |
+
values[key] = os.environ[key]
|
| 121 |
+
return cls(values=values, env_file=path)
|
| 122 |
+
|
| 123 |
+
# ββ typed accessors βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 124 |
+
|
| 125 |
+
def get(self, key: str, default: str = "") -> str:
|
| 126 |
+
return self.values.get(key, default) or default
|
| 127 |
+
|
| 128 |
+
def require(self, key: str) -> str:
|
| 129 |
+
value = self.values.get(key, "").strip()
|
| 130 |
+
if not value:
|
| 131 |
+
raise SystemExit(
|
| 132 |
+
f"{key} is not set. Add it to {self.env_file} "
|
| 133 |
+
f"(see config/pipeline.env.example)."
|
| 134 |
+
)
|
| 135 |
+
return value
|
| 136 |
+
|
| 137 |
+
def get_int(self, key: str, default: int) -> int:
|
| 138 |
+
raw = self.values.get(key, "").strip()
|
| 139 |
+
return int(raw) if raw else default
|
| 140 |
+
|
| 141 |
+
def get_float(self, key: str, default: float) -> float:
|
| 142 |
+
raw = self.values.get(key, "").strip()
|
| 143 |
+
return float(raw) if raw else default
|
| 144 |
+
|
| 145 |
+
# ββ derived values ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def es_name(self) -> str:
|
| 149 |
+
return self.get("ES_NAME", "simready-central")
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def main_index(self) -> str:
|
| 153 |
+
"""`{ES_NAME}-ver5.0` β OSCacheDict.get_index_name()."""
|
| 154 |
+
return f"{self.es_name}-ver5.0"
|
| 155 |
+
|
| 156 |
+
@property
|
| 157 |
+
def image_cache_index(self) -> str:
|
| 158 |
+
"""`{ES_NAME}-ver4.0-image-cache` β storage_index_name + field + "-cache"."""
|
| 159 |
+
return f"{self.es_name}-ver4.0-image-cache"
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def corpus_file(self) -> Path:
|
| 163 |
+
return REPO_ROOT / self.get("CORPUS_FILE", "config/corpus.json")
|
| 164 |
+
|
| 165 |
+
@property
|
| 166 |
+
def info_endpoint(self) -> str:
|
| 167 |
+
return self.get("INFO_ENDPOINT_URL", "http://localhost:8001").rstrip("/")
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def opensearch_url(self) -> str:
|
| 171 |
+
return self.get("OPENSEARCH_URL", "http://localhost:9200").rstrip("/")
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def neo4j_bolt_url(self) -> str:
|
| 175 |
+
return self.get("NEO4J_BOLT_URL", "bolt://localhost:7687")
|
| 176 |
+
|
| 177 |
+
# ββ siglip2 backend (local Triton vs. NVCF) βββββββββββββββββββββββββββββ
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def siglip2_backend(self) -> str:
|
| 181 |
+
backend = self.get("SIGLIP2_BACKEND", "local").strip().lower()
|
| 182 |
+
if backend not in ("local", "nvcf"):
|
| 183 |
+
raise SystemExit(f"SIGLIP2_BACKEND must be 'local' or 'nvcf', got {backend!r}")
|
| 184 |
+
return backend
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def siglip2_nvcf(self) -> bool:
|
| 188 |
+
return self.siglip2_backend == "nvcf"
|
| 189 |
+
|
| 190 |
+
# ββ HF Space GPU variant (local siglip2-triton, no NVCF) ββββββββββββββββ
|
| 191 |
+
# Independent of SIGLIP2_BACKEND above (which is the *indexing stack's*
|
| 192 |
+
# compose backend, local-GPU vs. NVCF) β this is a second, optional
|
| 193 |
+
# `bake`/`deploy-space-gpu` target that serves the same already-indexed
|
| 194 |
+
# corpus through docker/Dockerfile.hf-space-gpu instead of
|
| 195 |
+
# docker/Dockerfile.hf-space, off by default so existing pipelines that
|
| 196 |
+
# don't want the extra ~15 GB build/push are unaffected. See
|
| 197 |
+
# docs/plans/siglip2-embedded-hf-space.md.
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def hf_space_gpu_variant_enabled(self) -> bool:
|
| 201 |
+
return self.get("HF_SPACE_BUILD_GPU_VARIANT", "false").strip().lower() in ("1", "true", "yes")
|
| 202 |
+
|
| 203 |
+
# ββ storage backend (S3 vs. hf-storage-api / storage_api) ββββββββββββββ
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def storage_backend(self) -> str:
|
| 207 |
+
backend = self.get("STORAGE_BACKEND_TYPE", DEFAULT_STORAGE_BACKEND).strip().lower()
|
| 208 |
+
if backend not in STORAGE_BACKENDS:
|
| 209 |
+
raise SystemExit(f"STORAGE_BACKEND_TYPE must be one of {STORAGE_BACKENDS}, got {backend!r}")
|
| 210 |
+
return backend
|
| 211 |
+
|
| 212 |
+
@property
|
| 213 |
+
def storage_api_backend(self) -> bool:
|
| 214 |
+
return self.storage_backend == "storage_api"
|
| 215 |
+
|
| 216 |
+
def siglip2_nvcf_env(self) -> Dict[str, str]:
|
| 217 |
+
"""Env vars the gRPC Triton client (packages/siglip2-triton-client)
|
| 218 |
+
reads to reach an NVCF-hosted function instead of a local container β
|
| 219 |
+
same `SIGLIP2_TRITON_SERVER_*`-prefixed pydantic-settings fields
|
| 220 |
+
either way, just pointed at NVCF with auth headers instead of the
|
| 221 |
+
local siglip2-triton service.
|
| 222 |
+
|
| 223 |
+
`SIGLIP2_NVCF_AUTH_TOKEN_VAR`, if set, names one specific *environment
|
| 224 |
+
variable* to read the token from. Left unset (the default), the same
|
| 225 |
+
three-way fallback chain as upstream's smoke tooling applies β the
|
| 226 |
+
token itself is never read from `pipeline.env` or committed, same
|
| 227 |
+
pattern as the S3 artifact credentials.
|
| 228 |
+
"""
|
| 229 |
+
function_id = self.get("SIGLIP2_NVCF_FUNCTION_ID", SIGLIP2_NVCF_FUNCTION_ID_DEFAULT)
|
| 230 |
+
version_id = self.get("SIGLIP2_NVCF_FUNCTION_VERSION_ID").strip()
|
| 231 |
+
headers = {"function-id": function_id}
|
| 232 |
+
if version_id:
|
| 233 |
+
headers["function-version-id"] = version_id
|
| 234 |
+
|
| 235 |
+
explicit_var = self.get("SIGLIP2_NVCF_AUTH_TOKEN_VAR").strip()
|
| 236 |
+
token_vars = (explicit_var,) if explicit_var else SIGLIP2_NVCF_AUTH_TOKEN_VARS_DEFAULT
|
| 237 |
+
token = ""
|
| 238 |
+
for var in token_vars:
|
| 239 |
+
token = os.environ.get(var, "").strip()
|
| 240 |
+
if token:
|
| 241 |
+
break
|
| 242 |
+
if not token:
|
| 243 |
+
raise SystemExit(
|
| 244 |
+
f"SIGLIP2_BACKEND=nvcf but none of {', '.join('$' + v for v in token_vars)} "
|
| 245 |
+
f"is set in the environment. One must hold the NVCF run key "
|
| 246 |
+
f"(never committed to pipeline.env)."
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
return {
|
| 250 |
+
"SIGLIP2_TRITON_SERVER_URL": self.get("SIGLIP2_NVCF_ENDPOINT", SIGLIP2_NVCF_ENDPOINT_DEFAULT),
|
| 251 |
+
"SIGLIP2_TRITON_SERVER_SSL": "true",
|
| 252 |
+
"SIGLIP2_TRITON_SERVER_AUTH_TOKEN": token,
|
| 253 |
+
"SIGLIP2_TRITON_SERVER_HEADERS": json.dumps(headers),
|
| 254 |
+
# deepsearch-api reads this one unprefixed (main.py:161); the
|
| 255 |
+
# worker reads the SIGLIP2_TRITON_SERVER_URL above instead.
|
| 256 |
+
"TRITON_SERVER_URL": self.get("SIGLIP2_NVCF_ENDPOINT", SIGLIP2_NVCF_ENDPOINT_DEFAULT),
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
# ββ LLM parsing / VLM verification endpoint (HF Space) ββββββββββββββββββ
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def llm_endpoint_base_url(self) -> str:
|
| 263 |
+
"""Empty (the default) means the HF Space keeps shipping with both
|
| 264 |
+
roles disabled, exactly as it does today
|
| 265 |
+
(`USDSEARCH_LLM_PARSING_ENABLED`/`USDSEARCH_VISION_VALIDATION_ENABLED`
|
| 266 |
+
baked `false` in docker/Dockerfile.hf-space) β this is the one knob
|
| 267 |
+
that turns them on. Point it at any OpenAI-compatible
|
| 268 |
+
`/v1/chat/completions` server that serves both a `LLM_PARSING_MODEL`
|
| 269 |
+
alias and a `VISION_VALIDATION_MODEL` alias β e.g. either of this
|
| 270 |
+
repo's own deploy scripts' endpoints
|
| 271 |
+
(scripts/deploy_hf_endpoint.py / scripts/deploy_nvcf_endpoint.py)."""
|
| 272 |
+
return self.get("LLM_ENDPOINT_BASE_URL").strip()
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def llm_parsing_model(self) -> str:
|
| 276 |
+
return self.get("LLM_PARSING_MODEL", "parser")
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def vision_validation_model(self) -> str:
|
| 280 |
+
return self.get("VISION_VALIDATION_MODEL", "reranker")
|
| 281 |
+
|
| 282 |
+
def llm_endpoint_api_key(self) -> str:
|
| 283 |
+
"""Read from the environment only β never `pipeline.env`, same
|
| 284 |
+
policy as every other credential this pipeline touches (NVCF token,
|
| 285 |
+
S3 keys, HF Space deploy token). Provider-agnostic by design β HF
|
| 286 |
+
Inference Endpoints, NVCF, or anything else OpenAI-compatible β so
|
| 287 |
+
there's no single "the" NVCF/HF var name to default to; instead of
|
| 288 |
+
forcing every provider through one hardcoded `$LLM_ENDPOINT_API_KEY`
|
| 289 |
+
name, `LLM_ENDPOINT_API_KEY_VAR` (in `pipeline.env`, not sensitive
|
| 290 |
+
itself β it's just a variable *name*) names which env var actually
|
| 291 |
+
holds the bearer token for whichever endpoint `LLM_ENDPOINT_BASE_URL`
|
| 292 |
+
currently points at, so switching providers (e.g. NVCF's
|
| 293 |
+
`$NVCF_GENAI_RUN_KEY` vs. an HF Inference Endpoint's
|
| 294 |
+
`$HF_INFERENCE_ENDPOINT_TOKEN` β see docs/development.md) is a
|
| 295 |
+
`pipeline.env` edit, not an extra `export` before every command.
|
| 296 |
+
`$LLM_ENDPOINT_API_KEY` itself, if set, still wins outright β same
|
| 297 |
+
name/precedence as before this existed, so nothing already relying
|
| 298 |
+
on it breaks."""
|
| 299 |
+
explicit_var = self.get("LLM_ENDPOINT_API_KEY_VAR").strip()
|
| 300 |
+
token = os.environ.get("LLM_ENDPOINT_API_KEY", "").strip()
|
| 301 |
+
if not token and explicit_var:
|
| 302 |
+
token = os.environ.get(explicit_var, "").strip()
|
| 303 |
+
if not token:
|
| 304 |
+
hint = f" (or ${explicit_var}, per LLM_ENDPOINT_API_KEY_VAR)" if explicit_var else ""
|
| 305 |
+
raise SystemExit(
|
| 306 |
+
f"LLM_ENDPOINT_BASE_URL is set but no bearer token was found β set "
|
| 307 |
+
f"$LLM_ENDPOINT_API_KEY{hint} in the environment (never pipeline.env) to the "
|
| 308 |
+
f"key for that endpoint."
|
| 309 |
+
)
|
| 310 |
+
return token
|
| 311 |
+
|
| 312 |
+
def llm_endpoint_variables(self) -> Dict[str, str]:
|
| 313 |
+
"""Non-sensitive `USDSEARCH_LLM_*`/`USDSEARCH_VISION_VALIDATION_*`
|
| 314 |
+
env vars for the HF Space β plain runtime Space "Variables", not
|
| 315 |
+
Dockerfile build ARGs: docker/Dockerfile.hf-space already bakes
|
| 316 |
+
`USDSEARCH_LLM_PARSING_ENABLED`/`USDSEARCH_VISION_VALIDATION_ENABLED`
|
| 317 |
+
as `ENV ... false` defaults, and a runtime-injected env var of the
|
| 318 |
+
same name overrides an image's baked `ENV` value (standard Docker
|
| 319 |
+
behavior) β so applying these to an already-built, already-running
|
| 320 |
+
Space needs no rebuild, just a restart. See llm_endpoint_api_key()
|
| 321 |
+
for the one secret half of this (USDSEARCH_LLM_API_KEY).
|
| 322 |
+
|
| 323 |
+
`enable_thinking: false` is forced into both EXTRA_BODY values
|
| 324 |
+
unconditionally: it's required for this pipeline's own Qwen3.6
|
| 325 |
+
deploy scripts and a harmless no-op for a hypothetical non-Qwen
|
| 326 |
+
endpoint (OpenAI-compatible servers ignore unknown request-body
|
| 327 |
+
keys β see llm_client.client.LLMClient's own docstring in
|
| 328 |
+
../usdsearch). `USDSEARCH_VISION_VALIDATION_METHOD=logprob` matches
|
| 329 |
+
this pipeline's own deploy scripts' `reranker` role (a single-token
|
| 330 |
+
logprob judge, not the default `generative` structured-JSON verdict)
|
| 331 |
+
β pin explicitly rather than relying on the client's own default,
|
| 332 |
+
since a `generative`-only endpoint would otherwise get a request
|
| 333 |
+
shape it never expected. Verified live against the deployed NVCF
|
| 334 |
+
`reranker` endpoint: it returns real per-token `top_logprobs`, and a
|
| 335 |
+
full `/vlm_validate/search_result` round trip through this image
|
| 336 |
+
comes back with `reasoning: "P(Yes)=... from first-token
|
| 337 |
+
log-probs."`, not a generated sentence, confirming the logprob path
|
| 338 |
+
actually runs rather than just the flag being set.
|
| 339 |
+
|
| 340 |
+
The `..._LOGPROB_*` knobs are pinned so client default changes can't
|
| 341 |
+
drift this deployment. `max_tokens=1`: Qwen emits the verdict at
|
| 342 |
+
position 0. `top_logprobs=20`: the judge scores P(Yes)/(P(Yes)+P(No))
|
| 343 |
+
over the returned tokens, so 1 makes every verdict 0 or 100 and
|
| 344 |
+
abstains (504) when the top token is neither; it costs nothing extra
|
| 345 |
+
(the engine computes the full distribution) and must stay <= 20 (the
|
| 346 |
+
client cap; the engine allows 25). `MAX_TRIES=2`: the client calls
|
| 347 |
+
with max_retries=0, so one transient engine error would otherwise be a
|
| 348 |
+
503, which stops the Explorer validating the whole search.
|
| 349 |
+
"""
|
| 350 |
+
if not self.llm_endpoint_base_url:
|
| 351 |
+
# Nothing to override β defer entirely to the Dockerfile's own
|
| 352 |
+
# baked `false` defaults rather than pushing a redundant (and,
|
| 353 |
+
# for USDSEARCH_LLM_BASE_URL, actively wrong β an explicit empty
|
| 354 |
+
# string is not the same as "unset" to pydantic-settings)
|
| 355 |
+
# variable update.
|
| 356 |
+
return {}
|
| 357 |
+
extra_body = json.dumps({"chat_template_kwargs": {"enable_thinking": False}})
|
| 358 |
+
return {
|
| 359 |
+
"USDSEARCH_LLM_PARSING_ENABLED": "true",
|
| 360 |
+
"USDSEARCH_VISION_VALIDATION_ENABLED": "true",
|
| 361 |
+
"USDSEARCH_LLM_BASE_URL": self.llm_endpoint_base_url,
|
| 362 |
+
"USDSEARCH_LLM_PARSING_MODEL": self.llm_parsing_model,
|
| 363 |
+
"USDSEARCH_VISION_VALIDATION_MODEL": self.vision_validation_model,
|
| 364 |
+
"USDSEARCH_LLM_PARSING_EXTRA_BODY": extra_body,
|
| 365 |
+
"USDSEARCH_VISION_VALIDATION_EXTRA_BODY": extra_body,
|
| 366 |
+
"USDSEARCH_VISION_VALIDATION_METHOD": "logprob",
|
| 367 |
+
"USDSEARCH_VISION_VALIDATION_LOGPROB_COVER_MIN": "0.2",
|
| 368 |
+
"USDSEARCH_VISION_VALIDATION_LOGPROB_ABSTAIN_AS_NO": "false",
|
| 369 |
+
"USDSEARCH_VISION_VALIDATION_LOGPROB_TOP_LOGPROBS": "20",
|
| 370 |
+
"USDSEARCH_VISION_VALIDATION_LOGPROB_MAX_TOKENS": "1",
|
| 371 |
+
"USDSEARCH_VISION_VALIDATION_MAX_TRIES": "2",
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
# ββ VLM metadata extraction from thumbnails (indexing-time plugin) ββββββ
|
| 375 |
+
#
|
| 376 |
+
# The `thumbnail_to_vision_metadata` plugin (inactive by default, see
|
| 377 |
+
# config/plugin.config.yaml) calls out to an LLM/VLM at indexing time to
|
| 378 |
+
# caption/tag each asset's thumbnail. Off unless VLM_METADATA_ENABLED=true
|
| 379 |
+
# β same "additive, opt-in" posture as SIGLIP2_BACKEND=nvcf and
|
| 380 |
+
# STORAGE_BACKEND_TYPE=storage_api.
|
| 381 |
+
#
|
| 382 |
+
# Deliberately its OWN connection, separate from LLM_ENDPOINT_BASE_URL
|
| 383 |
+
# (the HF Space's real-time LLM-parsing/vision-validation connection,
|
| 384 |
+
# llm_endpoint_variables() above): this role runs batch/background at
|
| 385 |
+
# indexing time in its own container (monitor-worker-thumbnail-to-
|
| 386 |
+
# vision-metadata), never in the HF Space process, so there is no reason
|
| 387 |
+
# to share a connection sized/rate-limited for interactive query traffic.
|
| 388 |
+
#
|
| 389 |
+
# No pipeline-specific wrapper env vars here (unlike LLM_ENDPOINT_* above)
|
| 390 |
+
# β docker-compose.indexing.vlm-metadata.yml's worker reads the real
|
| 391 |
+
# container var names directly (USDSEARCH_LLM_API_KEY/_BASE_URL,
|
| 392 |
+
# USDSEARCH_VISION_METADATA_MODEL β packages/llm-client/llm_client/
|
| 393 |
+
# client.py's LLMConnectionConfig / metadata_generation.py's
|
| 394 |
+
# MetadataGenerationConfig in ../usdsearch), null-pass-through from
|
| 395 |
+
# whatever's already exported in the shell (compose_env()'s `dict(os.environ)`
|
| 396 |
+
# base already carries them to the `docker compose` subprocess β no
|
| 397 |
+
# translation needed). Left unset, USDSEARCH_LLM_BASE_URL/
|
| 398 |
+
# USDSEARCH_VISION_METADATA_MODEL fall back to llm_client's own shipped
|
| 399 |
+
# defaults (DEFAULT_BASE_URL = "https://inference-api.nvidia.com" / NVIDIA
|
| 400 |
+
# Inference Hub, MetadataGenerationConfig.model = "azure/openai/gpt-5.6-sol"
|
| 401 |
+
# β see ../usdsearch's docs/models-and-config.md, "Metadata gen" row) β
|
| 402 |
+
# exactly the endpoint+model this deployment wants. Only
|
| 403 |
+
# $USDSEARCH_LLM_API_KEY is required; require_vlm_metadata_credentials()
|
| 404 |
+
# below fails fast (before `up`) if it's missing.
|
| 405 |
+
|
| 406 |
+
@property
|
| 407 |
+
def vlm_metadata_enabled(self) -> bool:
|
| 408 |
+
return self.get("VLM_METADATA_ENABLED", "false").strip().lower() in ("1", "true", "yes")
|
| 409 |
+
|
| 410 |
+
def require_vlm_metadata_credentials(self) -> None:
|
| 411 |
+
if not os.environ.get("USDSEARCH_LLM_API_KEY", "").strip():
|
| 412 |
+
raise SystemExit(
|
| 413 |
+
"VLM_METADATA_ENABLED=true but $USDSEARCH_LLM_API_KEY is not set in the "
|
| 414 |
+
"environment (never pipeline.env) β the bearer key for the VLM metadata "
|
| 415 |
+
"endpoint (NVIDIA Inference Hub by default; see "
|
| 416 |
+
"../usdsearch/docs/models-and-config.md)."
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
def compose_env(self) -> Dict[str, str]:
|
| 420 |
+
"""The environment every `docker compose`/`docker run` subprocess
|
| 421 |
+
this pipeline spawns should get β `pipeline.env`'s values plus
|
| 422 |
+
(when `SIGLIP2_BACKEND=nvcf`) the computed NVCF endpoint/auth vars
|
| 423 |
+
the compose overlays interpolate.
|
| 424 |
+
|
| 425 |
+
A single source of truth for this, used everywhere a compose command
|
| 426 |
+
touches the indexing project (`run_pipeline.py`'s `up`/`down`,
|
| 427 |
+
`verify.py`'s runtime bring-up, and `export.py`/`restore.py`'s own
|
| 428 |
+
`stop`/`start neo4j asset-graph-service` calls) β those last two used
|
| 429 |
+
to build their `docker compose` commands with no `env=` at all,
|
| 430 |
+
silently inheriting the bare process environment instead. Harmless
|
| 431 |
+
so far only because neither `neo4j` nor `asset-graph-service`
|
| 432 |
+
actually reads the vars that went missing (`ES_NAME`,
|
| 433 |
+
`S3_STORAGE_BUCKET_NAME`, `SIGLIP2_TRITON_SERVER_*`) β but real
|
| 434 |
+
enough to show up as a page of "variable is not set" warnings in the
|
| 435 |
+
CI log, which is what surfaced it.
|
| 436 |
+
"""
|
| 437 |
+
env = dict(os.environ)
|
| 438 |
+
env.update({k: v for k, v in self.values.items() if v})
|
| 439 |
+
if self.siglip2_nvcf:
|
| 440 |
+
env.update(self.siglip2_nvcf_env())
|
| 441 |
+
if self.storage_api_backend:
|
| 442 |
+
# The hf-storage-api container's YAML-null env passthrough
|
| 443 |
+
# (docker-compose.indexing.storage-api.yml) forwards a host var
|
| 444 |
+
# only under its own literal name, HF_ADAPTER_HF_TOKEN β but
|
| 445 |
+
# every other HF credential in this pipeline (the enumerator,
|
| 446 |
+
# pipeline/hf_space.py) reads the standard $HF_TOKEN. Mapped here
|
| 447 |
+
# so operators only ever set one variable. Verified live: without
|
| 448 |
+
# this, hf-storage-api silently ran unauthenticated and every
|
| 449 |
+
# submission against a private dataset 401'd as "Asset missing".
|
| 450 |
+
hf_token = os.environ.get("HF_TOKEN", "").strip()
|
| 451 |
+
if hf_token:
|
| 452 |
+
env["HF_ADAPTER_HF_TOKEN"] = hf_token
|
| 453 |
+
# No vlm_metadata_enabled-specific merge needed here: `env` already
|
| 454 |
+
# started as `dict(os.environ)` above, so USDSEARCH_LLM_API_KEY/
|
| 455 |
+
# USDSEARCH_LLM_BASE_URL/USDSEARCH_VISION_METADATA_MODEL β whatever's
|
| 456 |
+
# already exported in the shell β reach docker-compose.indexing.vlm-metadata.yml's
|
| 457 |
+
# null-pass-through vars unchanged. See the "VLM metadata extraction"
|
| 458 |
+
# section above for why this role deliberately has no pipeline-specific
|
| 459 |
+
# wrapper vars the way LLM_ENDPOINT_* does.
|
| 460 |
+
return env
|
| 461 |
+
|
| 462 |
+
def indexing_compose_files(self) -> List[str]:
|
| 463 |
+
files = ["-f", str(REPO_ROOT / "compose" / "docker-compose.indexing.yml")]
|
| 464 |
+
if self.siglip2_nvcf:
|
| 465 |
+
files += ["-f", str(REPO_ROOT / "compose" / "docker-compose.indexing.nvcf-siglip2.yml")]
|
| 466 |
+
# Only the indexing stack ever talks to a storage backend directly β
|
| 467 |
+
# the runtime stack (verify/HF Space) serves an already-baked index and
|
| 468 |
+
# needs neither S3 nor a running hf-storage-api. See runtime_compose_files().
|
| 469 |
+
if self.storage_api_backend:
|
| 470 |
+
files += ["-f", str(REPO_ROOT / "compose" / "docker-compose.indexing.storage-api.yml")]
|
| 471 |
+
# Independent of storage backend (the new worker reads thumbnails
|
| 472 |
+
# through whichever backend is already configured), applied last by
|
| 473 |
+
# convention alongside the others above.
|
| 474 |
+
if self.vlm_metadata_enabled:
|
| 475 |
+
files += ["-f", str(REPO_ROOT / "compose" / "docker-compose.indexing.vlm-metadata.yml")]
|
| 476 |
+
return files
|
| 477 |
+
|
| 478 |
+
def runtime_compose_files(self) -> List[str]:
|
| 479 |
+
files = ["-f", str(REPO_ROOT / "compose" / "docker-compose.runtime.yml")]
|
| 480 |
+
if self.siglip2_nvcf:
|
| 481 |
+
files += ["-f", str(REPO_ROOT / "compose" / "docker-compose.runtime.nvcf-siglip2.yml")]
|
| 482 |
+
return files
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
BASE_PLUGINS = ("thumbnail_to_embedding", "asset_graph_generation")
|
| 486 |
+
"""The two plugins this pipeline always runs."""
|
| 487 |
+
|
| 488 |
+
# Every plugin key config/plugin.config.yaml lists β used to generate
|
| 489 |
+
# .build/plugin.config.yaml (see enumerate_assets.write_plugin_config_yaml).
|
| 490 |
+
# Keep in sync with that file's own keys.
|
| 491 |
+
ALL_PLUGIN_NAMES = (
|
| 492 |
+
"thumbnail_to_embedding",
|
| 493 |
+
"asset_graph_generation",
|
| 494 |
+
"thumbnail_generation",
|
| 495 |
+
"rendering_to_embedding",
|
| 496 |
+
"image_to_embedding",
|
| 497 |
+
"image_to_vision_metadata",
|
| 498 |
+
"rendering_to_vision_metadata",
|
| 499 |
+
"thumbnail_to_vision_metadata",
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def active_plugins(cfg: "Config") -> tuple:
|
| 504 |
+
"""The plugins this run actually enqueues β BASE_PLUGINS plus, opt-in,
|
| 505 |
+
`thumbnail_to_vision_metadata` (VLM_METADATA_ENABLED=true). Superseded
|
| 506 |
+
the old module-level `ACTIVE_PLUGINS` constant so this can vary per run
|
| 507 |
+
rather than being fixed at import time; every former caller of that
|
| 508 |
+
constant now calls this with its own `cfg` instead."""
|
| 509 |
+
plugins = list(BASE_PLUGINS)
|
| 510 |
+
if cfg.vlm_metadata_enabled:
|
| 511 |
+
plugins.append("thumbnail_to_vision_metadata")
|
| 512 |
+
return tuple(plugins)
|
pipeline/corpus.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Corpus definition: parsing, validation, and include/exclude matching.
|
| 2 |
+
|
| 3 |
+
The corpus JSON is the sole authority over what gets indexed. With the crawler
|
| 4 |
+
removed, nothing else filters and nothing else can widen the set, so the
|
| 5 |
+
matching rules here are deliberately dumb: literal `s3://bucket/key` prefixes
|
| 6 |
+
compared with `str.startswith`. No globs, no regex. Glob-vs-regex ambiguity is
|
| 7 |
+
exactly the kind of thing that silently indexes the wrong 200k assets.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import hashlib
|
| 13 |
+
import json
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Iterable, List, Sequence, Tuple, Union
|
| 17 |
+
|
| 18 |
+
from . import hf_addressing
|
| 19 |
+
|
| 20 |
+
DEFAULT_EXTENSIONS = (".usd", ".usda", ".usdc", ".usdz")
|
| 21 |
+
|
| 22 |
+
STORAGE_BACKENDS = ("s3", "storage_api")
|
| 23 |
+
DEFAULT_STORAGE_BACKEND = "s3"
|
| 24 |
+
|
| 25 |
+
# `hf-storage-api`'s catalog unit is an asset *folder*, identified by a marker
|
| 26 |
+
# file at its root (its `layout.marker`/`layout.manifest`, default
|
| 27 |
+
# "com.nvidia.simready.packaging.json" β the SimReady packaging convention).
|
| 28 |
+
# Once a folder is catalogued, every file beneath it is individually
|
| 29 |
+
# addressable (its own `resolve()` walks the whole subtree, not just the
|
| 30 |
+
# marker) β so this pipeline's existing file-level include/exclude/extension
|
| 31 |
+
# matching still selects exactly which files get indexed; `asset_marker` only
|
| 32 |
+
# tells the enumerator (and the catalog it generates) which ancestor folder of
|
| 33 |
+
# each matched file is "the asset" for hf-storage-api's access-boundary.
|
| 34 |
+
DEFAULT_ASSET_MARKER = "com.nvidia.simready.packaging.json"
|
| 35 |
+
|
| 36 |
+
# The revision an hf:// corpus entry with no /resolve/<sha>/ segment is taken
|
| 37 |
+
# to track β matches hf-storage-api's own RepoConfig.revision default.
|
| 38 |
+
DEFAULT_HF_REVISION = "main"
|
| 39 |
+
|
| 40 |
+
# Applied on top of the corpus, mirroring the upstream monitor's OV_EXCLUDE_PATHS
|
| 41 |
+
# default. Thumbnails are inputs to thumbnail_to_embedding, never assets.
|
| 42 |
+
ALWAYS_EXCLUDE_SUBSTRINGS = ("/.thumbs/", ".__omni_channel__")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class CorpusError(ValueError):
|
| 46 |
+
"""Raised for a corpus file that cannot be used as written."""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass(frozen=True)
|
| 50 |
+
class S3Prefix:
|
| 51 |
+
bucket: str
|
| 52 |
+
key: str
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def uri(self) -> str:
|
| 56 |
+
return f"s3://{self.bucket}/{self.key}"
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def parse(raw: str, whence: str) -> "S3Prefix":
|
| 60 |
+
if not isinstance(raw, str) or not raw.startswith("s3://"):
|
| 61 |
+
raise CorpusError(f"{whence}: {raw!r} is not an s3:// URL")
|
| 62 |
+
remainder = raw[len("s3://"):]
|
| 63 |
+
if "/" not in remainder:
|
| 64 |
+
# "s3://bucket" with no key would mean the whole bucket. Require an
|
| 65 |
+
# explicit trailing slash so that intent is visible in the file.
|
| 66 |
+
raise CorpusError(
|
| 67 |
+
f"{whence}: {raw!r} has no key. Write 's3://{remainder}/' if you "
|
| 68 |
+
f"really mean the entire bucket."
|
| 69 |
+
)
|
| 70 |
+
bucket, _, key = remainder.partition("/")
|
| 71 |
+
if not bucket:
|
| 72 |
+
raise CorpusError(f"{whence}: {raw!r} has an empty bucket name")
|
| 73 |
+
return S3Prefix(bucket=bucket, key=key)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass(frozen=True)
|
| 77 |
+
class HfPrefix:
|
| 78 |
+
"""An ``hf://`` prefix naming a HuggingFace repo (+ pinned revision) and an
|
| 79 |
+
optional path prefix within it β the ``storage_api`` backend's equivalent
|
| 80 |
+
of :class:`S3Prefix`'s bucket/key. See `pipeline/hf_addressing.py` for the
|
| 81 |
+
address grammar.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
host: str
|
| 85 |
+
repo_type: str
|
| 86 |
+
repo_id: str
|
| 87 |
+
revision: str
|
| 88 |
+
key: str # trailing "/" preserved for a folder, exactly like S3Prefix.key
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def repo_key(self) -> Tuple[str, str, str, str]:
|
| 92 |
+
"""Groups prefixes that share one repo+revision β the `storage_api`
|
| 93 |
+
equivalent of `S3Prefix.bucket`, used by `hf_repos`/
|
| 94 |
+
`include_prefixes_for_repo`."""
|
| 95 |
+
return (self.host, self.repo_type, self.repo_id, self.revision)
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def uri(self) -> str:
|
| 99 |
+
is_folder = self.key == "" or self.key.endswith("/")
|
| 100 |
+
return hf_addressing.build(
|
| 101 |
+
host=self.host, repo_type=self.repo_type, repo_id=self.repo_id,
|
| 102 |
+
revision=self.revision, path=self.key, is_folder=is_folder,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
@staticmethod
|
| 106 |
+
def parse(raw: str, whence: str) -> "HfPrefix":
|
| 107 |
+
if not isinstance(raw, str) or not raw.startswith("hf://"):
|
| 108 |
+
raise CorpusError(f"{whence}: {raw!r} is not an hf:// URL")
|
| 109 |
+
try:
|
| 110 |
+
addr = hf_addressing.parse(raw)
|
| 111 |
+
except hf_addressing.AddressError as exc:
|
| 112 |
+
raise CorpusError(f"{whence}: {raw!r} is not a valid hf:// address ({exc})") from exc
|
| 113 |
+
# A /resolve/<rev>/ segment is only ever valid as a pinned commit sha:
|
| 114 |
+
# hf-storage-api's own address minting omits it entirely for a moving
|
| 115 |
+
# ref (`AssetStore._emit_revision`), and its resolve path checks a
|
| 116 |
+
# present segment against the repo's *actual* resolved sha
|
| 117 |
+
# (`_serves_revision`) β so a literal `resolve/main/` never matches
|
| 118 |
+
# and the address 404s. Verified live. No segment at all means "the
|
| 119 |
+
# repo's configured revision", which this pipeline always takes to be
|
| 120 |
+
# `main` (moving) β see DEFAULT_HF_REVISION.
|
| 121 |
+
if addr.revision and not hf_addressing.looks_like_sha(addr.revision):
|
| 122 |
+
raise CorpusError(
|
| 123 |
+
f"{whence}: {raw!r} pins /resolve/{addr.revision}/, which is not a full "
|
| 124 |
+
f"commit sha. hf-storage-api never resolves a moving ref embedded this "
|
| 125 |
+
f"way (it always 404s) β either pin an actual sha, or drop the "
|
| 126 |
+
f"/resolve/{addr.revision}/ segment entirely to track '{DEFAULT_HF_REVISION}'."
|
| 127 |
+
)
|
| 128 |
+
revision = addr.revision or DEFAULT_HF_REVISION
|
| 129 |
+
key = addr.path
|
| 130 |
+
if addr.is_folder and key and not key.endswith("/"):
|
| 131 |
+
key += "/"
|
| 132 |
+
return HfPrefix(
|
| 133 |
+
host=addr.host, repo_type=addr.repo_type, repo_id=addr.repo_id,
|
| 134 |
+
revision=revision, key=key,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
Prefix = Union[S3Prefix, HfPrefix]
|
| 139 |
+
|
| 140 |
+
_PREFIX_PARSERS = {"s3": S3Prefix.parse, "storage_api": HfPrefix.parse}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@dataclass
|
| 144 |
+
class Corpus:
|
| 145 |
+
name: str
|
| 146 |
+
storage_backend: str
|
| 147 |
+
include: List[Prefix]
|
| 148 |
+
exclude: List[Prefix]
|
| 149 |
+
removed: List[str]
|
| 150 |
+
extensions: Tuple[str, ...]
|
| 151 |
+
sha256: str
|
| 152 |
+
source_path: Path
|
| 153 |
+
asset_marker: str = DEFAULT_ASSET_MARKER
|
| 154 |
+
# Both passed straight through to the generated hf-storage-api catalog's
|
| 155 |
+
# `layout:` block (pipeline/hf_catalog.py) β "" means omit the key and
|
| 156 |
+
# let hf-storage-api's own default apply. Needed for a dataset whose
|
| 157 |
+
# thumbnails/root-USD don't sit at hf-storage-api's default SimReady
|
| 158 |
+
# locations: verified live against nvidia/srp-staging-controlled, whose
|
| 159 |
+
# actual layout is `catalog_kind: wrapp` (root USD resolved from the
|
| 160 |
+
# `.wrapp` package's own top-level file, not the `{data_dir}/{asset_name}.usd`
|
| 161 |
+
# naming guess) with thumbnails at `.thumbs/` directly under the asset
|
| 162 |
+
# folder, not `{data_dir}/.thumbs`. Without these, hf-storage-api's
|
| 163 |
+
# GetMetadata silently computes a nonsense root_usd/thumbnail path and
|
| 164 |
+
# thumbnail_to_embedding "succeeds" with zero embeddings β no error,
|
| 165 |
+
# just 0% coverage.
|
| 166 |
+
catalog_kind: str = ""
|
| 167 |
+
thumbs_dir: str = ""
|
| 168 |
+
# `content_fallback` is hf-storage-api's own naming-convention template
|
| 169 |
+
# for the root USD when no `root_usd_index`/`.wrapp` sidecar is present
|
| 170 |
+
# (default `{data_dir}/{asset_name}.usd`) β overridable here for a
|
| 171 |
+
# dataset whose filenames don't match that guess. Verified live against
|
| 172 |
+
# nvidia/srp-staging-controlled's 2026-09 layout restructure: every one
|
| 173 |
+
# of 1488 assets' root USD is `{folder}/simready_usd/sm_{folder}_01.usd`,
|
| 174 |
+
# not `{folder}/simready_usd/{folder}.usd` β and that restructure also
|
| 175 |
+
# dropped the `.wrapp` sidecar `catalog_kind: wrapp` depends on entirely
|
| 176 |
+
# (folder() itself 404s without one, before content_fallback even runs),
|
| 177 |
+
# so this corpus now needs `catalog_kind: manifest` (the default) with
|
| 178 |
+
# this override, not `wrapp` + thumbs_dir as before.
|
| 179 |
+
content_fallback: str = ""
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def buckets(self) -> List[str]:
|
| 183 |
+
"""S3 only β see `hf_repos` for the `storage_api` equivalent."""
|
| 184 |
+
return sorted({p.bucket for p in self.include})
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def hf_repos(self) -> List[Tuple[str, str, str, str]]:
|
| 188 |
+
"""`storage_api` only β see `buckets` for the S3 equivalent. Each
|
| 189 |
+
tuple is `(host, repo_type, repo_id, revision)`, i.e. `HfPrefix.repo_key`."""
|
| 190 |
+
return sorted({p.repo_key for p in self.include})
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def removed_set(self) -> frozenset:
|
| 194 |
+
return frozenset(self.removed)
|
| 195 |
+
|
| 196 |
+
def include_prefixes_for(self, bucket: str) -> List[str]:
|
| 197 |
+
return [p.key for p in self.include if p.bucket == bucket]
|
| 198 |
+
|
| 199 |
+
def include_prefixes_for_repo(self, repo_key: Tuple[str, str, str, str]) -> List[str]:
|
| 200 |
+
return [p.key for p in self.include if p.repo_key == repo_key]
|
| 201 |
+
|
| 202 |
+
# ββ matching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 203 |
+
|
| 204 |
+
def is_excluded(self, uri: str) -> bool:
|
| 205 |
+
if any(s in uri for s in ALWAYS_EXCLUDE_SUBSTRINGS):
|
| 206 |
+
return True
|
| 207 |
+
return any(uri.startswith(p.uri) for p in self.exclude)
|
| 208 |
+
|
| 209 |
+
def is_included(self, uri: str) -> bool:
|
| 210 |
+
return any(uri.startswith(p.uri) for p in self.include)
|
| 211 |
+
|
| 212 |
+
def has_wanted_extension(self, uri: str) -> bool:
|
| 213 |
+
lowered = uri.lower()
|
| 214 |
+
return any(lowered.endswith(ext) for ext in self.extensions)
|
| 215 |
+
|
| 216 |
+
def accepts(self, uri: str) -> bool:
|
| 217 |
+
"""Exclude wins over include, matching upstream `exclude_items` order."""
|
| 218 |
+
if self.is_excluded(uri):
|
| 219 |
+
return False
|
| 220 |
+
if not self.is_included(uri):
|
| 221 |
+
return False
|
| 222 |
+
return self.has_wanted_extension(uri)
|
| 223 |
+
|
| 224 |
+
# ββ loading βββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½βββββ
|
| 225 |
+
|
| 226 |
+
@classmethod
|
| 227 |
+
def load(cls, path: Path) -> "Corpus":
|
| 228 |
+
if not path.exists():
|
| 229 |
+
raise CorpusError(
|
| 230 |
+
f"corpus file not found: {path}. Copy config/corpus.example.json "
|
| 231 |
+
f"and edit it."
|
| 232 |
+
)
|
| 233 |
+
raw_bytes = path.read_bytes()
|
| 234 |
+
try:
|
| 235 |
+
doc = json.loads(raw_bytes)
|
| 236 |
+
except json.JSONDecodeError as exc:
|
| 237 |
+
raise CorpusError(f"{path}: invalid JSON β {exc}") from exc
|
| 238 |
+
return cls.from_dict(doc, sha256=hashlib.sha256(raw_bytes).hexdigest(), source_path=path)
|
| 239 |
+
|
| 240 |
+
@classmethod
|
| 241 |
+
def from_dict(cls, doc: dict, sha256: str = "", source_path: Path = Path(".")) -> "Corpus":
|
| 242 |
+
if not isinstance(doc, dict):
|
| 243 |
+
raise CorpusError("corpus must be a JSON object")
|
| 244 |
+
|
| 245 |
+
version = doc.get("version", 1)
|
| 246 |
+
if version != 1:
|
| 247 |
+
raise CorpusError(f"unsupported corpus version {version!r}; expected 1")
|
| 248 |
+
|
| 249 |
+
name = doc.get("name") or ""
|
| 250 |
+
if not name:
|
| 251 |
+
raise CorpusError("corpus 'name' is required")
|
| 252 |
+
|
| 253 |
+
storage_backend = doc.get("storage_backend") or DEFAULT_STORAGE_BACKEND
|
| 254 |
+
if storage_backend not in STORAGE_BACKENDS:
|
| 255 |
+
raise CorpusError(
|
| 256 |
+
f"storage_backend: {storage_backend!r} is not one of {STORAGE_BACKENDS}"
|
| 257 |
+
)
|
| 258 |
+
parse_prefix = _PREFIX_PARSERS[storage_backend]
|
| 259 |
+
|
| 260 |
+
raw_include = doc.get("include") or []
|
| 261 |
+
if not raw_include:
|
| 262 |
+
# An empty include is almost always an accident, and reading it as
|
| 263 |
+
# "everything" would index an entire bucket by omission.
|
| 264 |
+
raise CorpusError(
|
| 265 |
+
"corpus 'include' is empty. Refusing to interpret that as "
|
| 266 |
+
"'index everything' β list the prefixes explicitly."
|
| 267 |
+
)
|
| 268 |
+
include = [parse_prefix(r, "include") for r in raw_include]
|
| 269 |
+
exclude = [parse_prefix(r, "exclude") for r in (doc.get("exclude") or [])]
|
| 270 |
+
# Exact asset URLs (not prefixes) known to have been indexed before and
|
| 271 |
+
# since deleted from the source bucket. Matched by exact equality
|
| 272 |
+
# against a baseline run's asset list, never by prefix β see `clear`
|
| 273 |
+
# (restore/delta indexing). Validated the same way as include/exclude
|
| 274 |
+
# so a malformed URL is caught here, not at Neo4j/OpenSearch call time.
|
| 275 |
+
removed = [parse_prefix(r, "removed").uri for r in (doc.get("removed") or [])]
|
| 276 |
+
|
| 277 |
+
extensions = tuple(
|
| 278 |
+
e.lower() if e.startswith(".") else f".{e.lower()}"
|
| 279 |
+
for e in (doc.get("extensions") or DEFAULT_EXTENSIONS)
|
| 280 |
+
)
|
| 281 |
+
asset_marker = doc.get("asset_marker") or DEFAULT_ASSET_MARKER
|
| 282 |
+
if storage_backend != "storage_api" and "asset_marker" in doc:
|
| 283 |
+
raise CorpusError("asset_marker is only meaningful for storage_backend: storage_api")
|
| 284 |
+
catalog_kind = doc.get("catalog_kind") or ""
|
| 285 |
+
if storage_backend != "storage_api" and "catalog_kind" in doc:
|
| 286 |
+
raise CorpusError("catalog_kind is only meaningful for storage_backend: storage_api")
|
| 287 |
+
thumbs_dir = doc.get("thumbs_dir") or ""
|
| 288 |
+
if storage_backend != "storage_api" and "thumbs_dir" in doc:
|
| 289 |
+
raise CorpusError("thumbs_dir is only meaningful for storage_backend: storage_api")
|
| 290 |
+
content_fallback = doc.get("content_fallback") or ""
|
| 291 |
+
if storage_backend != "storage_api" and "content_fallback" in doc:
|
| 292 |
+
raise CorpusError("content_fallback is only meaningful for storage_backend: storage_api")
|
| 293 |
+
|
| 294 |
+
corpus = cls(
|
| 295 |
+
name=name,
|
| 296 |
+
storage_backend=storage_backend,
|
| 297 |
+
include=include,
|
| 298 |
+
exclude=exclude,
|
| 299 |
+
removed=removed,
|
| 300 |
+
extensions=extensions,
|
| 301 |
+
sha256=sha256,
|
| 302 |
+
source_path=source_path,
|
| 303 |
+
asset_marker=asset_marker,
|
| 304 |
+
catalog_kind=catalog_kind,
|
| 305 |
+
thumbs_dir=thumbs_dir,
|
| 306 |
+
content_fallback=content_fallback,
|
| 307 |
+
)
|
| 308 |
+
corpus.validate()
|
| 309 |
+
return corpus
|
| 310 |
+
|
| 311 |
+
def validate(self) -> None:
|
| 312 |
+
"""Structural checks that catch the common typos."""
|
| 313 |
+
seen = set()
|
| 314 |
+
for prefix in self.include:
|
| 315 |
+
if prefix.uri in seen:
|
| 316 |
+
raise CorpusError(f"include: duplicate entry {prefix.uri!r}")
|
| 317 |
+
seen.add(prefix.uri)
|
| 318 |
+
|
| 319 |
+
# A prefix nested inside another include is redundant, not wrong β but
|
| 320 |
+
# it usually signals a misunderstanding of the trailing-slash rule.
|
| 321 |
+
for a in self.include:
|
| 322 |
+
for b in self.include:
|
| 323 |
+
if a is not b and a.uri != b.uri and a.uri.startswith(b.uri):
|
| 324 |
+
raise CorpusError(
|
| 325 |
+
f"include: {a.uri!r} is already covered by {b.uri!r}; "
|
| 326 |
+
f"remove the redundant entry"
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# A stray exclude β one that sits under no include β is nearly always a
|
| 330 |
+
# typo, and it silently does nothing.
|
| 331 |
+
for prefix in self.exclude:
|
| 332 |
+
if not any(prefix.uri.startswith(inc.uri) for inc in self.include):
|
| 333 |
+
raise CorpusError(
|
| 334 |
+
f"exclude: {prefix.uri!r} is not under any include prefix, so "
|
| 335 |
+
f"it would have no effect. Fix the path or drop the entry."
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
removed_seen = set()
|
| 339 |
+
for uri in self.removed:
|
| 340 |
+
if uri in removed_seen:
|
| 341 |
+
raise CorpusError(f"removed: duplicate entry {uri!r}")
|
| 342 |
+
removed_seen.add(uri)
|
| 343 |
+
|
| 344 |
+
@property
|
| 345 |
+
def run_id_suffix(self) -> str:
|
| 346 |
+
return self.sha256[:7]
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def summarize(corpus: Corpus) -> str:
|
| 350 |
+
if corpus.storage_backend == "storage_api":
|
| 351 |
+
sources_label = "hf repos"
|
| 352 |
+
sources = ", ".join(f"{repo_id}@{revision}" for _host, _rtype, repo_id, revision in corpus.hf_repos)
|
| 353 |
+
else:
|
| 354 |
+
sources_label = "buckets"
|
| 355 |
+
sources = ", ".join(corpus.buckets)
|
| 356 |
+
lines = [
|
| 357 |
+
f"corpus: {corpus.name}",
|
| 358 |
+
f"storage_backend: {corpus.storage_backend}",
|
| 359 |
+
f"sha256: {corpus.sha256}",
|
| 360 |
+
f"{sources_label}:{' ' * (12 - len(sources_label))} {sources}",
|
| 361 |
+
f"include: {len(corpus.include)} prefixes",
|
| 362 |
+
f"exclude: {len(corpus.exclude)} prefixes",
|
| 363 |
+
f"removed: {len(corpus.removed)} assets",
|
| 364 |
+
f"extensions: {', '.join(corpus.extensions)}",
|
| 365 |
+
]
|
| 366 |
+
return "\n".join(lines)
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def iter_uris(objects: Iterable[Tuple[str, str]]) -> Sequence[str]:
|
| 370 |
+
return [f"s3://{bucket}/{key}" for bucket, key in objects]
|
pipeline/enumerate_assets.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 0 β expand the corpus into an exact asset list.
|
| 2 |
+
|
| 3 |
+
With the crawler removed, nothing lists the bucket at index time, so this stage
|
| 4 |
+
owns enumeration. Its output is the pipeline's ground truth: `N = len(assets)`
|
| 5 |
+
is the expected document count, known before any indexing starts, and every
|
| 6 |
+
later stage asserts against it.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import subprocess
|
| 14 |
+
from dataclasses import asdict, dataclass
|
| 15 |
+
from datetime import datetime, timezone
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
| 18 |
+
|
| 19 |
+
import boto3
|
| 20 |
+
import yaml
|
| 21 |
+
from botocore import UNSIGNED
|
| 22 |
+
from botocore.client import Config as BotoConfig
|
| 23 |
+
|
| 24 |
+
from . import hf_addressing, hf_catalog
|
| 25 |
+
from .config import ALL_PLUGIN_NAMES, BUILD_DIR, REPO_ROOT, Config, active_plugins
|
| 26 |
+
from .corpus import Corpus, summarize
|
| 27 |
+
|
| 28 |
+
SETTINGS_TEMPLATE = """\
|
| 29 |
+
# Generated by `run_pipeline.py enumerate` β do not edit by hand.
|
| 30 |
+
# Mounted into info-endpoint and omni-writer as /app/settings.yaml.
|
| 31 |
+
backends:
|
| 32 |
+
default_backend:
|
| 33 |
+
search_backend_config:
|
| 34 |
+
es_host: opensearch
|
| 35 |
+
es_port: 9200
|
| 36 |
+
ES_NAME: {es_name}
|
| 37 |
+
backend_type: os_index
|
| 38 |
+
DS_EMBEDDING_DIM: {embedding_dim}
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class Asset:
|
| 44 |
+
url: str
|
| 45 |
+
size: int
|
| 46 |
+
etag: str
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _s3_client(cfg: Config):
|
| 50 |
+
"""An S3 client that signs only when credentials were supplied.
|
| 51 |
+
|
| 52 |
+
Mirrors the upstream storage client's behaviour: a public bucket must be
|
| 53 |
+
read anonymously, and passing empty-string credentials would force signed
|
| 54 |
+
requests and fail with NoCredentialsError.
|
| 55 |
+
"""
|
| 56 |
+
key = cfg.get("S3_STORAGE_AWS_ACCESS_KEY_ID").strip()
|
| 57 |
+
secret = cfg.get("S3_STORAGE_AWS_SECRET_ACCESS_KEY").strip()
|
| 58 |
+
endpoint = cfg.get("S3_STORAGE_AWS_ENDPOINT_URL").strip() or None
|
| 59 |
+
region = cfg.get("S3_STORAGE_REGION_NAME", "us-west-2")
|
| 60 |
+
|
| 61 |
+
if key and secret:
|
| 62 |
+
return boto3.client(
|
| 63 |
+
"s3",
|
| 64 |
+
aws_access_key_id=key,
|
| 65 |
+
aws_secret_access_key=secret,
|
| 66 |
+
endpoint_url=endpoint,
|
| 67 |
+
region_name=region,
|
| 68 |
+
)
|
| 69 |
+
return boto3.client(
|
| 70 |
+
"s3",
|
| 71 |
+
endpoint_url=endpoint,
|
| 72 |
+
region_name=region,
|
| 73 |
+
config=BotoConfig(signature_version=UNSIGNED),
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def list_prefix(client, bucket: str, prefix: str) -> Iterator[Dict]:
|
| 78 |
+
paginator = client.get_paginator("list_objects_v2")
|
| 79 |
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
| 80 |
+
for obj in page.get("Contents", []):
|
| 81 |
+
yield obj
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def enumerate_corpus(corpus: Corpus, cfg: Config) -> List[Asset]:
|
| 85 |
+
client = _s3_client(cfg)
|
| 86 |
+
seen: Dict[str, Asset] = {}
|
| 87 |
+
listed = 0
|
| 88 |
+
|
| 89 |
+
for bucket in corpus.buckets:
|
| 90 |
+
for prefix in corpus.include_prefixes_for(bucket):
|
| 91 |
+
print(f" listing s3://{bucket}/{prefix} ...", flush=True)
|
| 92 |
+
for obj in list_prefix(client, bucket, prefix):
|
| 93 |
+
listed += 1
|
| 94 |
+
url = f"s3://{bucket}/{obj['Key']}"
|
| 95 |
+
if not corpus.accepts(url):
|
| 96 |
+
continue
|
| 97 |
+
# Sibling include prefixes cannot overlap (validate() rejects
|
| 98 |
+
# nesting), but dedupe anyway so a future rule change cannot
|
| 99 |
+
# produce duplicate submissions.
|
| 100 |
+
seen.setdefault(
|
| 101 |
+
url,
|
| 102 |
+
Asset(url=url, size=int(obj.get("Size", 0)), etag=str(obj.get("ETag", "")).strip('"')),
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# `removed` always wins, even over a matching `include` prefix: it names
|
| 106 |
+
# an asset the operator knows is gone, and re-submitting it here would
|
| 107 |
+
# fight the `clear` step (restore/delta indexing) that is about to drop it
|
| 108 |
+
# from the index.
|
| 109 |
+
removed = corpus.removed_set
|
| 110 |
+
if removed:
|
| 111 |
+
dropped = [url for url in seen if url in removed]
|
| 112 |
+
for url in dropped:
|
| 113 |
+
del seen[url]
|
| 114 |
+
if dropped:
|
| 115 |
+
print(f" dropped {len(dropped)} 'removed' asset(s) from submission")
|
| 116 |
+
|
| 117 |
+
print(f" listed {listed} objects, accepted {len(seen)}")
|
| 118 |
+
return [seen[k] for k in sorted(seen)]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _hf_api():
|
| 122 |
+
"""A `huggingface_hub.HfApi`, authenticated from `$HF_TOKEN` when set.
|
| 123 |
+
|
| 124 |
+
Read from the environment only, never `pipeline.env` β same policy as
|
| 125 |
+
every other credential this pipeline touches (S3 keys, the NVCF token).
|
| 126 |
+
`None` is fine for a public dataset; `list_repo_tree` then calls
|
| 127 |
+
anonymously.
|
| 128 |
+
"""
|
| 129 |
+
from huggingface_hub import HfApi
|
| 130 |
+
|
| 131 |
+
return HfApi(token=os.environ.get("HF_TOKEN") or None)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def enumerate_corpus_hf(corpus: Corpus, cfg: Config) -> Tuple[List[Asset], Dict[hf_catalog.RepoKey, List[str]]]:
|
| 135 |
+
"""The `storage_api` backend's counterpart to `enumerate_corpus` β lists
|
| 136 |
+
each corpus HF repo's full file tree, applies the same include/exclude/
|
| 137 |
+
extension matching `Corpus.accepts` already uses for S3, and additionally
|
| 138 |
+
derives the `hf-storage-api` catalog's asset-folder set from the matched
|
| 139 |
+
files (see `pipeline/hf_catalog.py`) β one repo-tree read serves both.
|
| 140 |
+
"""
|
| 141 |
+
from huggingface_hub import RepoFile
|
| 142 |
+
|
| 143 |
+
api = _hf_api()
|
| 144 |
+
seen: Dict[str, Asset] = {}
|
| 145 |
+
matched_paths_by_repo: Dict[hf_catalog.RepoKey, List[str]] = {}
|
| 146 |
+
all_paths_by_repo: Dict[hf_catalog.RepoKey, Set[str]] = {}
|
| 147 |
+
listed = 0
|
| 148 |
+
|
| 149 |
+
for repo_key in corpus.hf_repos:
|
| 150 |
+
host, repo_type, repo_id, revision = repo_key
|
| 151 |
+
print(f" listing hf://{host}/{repo_type}s/{repo_id}/resolve/{revision}/ ...", flush=True)
|
| 152 |
+
file_entries = {
|
| 153 |
+
entry.path: entry
|
| 154 |
+
for entry in api.list_repo_tree(repo_id, recursive=True, revision=revision, repo_type=repo_type)
|
| 155 |
+
if isinstance(entry, RepoFile)
|
| 156 |
+
}
|
| 157 |
+
all_paths_by_repo[repo_key] = set(file_entries)
|
| 158 |
+
|
| 159 |
+
matched: List[str] = []
|
| 160 |
+
for path, entry in file_entries.items():
|
| 161 |
+
listed += 1
|
| 162 |
+
url = hf_addressing.build(host=host, repo_type=repo_type, repo_id=repo_id, revision=revision, path=path)
|
| 163 |
+
if not corpus.accepts(url):
|
| 164 |
+
continue
|
| 165 |
+
matched.append(path)
|
| 166 |
+
seen.setdefault(url, Asset(url=url, size=int(entry.size or 0), etag=entry.blob_id or ""))
|
| 167 |
+
matched_paths_by_repo[repo_key] = matched
|
| 168 |
+
|
| 169 |
+
# See enumerate_corpus()'s own comment: `removed` always wins.
|
| 170 |
+
removed = corpus.removed_set
|
| 171 |
+
if removed:
|
| 172 |
+
dropped = [url for url in seen if url in removed]
|
| 173 |
+
for url in dropped:
|
| 174 |
+
del seen[url]
|
| 175 |
+
if dropped:
|
| 176 |
+
print(f" dropped {len(dropped)} 'removed' asset(s) from submission")
|
| 177 |
+
|
| 178 |
+
print(f" listed {listed} objects, accepted {len(seen)}")
|
| 179 |
+
asset_folders = hf_catalog.asset_folders_by_repo(corpus, matched_paths_by_repo, all_paths_by_repo)
|
| 180 |
+
return [seen[k] for k in sorted(seen)], asset_folders
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _usdsearch_git_sha(cfg: Config) -> str:
|
| 184 |
+
repo = (REPO_ROOT / cfg.get("USDSEARCH_REPO", "../usdsearch")).resolve()
|
| 185 |
+
try:
|
| 186 |
+
out = subprocess.run(
|
| 187 |
+
["git", "-C", str(repo), "rev-parse", "--short", "HEAD"],
|
| 188 |
+
capture_output=True, text=True, timeout=10,
|
| 189 |
+
)
|
| 190 |
+
return out.stdout.strip() if out.returncode == 0 else "unknown"
|
| 191 |
+
except (OSError, subprocess.SubprocessError):
|
| 192 |
+
return "unknown"
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def build_run_id(cfg: Config, corpus: Corpus) -> str:
|
| 196 |
+
explicit = cfg.get("RUN_ID").strip()
|
| 197 |
+
if explicit:
|
| 198 |
+
return explicit
|
| 199 |
+
return f"{datetime.now(timezone.utc):%Y%m%d}-{corpus.run_id_suffix}"
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def write_settings_yaml(cfg: Config, path: Path) -> None:
|
| 203 |
+
path.write_text(
|
| 204 |
+
SETTINGS_TEMPLATE.format(
|
| 205 |
+
es_name=cfg.es_name,
|
| 206 |
+
embedding_dim=cfg.get_int("DS_EMBEDDING_DIM", 1536),
|
| 207 |
+
)
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
PLUGIN_CONFIG_SOURCE = REPO_ROOT / "config" / "plugin.config.yaml"
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def write_plugin_config_yaml(cfg: Config, path: Path) -> None:
|
| 215 |
+
"""Generate `.build/plugin.config.yaml`, the file actually mounted into
|
| 216 |
+
info-endpoint (docker-compose.indexing.yml) β a copy of
|
| 217 |
+
config/plugin.config.yaml with `thumbnail_to_vision_metadata.active`
|
| 218 |
+
overridden to match `VLM_METADATA_ENABLED`, the one plugin this pipeline
|
| 219 |
+
toggles per run rather than pinning statically. Every other key is left
|
| 220 |
+
exactly as authored in the source file (same "explicit list, no silent
|
| 221 |
+
all-active fallback" policy that file's own header describes).
|
| 222 |
+
"""
|
| 223 |
+
plugin_config: Dict[str, Dict] = yaml.safe_load(PLUGIN_CONFIG_SOURCE.read_text()) or {}
|
| 224 |
+
enabled = set(active_plugins(cfg))
|
| 225 |
+
for name in ALL_PLUGIN_NAMES:
|
| 226 |
+
plugin_config.setdefault(name, {})["active"] = name in enabled
|
| 227 |
+
path.write_text(
|
| 228 |
+
"# Generated by `run_pipeline.py enumerate` from config/plugin.config.yaml "
|
| 229 |
+
"β do not edit by hand.\n" + yaml.safe_dump(plugin_config, sort_keys=False)
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def run(cfg: Config, dry_run: bool = False) -> int:
|
| 234 |
+
corpus = Corpus.load(cfg.corpus_file)
|
| 235 |
+
print(summarize(corpus))
|
| 236 |
+
print()
|
| 237 |
+
|
| 238 |
+
if corpus.storage_backend != cfg.storage_backend:
|
| 239 |
+
print(
|
| 240 |
+
f"\nERROR: corpus.json declares storage_backend={corpus.storage_backend!r} but "
|
| 241 |
+
f"STORAGE_BACKEND_TYPE={cfg.storage_backend!r} (pipeline.env / environment). "
|
| 242 |
+
f"They must agree β indexing against the wrong backend silently matches zero assets."
|
| 243 |
+
)
|
| 244 |
+
return 1
|
| 245 |
+
|
| 246 |
+
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
| 247 |
+
|
| 248 |
+
asset_folders: Dict[hf_catalog.RepoKey, List[str]] = {}
|
| 249 |
+
if corpus.storage_backend == "storage_api":
|
| 250 |
+
print("Enumerating HuggingFace ...")
|
| 251 |
+
assets, asset_folders = enumerate_corpus_hf(corpus, cfg)
|
| 252 |
+
else:
|
| 253 |
+
print("Enumerating S3 ...")
|
| 254 |
+
assets = enumerate_corpus(corpus, cfg)
|
| 255 |
+
if not assets:
|
| 256 |
+
print(
|
| 257 |
+
"\nERROR: the corpus matched zero objects. Check the include prefixes "
|
| 258 |
+
"and the extension list against what is actually in the bucket/repo."
|
| 259 |
+
)
|
| 260 |
+
return 1
|
| 261 |
+
|
| 262 |
+
run_id = build_run_id(cfg, corpus)
|
| 263 |
+
|
| 264 |
+
assets_path = BUILD_DIR / "assets.jsonl"
|
| 265 |
+
if not dry_run:
|
| 266 |
+
with assets_path.open("w") as fh:
|
| 267 |
+
for asset in assets:
|
| 268 |
+
fh.write(json.dumps(asdict(asset)) + "\n")
|
| 269 |
+
|
| 270 |
+
manifest = {
|
| 271 |
+
"run_id": run_id,
|
| 272 |
+
"corpus_name": corpus.name,
|
| 273 |
+
"corpus_sha256": corpus.sha256,
|
| 274 |
+
"storage_backend": corpus.storage_backend,
|
| 275 |
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
| 276 |
+
"usdsearch_git_sha": _usdsearch_git_sha(cfg),
|
| 277 |
+
"es_name": cfg.es_name,
|
| 278 |
+
"main_index": cfg.main_index,
|
| 279 |
+
"image_cache_index": cfg.image_cache_index,
|
| 280 |
+
"buckets": corpus.buckets if corpus.storage_backend == "s3" else [],
|
| 281 |
+
"hf_repos": [list(k) for k in corpus.hf_repos] if corpus.storage_backend == "storage_api" else [],
|
| 282 |
+
"active_plugins": list(active_plugins(cfg)),
|
| 283 |
+
"asset_count": len(assets),
|
| 284 |
+
"removed": list(corpus.removed),
|
| 285 |
+
}
|
| 286 |
+
if not dry_run:
|
| 287 |
+
if corpus.storage_backend == "storage_api":
|
| 288 |
+
catalog = hf_catalog.build_catalog(corpus, asset_folders)
|
| 289 |
+
(BUILD_DIR / "hf-storage-api-catalog.yaml").write_text(hf_catalog.dump_catalog(catalog))
|
| 290 |
+
(BUILD_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
| 291 |
+
write_settings_yaml(cfg, BUILD_DIR / "settings.yaml")
|
| 292 |
+
write_plugin_config_yaml(cfg, BUILD_DIR / "plugin.config.yaml")
|
| 293 |
+
# The exact input, copied for reproducibility and uploaded in stage 3.
|
| 294 |
+
(BUILD_DIR / "corpus.json").write_text(cfg.corpus_file.read_text())
|
| 295 |
+
|
| 296 |
+
print()
|
| 297 |
+
print(f"run_id: {run_id}")
|
| 298 |
+
print(f"assets: {len(assets)}")
|
| 299 |
+
print(f"main index: {cfg.main_index}")
|
| 300 |
+
print(f"image index: {cfg.image_cache_index}")
|
| 301 |
+
if dry_run:
|
| 302 |
+
print("\n(dry run β nothing written)")
|
| 303 |
+
else:
|
| 304 |
+
print(f"\nwrote {assets_path.relative_to(REPO_ROOT)} and manifest.json")
|
| 305 |
+
return 0
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def load_assets(path: Optional[Path] = None) -> List[Asset]:
|
| 309 |
+
path = path or (BUILD_DIR / "assets.jsonl")
|
| 310 |
+
if not path.exists():
|
| 311 |
+
raise SystemExit(
|
| 312 |
+
f"{path} not found β run `python -m pipeline.run_pipeline enumerate` first."
|
| 313 |
+
)
|
| 314 |
+
out = []
|
| 315 |
+
for line in path.read_text().splitlines():
|
| 316 |
+
if line.strip():
|
| 317 |
+
out.append(Asset(**json.loads(line)))
|
| 318 |
+
return out
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def load_manifest() -> Dict:
|
| 322 |
+
path = BUILD_DIR / "manifest.json"
|
| 323 |
+
if not path.exists():
|
| 324 |
+
raise SystemExit(
|
| 325 |
+
f"{path} not found β run `python -m pipeline.run_pipeline enumerate` first."
|
| 326 |
+
)
|
| 327 |
+
return json.loads(path.read_text())
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def save_manifest(manifest: Dict) -> None:
|
| 331 |
+
(BUILD_DIR / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
pipeline/export.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 2 β export the OpenSearch indexes and the Neo4j database.
|
| 2 |
+
|
| 3 |
+
The OpenSearch export deliberately produces the exact file layout that
|
| 4 |
+
upstream's `services/deepsearch_api/loader.sh` consumes:
|
| 5 |
+
|
| 6 |
+
<index>.settings.json <index>.mapping.json <index>.data.json
|
| 7 |
+
|
| 8 |
+
so the artifacts are restorable by the existing Helm `data_loader_job` and
|
| 9 |
+
container-test compose, not only by our own bake step. That compatibility is the
|
| 10 |
+
acceptance test for this stage (see the plan, Β§9 step 5).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import gzip
|
| 16 |
+
import hashlib
|
| 17 |
+
import json
|
| 18 |
+
import shutil
|
| 19 |
+
import subprocess
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Dict, List
|
| 22 |
+
|
| 23 |
+
import httpx
|
| 24 |
+
|
| 25 |
+
from .config import BUILD_DIR, REPO_ROOT, Config
|
| 26 |
+
from .enumerate_assets import load_manifest, save_manifest
|
| 27 |
+
|
| 28 |
+
ARTIFACTS = BUILD_DIR / "artifacts"
|
| 29 |
+
OS_ARTIFACTS = ARTIFACTS / "opensearch"
|
| 30 |
+
NEO4J_ARTIFACTS = ARTIFACTS / "neo4j"
|
| 31 |
+
|
| 32 |
+
COMPOSE_PROJECT = "simready-indexing"
|
| 33 |
+
ELASTICDUMP_IMAGE = "elasticdump/elasticsearch-dump:latest"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _run(cmd: List[str], **kwargs) -> subprocess.CompletedProcess:
|
| 37 |
+
print(" $ " + " ".join(cmd[:6]) + (" ..." if len(cmd) > 6 else ""))
|
| 38 |
+
return subprocess.run(cmd, check=True, **kwargs)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _uid_gid() -> str:
|
| 42 |
+
import os
|
| 43 |
+
return f"{os.getuid()}:{os.getgid()}"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def discover_indexes(cfg: Config) -> List[str]:
|
| 47 |
+
"""Indexes belonging to this ES_NAME, read from the cluster.
|
| 48 |
+
|
| 49 |
+
Discovery rather than a hardcoded pair: the pointcloud cache exists in some
|
| 50 |
+
corpora, and a rename that missed one place would otherwise export an empty
|
| 51 |
+
index without complaint.
|
| 52 |
+
"""
|
| 53 |
+
response = httpx.get(f"{cfg.opensearch_url}/_cat/indices?format=json", timeout=30.0)
|
| 54 |
+
response.raise_for_status()
|
| 55 |
+
names = sorted(
|
| 56 |
+
row["index"] for row in response.json()
|
| 57 |
+
if row.get("index", "").startswith(f"{cfg.es_name}-")
|
| 58 |
+
)
|
| 59 |
+
if cfg.main_index not in names:
|
| 60 |
+
raise SystemExit(
|
| 61 |
+
f"main index {cfg.main_index} not found in the cluster. Found: {names or 'none'}. "
|
| 62 |
+
f"Check ES_NAME against what the writers actually created."
|
| 63 |
+
)
|
| 64 |
+
return names
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _network_name() -> str:
|
| 68 |
+
return f"{COMPOSE_PROJECT}_default"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def elasticdump(index: str, dump_type: str, out_path: Path) -> None:
|
| 72 |
+
_run([
|
| 73 |
+
"docker", "run", "--rm",
|
| 74 |
+
"--network", _network_name(),
|
| 75 |
+
"--user", _uid_gid(),
|
| 76 |
+
"-v", f"{OS_ARTIFACTS}:/out",
|
| 77 |
+
ELASTICDUMP_IMAGE,
|
| 78 |
+
f"--input=http://opensearch:9200/{index}",
|
| 79 |
+
f"--output=/out/{out_path.name}",
|
| 80 |
+
f"--type={dump_type}",
|
| 81 |
+
"--limit=1000",
|
| 82 |
+
"--overwrite",
|
| 83 |
+
])
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def sha256_of(path: Path) -> str:
|
| 87 |
+
digest = hashlib.sha256()
|
| 88 |
+
with path.open("rb") as fh:
|
| 89 |
+
for block in iter(lambda: fh.read(1024 * 1024), b""):
|
| 90 |
+
digest.update(block)
|
| 91 |
+
return digest.hexdigest()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def gzip_file(path: Path) -> Path:
|
| 95 |
+
dest = path.with_suffix(path.suffix + ".gz")
|
| 96 |
+
with path.open("rb") as src, gzip.open(dest, "wb") as out:
|
| 97 |
+
shutil.copyfileobj(src, out)
|
| 98 |
+
return dest
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def export_opensearch(cfg: Config, manifest: Dict) -> Dict:
|
| 102 |
+
OS_ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
indexes = discover_indexes(cfg)
|
| 104 |
+
print(f" indexes: {', '.join(indexes)}")
|
| 105 |
+
|
| 106 |
+
expected = manifest.get("asset_count", 0)
|
| 107 |
+
main_count = int(
|
| 108 |
+
httpx.get(f"{cfg.opensearch_url}/{cfg.main_index}/_count", timeout=30.0).json()["count"]
|
| 109 |
+
)
|
| 110 |
+
if expected and main_count != expected:
|
| 111 |
+
raise SystemExit(
|
| 112 |
+
f"pre-flight: {cfg.main_index} holds {main_count} docs but the run expects "
|
| 113 |
+
f"{expected}. Refusing to export a partial index β re-run stage 1."
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Flush so everything written is on disk and countable.
|
| 117 |
+
httpx.post(f"{cfg.opensearch_url}/_flush", timeout=120.0)
|
| 118 |
+
|
| 119 |
+
exported: Dict[str, Dict] = {}
|
| 120 |
+
for index in indexes:
|
| 121 |
+
print(f" exporting {index} ...")
|
| 122 |
+
files = {}
|
| 123 |
+
for dump_type, suffix in (
|
| 124 |
+
("settings", "settings.json"),
|
| 125 |
+
("mapping", "mapping.json"),
|
| 126 |
+
("data", "data.json"),
|
| 127 |
+
):
|
| 128 |
+
plain = OS_ARTIFACTS / f"{index}.{suffix}"
|
| 129 |
+
elasticdump(index, dump_type, plain)
|
| 130 |
+
if not plain.exists() or plain.stat().st_size == 0:
|
| 131 |
+
raise SystemExit(f"elasticdump produced no {dump_type} for {index}")
|
| 132 |
+
compressed = gzip_file(plain)
|
| 133 |
+
files[dump_type] = {
|
| 134 |
+
"file": compressed.name,
|
| 135 |
+
"bytes": compressed.stat().st_size,
|
| 136 |
+
"sha256": sha256_of(compressed),
|
| 137 |
+
}
|
| 138 |
+
count = int(
|
| 139 |
+
httpx.get(f"{cfg.opensearch_url}/{index}/_count", timeout=30.0).json()["count"]
|
| 140 |
+
)
|
| 141 |
+
data_lines = sum(
|
| 142 |
+
1 for _ in (OS_ARTIFACTS / f"{index}.data.json").open() if _.strip()
|
| 143 |
+
)
|
| 144 |
+
if data_lines != count:
|
| 145 |
+
raise SystemExit(
|
| 146 |
+
f"{index}: dumped {data_lines} rows but the index reports {count} docs"
|
| 147 |
+
)
|
| 148 |
+
exported[index] = {"doc_count": count, "files": files}
|
| 149 |
+
print(f" {count} docs")
|
| 150 |
+
|
| 151 |
+
return exported
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def verify_dump(cfg: Config, dump: Path) -> None:
|
| 155 |
+
"""Load the dump into a throwaway data dir to prove it is loadable.
|
| 156 |
+
|
| 157 |
+
Same principle as `bake.py::verify_baked` β check the artifact actually
|
| 158 |
+
works from a fresh process, not just that a file got written. A dump can
|
| 159 |
+
fail this while still existing on disk at a plausible size β see the
|
| 160 |
+
stop-ordering comment in `export_neo4j` β and `bake` finding out is
|
| 161 |
+
hours and several stages too late.
|
| 162 |
+
|
| 163 |
+
Runs as the host uid, unlike the real dump/load steps: those touch the
|
| 164 |
+
named `neo4j-data` volume, already owned by the in-container uid 7474,
|
| 165 |
+
so they must run as root. This scratch dir is fresh and ours, so running
|
| 166 |
+
as root here would just leave root-owned files behind that the host
|
| 167 |
+
user can't clean up afterwards.
|
| 168 |
+
"""
|
| 169 |
+
import tempfile
|
| 170 |
+
|
| 171 |
+
with tempfile.TemporaryDirectory() as scratch:
|
| 172 |
+
result = subprocess.run(
|
| 173 |
+
[
|
| 174 |
+
"docker", "run", "--rm",
|
| 175 |
+
"--user", _uid_gid(),
|
| 176 |
+
"-v", f"{dump.parent}:/dumps:ro",
|
| 177 |
+
"-v", f"{scratch}:/data",
|
| 178 |
+
f"neo4j:{cfg.get('NEO4J_VERSION', '5.23.0-community')}",
|
| 179 |
+
"neo4j-admin", "database", "load", "neo4j",
|
| 180 |
+
"--from-path=/dumps", "--overwrite-destination=true",
|
| 181 |
+
],
|
| 182 |
+
capture_output=True, text=True, check=False,
|
| 183 |
+
)
|
| 184 |
+
if result.returncode != 0:
|
| 185 |
+
raise SystemExit(
|
| 186 |
+
f"neo4j dump failed to verify-load β likely corrupted by AGS "
|
| 187 |
+
f"and neo4j being stopped concurrently (see the stop-ordering "
|
| 188 |
+
f"comment in export_neo4j):\n{result.stdout}\n{result.stderr}"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def export_neo4j(cfg: Config) -> Dict:
|
| 193 |
+
NEO4J_ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
|
| 195 |
+
# Community edition cannot stop the default database from Cypher (no
|
| 196 |
+
# manual `db.checkpoint()` either β that procedure doesn't exist outside
|
| 197 |
+
# Enterprise), so the dump has to be taken offline via a plain container
|
| 198 |
+
# stop, relying on Neo4j's own graceful-shutdown checkpoint.
|
| 199 |
+
#
|
| 200 |
+
# AGS *must* be given its own `stop` call, fully awaited, before neo4j's
|
| 201 |
+
# `stop` call starts β not one `stop asset-graph-service neo4j` command
|
| 202 |
+
# naming both. `docker compose stop` sends SIGTERM to every named service
|
| 203 |
+
# concurrently regardless of argument order, so a single combined call
|
| 204 |
+
# can (and, reproduced directly, does) stop AGS and neo4j at the same
|
| 205 |
+
# moment β if AGS is mid-write when neo4j's shutdown checkpoint starts,
|
| 206 |
+
# the resulting dump is corrupted (truncated trailing zstd frame,
|
| 207 |
+
# `Not a valid Neo4j archive`) while still landing on disk at a
|
| 208 |
+
# plausible size, silently, caught only by a `neo4j-admin database load`
|
| 209 |
+
# deep in a later `bake`. Two sequential calls make the ordering real
|
| 210 |
+
# instead of advisory; `verify_dump` below is the safety net regardless.
|
| 211 |
+
# `docker compose stop`'s default grace period (~10s) is nowhere near
|
| 212 |
+
# enough for Neo4j's own graceful-shutdown checkpoint on a real corpus β
|
| 213 |
+
# verified live: a ~69k-node/81k-relationship graph took ~103s to stop
|
| 214 |
+
# cleanly. Short of that, compose SIGKILLs it, which produces exactly the
|
| 215 |
+
# "plausible size, silently corrupted" dump the comment above warns
|
| 216 |
+
# about, just from a different cause (timeout, not concurrent-stop
|
| 217 |
+
# ordering) β caught by `verify_dump` below either way, but avoidable.
|
| 218 |
+
print(" stopping asset-graph-service ...")
|
| 219 |
+
_run(["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 220 |
+
"stop", "--timeout", "300", "asset-graph-service"], env=cfg.compose_env())
|
| 221 |
+
print(" stopping neo4j ...")
|
| 222 |
+
_run(["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 223 |
+
"stop", "--timeout", "300", "neo4j"], env=cfg.compose_env())
|
| 224 |
+
|
| 225 |
+
print(" dumping neo4j ...")
|
| 226 |
+
# Runs as root (the image default), NOT as the host uid: the data volume is
|
| 227 |
+
# owned by the in-container neo4j user (uid 7474), and neo4j-admin refuses
|
| 228 |
+
# with "You do not have permission to dump the database" under --user.
|
| 229 |
+
# The dump is chowned to the host uid afterwards so the artifact is readable
|
| 230 |
+
# and removable outside the container.
|
| 231 |
+
#
|
| 232 |
+
# rm -f the previous dump ourselves, rather than passing
|
| 233 |
+
# --overwrite-destination=true: that flag has a confirmed upstream bug
|
| 234 |
+
# (neo4j/neo4j#13604) where overwriting a pre-existing *larger* archive
|
| 235 |
+
# doesn't fully truncate it, leaving old bytes at the tail of the new one
|
| 236 |
+
# β `Not a valid Neo4j archive` / `ZstdIOException: Unknown frame
|
| 237 |
+
# descriptor` on load, reproduced here on every retry (each one dumping
|
| 238 |
+
# over the previous attempt's own leftover file). The Neo4j maintainers'
|
| 239 |
+
# own workaround (same issue, 2025-10-27) is to delete the destination
|
| 240 |
+
# first and never pass --overwrite-destination β a genuinely fresh
|
| 241 |
+
# destination file can't hit this bug.
|
| 242 |
+
_run([
|
| 243 |
+
"docker", "run", "--rm",
|
| 244 |
+
"--entrypoint", "bash",
|
| 245 |
+
"-v", f"{COMPOSE_PROJECT}_neo4j-data:/data",
|
| 246 |
+
"-v", f"{NEO4J_ARTIFACTS}:/dumps",
|
| 247 |
+
f"neo4j:{cfg.get('NEO4J_VERSION', '5.23.0-community')}",
|
| 248 |
+
"-c",
|
| 249 |
+
"set -e; rm -f /dumps/neo4j.dump; neo4j-admin database dump neo4j --to-path=/dumps; "
|
| 250 |
+
f"chown -R {_uid_gid()} /dumps",
|
| 251 |
+
])
|
| 252 |
+
|
| 253 |
+
dump = NEO4J_ARTIFACTS / "neo4j.dump"
|
| 254 |
+
if not dump.exists():
|
| 255 |
+
raise SystemExit(f"neo4j-admin produced no dump at {dump}")
|
| 256 |
+
|
| 257 |
+
print(" verifying the dump loads cleanly ...")
|
| 258 |
+
verify_dump(cfg, dump)
|
| 259 |
+
|
| 260 |
+
compressed = gzip_file(dump)
|
| 261 |
+
|
| 262 |
+
print(" restarting neo4j and asset-graph-service ...")
|
| 263 |
+
_run(["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 264 |
+
"start", "neo4j", "asset-graph-service"], env=cfg.compose_env())
|
| 265 |
+
|
| 266 |
+
return {
|
| 267 |
+
"file": compressed.name,
|
| 268 |
+
"bytes": compressed.stat().st_size,
|
| 269 |
+
"sha256": sha256_of(compressed),
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def run(cfg: Config) -> int:
|
| 274 |
+
manifest = load_manifest()
|
| 275 |
+
ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
| 276 |
+
|
| 277 |
+
print("Exporting OpenSearch ...")
|
| 278 |
+
manifest["opensearch_artifacts"] = export_opensearch(cfg, manifest)
|
| 279 |
+
|
| 280 |
+
print("\nExporting Neo4j ...")
|
| 281 |
+
manifest["neo4j_artifact"] = export_neo4j(cfg)
|
| 282 |
+
|
| 283 |
+
# Copy the inputs alongside the data so an artifact set is self-describing.
|
| 284 |
+
for name in ("corpus.json", "index-report.json", "assets.jsonl"):
|
| 285 |
+
src = BUILD_DIR / name
|
| 286 |
+
if src.exists():
|
| 287 |
+
shutil.copy2(src, ARTIFACTS / name)
|
| 288 |
+
|
| 289 |
+
save_manifest(manifest)
|
| 290 |
+
shutil.copy2(BUILD_DIR / "manifest.json", ARTIFACTS / "manifest.json")
|
| 291 |
+
|
| 292 |
+
print(f"\nartifacts in {ARTIFACTS.relative_to(REPO_ROOT)}:")
|
| 293 |
+
for path in sorted(ARTIFACTS.rglob("*")):
|
| 294 |
+
if path.is_file():
|
| 295 |
+
print(f" {path.relative_to(ARTIFACTS)} ({path.stat().st_size:,} bytes)")
|
| 296 |
+
return 0
|
pipeline/hf_addressing.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parse/build ``hf://`` asset addresses in the same namespace `hf-storage-api`
|
| 2 |
+
(the gRPC Omniverse Storage API server backing the ``storage_api`` backend,
|
| 3 |
+
https://gitlab-master.nvidia.com/omniverse/gen-ai/hf-storage-api-service) hands
|
| 4 |
+
out and resolves.
|
| 5 |
+
|
| 6 |
+
This is a small, faithful port of that service's own
|
| 7 |
+
``src/hf_storage_api/addressing.py`` (``build``/``parse``) β not an import,
|
| 8 |
+
since this repo has no dependency on that service's package. Keeping the
|
| 9 |
+
grammar here in sync with upstream matters: this pipeline's enumerator (stage
|
| 10 |
+
0) must mint the *exact* addresses the running `hf-storage-api` instance will
|
| 11 |
+
resolve at indexing time, or `/process/asset` submissions 404 against a
|
| 12 |
+
storage backend that considers them out of catalog.
|
| 13 |
+
|
| 14 |
+
Addresses look like ``hf://<host>/[datasets/]<owner>/<name>[/resolve/<rev>]/<path>``
|
| 15 |
+
β folders end in ``/``, assets never do. `hf://` (not `https://`) is used
|
| 16 |
+
throughout this repo's corpus files and compose wiring specifically because
|
| 17 |
+
Kit's `omni.client` has a built-in HTTPS provider that would otherwise
|
| 18 |
+
ambiguously claim a plain `https://huggingface.co/...` address ahead of the
|
| 19 |
+
registered Storage API one β see the cluster's `hf-storage-api.yaml`
|
| 20 |
+
(`config.uriScheme: hf`).
|
| 21 |
+
|
| 22 |
+
**The `/resolve/<rev>/` segment must be a pinned commit sha, or absent
|
| 23 |
+
entirely β never a moving ref like `main`.** `hf-storage-api`'s own address
|
| 24 |
+
minting (`AssetStore._emit_revision`) only ever emits that segment for a
|
| 25 |
+
repo whose configured revision looks like a sha; for a moving ref it emits
|
| 26 |
+
none, specifically so an asset's address survives a re-crawl. Its resolve
|
| 27 |
+
path (`AssetStore.stat`/`_serves_revision`) enforces the same rule on the
|
| 28 |
+
way in: an address carrying a literal `resolve/main/` is checked against the
|
| 29 |
+
repo's *actual* resolved sha and β since `"main" != "<sha>"` β never
|
| 30 |
+
matches, so `Stat` always answers `NOT_FOUND`. Verified live against a real
|
| 31 |
+
deployment: every submission against a `resolve/main/`-shaped address
|
| 32 |
+
404'd, and the exact same paths resolved cleanly with that segment removed.
|
| 33 |
+
`looks_like_sha()`/`build()`/`parse()` below enforce this at the address
|
| 34 |
+
grammar level rather than leaving it to each call site to remember.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
from dataclasses import dataclass
|
| 40 |
+
from urllib.parse import unquote, urlsplit
|
| 41 |
+
|
| 42 |
+
REPO_TYPE_PREFIX = {"dataset": "datasets", "space": "spaces", "model": ""}
|
| 43 |
+
|
| 44 |
+
SCHEME = "hf"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def looks_like_sha(revision: str) -> bool:
|
| 48 |
+
"""Mirrors `hf-storage-api`'s own `store._looks_like_sha` exactly β a
|
| 49 |
+
full 40-hex-char git commit sha, case-insensitive."""
|
| 50 |
+
return len(revision) == 40 and all(c in "0123456789abcdef" for c in revision.lower())
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class AddressError(ValueError):
|
| 54 |
+
"""The address is not a well-formed hf:// asset address."""
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass(frozen=True)
|
| 58 |
+
class Address:
|
| 59 |
+
host: str
|
| 60 |
+
repo_type: str
|
| 61 |
+
repo_id: str # "<owner>/<name>"
|
| 62 |
+
path: str # repo-relative, no leading slash; "" for the repo root
|
| 63 |
+
is_folder: bool
|
| 64 |
+
revision: str | None = None
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def parse(address: str) -> Address:
|
| 68 |
+
"""Parse ``hf://host/[datasets/]<owner>/<name>[/resolve/<rev>][/<path>]``."""
|
| 69 |
+
if not address:
|
| 70 |
+
raise AddressError("empty address")
|
| 71 |
+
|
| 72 |
+
split = urlsplit(address)
|
| 73 |
+
if split.scheme != SCHEME:
|
| 74 |
+
raise AddressError(f"expected scheme {SCHEME!r}, got {split.scheme!r} in {address!r}")
|
| 75 |
+
if not split.netloc:
|
| 76 |
+
raise AddressError(f"missing host in {address!r}")
|
| 77 |
+
|
| 78 |
+
is_folder = split.path.endswith("/")
|
| 79 |
+
segments = [unquote(s) for s in split.path.split("/") if s]
|
| 80 |
+
|
| 81 |
+
repo_type = "model"
|
| 82 |
+
for rtype, prefix in REPO_TYPE_PREFIX.items():
|
| 83 |
+
if prefix and segments[:1] == [prefix]:
|
| 84 |
+
repo_type, segments = rtype, segments[1:]
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
if len(segments) < 2:
|
| 88 |
+
raise AddressError(f"address does not name a repo (<owner>/<name>): {address!r}")
|
| 89 |
+
|
| 90 |
+
repo_id = f"{segments[0]}/{segments[1]}"
|
| 91 |
+
rest = segments[2:]
|
| 92 |
+
|
| 93 |
+
revision: str | None = None
|
| 94 |
+
if len(rest) >= 2 and rest[0] == "resolve":
|
| 95 |
+
revision, rest = rest[1], rest[2:]
|
| 96 |
+
|
| 97 |
+
return Address(
|
| 98 |
+
host=split.netloc,
|
| 99 |
+
repo_type=repo_type,
|
| 100 |
+
repo_id=repo_id,
|
| 101 |
+
path="/".join(rest),
|
| 102 |
+
is_folder=is_folder,
|
| 103 |
+
revision=revision,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def build(
|
| 108 |
+
*,
|
| 109 |
+
host: str,
|
| 110 |
+
repo_type: str,
|
| 111 |
+
repo_id: str,
|
| 112 |
+
revision: str,
|
| 113 |
+
path: str = "",
|
| 114 |
+
is_folder: bool = False,
|
| 115 |
+
) -> str:
|
| 116 |
+
"""Emit a canonical ``hf://`` address. Folders end in ``/``; assets never do.
|
| 117 |
+
|
| 118 |
+
``revision`` is the *repo's configured* revision (e.g. a corpus's
|
| 119 |
+
`HfPrefix.revision` or `hf-storage-api`'s `RepoConfig.revision`) β the
|
| 120 |
+
`/resolve/<rev>/` segment is emitted only when it looks like a pinned
|
| 121 |
+
commit sha (`looks_like_sha`), mirroring `AssetStore._emit_revision`
|
| 122 |
+
exactly. A moving ref (e.g. ``"main"``) is silently omitted from the
|
| 123 |
+
address rather than embedded literally β embedding it produces an
|
| 124 |
+
address `hf-storage-api` itself can never resolve (see the module
|
| 125 |
+
docstring).
|
| 126 |
+
"""
|
| 127 |
+
prefix = REPO_TYPE_PREFIX.get(repo_type, "")
|
| 128 |
+
resolve_segment = f"resolve/{revision}" if looks_like_sha(revision) else ""
|
| 129 |
+
parts = [p for p in (prefix, repo_id, resolve_segment, path.strip("/")) if p]
|
| 130 |
+
out = f"{SCHEME}://{host}/" + "/".join(parts)
|
| 131 |
+
if is_folder and not out.endswith("/"):
|
| 132 |
+
out += "/"
|
| 133 |
+
return out
|
pipeline/hf_catalog.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Derive the `hf-storage-api` catalog (stage 0 output) from this run's matched
|
| 2 |
+
HF asset files.
|
| 3 |
+
|
| 4 |
+
`hf-storage-api`'s catalog names asset *folders*, not individual files β a
|
| 5 |
+
folder is "the asset" once it (or an ancestor) carries `corpus.asset_marker`,
|
| 6 |
+
and every file beneath a catalogued folder is then individually addressable
|
| 7 |
+
(see `pipeline/corpus.py`'s `DEFAULT_ASSET_MARKER` docstring). This module
|
| 8 |
+
turns the flat list of matched file paths `enumerate_assets.py` already
|
| 9 |
+
computed (via the same include/exclude/extension rules used for the S3
|
| 10 |
+
backend) into the folder set that catalog needs, by walking each matched
|
| 11 |
+
file's ancestors for the nearest one containing the marker β using the same
|
| 12 |
+
full repo-tree listing already fetched for matching, so this costs no extra
|
| 13 |
+
HuggingFace calls.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import yaml
|
| 19 |
+
|
| 20 |
+
from pathlib import PurePosixPath
|
| 21 |
+
from typing import Dict, Iterable, List, Set, Tuple
|
| 22 |
+
|
| 23 |
+
from .corpus import Corpus
|
| 24 |
+
|
| 25 |
+
# (host, repo_type, repo_id, revision) -> set of every path present in that
|
| 26 |
+
# repo tree, used to test marker-file membership without extra HF calls.
|
| 27 |
+
RepoKey = Tuple[str, str, str, str]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _marker_path(folder: str, marker: str) -> str:
|
| 31 |
+
return f"{folder}/{marker}" if folder else marker
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _ancestors(folder: str) -> Iterable[str]:
|
| 35 |
+
"""`folder` itself, then each ancestor up to (and including) the repo root `""`."""
|
| 36 |
+
current = folder
|
| 37 |
+
while True:
|
| 38 |
+
yield current
|
| 39 |
+
if not current:
|
| 40 |
+
return
|
| 41 |
+
current = PurePosixPath(current).parent.as_posix()
|
| 42 |
+
if current == ".":
|
| 43 |
+
current = ""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def find_asset_folder(path: str, marker: str, all_paths: Set[str]) -> str:
|
| 47 |
+
"""The nearest ancestor of `path`'s directory that contains `marker`,
|
| 48 |
+
walking upward to the repo root. Falls back to `path`'s immediate parent
|
| 49 |
+
directory if no ancestor carries the marker β a corpus can include files
|
| 50 |
+
from a dataset with no marker file at all, and the folder is still a
|
| 51 |
+
well-defined catalog entry (just not marker-discoverable on its own).
|
| 52 |
+
"""
|
| 53 |
+
parent = PurePosixPath(path).parent.as_posix()
|
| 54 |
+
parent = "" if parent == "." else parent
|
| 55 |
+
for ancestor in _ancestors(parent):
|
| 56 |
+
if _marker_path(ancestor, marker) in all_paths:
|
| 57 |
+
return ancestor
|
| 58 |
+
return parent
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def asset_folders_by_repo(
|
| 62 |
+
corpus: Corpus,
|
| 63 |
+
matched_paths_by_repo: Dict[RepoKey, List[str]],
|
| 64 |
+
all_paths_by_repo: Dict[RepoKey, Set[str]],
|
| 65 |
+
) -> Dict[RepoKey, List[str]]:
|
| 66 |
+
"""One sorted, deduplicated folder list per repo, derived from this run's
|
| 67 |
+
matched files."""
|
| 68 |
+
out: Dict[RepoKey, List[str]] = {}
|
| 69 |
+
for repo_key, matched_paths in matched_paths_by_repo.items():
|
| 70 |
+
all_paths = all_paths_by_repo.get(repo_key, set())
|
| 71 |
+
folders = {
|
| 72 |
+
find_asset_folder(path, corpus.asset_marker, all_paths) for path in matched_paths
|
| 73 |
+
}
|
| 74 |
+
out[repo_key] = sorted(folders)
|
| 75 |
+
return out
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def build_catalog(corpus: Corpus, asset_folders: Dict[RepoKey, List[str]]) -> dict:
|
| 79 |
+
"""Render the `hf-storage-api` YAML catalog dict β see
|
| 80 |
+
`catalog.example.yaml` in
|
| 81 |
+
https://gitlab-master.nvidia.com/omniverse/gen-ai/hf-storage-api-service.
|
| 82 |
+
"""
|
| 83 |
+
repos = []
|
| 84 |
+
for host, repo_type, repo_id, revision in corpus.hf_repos:
|
| 85 |
+
folders = asset_folders.get((host, repo_type, repo_id, revision), [])
|
| 86 |
+
repos.append({
|
| 87 |
+
"repo_id": repo_id,
|
| 88 |
+
"repo_type": repo_type,
|
| 89 |
+
"revision": revision,
|
| 90 |
+
"assets": folders,
|
| 91 |
+
})
|
| 92 |
+
layout = {"marker": corpus.asset_marker, "manifest": corpus.asset_marker}
|
| 93 |
+
if corpus.catalog_kind:
|
| 94 |
+
layout["catalog_kind"] = corpus.catalog_kind
|
| 95 |
+
if corpus.thumbs_dir:
|
| 96 |
+
layout["thumbs_dir"] = corpus.thumbs_dir
|
| 97 |
+
if corpus.content_fallback:
|
| 98 |
+
layout["content_fallback"] = corpus.content_fallback
|
| 99 |
+
return {
|
| 100 |
+
# All corpus entries share one host (validated by HfPrefix parsing);
|
| 101 |
+
# hf-storage-api's `endpoint` is catalog-wide.
|
| 102 |
+
"endpoint": f"https://{corpus.hf_repos[0][0]}" if corpus.hf_repos else "https://huggingface.co",
|
| 103 |
+
"repos": repos,
|
| 104 |
+
"layout": layout,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def dump_catalog(catalog: dict) -> str:
|
| 109 |
+
return yaml.safe_dump(catalog, sort_keys=False)
|
pipeline/hf_space.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 6 β deploy the HF Space source (not a prebuilt image) to a Hugging
|
| 2 |
+
Face Space.
|
| 3 |
+
|
| 4 |
+
Wired into CI as a manual-dispatch-only step (never the nightly schedule β
|
| 5 |
+
see nvcf-pipeline.yml); run locally with
|
| 6 |
+
`python -m pipeline.run_pipeline deploy-space --env-file ...` once `bake`
|
| 7 |
+
has verified the hf_space Dockerfile builds for this corpus (see
|
| 8 |
+
docker/Dockerfile.hf-space's header for why this Space builds from source
|
| 9 |
+
with only public base images, rather than pulling a prebuilt image of ours:
|
| 10 |
+
Hugging Face's own infra builds a Space's Dockerfile with no credentials
|
| 11 |
+
available for a private registry pull). Gates on `bake`'s own
|
| 12 |
+
hf_space_smoke_passed (set by bake.py's verify_baked), not the separate
|
| 13 |
+
`verify` stage's smoke_passed β `verify` brings up the *entire* runtime
|
| 14 |
+
stack, including asset-graph-service and other components the hf_space
|
| 15 |
+
image doesn't even include, so requiring it to fully pass would block a
|
| 16 |
+
deploy over failures this image can't possibly be affected by.
|
| 17 |
+
|
| 18 |
+
Requires a Hugging Face token with write access to the target Space's org,
|
| 19 |
+
resolved the usual huggingface_hub way (`hf auth login`, or `HF_TOKEN` in
|
| 20 |
+
the environment) β never read from pipeline.env, same policy as every other
|
| 21 |
+
credential this pipeline touches.
|
| 22 |
+
|
| 23 |
+
The index itself is *not* redeployed by this script β pipeline/hf_space_seed.py
|
| 24 |
+
restores it from S3 at container startup (see that module's docstring), so a
|
| 25 |
+
Space already picks up a newly `push`ed run on its own next restart, with no
|
| 26 |
+
redeploy at all. What this script's `# deploy: <run_id> (<corpus>)` marker
|
| 27 |
+
line (prepended to the Dockerfile) is for is forcing that restart when you
|
| 28 |
+
want it now rather than waiting: Hugging Face only rebuilds/restarts a Space
|
| 29 |
+
on a new commit, never by noticing that S3 data changed β without the
|
| 30 |
+
marker, re-running this stage when the pipeline code hasn't otherwise
|
| 31 |
+
changed would produce byte-identical file content and silently commit
|
| 32 |
+
nothing. The previous container keeps serving traffic until the new build
|
| 33 |
+
is ready (or forever, if the new build fails) β see docs/hub/spaces-overview's
|
| 34 |
+
"restarting" note.
|
| 35 |
+
|
| 36 |
+
RESTORE_RUN_ID (pin the Space to one fixed run instead of always-latest) is
|
| 37 |
+
deliberately left unmanaged here β set it directly via the Space's Settings
|
| 38 |
+
-> Variables in the HF UI if needed, so a redeploy never silently clobbers
|
| 39 |
+
an operator's manual pin.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
from __future__ import annotations
|
| 43 |
+
|
| 44 |
+
import shutil
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
from typing import Dict
|
| 47 |
+
|
| 48 |
+
from huggingface_hub import HfApi, SpaceHardware
|
| 49 |
+
|
| 50 |
+
from .artifact_store import artifact_env
|
| 51 |
+
from .bake import DOCKER_DIR, hf_space_build_args, hf_space_gpu_build_args, image_tags
|
| 52 |
+
from .config import BUILD_DIR, HF_SPACE_GPU_HARDWARE_DEFAULT, REPO_ROOT, Config
|
| 53 |
+
from .enumerate_assets import load_manifest
|
| 54 |
+
|
| 55 |
+
STAGE_DIR = BUILD_DIR / "hf_space_deploy"
|
| 56 |
+
STAGE_DIR_GPU = BUILD_DIR / "hf_space_deploy_gpu"
|
| 57 |
+
|
| 58 |
+
README_TEMPLATE = """---
|
| 59 |
+
title: {title}
|
| 60 |
+
emoji: \U0001f50e
|
| 61 |
+
colorFrom: green
|
| 62 |
+
colorTo: blue
|
| 63 |
+
sdk: docker
|
| 64 |
+
app_port: 7860
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
# {title}
|
| 68 |
+
|
| 69 |
+
SimReady Central USD Search β single-container demo (OpenSearch + Redis +
|
| 70 |
+
deepsearch-api, SigLIP2 embeddings via NVCF). Built by Hugging Face from
|
| 71 |
+
this repo's docker/Dockerfile.hf-space; the index itself is pulled from the
|
| 72 |
+
artifact S3 bucket at container startup, not baked in. Deployed by
|
| 73 |
+
github.com/NVIDIA-dev/simready-central-indexing-internal's
|
| 74 |
+
`python -m pipeline.run_pipeline deploy-space`.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
# GPU variant β see docker/Dockerfile.hf-space-gpu and
|
| 78 |
+
# docs/plans/siglip2-embedded-hf-space.md. `hardware:` in the front matter
|
| 79 |
+
# only sets the *default* for a brand-new Space; run() below also calls
|
| 80 |
+
# request_space_hardware explicitly so an existing Space actually gets
|
| 81 |
+
# switched to it on redeploy, since HF only reads this front-matter field
|
| 82 |
+
# at Space-creation time.
|
| 83 |
+
README_TEMPLATE_GPU = """---
|
| 84 |
+
title: {title}
|
| 85 |
+
emoji: \U0001f50e
|
| 86 |
+
colorFrom: green
|
| 87 |
+
colorTo: blue
|
| 88 |
+
sdk: docker
|
| 89 |
+
app_port: 7860
|
| 90 |
+
hardware: {hardware}
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
# {title}
|
| 94 |
+
|
| 95 |
+
SimReady Central USD Search β single-container demo (OpenSearch + Neo4j +
|
| 96 |
+
Redis + deepsearch-api + asset-graph-service, SigLIP2 embeddings via a local
|
| 97 |
+
siglip2-triton process β see docker/Dockerfile.hf-space-gpu). Runs on a GPU
|
| 98 |
+
hardware tier ({hardware}), unlike the free-tier
|
| 99 |
+
docker/Dockerfile.hf-space variant, which routes SigLIP2 to NVCF instead.
|
| 100 |
+
The index itself is pulled from the artifact S3 bucket at container
|
| 101 |
+
startup, not baked in. Deployed by
|
| 102 |
+
github.com/NVIDIA-dev/simready-central-indexing-internal's
|
| 103 |
+
`python -m pipeline.run_pipeline deploy-space-gpu`.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def stage_repo(dest: Path, manifest: Dict, readme: str, dockerfile_name: str = "Dockerfile.hf-space") -> None:
|
| 108 |
+
if dest.exists():
|
| 109 |
+
shutil.rmtree(dest)
|
| 110 |
+
dest.mkdir(parents=True)
|
| 111 |
+
|
| 112 |
+
marker = f"# deploy: {manifest['run_id']} ({manifest['corpus_name']})\n"
|
| 113 |
+
dockerfile = (DOCKER_DIR / dockerfile_name).read_text()
|
| 114 |
+
(dest / "Dockerfile").write_text(marker + dockerfile)
|
| 115 |
+
(dest / "README.md").write_text(readme)
|
| 116 |
+
|
| 117 |
+
# docker/hf-space/siglip2-model/ (vendored via scripts/extract_siglip2_model.sh)
|
| 118 |
+
# is only referenced by Dockerfile.hf-space-gpu's own COPY instructions β
|
| 119 |
+
# excluded entirely for the NVCF/cpu-basic variant. Even for the GPU
|
| 120 |
+
# variant, that Dockerfile only COPYs the two ONNX encoders'
|
| 121 |
+
# config.pbtxt (tiny) β NOT their ~3.6 GB weight files under each
|
| 122 |
+
# encoder's own 1/ subdirectory, which are restored from S3 at container
|
| 123 |
+
# startup instead (pipeline/hf_space_seed.py --component=siglip2-model β
|
| 124 |
+
# see that Dockerfile's own comment for why: HF Space git repos cap
|
| 125 |
+
# storage at 1 GB by default, and a real deploy-space-gpu attempt 403'd
|
| 126 |
+
# with "Repository storage limit reached" until *both* of these
|
| 127 |
+
# exclusions existed β the first fix here only handled the NVCF variant,
|
| 128 |
+
# verified live to still be insufficient for the GPU one on its own).
|
| 129 |
+
def _ignore_siglip2_weights(dir_path: str, names: List[str]) -> List[str]:
|
| 130 |
+
if Path(dir_path).name in ("siglip2_vision_encoder_onnx", "siglip2_text_encoder_onnx") and "1" in names:
|
| 131 |
+
return ["1"]
|
| 132 |
+
return []
|
| 133 |
+
|
| 134 |
+
if dockerfile_name == "Dockerfile.hf-space-gpu":
|
| 135 |
+
hf_space_ignore = _ignore_siglip2_weights
|
| 136 |
+
else:
|
| 137 |
+
hf_space_ignore = shutil.ignore_patterns("siglip2-model")
|
| 138 |
+
shutil.copytree(DOCKER_DIR / "hf-space", dest / "docker" / "hf-space", ignore=hf_space_ignore)
|
| 139 |
+
shutil.copytree(
|
| 140 |
+
REPO_ROOT / "pipeline", dest / "pipeline",
|
| 141 |
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
| 142 |
+
)
|
| 143 |
+
# The Dockerfile COPYs this in directly now (see its own comment) β
|
| 144 |
+
# must ship alongside it or HF's build fails with a missing path.
|
| 145 |
+
# A dist built by simready-central's slim/scripts/build-explorer-public.sh
|
| 146 |
+
# carries services/explorer/dist/build-info.json ({product, version,
|
| 147 |
+
# bundle_id, built_at, flags}) β the bundle's provenance; the upstream
|
| 148 |
+
# USD Search Explorer build has no such file.
|
| 149 |
+
shutil.copytree(REPO_ROOT / "services" / "explorer" / "dist", dest / "services" / "explorer" / "dist")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def run(cfg: Config) -> int:
|
| 153 |
+
manifest = load_manifest()
|
| 154 |
+
if not manifest.get("hf_space_smoke_passed"):
|
| 155 |
+
print(
|
| 156 |
+
"The hf_space image's own smoke test has not passed for this "
|
| 157 |
+
"run. Run `bake` (its verify_baked step sets "
|
| 158 |
+
"hf_space_smoke_passed) before deploying β an unverified build "
|
| 159 |
+
"shouldn't go live."
|
| 160 |
+
)
|
| 161 |
+
return 1
|
| 162 |
+
if "hf_space" not in image_tags(cfg, manifest):
|
| 163 |
+
print("No hf_space image for this run (SIGLIP2_BACKEND != nvcf) β nothing to deploy.")
|
| 164 |
+
return 1
|
| 165 |
+
|
| 166 |
+
repo_id = cfg.require("HF_SPACE_REPO_ID") # e.g. "nvidia/simready-central"
|
| 167 |
+
title = cfg.get("HF_SPACE_TITLE", repo_id.split("/", 1)[-1])
|
| 168 |
+
siglip2_token = cfg.siglip2_nvcf_env()["SIGLIP2_TRITON_SERVER_AUTH_TOKEN"]
|
| 169 |
+
aws = artifact_env() # raises if the artifact-bucket keys aren't set
|
| 170 |
+
|
| 171 |
+
print(f"staging Space source at {STAGE_DIR} ...")
|
| 172 |
+
stage_repo(STAGE_DIR, manifest, README_TEMPLATE.format(title=title))
|
| 173 |
+
|
| 174 |
+
api = HfApi()
|
| 175 |
+
print(f"creating/reusing Space {repo_id} ...")
|
| 176 |
+
api.create_repo(repo_id=repo_id, repo_type="space", space_sdk="docker", exist_ok=True)
|
| 177 |
+
|
| 178 |
+
print("setting build-arg variables ...")
|
| 179 |
+
# Hugging Face passes any Space "Variable" whose name matches a
|
| 180 |
+
# Dockerfile ARG through as a --build-arg on its own build (see
|
| 181 |
+
# docs/hub/spaces-sdks-docker) β this is the *only* way these values
|
| 182 |
+
# reach HF's build, since nothing here invokes `docker build` directly
|
| 183 |
+
# against the committed Dockerfile. hf_space_build_args() is the same
|
| 184 |
+
# dict bake.py's own local/CI build passes via --build-arg, so the two
|
| 185 |
+
# can't silently drift apart.
|
| 186 |
+
for key, value in hf_space_build_args(cfg, manifest).items():
|
| 187 |
+
api.add_space_variable(repo_id, key, value)
|
| 188 |
+
|
| 189 |
+
print("setting runtime secrets ...")
|
| 190 |
+
# Names must match what pipeline/artifact_store.py's artifact_env() and
|
| 191 |
+
# config.py's siglip2_nvcf_env() read from the environment β Hugging
|
| 192 |
+
# Face injects Space secrets as plain runtime env vars, so no separate
|
| 193 |
+
# "buildtime" naming/mounting is needed (see hf_space_seed.py's
|
| 194 |
+
# docstring for why this pipeline restores at runtime rather than build
|
| 195 |
+
# time in the first place).
|
| 196 |
+
api.add_space_secret(repo_id, "SIMREADYCENTRAL_INDEX_STORE_AWS_ACCESS_KEY_ID", aws["AWS_ACCESS_KEY_ID"])
|
| 197 |
+
api.add_space_secret(repo_id, "SIMREADYCENTRAL_INDEX_STORE_AWS_SECRET_ACCESS_KEY", aws["AWS_SECRET_ACCESS_KEY"])
|
| 198 |
+
api.add_space_secret(repo_id, "SIGLIP2_TRITON_SERVER_AUTH_TOKEN", siglip2_token)
|
| 199 |
+
|
| 200 |
+
# LLM parsing / VLM verification: off by default (see
|
| 201 |
+
# docker/Dockerfile.hf-space's baked USDSEARCH_LLM_PARSING_ENABLED/
|
| 202 |
+
# USDSEARCH_VISION_VALIDATION_ENABLED=false) β only touched when
|
| 203 |
+
# LLM_ENDPOINT_BASE_URL is actually set, so a deploy with it unset
|
| 204 |
+
# doesn't require LLM_ENDPOINT_API_KEY either and behaves exactly as
|
| 205 |
+
# before this feature existed.
|
| 206 |
+
llm_vars = cfg.llm_endpoint_variables()
|
| 207 |
+
if llm_vars:
|
| 208 |
+
print(f"enabling LLM parsing / VLM verification -> {cfg.llm_endpoint_base_url}")
|
| 209 |
+
for key, value in llm_vars.items():
|
| 210 |
+
api.add_space_variable(repo_id, key, value)
|
| 211 |
+
api.add_space_secret(repo_id, "USDSEARCH_LLM_API_KEY", cfg.llm_endpoint_api_key())
|
| 212 |
+
|
| 213 |
+
print("committing Space source ...")
|
| 214 |
+
api.upload_folder(
|
| 215 |
+
repo_id=repo_id,
|
| 216 |
+
repo_type="space",
|
| 217 |
+
folder_path=str(STAGE_DIR),
|
| 218 |
+
commit_message=f"Deploy {manifest['corpus_name']} run {manifest['run_id']}",
|
| 219 |
+
# The Explorer bundle is content-hashed (static/js/main.<hash>.js):
|
| 220 |
+
# without this, every deploy after a bundle change leaves the previous
|
| 221 |
+
# hashed chunks behind as orphans on the Hub, and a stale index.html
|
| 222 |
+
# from another bundle can survive next to the new one.
|
| 223 |
+
delete_patterns=["services/explorer/dist/**"],
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
print(f"\ndeployed: https://huggingface.co/spaces/{repo_id}")
|
| 227 |
+
print(
|
| 228 |
+
"Hugging Face is now rebuilding on this commit β the previous "
|
| 229 |
+
"container (if any) keeps serving until the new one is ready."
|
| 230 |
+
)
|
| 231 |
+
return 0
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def run_gpu(cfg: Config) -> int:
|
| 235 |
+
"""Deploy the GPU variant (docker/Dockerfile.hf-space-gpu, local
|
| 236 |
+
siglip2-triton instead of NVCF) to its own, separate Space β
|
| 237 |
+
HF_SPACE_REPO_ID_GPU, not HF_SPACE_REPO_ID. Same shape as run() above;
|
| 238 |
+
see that function's own comments for anything not called out again
|
| 239 |
+
here. Gated on hf_space_gpu_smoke_passed (set by bake.py's
|
| 240 |
+
verify_baked, which needs a GPU host to run at all β see that
|
| 241 |
+
function's own comment) rather than hf_space_smoke_passed, since the
|
| 242 |
+
two images are built and verified independently.
|
| 243 |
+
"""
|
| 244 |
+
manifest = load_manifest()
|
| 245 |
+
if not manifest.get("hf_space_gpu_smoke_passed"):
|
| 246 |
+
print(
|
| 247 |
+
"The hf_space_gpu image's own smoke test has not passed for "
|
| 248 |
+
"this run. Run `bake` with HF_SPACE_BUILD_GPU_VARIANT=true on a "
|
| 249 |
+
"GPU-equipped host (its verify_baked step sets "
|
| 250 |
+
"hf_space_gpu_smoke_passed) before deploying."
|
| 251 |
+
)
|
| 252 |
+
return 1
|
| 253 |
+
if "hf_space_gpu" not in image_tags(cfg, manifest):
|
| 254 |
+
print("No hf_space_gpu image for this run (HF_SPACE_BUILD_GPU_VARIANT != true) β nothing to deploy.")
|
| 255 |
+
return 1
|
| 256 |
+
|
| 257 |
+
repo_id = cfg.require("HF_SPACE_REPO_ID_GPU") # e.g. "nvidia/simready-central-gpu"
|
| 258 |
+
title = cfg.get("HF_SPACE_TITLE_GPU", repo_id.split("/", 1)[-1])
|
| 259 |
+
hardware = cfg.get("HF_SPACE_GPU_HARDWARE", HF_SPACE_GPU_HARDWARE_DEFAULT)
|
| 260 |
+
aws = artifact_env() # raises if the artifact-bucket keys aren't set β no NVCF token needed here
|
| 261 |
+
|
| 262 |
+
print(f"staging Space source at {STAGE_DIR_GPU} ...")
|
| 263 |
+
stage_repo(
|
| 264 |
+
STAGE_DIR_GPU, manifest,
|
| 265 |
+
README_TEMPLATE_GPU.format(title=title, hardware=hardware),
|
| 266 |
+
dockerfile_name="Dockerfile.hf-space-gpu",
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
api = HfApi()
|
| 270 |
+
print(f"creating/reusing Space {repo_id} ...")
|
| 271 |
+
api.create_repo(repo_id=repo_id, repo_type="space", space_sdk="docker", exist_ok=True)
|
| 272 |
+
|
| 273 |
+
print(f"requesting {hardware} hardware ...")
|
| 274 |
+
# Front-matter `hardware:` (in README_TEMPLATE_GPU) only takes effect
|
| 275 |
+
# when HF creates a brand-new Space; an existing Space keeps whatever
|
| 276 |
+
# tier it's already on unless explicitly changed here β this call is
|
| 277 |
+
# what actually makes a redeploy switch it, e.g. after bumping
|
| 278 |
+
# HF_SPACE_GPU_HARDWARE.
|
| 279 |
+
api.request_space_hardware(repo_id, hardware=SpaceHardware(hardware))
|
| 280 |
+
|
| 281 |
+
print("setting build-arg variables ...")
|
| 282 |
+
for key, value in hf_space_gpu_build_args(cfg, manifest).items():
|
| 283 |
+
api.add_space_variable(repo_id, key, value)
|
| 284 |
+
|
| 285 |
+
print("setting runtime secrets ...")
|
| 286 |
+
# No SIGLIP2_TRITON_SERVER_AUTH_TOKEN here β this variant's local
|
| 287 |
+
# siglip2-triton process has no auth in front of it (loopback-only,
|
| 288 |
+
# same as OpenSearch/Neo4j/Redis in this image).
|
| 289 |
+
api.add_space_secret(repo_id, "SIMREADYCENTRAL_INDEX_STORE_AWS_ACCESS_KEY_ID", aws["AWS_ACCESS_KEY_ID"])
|
| 290 |
+
api.add_space_secret(repo_id, "SIMREADYCENTRAL_INDEX_STORE_AWS_SECRET_ACCESS_KEY", aws["AWS_SECRET_ACCESS_KEY"])
|
| 291 |
+
|
| 292 |
+
llm_vars = cfg.llm_endpoint_variables()
|
| 293 |
+
if llm_vars:
|
| 294 |
+
print(f"enabling LLM parsing / VLM verification -> {cfg.llm_endpoint_base_url}")
|
| 295 |
+
for key, value in llm_vars.items():
|
| 296 |
+
api.add_space_variable(repo_id, key, value)
|
| 297 |
+
api.add_space_secret(repo_id, "USDSEARCH_LLM_API_KEY", cfg.llm_endpoint_api_key())
|
| 298 |
+
|
| 299 |
+
print("committing Space source ...")
|
| 300 |
+
api.upload_folder(
|
| 301 |
+
repo_id=repo_id,
|
| 302 |
+
repo_type="space",
|
| 303 |
+
folder_path=str(STAGE_DIR_GPU),
|
| 304 |
+
commit_message=f"Deploy {manifest['corpus_name']} run {manifest['run_id']} (GPU variant)",
|
| 305 |
+
delete_patterns=["services/explorer/dist/**"],
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
print(f"\ndeployed: https://huggingface.co/spaces/{repo_id}")
|
| 309 |
+
print(
|
| 310 |
+
"Hugging Face is now rebuilding on this commit β the previous "
|
| 311 |
+
"container (if any) keeps serving until the new one is ready. "
|
| 312 |
+
f"Billing runs at the {hardware} rate while this Space is not asleep."
|
| 313 |
+
)
|
| 314 |
+
return 0
|
pipeline/hf_space_seed.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runtime OpenSearch/Neo4j/siglip2-model seeding for the HF Space image.
|
| 2 |
+
|
| 3 |
+
Run at container startup by supervisord, one call per data store β see
|
| 4 |
+
docker/hf-space/supervisord.conf (NVCF/cpu-basic variant) and
|
| 5 |
+
docker/hf-space/supervisord-gpu.conf (GPU variant): `--component=opensearch`
|
| 6 |
+
gates deepsearch-api's own startup on finishing (it waits for OpenSearch,
|
| 7 |
+
then restores into the already-running cluster over HTTP); `--component=neo4j`
|
| 8 |
+
*is* the neo4j supervisord program's command, since neo4j-admin's load
|
| 9 |
+
requires the database to be offline β restore has to happen and finish
|
| 10 |
+
*before* the real `neo4j` process starts, not after, unlike OpenSearch's
|
| 11 |
+
live-bulk-load. `--component=siglip2-model` (GPU variant only) *is* the
|
| 12 |
+
siglip2-triton supervisord program's command, same "restore then exec into
|
| 13 |
+
the real process" shape as neo4j's β see main_siglip2_model()'s own
|
| 14 |
+
docstring for why these particular weights are restored at runtime instead
|
| 15 |
+
of baked in, unlike everything else this variant's Dockerfile does bake.
|
| 16 |
+
Not at `docker build` time: HF Space secrets are only ever
|
| 17 |
+
plain runtime env vars for a Docker Space (there is no way to give a `FROM`
|
| 18 |
+
pull credentials at Hugging Face's own build time β see
|
| 19 |
+
docker/Dockerfile.hf-space's header β and using a runtime secret sidesteps
|
| 20 |
+
that entirely, since restoring becomes something the already-running,
|
| 21 |
+
already-public container does for itself, with the artifact-bucket AWS
|
| 22 |
+
keys arriving the same way the NVCF token does). Reuses `restore.py`'s
|
| 23 |
+
tested S3-download + PUT-index + `_bulk` + `_refresh` sequence for
|
| 24 |
+
OpenSearch directly, just without the indexing pipeline's own local
|
| 25 |
+
manifest.json/docker-compose state, since none of that exists inside a
|
| 26 |
+
Space container β only the plain env vars baked in by pipeline/bake.py
|
| 27 |
+
(ARTIFACT_S3_URI, ARTIFACT_S3_REGION, CORPUS_NAME) plus one live one:
|
| 28 |
+
|
| 29 |
+
RESTORE_RUN_ID β optional runtime *variable* (not secret; not baked in).
|
| 30 |
+
Empty (the default) means "whatever this corpus's `latest.json` pointer
|
| 31 |
+
currently names" β re-resolved independently by each component on every
|
| 32 |
+
container (re)start, so a Space picks up newly `push`ed runs on its next
|
| 33 |
+
restart with no redeploy needed. Set to an explicit run id (matching
|
| 34 |
+
pipeline/restore.py's own RESTORE_RUN_ID) to pin the Space to one fixed run
|
| 35 |
+
regardless of what gets published later β set via the Space's Settings ->
|
| 36 |
+
Variables (a plain, non-secret runtime var, editable without triggering a
|
| 37 |
+
rebuild). Both components resolve it the same way, so they always agree on
|
| 38 |
+
which run to restore β except for a narrow, accepted race: if a new run is
|
| 39 |
+
published to S3 in the brief window between the two components' own
|
| 40 |
+
independent (unpinned) `latest.json` reads, they could disagree by one run.
|
| 41 |
+
Low-probability and self-corrects on the Space's next restart; not worth
|
| 42 |
+
the added complexity of coordinating the two processes over this.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
import argparse
|
| 48 |
+
import gzip
|
| 49 |
+
import json
|
| 50 |
+
import os
|
| 51 |
+
import shutil
|
| 52 |
+
import subprocess
|
| 53 |
+
import sys
|
| 54 |
+
import time
|
| 55 |
+
from pathlib import Path
|
| 56 |
+
|
| 57 |
+
import httpx
|
| 58 |
+
|
| 59 |
+
from .artifact_store import artifact_env, aws_cmd, run_aws_with_retry
|
| 60 |
+
from .config import BUILD_DIR, Config
|
| 61 |
+
from .restore import RESTORE_DIR, download, restore_opensearch
|
| 62 |
+
|
| 63 |
+
OS_URL = "http://localhost:9200"
|
| 64 |
+
NEO4J_HOME = Path("/var/lib/neo4j")
|
| 65 |
+
NEO4J_DATA_DIR = Path("/data")
|
| 66 |
+
# Separate from OpenSearch's own RESTORE_DIR (pipeline/restore.py's default)
|
| 67 |
+
# so the two components' downloads, run by separate supervisord programs,
|
| 68 |
+
# never race on the same directory β see download()'s own docstring note.
|
| 69 |
+
NEO4J_RESTORE_DIR = BUILD_DIR / "restore-neo4j"
|
| 70 |
+
|
| 71 |
+
# GPU variant only (docker/Dockerfile.hf-space-gpu) β a third component,
|
| 72 |
+
# corpus-*independent* unlike the two above (the same weights serve every
|
| 73 |
+
# corpus this variant might ever be built for): restores the SigLIP2 ONNX
|
| 74 |
+
# encoders' weight files, which the Dockerfile deliberately does NOT bake
|
| 75 |
+
# in (see its own comment β HF Space git repos cap storage at 1 GB by
|
| 76 |
+
# default, well under the ~3.6 GB these weights need). Published once via
|
| 77 |
+
# scripts/publish_siglip2_model_to_s3.sh, whenever
|
| 78 |
+
# scripts/extract_siglip2_model.sh refreshes the vendored copy; restored
|
| 79 |
+
# here into the config.pbtxt-only skeleton the Dockerfile does bake.
|
| 80 |
+
MODELS_DIR = Path("/models")
|
| 81 |
+
SIGLIP2_MODEL_S3_PREFIX_DEFAULT = "siglip2-model/fp16-v1"
|
| 82 |
+
SIGLIP2_TRITON_CMD = [
|
| 83 |
+
"tritonserver", "--model-repository=/models", "--http-port=8003",
|
| 84 |
+
"--log-verbose=0", "--strict-model-config=true",
|
| 85 |
+
"--backend-config=onnxruntime,default-max-batch-size=32",
|
| 86 |
+
"--response-cache-byte-size=536870912", "--exit-on-error=false",
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def resolve_prefix(cfg: Config, base: str, corpus_name: str) -> str:
|
| 91 |
+
pinned = cfg.get("RESTORE_RUN_ID").strip()
|
| 92 |
+
if pinned:
|
| 93 |
+
print(f"==> RESTORE_RUN_ID={pinned} β using a pinned run, not latest.json")
|
| 94 |
+
return f"{base}/{corpus_name}/{pinned}"
|
| 95 |
+
|
| 96 |
+
pointer_uri = f"{base}/{corpus_name}/latest.json"
|
| 97 |
+
# Outer timeout raised alongside aws_cmd()'s own --cli-read-timeout
|
| 98 |
+
# default (see that function's comment) β even this tiny a file hit the
|
| 99 |
+
# same intermittent connectivity issue on a real T4 Space deploy.
|
| 100 |
+
result = subprocess.run(
|
| 101 |
+
aws_cmd(cfg) + ["cp", pointer_uri, "-"],
|
| 102 |
+
capture_output=True, text=True, env=artifact_env(), timeout=120,
|
| 103 |
+
)
|
| 104 |
+
if result.returncode != 0:
|
| 105 |
+
raise SystemExit(
|
| 106 |
+
f"no published run found at {pointer_uri} β run `upload` for "
|
| 107 |
+
f"this corpus at least once before starting this Space."
|
| 108 |
+
)
|
| 109 |
+
return json.loads(result.stdout)["prefix"]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def wait_for_cluster(timeout: float = 180.0) -> None:
|
| 113 |
+
deadline = time.monotonic() + timeout
|
| 114 |
+
while time.monotonic() < deadline:
|
| 115 |
+
try:
|
| 116 |
+
if httpx.get(f"{OS_URL}/_cluster/health", timeout=5.0).status_code < 400:
|
| 117 |
+
return
|
| 118 |
+
except httpx.HTTPError:
|
| 119 |
+
pass
|
| 120 |
+
time.sleep(2)
|
| 121 |
+
raise SystemExit("OpenSearch did not become ready before the seeding deadline")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def main_opensearch(cfg: Config, base: str, corpus_name: str) -> int:
|
| 125 |
+
print("==> waiting for OpenSearch")
|
| 126 |
+
wait_for_cluster()
|
| 127 |
+
|
| 128 |
+
prefix = resolve_prefix(cfg, base, corpus_name)
|
| 129 |
+
print(f"==> restoring from {prefix}")
|
| 130 |
+
download(cfg, prefix)
|
| 131 |
+
manifest = json.loads((RESTORE_DIR / "manifest.json").read_text())
|
| 132 |
+
restore_opensearch(cfg, manifest)
|
| 133 |
+
|
| 134 |
+
print("==> document counts")
|
| 135 |
+
for index in sorted(manifest.get("opensearch_artifacts") or {}):
|
| 136 |
+
count = httpx.get(f"{OS_URL}/{index}/_count", timeout=10.0).json()["count"]
|
| 137 |
+
print(f" {index}: {count}")
|
| 138 |
+
if count == 0:
|
| 139 |
+
raise SystemExit(f"index {index} is empty after seeding")
|
| 140 |
+
|
| 141 |
+
print("==> seed complete")
|
| 142 |
+
return 0
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main_neo4j(cfg: Config, base: str, corpus_name: str) -> int:
|
| 146 |
+
# Idempotency guard, same reasoning as docker/neo4j-seeded-entrypoint.sh:
|
| 147 |
+
# a supervisord restart of this program after a crash (not a fresh
|
| 148 |
+
# container) should just start neo4j, not re-download/re-load.
|
| 149 |
+
if (NEO4J_DATA_DIR / "databases" / "neo4j").exists():
|
| 150 |
+
print("==> /data already populated, skipping restore")
|
| 151 |
+
else:
|
| 152 |
+
prefix = resolve_prefix(cfg, base, corpus_name)
|
| 153 |
+
print(f"==> restoring from {prefix}")
|
| 154 |
+
download(cfg, prefix, dest=NEO4J_RESTORE_DIR)
|
| 155 |
+
manifest = json.loads((NEO4J_RESTORE_DIR / "manifest.json").read_text())
|
| 156 |
+
artifact = manifest.get("neo4j_artifact")
|
| 157 |
+
if not artifact:
|
| 158 |
+
raise SystemExit("manifest has no neo4j_artifact for this run")
|
| 159 |
+
|
| 160 |
+
load_dir = NEO4J_RESTORE_DIR / "neo4j" / "load"
|
| 161 |
+
load_dir.mkdir(parents=True, exist_ok=True)
|
| 162 |
+
src = NEO4J_RESTORE_DIR / "neo4j" / artifact["file"]
|
| 163 |
+
dest = load_dir / "neo4j.dump"
|
| 164 |
+
with gzip.open(src, "rb") as fh, dest.open("wb") as out:
|
| 165 |
+
shutil.copyfileobj(fh, out)
|
| 166 |
+
|
| 167 |
+
print("==> loading neo4j (offline β must finish before the server starts)")
|
| 168 |
+
subprocess.run(
|
| 169 |
+
[str(NEO4J_HOME / "bin" / "neo4j-admin"), "database", "load", "neo4j",
|
| 170 |
+
f"--from-path={load_dir}", "--overwrite-destination=true"],
|
| 171 |
+
check=True, timeout=600,
|
| 172 |
+
)
|
| 173 |
+
shutil.rmtree(NEO4J_RESTORE_DIR, ignore_errors=True)
|
| 174 |
+
print("==> neo4j load complete")
|
| 175 |
+
|
| 176 |
+
print("==> starting neo4j")
|
| 177 |
+
os.execvp(str(NEO4J_HOME / "bin" / "neo4j"), [str(NEO4J_HOME / "bin" / "neo4j"), "console"])
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def main_siglip2_model(cfg: Config) -> int:
|
| 181 |
+
"""Restore the SigLIP2 ONNX encoders' weight files, then exec tritonserver.
|
| 182 |
+
|
| 183 |
+
Corpus-independent (unlike main_opensearch/main_neo4j) β these weights
|
| 184 |
+
are the same regardless of which corpus this Space serves, published
|
| 185 |
+
once at a fixed S3 prefix (scripts/publish_siglip2_model_to_s3.sh), not
|
| 186 |
+
part of any indexing run's own artifact set. Restored at runtime rather
|
| 187 |
+
than baked into docker/Dockerfile.hf-space-gpu because HF Space git
|
| 188 |
+
repos cap storage at 1 GB by default β verified live 2026-09-23, a real
|
| 189 |
+
deploy-space-gpu attempt 403'd ("Repository storage limit reached") at
|
| 190 |
+
~3.6 GB of baked weights.
|
| 191 |
+
"""
|
| 192 |
+
# Idempotency guard, same reasoning as main_neo4j's: a supervisord
|
| 193 |
+
# restart of this program after a crash (not a fresh container) should
|
| 194 |
+
# just start tritonserver, not re-download ~3.6 GB.
|
| 195 |
+
marker = MODELS_DIR / ".siglip2_weights_restored"
|
| 196 |
+
if marker.exists():
|
| 197 |
+
print("==> siglip2 weights already restored, skipping")
|
| 198 |
+
else:
|
| 199 |
+
prefix_name = cfg.get("SIGLIP2_MODEL_S3_PREFIX", SIGLIP2_MODEL_S3_PREFIX_DEFAULT).strip()
|
| 200 |
+
# Deliberately not resolve_prefix()/ARTIFACT_S3_URI's <corpus>/<run>
|
| 201 |
+
# shape β these weights are corpus-independent, published once at a
|
| 202 |
+
# fixed prefix (see scripts/publish_siglip2_model_to_s3.sh), not
|
| 203 |
+
# part of any indexing run's own artifact set.
|
| 204 |
+
base = cfg.require("ARTIFACT_S3_URI").rstrip("/")
|
| 205 |
+
prefix = f"{base}/{prefix_name}"
|
| 206 |
+
print(f"==> restoring siglip2 model weights from {prefix}")
|
| 207 |
+
# run_aws_with_retry, not a bare subprocess.run: a real deploy hit
|
| 208 |
+
# an intermittent `exit 255` here (and, on a later deploy, on the
|
| 209 |
+
# much smaller neo4j sync too) that longer timeouts alone didn't
|
| 210 |
+
# fully fix β see that function's own docstring.
|
| 211 |
+
run_aws_with_retry(
|
| 212 |
+
aws_cmd(cfg) + ["sync", prefix + "/", str(MODELS_DIR), "--only-show-errors"],
|
| 213 |
+
env=artifact_env(), timeout=1800,
|
| 214 |
+
)
|
| 215 |
+
for name in ("siglip2_vision_encoder_onnx", "siglip2_text_encoder_onnx"):
|
| 216 |
+
weight_file = MODELS_DIR / name / "1" / "model.onnx"
|
| 217 |
+
if not weight_file.exists():
|
| 218 |
+
raise SystemExit(f"{weight_file} missing after sync from {prefix} β publish incomplete?")
|
| 219 |
+
marker.touch()
|
| 220 |
+
print("==> siglip2 weights restored")
|
| 221 |
+
|
| 222 |
+
print("==> starting siglip2-triton")
|
| 223 |
+
os.execvp(SIGLIP2_TRITON_CMD[0], SIGLIP2_TRITON_CMD)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def main() -> int:
|
| 227 |
+
parser = argparse.ArgumentParser()
|
| 228 |
+
parser.add_argument("--component", choices=["opensearch", "neo4j", "siglip2-model"], default="opensearch")
|
| 229 |
+
args = parser.parse_args()
|
| 230 |
+
|
| 231 |
+
cfg = Config(values=dict(os.environ))
|
| 232 |
+
|
| 233 |
+
if args.component == "siglip2-model":
|
| 234 |
+
return main_siglip2_model(cfg)
|
| 235 |
+
|
| 236 |
+
base = cfg.require("ARTIFACT_S3_URI").rstrip("/")
|
| 237 |
+
corpus_name = cfg.require("CORPUS_NAME")
|
| 238 |
+
|
| 239 |
+
if args.component == "neo4j":
|
| 240 |
+
return main_neo4j(cfg, base, corpus_name)
|
| 241 |
+
return main_opensearch(cfg, base, corpus_name)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
sys.exit(main())
|
pipeline/queues.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Redis plugin-queue depth, for submission backpressure and drain checks.
|
| 2 |
+
|
| 3 |
+
Key names are not guessed. From `packages/cache/cache/src/client/redis.py:109`
|
| 4 |
+
and `client/config.py:22`, the streams are:
|
| 5 |
+
|
| 6 |
+
deepsearch_{plugin_name}_jobs per-plugin job stream
|
| 7 |
+
deepsearch_results plugin results, drained by omni-writer
|
| 8 |
+
|
| 9 |
+
with consumer group `deepsearch-worker-pool`. "Unprocessed" for a stream is the
|
| 10 |
+
consumer group's backlog (`lag`) plus its in-flight entries (`pending`), which
|
| 11 |
+
is what `_total_unprocessed` reports internally.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from typing import Dict, Iterable
|
| 17 |
+
|
| 18 |
+
import redis.asyncio as aioredis
|
| 19 |
+
|
| 20 |
+
STREAM_PREFIX = "deepsearch"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def plugin_stream(plugin_name: str) -> str:
|
| 24 |
+
return f"{STREAM_PREFIX}_{plugin_name}_jobs"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
RESULTS_STREAM = f"{STREAM_PREFIX}_results"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
async def stream_backlog(client: "aioredis.Redis", stream: str) -> int:
|
| 31 |
+
"""Unprocessed entries in `stream`, across all consumer groups.
|
| 32 |
+
|
| 33 |
+
Before any consumer group exists the whole stream is unprocessed, so fall
|
| 34 |
+
back to XLEN rather than reporting a misleading zero.
|
| 35 |
+
"""
|
| 36 |
+
try:
|
| 37 |
+
groups = await client.xinfo_groups(stream)
|
| 38 |
+
except aioredis.ResponseError:
|
| 39 |
+
return 0 # stream does not exist yet
|
| 40 |
+
if not groups:
|
| 41 |
+
try:
|
| 42 |
+
return int(await client.xlen(stream))
|
| 43 |
+
except aioredis.ResponseError:
|
| 44 |
+
return 0
|
| 45 |
+
total = 0
|
| 46 |
+
for group in groups:
|
| 47 |
+
lag = group.get("lag")
|
| 48 |
+
total += int(lag or 0) + int(group.get("pending") or 0)
|
| 49 |
+
return total
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
async def queue_depths(client: "aioredis.Redis", plugins: Iterable[str]) -> Dict[str, int]:
|
| 53 |
+
depths = {p: await stream_backlog(client, plugin_stream(p)) for p in plugins}
|
| 54 |
+
depths["results"] = await stream_backlog(client, RESULTS_STREAM)
|
| 55 |
+
return depths
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def connect(url: str) -> "aioredis.Redis":
|
| 59 |
+
return aioredis.from_url(url, decode_responses=True)
|
pipeline/restore.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 1a (pre-submit) β restore a previous run's index into the live stack.
|
| 2 |
+
|
| 3 |
+
Paired with `clear`, this turns full-corpus indexing into delta indexing:
|
| 4 |
+
resume from the last successful run's complete OpenSearch + Neo4j state, then
|
| 5 |
+
`submit` only touches the assets this run's corpus.json actually names β
|
| 6 |
+
`new`, `changed` (a URL that already exists in the baseline, so it must be
|
| 7 |
+
cleared first β see `clear.py`), or `removed` (cleared and never resubmitted).
|
| 8 |
+
|
| 9 |
+
Skipped cleanly on the very first run for a corpus: no `latest.json` yet and
|
| 10 |
+
no explicit `RESTORE_RUN_ID` means there is nothing to restore, so the stack
|
| 11 |
+
just starts empty, exactly as before this feature existed.
|
| 12 |
+
|
| 13 |
+
The OpenSearch load mirrors `docker/opensearch-seed.sh` (same PUT-index +
|
| 14 |
+
`_bulk` + `_refresh` sequence, same settings/mappings merge from `bake.py`),
|
| 15 |
+
run directly against the live cluster instead of a build-time container. The
|
| 16 |
+
Neo4j load mirrors `export.py::export_neo4j`'s stop/dump/restart dance,
|
| 17 |
+
reversed: stop, `neo4j-admin database load --overwrite-destination=true`,
|
| 18 |
+
restart.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import gzip
|
| 24 |
+
import json
|
| 25 |
+
import shutil
|
| 26 |
+
import subprocess
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Dict, List, Optional
|
| 29 |
+
|
| 30 |
+
import httpx
|
| 31 |
+
|
| 32 |
+
from .artifact_store import artifact_env, aws_cmd, bucket_base, run_aws_with_retry
|
| 33 |
+
from .bake import build_create_body
|
| 34 |
+
from .config import BUILD_DIR, Config
|
| 35 |
+
from .enumerate_assets import load_manifest
|
| 36 |
+
from .export import COMPOSE_PROJECT
|
| 37 |
+
from .to_bulk import iter_bulk_lines
|
| 38 |
+
|
| 39 |
+
RESTORE_DIR = BUILD_DIR / "restore"
|
| 40 |
+
BASELINE_ASSETS = RESTORE_DIR / "assets.jsonl"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _run(cmd: List[str], **kwargs) -> subprocess.CompletedProcess:
|
| 44 |
+
print(" $ " + " ".join(str(c) for c in cmd[:8]) + (" ..." if len(cmd) > 8 else ""))
|
| 45 |
+
return subprocess.run(cmd, check=True, **kwargs)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def resolve_baseline_prefix(cfg: Config) -> Optional[str]:
|
| 49 |
+
"""The S3 prefix to restore from, or None if there is no previous run."""
|
| 50 |
+
corpus_name = load_manifest()["corpus_name"]
|
| 51 |
+
base = bucket_base(cfg)
|
| 52 |
+
|
| 53 |
+
explicit = cfg.get("RESTORE_RUN_ID").strip()
|
| 54 |
+
if explicit:
|
| 55 |
+
return f"{base}/{corpus_name}/{explicit}"
|
| 56 |
+
|
| 57 |
+
pointer_uri = f"{base}/{corpus_name}/latest.json"
|
| 58 |
+
try:
|
| 59 |
+
result = subprocess.run(
|
| 60 |
+
aws_cmd(cfg) + ["cp", pointer_uri, "-"],
|
| 61 |
+
capture_output=True, text=True, env=artifact_env(), timeout=60,
|
| 62 |
+
)
|
| 63 |
+
except subprocess.TimeoutExpired:
|
| 64 |
+
raise SystemExit(
|
| 65 |
+
f"aws s3 cp {pointer_uri} timed out after 60s β see artifact_store.py's "
|
| 66 |
+
f"aws_cmd() docstring for why this call is bounded at all."
|
| 67 |
+
)
|
| 68 |
+
if result.returncode != 0:
|
| 69 |
+
return None
|
| 70 |
+
pointer = json.loads(result.stdout)
|
| 71 |
+
return pointer["prefix"]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def download(cfg: Config, prefix: str, dest: Path = RESTORE_DIR) -> None:
|
| 75 |
+
# dest is overridable so two independent restores (e.g. the HF Space's
|
| 76 |
+
# OpenSearch and Neo4j restores, run by separate supervisord programs β
|
| 77 |
+
# see pipeline/hf_space_seed.py) can download in parallel without
|
| 78 |
+
# racing on the same directory: one's `rmtree` could wipe out files the
|
| 79 |
+
# other is still reading.
|
| 80 |
+
if dest.exists():
|
| 81 |
+
shutil.rmtree(dest)
|
| 82 |
+
dest.mkdir(parents=True)
|
| 83 |
+
# Outer wall-clock timeout raised alongside aws_cmd()'s own
|
| 84 |
+
# --cli-read-timeout default (see that function's comment) β a per-read
|
| 85 |
+
# timeout larger than this outer one would just get preempted by it.
|
| 86 |
+
# run_aws_with_retry (not the bare _run() every other caller in this
|
| 87 |
+
# file uses) β see its own docstring for why this specific call needed
|
| 88 |
+
# backoff-retry, not just a longer timeout.
|
| 89 |
+
print(" $ " + " ".join(str(c) for c in (aws_cmd(cfg) + ["sync", prefix + "/", str(dest), "--only-show-errors"])[:8]) + " ...")
|
| 90 |
+
run_aws_with_retry(
|
| 91 |
+
aws_cmd(cfg) + ["sync", prefix + "/", str(dest), "--only-show-errors"],
|
| 92 |
+
env=artifact_env(), timeout=360,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def bulk_load(opensearch_url: str, index: str, data_path: Path, chunk_docs: int) -> int:
|
| 97 |
+
lines = iter_bulk_lines(data_path, index)
|
| 98 |
+
batch: List[str] = []
|
| 99 |
+
docs_in_batch = 0
|
| 100 |
+
loaded = 0
|
| 101 |
+
|
| 102 |
+
def flush() -> None:
|
| 103 |
+
nonlocal batch, docs_in_batch, loaded
|
| 104 |
+
if not batch:
|
| 105 |
+
return
|
| 106 |
+
response = httpx.post(
|
| 107 |
+
f"{opensearch_url}/_bulk",
|
| 108 |
+
content=("\n".join(batch) + "\n").encode(),
|
| 109 |
+
headers={"Content-Type": "application/x-ndjson"},
|
| 110 |
+
timeout=120.0,
|
| 111 |
+
)
|
| 112 |
+
response.raise_for_status()
|
| 113 |
+
result = response.json()
|
| 114 |
+
if result.get("errors"):
|
| 115 |
+
failed = [
|
| 116 |
+
item for action in result.get("items", [])
|
| 117 |
+
for item in action.values() if item.get("error")
|
| 118 |
+
]
|
| 119 |
+
raise SystemExit(
|
| 120 |
+
f"{index}: bulk restore rejected {len(failed)} document(s): {failed[:3]}"
|
| 121 |
+
)
|
| 122 |
+
loaded += docs_in_batch
|
| 123 |
+
batch = []
|
| 124 |
+
docs_in_batch = 0
|
| 125 |
+
|
| 126 |
+
for action_line, source_line in zip(*[lines] * 2):
|
| 127 |
+
batch.append(action_line)
|
| 128 |
+
batch.append(source_line)
|
| 129 |
+
docs_in_batch += 1
|
| 130 |
+
if docs_in_batch >= chunk_docs:
|
| 131 |
+
flush()
|
| 132 |
+
flush()
|
| 133 |
+
return loaded
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def restore_opensearch(cfg: Config, manifest: Dict) -> None:
|
| 137 |
+
artifacts = manifest.get("opensearch_artifacts") or {}
|
| 138 |
+
if not artifacts:
|
| 139 |
+
print(" no opensearch_artifacts in the baseline manifest, nothing to load")
|
| 140 |
+
return
|
| 141 |
+
|
| 142 |
+
os_dir = RESTORE_DIR / "opensearch"
|
| 143 |
+
chunk_docs = cfg.get_int("BULK_CHUNK_DOCS", 500)
|
| 144 |
+
for index in sorted(artifacts):
|
| 145 |
+
print(f" loading {index} ...")
|
| 146 |
+
# info-endpoint/omni-writer create the index empty at their own
|
| 147 |
+
# startup, with whatever minimal/dynamic mapping their app code uses
|
| 148 |
+
# β not the full settings+mappings a previous run resolved. Loading
|
| 149 |
+
# into that leaves fields like `usd_dimensions.*` dynamically typed,
|
| 150 |
+
# which OpenSearch resolves per-document during the bulk load itself
|
| 151 |
+
# and can reject a later doc with "cannot be changed from type X to
|
| 152 |
+
# Y" if two documents' JSON numbers imply different types. An index
|
| 153 |
+
# that already holds documents is left alone (never drop real data);
|
| 154 |
+
# an empty one is replaced with the authoritative baseline mapping.
|
| 155 |
+
count_response = httpx.get(f"{cfg.opensearch_url}/{index}/_count", timeout=10.0)
|
| 156 |
+
existing_docs = count_response.json().get("count", 0) if count_response.status_code < 400 else 0
|
| 157 |
+
if existing_docs == 0:
|
| 158 |
+
if count_response.status_code < 400:
|
| 159 |
+
httpx.delete(f"{cfg.opensearch_url}/{index}", timeout=30.0)
|
| 160 |
+
body = build_create_body(
|
| 161 |
+
os_dir / f"{index}.settings.json.gz",
|
| 162 |
+
os_dir / f"{index}.mapping.json.gz",
|
| 163 |
+
)
|
| 164 |
+
response = httpx.put(f"{cfg.opensearch_url}/{index}", json=body, timeout=30.0)
|
| 165 |
+
if response.status_code >= 400:
|
| 166 |
+
# raise_for_status()'s own message is just "403 Forbidden" β
|
| 167 |
+
# the actual reason (e.g. a disk-watermark read-only block)
|
| 168 |
+
# is in the body, and only there.
|
| 169 |
+
raise SystemExit(
|
| 170 |
+
f"PUT {cfg.opensearch_url}/{index} failed: "
|
| 171 |
+
f"{response.status_code} {response.text}"
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
print(f" {index} already holds {existing_docs} doc(s) β leaving its mapping as-is")
|
| 175 |
+
|
| 176 |
+
loaded = bulk_load(cfg.opensearch_url, index, os_dir / f"{index}.data.json.gz", chunk_docs)
|
| 177 |
+
print(f" loaded {loaded} docs")
|
| 178 |
+
|
| 179 |
+
# Same reasoning as opensearch-seed.sh: the index is created with a long
|
| 180 |
+
# refresh_interval, so `_count` reads 0 until an explicit refresh happens.
|
| 181 |
+
httpx.post(f"{cfg.opensearch_url}/_refresh", timeout=60.0)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def restore_neo4j(cfg: Config, manifest: Dict) -> None:
|
| 185 |
+
artifact = manifest.get("neo4j_artifact")
|
| 186 |
+
if not artifact:
|
| 187 |
+
print(" no neo4j_artifact in the baseline manifest, nothing to load")
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
load_dir = RESTORE_DIR / "neo4j" / "load"
|
| 191 |
+
if load_dir.exists():
|
| 192 |
+
shutil.rmtree(load_dir)
|
| 193 |
+
load_dir.mkdir(parents=True)
|
| 194 |
+
src = RESTORE_DIR / "neo4j" / artifact["file"]
|
| 195 |
+
dest = load_dir / "neo4j.dump"
|
| 196 |
+
with gzip.open(src, "rb") as fh, dest.open("wb") as out:
|
| 197 |
+
shutil.copyfileobj(fh, out)
|
| 198 |
+
|
| 199 |
+
print(" stopping asset-graph-service and neo4j ...")
|
| 200 |
+
_run(["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 201 |
+
"stop", "asset-graph-service", "neo4j"], env=cfg.compose_env())
|
| 202 |
+
|
| 203 |
+
print(" loading neo4j ...")
|
| 204 |
+
# Runs as root (the image default, same as export's dump), then chowns
|
| 205 |
+
# back to uid 7474 β the volume is owned by the in-container neo4j user,
|
| 206 |
+
# same fact export.py's dump step relies on in the other direction.
|
| 207 |
+
_run([
|
| 208 |
+
"docker", "run", "--rm",
|
| 209 |
+
"--entrypoint", "bash",
|
| 210 |
+
"-v", f"{COMPOSE_PROJECT}_neo4j-data:/data",
|
| 211 |
+
"-v", f"{load_dir}:/dumps",
|
| 212 |
+
f"neo4j:{cfg.get('NEO4J_VERSION', '5.23.0-community')}",
|
| 213 |
+
"-c",
|
| 214 |
+
"set -e; neo4j-admin database load neo4j --from-path=/dumps "
|
| 215 |
+
"--overwrite-destination=true; chown -R 7474:7474 /data",
|
| 216 |
+
])
|
| 217 |
+
|
| 218 |
+
print(" restarting neo4j and asset-graph-service ...")
|
| 219 |
+
_run(["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 220 |
+
"start", "neo4j", "asset-graph-service"], env=cfg.compose_env())
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def run(cfg: Config) -> int:
|
| 224 |
+
prefix = resolve_baseline_prefix(cfg)
|
| 225 |
+
if prefix is None:
|
| 226 |
+
print("no previous run found for this corpus β starting from an empty index")
|
| 227 |
+
if RESTORE_DIR.exists():
|
| 228 |
+
shutil.rmtree(RESTORE_DIR)
|
| 229 |
+
RESTORE_DIR.mkdir(parents=True)
|
| 230 |
+
BASELINE_ASSETS.write_text("")
|
| 231 |
+
return 0
|
| 232 |
+
|
| 233 |
+
print(f"restoring baseline from {prefix}")
|
| 234 |
+
download(cfg, prefix)
|
| 235 |
+
manifest = json.loads((RESTORE_DIR / "manifest.json").read_text())
|
| 236 |
+
|
| 237 |
+
print("\nRestoring OpenSearch ...")
|
| 238 |
+
restore_opensearch(cfg, manifest)
|
| 239 |
+
|
| 240 |
+
print("\nRestoring Neo4j ...")
|
| 241 |
+
restore_neo4j(cfg, manifest)
|
| 242 |
+
|
| 243 |
+
print(f"\nrestored run {manifest['run_id']} ({manifest.get('asset_count', '?')} baseline assets)")
|
| 244 |
+
return 0
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def load_baseline_urls() -> set:
|
| 248 |
+
"""URLs already indexed as of the restored baseline. Empty on a first run."""
|
| 249 |
+
if not BASELINE_ASSETS.exists():
|
| 250 |
+
return set()
|
| 251 |
+
return {
|
| 252 |
+
json.loads(line)["url"]
|
| 253 |
+
for line in BASELINE_ASSETS.read_text().splitlines()
|
| 254 |
+
if line.strip()
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def expected_total(removed: set, current_urls: set) -> int:
|
| 259 |
+
"""Doc/node count the whole index should hold once this run finishes.
|
| 260 |
+
|
| 261 |
+
baseline, minus whatever this run explicitly removed, unioned with
|
| 262 |
+
whatever this run submits (new assets add, repeating assets overwrite in
|
| 263 |
+
place so they don't double-count).
|
| 264 |
+
"""
|
| 265 |
+
baseline = load_baseline_urls()
|
| 266 |
+
return len((baseline - removed) | current_urls)
|
pipeline/run_pipeline.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""USD Search index re-generation pipeline β single entry point.
|
| 2 |
+
|
| 3 |
+
python -m pipeline.run_pipeline <stage> [options]
|
| 4 |
+
|
| 5 |
+
Stages run in order and each is independently re-runnable:
|
| 6 |
+
|
| 7 |
+
enumerate corpus.json -> exact asset list (N) + manifest
|
| 8 |
+
index bring the stack up, restore the baseline, clear, submit the
|
| 9 |
+
delta, wait
|
| 10 |
+
export elasticdump the indexes; neo4j-admin dump the graph
|
| 11 |
+
upload publish the artifact set to S3
|
| 12 |
+
bake build images with the data pre-loaded
|
| 13 |
+
verify runtime-only stack from the baked images + smoke test
|
| 14 |
+
push push the baked images (only after verify passes)
|
| 15 |
+
all every stage in order
|
| 16 |
+
|
| 17 |
+
deploy-space deploy the hf_space image to a Hugging Face Space (not
|
| 18 |
+
part of `all` β run manually after `push`; see
|
| 19 |
+
pipeline/hf_space.py)
|
| 20 |
+
deploy-space-gpu same, for the GPU variant (local siglip2-triton
|
| 21 |
+
instead of NVCF, HF_SPACE_BUILD_GPU_VARIANT=true β
|
| 22 |
+
see docker/Dockerfile.hf-space-gpu and
|
| 23 |
+
docs/plans/siglip2-embedded-hf-space.md)
|
| 24 |
+
|
| 25 |
+
Helper stages: `up`, `down`, `restore`, `clear`, `submit`, `wait`, `status`.
|
| 26 |
+
|
| 27 |
+
`restore`/`clear` turn this into delta indexing: `restore` loads the previous
|
| 28 |
+
run's complete OpenSearch + Neo4j state (from `latest.json`, or an explicit
|
| 29 |
+
`RESTORE_RUN_ID`) into the freshly-up stack; `clear` then removes anything
|
| 30 |
+
this run's corpus.json marks `removed`, plus anything that's about to be
|
| 31 |
+
resubmitted (a URL present in both the baseline and this run's assets β
|
| 32 |
+
Neo4j's dependency edges aren't idempotent on reprocessing, so the old ones
|
| 33 |
+
must go first). `submit` then only touches this run's assets.jsonl β the
|
| 34 |
+
delta the corpus.json actually names. First run for a corpus (no previous
|
| 35 |
+
`latest.json`) skips `restore` cleanly and behaves exactly as before this
|
| 36 |
+
feature existed.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
from __future__ import annotations
|
| 40 |
+
|
| 41 |
+
import argparse
|
| 42 |
+
import os
|
| 43 |
+
import subprocess
|
| 44 |
+
import sys
|
| 45 |
+
from pathlib import Path
|
| 46 |
+
from typing import List
|
| 47 |
+
|
| 48 |
+
from .config import BUILD_DIR, Config
|
| 49 |
+
|
| 50 |
+
INDEXING_PROJECT = "simready-indexing"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _compose(cfg: Config, args: List[str]) -> int:
|
| 54 |
+
cmd = ["docker", "compose", "-p", INDEXING_PROJECT, *cfg.indexing_compose_files(), *args]
|
| 55 |
+
print("$ " + " ".join(cmd[:8]) + (" ..." if len(cmd) > 8 else ""))
|
| 56 |
+
return subprocess.run(cmd, env=cfg.compose_env(), check=False).returncode
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# ββ stages βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def stage_enumerate(cfg: Config, args) -> int:
|
| 63 |
+
from .enumerate_assets import run
|
| 64 |
+
return run(cfg, dry_run=args.dry_run)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def stage_up(cfg: Config, args) -> int:
|
| 68 |
+
if not (BUILD_DIR / "settings.yaml").exists() or not (BUILD_DIR / "plugin.config.yaml").exists():
|
| 69 |
+
print("`.build/settings.yaml`/`.build/plugin.config.yaml` are missing β run `enumerate` first.")
|
| 70 |
+
return 1
|
| 71 |
+
if cfg.siglip2_nvcf:
|
| 72 |
+
print(f"siglip2 backend: nvcf ({cfg.get('SIGLIP2_NVCF_ENDPOINT', 'grpc.nvcf.nvidia.com:443')}) "
|
| 73 |
+
f"β no local GPU needed for embeddings.")
|
| 74 |
+
if cfg.vlm_metadata_enabled:
|
| 75 |
+
cfg.require_vlm_metadata_credentials()
|
| 76 |
+
model = os.environ.get("USDSEARCH_VISION_METADATA_MODEL", "").strip() or "azure/openai/gpt-5.6-sol (default)"
|
| 77 |
+
base_url = os.environ.get("USDSEARCH_LLM_BASE_URL", "").strip() or "https://inference-api.nvidia.com (default)"
|
| 78 |
+
print(f"VLM metadata extraction: enabled (model={model!r} @ {base_url})")
|
| 79 |
+
# Independent replica counts, not one shared knob: thumbnail_to_embedding
|
| 80 |
+
# and asset_graph_generation don't actually bottleneck together in
|
| 81 |
+
# practice β the Kit-based USD parse graph-builder does for
|
| 82 |
+
# asset_graph_generation is far slower per-asset than a siglip2 call, so
|
| 83 |
+
# a real corpus typically wants these scaled very differently. All three
|
| 84 |
+
# services dropped container_name (and, for the two workers, host ports)
|
| 85 |
+
# in compose/docker-compose.indexing.yml specifically to allow this.
|
| 86 |
+
scale_args: List[str] = []
|
| 87 |
+
scale_targets = [
|
| 88 |
+
("monitor-worker-thumbnail-to-embedding", "THUMBNAIL_WORKER_REPLICAS"),
|
| 89 |
+
("monitor-worker-asset-graph-generation", "AGS_WORKER_REPLICAS"),
|
| 90 |
+
("graph-builder", "GRAPH_BUILDER_REPLICAS"),
|
| 91 |
+
]
|
| 92 |
+
if cfg.vlm_metadata_enabled:
|
| 93 |
+
scale_targets.append(
|
| 94 |
+
("monitor-worker-thumbnail-to-vision-metadata", "VLM_METADATA_WORKER_REPLICAS")
|
| 95 |
+
)
|
| 96 |
+
for service, key in scale_targets:
|
| 97 |
+
replicas = cfg.get_int(key, 1)
|
| 98 |
+
if replicas > 1:
|
| 99 |
+
print(f"scaling {service} to {replicas} replicas ({key})")
|
| 100 |
+
scale_args += ["--scale", f"{service}={replicas}"]
|
| 101 |
+
return _compose(cfg, ["up", "-d", "--wait", *scale_args])
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def stage_down(cfg: Config, args) -> int:
|
| 105 |
+
return _compose(cfg, ["down"] + (["-v"] if args.volumes else []))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def stage_restore(cfg: Config, args) -> int:
|
| 109 |
+
from .restore import run
|
| 110 |
+
return run(cfg)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def stage_clear(cfg: Config, args) -> int:
|
| 114 |
+
from .clear import run
|
| 115 |
+
return run(cfg)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def stage_submit(cfg: Config, args) -> int:
|
| 119 |
+
from .submit import run
|
| 120 |
+
return run(cfg, resume=not args.no_resume)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def stage_wait(cfg: Config, args) -> int:
|
| 124 |
+
from .status import run
|
| 125 |
+
return run(cfg)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def stage_index(cfg: Config, args) -> int:
|
| 129 |
+
"""Bring the stack up, restore the baseline, clear, submit the delta, wait."""
|
| 130 |
+
steps = (
|
| 131 |
+
("up", stage_up),
|
| 132 |
+
("restore", stage_restore),
|
| 133 |
+
("clear", stage_clear),
|
| 134 |
+
("submit", stage_submit),
|
| 135 |
+
("wait", stage_wait),
|
| 136 |
+
)
|
| 137 |
+
for step, fn in steps:
|
| 138 |
+
print(f"\n{'=' * 70}\n== index: {step}\n{'=' * 70}")
|
| 139 |
+
code = fn(cfg, args)
|
| 140 |
+
if code != 0:
|
| 141 |
+
print(f"\nindex stage failed at `{step}` (exit {code})")
|
| 142 |
+
return code
|
| 143 |
+
return 0
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def stage_export(cfg: Config, args) -> int:
|
| 147 |
+
from .export import run
|
| 148 |
+
return run(cfg)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def stage_upload(cfg: Config, args) -> int:
|
| 152 |
+
from .upload import run
|
| 153 |
+
return run(cfg)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def stage_bake(cfg: Config, args) -> int:
|
| 157 |
+
from .bake import run
|
| 158 |
+
return run(cfg, push=False)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def stage_verify(cfg: Config, args) -> int:
|
| 162 |
+
from .verify import run
|
| 163 |
+
return run(cfg, keep_up=args.keep_up)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def stage_deploy_space(cfg: Config, args) -> int:
|
| 167 |
+
from .hf_space import run
|
| 168 |
+
return run(cfg)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def stage_deploy_space_gpu(cfg: Config, args) -> int:
|
| 172 |
+
from .hf_space import run_gpu
|
| 173 |
+
return run_gpu(cfg)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def stage_push(cfg: Config, args) -> int:
|
| 177 |
+
from .bake import image_tags, latest_tags
|
| 178 |
+
from .enumerate_assets import load_manifest
|
| 179 |
+
manifest = load_manifest()
|
| 180 |
+
if not manifest.get("smoke_passed") and not args.force:
|
| 181 |
+
print(
|
| 182 |
+
"The smoke test has not passed for this run. Run `verify` first, or "
|
| 183 |
+
"pass --force if you are pushing deliberately without it."
|
| 184 |
+
)
|
| 185 |
+
return 1
|
| 186 |
+
latest = latest_tags(cfg, manifest)
|
| 187 |
+
for name, tag in image_tags(cfg, manifest).items():
|
| 188 |
+
print(f"pushing {tag}")
|
| 189 |
+
result = subprocess.run(["docker", "push", tag], check=False)
|
| 190 |
+
if result.returncode != 0:
|
| 191 |
+
return result.returncode
|
| 192 |
+
|
| 193 |
+
# `-latest` only ever means "the most recently *published* run" β
|
| 194 |
+
# a `docker tag` here, not built alongside the versioned tag in
|
| 195 |
+
# bake.py, so a plain `bake` (never pushed) never touches it.
|
| 196 |
+
latest_tag = latest[name]
|
| 197 |
+
print(f"tagging {latest_tag}")
|
| 198 |
+
subprocess.run(["docker", "tag", tag, latest_tag], check=True)
|
| 199 |
+
print(f"pushing {latest_tag}")
|
| 200 |
+
result = subprocess.run(["docker", "push", latest_tag], check=False)
|
| 201 |
+
if result.returncode != 0:
|
| 202 |
+
return result.returncode
|
| 203 |
+
return 0
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def stage_all(cfg: Config, args) -> int:
|
| 207 |
+
order = [
|
| 208 |
+
("enumerate", stage_enumerate),
|
| 209 |
+
("index", stage_index),
|
| 210 |
+
("export", stage_export),
|
| 211 |
+
("upload", stage_upload),
|
| 212 |
+
("bake", stage_bake),
|
| 213 |
+
("verify", stage_verify),
|
| 214 |
+
("push", stage_push),
|
| 215 |
+
]
|
| 216 |
+
names = [n for n, _ in order]
|
| 217 |
+
start = names.index(args.start) if args.start else 0
|
| 218 |
+
stop = names.index(args.stop) + 1 if args.stop else len(order)
|
| 219 |
+
|
| 220 |
+
for i, (name, fn) in enumerate(order[start:stop], start=start):
|
| 221 |
+
print(f"\n{'#' * 70}\n# stage: {name}\n{'#' * 70}")
|
| 222 |
+
code = fn(cfg, args)
|
| 223 |
+
if name == "upload" and code == 2:
|
| 224 |
+
# ARTIFACT_S3_URI is the one unresolved input; skipping it must not
|
| 225 |
+
# block baking and verifying locally.
|
| 226 |
+
print("skipping upload (ARTIFACT_S3_URI unset) and continuing")
|
| 227 |
+
continue
|
| 228 |
+
if code != 0:
|
| 229 |
+
print(f"\npipeline stopped: stage `{name}` failed (exit {code})")
|
| 230 |
+
return code
|
| 231 |
+
if name == "export" and i + 1 < stop:
|
| 232 |
+
# `export` already captured everything the indexing stack has to
|
| 233 |
+
# offer β neither `upload` nor `bake` touch it again, and
|
| 234 |
+
# `verify` next brings up the *runtime* stack, which binds the
|
| 235 |
+
# same host ports (9200, 7474/7687, ...) anyway. Tear it down
|
| 236 |
+
# here, not after `bake`: leaving opensearch/neo4j/usdsearch/
|
| 237 |
+
# graph-builder running for the full duration of `bake`'s two
|
| 238 |
+
# `docker build`s serves no purpose and competes for the same
|
| 239 |
+
# constrained disk/memory a CI runner has β a run was observed
|
| 240 |
+
# to hang inside `bake` for 44+ minutes, unresponsive even to
|
| 241 |
+
# the job's own force-cancel, consistent with the runner
|
| 242 |
+
# thrashing under that contention rather than any single
|
| 243 |
+
# command actually being stuck. Skipped if `--stop export` or
|
| 244 |
+
# `--stop upload` means nothing after this needs the stack down,
|
| 245 |
+
# so an operator who stopped here on purpose can still inspect it.
|
| 246 |
+
print("\ntearing down the indexing stack (nothing after `export` needs it) ...")
|
| 247 |
+
_compose(cfg, ["down", "--volumes"])
|
| 248 |
+
print("\npipeline complete")
|
| 249 |
+
return 0
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
STAGES = {
|
| 253 |
+
"enumerate": stage_enumerate,
|
| 254 |
+
"up": stage_up,
|
| 255 |
+
"down": stage_down,
|
| 256 |
+
"restore": stage_restore,
|
| 257 |
+
"clear": stage_clear,
|
| 258 |
+
"submit": stage_submit,
|
| 259 |
+
"wait": stage_wait,
|
| 260 |
+
"status": stage_wait,
|
| 261 |
+
"index": stage_index,
|
| 262 |
+
"export": stage_export,
|
| 263 |
+
"upload": stage_upload,
|
| 264 |
+
"bake": stage_bake,
|
| 265 |
+
"verify": stage_verify,
|
| 266 |
+
"push": stage_push,
|
| 267 |
+
"deploy-space": stage_deploy_space,
|
| 268 |
+
"deploy-space-gpu": stage_deploy_space_gpu,
|
| 269 |
+
"all": stage_all,
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def main(argv: List[str] | None = None) -> int:
|
| 274 |
+
parser = argparse.ArgumentParser(
|
| 275 |
+
prog="run_pipeline",
|
| 276 |
+
description=__doc__,
|
| 277 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 278 |
+
)
|
| 279 |
+
parser.add_argument("stage", choices=sorted(STAGES))
|
| 280 |
+
parser.add_argument("--env-file", type=Path, default=None,
|
| 281 |
+
help="defaults to config/pipeline.env")
|
| 282 |
+
parser.add_argument("--dry-run", action="store_true",
|
| 283 |
+
help="enumerate: list and filter without writing")
|
| 284 |
+
parser.add_argument("--no-resume", action="store_true",
|
| 285 |
+
help="submit: ignore the checkpoint and resubmit everything")
|
| 286 |
+
parser.add_argument("--volumes", action="store_true",
|
| 287 |
+
help="down: also remove volumes (destroys the index)")
|
| 288 |
+
parser.add_argument("--keep-up", action="store_true",
|
| 289 |
+
help="verify: leave the runtime stack running afterwards")
|
| 290 |
+
parser.add_argument("--force", action="store_true",
|
| 291 |
+
help="push: push without a passing smoke test")
|
| 292 |
+
parser.add_argument("--start", choices=list(STAGES), default=None,
|
| 293 |
+
help="all: resume from this stage")
|
| 294 |
+
parser.add_argument("--stop", choices=list(STAGES), default=None,
|
| 295 |
+
help="all: stop after this stage")
|
| 296 |
+
args = parser.parse_args(argv)
|
| 297 |
+
|
| 298 |
+
cfg = Config.load(args.env_file)
|
| 299 |
+
if not cfg.values:
|
| 300 |
+
print(
|
| 301 |
+
f"No configuration found at {cfg.env_file}.\n"
|
| 302 |
+
f"Copy config/pipeline.env.example to config/pipeline.env and edit it.",
|
| 303 |
+
file=sys.stderr,
|
| 304 |
+
)
|
| 305 |
+
return 1
|
| 306 |
+
|
| 307 |
+
return STAGES[args.stage](cfg, args)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
if __name__ == "__main__":
|
| 311 |
+
sys.exit(main())
|
pipeline/status.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stage 1b β wait for indexing to complete, then report on it.
|
| 2 |
+
|
| 3 |
+
Two phases, because the endpoint's two halves have different timing
|
| 4 |
+
(see submit.py):
|
| 5 |
+
|
| 6 |
+
Phase A (deterministic) the base metadata write is synchronous, so once
|
| 7 |
+
submission succeeds for N assets the main index must
|
| 8 |
+
hold exactly N documents. No polling.
|
| 9 |
+
|
| 10 |
+
Phase B (polled) plugin work is asynchronous. Poll
|
| 11 |
+
/info/indexing/asset/status per asset, which reports
|
| 12 |
+
per-plugin state *and the failure reason*.
|
| 13 |
+
|
| 14 |
+
`PluginStatusType` (services/info-endpoint/.../api/models.py:26) has five
|
| 15 |
+
states, not the three its endpoint docstring lists β `failed_indexing` and
|
| 16 |
+
`out_of_sync_and_failed_indexing` are terminal failures we can read directly
|
| 17 |
+
rather than inferring from history strings.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import asyncio
|
| 23 |
+
import json
|
| 24 |
+
import subprocess
|
| 25 |
+
import time
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
from typing import Dict, List, Optional, Set, Tuple
|
| 28 |
+
|
| 29 |
+
import httpx
|
| 30 |
+
|
| 31 |
+
from .config import BUILD_DIR, Config, active_plugins
|
| 32 |
+
from .enumerate_assets import load_assets, load_manifest
|
| 33 |
+
from .export import COMPOSE_PROJECT
|
| 34 |
+
from .queues import connect, queue_depths
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def dump_worker_logs(cfg: Config) -> None:
|
| 38 |
+
"""Surface why assets never went terminal, when the poll loop gives up.
|
| 39 |
+
|
| 40 |
+
The actual failure (if any) happened inside these containers β the
|
| 41 |
+
worker calling out to NVCF/Triton, or info-endpoint dispatching the
|
| 42 |
+
job β not in this script, so a bare "TIMEOUT" here explains nothing on
|
| 43 |
+
its own. Best-effort: never let a logging step mask the real failure.
|
| 44 |
+
"""
|
| 45 |
+
print("\n worker/info-endpoint logs (last 100 lines each), for diagnosis:")
|
| 46 |
+
services = ["monitor-worker-thumbnail-to-embedding", "monitor-worker-asset-graph-generation", "info-endpoint"]
|
| 47 |
+
if cfg.vlm_metadata_enabled:
|
| 48 |
+
services.insert(2, "monitor-worker-thumbnail-to-vision-metadata")
|
| 49 |
+
for service in services:
|
| 50 |
+
print(f"\n --- {service} ---")
|
| 51 |
+
try:
|
| 52 |
+
subprocess.run(
|
| 53 |
+
["docker", "compose", "-p", COMPOSE_PROJECT, *cfg.indexing_compose_files(),
|
| 54 |
+
"logs", "--tail", "100", service],
|
| 55 |
+
check=False, timeout=30, env=cfg.compose_env(),
|
| 56 |
+
)
|
| 57 |
+
except subprocess.SubprocessError as exc:
|
| 58 |
+
print(f" (could not fetch logs for {service}: {exc})")
|
| 59 |
+
|
| 60 |
+
DONE = {"in_sync"}
|
| 61 |
+
TERMINAL_FAILURE = {"failed_indexing", "out_of_sync_and_failed_indexing"}
|
| 62 |
+
PENDING = {"out_of_sync", "not_found"}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclass
|
| 66 |
+
class PluginOutcome:
|
| 67 |
+
url: str
|
| 68 |
+
plugin: str
|
| 69 |
+
status: str
|
| 70 |
+
exception: str = ""
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclass
|
| 74 |
+
class Tally:
|
| 75 |
+
ok: int = 0
|
| 76 |
+
failed: int = 0
|
| 77 |
+
pending: int = 0
|
| 78 |
+
failures: List[PluginOutcome] = field(default_factory=list)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
async def fetch_status(
|
| 82 |
+
client: httpx.AsyncClient, base_url: str, url: str
|
| 83 |
+
) -> Optional[Dict]:
|
| 84 |
+
try:
|
| 85 |
+
response = await client.get(
|
| 86 |
+
f"{base_url}/info/indexing/asset/status", params={"url": url}
|
| 87 |
+
)
|
| 88 |
+
except (httpx.TransportError, httpx.TimeoutException):
|
| 89 |
+
return None
|
| 90 |
+
if response.status_code >= 400:
|
| 91 |
+
return None
|
| 92 |
+
return response.json()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def classify(doc: Dict, plugins: Tuple[str, ...]) -> Dict[str, Tuple[str, str]]:
|
| 96 |
+
"""-> {plugin: (verdict, detail)} where verdict is ok|failed|pending."""
|
| 97 |
+
out: Dict[str, Tuple[str, str]] = {}
|
| 98 |
+
for plugin, info in (doc.get("plugins_statuses") or {}).items():
|
| 99 |
+
if plugin not in plugins:
|
| 100 |
+
continue
|
| 101 |
+
state = info.get("indexing_status", "not_found")
|
| 102 |
+
detail = ""
|
| 103 |
+
history = info.get("plugin_status_history") or []
|
| 104 |
+
if history:
|
| 105 |
+
last = history[-1]
|
| 106 |
+
detail = str(last.get("exception") or last.get("status") or "")
|
| 107 |
+
if state in DONE:
|
| 108 |
+
out[plugin] = ("ok", detail)
|
| 109 |
+
elif state in TERMINAL_FAILURE:
|
| 110 |
+
out[plugin] = ("failed", detail or state)
|
| 111 |
+
else:
|
| 112 |
+
out[plugin] = ("pending", state)
|
| 113 |
+
# A plugin absent from the response has never been seen for this asset.
|
| 114 |
+
for plugin in plugins:
|
| 115 |
+
out.setdefault(plugin, ("pending", "absent"))
|
| 116 |
+
return out
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
async def poll_until_done(cfg: Config, urls: List[str]) -> Tuple[Dict[str, Tally], List[str]]:
|
| 120 |
+
"""Poll the outstanding set until every (asset, plugin) pair is terminal."""
|
| 121 |
+
interval = cfg.get_float("STATUS_POLL_INTERVAL", 15.0)
|
| 122 |
+
timeout = cfg.get_float("STATUS_TIMEOUT", 21600.0)
|
| 123 |
+
concurrency = cfg.get_int("STATUS_CONCURRENCY", 16)
|
| 124 |
+
redis_url = f"redis://localhost:{cfg.get_int('REDIS_HOST_PORT', 6380)}"
|
| 125 |
+
plugins = active_plugins(cfg)
|
| 126 |
+
|
| 127 |
+
tallies: Dict[str, Tally] = {p: Tally() for p in plugins}
|
| 128 |
+
settled: Dict[Tuple[str, str], Tuple[str, str]] = {}
|
| 129 |
+
outstanding: Set[str] = set(urls)
|
| 130 |
+
deadline = time.monotonic() + timeout
|
| 131 |
+
semaphore = asyncio.Semaphore(concurrency)
|
| 132 |
+
redis_client = connect(redis_url)
|
| 133 |
+
|
| 134 |
+
limits = httpx.Limits(max_connections=concurrency * 2)
|
| 135 |
+
async with httpx.AsyncClient(limits=limits, timeout=httpx.Timeout(30.0)) as client:
|
| 136 |
+
while outstanding:
|
| 137 |
+
if time.monotonic() > deadline:
|
| 138 |
+
print(f"\nTIMEOUT after {timeout:.0f}s with {len(outstanding)} assets outstanding.")
|
| 139 |
+
dump_worker_logs(cfg)
|
| 140 |
+
break
|
| 141 |
+
|
| 142 |
+
async def check(url: str) -> Tuple[str, Optional[Dict]]:
|
| 143 |
+
async with semaphore:
|
| 144 |
+
return url, await fetch_status(client, cfg.info_endpoint, url)
|
| 145 |
+
|
| 146 |
+
batch = await asyncio.gather(*(check(u) for u in sorted(outstanding)))
|
| 147 |
+
|
| 148 |
+
for url, doc in batch:
|
| 149 |
+
if doc is None:
|
| 150 |
+
continue
|
| 151 |
+
verdicts = classify(doc, plugins)
|
| 152 |
+
if all(v[0] != "pending" for v in verdicts.values()):
|
| 153 |
+
outstanding.discard(url)
|
| 154 |
+
for plugin, (verdict, detail) in verdicts.items():
|
| 155 |
+
settled[(url, plugin)] = (verdict, detail)
|
| 156 |
+
|
| 157 |
+
depths = await queue_depths(redis_client, plugins)
|
| 158 |
+
queued = sum(v for k, v in depths.items() if k != "results")
|
| 159 |
+
print(
|
| 160 |
+
f" outstanding={len(outstanding)} "
|
| 161 |
+
f"queued={queued} results={depths['results']}",
|
| 162 |
+
flush=True,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
if not outstanding:
|
| 166 |
+
break
|
| 167 |
+
|
| 168 |
+
# Everything drained but assets are still not terminal: the jobs
|
| 169 |
+
# were never picked up. The usual cause is a worker whose
|
| 170 |
+
# JOB_ITEM_TYPE omits "priority", which discards every job this
|
| 171 |
+
# pipeline creates.
|
| 172 |
+
if queued == 0 and depths["results"] == 0:
|
| 173 |
+
await asyncio.sleep(interval)
|
| 174 |
+
depths = await queue_depths(redis_client, plugins)
|
| 175 |
+
if sum(depths.values()) == 0:
|
| 176 |
+
print(
|
| 177 |
+
f"\nQueues are empty but {len(outstanding)} assets never reached a "
|
| 178 |
+
f"terminal state.\nCheck that both workers set "
|
| 179 |
+
f"DEEPSEARCH_MONITOR_WORKER_CONFIG_JOB_ITEM_TYPE to "
|
| 180 |
+
f'\'["priority","normal","none"]\' β the default drops '
|
| 181 |
+
f"priority jobs silently."
|
| 182 |
+
)
|
| 183 |
+
dump_worker_logs(cfg)
|
| 184 |
+
break
|
| 185 |
+
await asyncio.sleep(interval)
|
| 186 |
+
|
| 187 |
+
await redis_client.aclose()
|
| 188 |
+
|
| 189 |
+
for (url, plugin), (verdict, detail) in settled.items():
|
| 190 |
+
tally = tallies[plugin]
|
| 191 |
+
if verdict == "ok":
|
| 192 |
+
tally.ok += 1
|
| 193 |
+
else:
|
| 194 |
+
tally.failed += 1
|
| 195 |
+
tally.failures.append(PluginOutcome(url, plugin, verdict, detail))
|
| 196 |
+
for url in outstanding:
|
| 197 |
+
for plugin in plugins:
|
| 198 |
+
tallies[plugin].pending += 1
|
| 199 |
+
|
| 200 |
+
return tallies, sorted(outstanding)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
# ββ OpenSearch / Neo4j measurements ββββββββββββββββββββββββββββββββββββββββββ
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def refresh_index(cfg: Config, index: str) -> None:
|
| 207 |
+
"""Force docs into a searchable state before counting.
|
| 208 |
+
|
| 209 |
+
Indexes are created with a long `refresh_interval` (Β§2.8 batch tuning), so
|
| 210 |
+
`_count` can lag a synchronous write by up to that interval and report a
|
| 211 |
+
false submission gap β the same class of bug Β§8.2 already found at bake
|
| 212 |
+
time ("`_refresh`, not just `_flush`").
|
| 213 |
+
"""
|
| 214 |
+
try:
|
| 215 |
+
httpx.post(f"{cfg.opensearch_url}/{index}/_refresh", timeout=30.0)
|
| 216 |
+
except httpx.HTTPError:
|
| 217 |
+
pass # os_count will report a possibly-stale count; not fatal here
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def os_count(cfg: Config, index: str) -> int:
|
| 221 |
+
try:
|
| 222 |
+
response = httpx.get(f"{cfg.opensearch_url}/{index}/_count", timeout=30.0)
|
| 223 |
+
if response.status_code >= 400:
|
| 224 |
+
return 0
|
| 225 |
+
return int(response.json().get("count", 0))
|
| 226 |
+
except httpx.HTTPError:
|
| 227 |
+
return 0
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def embedding_coverage(cfg: Config) -> Tuple[int, float]:
|
| 231 |
+
"""Documents carrying a siglip2 embedding, as a fraction of the index.
|
| 232 |
+
|
| 233 |
+
Every corpus asset is assumed to already have a thumbnail (see the plan,
|
| 234 |
+
Β§5.4), so this should be 1.0. It is measured rather than trusted because a
|
| 235 |
+
missing thumbnail produces a searchable-by-text but vector-less document β
|
| 236 |
+
a silent recall hole.
|
| 237 |
+
"""
|
| 238 |
+
total = os_count(cfg, cfg.main_index)
|
| 239 |
+
if total == 0:
|
| 240 |
+
return 0, 0.0
|
| 241 |
+
query = {"query": {"nested": {
|
| 242 |
+
"path": "siglip2-embedding",
|
| 243 |
+
"query": {"exists": {"field": "siglip2-embedding.embedding"}},
|
| 244 |
+
}}}
|
| 245 |
+
try:
|
| 246 |
+
response = httpx.post(
|
| 247 |
+
f"{cfg.opensearch_url}/{cfg.main_index}/_count",
|
| 248 |
+
json=query, timeout=60.0,
|
| 249 |
+
)
|
| 250 |
+
if response.status_code >= 400:
|
| 251 |
+
return 0, 0.0
|
| 252 |
+
with_embedding = int(response.json().get("count", 0))
|
| 253 |
+
except httpx.HTTPError:
|
| 254 |
+
return 0, 0.0
|
| 255 |
+
return with_embedding, with_embedding / total
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def neo4j_counts(cfg: Config) -> Tuple[int, int]:
|
| 259 |
+
try:
|
| 260 |
+
from neo4j import GraphDatabase
|
| 261 |
+
except ImportError:
|
| 262 |
+
return -1, -1
|
| 263 |
+
try:
|
| 264 |
+
# Neo4j runs with auth disabled in this stack, so no credentials.
|
| 265 |
+
driver = GraphDatabase.driver(cfg.neo4j_bolt_url, auth=None)
|
| 266 |
+
with driver.session() as session:
|
| 267 |
+
nodes = session.run("MATCH (n) RETURN count(n) AS c").single()["c"]
|
| 268 |
+
rels = session.run("MATCH ()-[r]->() RETURN count(r) AS c").single()["c"]
|
| 269 |
+
driver.close()
|
| 270 |
+
return int(nodes), int(rels)
|
| 271 |
+
except Exception as exc: # noqa: BLE001 - report, never abort the stage
|
| 272 |
+
print(f" warning: could not read Neo4j counts ({type(exc).__name__}: {exc})")
|
| 273 |
+
return -1, -1
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def run(cfg: Config) -> int:
|
| 277 |
+
from .restore import expected_total as compute_expected_total
|
| 278 |
+
|
| 279 |
+
manifest = load_manifest()
|
| 280 |
+
assets = load_assets()
|
| 281 |
+
expected = len(assets) # submitted this run β scopes Phase B / plugin ratios
|
| 282 |
+
urls = [a.url for a in assets]
|
| 283 |
+
|
| 284 |
+
# Whole-index target: baseline (restored, minus this run's `removed`) plus
|
| 285 |
+
# whatever this run submits. Equals `expected` when there is no baseline
|
| 286 |
+
# (first run for a corpus, or `restore` never ran).
|
| 287 |
+
removed = set(manifest.get("removed") or [])
|
| 288 |
+
total_expected = compute_expected_total(removed, {a.url for a in assets})
|
| 289 |
+
|
| 290 |
+
print(f"expected assets this run: {expected}")
|
| 291 |
+
print(f"expected index total: {total_expected}\n")
|
| 292 |
+
|
| 293 |
+
print("Phase A β base documents (synchronous write, no polling needed)")
|
| 294 |
+
refresh_index(cfg, cfg.main_index)
|
| 295 |
+
base_docs = os_count(cfg, cfg.main_index)
|
| 296 |
+
print(f" {cfg.main_index}: {base_docs} docs (expected {total_expected})")
|
| 297 |
+
|
| 298 |
+
print("\nPhase B β plugin completion")
|
| 299 |
+
tallies, outstanding = asyncio.run(poll_until_done(cfg, urls))
|
| 300 |
+
|
| 301 |
+
print("\nMeasuring index ...")
|
| 302 |
+
refresh_index(cfg, cfg.main_index)
|
| 303 |
+
refresh_index(cfg, cfg.image_cache_index)
|
| 304 |
+
image_docs = os_count(cfg, cfg.image_cache_index)
|
| 305 |
+
embedded, coverage = embedding_coverage(cfg)
|
| 306 |
+
nodes, rels = neo4j_counts(cfg)
|
| 307 |
+
base_docs = os_count(cfg, cfg.main_index)
|
| 308 |
+
|
| 309 |
+
report = {
|
| 310 |
+
"run_id": manifest["run_id"],
|
| 311 |
+
"asset_count_expected": expected,
|
| 312 |
+
"index_total_expected": total_expected,
|
| 313 |
+
"base_docs_indexed": base_docs,
|
| 314 |
+
"image_cache_docs": image_docs,
|
| 315 |
+
"embedding_coverage": round(coverage, 4),
|
| 316 |
+
"docs_with_embedding": embedded,
|
| 317 |
+
"neo4j_node_count": nodes,
|
| 318 |
+
"neo4j_relationship_count": rels,
|
| 319 |
+
"per_plugin": {
|
| 320 |
+
p: {"ok": t.ok, "failed": t.failed, "pending": t.pending}
|
| 321 |
+
for p, t in tallies.items()
|
| 322 |
+
},
|
| 323 |
+
"failures": [
|
| 324 |
+
{"url": f.url, "plugin": f.plugin, "status": f.status, "exception": f.exception}
|
| 325 |
+
for t in tallies.values() for f in t.failures
|
| 326 |
+
],
|
| 327 |
+
"never_terminal": outstanding,
|
| 328 |
+
}
|
| 329 |
+
(BUILD_DIR / "index-report.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 330 |
+
|
| 331 |
+
# Carry the counts forward; the smoke test and export's pre-flight check
|
| 332 |
+
# both assert against these, and they need to reflect the whole merged
|
| 333 |
+
# index (baseline + delta), not just what this run submitted.
|
| 334 |
+
manifest.update({
|
| 335 |
+
"asset_count": total_expected,
|
| 336 |
+
"base_docs_indexed": base_docs,
|
| 337 |
+
"image_cache_docs": image_docs,
|
| 338 |
+
"neo4j_node_count": nodes,
|
| 339 |
+
"neo4j_relationship_count": rels,
|
| 340 |
+
})
|
| 341 |
+
from .enumerate_assets import save_manifest
|
| 342 |
+
save_manifest(manifest)
|
| 343 |
+
|
| 344 |
+
# ββ acceptance ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 345 |
+
print("\nββ Report ββ")
|
| 346 |
+
print(f" base docs: {base_docs} / {total_expected}")
|
| 347 |
+
print(f" image cache docs: {image_docs}")
|
| 348 |
+
print(f" embedding coverage: {coverage:.2%} ({embedded}/{base_docs})")
|
| 349 |
+
print(f" neo4j nodes/rels: {nodes} / {rels}")
|
| 350 |
+
for plugin, tally in tallies.items():
|
| 351 |
+
print(f" {plugin}: ok={tally.ok} failed={tally.failed} pending={tally.pending}")
|
| 352 |
+
|
| 353 |
+
failed = False
|
| 354 |
+
if base_docs != total_expected:
|
| 355 |
+
print(
|
| 356 |
+
f"\nFAIL: base document count {base_docs} != expected {total_expected} "
|
| 357 |
+
f"(baseline + this run's delta, minus removed). The metadata write is "
|
| 358 |
+
f"synchronous, so this is a submission gap, not a timing one β see "
|
| 359 |
+
f".build/submit-failures.json."
|
| 360 |
+
)
|
| 361 |
+
failed = True
|
| 362 |
+
|
| 363 |
+
plugin_min = cfg.get_float("PLUGIN_OK_MIN", 0.95)
|
| 364 |
+
for plugin, tally in tallies.items():
|
| 365 |
+
ratio = tally.ok / expected if expected else 0.0
|
| 366 |
+
if ratio < plugin_min:
|
| 367 |
+
print(f"\nFAIL: {plugin} completed {ratio:.2%} of assets, below PLUGIN_OK_MIN {plugin_min:.2%}.")
|
| 368 |
+
failed = True
|
| 369 |
+
|
| 370 |
+
coverage_min = cfg.get_float("EMBEDDING_COVERAGE_MIN", 0.99)
|
| 371 |
+
if coverage < coverage_min:
|
| 372 |
+
print(
|
| 373 |
+
f"\nFAIL: embedding coverage {coverage:.2%} is below EMBEDDING_COVERAGE_MIN "
|
| 374 |
+
f"{coverage_min:.2%}.\nThe corpus is assumed to be fully thumbnailed; a "
|
| 375 |
+
f"systematic gap here means it is not, and the fix is to enable the "
|
| 376 |
+
f"thumbnail_generation plugin."
|
| 377 |
+
)
|
| 378 |
+
failed = True
|
| 379 |
+
|
| 380 |
+
print(f"\nwrote .build/index-report.json")
|
| 381 |
+
return 1 if failed else 0
|