diff --git a/samples/ai/edge-ai-demo-studio/.gitignore b/samples/ai/edge-ai-demo-studio/.gitignore index b9358c5f..00cc7758 100644 --- a/samples/ai/edge-ai-demo-studio/.gitignore +++ b/samples/ai/edge-ai-demo-studio/.gitignore @@ -1,5 +1,6 @@ # Python environment .venv +.export-venv __pycache__ .cache diff --git a/samples/ai/edge-ai-demo-studio/README.md b/samples/ai/edge-ai-demo-studio/README.md index aafefe26..14b6bf94 100644 --- a/samples/ai/edge-ai-demo-studio/README.md +++ b/samples/ai/edge-ai-demo-studio/README.md @@ -21,7 +21,8 @@ Edge AI Demo Studio is a modern toolkit for deploying, managing, and serving AI - **Image Generation** — Generate images from text prompts using diffusion models accelerated with OpenVINO. - **MCP Manager** — Manage Model Context Protocol servers and their tool integrations. - **Wake Word Detection** — Detect custom wake words from microphone input and send webhook notifications on detection events. -- **Samples:** Sample use cases that implement the AI services (see [Exporting Samples](#exporting-samples) to package a subset for standalone deployment) + - **OCR** — Optical character recognition — extract text and its location from images using OCR models on OpenVINO. +- **Samples:** Sample use cases that implement the AI services (see [Exporting Samples & Services](#exporting-samples--services) to package a subset for standalone deployment) - **Digital Avatar** — Interact with an AI-powered avatar that combines real-time video with intelligent conversation. - **Digital Avatar Lite** — A lightweight animated robot avatar that brings conversations to life with responsive movements and expressions. - **RAG Chatbot** — Upload documents and chat with an AI that retrieves relevant context to answer your questions. @@ -121,17 +122,19 @@ By default, every service is created with its built-in defaults and stays stoppe The file is read on every startup: the default services are loaded first, then your presets overwrite them. Services with `"status": "online"` are started automatically (they move to `prepare` and are promoted to `active` once their health check passes); everything else stays offline. +The Electron packaging scripts (`scripts/bash/package.sh` / `scripts/win/package.ps1`) bundle the project-root `deployment.json` into the package, so presets set before packaging ship with the app. In a packaged build the file lives at `resources/deployment.json` (next to the bundled frontend) — edit it there and restart the app to change the presets of an existing package, or point the `DEPLOYMENT_CONFIG_PATH` environment variable at a file elsewhere. See [docs/deployment-config.md](docs/deployment-config.md#packaged-electron-builds) for details. + See [docs/deployment-config.md](docs/deployment-config.md) for the full JSON guidelines — all supported fields plus each service's available models, devices, and engines. The reference (and the [docs/deployment.schema.json](docs/deployment.schema.json) editor schema referenced via `$schema` above) is auto-generated from the service registry, so it always matches the version you are running. --- -## Exporting Samples +## Exporting Samples & Services -The **Export Samples** feature lets you produce a slim, self-contained copy of Demo Studio that contains only the sample(s) you select, together with the services and workers they depend on. The exported directory includes its own `setup.sh` / `setup_win.bat` and `start.sh` / `start_win.bat` scripts so it can be set up and run independently. +The **Export Bundle** feature (`scripts/export-bundle.mjs`) lets you produce a slim, self-contained copy of Demo Studio that contains only the sample(s) and/or service(s) you select, together with the services and workers they depend on. You can export samples (their services are resolved automatically), specific services on their own with no samples at all, or a mix of both. The exported directory includes its own `setup.sh` / `setup_win.bat` and `start.sh` / `start_win.bat` scripts so it can be set up and run independently. ### Via the Launcher Script -Run from the repository root — Node.js is bootstrapped automatically from `thirdparty/` if not already installed. The script lists all available samples, lets you pick one or more by number, then prompts for output directory, optional dependencies, and dry-run preference. +Run from the repository root — Node.js is bootstrapped automatically from `thirdparty/` if not already installed. The script lists all available samples and services, lets you pick any combination by number (leave a prompt blank to skip that category — e.g. pick only services for a samples-free export), then prompts for output directory, optional dependencies, and dry-run preference. For Linux: ```bash @@ -143,6 +146,16 @@ For Windows (PowerShell/Command Prompt): .\export.bat ``` +Arguments are forwarded to `scripts/export-bundle.mjs` for non-interactive use: + +```bash +./export.sh --samples=rag-chatbot # sample + its service deps +./export.sh --services=text-to-speech,ocr # services only, no samples +./export.sh --samples=rag-chatbot --services=ocr # mix of both +./export.sh --list # list available ids +./export.sh --samples=rag-chatbot --dry-run # preview the plan only +``` + ### Via the Frontend GUI 1. Open the web UI and navigate to the **Samples** page (`http://localhost:8080/samples`). @@ -191,3 +204,35 @@ See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for deployment guidelines. **Q: Why is Electron Skipped by default** This is because Electron is being used to create a packaged release only. If you need a packaged release, please refer to [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) + +**Q: Unable to Start Text Generation service (possibly due to Long Paths Issue) on Windows** + +The model could not be downloaded because the path is too long, causing the service to fail to start. Move the `edge-ai-demo-studio` project folder to a shorter path (e.g. `C:\edge-ai-demo-studio`) and try again. + +**Q: Unable to start Speech-to-Text service (or other services) on Windows** + +If you are behind a private/corporate network, check whether `NO_PROXY` is set in the terminal (CMD prompt/PowerShell) that you start Edge AI Demo Studio from, e.g.: + +```powershell +echo %NO_PROXY% +``` + +or in PowerShell: + +```powershell +echo $env:NO_PROXY +``` + +The terminal may not have picked up a `NO_PROXY` value that was set via the GUI (Settings > Network & internet > Proxy). Try setting `NO_PROXY` directly in the same terminal session before starting Edge AI Demo Studio: + +- CMD prompt (session-only): + + ```cmd + set NO_PROXY=localhost,127.0.0.1 + ``` + +- PowerShell (session-only): + + ```powershell + $env:NO_PROXY = "localhost,127.0.0.1" + ``` \ No newline at end of file diff --git a/samples/ai/edge-ai-demo-studio/docs/deployment-config.md b/samples/ai/edge-ai-demo-studio/docs/deployment-config.md index 78e54b9c..e07177e6 100644 --- a/samples/ai/edge-ai-demo-studio/docs/deployment-config.md +++ b/samples/ai/edge-ai-demo-studio/docs/deployment-config.md @@ -13,6 +13,22 @@ it to `active`). The file location can be overridden with the `DEPLOYMENT_CONFIG_PATH` environment variable. Add `"$schema": "./docs/deployment.schema.json"` for editor validation and autocompletion. +## Packaged (Electron) builds + +The packaging scripts (`scripts/bash/package.sh` / `scripts/win/package.ps1`) +bundle the project-root `deployment.json` (plus `docs/deployment.schema.json`) +into the Electron package automatically, so presets set before packaging ship +with the app. Inside the packaged app the file lives in the `resources` +directory next to the bundled frontend: + +- Linux (zip): `EdgeAIDemoStudio/linux-unpacked/resources/deployment.json` +- Windows (installer): `\resources\deployment.json` + +To change the presets of an already packaged build, edit that file and +restart the app; delete it to fall back to the built-in defaults. Setting +the `DEPLOYMENT_CONFIG_PATH` environment variable before launching the app +overrides the bundled file. + ## Example ```json @@ -122,6 +138,27 @@ Known models: | `OpenVINO/Qwen3-Embedding-0.6B-int8-ov` | `CPU`, `GPU`, `NPU` | — | | `Qwen/Qwen3-Embedding-0.6B-GGUF` | `CPU`, `GPU` | — | +### `face-recognition` — Face Recognition + +Detect and identify faces against a reference gallery using Open Model Zoo or OpenCV Zoo pipelines. + +| Field | Value | +| --- | --- | +| Default port | 8031 | +| Engines | `worker` | +| Default model | `omz-retail` on `CPU` | +| Devices | `CPU`, `GPU` | +| Custom models | no | +| Supported OS | `linux`, `windows` | + +Known models: + +| Model (`models.default.name`) | Devices | Backend | +| --- | --- | --- | +| `omz-retail` | `CPU`, `GPU` | `openvino` | +| `omz-adas` | `CPU`, `GPU` | `openvino` | +| `yunet-sface` | `CPU`, `GPU` | `openvino` | + ### `file-watcher` — File Watcher Watches a folder for new image files and broadcasts them over WebSocket for real-time processing. @@ -135,6 +172,26 @@ Watches a folder for new image files and broadcasts them over WebSocket for real | Custom models | no | | Supported OS | `linux`, `windows` | +### `frame-generation` — Frame Generation + +RIFE video frame interpolation: fill in-between frames or upscale video FPS. + +| Field | Value | +| --- | --- | +| Default port | 8031 | +| Engines | `worker` | +| Default model | `RIFE` on `CPU` | +| Devices | `CPU`, `GPU` | +| Custom models | no | +| Model sources | `huggingface`, `modelscope` | +| Supported OS | `linux`, `windows` | + +Known models: + +| Model (`models.default.name`) | Devices | Backend | +| --- | --- | --- | +| `RIFE` | `CPU`, `GPU` | `openvino` | + ### `geti-classifier` — Geti Image Classifier Serves inference from a local Intel Geti deployment and collects feedback for continuous model improvement. diff --git a/samples/ai/edge-ai-demo-studio/docs/deployment.schema.json b/samples/ai/edge-ai-demo-studio/docs/deployment.schema.json index 0fc15e27..fbe800dc 100644 --- a/samples/ai/edge-ai-demo-studio/docs/deployment.schema.json +++ b/samples/ai/edge-ai-demo-studio/docs/deployment.schema.json @@ -328,6 +328,150 @@ } } }, + "face-recognition": { + "type": "object", + "additionalProperties": false, + "description": "Detect and identify faces against a reference gallery using Open Model Zoo or OpenCV Zoo pipelines.", + "properties": { + "status": { + "type": "string", + "enum": [ + "online", + "offline" + ], + "description": "'online' starts the service when the app boots; 'offline' (default) leaves it stopped." + }, + "engine": { + "type": "string", + "enum": [ + "worker" + ], + "description": "Execution engine for this service" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Port to expose the service on (default: 8031)" + }, + "models": { + "type": "object", + "description": "Partial per-model overrides, merged over the defaults. 'default' is the primary model.", + "properties": { + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Model identifier (e.g. Hugging Face repo id)", + "enum": [ + "omz-retail", + "omz-adas", + "yunet-sface" + ] + }, + "device": { + "type": "string", + "description": "Accelerator to run on (e.g. CPU, GPU, NPU)", + "examples": [ + "CPU", + "GPU" + ] + }, + "source": { + "type": "string", + "enum": [ + "huggingface", + "modelscope", + "custom" + ] + }, + "quant": { + "type": "string", + "description": "Weight format / quantization" + }, + "params": { + "type": "string" + }, + "backend": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Model identifier (e.g. Hugging Face repo id)" + }, + "device": { + "type": "string", + "description": "Accelerator to run on (e.g. CPU, GPU, NPU)" + }, + "source": { + "type": "string", + "enum": [ + "huggingface", + "modelscope", + "custom" + ] + }, + "quant": { + "type": "string", + "description": "Weight format / quantization" + }, + "params": { + "type": "string" + }, + "backend": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "metadata": { + "type": "object", + "description": "Service metadata, merged over existing values", + "properties": { + "clientIceServerUrl": { + "type": "string", + "description": "Client-side ICE server URL (default: STUN)" + }, + "serverIceServerUrl": { + "type": "string", + "description": "Server-side ICE server URL (default: TURN)" + }, + "turnServerIp": { + "type": "string", + "description": "Deprecated: Use clientIceServerUrl / serverIceServerUrl instead" + }, + "languageCode": { + "type": "string", + "description": "Language Code for TTS" + }, + "vadThreshold": { + "type": "number", + "default": 0.2, + "description": "VAD Threshold for Wake Word Detection" + }, + "cpuAffinity": { + "type": "string", + "description": "CPU cores to pin this service to (numactl -C format, e.g. \"0-7\" or \"0,2,4\"). Empty / missing = all cores. Linux only." + } + } + } + } + }, "file-watcher": { "type": "object", "additionalProperties": false, @@ -463,6 +607,148 @@ } } }, + "frame-generation": { + "type": "object", + "additionalProperties": false, + "description": "RIFE video frame interpolation: fill in-between frames or upscale video FPS.", + "properties": { + "status": { + "type": "string", + "enum": [ + "online", + "offline" + ], + "description": "'online' starts the service when the app boots; 'offline' (default) leaves it stopped." + }, + "engine": { + "type": "string", + "enum": [ + "worker" + ], + "description": "Execution engine for this service" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Port to expose the service on (default: 8031)" + }, + "models": { + "type": "object", + "description": "Partial per-model overrides, merged over the defaults. 'default' is the primary model.", + "properties": { + "default": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Model identifier (e.g. Hugging Face repo id)", + "enum": [ + "RIFE" + ] + }, + "device": { + "type": "string", + "description": "Accelerator to run on (e.g. CPU, GPU, NPU)", + "examples": [ + "CPU", + "GPU" + ] + }, + "source": { + "type": "string", + "enum": [ + "huggingface", + "modelscope", + "custom" + ] + }, + "quant": { + "type": "string", + "description": "Weight format / quantization" + }, + "params": { + "type": "string" + }, + "backend": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Model identifier (e.g. Hugging Face repo id)" + }, + "device": { + "type": "string", + "description": "Accelerator to run on (e.g. CPU, GPU, NPU)" + }, + "source": { + "type": "string", + "enum": [ + "huggingface", + "modelscope", + "custom" + ] + }, + "quant": { + "type": "string", + "description": "Weight format / quantization" + }, + "params": { + "type": "string" + }, + "backend": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "metadata": { + "type": "object", + "description": "Service metadata, merged over existing values", + "properties": { + "clientIceServerUrl": { + "type": "string", + "description": "Client-side ICE server URL (default: STUN)" + }, + "serverIceServerUrl": { + "type": "string", + "description": "Server-side ICE server URL (default: TURN)" + }, + "turnServerIp": { + "type": "string", + "description": "Deprecated: Use clientIceServerUrl / serverIceServerUrl instead" + }, + "languageCode": { + "type": "string", + "description": "Language Code for TTS" + }, + "vadThreshold": { + "type": "number", + "default": 0.2, + "description": "VAD Threshold for Wake Word Detection" + }, + "cpuAffinity": { + "type": "string", + "description": "CPU cores to pin this service to (numactl -C format, e.g. \"0-7\" or \"0,2,4\"). Empty / missing = all cores. Linux only." + } + } + } + } + }, "geti-classifier": { "type": "object", "additionalProperties": false, diff --git a/samples/ai/edge-ai-demo-studio/electron/main.js b/samples/ai/edge-ai-demo-studio/electron/main.js index e30e3a76..cc058012 100644 --- a/samples/ai/edge-ai-demo-studio/electron/main.js +++ b/samples/ai/edge-ai-demo-studio/electron/main.js @@ -427,7 +427,9 @@ async function runInstaller(splash) { const projectRoot = getEnvironmentPath(path.join(__dirname, ".."), process.resourcesPath); const installerPath = path.join(projectRoot, "scripts", "install_dependencies.sh"); - const installMarker = path.join(app.getPath("userData"), ".installed"); + const installMarker = fileURLToPath( + new URL(`file://${path.join(app.getPath("userData"), ".installed")}`) + ); updateSplashProgress(splash, "Checking system dependencies...", 0); @@ -501,6 +503,40 @@ function getFrontendPath() { ); } +/** + * Gets the deployment.json path based on environment. + * In packaged builds the file is bundled into resources/ by + * scripts/bash/package.sh / scripts/win/package.ps1; in development it + * lives in the project root. A user-provided DEPLOYMENT_CONFIG_PATH + * @returns {string} The deployment.json path to pass to the frontend + */ +function getDeploymentConfigPath() { + const defaultPath = getEnvironmentPath( + path.join(__dirname, "..", "deployment.json"), + path.join(process.resourcesPath, "deployment.json") + ); + + const override = process.env.DEPLOYMENT_CONFIG_PATH; + if (!override) { + return defaultPath; + } + + const resolvedOverride = fileURLToPath(new URL(`file://${path.resolve(override)}`)); + const isValid = + path.extname(resolvedOverride).toLowerCase() === ".json" && + fs.existsSync(resolvedOverride) && + fs.statSync(resolvedOverride).isFile(); + + if (!isValid) { + console.error( + `Ignoring invalid DEPLOYMENT_CONFIG_PATH "${override}": file does not exist or is not a .json file.` + ); + return defaultPath; + } + + return resolvedOverride; +} + /** * Starts the Next.js server * @returns {Promise} A rejection-only promise that rejects if the server exits before being declared ready @@ -520,7 +556,12 @@ function startNextServer() { const serverProcess = spawn(nodePath, ["server.js"], { cwd: frontendPath, stdio: "pipe", - env: { ...process.env, PORT: "8080", NEXT_MANUAL_SIG_HANDLE: "true" }, + env: { + ...process.env, + PORT: "8080", + NEXT_MANUAL_SIG_HANDLE: "true", + DEPLOYMENT_CONFIG_PATH: getDeploymentConfigPath(), + }, }); childProcesses.push(serverProcess); diff --git a/samples/ai/edge-ai-demo-studio/export.sh b/samples/ai/edge-ai-demo-studio/export.sh old mode 100644 new mode 100755 index e8640969..796b4b79 --- a/samples/ai/edge-ai-demo-studio/export.sh +++ b/samples/ai/edge-ai-demo-studio/export.sh @@ -8,6 +8,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NODE_BIN_DIR="$SCRIPT_DIR/thirdparty/node/bin" NODE_PATH="$NODE_BIN_DIR/node" SETUP_THIRDPARTY="$SCRIPT_DIR/scripts/bash/setup_thirdparty.sh" +EXPORT_SCRIPT="$SCRIPT_DIR/scripts/export-bundle.mjs" # Install bundled Node.js if not already present if [ ! -f "$NODE_PATH" ]; then @@ -26,22 +27,49 @@ if ! command -v node >/dev/null 2>&1; then exit 1 fi -EXPORT_SCRIPT="$SCRIPT_DIR/scripts/export-samples.mjs" +# --dry-run/--plan alone should still walk through the interactive prompts +# (just previewing the plan instead of writing files), so pull it out before +# deciding whether to forward everything non-interactively. +DRY_RUN_FLAG="" +REMAINING_ARGS=() +for a in "$@"; do + if [ "$a" = "--dry-run" ] || [ "$a" = "--plan" ]; then + DRY_RUN_FLAG="$a" + else + REMAINING_ARGS+=("$a") + fi +done -# If arguments were passed, forward them directly (non-interactive mode). -if [ "$#" -gt 0 ]; then +# If other arguments were passed, forward everything directly (non-interactive mode). +if [ "${#REMAINING_ARGS[@]}" -gt 0 ]; then exec node "$EXPORT_SCRIPT" "$@" fi # ── Interactive mode ────────────────────────────────────────────── -echo "Discovering available samples..." +echo "" +echo "Discovering available samples and services..." +# --list prints two sections ("Available samples:" / "Available services:"), +# each with " - " entries. SAMPLES=() +SERVICES=() +section="" while IFS= read -r line; do - SAMPLES+=("$line") -done < <(node "$EXPORT_SCRIPT" --list | sed -n 's/^ - //p') + case "$line" in + "Available samples:") section="samples"; continue ;; + "Available services:") section="services"; continue ;; + esac + if [[ "$line" =~ ^[[:space:]]+-[[:space:]] ]]; then + id="$(echo "${line#*- }" | xargs)" + if [ "$section" = "services" ]; then + SERVICES+=("$id") + else + SAMPLES+=("$id") + fi + fi +done < <(node "$EXPORT_SCRIPT" --list) -if [ "${#SAMPLES[@]}" -eq 0 ]; then - echo "ERROR: No samples found." >&2 +if [ "${#SAMPLES[@]}" -eq 0 ] && [ "${#SERVICES[@]}" -eq 0 ]; then + echo "ERROR: No samples or services found." >&2 exit 1 fi @@ -52,34 +80,93 @@ for i in "${!SAMPLES[@]}"; do done echo "" -read -r -p "Enter sample number(s) to export (space/comma separated): " selection -selection="${selection//,/ }" +echo "Available services:" +NUM_SAMPLES="${#SAMPLES[@]}" +for i in "${!SERVICES[@]}"; do + printf " %2d) %s\n" "$((NUM_SAMPLES + i + 1))" "${SERVICES[$i]}" +done -CHOSEN=() -for n in $selection; do - if ! [[ "$n" =~ ^[0-9]+$ ]] || [ "$n" -lt 1 ] || [ "$n" -gt "${#SAMPLES[@]}" ]; then - echo "ERROR: Invalid selection '$n'." >&2 +NUM_SERVICES="${#SERVICES[@]}" +TOTAL=$((NUM_SAMPLES + NUM_SERVICES)) + +# Resolve a space/comma-separated selection (numbers continue past the sample +# list into the service list, or literal ids) into CHOSEN_SAMPLES / +# CHOSEN_SERVICES. Exits with an error on any unmatched token. +resolve_selection() { + local input="${1//,/ }" + local tok item + for tok in $input; do + if [[ "$tok" =~ ^[0-9]+$ ]] && [ "$tok" -ge 1 ] && [ "$tok" -le "$TOTAL" ]; then + if [ "$tok" -le "$NUM_SAMPLES" ]; then + CHOSEN_SAMPLES+=("${SAMPLES[$((tok - 1))]}") + else + CHOSEN_SERVICES+=("${SERVICES[$((tok - 1 - NUM_SAMPLES))]}") + fi + continue + fi + for item in "${SAMPLES[@]}"; do + if [ "$item" = "$tok" ]; then + CHOSEN_SAMPLES+=("$item") + continue 2 + fi + done + for item in "${SERVICES[@]}"; do + if [ "$item" = "$tok" ]; then + CHOSEN_SERVICES+=("$item") + continue 2 + fi + done + echo "ERROR: Invalid selection '$tok'." >&2 exit 1 - fi - CHOSEN+=("${SAMPLES[$((n - 1))]}") -done + done +} -if [ "${#CHOSEN[@]}" -eq 0 ]; then - echo "ERROR: No samples selected." >&2 +echo "" +read -r -p "Enter number(s) or name(s) to export (samples and/or services, space/comma separated, blank for none): " selection +CHOSEN_SAMPLES=() +CHOSEN_SERVICES=() +resolve_selection "$selection" + +# Drop duplicates (e.g. the same service picked once by number, once by name). +dedup() { + local -n _arr="$1" + local -A seen=() + local out=() x + for x in "${_arr[@]}"; do + if [ -z "${seen[$x]:-}" ]; then + seen[$x]=1 + out+=("$x") + fi + done + _arr=("${out[@]}") +} +dedup CHOSEN_SAMPLES +dedup CHOSEN_SERVICES + +if [ "${#CHOSEN_SAMPLES[@]}" -eq 0 ] && [ "${#CHOSEN_SERVICES[@]}" -eq 0 ]; then + echo "ERROR: Nothing selected - pick at least one sample or service." >&2 exit 1 fi -ARGS=("--samples=$(IFS=,; echo "${CHOSEN[*]}")") +ARGS=() +if [ "${#CHOSEN_SAMPLES[@]}" -gt 0 ]; then + ARGS+=("--samples=$(IFS=,; echo "${CHOSEN_SAMPLES[*]}")") +fi +if [ "${#CHOSEN_SERVICES[@]}" -gt 0 ]; then + ARGS+=("--services=$(IFS=,; echo "${CHOSEN_SERVICES[*]}")") +fi -read -r -p "Include optional service dependencies? [Y/n]: " opt -case "$opt" in n|N) ARGS+=("--no-optional") ;; esac +# Optional deps only come from selected samples; skip if none were chosen. +if [ "${#CHOSEN_SAMPLES[@]}" -gt 0 ]; then + read -r -p "Include optional service dependencies? [Y/n]: " opt + case "$opt" in n|N) ARGS+=("--no-optional") ;; esac +fi read -r -p "Output directory (blank for default): " outdir [ -n "$outdir" ] && ARGS+=("--out=$outdir") -read -r -p "Dry run (preview plan only)? [y/N]: " dry -case "$dry" in y|Y) ARGS+=("--dry-run") ;; esac +[ -n "$DRY_RUN_FLAG" ] && ARGS+=("$DRY_RUN_FLAG") echo "" -echo "Running: export-samples ${ARGS[*]}" +echo "Running: export-bundle ${ARGS[*]}" exec node "$EXPORT_SCRIPT" "${ARGS[@]}" diff --git a/samples/ai/edge-ai-demo-studio/frontend/eslint.config.mjs b/samples/ai/edge-ai-demo-studio/frontend/eslint.config.mjs index 71d0a1b1..4bf2c686 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/eslint.config.mjs +++ b/samples/ai/edge-ai-demo-studio/frontend/eslint.config.mjs @@ -18,7 +18,7 @@ const compat = new FlatCompat({ // // Optional service integrations are wired through the generated feature-provider // registry (`@/services/_generated/feature-providers`) so the export tool -// (scripts/export-samples.mjs) can prune their folders with `--no-optional`. +// (scripts/export-bundle.mjs) can prune their folders with `--no-optional`. // The export walks the *static* import graph and force-includes any service // folder it reaches — so a sample that statically imports a service it declares // `optional` drags that folder back in and silently defeats `--no-optional`. @@ -26,7 +26,7 @@ const compat = new FlatCompat({ // See docs/OPTIONAL-SERVICES.md. const SAMPLES_DIR = join(import.meta.dirname, "src", "samples"); -// Mirrors the dependency parser in scripts/export-samples.mjs (serviceId before +// Mirrors the dependency parser in scripts/export-bundle.mjs (serviceId before // role, no nested braces — matches the shape of ServiceDependency literals). const DEP_RE = /\{\s*serviceId\s*:\s*['"]([\w-]+)['"][^{}]*?role\s*:\s*['"](required|optional)['"][^{}]*?\}/gs; diff --git a/samples/ai/edge-ai-demo-studio/frontend/package.json b/samples/ai/edge-ai-demo-studio/frontend/package.json index f2122e79..58fdb17d 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/package.json +++ b/samples/ai/edge-ai-demo-studio/frontend/package.json @@ -11,7 +11,7 @@ "lint:fix": "eslint . --fix", "codegen": "node ./scripts/generate-registries.mjs && eslint . --fix", "db:reset": "node ./scripts/reset-db.mjs", - "export-samples": "node ../scripts/export-samples.mjs", + "export-bundle": "node ../scripts/export-bundle.mjs", "test": "playwright test", "test:ui": "playwright test --ui --ui-port 8081", "knip": "NODE_OPTIONS='--require tsconfig-paths/register' knip" diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/app/(dashboard)/samples/page.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/app/(dashboard)/samples/page.tsx index bdedb274..d7eb06aa 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/app/(dashboard)/samples/page.tsx +++ b/samples/ai/edge-ai-demo-studio/frontend/src/app/(dashboard)/samples/page.tsx @@ -6,7 +6,7 @@ import { CheckSquare, Download, Sparkles, X } from 'lucide-react' import { useMemo, useState } from 'react' import { createPortal } from 'react-dom' -import { ExportSamplesDialog } from '@/components/dashboard/samples/export-samples-dialog' +import { ExportBundleDialog } from '@/components/dashboard/samples/export-bundle-dialog' import { SampleCard } from '@/components/dashboard/samples/sample-card' import { type OSFilter, @@ -331,7 +331,7 @@ export default function SamplesPage() { document.body, )} - { }) } -/** POST /api/export-samples { samples, includeOptional } → zip download. */ +/** POST /api/export-bundle { samples, services, includeOptional } → zip download. */ export async function POST(request: NextRequest) { let bundleTmpDir: string | undefined try { const body = (await request.json().catch(() => ({}))) as { samples?: unknown + services?: unknown includeOptional?: unknown } - const sampleIds = parseSampleIds(body.samples) + const selection = parseExportSelection(body.samples, body.services) const includeOptional = body.includeOptional === true const { outDir, tmpDir } = await buildExportBundle( - sampleIds, + selection, includeOptional, ) bundleTmpDir = tmpDir - const fileName = exportFileName(sampleIds) + const fileName = exportFileName(selection) // Use the zip's base name (sans `.zip`) as the single wrapping folder. const rootDir = fileName.replace(/\.zip$/, '') const zip = await zipDirectory(outDir, rootDir) diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/app/api/services/text-generation/chat/route.ts b/samples/ai/edge-ai-demo-studio/frontend/src/app/api/services/text-generation/chat/route.ts index 49280c58..87e0616e 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/app/api/services/text-generation/chat/route.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/app/api/services/text-generation/chat/route.ts @@ -136,6 +136,7 @@ interface LipsyncConfig { sessionId: string voice: string speed: string + frameGeneration?: boolean } function dispatchSentenceToLipsync( @@ -158,6 +159,7 @@ function dispatchSentenceToLipsync( chat_type: 'echo', voice: lipsync.voice, speed: lipsync.speed, + frame_generation: lipsync.frameGeneration ?? false, ...(ttsUrl ? { tts_url: ttsUrl } : {}), }), }).catch((err) => { diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-samples-dialog.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-bundle-dialog.tsx similarity index 77% rename from samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-samples-dialog.tsx rename to samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-bundle-dialog.tsx index 500f4320..b3147719 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-samples-dialog.tsx +++ b/samples/ai/edge-ai-demo-studio/frontend/src/components/dashboard/samples/export-bundle-dialog.tsx @@ -35,13 +35,16 @@ interface ExportPlan { id: string deps: { serviceId: string; role: 'required' | 'optional' }[] }[] + requestedServices: string[] services: { required: string[]; optional: string[]; included: string[] } workers: string[] includeOptional: boolean } -interface ExportSamplesDialogProps { +interface ExportBundleDialogProps { sampleIds: string[] + /** Services to export directly — may be used alone with no samples. */ + serviceIds?: string[] open: boolean onOpenChange: (open: boolean) => void /** Called after a successful export download. */ @@ -56,12 +59,13 @@ function sampleTitle(id: string): string { return getSampleById(id)?.title ?? id } -export function ExportSamplesDialog({ +export function ExportBundleDialog({ sampleIds, + serviceIds = [], open, onOpenChange, onExported, -}: ExportSamplesDialogProps) { +}: ExportBundleDialogProps) { const [includeOptional, setIncludeOptional] = useState(false) const [plan, setPlan] = useState(null) const [planLoading, setPlanLoading] = useState(false) @@ -69,23 +73,26 @@ export function ExportSamplesDialog({ const [exporting, setExporting] = useState(false) const samplesKey = sampleIds.join(',') + const servicesKey = serviceIds.join(',') + const selectionCount = sampleIds.length + serviceIds.length // Resolve the plan whenever the dialog opens, the selection changes, or the // optional toggle flips — the API mirrors exactly what an export produces. useEffect(() => { - if (!open || sampleIds.length === 0) return + if (!open || selectionCount === 0) return const controller = new AbortController() const params = new URLSearchParams({ - samples: samplesKey, includeOptional: String(includeOptional), }) + if (samplesKey) params.set('samples', samplesKey) + if (servicesKey) params.set('services', servicesKey) const loadPlan = async () => { setPlanLoading(true) setPlanError(null) try { - const res = await fetch(`/api/export-samples?${params.toString()}`, { + const res = await fetch(`/api/export-bundle?${params.toString()}`, { signal: controller.signal, }) if (!res.ok) { @@ -107,15 +114,19 @@ export function ExportSamplesDialog({ loadPlan() return () => controller.abort() - }, [open, samplesKey, sampleIds.length, includeOptional]) + }, [open, samplesKey, servicesKey, selectionCount, includeOptional]) const handleExport = useCallback(async () => { setExporting(true) try { - const res = await fetch('/api/export-samples', { + const res = await fetch('/api/export-bundle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ samples: sampleIds, includeOptional }), + body: JSON.stringify({ + samples: sampleIds, + services: serviceIds, + includeOptional, + }), }) if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string } @@ -147,7 +158,7 @@ export function ExportSamplesDialog({ } finally { setExporting(false) } - }, [sampleIds, includeOptional, onExported, onOpenChange]) + }, [sampleIds, serviceIds, includeOptional, onExported, onOpenChange]) // Declared optional services that the resolved plan actually keeps. const declaredOptional = plan @@ -163,6 +174,22 @@ export function ExportSamplesDialog({ : [] const isSingle = sampleIds.length === 1 + const samplesOnly = serviceIds.length === 0 + const servicesOnly = sampleIds.length === 0 + + const title = servicesOnly + ? serviceIds.length === 1 + ? `Export “${serviceName(serviceIds[0])}”` + : `Export ${serviceIds.length} services` + : samplesOnly + ? isSingle + ? `Export “${sampleTitle(sampleIds[0])}”` + : `Export ${sampleIds.length} samples` + : `Export ${selectionCount} items` + + const description = servicesOnly + ? `Build a self-contained copy of Demo Studio with only the selected service${serviceIds.length === 1 ? '' : 's'} and no samples.` + : `Build a self-contained copy of Demo Studio with only the selected ${samplesOnly ? `sample${isSingle ? '' : 's'}` : 'samples and services'} and the services they depend on.` return ( @@ -175,37 +202,54 @@ export function ExportSamplesDialog({ - - {isSingle - ? `Export “${sampleTitle(sampleIds[0])}”` - : `Export ${sampleIds.length} samples`} - + {title} - Build a self-contained copy of Demo Studio with only the selected - sample{isSingle ? '' : 's'} and the services they depend on. + {description}
{/* Selected samples */} -
-

- - Sample{isSingle ? '' : 's'} - - ({sampleIds.length}) - -

-
- {sampleIds.map((id) => ( - - {sampleTitle(id)} - - ))} + {sampleIds.length > 0 && ( +
+

+ + Sample{isSingle ? '' : 's'} + + ({sampleIds.length}) + +

+
+ {sampleIds.map((id) => ( + + {sampleTitle(id)} + + ))} +
-
+ )} + + {/* Directly selected services */} + {serviceIds.length > 0 && ( +
+

+ + Service{serviceIds.length === 1 ? '' : 's'} + + ({serviceIds.length}) + +

+
+ {serviceIds.map((id) => ( + + {serviceName(id)} + + ))} +
+
+ )} {/* Optional toggle */}
- \\resources\\deployment.json`', + ) + lines.push('') + lines.push( + 'To change the presets of an already packaged build, edit that file and', + ) + lines.push( + 'restart the app; delete it to fall back to the built-in defaults. Setting', + ) + lines.push( + 'the `DEPLOYMENT_CONFIG_PATH` environment variable before launching the app', + ) + lines.push('overrides the bundled file.') + lines.push('') lines.push('## Example') lines.push('') lines.push('```json') diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/lib/export-samples.ts b/samples/ai/edge-ai-demo-studio/frontend/src/lib/export-bundle.ts similarity index 68% rename from samples/ai/edge-ai-demo-studio/frontend/src/lib/export-samples.ts rename to samples/ai/edge-ai-demo-studio/frontend/src/lib/export-bundle.ts index 33845dc5..be2e756e 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/lib/export-samples.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/lib/export-bundle.ts @@ -8,15 +8,15 @@ import path from 'node:path' import { WORKER_DIR } from '@/lib/constants' import { logger } from '@/lib/logger' -// The export tooling lives at /scripts/export-samples.mjs. WORKER_DIR +// The export tooling lives at /scripts/export-bundle.mjs. WORKER_DIR // is /workers, so the repo root is one level up — this mirrors how // the rest of the app resolves project paths (see lib/constants.ts). const REPO_ROOT = path.resolve(WORKER_DIR, '..') -const EXPORT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'export-samples.mjs') +const EXPORT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'export-bundle.mjs') -// Sample ids are folder names; keep the allow-list strict so nothing resembling -// a flag (e.g. `--out`) can be smuggled in as a "sample". -const SAMPLE_ID_RE = /^[a-z0-9][a-z0-9-]*$/ +// Sample/service ids are folder names; keep the allow-list strict so nothing +// resembling a flag (e.g. `--out`) can be smuggled in as an id. +const ENTITY_ID_RE = /^[a-z0-9][a-z0-9-]*$/ const PLAN_TIMEOUT_MS = 30_000 const EXPORT_TIMEOUT_MS = 5 * 60_000 @@ -28,11 +28,18 @@ export interface ExportSampleSummary { export interface ExportPlan { samples: ExportSampleSummary[] + /** Service ids the caller asked for directly (not derived from samples). */ + requestedServices: string[] services: { required: string[]; optional: string[]; included: string[] } workers: string[] includeOptional: boolean } +export interface ExportSelection { + sampleIds: string[] + serviceIds: string[] +} + export class ExportError extends Error { status: number constructor(message: string, status = 500) { @@ -42,8 +49,8 @@ export class ExportError extends Error { } } -/** Validate caller-supplied sample ids before they reach the spawned script. */ -export function parseSampleIds(raw: unknown): string[] { +/** Validate a caller-supplied id list; empty input yields an empty list. */ +function parseIdList(raw: unknown, kind: 'sample' | 'service'): string[] { const ids = Array.isArray(raw) ? raw : typeof raw === 'string' @@ -53,17 +60,31 @@ export function parseSampleIds(raw: unknown): string[] { .map((s) => (typeof s === 'string' ? s.trim() : '')) .filter(Boolean) - if (cleaned.length === 0) { - throw new ExportError('At least one sample id is required', 400) - } for (const id of cleaned) { - if (!SAMPLE_ID_RE.test(id)) { - throw new ExportError(`Invalid sample id: ${id}`, 400) + if (!ENTITY_ID_RE.test(id)) { + throw new ExportError(`Invalid ${kind} id: ${id}`, 400) } } return [...new Set(cleaned)] } +/** + * Validate caller-supplied sample/service ids before they reach the spawned + * script. Samples may be empty for a services-only export — the only invalid + * selection is one with neither samples nor services. + */ +export function parseExportSelection( + rawSamples: unknown, + rawServices: unknown, +): ExportSelection { + const sampleIds = parseIdList(rawSamples, 'sample') + const serviceIds = parseIdList(rawServices, 'service') + if (sampleIds.length === 0 && serviceIds.length === 0) { + throw new ExportError('At least one sample or service id is required', 400) + } + return { sampleIds, serviceIds } +} + interface SpawnResult { stdout: string stderr: string @@ -107,14 +128,22 @@ function runExportScript( }) } +/** Build the id-selection CLI args, omitting empty lists entirely. */ +function selectionArgs({ sampleIds, serviceIds }: ExportSelection): string[] { + const args: string[] = [] + if (sampleIds.length > 0) args.push(`--samples=${sampleIds.join(',')}`) + if (serviceIds.length > 0) args.push(`--services=${serviceIds.join(',')}`) + return args +} + /** Resolve the export plan (services, workers, samples) without writing files. */ export async function resolveExportPlan( - sampleIds: string[], + selection: ExportSelection, includeOptional: boolean, ): Promise { const { stdout, stderr, exitCode } = await runExportScript( [ - `--samples=${sampleIds.join(',')}`, + ...selectionArgs(selection), includeOptional ? '--include-optional' : '--no-optional', '--dry-run', '--json', @@ -143,7 +172,7 @@ export interface ExportBundle { /** Run a full export into a fresh temp directory. Caller must clean it up. */ export async function buildExportBundle( - sampleIds: string[], + selection: ExportSelection, includeOptional: boolean, ): Promise { const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'edge-ai-export-')) @@ -151,7 +180,7 @@ export async function buildExportBundle( const { stderr, exitCode } = await runExportScript( [ - `--samples=${sampleIds.join(',')}`, + ...selectionArgs(selection), includeOptional ? '--include-optional' : '--no-optional', `--out=${outDir}`, ], @@ -171,8 +200,11 @@ export async function cleanupBundle(tmpDir: string): Promise { } /** Build a safe download filename for the exported zip. */ -export function exportFileName(sampleIds: string[]): string { - const slug = sampleIds +export function exportFileName({ + sampleIds, + serviceIds, +}: ExportSelection): string { + const slug = [...sampleIds, ...serviceIds] .join('_') .replace(/[^a-z0-9_-]/gi, '') .slice(0, 80) diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/lib/port-checker.ts b/samples/ai/edge-ai-demo-studio/frontend/src/lib/port-checker.ts index 0facd1ca..c468b0d1 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/lib/port-checker.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/lib/port-checker.ts @@ -51,6 +51,8 @@ const ALLOWED_PROCESS_IDENTIFIERS = { 'python3.exe', 'node', 'node.exe', + 'ovms', + 'ovms.exe', ], scriptNames: ['main.py', 'uvicorn'], frameworkMarkers: ['fastapi'], diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/payload-types.ts b/samples/ai/edge-ai-demo-studio/frontend/src/payload-types.ts index a35596de..5d2bb58f 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/payload-types.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/payload-types.ts @@ -158,7 +158,9 @@ export interface Service { type: | 'diarization' | 'embeddings' + | 'face-recognition' | 'file-watcher' + | 'frame-generation' | 'geti-classifier' | 'image-based-video-search' | 'image-generation' diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/data.ts b/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/data.ts index 0f78382f..0f67416d 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/data.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/data.ts @@ -58,7 +58,20 @@ export const sample: Sample = { impactText: 'MCP tool integrations will not be available.', }, { serviceId: 'text-to-speech', role: 'required' }, - { serviceId: 'lipsync', role: 'required' }, + { + serviceId: 'lipsync', + role: 'required', + recommended: { + device: 'xpu', + }, + }, + { + serviceId: 'frame-generation', + role: 'optional', + capabilityKey: 'frame_generation', + impactText: + 'Lipsync frame generation (smoother avatar video on slower devices) will be unavailable.', + }, ], pipeline: [ 'wake-word-detection', diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/demo.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/demo.tsx index c21ea111..358d9e43 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/demo.tsx +++ b/samples/ai/edge-ai-demo-studio/frontend/src/samples/digital-avatar/demo.tsx @@ -58,6 +58,11 @@ export function DigitalAvatarDemo({ sample }: { sample: Sample }) { const lipsyncService = useGetService('lipsync') const ttsService = useGetService('text-to-speech') const textGenService = useGetService('text-generation') + // Optional service: when it is running, ask lipsync for frame generation + // on every utterance; the worker only interpolates when its inference + // cannot match the avatar frame rate on its own. + const frameGenService = useGetService('frame-generation') + const frameGenerationAvailable = frameGenService?.status === 'online' const isMultimodal = textGenService?.currentModelType === 'multimodal' const clientIceServerUrl = ( lipsyncService?.metadata as { clientIceServerUrl?: string } | undefined @@ -186,6 +191,7 @@ export function DigitalAvatarDemo({ sample }: { sample: Sample }) { voice: tts.values.voice, speed: String(tts.values.speed), ttsUrl, + frameGeneration: frameGenerationAvailable, }, } : {} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/docs.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/docs.ts index 16b2e601..bc97b9cd 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/docs.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/docs.ts @@ -11,6 +11,7 @@ export type DocsFactory = (opts: { import { getDocsData as diarizationDocs } from '../diarization/docs' import { getDocsData as embeddingsDocs } from '../embeddings/docs' +import { getDocsData as frameGenerationDocs } from '../frame-generation/docs' import { getDocsData as imageGenerationDocs } from '../image-generation/docs' import { getDocsData as lipsyncDocs } from '../lipsync/docs' import { getDocsData as mcpDocs } from '../mcp/docs' @@ -26,6 +27,7 @@ import { getDocsData as wakeWordDetectionDocs } from '../wake-word-detection/doc export const docsRegistry: Record = { diarization: diarizationDocs, embeddings: embeddingsDocs, + 'frame-generation': frameGenerationDocs, 'image-generation': imageGenerationDocs, lipsync: lipsyncDocs, mcp: mcpDocs, diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/meta.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/meta.ts index e6530efc..ec16031c 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/meta.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/meta.ts @@ -4,7 +4,9 @@ import { service as diarizationMeta } from '../diarization/data' import { service as embeddingsMeta } from '../embeddings/data' +import { service as faceRecognitionMeta } from '../face-recognition/data' import { service as fileWatcherMeta } from '../file-watcher/data' +import { service as frameGenerationMeta } from '../frame-generation/data' import { service as getiClassifierMeta } from '../geti-classifier/data' import { service as imageBasedVideoSearchMeta } from '../suites/metro-ai-suite/image-based-video-search/data' import { service as imageGenerationMeta } from '../image-generation/data' @@ -28,7 +30,9 @@ import { service as wakeWordDetectionMeta } from '../wake-word-detection/data' export const metaMap = { diarization: diarizationMeta, embeddings: embeddingsMeta, + 'face-recognition': faceRecognitionMeta, 'file-watcher': fileWatcherMeta, + 'frame-generation': frameGenerationMeta, 'geti-classifier': getiClassifierMeta, 'image-based-video-search': imageBasedVideoSearchMeta, 'image-generation': imageGenerationMeta, diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/services.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/services.ts index 6902861a..f3abee86 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/services.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/services.ts @@ -7,7 +7,9 @@ import type { Service as ServiceType } from '@/payload-types' // Data imports import { service as diarizationMeta } from '../diarization/data' import { service as embeddingsMeta } from '../embeddings/data' +import { service as faceRecognitionMeta } from '../face-recognition/data' import { service as fileWatcherMeta } from '../file-watcher/data' +import { service as frameGenerationMeta } from '../frame-generation/data' import { service as getiClassifierMeta } from '../geti-classifier/data' import { service as imageBasedVideoSearchMeta } from '../suites/metro-ai-suite/image-based-video-search/data' import { service as imageGenerationMeta } from '../image-generation/data' @@ -30,7 +32,9 @@ import { service as wakeWordDetectionMeta } from '../wake-word-detection/data' // Demo imports import { DiarizationDemo } from '../diarization/demo' import { EmbeddingDemo } from '../embeddings/demo' +import { FaceRecognitionDemo } from '../face-recognition/demo' import { FileWatcherDemo } from '../file-watcher/demo' +import { FrameGenerationDemo } from '../frame-generation/demo' import { ImageGenerationDemo } from '../image-generation/demo' import { LipsyncDemo } from '../lipsync/demo' import { McpDemo } from '../mcp/demo' @@ -56,11 +60,21 @@ export const serviceMap: Record = { status: 'offline', demo: EmbeddingDemo, }, + 'face-recognition': { + ...faceRecognitionMeta, + status: 'offline', + demo: FaceRecognitionDemo, + }, 'file-watcher': { ...fileWatcherMeta, status: 'offline', demo: FileWatcherDemo, }, + 'frame-generation': { + ...frameGenerationMeta, + status: 'offline', + demo: FrameGenerationDemo, + }, 'geti-classifier': { ...getiClassifierMeta, status: 'offline', diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/workers.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/workers.ts index 78cab454..af539158 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/workers.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/_generated/workers.ts @@ -6,7 +6,9 @@ import type { Service } from '@/payload-types' import type { WorkerConfig } from '../types' import { worker as diarizationWorker } from '../diarization/data' +import { worker as faceRecognitionWorker } from '../face-recognition/data' import { worker as fileWatcherWorker } from '../file-watcher/data' +import { worker as frameGenerationWorker } from '../frame-generation/data' import { worker as getiClassifierWorker } from '../geti-classifier/data' import { worker as imageBasedVideoSearchWorker } from '../suites/metro-ai-suite/image-based-video-search/data' import { worker as imageGenerationWorker } from '../image-generation/data' @@ -26,7 +28,9 @@ import { worker as wakeWordDetectionWorker } from '../wake-word-detection/data' /** Worker configuration registry keyed by Payload service type. */ const workerRegistry: Partial> = { diarization: diarizationWorker, + 'face-recognition': faceRecognitionWorker, 'file-watcher': fileWatcherWorker, + 'frame-generation': frameGenerationWorker, 'geti-classifier': getiClassifierWorker, 'image-based-video-search': imageBasedVideoSearchWorker, 'image-generation': imageGenerationWorker, diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-gallery-panel.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-gallery-panel.tsx new file mode 100644 index 00000000..acbc988a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-gallery-panel.tsx @@ -0,0 +1,306 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { + AlertCircle, + ImagePlus, + Loader2, + Trash2, + UserPlus, + Users, + X, +} from 'lucide-react' +import Image from 'next/image' +import { useCallback, useRef, useState } from 'react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Separator } from '@/components/ui/separator' +import { useImagePreviewUrl } from '@/hooks/use-image-preview-url' +import { cn } from '@/lib/utils' +import { + useClearGallery, + useDeletePerson, + useEnroll, + useGallery, + type EnrollFileStatus, +} from '../hooks' + +function PersonThumbnail({ + url, + alt, + className, +}: { + url: string + alt: string + className?: string +}) { + const { previewUrl } = useImagePreviewUrl(url) + if (!previewUrl) return null + return ( + {alt} + ) +} + +const ACCEPT = '.jpg,.jpeg,.png,.bmp,.tiff,.webp' +const MAX_BYTES = 10 * 1024 * 1024 + +/** Reference-gallery panel: enroll persons and manage who is enrolled. */ +export function FaceGalleryPanel({ isOnline }: { isOnline: boolean }) { + const gallery = useGallery(isOnline) + const enroll = useEnroll() + const deletePerson = useDeletePerson() + const clearGallery = useClearGallery() + + const [name, setName] = useState('') + const [pending, setPending] = useState([]) + const [fileError, setFileError] = useState(null) + const [isDragging, setIsDragging] = useState(false) + const [lastEnrollFiles, setLastEnrollFiles] = useState([]) + const inputRef = useRef(null) + + const addFiles = useCallback((incoming: FileList | File[]) => { + setFileError(null) + const accepted: File[] = [] + for (const file of Array.from(incoming)) { + if (!file.type.startsWith('image/')) { + setFileError(`${file.name}: not an image file`) + continue + } + if (file.size > MAX_BYTES) { + setFileError(`${file.name}: larger than 10MB`) + continue + } + accepted.push(file) + } + if (accepted.length) setPending((prev) => [...prev, ...accepted]) + }, []) + + const handleEnroll = () => { + if (!name.trim() || pending.length === 0) return + enroll.mutate( + { name: name.trim(), files: pending }, + { + onSuccess: (data) => { + setName('') + setPending([]) + setLastEnrollFiles(data.files.filter((f) => f.error)) + }, + }, + ) + } + + const persons = gallery.data?.persons ?? [] + + return ( + + + + + Reference Gallery + + + Enroll each person with one or more reference photos. The largest face + in each photo is embedded by the active model. + + + + {/* ── Enroll form ──────────────────────────────────────────────── */} +
+ + setName(e.target.value)} + disabled={!isOnline || enroll.isPending} + /> +
+ +
{ + e.preventDefault() + setIsDragging(true) + }} + onDragOver={(e) => e.preventDefault()} + onDragLeave={(e) => { + e.preventDefault() + setIsDragging(false) + }} + onDrop={(e) => { + e.preventDefault() + setIsDragging(false) + addFiles(e.dataTransfer.files) + }} + onClick={() => inputRef.current?.click()} + className={cn( + 'cursor-pointer rounded-lg border-2 border-dashed p-4 text-center transition-colors', + isDragging + ? 'border-primary bg-primary/5' + : 'border-muted-foreground/25 hover:border-primary/50 hover:bg-muted/50', + (!isOnline || enroll.isPending) && 'pointer-events-none opacity-50', + )} + > + { + if (e.target.files) addFiles(e.target.files) + e.target.value = '' + }} + /> + +

Drop reference photo(s)

+

+ or click to browse — multiple images improve matching +

+
+ + {pending.length > 0 && ( +
+ {pending.map((file, i) => ( + + {file.name} + + + ))} +
+ )} + + {(fileError || enroll.error) && ( +

+ + {fileError ?? enroll.error?.message} +

+ )} + {lastEnrollFiles.map((f) => ( +

+ + {f.file}: {f.error} +

+ ))} + + + + + + {/* ── Enrolled persons ─────────────────────────────────────────── */} +
+

Enrolled ({persons.length})

+ {persons.length > 0 && ( + + )} +
+ {persons.length === 0 ? ( +

+ No one enrolled yet. Faces in the probe image will show as + “Unknown”. +

+ ) : ( +
    + {persons.map((person) => ( +
  • +
    + {person.thumbnails.slice(0, 3).map((thumb, i) => ( + + ))} +
    +
    +

    {person.name}

    +

    + {person.num_images} image + {person.num_images === 1 ? '' : 's'} +

    +
    + +
  • + ))} +
+ )} +
+
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-image-overlay.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-image-overlay.tsx new file mode 100644 index 00000000..28733dd7 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-image-overlay.tsx @@ -0,0 +1,82 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import type { RecognizedFace } from '../hooks' +import { RefImage } from './ref-image' + +interface FaceImageOverlayProps { + imageUrl: string + naturalWidth: number + naturalHeight: number + faces: RecognizedFace[] +} + +/** Probe image with detected face boxes, landmarks and match labels. */ +export function FaceImageOverlay({ + imageUrl, + naturalWidth, + naturalHeight, + faces, +}: FaceImageOverlayProps) { + const stroke = Math.max(2, Math.max(naturalWidth, naturalHeight) / 400) + const fontSize = Math.max(11, Math.max(naturalWidth, naturalHeight) / 40) + + return ( +
+ + {faces.length > 0 && naturalWidth > 0 && naturalHeight > 0 && ( + + {faces.map((face, faceIndex) => { + const [x, y, w, h] = face.box + const label = face.match + ? `${face.matched ? face.match.name : 'Unknown'} ${face.match.similarity.toFixed(2)}` + : 'No gallery' + return ( + + + {face.landmarks.map(([lx, ly], i) => ( + + ))} + + {label} + + + ) + })} + + )} +
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-input-panel.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-input-panel.tsx new file mode 100644 index 00000000..6f0d89e4 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-input-panel.tsx @@ -0,0 +1,234 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { + Camera, + FileImage, + ImageUp, + Loader2, + RotateCcw, + ScanFace, +} from 'lucide-react' +import { useCallback } from 'react' +import { FileDropZone } from '@/components/common/file-drop-zone' +import { WebcamStream } from '@/components/common/webcam-stream' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip' +import type { useWebcamStream } from '@/hooks/use-webcam-stream' +import type { RecognizedFace } from '../hooks' +import { FaceImageOverlay } from './face-image-overlay' + +export type FaceInputMode = 'upload' | 'webcam' + +export interface FaceSource { + url: string + file: File + naturalWidth: number + naturalHeight: number +} + +const ACCEPT = '.jpg,.jpeg,.png,.bmp,.tiff,.webp' +const MAX_BYTES = 10 * 1024 * 1024 + +function validateImage(file: File): string | null { + if (!file.type.startsWith('image/')) return 'Please choose an image file.' + if (file.size > MAX_BYTES) return 'Image is larger than 10MB.' + return null +} + +/** Load an image File/URL and resolve its natural pixel dimensions. */ +function loadImageSize( + url: string, +): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => + resolve({ width: img.naturalWidth, height: img.naturalHeight }) + img.onerror = () => reject(new Error('Could not read image')) + img.src = url + }) +} + +function dataUrlToFile(dataUrl: string, filename: string): File { + const [header, base64] = dataUrl.split(',') + const mime = header.match(/:(.*?);/)?.[1] ?? 'image/png' + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + return new File([bytes], filename, { type: mime }) +} + +interface FaceInputPanelProps { + inputMode: FaceInputMode + onInputModeChange: (mode: FaceInputMode) => void + source: FaceSource | null + onSource: (source: FaceSource | null) => void + webcam: ReturnType + faces: RecognizedFace[] + onRun: () => void + isRunning: boolean +} + +/** Probe-image panel: upload or webcam capture, then run recognition. */ +export function FaceInputPanel({ + inputMode, + onInputModeChange, + source, + onSource, + webcam, + faces, + onRun, + isRunning, +}: FaceInputPanelProps) { + const handleFile = useCallback( + async (file: File | null) => { + if (!file) { + onSource(null) + return + } + const url = URL.createObjectURL(file) + const { width, height } = await loadImageSize(url) + onSource({ url, file, naturalWidth: width, naturalHeight: height }) + }, + [onSource], + ) + + const handleCapture = useCallback(async () => { + const dataUrl = webcam.captureImage() + if (!dataUrl) return + const file = dataUrlToFile(dataUrl, `face-capture-${Date.now()}.png`) + const { width, height } = await loadImageSize(dataUrl) + onSource({ url: dataUrl, file, naturalWidth: width, naturalHeight: height }) + }, [webcam, onSource]) + + const overlay = source && ( + + ) + + return ( +
+ { + onInputModeChange(v as FaceInputMode) + onSource(null) + }} + > + + + + Upload Image + + + + Webcam + + + + + {overlay ?? ( + + )} + + + + {overlay ?? ( +
+ +
+ )} + + {!source && ( + + )} +
+
+ + {source && ( +
+ {inputMode === 'webcam' ? ( + + + Captured frame + + ) : ( + + + + + {source.file.name} + + + {source.file.name} + + )} +
+ + +
+
+ )} +
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-results-panel.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-results-panel.tsx new file mode 100644 index 00000000..309e1c6f --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/face-results-panel.tsx @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { AlertCircle, Loader2, ScanFace } from 'lucide-react' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import type { RecognizeResult } from '../hooks' + +interface FaceResultsPanelProps { + result: RecognizeResult | null + isRunning: boolean + error: Error | null +} + +export function FaceResultsPanel({ + result, + isRunning, + error, +}: FaceResultsPanelProps) { + if (isRunning) { + return ( +
+ + Running recognition… +
+ ) + } + if (error) { + return ( +
+

+ + {error.message} +

+
+ ) + } + if (!result) { + return ( +
+ + Recognition results will appear here. +
+ ) + } + + return ( + + + + {result.label} + + {result.runtime} + + +

+ detect {result.detect_ms.toFixed(1)} ms · embed{' '} + {result.embed_ms.toFixed(1)} ms · threshold ≥ {result.threshold} ·{' '} + {result.gallery_size} enrolled person + {result.gallery_size === 1 ? '' : 's'} +

+
+ + {result.faces.length === 0 ? ( +

No faces detected.

+ ) : ( + + + + # + Best match + Similarity + Verdict + + + + {result.faces.map((face, i) => ( + + + {i + 1} + + +

+ {face.match?.name ?? '—'} +

+ {face.similarities.length > 1 && ( +

+ {face.similarities + .slice(1, 4) + .map((s) => `${s.name} ${s.similarity.toFixed(2)}`) + .join(' · ')} +

+ )} +
+ + {face.match ? face.match.similarity.toFixed(4) : '—'} + + + {face.match ? ( + + {face.matched ? face.match.name : 'Unknown'} + + ) : ( + No gallery + )} + +
+ ))} +
+
+ )} +
+
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/ref-image.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/ref-image.tsx new file mode 100644 index 00000000..b3d3fdff --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/components/ref-image.tsx @@ -0,0 +1,34 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { useEffect, useRef, type CSSProperties } from 'react' + +/** + * Image whose src (an object URL or worker-provided data URL) is assigned + * imperatively via ref instead of a dynamic src prop, matching the pattern + * used by FileDropZone's preview (keeps static analysers happy and revocation + * Strict Mode-safe). + */ +export function RefImage({ + src, + alt, + className, + style, +}: { + src: string + alt: string + className?: string + style?: CSSProperties +}) { + const imgRef = useRef(null) + + useEffect(() => { + const img = imgRef.current + if (img) img.src = src + }, [src]) + + // eslint-disable-next-line @next/next/no-img-element -- src is a local object URL / data URL assigned imperatively; next/image cannot optimize these + return {alt} +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/config.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/config.ts new file mode 100644 index 00000000..b0859be7 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/config.ts @@ -0,0 +1,34 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import type { ServiceConfig } from '@/services/types' + +const OPENVINO_DEVICES = ['CPU', 'GPU'] + +export const serviceConfig: ServiceConfig = { + availableModels: [ + { + value: 'omz-retail', + label: + 'OMZ face-detection-retail-0004 + landmarks-0009 + reid-0095, 256-d', + backend: 'openvino', + availableDevices: OPENVINO_DEVICES, + }, + { + value: 'omz-adas', + label: 'OMZ face-detection-adas-0001 + landmarks-0009 + reid-0095, 256-d', + backend: 'openvino', + availableDevices: OPENVINO_DEVICES, + }, + { + value: 'yunet-sface', + label: 'YuNet + SFace — OpenCV Zoo, 128-d (OpenVINO)', + backend: 'openvino', + availableDevices: OPENVINO_DEVICES, + }, + ], + availableDevices: OPENVINO_DEVICES, + // Models are fixed registry presets, not arbitrary model IDs, so the + // "Custom model" input in the configure panel is disabled. + supportsCustomModel: false, +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/data.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/data.ts new file mode 100644 index 00000000..4df77fe0 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/data.ts @@ -0,0 +1,44 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { ScanFace } from 'lucide-react' +import type { Service as PayloadService } from '@/payload-types' +import type { ServiceMeta, WorkerConfig } from '@/services/types' +import { serviceConfig } from './config' + +const DEFAULT_MODEL = 'omz-retail' +const DEFAULT_DEVICE = 'CPU' + +export const service: ServiceMeta = { + id: 'face-recognition', + name: 'Face Recognition', + description: + 'Detect and identify faces against a reference gallery using Open Model Zoo or OpenCV Zoo pipelines.', + longDescription: + 'Face recognition service with selectable pipelines on OpenVINO. The Open Model Zoo presets follow the face_recognition_demo chain — an SSD face detector (retail-0004 or adas-0001), landmarks-regression-retail-0009 for five-point alignment and face-reidentification-retail-0095 for 256-d descriptors; the OpenCV Zoo preset pairs YuNet detection with SFace 128-d embeddings. Enroll people with one or more reference images, then identify faces in uploads or webcam captures; the gallery is re-embedded automatically when the model is switched.', + icon: ScanFace, + port: 8031, + supportedOS: ['linux', 'windows'], + execution: { mode: 'worker' }, + defaultModel: { + name: DEFAULT_MODEL, + device: DEFAULT_DEVICE, + }, + config: serviceConfig, + logSources: [ + { type: 'service', label: 'face-recognition', target: 'face-recognition' }, + ], + healthCheck: { + url: '/healthcheck', + }, +} + +export const worker: WorkerConfig = { + modelDirectories: ['models/face-recognition'], + workerSubDir: 'face-recognition', + buildArgs: (doc: PayloadService) => { + const model = doc.models?.default?.name ?? DEFAULT_MODEL + const device = doc.models?.default?.device ?? DEFAULT_DEVICE + return ['--port', String(doc.port), '--model', model, '--device', device] + }, +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/demo.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/demo.tsx new file mode 100644 index 00000000..fd727b0e --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/demo.tsx @@ -0,0 +1,92 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { ServerOff } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useServiceLiveStatus } from '@/context/service-status-context' +import { useWebcamStream } from '@/hooks/use-webcam-stream' +import type { Service } from '@/services/types' +import { FaceGalleryPanel } from './components/face-gallery-panel' +import { + FaceInputPanel, + type FaceInputMode, + type FaceSource, +} from './components/face-input-panel' +import { FaceResultsPanel } from './components/face-results-panel' +import { useRecognize } from './hooks' + +export function FaceRecognitionDemo(_props: { service: Service }) { + const status = useServiceLiveStatus('face-recognition') + const isOnline = status === 'online' + + const webcam = useWebcamStream() + const recognize = useRecognize() + + const [inputMode, setInputMode] = useState('upload') + const [source, setSource] = useState(null) + + // Revoke the previous object URL when the source changes or the demo + // unmounts, so repeated uploads/captures don't leak blob: URLs. + useEffect(() => { + const url = source?.url + return () => { + if (url) URL.revokeObjectURL(url) + } + }, [source]) + + const handleSource = (next: FaceSource | null) => { + setSource(next) + recognize.reset() + } + + const run = () => { + if (!source) return + recognize.mutate(source.file) + } + + // ── Service offline gate ─────────────────────────────────────────────────── + if (!isOnline) { + return ( +
+
+
+ +
+

+ Face recognition service offline +

+

+ Start the face-recognition worker to enroll and identify faces. +

+
+
+ ) + } + + return ( +
+
+ +
+ + +
+
+
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/index.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/index.ts new file mode 100644 index 00000000..e7a5bfa3 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/index.ts @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +export * from './use-face-recognition' diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/use-face-recognition.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/use-face-recognition.ts new file mode 100644 index 00000000..8dc83a2e --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/face-recognition/hooks/use-face-recognition.ts @@ -0,0 +1,178 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { + useMutation, + useQuery, + useQueryClient, + type UseMutationOptions, +} from '@tanstack/react-query' + +// ── Types (mirror workers/face-recognition responses) ─────────────────────── + +export interface FaceSimilarity { + person_id: string + name: string + similarity: number +} + +/** One detected face with its gallery match. `box` is [x, y, w, h] pixels. */ +export interface RecognizedFace { + box: [number, number, number, number] + score: number + landmarks: [number, number][] + /** Best gallery candidate (null when the gallery is empty). */ + match: FaceSimilarity | null + /** True when the best similarity clears the pipeline's threshold. */ + matched: boolean + /** All gallery candidates, sorted by similarity descending. */ + similarities: FaceSimilarity[] +} + +export interface RecognizeResult { + elapsed_ms: number + image: { width: number; height: number } + gallery_size: number + model: string + label: string + runtime: string + threshold: number + detect_ms: number + embed_ms: number + num_faces: number + faces: RecognizedFace[] +} + +export interface GalleryPerson { + id: string + name: string + num_images: number + /** Base64 JPEG data URLs of the enrolled face crops. */ + thumbnails: string[] +} + +export interface GalleryResponse { + persons: GalleryPerson[] +} + +export interface EnrollRequest { + name: string + files: File[] +} + +export interface EnrollFileStatus { + file: string + error?: string +} + +export interface EnrollResponse { + person: GalleryPerson + files: EnrollFileStatus[] +} + +// ── Fetchers ──────────────────────────────────────────────────────────────── + +const BASE = '/api/face-recognition' + +async function parseOrThrow(res: Response, fallback: string): Promise { + const data = (await res.json()) as T & { detail?: string; error?: string } + if (!res.ok) { + throw new Error(data.detail ?? data.error ?? fallback) + } + return data +} + +export async function fetchGallery(): Promise { + const url = new URL(`${BASE}/gallery`, window.location.origin) + const res = await fetch(url) + return parseOrThrow(res, 'Failed to load gallery') +} + +export async function enrollPerson( + req: EnrollRequest, +): Promise { + const formData = new FormData() + formData.append('name', req.name) + for (const file of req.files) formData.append('files', file) + const url = new URL(`${BASE}/gallery`, window.location.origin) + const res = await fetch(url, { method: 'POST', body: formData }) + return parseOrThrow(res, 'Failed to enroll person') +} + +export async function deletePerson(personId: string): Promise { + const url = new URL(`${BASE}/gallery/${personId}`, window.location.origin) + const res = await fetch(url, { method: 'DELETE' }) + return parseOrThrow(res, 'Failed to delete person') +} + +export async function clearGallery(): Promise { + const url = new URL(`${BASE}/gallery`, window.location.origin) + const res = await fetch(url, { method: 'DELETE' }) + return parseOrThrow(res, 'Failed to clear gallery') +} + +export async function recognizeImage(file: File): Promise { + const formData = new FormData() + formData.append('file', file) + const url = new URL(`${BASE}/recognize`, window.location.origin) + const res = await fetch(url, { + method: 'POST', + body: formData, + }) + return parseOrThrow(res, 'Recognition request failed') +} + +// ── Hooks ─────────────────────────────────────────────────────────────────── + +const GALLERY_KEY = ['face-recognition', 'gallery'] + +export function useGallery(enabled = true) { + return useQuery({ + queryKey: GALLERY_KEY, + queryFn: fetchGallery, + enabled, + }) +} + +/** Invalidates the gallery query after any gallery mutation settles. */ +function useGalleryMutation( + mutationFn: (variables: TVariables) => Promise, + options?: UseMutationOptions, +) { + const queryClient = useQueryClient() + return useMutation({ + mutationFn, + ...options, + onSettled: (...args) => { + queryClient.invalidateQueries({ queryKey: GALLERY_KEY }) + options?.onSettled?.(...args) + }, + }) +} + +export function useEnroll( + options?: UseMutationOptions, +) { + return useGalleryMutation(enrollPerson, options) +} + +export function useDeletePerson( + options?: UseMutationOptions, +) { + return useGalleryMutation(deletePerson, options) +} + +export function useClearGallery( + options?: UseMutationOptions, +) { + return useGalleryMutation(() => clearGallery(), options) +} + +export function useRecognize( + options?: UseMutationOptions, +) { + return useMutation({ + mutationFn: recognizeImage, + ...options, + }) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/config.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/config.ts new file mode 100644 index 00000000..1e41f056 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/config.ts @@ -0,0 +1,22 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import type { ServiceConfig } from '@/services/types' + +// NPU is omitted: the NPU compiler cannot fit RIFE's GridSample warp in CMX at +// full-frame resolutions, so interpolation hangs at compile time on real inputs. +const FRAME_GENERATION_DEVICES = ['CPU', 'GPU'] + +export const serviceConfig: ServiceConfig = { + supportsCustomModel: false, + availableModels: [ + { + value: 'RIFE', + label: 'RIFE', + availableDevices: FRAME_GENERATION_DEVICES, + backend: 'openvino', + }, + ], + availableDevices: FRAME_GENERATION_DEVICES, + availableModelSources: [{ value: 'huggingface', label: 'Hugging Face' }], +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/data.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/data.ts new file mode 100644 index 00000000..ddd41e8e --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/data.ts @@ -0,0 +1,44 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Film } from 'lucide-react' +import type { Service as PayloadService } from '@/payload-types' +import type { ServiceMeta, WorkerConfig } from '@/services/types' +import { serviceConfig } from './config' + +export const service: ServiceMeta = { + id: 'frame-generation', + name: 'Frame Generation', + description: + 'RIFE video frame interpolation: fill in-between frames or upscale video FPS.', + longDescription: + 'AI-powered frame generation service using the RIFE interpolation model on OpenVINO. Generates intermediate frames between keyframe pairs — used by the Lipsync service to reach the avatar frame rate on slower accelerators — and upscales the frame rate of uploaded videos (2x-4x). The interpolation device (CPU/GPU) is configured on this service.', + icon: Film, + port: 8031, + supportedOS: ['linux', 'windows'], + execution: { mode: 'worker' }, + defaultModel: { + name: 'RIFE', + device: 'CPU', + }, + config: serviceConfig, + logSources: [ + { type: 'service', label: 'frame-generation', target: 'frame-generation' }, + ], + healthCheck: { + url: '/healthcheck', + }, +} + +export const worker: WorkerConfig = { + buildArgs: (doc: PayloadService) => [ + '--port', + String(doc.port), + '--device', + doc.models?.default?.device ?? 'CPU', + '--source', + doc.models?.default?.source || 'huggingface', + ], + workerSubDir: 'frame-generation', + modelDirectories: ['models/rife'], +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/demo.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/demo.tsx new file mode 100644 index 00000000..3a2b6d2f --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/demo.tsx @@ -0,0 +1,345 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +'use client' + +import { Download, Film, Loader2, Upload, X } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + type InterpolationMode, + useInterpolateVideo, + useResultVideo, + useVideoTaskStatus, +} from './hooks' + +const MULTIPLIER_OPTIONS = ['2', '3', '4'] + +const MODE_OPTIONS: { value: InterpolationMode; label: string }[] = [ + { value: 'fps', label: 'Higher FPS (same duration)' }, + { value: 'slowmo', label: 'Slow motion (same FPS, longer)' }, +] + +export function FrameGenerationDemo() { + const fileInputRef = useRef(null) + // Video srcs are assigned imperatively via refs (not a src prop) so static + // analysis can verify no attacker-controlled URL reaches the DOM. + const sourceVideoRef = useRef(null) + const resultVideoRef = useRef(null) + const [videoFile, setVideoFile] = useState(null) + const [multiplier, setMultiplier] = useState('2') + const [mode, setMode] = useState('fps') + const [taskId, setTaskId] = useState(null) + + const { mutate: interpolate, isPending: isUploading } = useInterpolateVideo() + const { + data: task, + error: taskStatusError, + isError: isTaskStatusError, + } = useVideoTaskStatus(taskId) + + const isQueued = !!taskId && task?.status === 'queued' + const isProcessing = + !!taskId && + !isTaskStatusError && + (!task || isQueued || task.status === 'running') + const isFinished = !!taskId && task?.status === 'finished' + const isError = + !!taskId && + (isTaskStatusError || + task?.status === 'error' || + task?.status === 'not_found') + const errorDetail = isTaskStatusError + ? taskStatusError instanceof Error + ? taskStatusError.message + : 'Unable to check interpolation status' + : task?.status === 'not_found' + ? 'The interpolation task was not found' + : (task?.detail ?? 'unknown error') + const progress = Math.round((task?.progress ?? 0) * 100) + + const { + data: resultBlob, + isLoading: isResultLoading, + isError: isResultError, + } = useResultVideo(taskId, isFinished) + + useEffect(() => { + const video = sourceVideoRef.current + if (!video || !videoFile) return + const url = URL.createObjectURL(videoFile) + video.src = url + return () => { + video.removeAttribute('src') + URL.revokeObjectURL(url) + } + }, [videoFile]) + + useEffect(() => { + const video = resultVideoRef.current + if (!video || !resultBlob) return + const url = URL.createObjectURL(resultBlob) + video.src = url + return () => { + video.removeAttribute('src') + URL.revokeObjectURL(url) + } + }, [resultBlob]) + + const handleFileChange = useCallback((file: File | null) => { + setVideoFile(file) + setTaskId(null) + }, []) + + const handleSubmit = useCallback(() => { + if (!videoFile) return + setTaskId(null) + interpolate( + { videoFile, multiplier: Number(multiplier), mode }, + { + onSuccess: ({ taskId: id }) => setTaskId(id), + onError: (e) => + toast.error( + e instanceof Error ? e.message : 'Failed to start interpolation', + ), + }, + ) + }, [videoFile, multiplier, mode, interpolate]) + + const handleDownload = useCallback(() => { + if (!taskId || !resultBlob) return + const url = URL.createObjectURL(resultBlob) + const link = document.createElement('a') + link.href = url + link.download = `interpolated_${taskId}.mp4` + link.click() + URL.revokeObjectURL(url) + }, [taskId, resultBlob]) + + return ( +
+

+ Interpolate a video with RIFE: generate intermediate frames between + every pair of consecutive frames, either to raise the frame rate or to + create a smooth slow-motion version. Jobs are processed one at a time. +

+ +
+
+

+ Source video +

+ + {videoFile ? ( +
+
+ ) : ( + + )} + handleFileChange(e.target.files?.[0] ?? null)} + /> + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ Result +

+ + {isQueued && ( +

+ Waiting in queue + {task?.position ? ` (position ${task.position})` : ''}... +

+ )} + + {isProcessing && !isQueued && ( +
+
+
+
+

+ Generating frames... {progress}% +

+
+ )} + + {isError && ( +

+ Interpolation failed: {errorDetail} +

+ )} + + {isFinished && taskId && isResultLoading && ( +

+ + Loading result... +

+ )} + + {isFinished && taskId && isResultError && ( +

+ Failed to load the interpolated video. +

+ )} + + {isFinished && taskId && resultBlob && ( +
+
+ )} + + {!taskId && ( +

+ The interpolated video will appear here. +

+ )} +
+
+
+ ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/docs.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/docs.ts new file mode 100644 index 00000000..99b1e31e --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/docs.ts @@ -0,0 +1,132 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import type { ServiceDocsData } from '@/services/types' + +export const getDocsData = ({ host }: { host: string }): ServiceDocsData => ({ + overview: + 'The Frame Generation service interpolates video frames with the RIFE model on OpenVINO. It fills the frames between keyframe pairs (used by the Lipsync service to reach the avatar frame rate on slower accelerators) and upscales the frame rate of uploaded videos by an integer multiplier.', + endpoints: [ + { + method: 'POST', + path: '/v1/frame-generation/interpolate', + description: + 'Fill the frames between keyframe pairs. Body is an uncompressed .npz (application/octet-stream) with frames_a [N,H,W,3] uint8, frames_b [N,H,W,3] uint8 and counts [N] int; the response is an .npz with arrays gap_0..gap_{N-1}.', + }, + { + method: 'POST', + path: '/v1/frame-generation/benchmark', + description: + 'Measure interpolation throughput (frames/sec) for a gap schedule; also warms lazily-compiled shapes for that schedule', + params: [ + { + name: 'image_size', + type: 'number', + required: false, + desc: 'Square frame size to benchmark with (default: 256)', + }, + { + name: 'gap_sizes', + type: 'number[]', + required: false, + desc: 'Frames to generate per gap, e.g. [1, 1, 1] (default: [1])', + }, + { + name: 'rounds', + type: 'number', + required: false, + desc: 'Timed repetitions; the median is reported (default: 3)', + }, + ], + }, + { + method: 'POST', + path: '/v1/frame-generation/video', + description: + 'Queue a video interpolation job (FPS upscaling or slow motion); jobs run one at a time on a worker thread and the returned task id is polled for progress', + params: [ + { + name: 'video', + type: 'file (binary)', + required: true, + desc: 'Source video file', + }, + { + name: 'multiplier', + type: 'number', + required: false, + desc: 'Frame multiplier, 2-4 (default: 2)', + }, + { + name: 'mode', + type: 'string', + required: false, + desc: "'fps' multiplies the frame rate keeping the duration (audio kept); 'slowmo' keeps the frame rate and stretches the duration (audio dropped). Default: 'fps'", + }, + ], + }, + { + method: 'GET', + path: '/v1/tasks/{taskId}', + description: + 'Poll a video interpolation task (status: queued | running | finished | error; queued responses include the queue position)', + }, + { + method: 'GET', + path: '/v1/frame-generation/video/{taskId}', + description: 'Download the interpolated video of a finished task', + }, + ], + sampleCode: [ + { + title: 'Upscale a video to 2x FPS', + codeSnippets: [ + { + language: 'Python', + languageCode: 'python', + code: `import time +import requests + +with open("input.mp4", "rb") as f: + task = requests.post( + "${host}/v1/frame-generation/video", + files={"video": f}, + data={"multiplier": 2}, + ).json() + +while True: + status = requests.get("${host}/v1/tasks/" + task["taskId"]).json() + if status["status"] in ("finished", "error"): + break + time.sleep(1) + +if status["status"] == "finished": + video = requests.get( + "${host}/v1/frame-generation/video/" + task["taskId"] + ) + with open("output.mp4", "wb") as f: + f.write(video.content)`, + }, + ], + }, + { + title: 'Benchmark interpolation throughput', + codeSnippets: [ + { + language: 'Python', + languageCode: 'python', + code: `import requests + +response = requests.post( + "${host}/v1/frame-generation/benchmark", + json={"image_size": 256, "gap_sizes": [1, 1, 1]}, +) +print(response.json()) # {"fps": ..., "seconds_per_frame": ...}`, + }, + ], + }, + ], + responseExample: `{ + "taskId": "1a2b3c4d" +}`, +}) diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/hooks/index.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/hooks/index.ts new file mode 100644 index 00000000..5707fb33 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/frame-generation/hooks/index.ts @@ -0,0 +1,96 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useMutation, useQuery } from '@tanstack/react-query' + +const API_BASE = '/api/frame-generation' + +function resultVideoUrl(taskId: string) { + return `${API_BASE}/v1/frame-generation/video/${taskId}` +} + +export type InterpolationMode = 'fps' | 'slowmo' + +export interface VideoTaskStatus { + status: 'queued' | 'running' | 'finished' | 'error' | 'not_found' + progress?: number + position?: number + mode?: InterpolationMode + multiplier?: number + input_fps?: number + output_fps?: number + frames?: number + detail?: string +} + +const TERMINAL_TASK_STATUSES = ['finished', 'error', 'not_found'] + +export function useInterpolateVideo() { + return useMutation({ + mutationFn: async (params: { + videoFile: File + multiplier: number + mode: InterpolationMode + }): Promise<{ taskId: string }> => { + const formData = new FormData() + formData.append('video', params.videoFile) + formData.append('multiplier', String(params.multiplier)) + formData.append('mode', params.mode) + + const url = new URL( + `${API_BASE}/v1/frame-generation/video`, + window.location.origin, + ) + const res = await fetch(url, { + method: 'POST', + body: formData, + }) + if (!res.ok) throw new Error(await res.text()) + return res.json() + }, + }) +} + +export function useVideoTaskStatus(taskId: string | null) { + return useQuery({ + queryKey: ['frame-generation', 'task', taskId], + enabled: !!taskId, + queryFn: async () => { + const url = new URL( + `${API_BASE}/v1/tasks/${taskId}`, + window.location.origin, + ) + const res = await fetch(url) + if (!res.ok) throw new Error(await res.text()) + return res.json() + }, + refetchInterval: (query) => { + const status = query.state.data?.status + return status && TERMINAL_TASK_STATUSES.includes(status) ? false : 1500 + }, + }) +} + +/** + * Download a finished task's video as a Blob. Playing from a local blob + * instead of streaming through the dev proxy keeps previews working when + * the UI is accessed from another machine, and lets download reuse the + * same bytes. + */ +export function useResultVideo(taskId: string | null, enabled: boolean) { + return useQuery({ + queryKey: ['frame-generation', 'result', taskId], + enabled: !!taskId && enabled, + staleTime: Infinity, + retry: 1, + queryFn: async () => { + const url = new URL( + resultVideoUrl(taskId as string), + window.location.origin, + ) + const res = await fetch(url) + if (!res.ok) throw new Error(await res.text()) + return res.blob() + }, + }) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/components/audio-lipsync-tab.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/components/audio-lipsync-tab.tsx index 0af89910..b82735f3 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/components/audio-lipsync-tab.tsx +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/components/audio-lipsync-tab.tsx @@ -17,6 +17,7 @@ import { } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { useAudioLipsync } from '../hooks' +import { FrameGenerationSwitch } from './frame-generation-switch' const SUPPORTED_AUDIO_FORMATS = '.wav,.mp3' @@ -32,9 +33,15 @@ const LANGUAGE_OPTIONS = [ export interface AudioLipsyncTabProps { sessionId: string | null + frameGeneration: boolean + onFrameGenerationChange: (checked: boolean) => void } -export function AudioLipsyncTab({ sessionId }: AudioLipsyncTabProps) { +export function AudioLipsyncTab({ + sessionId, + frameGeneration, + onFrameGenerationChange, +}: AudioLipsyncTabProps) { const [selectedAudioFile, setSelectedAudioFile] = useState(null) const [textOverlay, setTextOverlay] = useState('') const [languageCode, setLanguageCode] = useState('en-US') @@ -52,6 +59,7 @@ export function AudioLipsyncTab({ sessionId }: AudioLipsyncTabProps) { sessionId, textOverlay: textOverlay.trim() || undefined, languageCode, + frameGeneration, }, { onSuccess: () => { @@ -139,6 +147,12 @@ export function AudioLipsyncTab({ sessionId }: AudioLipsyncTabProps) {
+ + + )} + + ) +} diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/data.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/data.ts index 9c2da19b..2076e922 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/data.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/data.ts @@ -4,6 +4,7 @@ import { Video } from 'lucide-react' import type { Service as PayloadService } from '@/payload-types' import type { ServiceMeta, WorkerConfig } from '@/services/types' +import { service as frameGenerationService } from '../frame-generation/data' import { serviceConfig } from './config' export const service: ServiceMeta = { @@ -50,12 +51,15 @@ export const worker: WorkerConfig = { args.push('--ice_server', serverIceServerUrl) } - // 'auto' lets the worker enable frame generation only when measured - // inference FPS cannot reach the avatar frame rate. - const frameGeneration = - (doc.metadata as { frameGeneration?: boolean } | undefined) - ?.frameGeneration ?? true - args.push('--frame_gen', frameGeneration ? 'auto' : 'off') + // Frame generation is requested per lipsync request (frame_generation + // in the chat/audio payload), so the worker always gets the Frame + // Generation service URL. It measures its inference FPS at startup and + // only interpolates when inference alone cannot reach the avatar frame + // rate; requests degrade gracefully while that service is unreachable. + args.push( + '--frame_gen_url', + `http://localhost:${frameGenerationService.port}`, + ) return args }, diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/demo.tsx b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/demo.tsx index 3ee9934e..3236d27a 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/demo.tsx +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/demo.tsx @@ -19,6 +19,7 @@ export function LipsyncDemo({ service }: { service: Service }) { const [statusMessage, setStatusMessage] = useState(null) const [isConnecting, setIsConnecting] = useState(false) const [isMediaConnected, setIsMediaConnected] = useState(false) + const [frameGeneration, setFrameGeneration] = useState(false) const videoRef = useRef(null) const pcRef = useRef(null) @@ -174,11 +175,19 @@ export function LipsyncDemo({ service }: { service: Service }) { - + - + diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/docs.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/docs.ts index ef95b1d7..0ba0d4f1 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/docs.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/docs.ts @@ -75,6 +75,12 @@ export const getDocsData = ({ host }: { host: string }): ServiceDocsData => ({ required: false, desc: 'Speech speed', }, + { + name: 'frame_generation', + type: 'boolean', + required: false, + desc: 'Fill in-between frames with AI interpolation (needs the Frame Generation service; only activates when inference cannot match the avatar frame rate)', + }, ], }, { @@ -106,6 +112,12 @@ export const getDocsData = ({ host }: { host: string }): ServiceDocsData => ({ required: false, desc: "Language code for text overlay (default: 'en-US')", }, + { + name: 'frame_generation', + type: 'boolean', + required: false, + desc: 'Fill in-between frames with AI interpolation (needs the Frame Generation service; only activates when inference cannot match the avatar frame rate)', + }, ], }, { diff --git a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/hooks/index.ts b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/hooks/index.ts index 20b0c2bd..bb2bb8ec 100644 --- a/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/hooks/index.ts +++ b/samples/ai/edge-ai-demo-studio/frontend/src/services/lipsync/hooks/index.ts @@ -106,6 +106,7 @@ export function useLipsyncChat() { voice?: string speed?: string tts_url?: string + frame_generation?: boolean }) => { const res = await fetch(`${API_BASE}/v1/lipsync/chat`, { method: 'POST', @@ -125,6 +126,7 @@ export function useAudioLipsync() { sessionId: string textOverlay?: string languageCode?: string + frameGeneration?: boolean }) => { const formData = new FormData() formData.append('file', params.audioFile) @@ -135,6 +137,9 @@ export function useAudioLipsync() { if (params.languageCode) { formData.append('language_code', params.languageCode) } + if (params.frameGeneration) { + formData.append('frame_generation', 'true') + } const res = await fetch(`${API_BASE}/v1/lipsync`, { method: 'POST', diff --git a/samples/ai/edge-ai-demo-studio/scripts/bash/package.sh b/samples/ai/edge-ai-demo-studio/scripts/bash/package.sh index a0ca0c2e..5833460f 100755 --- a/samples/ai/edge-ai-demo-studio/scripts/bash/package.sh +++ b/samples/ai/edge-ai-demo-studio/scripts/bash/package.sh @@ -267,6 +267,44 @@ copy_scripts() { echo "✓ Scripts copied successfully." } +# Copy deployment.json (and its editor schema) into the build directory so +# electron-builder bundles them into resources/ (see extraResources in +# electron/package.json). The packaged frontend then picks the presets up from +# resources/deployment.json on startup. +copy_deployment_config() { + echo "Copying deployment.json to temporary directory..." + + if [ -f "$PROJECT_DIR/deployment.json" ]; then + cp "$PROJECT_DIR/deployment.json" "$TEMP_DIR/" || { + echo "Error: Failed to copy deployment.json to $TEMP_DIR" + echo "Check file permissions and available disk space." + exit 1 + } + echo "✓ deployment.json copied successfully." + else + echo "Warning: deployment.json not found at $PROJECT_DIR/deployment.json - the package will use built-in service defaults." + fi + + # Ship the deployment docs alongside it: the schema keeps the + # "$schema": "./docs/deployment.schema.json" reference working for editor + # validation, and shipping deployment-config.md means the startup doc + # regeneration (frontend/src/lib/deployment-docs.ts) finds identical content + # in resources/docs and skips rewriting it. + local DOC_FILE + for DOC_FILE in deployment.schema.json deployment-config.md; do + if [ -f "$PROJECT_DIR/docs/$DOC_FILE" ]; then + mkdir -p "$TEMP_DIR/docs" || { + echo "Error: Failed to create docs folder at $TEMP_DIR/docs" + exit 1 + } + cp "$PROJECT_DIR/docs/$DOC_FILE" "$TEMP_DIR/docs/" || { + echo "Warning: Failed to copy $DOC_FILE - continuing without it." + } + echo "✓ $DOC_FILE copied successfully." + fi + done +} + finalize_package() { echo "Finalizing package..." @@ -692,6 +730,7 @@ main() { setup_node_env || exit 1 copy_workers || exit 1 copy_scripts || exit 1 + copy_deployment_config || exit 1 setup_frontend_package || exit 1 run_electron_package || exit 1 prune_native_binaries || exit 1 diff --git a/samples/ai/edge-ai-demo-studio/scripts/export-samples.mjs b/samples/ai/edge-ai-demo-studio/scripts/export-bundle.mjs similarity index 86% rename from samples/ai/edge-ai-demo-studio/scripts/export-samples.mjs rename to samples/ai/edge-ai-demo-studio/scripts/export-bundle.mjs index c8302717..293ae9de 100644 --- a/samples/ai/edge-ai-demo-studio/scripts/export-samples.mjs +++ b/samples/ai/edge-ai-demo-studio/scripts/export-bundle.mjs @@ -5,11 +5,13 @@ /** * Export a slim subset of Demo Studio that contains only the requested - * samples plus the services and workers they depend on. + * samples plus the services and workers they depend on. Services can also be + * requested directly (with or without any samples) via --services. * * Usage: - * node scripts/export-samples.mjs --samples=rag-chatbot,medical-scribe [--out=out/my-export] [--include-optional] - * node scripts/export-samples.mjs --list + * node scripts/export-bundle.mjs --samples=rag-chatbot,medical-scribe [--out=out/my-export] [--include-optional] + * node scripts/export-bundle.mjs --services=text-to-speech,ocr + * node scripts/export-bundle.mjs --list * * The output directory is self-contained: it has its own setup.sh / * setup_win.bat and start.sh / start_win.bat, with only the necessary @@ -45,6 +47,7 @@ function parseArgs(argv) { // --no-optional to exclude them. const out = { samples: [], + services: [], includeOptional: true, list: false, out: null, @@ -68,6 +71,12 @@ function parseArgs(argv) { .split(',') .map((s) => s.trim()) .filter(Boolean) + } else if (arg.startsWith('--services=')) { + out.services = arg + .slice('--services='.length) + .split(',') + .map((s) => s.trim()) + .filter(Boolean) } else if (arg.startsWith('--out=')) { out.out = arg.slice('--out='.length) } else if (arg === '--help' || arg === '-h') { @@ -83,23 +92,28 @@ function parseArgs(argv) { } function printHelp() { - console.log(`Usage: node scripts/export-samples.mjs --samples= [options] + console.log(`Usage: node scripts/export-bundle.mjs --samples= [options] Options: - --samples= Comma-separated sample IDs to export (required) + --samples= Comma-separated sample IDs to export + --services= Comma-separated service IDs to export directly (can be + used alone for a services-only export with no samples) --no-optional Exclude each sample's optional service deps (optional deps are INCLUDED by default) --include-optional Explicitly include optional deps (default behavior) - --out= Output directory (default: out/) + --out= Output directory (default: out/) --dry-run, --plan Resolve the export plan and exit without writing files --json Emit machine-readable JSON (with --list or --dry-run) - --list, -l List available samples and exit + --list, -l List available samples and services, then exit --help, -h Show this help +At least one of --samples / --services is required. + Examples: - node scripts/export-samples.mjs --samples=rag-chatbot - node scripts/export-samples.mjs --samples=rag-chatbot --no-optional - node scripts/export-samples.mjs --samples=medical-scribe,rag-chatbot + node scripts/export-bundle.mjs --samples=rag-chatbot + node scripts/export-bundle.mjs --samples=rag-chatbot --no-optional + node scripts/export-bundle.mjs --samples=medical-scribe,rag-chatbot + node scripts/export-bundle.mjs --services=text-to-speech,ocr `) } @@ -381,16 +395,26 @@ const args = parseArgs(process.argv.slice(2)) if (args.list) { if (args.json) { - console.log(JSON.stringify({ samples: listSamples() }, null, 2)) + console.log( + JSON.stringify( + { samples: listSamples(), services: listServiceFolders() }, + null, + 2, + ), + ) } else { console.log('Available samples:') for (const s of listSamples()) console.log(` - ${s}`) + console.log('Available services:') + for (const s of listServiceFolders()) console.log(` - ${s}`) } process.exit(0) } -if (args.samples.length === 0) { - console.error('Error: --samples= is required (or use --list)') +if (args.samples.length === 0 && args.services.length === 0) { + console.error( + 'Error: --samples= or --services= is required (or use --list)', + ) printHelp() process.exit(1) } @@ -405,8 +429,17 @@ if (unknown.length > 0) { process.exit(1) } -// Resolve services -const requiredServiceIds = new Set() +const unknownServices = args.services.filter((s) => !allServices.includes(s)) +if (unknownServices.length > 0) { + console.error(`Unknown service(s): ${unknownServices.join(', ')}`) + console.error('Run with --list to see available services.') + process.exit(1) +} + +// Resolve services. Explicitly requested services are treated as required so +// a services-only export (no samples) still resolves workers and transitive +// service deps exactly like a sample-driven one. +const requiredServiceIds = new Set(args.services) const optionalServiceIds = new Set() const sampleSummaries = [] for (const sampleId of args.samples) { @@ -549,6 +582,7 @@ const reportedWorkerDirs = [...workerDirs, ...suiteWorkerDirs].sort() // matches what an actual export would produce. const exportPlan = { samples: sampleSummaries, + requestedServices: [...args.services].sort(), services: { required: [...requiredServiceIds].sort(), optional: [...optionalServiceIds].sort(), @@ -558,33 +592,64 @@ const exportPlan = { includeOptional: args.includeOptional, } +/** + * Human-readable plan. Only mentions what was actually requested: a + * services-only export talks about services, a sample export lists each + * sample with the services being exported for it. + */ +function printPlan(header) { + console.log(header) + if (args.samples.length > 0) { + console.log(' Samples:') + for (const s of sampleSummaries) { + const svcs = s.deps + .filter((d) => includedServiceIds.has(d.serviceId)) + .map((d) => + d.role === 'optional' ? `${d.serviceId} (optional)` : d.serviceId, + ) + console.log(` - ${s.id} (services: ${svcs.join(', ') || 'none'})`) + } + } + if (args.services.length > 0) { + const label = + args.samples.length > 0 ? 'Services (requested directly)' : 'Services' + console.log(` ${label}: ${[...args.services].sort().join(', ')}`) + } + // Services pulled in transitively (via imports of the ones above) that + // haven't been mentioned yet. + const mentioned = new Set(args.services) + for (const s of sampleSummaries) { + for (const d of s.deps) mentioned.add(d.serviceId) + } + const extra = [...includedServiceIds] + .filter((id) => !mentioned.has(id)) + .sort() + if (extra.length > 0) { + console.log(` Services (transitive deps): ${extra.join(', ')}`) + } + console.log(` Workers: ${reportedWorkerDirs.join(', ')}`) +} + // Dry run: resolve the plan and stop before touching the filesystem. if (args.dryRun) { if (args.json) { console.log(JSON.stringify(exportPlan, null, 2)) } else { - console.log(`Export plan (dry run)`) - console.log(` Samples: ${args.samples.join(', ')}`) - console.log(` Services: ${exportPlan.services.included.join(', ') || '(none)'}`) - console.log(` Workers: ${exportPlan.workers.join(', ')}`) + printPlan('Export plan (dry run)') } process.exit(0) } // Output directory — resolved relative to the user's cwd so that paths like // `--out=out/foo` work whether the script is invoked from the repo root or -// from frontend/ via `npm run export-samples`. -const outDirRel = args.out ?? `out/${args.samples.join('_')}` +// from frontend/ via `npm run export-bundle`. +const outDirRel = args.out ?? `out/${[...args.samples, ...args.services].join('_')}` const outDir = args.out ? resolve(process.cwd(), outDirRel) : resolve(REPO_ROOT, outDirRel) -console.log(`\nExport plan`) -console.log(` Samples: ${args.samples.join(', ')}`) -console.log( - ` Services: ${[...includedServiceIds].sort().join(', ') || '(none)'}`, -) -console.log(` Workers: ${reportedWorkerDirs.join(', ')}`) +console.log('') +printPlan('Export plan') console.log(` Output: ${relative(REPO_ROOT, outDir)}\n`) // Wipe & recreate output diff --git a/samples/ai/edge-ai-demo-studio/scripts/win/export.ps1 b/samples/ai/edge-ai-demo-studio/scripts/win/export.ps1 index f5722592..486f3e51 100644 --- a/samples/ai/edge-ai-demo-studio/scripts/win/export.ps1 +++ b/samples/ai/edge-ai-demo-studio/scripts/win/export.ps1 @@ -11,7 +11,7 @@ $REPO_ROOT = Resolve-Path (Join-Path $SCRIPT_DIR '..\..') $NODE_DIR = Join-Path $REPO_ROOT 'thirdparty\node' $NODE_PATH = Join-Path $NODE_DIR 'node.exe' $SETUP_THIRDPARTY = Join-Path $SCRIPT_DIR 'setup_thirdparty.ps1' -$EXPORT_SCRIPT = Join-Path $REPO_ROOT 'scripts\export-samples.mjs' +$EXPORT_SCRIPT = Join-Path $REPO_ROOT 'scripts\export-bundle.mjs' # Install bundled Node.js if not already present if (-not (Test-Path $NODE_PATH)) { @@ -34,21 +34,45 @@ try { exit 1 } -# If arguments were passed, forward them directly (non-interactive mode). -if ($args.Count -gt 0) { +# --dry-run/--plan alone should still walk through the interactive prompts +# (just previewing the plan instead of writing files), so pull it out before +# deciding whether to forward everything non-interactively. +$dryRunFlag = $null +$remainingArgs = @() +foreach ($a in $args) { + if ($a -eq '--dry-run' -or $a -eq '--plan') { + $dryRunFlag = $a + } else { + $remainingArgs += $a + } +} + +# If other arguments were passed, forward everything directly (non-interactive mode). +if ($remainingArgs.Count -gt 0) { & $NODE_PATH $EXPORT_SCRIPT @args exit $LASTEXITCODE } # ── Interactive mode ────────────────────────────────────────────── Write-Host "" -Write-Host "Discovering available samples..." -$samples = (& $NODE_PATH $EXPORT_SCRIPT --list) | - Where-Object { $_ -match '^\s+-\s' } | - ForEach-Object { ($_ -replace '^\s+-\s', '').Trim() } +Write-Host "Discovering available samples and services..." +# --list prints two sections ("Available samples:" / "Available services:"), +# each with " - " entries. +$listOutput = & $NODE_PATH $EXPORT_SCRIPT --list +$samples = @() +$services = @() +$section = '' +foreach ($line in $listOutput) { + if ($line -match '^Available samples:') { $section = 'samples'; continue } + if ($line -match '^Available services:') { $section = 'services'; continue } + if ($line -match '^\s+-\s') { + $id = ($line -replace '^\s+-\s', '').Trim() + if ($section -eq 'services') { $services += $id } else { $samples += $id } + } +} -if (-not $samples -or $samples.Count -eq 0) { - Write-Host "ERROR: No samples found." -ForegroundColor Red +if ($samples.Count -eq 0 -and $services.Count -eq 0) { + Write-Host "ERROR: No samples or services found." -ForegroundColor Red exit 1 } @@ -57,34 +81,69 @@ Write-Host "Available samples:" for ($i = 0; $i -lt $samples.Count; $i++) { Write-Host ("{0,4}) {1}" -f ($i + 1), $samples[$i]) } - Write-Host "" -$selection = Read-Host "Enter sample number(s) to export (space/comma separated)" -$chosen = @() -foreach ($n in ($selection -split '[,\s]+' | Where-Object { $_ })) { - if ($n -notmatch '^\d+$' -or [int]$n -lt 1 -or [int]$n -gt $samples.Count) { - Write-Host "ERROR: Invalid selection '$n'." -ForegroundColor Red +Write-Host "Available services:" +for ($i = 0; $i -lt $services.Count; $i++) { + Write-Host ("{0,4}) {1}" -f ($samples.Count + $i + 1), $services[$i]) +} + +# Resolve a space/comma-separated selection (numbers continue past the sample +# list into the service list, or literal ids) into sample/service id arrays. +function Resolve-Selection([string]$Prompt) { + $selection = Read-Host $Prompt + $pickedSamples = @() + $pickedServices = @() + $total = $samples.Count + $services.Count + foreach ($tok in ($selection -split '[,\s]+' | Where-Object { $_ })) { + if ($tok -match '^\d+$' -and [int]$tok -ge 1 -and [int]$tok -le $total) { + if ([int]$tok -le $samples.Count) { + $pickedSamples += $samples[[int]$tok - 1] + } else { + $pickedServices += $services[[int]$tok - 1 - $samples.Count] + } + continue + } + if ($samples -contains $tok) { + $pickedSamples += $tok + continue + } + if ($services -contains $tok) { + $pickedServices += $tok + continue + } + Write-Host "ERROR: Invalid selection '$tok'." -ForegroundColor Red exit 1 } - $chosen += $samples[[int]$n - 1] + return ,@($pickedSamples, $pickedServices) } -if ($chosen.Count -eq 0) { - Write-Host "ERROR: No samples selected." -ForegroundColor Red + +Write-Host "" +$chosen = Resolve-Selection "Enter number(s) or name(s) to export (samples and/or services, space/comma separated, blank for none)" +# Drop duplicates (e.g. the same service picked once by number, once by name). +$chosenSamples = @($chosen[0] | Select-Object -Unique) +$chosenServices = @($chosen[1] | Select-Object -Unique) + +if ($chosenSamples.Count -eq 0 -and $chosenServices.Count -eq 0) { + Write-Host "ERROR: Nothing selected - pick at least one sample or service." -ForegroundColor Red exit 1 } -$exportArgs = @("--samples=$($chosen -join ',')") +$exportArgs = @() +if ($chosenSamples.Count -gt 0) { $exportArgs += "--samples=$($chosenSamples -join ',')" } +if ($chosenServices.Count -gt 0) { $exportArgs += "--services=$($chosenServices -join ',')" } -$opt = Read-Host "Include optional service dependencies? [Y/n]" -if ($opt -match '^[Nn]') { $exportArgs += '--no-optional' } +# Optional deps only come from selected samples; skip if none were chosen. +if ($chosenSamples.Count -gt 0) { + $opt = Read-Host "Include optional service dependencies? [Y/n]" + if ($opt -match '^[Nn]') { $exportArgs += '--no-optional' } +} $outdir = Read-Host "Output directory (blank for default)" if ($outdir) { $exportArgs += "--out=$outdir" } -$dry = Read-Host "Dry run (preview plan only)? [y/N]" -if ($dry -match '^[Yy]') { $exportArgs += '--dry-run' } +if ($dryRunFlag) { $exportArgs += $dryRunFlag } Write-Host "" -Write-Host "Running: export-samples $($exportArgs -join ' ')" +Write-Host "Running: export-bundle $($exportArgs -join ' ')" & $NODE_PATH $EXPORT_SCRIPT @exportArgs exit $LASTEXITCODE diff --git a/samples/ai/edge-ai-demo-studio/scripts/win/package.ps1 b/samples/ai/edge-ai-demo-studio/scripts/win/package.ps1 index 9bef6cef..cf5226b9 100644 --- a/samples/ai/edge-ai-demo-studio/scripts/win/package.ps1 +++ b/samples/ai/edge-ai-demo-studio/scripts/win/package.ps1 @@ -255,6 +255,41 @@ function Add-ScriptFiles { } } +# Copy deployment.json (and its editor schema) into the build directory so +# electron-builder bundles them into resources/ (see extraResources in +# electron/package.json). The packaged frontend then picks the presets up from +# resources/deployment.json on startup. +function Add-DeploymentConfig { + Write-Host "Copying deployment.json to temporary directory..." -ForegroundColor Green + + try { + $deploymentConfig = Join-Path $PROJECT_ROOT "deployment.json" + if (Test-Path $deploymentConfig -PathType Leaf) { + Copy-Item $deploymentConfig "$TEMP_DIR/" -Force -ErrorAction Stop + Write-Host "deployment.json copied successfully." -ForegroundColor Green + } else { + Write-Host "Warning: deployment.json not found at $deploymentConfig - the package will use built-in service defaults." -ForegroundColor Yellow + } + + # Ship the deployment docs alongside it: the schema keeps the + # "$schema": "./docs/deployment.schema.json" reference working for editor + # validation, and shipping deployment-config.md means the startup doc + # regeneration (frontend/src/lib/deployment-docs.ts) finds identical content + # in resources/docs and skips rewriting it. + foreach ($docFile in @("deployment.schema.json", "deployment-config.md")) { + $docPath = Join-Path $PROJECT_ROOT "docs/$docFile" + if (Test-Path $docPath -PathType Leaf) { + New-Item -ItemType Directory -Path "$TEMP_DIR/docs" -Force -ErrorAction Stop | Out-Null + Copy-Item $docPath "$TEMP_DIR/docs/" -Force -ErrorAction Stop + Write-Host "$docFile copied successfully." -ForegroundColor Green + } + } + } catch { + Write-Host "Error copying deployment config: $_" -ForegroundColor Red + throw + } +} + function Invoke-FrontendBuild { # Build the frontend application Write-Host "Building frontend application..." -ForegroundColor Green @@ -455,6 +490,7 @@ try { Add-TempDir Add-WorkerFiles Add-ScriptFiles + Add-DeploymentConfig Invoke-FrontendBuild Start-ElectronPackage Invoke-FinalizePackage diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/cli.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/cli.py index 5c8f945d..f7419454 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/cli.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/cli.py @@ -53,28 +53,28 @@ def _load_config(self) -> Dict[str, Any]: "active_models": { "text_generation": { "repo_id": None, - "n_ctx": 4096, + "n_ctx": 0, "n_gpu_layers": 35, "model_path": None, "mmproj_path": None, }, "embeddings": { "repo_id": None, - "n_ctx": 4096, + "n_ctx": 0, "n_gpu_layers": 35, "model_path": None, "mmproj_path": None, }, "rerank": { "repo_id": None, - "n_ctx": 4096, + "n_ctx": 0, "n_gpu_layers": 35, "model_path": None, "mmproj_path": None, }, "multimodal": { "repo_id": None, - "n_ctx": 4096, + "n_ctx": 0, "n_gpu_layers": 35, "model_path": None, "mmproj_path": None, @@ -310,7 +310,7 @@ def start_local_model( self, repo_id, task, - context_size: int = 4096, + context_size: int = 0, device: str = "GPU", model_path: str = "", mmproj_path: str = "", @@ -360,6 +360,9 @@ def stop_local_model(self, repo_id, task): return False + def has_multimodal_projector(self, task: str) -> bool: + return self.manager.has_multimodal_projector(task) + def is_active_model(self, task, model_id: str): current_model = self.manager.get_current_active_model(task) if model_id != current_model: diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/gguf_downloader.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/gguf_downloader.py index 71493cde..ad75abaf 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/gguf_downloader.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/gguf_downloader.py @@ -6,6 +6,7 @@ import requests import sys import hashlib +import shutil import threading import yaml from pathlib import Path @@ -352,17 +353,31 @@ def get_model_info_for_repo(self, hf_repo_with_tag: str) -> str: hf_repo_id = hf_repo_with_tag.split(":")[0] possible_tasks = ["text_generation", "embeddings", "rerank", "multimodal"] + empty_task_dir = None for task in possible_tasks: local_path = Path(self.models_base_dir) / task / hf_repo_id - if local_path.is_dir(): + if not local_path.is_dir(): + continue + + # A directory left behind by a deleted model or an interrupted + # download (it keeps a .cache/ but no weights) must not claim the + # model, or it hijacks the lookup from the task that actually + # holds the GGUF. + if any(local_path.glob("*.gguf")): return task + if empty_task_dir is None: + empty_task_dir = task + if hf_repo_with_tag in self.verified_models: return self.verified_models[hf_repo_with_tag].get("task", None) if hf_repo_id in self.verified_models.keys(): return self.verified_models[hf_repo_id].get("task", None) + if empty_task_dir: + return empty_task_dir + raise ValueError( f"Model {hf_repo_with_tag} not found locally or in the verified list." ) @@ -541,8 +556,11 @@ def delete_downloaded_model(self, hf_repo_with_tag: str) -> bool: if files_removed: repo_dir = Path(self.models_base_dir) / task / manifest["hf_repo"] try: - if not any(repo_dir.iterdir()): - repo_dir.rmdir() + # Drop the whole repo dir once no weights remain. Leaving it + # for the sake of huggingface_hub's .cache/ would make this + # task shadow the one the model is later downloaded into. + if not any(repo_dir.glob("*.gguf")): + shutil.rmtree(repo_dir, ignore_errors=True) return True except OSError: return False diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/llama_cpp_manager.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/llama_cpp_manager.py index cb40e17b..6d8bf874 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/llama_cpp_manager.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/llamacpp/llama_cpp_manager.py @@ -260,7 +260,7 @@ def _construct_llama_server_cmd( logical_bz = 2048 physical_bz = 512 ngl = 0 if device == "CPU" else -1 - model_context_size = 4096 + model_context_size = 0 if gguf_metadata: logical_bz = gguf_metadata.get("estimate", {}).get("logicalBatchSize", 2048) @@ -276,7 +276,7 @@ def _construct_llama_server_cmd( ngl = min(int(items[0].get("offloadLayers")), ngl) model_context_size = gguf_metadata.get("estimate", {}).get( - "contextSize", 4096 + "contextSize", 0 ) logical_bz = min(2048, logical_bz) @@ -302,21 +302,17 @@ def _construct_llama_server_cmd( "-ub", str(largest_bz), ] - elif task == "multimodal": - server_executable = get_resource_path(self.CUSTOM_SERVER_EXECUTABLE) - extra_args = [ - "--mmproj", - mmproj_path, - "--jinja", - "--reasoning_format", - "deepseek", - ] - if self.config_file != None: - extra_args.extend(["--config-file", str(self.config_file)]) else: server_executable = get_resource_path(self.CUSTOM_SERVER_EXECUTABLE) extra_args = ["--jinja", "--reasoning_format", "deepseek"] + # A GGUF is a VLM because it ships a projector, not because of the + # task folder it was registered under. Load whatever projector was + # resolved so vision models added as text_generation still see + # images, and never emit a bare --mmproj when there is none. + if mmproj_path: + extra_args.extend(["--mmproj", str(mmproj_path)]) + if self.config_file != None: extra_args.extend(["--config-file", str(self.config_file)]) @@ -324,20 +320,21 @@ def _construct_llama_server_cmd( if chat_template_file.exists(): extra_args.extend(["--chat-template-file", str(chat_template_file)]) - suggested_ctx_size, oom = optimize_context_size( - model_path, model_context_size, bypass_oom=skip_oom - ) - if oom: - raise RuntimeError(f"Failed to start server for {task}: Out of Memory") - - if n_ctx > -1: - context_size = n_ctx - - if context_size > 0: - context_size = min(context_size, suggested_ctx_size) - else: - context_size = min(model_context_size, suggested_ctx_size) + # No explicit context size means no -c at all: llama-server then picks + # the model's own context from the GGUF instead of being pinned to a + # number we made up. Probing VRAM is only worth it when we are actually + # going to clamp a requested size against it. + context_size = 0 + if n_ctx > 0: + # Without GGUF metadata, cap the search at what was asked for + # rather than at an invented ceiling that would clamp the request. + suggested_ctx_size, oom = optimize_context_size( + model_path, model_context_size or n_ctx, bypass_oom=skip_oom + ) + if oom: + raise RuntimeError(f"Failed to start server for {task}: Out of Memory") + context_size = min(n_ctx, suggested_ctx_size) if suggested_ctx_size > 0 else n_ctx extra_args.extend(["-c", str(context_size)]) if "-ngl" not in extra_args: @@ -444,16 +441,22 @@ def get_model_file_path(self, hf_repo_with_tag: str) -> Tuple[str, str]: f"Model file not found locally: {local_path}. Please download it first." ) - if task == "multimodal": - mmproj_path = Path(self.models_base_dir) / task / hf_repo / mmproj_filename - if not mmproj_path.exists(): + mmproj_path = "" + if mmproj_filename: + candidate = Path(self.models_base_dir) / task / hf_repo / mmproj_filename + if candidate.exists(): + mmproj_path = str(candidate) + elif task == "multimodal": raise FileNotFoundError( - f"Multimodal projection file not found locally: {mmproj_path}. Please download it first." + f"Multimodal projection file not found locally: {candidate}. Please download it first." ) - else: - mmproj_path = "" + elif task == "multimodal": + raise FileNotFoundError( + f"No multimodal projection file is published for {hf_repo_with_tag}. " + f"This model cannot be served as a multimodal model." + ) - return str(local_path), str(mmproj_path) + return str(local_path), mmproj_path def start_server(self, hf_repo_with_tag: str, **kwargs): task = self.downloader.get_model_info_for_repo( @@ -528,6 +531,7 @@ def start_server(self, hf_repo_with_tag: str, **kwargs): "batch_size": logical_bz, "ubatch_size": physical_bz, "ngl": ngl, + "mmproj_path": mmproj_path or "", } props = self._wait_for_server(server_url, timeout=timeout) @@ -710,6 +714,7 @@ def start_server_with_file(self, repo_id: str, task: str, **kwargs): "batch_size": logical_bz, "ubatch_size": physical_bz, "ngl": ngl, + "mmproj_path": mmproj_path or "", } props = self._wait_for_server(server_url, timeout=timeout) @@ -794,6 +799,13 @@ def get_server_status(self, task: str) -> Tuple[str, str]: def get_server_info(self) -> Dict: return self.running_servers + def has_multimodal_projector(self, task: str) -> bool: + server_info = self.running_servers.get(task) + if not server_info: + return False + + return bool(server_info.get("mmproj_path")) + def get_current_active_model(self, task: str) -> str: if task in self.running_servers: server_info = self.running_servers[task] diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/model_schema.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/model_schema.py index 14822ff0..eccaf92f 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/model_schema.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/modules/model_schema.py @@ -34,7 +34,7 @@ class HybridModelRequest(BaseModel): example="openvino:OpenVINO/Qwen3-8B-int4-ov", ) task: str = Field(..., example="text_generation") - context_size: Optional[int] = Field(default=4096, example=4096) + context_size: Optional[int] = Field(default=0, example=4096) device: Optional[str] = Field(None, example="GPU") model_path: Optional[str] = Field(None, example="") diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_api_router.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_api_router.py index e2319cd3..8f45423e 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_api_router.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_api_router.py @@ -48,7 +48,7 @@ class ModelRequest(BaseModel): class ModelWithTaskStartRequest(BaseModel): repo_id: str = Field(..., example="Qwen/Qwen3-1.7B-GGUF") task: str = Field(..., example="text_generation") - context_size: Optional[int] = Field(..., example=4096) + context_size: Optional[int] = Field(0, example=4096) device: Optional[str] = Field(None, example="GPU") model_path: Optional[str] = Field( None, @@ -297,7 +297,8 @@ def get_server_status(): @router.post("/start", tags=["Server Control"]) def start_model_server(request: ModelWithTaskStartRequest): - if request.context_size < 0: + context_size = request.context_size or 0 + if context_size < 0: raise HTTPException( status_code=500, detail=f"Context Size cannot be negative." ) @@ -308,7 +309,16 @@ def start_model_server(request: ModelWithTaskStartRequest): if llmcpp_manager.start_or_swap_model( repo_id, request.device, request.timeout ): - return {"context_size": request.context_size} + # Report what the server actually negotiated -- with no + # requested size there is nothing to echo back. + try: + running_ctx = llmcpp_manager.get_task_metadata( + request.task + ).get("context_size", context_size) + except Exception: + running_ctx = context_size + + return {"context_size": running_ctx} else: raise HTTPException( status_code=500, @@ -318,14 +328,14 @@ def start_model_server(request: ModelWithTaskStartRequest): recommended_ctx = llmcpp_manager.start_local_model( repo_id, request.task, - request.context_size, + context_size, request.device, request.model_path, request.mmproj_path, request.extra_args, request.timeout, ) - if recommended_ctx != request.context_size: + if recommended_ctx != context_size: return recommended_ctx return "OK" diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_openai_proxy_router.py b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_openai_proxy_router.py index 5534864d..eda0dbd8 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_openai_proxy_router.py +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/routers/llamacpp_openai_proxy_router.py @@ -271,6 +271,7 @@ async def chat_completions_proxy(request: Request): except json.JSONDecodeError: raise HTTPException(status_code=400, detail="Invalid JSON body") + task = "" headers = {"Content-Type": "application/json"} provider, repo_id = model_name_parser(model_id) if provider != None and repo_id != None: @@ -316,7 +317,9 @@ async def chat_completions_proxy(request: Request): else: raise HTTPException(status_code=400, detail="Invalid Model Id") - if task == "multimodal": + # Vision support follows the projector the server was started with, so + # a VLM registered under text_generation still gets its images inlined. + if task == "multimodal" or llmcpp_manager.has_multimodal_projector(task): await process_request_images(request_body) is_streaming = request_body.get("stream", False) diff --git a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/verified.yaml b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/verified.yaml index 49e18d73..ccd5bdf6 100644 --- a/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/verified.yaml +++ b/samples/ai/edge-ai-demo-studio/workers/engine/multiserve/verified.yaml @@ -39,6 +39,11 @@ models: quant: [Q4_K_M] source: ["huggingface", "modelscope"] + unsloth/gemma-4-E4B-it-GGUF: + task: multimodal + quant: [Q4_K_M] + source: ["huggingface", "modelscope"] + ov_models: OpenVINO/Qwen3.5-4B-int4-ov: task: text_generation diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/.python-version b/samples/ai/edge-ai-demo-studio/workers/face-recognition/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/README.md b/samples/ai/edge-ai-demo-studio/workers/face-recognition/README.md new file mode 100644 index 00000000..e8f0305c --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/README.md @@ -0,0 +1,48 @@ +# Face Recognition Worker + +FastAPI worker running one of these face-recognition pipelines at a time: + +| Preset | Detector | Landmarks | Recogniser | Embedding | +|---|---|---|---|---| +| `omz-retail` (default) | `face-detection-retail-0004` (300x300) | `landmarks-regression-retail-0009` | `face-reidentification-retail-0095` | 256-d | +| `omz-adas` | `face-detection-adas-0001` (672x384) | `landmarks-regression-retail-0009` | `face-reidentification-retail-0095` | 256-d | +| `yunet-sface` | YuNet (OpenCV Zoo) | from the detector | SFace | 128-d | + +Everything runs on OpenVINO. The `omz-*` presets are a port of the Open Model +Zoo [`face_recognition_demo`](https://github.com/openvinotoolkit/open_model_zoo/blob/master/demos/face_recognition_demo/python/README.md): +detect -> regress 5 landmarks on the (1.15x expanded) ROI -> align -> embed. +Their defaults mirror the demo's `-t_fd 0.6`, `-exp_r_fd 1.15` and `-t_id 0.3` +(that cosine *distance* is `0.5 * (1 - similarity)`, so the equivalent +similarity threshold used here is 0.4). + +Models are downloaded on first load into `models/face-recognition/` at the +project root — Intel FP16 IR from storage.openvinotoolkit.org and OpenCV Zoo +ONNX files, all Apache-2.0. + +## Flow + +1. Enroll one or more reference images per person: `POST /gallery` + (multipart `name` + `files[]`). The largest face in each image is embedded + by the active model; enrolling the same name again appends images. The + original bytes are kept, so switching models re-embeds the gallery + automatically. +2. Recognise: `POST /recognize` (multipart `file`). Every face is matched + against the gallery by cosine similarity. + +Decision thresholds (cosine, on L2-normalised embeddings): `face-reidentification-retail-0095` +0.4 (the demo's `-t_id 0.3` distance), SFace 0.363 (OpenCV's calibrated +default). + +## Endpoints + +- `GET /healthcheck` — 200 once every requested pipeline is loaded +- `GET /devices` — OpenVINO device list +- `GET /models`, `GET /models/active`, `POST /models/load {model, device}` +- `GET /gallery`, `POST /gallery`, `DELETE /gallery`, `DELETE /gallery/{id}` +- `POST /recognize` + +## Run + +```bash +./start.sh --port 8031 --model omz-retail --device AUTO +``` diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/config.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/config.py new file mode 100644 index 00000000..cfc6e6b6 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/config.py @@ -0,0 +1,31 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Central configuration and defaults for the face-recognition worker. + +Everything here can be overridden from the CLI (see ``main.py``) or via the +``POST /models/load`` endpoint, so the defaults below are just the +out-of-the-box behaviour. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +WORKER_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = WORKER_DIR.parents[1] + +MODELS_DIR = Path( + os.environ.get( + "FACE_RECOGNITION_MODELS_DIR", PROJECT_ROOT / "models" / "face-recognition" + ) +) + +DEFAULT_DEVICE = os.environ.get("FACE_RECOGNITION_DEVICE", "AUTO") +DEFAULT_MODEL = os.environ.get("FACE_RECOGNITION_MODEL", "omz-retail") +DEFAULT_PORT = int(os.environ.get("FACE_RECOGNITION_PORT", "8031")) +CORS_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", +] diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/__init__.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/__init__.py new file mode 100644 index 00000000..91da480a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/align.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/align.py new file mode 100644 index 00000000..d7edaa95 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/align.py @@ -0,0 +1,117 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Five-point face alignment. + +Two flavours live here, one per recognition model family: + +* :func:`norm_crop` — SFace's 112x112 ArcFace template applied to landmarks in + source-image coordinates, via the Umeyama least-squares similarity transform + (the same maths as OpenCV's ``FaceRecognizerSF::alignCrop``). +* :func:`align_roi_landmarks` — the Open Model Zoo variant, ported from the + ``face_recognition_demo``'s ``FaceIdentifier._align_rois``: it warps an ROI + crop in place using landmarks relative to that crop, keeping the crop's own + size and aspect ratio (the reid model resizes afterwards). +""" + +from __future__ import annotations + +import cv2 +import numpy as np + +# Reference positions of [left eye, right eye, nose, left mouth, right mouth] +# in the 112x112 aligned crop (the standard ArcFace template). +ARCFACE_DST = np.array( + [ + [38.2946, 51.6963], + [73.5318, 51.5014], + [56.0252, 71.7366], + [41.5493, 92.3655], + [70.7299, 92.2041], + ], + dtype=np.float32, +) + +CROP_SIZE = 112 + + +def _similarity_transform(src: np.ndarray, dst: np.ndarray) -> np.ndarray: + """Umeyama similarity transform (2x3) mapping ``src`` points onto ``dst``.""" + src = src.astype(np.float64) + dst = dst.astype(np.float64) + src_mean = src.mean(axis=0) + dst_mean = dst.mean(axis=0) + src_d = src - src_mean + dst_d = dst - dst_mean + cov = dst_d.T @ src_d / len(src) + u, s, vt = np.linalg.svd(cov) + d = np.sign(np.linalg.det(u) * np.linalg.det(vt)) + diag = np.diag([1.0, d]) + rotation = u @ diag @ vt + var_src = (src_d**2).sum() / len(src) + scale = np.trace(np.diag(s) @ diag) / var_src + translation = dst_mean - scale * rotation @ src_mean + return np.hstack([scale * rotation, translation[:, None]]) + + +def norm_crop(image: np.ndarray, landmarks: np.ndarray) -> np.ndarray: + """Align a face to the 112x112 ArcFace template from its 5 landmarks.""" + matrix = _similarity_transform( + np.asarray(landmarks, dtype=np.float32).reshape(5, 2), ARCFACE_DST + ) + return cv2.warpAffine(image, matrix, (CROP_SIZE, CROP_SIZE)) + + +# face-reidentification-retail-0095's reference landmarks, expressed as +# fractions of its 96x112 training crop (left eye, right eye, nose tip, left +# and right mouth corner). +OMZ_REFERENCE_LANDMARKS = np.array( + [ + [30.2946 / 96, 51.6963 / 112], + [65.5318 / 96, 51.5014 / 112], + [48.0252 / 96, 71.7366 / 112], + [33.5493 / 96, 92.3655 / 112], + [62.7299 / 96, 92.2041 / 112], + ], + dtype=np.float64, +) + + +def _standardize(points: np.ndarray) -> tuple[np.ndarray, float]: + """Centre ``points`` per column and scale to unit std (in place).""" + mean = points.mean(axis=0) + points -= mean + std = points.std() + points /= std + return mean, float(std) + + +def _omz_transform(src: np.ndarray, dst: np.ndarray) -> np.ndarray: + """Least-squares similarity transform used by the OMZ face demo.""" + src_mean, src_std = _standardize(src) + dst_mean, dst_std = _standardize(dst) + u, _, vt = np.linalg.svd(src.T @ dst) + rotation = (u @ vt).T + + transform = np.empty((2, 3)) + transform[:, 0:2] = rotation * (dst_std / src_std) + transform[:, 2] = dst_mean.T - transform[:, 0:2] @ src_mean.T + return transform + + +def align_roi_landmarks(crop: np.ndarray, landmarks: np.ndarray) -> np.ndarray: + """Align a face ROI from landmarks given as fractions of that ROI. + + Returns a new image of the same size as ``crop``, with the face rotated and + scaled onto :data:`OMZ_REFERENCE_LANDMARKS`. + """ + height, width = crop.shape[:2] + scale = np.array((width, height), dtype=np.float64) + desired = OMZ_REFERENCE_LANDMARKS * scale + observed = np.asarray(landmarks, dtype=np.float64).reshape(-1, 2) * scale + # Solving desired -> observed and inverting the map is what the demo does; + # it samples the source pixel for every destination pixel. + transform = _omz_transform(desired, observed) + return cv2.warpAffine( + crop, transform, (width, height), flags=cv2.WARP_INVERSE_MAP + ) diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/device.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/device.py new file mode 100644 index 00000000..028b3952 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/device.py @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Device discovery and validation for OpenVINO. + +A single shared :class:`openvino.Core` is reused across every model so we only +query the runtime once. +""" + +from __future__ import annotations + +import logging +from typing import Any + +import openvino as ov + +logger = logging.getLogger(__name__) + +# "AUTO" lets OpenVINO pick the best plugin; the others are explicit targets. +# "XPU" is accepted as an alias for an Intel GPU (see OVRunner). +SUPPORTED_DEVICES = {"AUTO", "CPU", "GPU", "NPU", "XPU"} + +_core: ov.Core | None = None + + +def get_core() -> ov.Core: + """Return the process-wide OpenVINO Core (created on first use).""" + global _core + if _core is None: + _core = ov.Core() + return _core + + +def get_available_devices() -> list[dict[str, Any]]: + """List the physical OpenVINO devices present on this machine.""" + try: + core = get_core() + devices = [] + for name in core.available_devices: + try: + full_name = core.get_property(name, "FULL_DEVICE_NAME") + except Exception: + full_name = name + if name.startswith("GPU"): + dev_type = "GPU" + elif name.startswith("NPU"): + dev_type = "NPU" + else: + dev_type = "CPU" + devices.append({"name": name, "full_name": full_name, "type": dev_type}) + logger.info( + "[DEVICE] Available: " + + ", ".join(f"{d['name']} ({d['full_name']})" for d in devices) + ) + return devices + except Exception as exc: + logger.warning(f"[DEVICE] Could not query OpenVINO devices: {exc}") + return [{"name": "CPU", "full_name": "CPU (fallback)", "type": "CPU"}] + + +def normalize_device(device: str | None) -> str: + """Validate and canonicalise a requested device string. + + A bare base name (``GPU``, ``AUTO``, ``CPU``, ``NPU``, ``XPU``) is + upper-cased and checked against the supported set. Indexed names such as + ``GPU.1`` or ``xpu:0`` are accepted so multi-GPU systems can target a + specific tile. + """ + if not device: + return "AUTO" + cleaned = device.strip() + sep = "." if "." in cleaned else (":" if ":" in cleaned else None) + base = (cleaned.split(sep)[0] if sep else cleaned).upper() + if base not in SUPPORTED_DEVICES: + raise ValueError( + f"Unsupported device '{device}'. " + f"Must be one of {sorted(SUPPORTED_DEVICES)} (optionally with an " + f"index, e.g. 'GPU.1')." + ) + if sep is None: + return base + return f"{base}{sep}{cleaned.split(sep, 1)[1]}" + + +def ov_device_available(device: str) -> bool: + """True when the base of an OpenVINO device string is actually present.""" + if device == "AUTO": + return True + try: + available = get_core().available_devices + except Exception: + return False + # available_devices lists indexed names on multi-device systems (GPU.0, + # GPU.1, ...), so match either exactly or on the base name. + base = device.split(".")[0] + return any(d == device or d.split(".")[0] == base for d in available) diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/download.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/download.py new file mode 100644 index 00000000..31080177 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/download.py @@ -0,0 +1,57 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Model asset download helpers (plain files and OpenVINO IR pairs).""" + +from __future__ import annotations + +import logging +from pathlib import Path + +import requests +from tqdm import tqdm + +logger = logging.getLogger(__name__) + +_CHUNK = 1 << 20 # 1 MiB + + +def download_file(url: str, dest: Path) -> Path: + """Stream ``url`` into ``dest`` (idempotent, .part temp + progress bar).""" + if dest.exists(): + return dest + dest.parent.mkdir(parents=True, exist_ok=True) + part = dest.with_suffix(dest.suffix + ".part") + logger.info(f"Downloading {url} -> {dest}") + with requests.get(url, stream=True, timeout=60) as resp: + resp.raise_for_status() + total = int(resp.headers.get("content-length", 0)) or None + with ( + open(part, "wb") as fh, + tqdm(total=total, unit="B", unit_scale=True, desc=dest.name) as bar, + ): + for chunk in resp.iter_content(chunk_size=_CHUNK): + fh.write(chunk) + bar.update(len(chunk)) + part.rename(dest) + return dest + + +def download_ir(xml_url: str, root: Path) -> Path: + """Fetch an OpenVINO IR pair (``.xml`` + ``.bin``) and return the ``.xml``. + + ``xml_url`` is an Open Model Zoo storage URL of the form + ``...///.xml``; the weights live next to + it. Files are mirrored under ``root///``. + """ + parts = xml_url.rstrip("/").split("/") + if len(parts) < 3 or not parts[-1].endswith(".xml"): + raise ValueError(f"Not an OpenVINO IR url: {xml_url}") + model_name, precision, filename = parts[-3], parts[-2], parts[-1] + dest_dir = root / model_name / precision + + xml_path = download_file(xml_url, dest_dir / filename) + download_file( + xml_url[: -len(".xml")] + ".bin", dest_dir / f"{filename[:-4]}.bin" + ) + return xml_path diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/image_io.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/image_io.py new file mode 100644 index 00000000..73f34d27 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/image_io.py @@ -0,0 +1,36 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Image decode / encode helpers shared by every input mode.""" + +from __future__ import annotations + +import base64 + +import cv2 +import numpy as np + + +class ImageDecodeError(ValueError): + """Raised when raw bytes cannot be decoded into an image.""" + + +def decode_image(data: bytes) -> np.ndarray: + """Decode raw image bytes (JPG/PNG/BMP/...) into a BGR ``np.ndarray``.""" + if not data: + raise ImageDecodeError("Empty image payload") + arr = np.frombuffer(data, np.uint8) + bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR) + if bgr is None: + raise ImageDecodeError( + "Could not decode image. Supported: JPG, PNG, BMP, TIFF, WEBP" + ) + return bgr + + +def encode_jpeg_base64(bgr: np.ndarray, quality: int = 90) -> str: + """Encode a BGR image as a base64 data URL (used for gallery thumbnails).""" + ok, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, int(quality)]) + if not ok: + raise RuntimeError("JPEG encoding failed") + return "data:image/jpeg;base64," + base64.b64encode(buf.tobytes()).decode("ascii") diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/runners.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/runners.py new file mode 100644 index 00000000..20a71be1 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/core/runners.py @@ -0,0 +1,77 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""OpenVINO inference runner. + +A runner is a callable ``(blob: np.ndarray) -> list[np.ndarray]`` returning the +network outputs in the model's declared output order, so pipeline code stays +free of runtime details. Both ONNX (OpenCV Zoo) and IR (Open Model Zoo) models +are read natively by OpenVINO. +""" + +from __future__ import annotations + +import logging +from pathlib import Path + +import numpy as np + +from core.device import get_core, ov_device_available + +logger = logging.getLogger(__name__) + + +class OVRunner: + """Compile a model with OpenVINO, falling back to CPU if needed.""" + + def __init__( + self, + model_path: Path, + device: str, + input_shape: list[int] | None = None, + ) -> None: + core = get_core() + model = core.read_model(model_path) + if input_shape is not None: + model.reshape(input_shape) + + # OpenVINO has no "XPU" plugin — an XPU request targets the same Intel + # GPU through the GPU plugin. + target = "GPU" if device.split(":")[0] == "XPU" else device + if not ov_device_available(target): + logger.warning( + f"[{model_path.name}] Device '{target}' not available in " + "OpenVINO; falling back to CPU." + ) + target = "CPU" + config = ( + {"GPU_DISABLE_WINOGRAD_CONVOLUTION": "YES"} if "GPU" in target else {} + ) + try: + self.compiled = core.compile_model(model, target, config) + except Exception as exc: + if target == "CPU": + raise + logger.warning( + f"[{model_path.name}] Compile on '{target}' failed ({exc}); " + "retrying on CPU." + ) + target = "CPU" + self.compiled = core.compile_model(model, target) + self.device = target + self.runtime = f"openvino:{target}" + self._request = self.compiled.create_infer_request() + self.input_shape = tuple(self.compiled.inputs[0].shape) + # An output tensor can carry several names; index them all so callers + # can look outputs up by the ONNX name regardless of plugin. + self.name_to_index: dict[str, int] = {} + for i, out in enumerate(self.compiled.outputs): + for name in out.get_names(): + self.name_to_index[name] = i + + def __call__(self, blob: np.ndarray) -> list[np.ndarray]: + self._request.infer({0: blob}) + return [ + self._request.get_output_tensor(i).data.copy() + for i in range(len(self.compiled.outputs)) + ] diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/main.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/main.py new file mode 100644 index 00000000..279360c6 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/main.py @@ -0,0 +1,176 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import argparse +import logging +import sys +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI, File, Form, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from starlette.concurrency import run_in_threadpool + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +# Force UTF-8 on stdout/stderr so Unicode output doesn't crash the worker on +# Windows consoles that default to cp1252. +for _stream in (sys.stdout, sys.stderr): + reconfigure = getattr(_stream, "reconfigure", None) + if reconfigure is not None: + reconfigure(encoding="utf-8", errors="replace") + +import config +from schemas import LoadModelRequest +from worker import FaceRecognitionWorker + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) +logger = logging.getLogger("face-recognition") + +# Populated from CLI args in __main__; defaults let `uvicorn main:app` work too. +STARTUP = { + "model": config.DEFAULT_MODEL, + "device": config.DEFAULT_DEVICE, + "autoload": True, +} + +worker = FaceRecognitionWorker() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + worker.startup( + default_model=STARTUP["model"], + device=STARTUP["device"], + autoload=STARTUP["autoload"], + ) + yield + worker.shutdown() + + +app = FastAPI(title="Face Recognition Worker", lifespan=lifespan) +app.add_middleware( + CORSMiddleware, + allow_origins=config.CORS_ORIGINS, + allow_methods=["GET", "POST", "DELETE"], + allow_headers=["*"], +) + + +# ── Health / devices ──────────────────────────────────────────────────────── + + +@app.get("/healthcheck") +def healthcheck(): + info = worker.healthcheck() + # Only report healthy (HTTP 200) once the model is loaded, so the frontend + # marks the service "online" only when it can actually serve inference. + status_code = 200 if info.get("model_loaded") else 503 + return JSONResponse(info, status_code=status_code) + + +@app.get("/devices") +def devices(): + from core.device import get_available_devices + + return {"devices": get_available_devices()} + + +# ── Model management ──────────────────────────────────────────────────────── + + +@app.get("/models") +def list_models(): + return worker.list_models() + + +@app.get("/models/active") +def active_model(): + return worker.get_active() + + +@app.post("/models/load") +async def load_model(req: LoadModelRequest): + return await run_in_threadpool( + worker.load_model, req.model, req.device, req.options + ) + + +# ── Reference gallery ─────────────────────────────────────────────────────── + + +@app.get("/gallery") +def list_gallery(): + return worker.list_gallery() + + +@app.post("/gallery") +async def enroll( + name: str = Form(...), + files: list[UploadFile] = File(...), +): + """Enroll a person from one or more reference images. + + Re-using an existing name appends the images to that person, so a person + can be enrolled incrementally with multiple reference shots. The original + image bytes are kept so a later model swap re-embeds the gallery. + """ + payload = [(f.filename or "image", await f.read()) for f in files] + return await run_in_threadpool(worker.enroll, name, payload) + + +@app.delete("/gallery/{person_id}") +def delete_person(person_id: str): + return worker.delete_person(person_id) + + +@app.delete("/gallery") +def clear_gallery(): + return worker.clear_gallery() + + +# ── Recognition ───────────────────────────────────────────────────────────── + + +@app.post("/recognize") +async def recognize(file: UploadFile = File(...)): + """Detect all faces and match each against the enrolled gallery.""" + data = await file.read() + return await run_in_threadpool(worker.recognize, data) + + +# ── Entrypoint ────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + import uvicorn + + parser = argparse.ArgumentParser(description="Face Recognition Worker") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=config.DEFAULT_PORT) + parser.add_argument( + "--model", + default=config.DEFAULT_MODEL, + help="Model preset to bring up on startup (see /models).", + ) + parser.add_argument( + "--device", + default=config.DEFAULT_DEVICE, + help="Device: AUTO|CPU|GPU|NPU (OpenVINO) or XPU (PyTorch fallback).", + ) + parser.add_argument( + "--no-autoload", + action="store_true", + help="Do not load the startup model until first /models/load.", + ) + args = parser.parse_args() + + STARTUP["model"] = args.model + STARTUP["device"] = args.device + STARTUP["autoload"] = not args.no_autoload + + uvicorn.run(app, host=args.host, port=args.port, log_level="info") diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/__init__.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/__init__.py new file mode 100644 index 00000000..91da480a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/base.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/base.py new file mode 100644 index 00000000..0481a2a6 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/base.py @@ -0,0 +1,165 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Abstract base for all face-recognition pipelines. + +A pipeline bundles a face *detector* and a face *embedder*: + + load() -> download + compile the model pair + _detect() -> find faces (box, score, 5 landmarks) in a BGR image + _embed() -> L2-normalised embedding for one aligned face + +``BaseFacePipeline`` wraps these with a re-entrant lock so a pipeline is loaded +exactly once and inference is serialised (OpenVINO infer requests are stateful +and not safe to call concurrently). +""" + +from __future__ import annotations + +import logging +import threading +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Any + +import numpy as np + +from core.device import normalize_device + +logger = logging.getLogger(__name__) + + +@dataclass +class Face: + """One detected face. Coordinates are pixels of the source image.""" + + box: tuple[float, float, float, float] # x, y, w, h + score: float + landmarks: np.ndarray # (5, 2) — eyes, nose, mouth corners + embedding: np.ndarray | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "box": [round(float(v), 2) for v in self.box], + "score": round(float(self.score), 4), + "landmarks": [ + [round(float(x), 2), round(float(y), 2)] for x, y in self.landmarks + ], + } + + +@dataclass +class DetectionTimings: + detect_ms: float = 0.0 + embed_ms: float = 0.0 + faces: list[Face] = field(default_factory=list) + + +class BaseFacePipeline(ABC): + #: Registry key / human-facing identifier. Set by the registry factory. + key: str = "base" + description: str = "" + #: Cosine-similarity decision threshold (embeddings are L2-normalised). + match_threshold: float = 0.5 + #: Runtime actually used after load(), e.g. "openvino:GPU" / "pytorch:xpu". + runtime: str = "" + + def __init__(self, device: str | None = None, **options: Any) -> None: + self.device = normalize_device(device) + self.options = options + self._loaded = False + # Re-entrant: ensure_loaded() holds the lock while warmup() re-acquires. + self._lock = threading.RLock() + + # ── Public API ───────────────────────────────────────────────────────── + + @property + def loaded(self) -> bool: + return self._loaded + + def ensure_loaded(self) -> None: + """Load + warm up the pipeline exactly once (idempotent, thread-safe).""" + if self._loaded: + return + with self._lock: + if self._loaded: + return + t0 = time.time() + logger.info(f"[{self.key}] Loading on device={self.device} ...") + self.load() + self.warmup() + self._loaded = True + logger.info( + f"[{self.key}] Ready ({self.runtime}) in " + f"{(time.time() - t0) * 1000:.0f} ms" + ) + + def detect(self, image: np.ndarray) -> DetectionTimings: + """Detect faces and compute an embedding for each one.""" + if not self._loaded: + self.ensure_loaded() + with self._lock: + t0 = time.time() + faces = self._detect(image) + t1 = time.time() + for face in faces: + face.embedding = self._embed(image, face) + t2 = time.time() + return DetectionTimings( + detect_ms=(t1 - t0) * 1000.0, + embed_ms=(t2 - t1) * 1000.0, + faces=faces, + ) + + def embed_largest_face(self, image: np.ndarray) -> Face | None: + """Detect + embed only the largest face (used for gallery enrollment).""" + if not self._loaded: + self.ensure_loaded() + with self._lock: + faces = self._detect(image) + if not faces: + return None + face = max(faces, key=lambda f: f.box[2] * f.box[3]) + face.embedding = self._embed(image, face) + return face + + def warmup(self) -> None: + """Default warmup: a single pass over a small blank image.""" + try: + self._detect(np.zeros((320, 320, 3), dtype=np.uint8)) + except Exception as exc: + logger.warning(f"[{self.key}] Warmup skipped: {exc}") + + def release(self) -> None: + """Drop compiled-model references so memory can be reclaimed.""" + with self._lock: + self._teardown() + self._loaded = False + + def _teardown(self) -> None: + """Subclasses override this (not ``release``) to drop model handles.""" + + def info(self) -> dict[str, Any]: + return { + "key": self.key, + "description": self.description, + "device": self.device, + "runtime": self.runtime, + "loaded": self._loaded, + "match_threshold": self.match_threshold, + } + + # ── To implement ─────────────────────────────────────────────────────── + + @abstractmethod + def load(self) -> None: + """Download + compile the detector and embedder. Called once.""" + + @abstractmethod + def _detect(self, image: np.ndarray) -> list[Face]: + """Detect faces in a BGR image.""" + + @abstractmethod + def _embed(self, image: np.ndarray, face: Face) -> np.ndarray: + """Return the L2-normalised embedding for one detected face.""" diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/__init__.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/__init__.py new file mode 100644 index 00000000..91da480a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/model.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/model.py new file mode 100644 index 00000000..3a336b1c --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/omz/model.py @@ -0,0 +1,178 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Open Model Zoo face recognition: detection -> landmarks -> reidentification. + +A direct port of the OMZ ``face_recognition_demo`` (Python) pipeline, which +chains three Intel pre-trained IR models: + +* **Face detection** — ``face-detection-retail-0004`` / ``face-detection-adas-0001`` + (SSD, one ``[1, 1, N, 7]`` output of ``[image_id, label, conf, x1, y1, x2, y2]`` + in normalised coordinates, sorted by confidence). Each ROI is expanded by + ``roi_scale_factor`` (the demo's ``-exp_r_fd``, default 1.15) before it is + handed to the next stage. +* **Landmarks regression** — ``landmarks-regression-retail-0009`` runs on the + cropped ROI and returns 5 points (eyes, nose tip, mouth corners) as + coordinates relative to that crop. +* **Face reidentification** — ``face-reidentification-retail-0095`` embeds the + landmark-aligned ROI into a 256-d descriptor. + +All three are Apache-2.0 Intel models served from storage.openvinotoolkit.org, +and every stage runs on OpenVINO natively from IR. + +The demo compares descriptors with ``0.5 * (1 - cosine_similarity)`` and calls a +face known when that distance is below ``-t_id`` (default 0.3); this worker +matches on cosine similarity directly, so the equivalent threshold is +``1 - 2 * 0.3 = 0.4``. +""" + +from __future__ import annotations + +import logging +from typing import Any + +import cv2 +import numpy as np + +import config +from core.align import align_roi_landmarks +from core.download import download_ir +from core.runners import OVRunner +from models.base import BaseFacePipeline, Face + +logger = logging.getLogger(__name__) + + +class OMZFacePipeline(BaseFacePipeline): + def __init__(self, device: str | None = None, **options: Any) -> None: + super().__init__(device, **options) + self.det_threshold = float(options.get("det_threshold", 0.6)) + self.roi_scale_factor = float(options.get("roi_scale_factor", 1.15)) + self.match_threshold = float(options.get("match_threshold", 0.4)) + self._det: OVRunner | None = None + self._landmarks: OVRunner | None = None + self._reid: OVRunner | None = None + + # ── Loading ──────────────────────────────────────────────────────────── + + def load(self) -> None: + root = config.MODELS_DIR / "open-model-zoo" + det_path = download_ir(self.options["det_url"], root) + lm_path = download_ir(self.options["landmarks_url"], root) + reid_path = download_ir(self.options["reid_url"], root) + + self._det = OVRunner(det_path, self.device) + self._landmarks = OVRunner(lm_path, self.device) + self._reid = OVRunner(reid_path, self.device) + self.runtime = self._det.runtime + + def _teardown(self) -> None: + self._det = None + self._landmarks = None + self._reid = None + + # ── Detection + landmarks ────────────────────────────────────────────── + + def _detect(self, image: np.ndarray) -> list[Face]: + assert self._det is not None + h, w = image.shape[:2] + # OMZ detectors take BGR 0..255 resized to their static input size — + # no aspect-ratio padding, exactly like the demo's resize_input(). + _, _, det_h, det_w = self._det.input_shape + blob = ( + cv2.resize(image, (det_w, det_h)) + .transpose(2, 0, 1)[None] + .astype(np.float32) + ) + detections = self._det(blob)[0].reshape(-1, 7) + + faces: list[Face] = [] + for row in detections: + score = float(row[2]) + # Rows are sorted by decreasing confidence and padded with zeros. + if score < self.det_threshold: + break + box = self._roi(row[3:7], w, h) + if box is None: + continue + faces.append( + Face( + box=box, + score=score, + landmarks=self._landmark_points(image, box), + ) + ) + return faces + + def _roi( + self, corners: np.ndarray, width: int, height: int + ) -> tuple[float, float, float, float] | None: + """Normalised [x1, y1, x2, y2] -> expanded, clipped pixel ROI.""" + x1, y1 = float(corners[0]) * width, float(corners[1]) * height + x2, y2 = float(corners[2]) * width, float(corners[3]) * height + bw, bh = x2 - x1, y2 - y1 + # -exp_r_fd: grow the box around its centre so the landmark and reid + # crops see some context beyond the detector's tight box. + pad = 0.5 * (self.roi_scale_factor - 1.0) + x1, y1 = x1 - bw * pad, y1 - bh * pad + bw, bh = bw * self.roi_scale_factor, bh * self.roi_scale_factor + + x1 = min(max(x1, 0.0), float(width)) + y1 = min(max(y1, 0.0), float(height)) + bw = min(bw, float(width) - x1) + bh = min(bh, float(height) - y1) + if bw < 1.0 or bh < 1.0: + return None + return (x1, y1, bw, bh) + + def _landmark_points( + self, image: np.ndarray, box: tuple[float, float, float, float] + ) -> np.ndarray: + """Run the landmarks regressor on one ROI, in source-image pixels.""" + assert self._landmarks is not None + crop, (x1, y1) = self._crop(image, box) + _, _, lm_h, lm_w = self._landmarks.input_shape + blob = ( + cv2.resize(crop, (lm_w, lm_h)) + .transpose(2, 0, 1)[None] + .astype(np.float32) + ) + relative = self._landmarks(blob)[0].reshape(-1, 2).astype(np.float64) + return relative * [crop.shape[1], crop.shape[0]] + [x1, y1] + + @staticmethod + def _crop( + image: np.ndarray, box: tuple[float, float, float, float] + ) -> tuple[np.ndarray, tuple[int, int]]: + """Integer-pixel ROI crop plus its top-left origin.""" + h, w = image.shape[:2] + x, y, bw, bh = box + x1 = int(np.clip(x, 0, w - 1)) + y1 = int(np.clip(y, 0, h - 1)) + x2 = int(np.clip(round(x + bw), x1 + 1, w)) + y2 = int(np.clip(round(y + bh), y1 + 1, h)) + return image[y1:y2, x1:x2], (x1, y1) + + # ── Recognition ──────────────────────────────────────────────────────── + + def _embed(self, image: np.ndarray, face: Face) -> np.ndarray: + assert self._reid is not None + crop, (x1, y1) = self._crop(image, face.box) + relative = (face.landmarks - [x1, y1]) / [crop.shape[1], crop.shape[0]] + aligned = align_roi_landmarks(crop, relative) + _, _, reid_h, reid_w = self._reid.input_shape + blob = ( + cv2.resize(aligned, (reid_w, reid_h)) + .transpose(2, 0, 1)[None] + .astype(np.float32) + ) + embedding = self._reid(blob)[0].reshape(-1).astype(np.float32) + # The demo normalises inside its cosine distance; we do it once here so + # the worker can match with a plain dot product. + return embedding / (np.linalg.norm(embedding) + 1e-12) + + def info(self) -> dict[str, Any]: + data = super().info() + data["det_threshold"] = self.det_threshold + data["roi_scale_factor"] = self.roi_scale_factor + return data diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/registry.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/registry.py new file mode 100644 index 00000000..d67fff05 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/registry.py @@ -0,0 +1,138 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Pipeline registry + factory. + +Each entry is a self-contained preset (model URLs, thresholds, backend). A +pipeline bundles a detector and a recogniser; exactly one pipeline is active +in the worker at a time. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from models.base import BaseFacePipeline + +# OpenCV Zoo hosts models via Git LFS; media.githubusercontent serves the blobs. +_ZOO = "https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models" +# Intel pre-trained IR models from the Open Model Zoo (Apache-2.0). +_OMZ = "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2023.0/models_bin/1" + + +def _ir(name: str, precision: str = "FP16") -> str: + return f"{_OMZ}/{name}/{precision}/{name}.xml" + + +# Shared by every Open Model Zoo preset — only the detector differs. +_OMZ_COMMON: dict[str, Any] = { + "landmarks_url": _ir("landmarks-regression-retail-0009"), + "reid_url": _ir("face-reidentification-retail-0095"), + # Demo defaults: -t_fd 0.6, -exp_r_fd 1.15, -t_id 0.3 expressed as cosine + # similarity (the demo's distance is 0.5 * (1 - similarity)). + "det_threshold": 0.6, + "roi_scale_factor": 1.15, + "match_threshold": 0.4, +} + + +@dataclass(frozen=True) +class ModelSpec: + key: str + family: str # "opencv-zoo" | "open-model-zoo" + label: str + description: str + options: dict[str, Any] = field(default_factory=dict) + + +REGISTRY: dict[str, ModelSpec] = { + "omz-retail": ModelSpec( + key="omz-retail", + family="open-model-zoo", + label="OMZ face-detection-retail-0004", + description=( + "Open Model Zoo face_recognition_demo pipeline: " + "face-detection-retail-0004 (300x300 SSD) + " + "landmarks-regression-retail-0009 + " + "face-reidentification-retail-0095 256-d — small and fast." + ), + options={ + **_OMZ_COMMON, + "det_url": _ir("face-detection-retail-0004"), + }, + ), + "omz-adas": ModelSpec( + key="omz-adas", + family="open-model-zoo", + label="OMZ face-detection-adas-0001", + description=( + "Open Model Zoo face_recognition_demo pipeline: " + "face-detection-adas-0001 (672x384 SSD, wider field of view) + " + "landmarks-regression-retail-0009 + " + "face-reidentification-retail-0095 256-d." + ), + options={ + **_OMZ_COMMON, + "det_url": _ir("face-detection-adas-0001"), + }, + ), + "yunet-sface": ModelSpec( + key="yunet-sface", + family="opencv-zoo", + label="YuNet + SFace", + description=( + "OpenCV Zoo YuNet detector + SFace 128-d recogniser on OpenVINO " + "— tiny and fast, cosine threshold 0.363." + ), + options={ + "det_url": f"{_ZOO}/face_detection_yunet/face_detection_yunet_2023mar.onnx", + "rec_url": f"{_ZOO}/face_recognition_sface/face_recognition_sface_2021dec.onnx", + "det_size": 640, + "score_threshold": 0.7, + "nms_threshold": 0.3, + "match_threshold": 0.363, + }, + ), +} + +DEFAULT_KEY = "omz-retail" + + +def list_specs() -> list[dict[str, Any]]: + """Serialisable summary of every registered preset.""" + return [ + { + "key": s.key, + "family": s.family, + "label": s.label, + "description": s.description, + } + for s in REGISTRY.values() + ] + + +def create_pipeline( + key: str, + device: str | None = None, + overrides: dict[str, Any] | None = None, +) -> BaseFacePipeline: + """Instantiate (but do not load) the pipeline for ``key``.""" + if key not in REGISTRY: + raise KeyError(f"Unknown model '{key}'. Available: {sorted(REGISTRY)}") + spec = REGISTRY[key] + opts = {**spec.options, **(overrides or {})} + + if spec.family == "opencv-zoo": + from models.yunet_sface.model import YuNetSFacePipeline + + pipeline: BaseFacePipeline = YuNetSFacePipeline(device=device, **opts) + elif spec.family == "open-model-zoo": + from models.omz.model import OMZFacePipeline + + pipeline = OMZFacePipeline(device=device, **opts) + else: + raise KeyError(f"Unknown model family '{spec.family}'") + pipeline.key = spec.key + pipeline.description = spec.description + return pipeline diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/__init__.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/__init__.py new file mode 100644 index 00000000..91da480a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/model.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/model.py new file mode 100644 index 00000000..4660d20f --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/models/yunet_sface/model.py @@ -0,0 +1,145 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""YuNet (detection) + SFace (recognition) on OpenVINO. + +Both models come from the OpenCV Zoo as ONNX and are read natively by +OpenVINO. OpenCV's ``FaceDetectorYN``/``FaceRecognizerSF`` wrappers are tied to +the OpenCV DNN backend, so the pre/post-processing they normally do is +reimplemented here (ported from OpenCV's C++ implementation): + +* YuNet — raw BGR input; per-stride (8/16/32) grids of cls/obj scores, box + regressions (centre offset + log size) and 5 landmarks; score = + sqrt(cls * obj); NMS at the end. +* SFace — 112x112 crop aligned to the ArcFace template, RGB 0..255 input, + 128-d embedding compared by cosine similarity (OpenCV threshold 0.363). +""" + +from __future__ import annotations + +import logging +from typing import Any + +import cv2 +import numpy as np + +import config +from core.align import norm_crop +from core.download import download_file +from core.runners import OVRunner +from models.base import BaseFacePipeline, Face + +logger = logging.getLogger(__name__) + +_STRIDES = (8, 16, 32) + + +class YuNetSFacePipeline(BaseFacePipeline): + def __init__(self, device: str | None = None, **options: Any) -> None: + super().__init__(device, **options) + self.det_size = int(options.get("det_size", 640)) + self.score_threshold = float(options.get("score_threshold", 0.7)) + self.nms_threshold = float(options.get("nms_threshold", 0.3)) + self.match_threshold = float(options.get("match_threshold", 0.363)) + self._det: OVRunner | None = None + self._rec: OVRunner | None = None + + # ── Loading ──────────────────────────────────────────────────────────── + + def load(self) -> None: + det_path = download_file( + self.options["det_url"], config.MODELS_DIR / "yunet" / "yunet.onnx" + ) + rec_path = download_file( + self.options["rec_url"], config.MODELS_DIR / "sface" / "sface.onnx" + ) + # YuNet's ONNX has dynamic spatial dims; fix them once so the decode + # grids are known and the model compiles a single static shape. + self._det = OVRunner( + det_path, + self.device, + input_shape=[1, 3, self.det_size, self.det_size], + ) + self._rec = OVRunner(rec_path, self.device) + self.runtime = self._det.runtime + + def _teardown(self) -> None: + self._det = None + self._rec = None + + # ── Detection ────────────────────────────────────────────────────────── + + def _detect(self, image: np.ndarray) -> list[Face]: + assert self._det is not None + h, w = image.shape[:2] + scale = min(self.det_size / w, self.det_size / h) + new_w, new_h = int(round(w * scale)), int(round(h * scale)) + resized = cv2.resize(image, (new_w, new_h)) + padded = np.zeros((self.det_size, self.det_size, 3), dtype=np.uint8) + padded[:new_h, :new_w] = resized + + blob = padded.transpose(2, 0, 1)[None].astype(np.float32) + outputs = self._det(blob) + index = self._det.name_to_index + named = {name: outputs[i] for name, i in index.items()} + + boxes: list[list[float]] = [] + scores: list[float] = [] + landmarks: list[np.ndarray] = [] + for stride in _STRIDES: + cls = named[f"cls_{stride}"].reshape(-1) + obj = named[f"obj_{stride}"].reshape(-1) + bbox = named[f"bbox_{stride}"].reshape(-1, 4) + kps = named[f"kps_{stride}"].reshape(-1, 10) + + cols = self.det_size // stride + score = np.sqrt(np.clip(cls, 0, 1) * np.clip(obj, 0, 1)) + keep = np.where(score >= self.score_threshold)[0] + if keep.size == 0: + continue + rows_idx = keep // cols + cols_idx = keep % cols + cx = (cols_idx + bbox[keep, 0]) * stride + cy = (rows_idx + bbox[keep, 1]) * stride + bw = np.exp(bbox[keep, 2]) * stride + bh = np.exp(bbox[keep, 3]) * stride + x1 = cx - bw / 2 + y1 = cy - bh / 2 + + kp = kps[keep].reshape(-1, 5, 2) + kp[:, :, 0] = (cols_idx[:, None] + kp[:, :, 0]) * stride + kp[:, :, 1] = (rows_idx[:, None] + kp[:, :, 1]) * stride + + for i in range(keep.size): + boxes.append([float(x1[i]), float(y1[i]), float(bw[i]), float(bh[i])]) + scores.append(float(score[keep[i]])) + landmarks.append(kp[i]) + + if not boxes: + return [] + picked = cv2.dnn.NMSBoxes( + boxes, scores, self.score_threshold, self.nms_threshold + ) + faces = [] + for idx in np.asarray(picked).flatten(): + x, y, bw, bh = (v / scale for v in boxes[idx]) + faces.append( + Face( + box=(x, y, bw, bh), + score=scores[idx], + landmarks=landmarks[idx] / scale, + ) + ) + faces.sort(key=lambda f: f.score, reverse=True) + return faces + + # ── Recognition ──────────────────────────────────────────────────────── + + def _embed(self, image: np.ndarray, face: Face) -> np.ndarray: + assert self._rec is not None + aligned = norm_crop(image, face.landmarks) + # OpenCV FaceRecognizerSF: blobFromImage(aligned, 1.0, 112x112, + # mean=0, swapRB=True) -> RGB, raw 0..255. + blob = aligned[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) + embedding = self._rec(blob)[0].reshape(-1).astype(np.float32) + return embedding / (np.linalg.norm(embedding) + 1e-12) diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/pyproject.toml b/samples/ai/edge-ai-demo-studio/workers/face-recognition/pyproject.toml new file mode 100644 index 00000000..002aa663 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "face-recognition" +version = "0.1.0" +description = "Face recognition FastAPI worker (Open Model Zoo and OpenCV Zoo pipelines on OpenVINO)" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.111.0", + "uvicorn>=0.29.0", + "python-multipart>=0.0.9", + # Use opencv-python (not headless) so it works on Windows desktop too. + "opencv-python>=4.10", + "numpy>=1.24,<2.2", + "openvino>=2025.2", + "requests>=2.31", + "tqdm>=4.66", +] + +[tool.uv] +environments = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", +] diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/schemas.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/schemas.py new file mode 100644 index 00000000..6eb7c87d --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/schemas.py @@ -0,0 +1,16 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Pydantic request models for the face-recognition worker.""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + + +class LoadModelRequest(BaseModel): + model: str + device: str | None = None + options: dict[str, Any] | None = None diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.ps1 b/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.ps1 new file mode 100644 index 00000000..db21828c --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.ps1 @@ -0,0 +1,18 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +$ErrorActionPreference = "Stop" + +$ScriptDir = $PSScriptRoot +$WorkersDir = (Resolve-Path (Join-Path $ScriptDir "..")).Path +$UvCmd = Join-Path $WorkersDir "thirdparty\uv\uv.exe" +if (-not (Test-Path $UvCmd)) { $UvCmd = "uv" } + +if (-not (Test-Path $UvCmd) -and -not (Get-Command uv -ErrorAction SilentlyContinue)) { + Write-Host "ERROR: uv not found." -ForegroundColor Red + Write-Host "Please run the workers setup script first." -ForegroundColor Red + exit 1 +} + +Set-Location $ScriptDir +& $UvCmd run main.py @args diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.sh b/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.sh new file mode 100755 index 00000000..a640b23c --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/start.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKERS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +UV_CMD="$WORKERS_DIR/thirdparty/uv/uv" +[ -x "$UV_CMD" ] || UV_CMD="uv" + +if ! [ -x "$UV_CMD" ] && ! command -v uv >/dev/null 2>&1; then + echo "ERROR: uv not found." + echo "Please run the workers setup script first." + exit 1 +fi + +cd "$SCRIPT_DIR" +exec "$UV_CMD" run main.py "$@" diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/uv.lock b/samples/ai/edge-ai-demo-studio/workers/face-recognition/uv.lock new file mode 100644 index 00000000..c0fa2cca --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/uv.lock @@ -0,0 +1,534 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", +] +supported-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "idna", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "face-recognition" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opencv-python", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openvino", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-multipart", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "uvicorn", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.111.0" }, + { name = "numpy", specifier = ">=1.24,<2.2" }, + { name = "opencv-python", specifier = ">=4.10" }, + { name = "openvino", specifier = ">=2025.2" }, + { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "requests", specifier = ">=2.31" }, + { name = "tqdm", specifier = ">=4.66" }, + { name = "uvicorn", specifier = ">=0.29.0" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "starlette", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862, upload-time = "2024-11-02T17:31:41.486Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151, upload-time = "2024-11-02T17:32:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107, upload-time = "2024-11-02T17:32:34.361Z" }, + { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789, upload-time = "2024-11-02T17:32:57.152Z" }, + { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706, upload-time = "2024-11-02T17:33:09.12Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165, upload-time = "2024-11-02T17:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715, upload-time = "2024-11-02T17:35:06.564Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644, upload-time = "2024-11-02T17:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217, upload-time = "2024-11-02T17:35:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053, upload-time = "2024-11-02T17:36:22.3Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741, upload-time = "2024-11-02T17:36:33.552Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487, upload-time = "2024-11-02T17:36:52.909Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089, upload-time = "2024-11-02T17:38:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185, upload-time = "2024-11-02T17:38:51.07Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751, upload-time = "2024-11-02T17:39:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705, upload-time = "2024-11-02T17:39:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077, upload-time = "2024-11-02T17:39:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858, upload-time = "2024-11-02T17:40:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451, upload-time = "2024-11-02T17:41:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822, upload-time = "2024-11-02T17:42:07.595Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822, upload-time = "2024-11-02T17:42:32.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598, upload-time = "2024-11-02T17:42:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021, upload-time = "2024-11-02T17:46:19.171Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405, upload-time = "2024-11-02T17:46:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951, upload-time = "2024-11-02T17:44:25.881Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655, upload-time = "2024-11-02T17:44:50.115Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902, upload-time = "2024-11-02T17:45:15.685Z" }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180, upload-time = "2024-11-02T17:45:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907, upload-time = "2024-11-02T17:45:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098, upload-time = "2024-11-02T17:46:07.941Z" }, + { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601, upload-time = "2024-11-02T17:47:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397, upload-time = "2024-11-02T17:48:05.988Z" }, +] + +[[package]] +name = "opencv-python" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" }, + { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" }, +] + +[[package]] +name = "openvino" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openvino-telemetry", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/e0/0927427c7e1ae724bdc3c8e1908bc0f06fd019c20d3100a30627a1bf6ebb/openvino-2026.3.0-22451-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d57ed8cd04d6e4991e634afe4f1f89b42e8967fe856c50e0fd06fd2107ac37d", size = 57464906, upload-time = "2026-08-04T09:05:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d6/8c6279c3894bfa0897807b3f2a4393ae72bfec7d1b6a0d416f0ef2eaffb6/openvino-2026.3.0-22451-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:5caa09e142a4c167a2662c1b80230e2879beb9a58dace0ae831d9225fb4a8c0a", size = 28758816, upload-time = "2026-08-04T09:05:59.185Z" }, + { url = "https://files.pythonhosted.org/packages/ba/81/d7d7217c8a6d1f57dac3ff6d45a914bdf9ceb29fe84591115c38ae7c0982/openvino-2026.3.0-22451-cp310-cp310-win_amd64.whl", hash = "sha256:697a7b349eba363553745ee4987f6bc9eae1063f2f56a9abb46a3c06506901b0", size = 75784427, upload-time = "2026-08-04T09:06:03.98Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/b162407883ad51958d481c2af29d59e694caf4d8216b0c409c59294fd1aa/openvino-2026.3.0-22451-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:93e111164ccfd5c801edcfc3437683210837463bf57a2bd0518b940598664c2c", size = 57472600, upload-time = "2026-08-04T09:06:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5b/6cb9709ced1c2f0a89886e70980278363b1240b50ece7cecf9021342761e/openvino-2026.3.0-22451-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:e13cfe5fad386d615caa55592dc48be31099506eafea2cdb6dda06f2455bd248", size = 28768848, upload-time = "2026-08-04T09:06:14.926Z" }, + { url = "https://files.pythonhosted.org/packages/03/f8/4970bc51a626c07c4d09a2c9395c13e99dfadca05b2c037fd612927f913a/openvino-2026.3.0-22451-cp311-cp311-win_amd64.whl", hash = "sha256:7b688e38cc129cc268253c17d29a70d57e6e3ef8fb477a211294d1506440da15", size = 75789537, upload-time = "2026-08-04T09:06:19.658Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3c/e5eb8b5e52877251d2e98e91781b265588e10cc2ddf82265285ef808f20f/openvino-2026.3.0-22451-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a6887adfddf18837e6ed8230c837702d878db83b50b96c5759f8484be495f696", size = 57480757, upload-time = "2026-08-04T09:06:27.245Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/a726bd2e1e671d1d3aa06def11c24f8c6dbffb076b5d1d29635ffdd69fb7/openvino-2026.3.0-22451-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:70cd11e4b14bde48842b611630dc5b141fbfc235ff863640e08463b86a7816de", size = 28760034, upload-time = "2026-08-04T09:06:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/59/c0/0321a5bc8179b589a6f9fe401d72ec9e95912cdf62dec3a2e7b6f9f4d807/openvino-2026.3.0-22451-cp312-cp312-win_amd64.whl", hash = "sha256:865d2e7e947e544d67117cf79dc160241584ce2138dd5a878e91053ba4f50bf0", size = 75797293, upload-time = "2026-08-04T09:06:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d3/69e7083339f32621359f0bce2be3a7454db3c9a71fa7492f0a0941c00037/openvino-2026.3.0-22451-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7d09f20f009b9167a863f615a2b27400ce025bda8086cc72a2eb6ac3bf1d2af", size = 57480757, upload-time = "2026-08-04T09:06:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/80/61/19ac3641dcfcaee5bf9ada945579b3ab69701750e5270346633c56fdf876/openvino-2026.3.0-22451-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:0b071a4e2f731ab29bb9a08bab804c326cc4893283274c352ca56fccafa44749", size = 28760034, upload-time = "2026-08-04T09:06:47.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/f8/f71508784562a3d427f52f59d74a6364d559e6e82bb112cab5d6a6a677bb/openvino-2026.3.0-22451-cp313-cp313-win_amd64.whl", hash = "sha256:1c41f2d1072d600c260741b350aaf4a09d53f821a9a8fc8989bdc79a46b50a2c", size = 75797507, upload-time = "2026-08-04T09:06:51.858Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/fdace942843da232ea06a5a67cc3a70d292873657dc933408fdb9bb796a8/openvino-2026.3.0-22451-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:81a64aebd1a80da93bc9413b3ba9c93846c7cac57161cd4d7b287b354d77ad19", size = 57482441, upload-time = "2026-08-04T09:06:59.277Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/e2c7693ebfc6b5833b0065fc52359b0f63e5004f8e49c31375be4b13445e/openvino-2026.3.0-22451-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:875bb9df4bc694a86541c8598f60a6c797a29d20daf493681db9506eee6fe04f", size = 28767134, upload-time = "2026-08-04T09:07:02.289Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/ed637225c7373d9a4267e7fa7252c1f3767fbc9853a906d78068a279b73a/openvino-2026.3.0-22451-cp314-cp314-win_amd64.whl", hash = "sha256:07a8c1cb32e3f7942aab4a0c48404b591e63c89813906c7bc5b001f94bed447c", size = 75798958, upload-time = "2026-08-04T09:07:07.28Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3c/40c0bbd5c57fc0b5c0c41f9e6f0ec722f231ff25c37d20240d2baf446409/openvino-2026.3.0-22451-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:65f34103c28107e830e1fb70cb9c62afdb31cfde4f8b6056c942761796b1d4fe", size = 57526976, upload-time = "2026-08-04T09:07:15.131Z" }, + { url = "https://files.pythonhosted.org/packages/7b/33/b8c8fdf461a595b6d4f817224c5edd217494ed0013227513ce4804d3dbf4/openvino-2026.3.0-22451-cp314-cp314t-manylinux_2_35_aarch64.whl", hash = "sha256:d1ee0ecb6abfbe30b723ed6d97d7d55bc5b80b6a3fb8149fd4c97f4c45ef7e4f", size = 25899258, upload-time = "2026-08-04T09:07:18.216Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ca/927354fa957dd0e8c8f53401dcd1239c4cd50d95a26ff5da3bc4b502f4dc/openvino-2026.3.0-22451-cp314-cp314t-win_amd64.whl", hash = "sha256:17581c5acbdaf9bf503cd21d5ad852df7e547073ad7a529661b19f40ab3c8db6", size = 75963071, upload-time = "2026-08-04T09:07:24.796Z" }, +] + +[[package]] +name = "openvino-telemetry" +version = "2025.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/8a/89d82f1a9d913fb266c2e6dc2f6030935db24b7152963a8db6c4f039787f/openvino_telemetry-2025.2.0.tar.gz", hash = "sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c", size = 18894, upload-time = "2025-07-07T10:29:51.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ac/5ab0ca0aa269ad3c73f7bfc3801b10e5f56f75a31bf68c1ae8bd51cf70a4/openvino_telemetry-2025.2.0-py3-none-any.whl", hash = "sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599", size = 25227, upload-time = "2025-07-07T10:29:50.189Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-core", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "charset-normalizer", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "idna", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "h11", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, +] diff --git a/samples/ai/edge-ai-demo-studio/workers/face-recognition/worker.py b/samples/ai/edge-ai-demo-studio/workers/face-recognition/worker.py new file mode 100644 index 00000000..54225550 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/face-recognition/worker.py @@ -0,0 +1,346 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""Orchestrator that ties the active pipeline, devices and the gallery together. + +``main.py`` holds only FastAPI routing; every route delegates to one method +here. This class owns the single *active* pipeline and serialises model swaps +so a load in flight never races an inference. + +The gallery keeps the original reference-image bytes, so when a different +pipeline is loaded later the references are re-embedded for it automatically +(embeddings are cached per pipeline key). +""" + +from __future__ import annotations + +import logging +import threading +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +import cv2 +import numpy as np +from fastapi import HTTPException + +from config import DEFAULT_DEVICE +from core import device as device_mod +from core.image_io import ImageDecodeError, decode_image, encode_jpeg_base64 +from models import registry +from models.base import BaseFacePipeline, Face + +logger = logging.getLogger(__name__) + + +@dataclass +class GalleryImage: + """One enrolled reference image and its per-pipeline embeddings.""" + + data: bytes + thumbnail: str # base64 JPEG of the detected face crop + embeddings: dict[str, np.ndarray] = field(default_factory=dict) + + +@dataclass +class Person: + id: str + name: str + images: list[GalleryImage] = field(default_factory=list) + + def public(self) -> dict[str, Any]: + return { + "id": self.id, + "name": self.name, + "num_images": len(self.images), + "thumbnails": [img.thumbnail for img in self.images], + } + + +class FaceRecognitionWorker: + def __init__(self) -> None: + self._device = DEFAULT_DEVICE + self._model_key: str | None = None + self._pipeline: BaseFacePipeline | None = None + self._swap_lock = threading.Lock() + self._gallery: dict[str, Person] = {} + self._gallery_lock = threading.Lock() + + # ── Lifecycle ────────────────────────────────────────────────────────── + + def startup( + self, + default_model: str, + device: str | None = None, + autoload: bool = True, + ) -> None: + self._device = device_mod.normalize_device(device or DEFAULT_DEVICE) + logger.info("=" * 60) + for d in device_mod.get_available_devices(): + logger.info(f" {d['name']:8s} | {d['full_name']}") + logger.info(f" default device requested: {self._device}") + logger.info("=" * 60) + + if autoload: + try: + self.load_model(default_model, self._device) + except Exception as exc: + # Fail hard: a startup model that can't load means the worker + # cannot serve. Re-raise so the process exits non-zero instead + # of running half-broken. + logger.error(f"Autoload of '{default_model}' failed: {exc}") + raise + + def shutdown(self) -> None: + if self._pipeline is not None: + self._pipeline.release() + logger.info("Face-recognition worker shut down") + + # ── Model management ─────────────────────────────────────────────────── + + def load_model( + self, + key: str, + device: str | None = None, + options: dict[str, Any] | None = None, + ) -> dict[str, Any]: + try: + target_device = device_mod.normalize_device(device or self._device) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + try: + new_pipeline = registry.create_pipeline(key, target_device, options) + except KeyError as exc: + raise HTTPException(status_code=404, detail=str(exc)) + + # Load outside the swap lock is not needed here (downloads are guarded + # per-file), but serialise concurrent loads so two requests don't both + # build a pipeline. + with self._swap_lock: + try: + new_pipeline.ensure_loaded() + except Exception as exc: + logger.error(f"Failed to load '{key}': {exc}", exc_info=True) + raise HTTPException( + status_code=500, detail=f"Failed to load '{key}': {exc}" + ) + old = self._pipeline + self._pipeline = new_pipeline + self._model_key = key + self._device = target_device + if old is not None and old is not new_pipeline: + old.release() + + self._reembed_gallery() + logger.info(f"Active model -> {key} on {target_device}") + return self.get_active() + + def list_models(self) -> dict[str, Any]: + return { + "active": self._model_key, + "device": self._device, + "available": registry.list_specs(), + } + + def get_active(self) -> dict[str, Any]: + if self._pipeline is None: + return {"active": None, "loaded": False} + info = self._pipeline.info() + info["active"] = self._model_key + return info + + def _require_pipeline(self) -> BaseFacePipeline: + if self._pipeline is None or not self._pipeline.loaded: + raise HTTPException( + status_code=503, + detail="No model loaded. Call POST /models/load first.", + ) + return self._pipeline + + # ── Health ───────────────────────────────────────────────────────────── + + def healthcheck(self) -> dict[str, Any]: + return { + "status": "ok", + "active_model": self._model_key, + "model_loaded": self._pipeline is not None and self._pipeline.loaded, + "device": self._device, + "gallery_size": len(self._gallery), + } + + # ── Gallery ──────────────────────────────────────────────────────────── + + def list_gallery(self) -> dict[str, Any]: + with self._gallery_lock: + return {"persons": [p.public() for p in self._gallery.values()]} + + def enroll(self, name: str, files: list[tuple[str, bytes]]) -> dict[str, Any]: + pipeline = self._require_pipeline() + name = name.strip() + if not name: + raise HTTPException(status_code=400, detail="Person name is required") + if not files: + raise HTTPException( + status_code=400, detail="At least one reference image is required" + ) + + accepted: list[GalleryImage] = [] + file_results: list[dict[str, Any]] = [] + for filename, data in files: + status: dict[str, Any] = {"file": filename} + try: + image = decode_image(data) + except ImageDecodeError as exc: + status["error"] = str(exc) + file_results.append(status) + continue + + face = pipeline.embed_largest_face(image) + if face is None or face.embedding is None: + status["error"] = "No face detected in this image" + file_results.append(status) + continue + + accepted.append( + GalleryImage( + data=data, + thumbnail=self._face_thumbnail(image, face), + embeddings={pipeline.key: face.embedding}, + ) + ) + file_results.append(status) + + if not accepted: + raise HTTPException( + status_code=400, + detail="No face was detected in any of the provided images", + ) + + with self._gallery_lock: + person = next( + ( + p + for p in self._gallery.values() + if p.name.lower() == name.lower() + ), + None, + ) + if person is None: + person = Person(id=uuid.uuid4().hex[:8], name=name) + self._gallery[person.id] = person + person.images.extend(accepted) + return {"person": person.public(), "files": file_results} + + def delete_person(self, person_id: str) -> dict[str, Any]: + with self._gallery_lock: + if person_id not in self._gallery: + raise HTTPException(status_code=404, detail="Unknown person id") + del self._gallery[person_id] + return self.list_gallery() + + def clear_gallery(self) -> dict[str, Any]: + with self._gallery_lock: + self._gallery.clear() + return self.list_gallery() + + def _reembed_gallery(self) -> None: + """Compute missing embeddings for the active pipeline after a swap.""" + pipeline = self._pipeline + if pipeline is None: + return + with self._gallery_lock: + for person in self._gallery.values(): + for img in person.images: + if pipeline.key in img.embeddings: + continue + try: + image = decode_image(img.data) + except ImageDecodeError: + continue + face = pipeline.embed_largest_face(image) + if face is not None and face.embedding is not None: + img.embeddings[pipeline.key] = face.embedding + + @staticmethod + def _face_thumbnail(image: np.ndarray, face: Face, size: int = 96) -> str: + """Square crop around the detected box, encoded as a base64 JPEG.""" + h, w = image.shape[:2] + x, y, bw, bh = face.box + cx, cy = x + bw / 2, y + bh / 2 + half = max(bw, bh) * 0.65 + x1 = int(max(0, cx - half)) + y1 = int(max(0, cy - half)) + x2 = int(min(w, cx + half)) + y2 = int(min(h, cy + half)) + crop = image[y1:y2, x1:x2] + if crop.size == 0: + crop = image + crop = cv2.resize(crop, (size, size)) + return encode_jpeg_base64(crop) + + # ── Recognition ──────────────────────────────────────────────────────── + + def recognize(self, data: bytes) -> dict[str, Any]: + pipeline = self._require_pipeline() + try: + image = decode_image(data) + except ImageDecodeError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + t0 = time.time() + try: + timings = pipeline.detect(image) + except Exception as exc: + logger.error(f"Inference error: {exc}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Inference error: {exc}") + + faces = [] + for face in timings.faces: + entry = face.to_dict() + entry.update(self._match(pipeline.key, face, pipeline.match_threshold)) + faces.append(entry) + + spec = registry.REGISTRY.get(pipeline.key) + h, w = image.shape[:2] + return { + "elapsed_ms": round((time.time() - t0) * 1000, 2), + "image": {"width": w, "height": h}, + "gallery_size": len(self._gallery), + "model": pipeline.key, + "label": spec.label if spec else pipeline.key, + "runtime": pipeline.runtime, + "threshold": pipeline.match_threshold, + "detect_ms": round(timings.detect_ms, 2), + "embed_ms": round(timings.embed_ms, 2), + "num_faces": len(faces), + "faces": faces, + } + + def _match( + self, pipeline_key: str, face: Face, threshold: float + ) -> dict[str, Any]: + """Cosine-match one embedding against every enrolled person.""" + if face.embedding is None: + return {"match": None, "matched": False, "similarities": []} + with self._gallery_lock: + sims = [] + for person in self._gallery.values(): + person_sims = [ + float(np.dot(face.embedding, img.embeddings[pipeline_key])) + for img in person.images + if pipeline_key in img.embeddings + ] + if person_sims: + sims.append( + { + "person_id": person.id, + "name": person.name, + "similarity": round(max(person_sims), 4), + } + ) + sims.sort(key=lambda s: -s["similarity"]) + best = sims[0] if sims else None + matched = best is not None and best["similarity"] >= threshold + return {"match": best, "matched": matched, "similarities": sims} diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/.gitignore b/samples/ai/edge-ai-demo-studio/workers/frame-generation/.gitignore new file mode 100644 index 00000000..0b82205d --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/.gitignore @@ -0,0 +1,3 @@ +/data +/logs +/models diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/README.md b/samples/ai/edge-ai-demo-studio/workers/frame-generation/README.md new file mode 100644 index 00000000..47bfb763 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/README.md @@ -0,0 +1,36 @@ +# Frame Generation Worker + +Standalone RIFE (IFNet) video frame interpolation service running on OpenVINO +(CPU/GPU/NPU). + +It serves two use cases: + +- **Pairwise interpolation** (`POST /v1/frame-generation/interpolate`) — fill + the frames between keyframe pairs. Used by the lipsync worker to reach the + avatar frame rate when Wav2Lip inference alone cannot keep up. +- **Video interpolation** (`POST /v1/frame-generation/video`) — upload a video + and interpolate between every pair of consecutive frames, either to multiply + its frame rate (`mode=fps`, 2x-4x, same duration, audio kept) or to produce + smooth slow motion (`mode=slowmo`, same frame rate, 2x-4x duration, audio + dropped). Jobs are queued and run one at a time on a dedicated worker + thread; poll `GET /v1/tasks/{taskId}` for queue position and progress. + +There is also `POST /v1/frame-generation/benchmark`, which measures the +interpolation throughput (frames/sec) for a given gap schedule so callers such +as the lipsync planner can decide how densely to interpolate. + +## Run + +```bash +./start.sh --port 8031 --device GPU +``` + +Arguments: + +- `--port` — server port (default `8031`) +- `--device` — OpenVINO device for the RIFE model: `CPU`, `GPU`, `GPU.1`, `NPU` + (default `CPU`) +- `--source` — model download source: `huggingface` or `modelscope` + +The RIFE weights are downloaded to `models/rife/` on first start and converted +to an OpenVINO IR automatically. diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/main.py b/samples/ai/edge-ai-demo-studio/workers/frame-generation/main.py new file mode 100644 index 00000000..720518f0 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/main.py @@ -0,0 +1,330 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import io +import queue +import re +import shutil +from contextlib import asynccontextmanager +from itertools import count +from pathlib import Path +from threading import Lock, Thread +from uuid import uuid4 + +import numpy as np +import uvicorn +from fastapi import ( + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, JSONResponse, Response +from pydantic import BaseModel, Field + +from modules.frame_generator_ov import ( + OpenVINOFrameGenerator, + download_rife_model, + measure_framegen_fps, +) +from modules.logger import getLogger +from modules.video import interpolate_video_file, probe_video + +UPLOAD_DIR = Path("data/uploads") +OUTPUT_DIR = Path("data/outputs") +MAX_MULTIPLIER = 4 +# One gap in a single interpolate request may ask for at most this many +# frames; guards the level-synchronous subdivision from unbounded recursion. +MAX_FRAMES_PER_GAP = 15 +TASK_ID_RE = re.compile(r"^[a-f0-9]{8}$") + +tasks = {} + +# The single shared generator and the lock serializing its inference request +# (the underlying OpenVINO infer request is not thread-safe). +state = {"generator": None, "lock": Lock(), "device": "CPU"} + +# Video jobs run one at a time on a dedicated worker thread, decoupled from +# the request lifecycle: the upload returns immediately with a task id and +# additional uploads queue behind the running job instead of competing for +# the (serialized) generator or tying up the server's request thread pool. +video_jobs = queue.Queue() +_job_seq = count() + + +def _video_job_worker(): + while True: + job = video_jobs.get() + if job is None: + break + run_video_interpolation(**job) + video_jobs.task_done() + + +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--port", + type=str, + default="8031", + help="Server port (default: 8031)", + ) + parser.add_argument( + "--device", + type=str, + default="CPU", + help="OpenVINO device for the RIFE model (CPU/GPU/GPU.1/NPU)", + ) + parser.add_argument( + "--source", + type=str, + default="huggingface", + choices=["huggingface", "modelscope"], + help="Model source (default: huggingface)", + ) + return parser.parse_args() + + +class BenchmarkRequest(BaseModel): + image_size: int = Field(default=256, ge=32, le=2048) + gap_sizes: list[int] = Field(default=[1], min_length=1, max_length=64) + rounds: int = Field(default=3, ge=1, le=10) + + +def create_app(args): + @asynccontextmanager + async def lifespan(app: FastAPI): + getLogger(__name__).info("Starting lifespan...") + try: + download_rife_model(source=args.source) + generator = OpenVINOFrameGenerator(args.device) + generator.warm_up(256) + state["generator"] = generator + state["device"] = args.device.upper() + Thread(target=_video_job_worker, daemon=True).start() + app.state.ready = True + getLogger(__name__).info("Startup complete; server is ready.") + except Exception as e: + getLogger(__name__).error(f"Error in lifespan startup: {e}") + exit(1) + yield + video_jobs.put(None) + + app = FastAPI(lifespan=lifespan) + # Not ready until lifespan startup (model download/convert/warmup) finishes. + app.state.ready = False + + app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:8080", "http://127.0.0.1:8080"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + return app + + +def run_video_interpolation(task_id, video_path, multiplier, mode): + tasks[task_id] = {"status": "running", "progress": 0.0} + + def on_progress(fraction): + tasks[task_id]["progress"] = round(fraction, 3) + + output_path = OUTPUT_DIR / f"{task_id}.mp4" + try: + info = interpolate_video_file( + state["generator"], + state["lock"], + video_path, + output_path, + multiplier, + mode=mode, + progress_cb=on_progress, + ) + tasks[task_id] = {"status": "finished", "progress": 1.0, **info} + except Exception as e: + getLogger(__name__).exception("Video interpolation failed") + tasks[task_id] = {"status": "error", "detail": str(e) or repr(e)} + output_path.unlink(missing_ok=True) + finally: + Path(video_path).unlink(missing_ok=True) + + +def setup_routes(app: FastAPI, args): + @app.get("/healthcheck") + async def healthcheck(): + # Report healthy only after startup (model download/convert) completes. + if not getattr(app.state, "ready", False): + return JSONResponse({"status": "initializing"}, status_code=503) + return JSONResponse({"status": "ok", "device": state["device"]}) + + @app.post("/v1/frame-generation/benchmark") + async def benchmark(req: BenchmarkRequest): + """Measure interpolation throughput (frames/sec) for a gap schedule. + + Also warms any lazily-compiled static shapes for that schedule, so a + caller that benchmarks its exact production schedule (as the lipsync + planner does) gets a ready model afterwards. + """ + if any(n < 1 or n > MAX_FRAMES_PER_GAP for n in req.gap_sizes): + raise HTTPException( + status_code=400, + detail=f"gap_sizes entries must be 1..{MAX_FRAMES_PER_GAP}", + ) + fps = measure_framegen_fps( + state["generator"], req.image_size, req.gap_sizes, req.rounds + ) + return {"fps": fps, "seconds_per_frame": 1.0 / fps} + + @app.post("/v1/frame-generation/interpolate") + async def interpolate(request: Request): + """Fill the frames between keyframe pairs. + + Body: an uncompressed .npz (application/octet-stream) with + frames_a: [N,H,W,3] uint8 — gap start frames + frames_b: [N,H,W,3] uint8 — gap end frames + counts: [N] int — intermediate frames to generate per gap + Response: .npz with arrays gap_0..gap_{N-1}, each [counts[i],H,W,3] uint8. + """ + body = await request.body() + try: + data = np.load(io.BytesIO(body), allow_pickle=False) + frames_a, frames_b = data["frames_a"], data["frames_b"] + counts = data["counts"].astype(int).tolist() + except Exception: + raise HTTPException(status_code=400, detail="Invalid npz payload") + + if ( + frames_a.ndim != 4 + or frames_a.shape[-1] != 3 + or frames_a.shape != frames_b.shape + or len(counts) != len(frames_a) + or len(counts) == 0 + ): + raise HTTPException(status_code=400, detail="Invalid frame shapes") + if any(n < 1 or n > MAX_FRAMES_PER_GAP for n in counts): + raise HTTPException( + status_code=400, + detail=f"counts entries must be 1..{MAX_FRAMES_PER_GAP}", + ) + + gaps = [ + (frames_a[i], frames_b[i], counts[i]) for i in range(len(counts)) + ] + with state["lock"]: + fills = state["generator"].interpolate_gaps(gaps) + + out = io.BytesIO() + np.savez( + out, + **{ + f"gap_{i}": np.stack(fill).clip(0, 255).astype(np.uint8) + for i, fill in enumerate(fills) + }, + ) + return Response( + content=out.getvalue(), media_type="application/octet-stream" + ) + + @app.post("/v1/frame-generation/video") + async def interpolate_video( + video: UploadFile = File(...), + multiplier: int = Form(2), + mode: str = Form("fps"), + ): + """Queue a video interpolation job: FPS upscaling or slow motion. + + mode="fps" multiplies the frame rate (same duration); mode="slowmo" + keeps the frame rate and stretches the duration. Jobs run one at a + time on a worker thread; poll /v1/tasks/{taskId} for progress. + """ + if not video or not video.filename: + raise HTTPException(status_code=400, detail="Missing video file") + if multiplier < 2 or multiplier > MAX_MULTIPLIER: + raise HTTPException( + status_code=400, + detail=f"multiplier must be 2..{MAX_MULTIPLIER}", + ) + if mode not in ("fps", "slowmo"): + raise HTTPException( + status_code=400, detail="mode must be 'fps' or 'slowmo'" + ) + + task_id = uuid4().hex[:8] + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + suffix = re.sub(r"[^A-Za-z0-9.]", "", Path(video.filename).suffix) or ".mp4" + video_path = UPLOAD_DIR / f"{task_id}{suffix.lower()}" + + with open(video_path, "wb") as f: + shutil.copyfileobj(video.file, f) + + # Fail fast on files av cannot open instead of erroring in the task. + try: + fps, _ = probe_video(str(video_path)) + if not fps: + raise ValueError("No video frame rate detected") + except Exception: + video_path.unlink(missing_ok=True) + raise HTTPException( + status_code=400, detail="Invalid or unsupported video" + ) + + tasks[task_id] = {"status": "queued", "progress": 0.0, "seq": next(_job_seq)} + video_jobs.put( + { + "task_id": task_id, + "video_path": str(video_path), + "multiplier": multiplier, + "mode": mode, + } + ) + return JSONResponse({"taskId": task_id}) + + @app.get("/v1/tasks/{task_id}") + async def task_status(task_id: str): + task = tasks.get(task_id) + if task is None: + return {"status": "not_found"} + status = {k: v for k, v in task.items() if k != "seq"} + if task["status"] == "queued": + status["position"] = 1 + sum( + 1 + for other in tasks.values() + if other["status"] == "queued" and other["seq"] < task["seq"] + ) + return status + + @app.get("/v1/frame-generation/video/{task_id}") + async def download_video(task_id: str): + if not TASK_ID_RE.fullmatch(task_id): + raise HTTPException(status_code=400, detail="Invalid task id") + output_path = (OUTPUT_DIR / f"{task_id}.mp4").resolve() + if output_path.parent != OUTPUT_DIR.resolve() or not output_path.exists(): + raise HTTPException(status_code=404, detail="Result not found") + return FileResponse( + output_path, + media_type="video/mp4", + filename=f"interpolated_{task_id}.mp4", + ) + + +def main(): + args = parse_arguments() + getLogger(__name__).info(f"Frame generation device={args.device}") + + app = create_app(args) + setup_routes(app, args) + + port = int(args.port) + getLogger(__name__).info(f"Starting Frame Generation server on port {port}") + uvicorn.run(app, host="0.0.0.0", port=port) + + +if __name__ == "__main__": + main() diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/frame_generator_ov.py b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/frame_generator_ov.py similarity index 79% rename from samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/frame_generator_ov.py rename to samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/frame_generator_ov.py index 7ad7f212..50ad9251 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/frame_generator_ov.py +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/frame_generator_ov.py @@ -5,16 +5,42 @@ import math import os +import secrets +import shutil import time import numpy as np import openvino as ov -from modules.base.logger import getLogger +from modules.logger import getLogger DEFAULT_MODEL_PATH = "models/rife/flownet.safetensors" +def download_rife_model(model_path=DEFAULT_MODEL_PATH, source="huggingface"): + """Fetch the RIFE flownet safetensors weights if not already present.""" + if os.path.exists(model_path): + return model_path + + getLogger(__file__).info("Downloading RIFE frame generation model...") + if source == "modelscope": + from modelscope import snapshot_download + + repo_dir = snapshot_download( + "TensorForger/RIFE-safetensors", allow_patterns=["flownet.safetensors"] + ) + src = os.path.join(repo_dir, "flownet.safetensors") + else: + from huggingface_hub import hf_hub_download + + src = hf_hub_download("TensorForger/RIFE-safetensors", "flownet.safetensors") + + os.makedirs(os.path.dirname(model_path), exist_ok=True) + shutil.copy(src, model_path) + getLogger(__file__).info(f"RIFE model ready at {model_path}") + return model_path + + def convert_rife_to_openvino(model_path=DEFAULT_MODEL_PATH, output_path=None): """Convert the RIFE safetensors checkpoint to an OpenVINO IR (FP16). @@ -29,7 +55,7 @@ def convert_rife_to_openvino(model_path=DEFAULT_MODEL_PATH, output_path=None): import torch from safetensors.torch import load_file - from modules.frame_generation.interpolation_model import IFNet + from modules.interpolation_model import IFNet model = IFNet() model.load_state_dict(load_file(model_path)) @@ -57,8 +83,7 @@ def convert_rife_to_openvino(model_path=DEFAULT_MODEL_PATH, output_path=None): class OpenVINOFrameGenerator: """RIFE (IFNet) frame interpolator running on OpenVINO (CPU/GPU/NPU). - Drop-in replacement for FrameGenerator: same warm_up() and - interpolate_gaps() contract, frames as HxWx3 arrays in 0..255. + Frames are HxWx3 arrays in 0..255; see warm_up() and interpolate_gaps(). """ def __init__(self, device, model_path=DEFAULT_MODEL_PATH, max_batch=16): @@ -174,8 +199,10 @@ def interpolate_gaps(self, gaps): """ Fill several keyframe gaps with interpolated frames in batched passes. - Same recursive, level-synchronous binary subdivision as - FrameGenerator.interpolate_gaps; see that docstring for details. + Recursive, level-synchronous binary subdivision: every pass computes + the RIFE midpoints of all outstanding (a, b) pairs across all gaps in + one batched model call, then subdivides until each gap's slot tree is + full, and finally picks the evenly-spaced slots each gap needs. Args: gaps: list of (frame_a, frame_b, n_frames) tuples, where frames are @@ -227,3 +254,30 @@ def interpolate_gaps(self, gaps): chosen = np.concatenate([slots[gi][i] for i in indices]) results.append(self._array_to_frames(chosen, h, w)) return results + + +def measure_framegen_fps(generator, image_size, gap_sizes, rounds=3): + """Median interpolated frames/sec on an exact gap schedule. + + Also serves as the schedule-specific warmup: it primes any lazily-compiled + static shapes (GPU/NPU) before the first real batch arrives. + """ + frames = [ + np.frombuffer( + secrets.token_bytes(image_size * image_size * 3), dtype=np.uint8 + ) + .reshape(image_size, image_size, 3) + .astype(np.float32) + for _ in range(len(gap_sizes) + 1) + ] + gaps = [(frames[i], frames[i + 1], n) for i, n in enumerate(gap_sizes)] + + generator.interpolate_gaps(gaps) + times = [] + for _ in range(rounds): + start = time.perf_counter() + results = generator.interpolate_gaps(gaps) + times.append(time.perf_counter() - start) + + produced = sum(len(r) for r in results) + return produced / sorted(times)[len(times) // 2] diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/interpolation_model.py b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/interpolation_model.py similarity index 100% rename from samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/interpolation_model.py rename to samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/interpolation_model.py diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/logger.py b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/logger.py new file mode 100644 index 00000000..a3b50ffb --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/logger.py @@ -0,0 +1,32 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +import logging + +logger = None + + +def getLogger(name=__name__): + global logger + + Path("logs").mkdir(parents=True, exist_ok=True) + + if logger == None: + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(funcName)s - %(levelname)s - %(message)s" + ) + + fhandler = logging.FileHandler("logs/debug.log") + fhandler.setFormatter(formatter) + fhandler.setLevel(logging.INFO) + logger.addHandler(fhandler) + + stdHandler = logging.StreamHandler() + stdHandler.setFormatter(formatter) + stdHandler.setLevel(logging.INFO) + logger.addHandler(stdHandler) + + return logger diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/video.py b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/video.py new file mode 100644 index 00000000..363c990d --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/modules/video.py @@ -0,0 +1,192 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""FPS upscaling / slow motion of whole video files with the RIFE interpolator. + +Decodes the source video and generates `multiplier - 1` intermediate frames +between every pair of consecutive frames. In "fps" mode the result is encoded +at `multiplier x` the original frame rate (same duration, smoother motion) and +the audio track, if any, is remuxed unchanged. In "slowmo" mode the result +keeps the original frame rate (duration stretches by `multiplier x`) and audio +is dropped, as it cannot be stretched meaningfully. +""" + +from fractions import Fraction + +import av +import numpy as np + +from modules.logger import getLogger + +# Pairs interpolated per model call. Full frames are much larger than the +# face crops the lipsync path uses, so keep the batch small to bound memory. +GAP_CHUNK = 4 + + +def probe_video(input_path): + """Return (fps, n_frames_estimate) for the video stream.""" + with av.open(input_path) as container: + stream = container.streams.video[0] + rate = stream.average_rate or stream.guessed_rate + n_frames = stream.frames + if not n_frames and stream.duration and rate: + n_frames = int(stream.duration * stream.time_base * rate) + return float(rate) if rate else None, n_frames or 0 + + +def interpolate_video_file( + generator, + lock, + input_path, + output_path, + multiplier, + mode="fps", + progress_cb=None, +): + """Interpolate input_path with RIFE, for FPS upscaling or slow motion. + + Frames are never rescaled: the output keeps the source resolution, with + two display-fidelity exceptions. Rotation metadata (phone videos) is + baked into the pixels so the output plays upright without relying on a + display matrix, and odd dimensions are cropped by one pixel because + yuv420p H.264 requires even sizes. Anamorphic sources keep their sample + aspect ratio, so the displayed proportions match the original. + + Args: + generator: OpenVINOFrameGenerator (shared, not thread-safe). + lock: lock serializing access to the generator. + input_path: source video path. + output_path: output .mp4 path. + multiplier: integer frame multiplier (>= 2). + mode: "fps" (multiplier x frame rate, same duration, audio kept) or + "slowmo" (same frame rate, multiplier x duration, audio dropped). + progress_cb: optional callable(fraction_done: float). + """ + n_fill = multiplier - 1 + slowmo = mode == "slowmo" + + with av.open(str(input_path)) as src, av.open(str(output_path), "w") as out: + in_v = src.streams.video[0] + in_rate = in_v.average_rate or in_v.guessed_rate or Fraction(25, 1) + out_rate = Fraction(in_rate) if slowmo else Fraction(in_rate) * multiplier + + total_frames = in_v.frames + if not total_frames and in_v.duration: + total_frames = int(in_v.duration * in_v.time_base * in_rate) + + out_v = out.add_stream("libx264", rate=out_rate) + out_v.pix_fmt = "yuv420p" + out_v.options = {"crf": "20", "preset": "veryfast"} + # Anamorphic sources: keep the pixel aspect ratio so the displayed + # proportions match the original without resampling any pixels. + if in_v.sample_aspect_ratio: + out_v.sample_aspect_ratio = in_v.sample_aspect_ratio + + # Slow motion drops audio: the stretched timeline has no meaningful + # audio track to carry over. + in_a = ( + None + if slowmo + else next((s for s in src.streams if s.type == "audio"), None) + ) + out_a = out.add_stream_from_template(in_a) if in_a else None + + state = {"index": 0, "decoded": 0} + + def emit(arr): + frame = av.VideoFrame.from_ndarray( + np.clip(arr, 0, 255).astype(np.uint8), format="rgb24" + ) + frame.pts = state["index"] + frame.time_base = Fraction(1, 1) / out_rate + state["index"] += 1 + out.mux(out_v.encode(frame)) + + def flush_pairs(pairs): + if not pairs: + return + gaps = [(a, b, n_fill) for a, b in pairs] + with lock: + fills = generator.interpolate_gaps(gaps) + for (a, _), fill in zip(pairs, fills): + emit(a) + for f in fill: + emit(f) + state["decoded"] += len(pairs) + if progress_cb and total_frames: + progress_cb(min(state["decoded"] / total_frames, 1.0)) + + rotation_quarters = None + + def prepare(frame): + """Decoded frame -> upright, even-dimensioned rgb24 ndarray.""" + nonlocal rotation_quarters + arr = frame.to_ndarray(format="rgb24") + if rotation_quarters is None: + # frame.rotation is the display-matrix angle in degrees + # counterclockwise (phone recordings); bake it into the + # pixels so the output plays upright everywhere. Only + # right-angle rotations occur in practice. + angle = frame.rotation or 0 + rotation_quarters = ( + round(angle / 90) % 4 if angle % 90 == 0 else 0 + ) + if rotation_quarters: + getLogger(__file__).info( + f"Applying {angle} degree display rotation from " + "source metadata" + ) + if rotation_quarters: + arr = np.rot90(arr, k=rotation_quarters) + # yuv420p H.264 requires even dimensions; crop at most 1 pixel. + h, w = arr.shape[:2] + if (h % 2) or (w % 2): + arr = arr[: h - h % 2, : w - w % 2] + return np.ascontiguousarray(arr) + + # The encoder stream defaults to 640x480 and PyAV silently rescales + # every frame to the stream size, so the real dimensions must be set + # from the first prepared frame before anything is encoded. + dims_set = False + prev = None + pending = [] + for frame in src.decode(in_v): + arr = prepare(frame) + if not dims_set: + out_v.width, out_v.height = arr.shape[1], arr.shape[0] + dims_set = True + if prev is not None: + pending.append((prev, arr)) + if len(pending) >= GAP_CHUNK: + flush_pairs(pending) + pending = [] + prev = arr + + flush_pairs(pending) + if prev is not None: + emit(prev) + out.mux(out_v.encode()) + + if in_a is not None: + with av.open(str(input_path)) as src_audio: + audio_stream = next( + s for s in src_audio.streams if s.type == "audio" + ) + for packet in src_audio.demux(audio_stream): + if packet.dts is None: + continue + packet.stream = out_a + out.mux(packet) + + getLogger(__file__).info( + f"Interpolated {input_path} -> {output_path} ({mode}): " + f"{float(in_rate):.2f} FPS x{multiplier} => {float(out_rate):.2f} FPS, " + f"{state['index']} frames" + ) + return { + "mode": mode, + "multiplier": multiplier, + "input_fps": float(in_rate), + "output_fps": float(out_rate), + "frames": state["index"], + } diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/pyproject.toml b/samples/ai/edge-ai-demo-studio/workers/frame-generation/pyproject.toml new file mode 100644 index 00000000..df32e864 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "frame-generation" +version = "0.1.0" +description = "RIFE video frame interpolation (frame generation) service" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "av>=13.0.0", + "fastapi[standard]>=0.120.1", + "huggingface-hub>=0.35.1", + "modelscope>=1.34.0", + "numpy>=2.2.6", + "openvino>=2026.2.1", + "python-multipart>=0.0.22", + "safetensors>=0.4.5", + "torch>=2.11.0", + "uvicorn>=0.34.3", +] + +[tool.uv.sources] +torch = { index = "pytorch-cpu" } + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.ps1 b/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.ps1 new file mode 100644 index 00000000..7eb10eca --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.ps1 @@ -0,0 +1,18 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +$ErrorActionPreference = "Stop" +$SCRIPT_DIR = $PSScriptRoot +$UV_CMD = Join-Path $SCRIPT_DIR "..\thirdparty\uv\uv.exe" + +function Test-UV { + if (Test-Path $UV_CMD) { return } + Write-Host "ERROR: uv not found at $UV_CMD" -ForegroundColor Red + Write-Host "Please run the workers setup script first." -ForegroundColor Red + exit 1 +} + +Test-UV +Set-Location $SCRIPT_DIR +& $UV_CMD run main.py @args +exit $LASTEXITCODE diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.sh b/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.sh new file mode 100755 index 00000000..b31fe826 --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/start.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UV_CMD="$SCRIPT_DIR/../thirdparty/uv/uv" + +check_uv() { + if [ -x "$UV_CMD" ]; then + return 0 + fi + echo "ERROR: uv not found at $UV_CMD" + echo "Please run the workers setup script first." + exit 1 +} + +cd "$SCRIPT_DIR" +check_uv +exec "$UV_CMD" run main.py "$@" diff --git a/samples/ai/edge-ai-demo-studio/workers/frame-generation/uv.lock b/samples/ai/edge-ai-demo-studio/workers/frame-generation/uv.lock new file mode 100644 index 00000000..3e6c15ed --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/frame-generation/uv.lock @@ -0,0 +1,1697 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version < '3.12' and sys_platform != 'darwin'", + "python_full_version < '3.12' and sys_platform == 'darwin'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "av" +version = "18.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/a4/570a5a35c8638aba01e739925846c35fdd6b0756a15526766d0a4dd3b7df/av-18.0.0.tar.gz", hash = "sha256:4ef7e72c3d3a872584a1215173b16e0226811037f40dcdbf75992631098df1ba", size = 4340222, upload-time = "2026-07-02T06:37:58.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/4a/9e3463df030e063d757fa12f0f39be6541b45b06b5bad48c2ce361b924bf/av-18.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:149289d40e732a6e49c9530bc245b49d9964cfd1c8c9e06778703b7d5bba6b25", size = 22499354, upload-time = "2026-07-02T06:36:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/77/b3/2576a44b4f39c7462ced4c17fec04c756f7b0f3c5cb940d124173e417d6a/av-18.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:35274c20d2ad3b4774fe632bcef2e34af79858ddf899352339cc3babbc13a484", size = 18175248, upload-time = "2026-07-02T06:37:01.741Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/6732f17b96dc23fd23b876b2805435855abdc8a3b397142be4e581165de8/av-18.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4d683b7747a0ba9222b8a5f81e41db5f796e7f64473454ec4fe2548e083c2fa0", size = 33387843, upload-time = "2026-07-02T06:37:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/7708c43fed7ae28b4a1bad060b4221e3334cd827cec24f7165902a6ac1f4/av-18.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ae56b40b6f8b067a8ad2dac664fbfbabac7f7a55b9a7bb031eb99289252bc017", size = 35536910, upload-time = "2026-07-02T06:37:08.806Z" }, + { url = "https://files.pythonhosted.org/packages/5a/94/eba99691d184f6a395a242d54dc370e2fd2265e95bbc98e2963a0fdbdd6c/av-18.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ea2e8ebbce521f21b55df9400e00d721623c9020ef158f5a188a96130be0743f", size = 38984619, upload-time = "2026-07-02T06:37:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/0d7aee07fe16aa9ffdf96043c14bed5485a52c0dea4259de87aa306ecab4/av-18.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef96dabb3e50dac249913145dff5424b302b257fd95dcb64be3c7b7a8aef16d1", size = 34451176, upload-time = "2026-07-02T06:37:15.154Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/810da80b12680d4c4fe235bd1b4003289be9213ac7f114b77b8ecf0e3b3e/av-18.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0f65518a184613e41536f29e8758c8e3d8293e46bf5bef108f04f925bbfa3f44", size = 36619869, upload-time = "2026-07-02T06:37:18.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/0f121ff43dc5a70696676c98a8f1674e2fa787614c2abaacb15fa1a9bc99/av-18.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:aaf4d354d2beaa6651e4f92e54409a578bde64f79c0beef9a30b388d06f7c629", size = 27556236, upload-time = "2026-07-02T06:37:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f6/2509754d4d2356abc6fc0ea3d57c12ade29bac23a1fb7fc215a53ca518fb/av-18.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:adac2b3833b6cb9bd6cb52664a522b94db453615b3675b1dbb26e13fe1c80da6", size = 20221133, upload-time = "2026-07-02T06:37:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/4ee23a7f1609adf9b2f140c7a8ffade64a1449d89ab431d922a809eebf19/av-18.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:88dd8e35e9242662b409a6a05fd24a6775d949eb05da0ba31cab4f250eacbab5", size = 22740741, upload-time = "2026-07-02T06:37:26.659Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f0/b9f8363d07aa4521913e483f6a30c7c164973ef01de62769bf9b97049cd8/av-18.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f8f454349c402e2c8d6fa80b54eb2a3f86c00f414d2b399f01ae6dab075c6fd8", size = 18384189, upload-time = "2026-07-02T06:37:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e5/69397019aed280a72a43e97a252dee4295df1a9e608848452e5300ec4dab/av-18.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:88ce194c2201c6a6d40336adee8a5ddde46ed743eacb500e3ae9368d1c6d889e", size = 36749881, upload-time = "2026-07-02T06:37:33.096Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/1614d74f0d676ea6745eb59553c9ad01ca25db523cba808d522e838f4f5b/av-18.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:aa15e567a018cc94a26b0ab45da676dee70c4146ace6e92e47d30cc9689cbfbe", size = 38645927, upload-time = "2026-07-02T06:37:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3c/5f54710d69b0ea93634134f92b49c7a2a7fd27da5486a8a7e6251ac1cfb4/av-18.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:613153e48cefc91700746dde0ad0282d4677b194cba22cc771de14c78411cf8b", size = 40454783, upload-time = "2026-07-02T06:37:40.904Z" }, + { url = "https://files.pythonhosted.org/packages/26/92/8293e6a267e0591b543abd96ae01e7e8ed228509bdb4e4644a8a8395d90f/av-18.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:30404f53ca1ea7f350ac86ff22a2c04f903014758e9b33f398c5a62de34bd84f", size = 37573117, upload-time = "2026-07-02T06:37:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/0c/38ed7601277ae57dfe857d040be4762530fd728efff45c2fb8f035fef96a/av-18.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6882a48f7aec2863c96cddee3256ff2da98f7fb6cbed83cee9d7e70a8f186a6b", size = 39669026, upload-time = "2026-07-02T06:37:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/0636ca04d5d89d01c49bd366d2b660cc85d1f8117c476b2be62eb0c70855/av-18.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:55a646e9afce9fdc5de5224205a8a12c7ed1ba9803145dcc876c40bfc03a109b", size = 28448336, upload-time = "2026-07-02T06:37:52.477Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/1e24450ea981c44ed328691496fd2774dfa9fa3c3b00fd07f72fd5614abe/av-18.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:96f594ff506a09475e5549359352332049a25d37a08f00b4623f7f6e92e45b9c", size = 21377289, upload-time = "2026-07-02T06:37:55.935Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.139.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/af/a5f50ccfa659ec1802cb4ca842c23f06d906a8cc9aef6016a2caeea3d4ed/fastapi-0.139.0.tar.gz", hash = "sha256:99ab7b2d92223c76d6cf10757ab3f89d45b38267fc20b2a136cf02f6beac3145", size = 423016, upload-time = "2026-07-01T16:35:33.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7c/8e3c6ad324ea5cb36604fc3f968554887891c316d9dfde57761611d907ad/fastapi-0.139.0-py3-none-any.whl", hash = "sha256:cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", size = 130339, upload-time = "2026-07-01T16:35:32.19Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/a9/d618b7743180fb5c1252ad5b682d512323c1b93c8a13f08521b351d511dd/fastapi_cli-0.0.30.tar.gz", hash = "sha256:f808773b1a7aa1531ef750b68905787f3dd13834e78c44d1501f017335a4c1a7", size = 24971, upload-time = "2026-07-15T17:21:50.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a0/f95bb8e07c00efb98f26e1876d6504b416af09e0bbe0a52cce979eae8d7e/fastapi_cli-0.0.30-py3-none-any.whl", hash = "sha256:11375b2a2c8c5a5ee05087960b6f1404ba1b48da96fa1a5aa68e2a5caf76f756", size = 14185, upload-time = "2026-07-15T17:21:49.878Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "detect-installer" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/f2/36bfe990baa656de89a2b98a77a15dcd018474f7245c8e4a10cada0553c5/fastapi_cloud_cli-0.22.2.tar.gz", hash = "sha256:7ec78c1fed58f578af5eb1fb54ec4b456eba4dc1eaca1c3a93cf499a7cbc7ab3", size = 94480, upload-time = "2026-07-14T09:27:01.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/25/e01631a63a5213fc783e5b84e8a27eca800f21549aa414644a0a29181045/fastapi_cloud_cli-0.22.2-py3-none-any.whl", hash = "sha256:37c6b05adb94a4c9f59916a559d041c565db36b5f3dddeed420bf0a51182c363", size = 77744, upload-time = "2026-07-14T09:27:02.571Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7a/fb367bdaf4efa2c7952a45aeab2e87a564293ecffe150af673ec8edfda46/fastar-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b82fd6f996e65a86f67a6bd64dd22ef3e8ae2dcaed0ae3b550e71f7e1bbb1df5", size = 709869, upload-time = "2026-04-13T17:09:55.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/5872b28c72c27ec1a00760eace6ff35f714f41ebbd5208cf016b12e29250/fastar-0.11.0-cp311-cp311-win32.whl", hash = "sha256:030b2580fc394f2c9b7890b6735810404e9b9ed5e0344db150b945965b5482b7", size = 457368, upload-time = "2026-04-13T17:11:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/ce6832a16193eb4466f4108be8809c249b51cb1f89dd7894545700d079d5/fastar-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:83ab57ae067969cd0b483ac3b6dccc4b595fc77f5c820760998648d4c42822b5", size = 488605, upload-time = "2026-04-13T17:11:29.161Z" }, + { url = "https://files.pythonhosted.org/packages/15/5a/9cfb80661cf38fd7b0889224beb7d2746784d4ade2a931ed9775a18d8602/fastar-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:27b1a4cee2298b704de8151d310462ee7335ed036011ca9aa6e784b30b6c73a9", size = 464580, upload-time = "2026-04-13T17:11:18.583Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/9bbeffbf1905391446dd98aa520422ce7affde5c9a7c22d757cc5d7c1397/fastar-0.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1266d6a004f427b0d61bd6c7b544d84cc964691b2232c2f4d635a1b75f2f6d5e", size = 711644, upload-time = "2026-04-13T17:10:07.663Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, +] + +[[package]] +name = "filelock" +version = "3.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2b/8b6480a70a647035334a604d0931926de4b5cd1f57835d45ad5eed2b1a1e/filelock-3.30.0.tar.gz", hash = "sha256:1774e682dbe443bd60f9609162fc596e2c80dc84ffc2957068953406d0520090", size = 174927, upload-time = "2026-07-16T03:53:58.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/af/9b01bcf5c91e81899bb890b87bd9077732a9b3365c098e67fe77958c39ed/filelock-3.30.0-py3-none-any.whl", hash = "sha256:40632998f0772e64183bb819f086a1b9def6be1090cf1dcb9d45f46806ef279b", size = 93131, upload-time = "2026-07-16T03:53:56.727Z" }, +] + +[[package]] +name = "frame-generation" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "av" }, + { name = "fastapi", extra = ["standard"] }, + { name = "huggingface-hub" }, + { name = "modelscope" }, + { name = "numpy" }, + { name = "openvino" }, + { name = "python-multipart" }, + { name = "safetensors" }, + { name = "torch", version = "2.13.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "av", specifier = ">=13.0.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.120.1" }, + { name = "huggingface-hub", specifier = ">=0.35.1" }, + { name = "modelscope", specifier = ">=1.34.0" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "openvino", specifier = ">=2026.2.1" }, + { name = "python-multipart", specifier = ">=0.0.22" }, + { name = "safetensors", specifier = ">=0.4.5" }, + { name = "torch", specifier = ">=2.11.0", index = "https://download.pytorch.org/whl/cpu" }, + { name = "uvicorn", specifier = ">=0.34.3" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" }, + { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/8f/999e4dda11c6187c78f090eac00895a47e11a0049308f07579bcb7aa3aa2/huggingface_hub-1.23.0.tar.gz", hash = "sha256:c04997fb8bbdace1e57b7703d30ed7678af51f70d00d241819ff411b92ae9a88", size = 919163, upload-time = "2026-07-09T14:49:32.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/ce/13b2ba57838b8db1e6bd033c1b21ce0b9f6153b87d4e4939f77074e41eb0/huggingface_hub-1.23.0-py3-none-any.whl", hash = "sha256:b1d604788f5adc7f0eb246e03e0ec19011ca06e38400218c347dccc3dffa64a2", size = 770336, upload-time = "2026-07-09T14:49:30.597Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "modelscope" +version = "1.38.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "modelscope-hub" }, + { name = "packaging" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/72/b37f46f8e1900c64485420b210eecda1f758cbd0fe8ade60c5bbf883b25e/modelscope-1.38.1.tar.gz", hash = "sha256:a81f3685b1545f52b415d88a763456416aa65170e622f9dcc02eadb3f8e1cfa0", size = 4542886, upload-time = "2026-07-06T15:13:56.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/f3/caf5ef8c4adc99cd66607639642ae503ac190a3358f782776687200a7c36/modelscope-1.38.1-py3-none-any.whl", hash = "sha256:7d65be96999144ca045386d27a8ea057b8777504c538d9755d60ec2c8906fe72", size = 6034052, upload-time = "2026-07-06T15:13:53.923Z" }, +] + +[[package]] +name = "modelscope-hub" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/cb/6b9d3fcbcd09db6cdd7f1e0fd60be2998c99db07be28f37982c0dbd14496/modelscope_hub-0.1.7.tar.gz", hash = "sha256:b78730f3923cf13d5fbc56c6224a5ba617bf111a562fe3808c03e34c3c07840f", size = 126616, upload-time = "2026-07-07T07:35:36.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/19/3554c99cfc633cca2ffab2cfc3c8cb064fa17f88bcb68afee7a81957ccc8/modelscope_hub-0.1.7-py3-none-any.whl", hash = "sha256:907e5da4d8b050277a3cbd79d1c2df4f723b549ba38f573f4ffb6d1cd1525349", size = 135758, upload-time = "2026-07-07T07:35:35.283Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "openvino" +version = "2026.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "openvino-telemetry" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/b5/726e3028016bdbbfe3e0f939666df04a6ba2a0ff61619b5ed07834bef89b/openvino-2026.2.1-21919-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:616cdf8298a725e5a31a01049e8cf2b0cb8e6b9f90e523c75dbfe0af2834f592", size = 31126312, upload-time = "2026-06-17T09:20:41.696Z" }, + { url = "https://files.pythonhosted.org/packages/6d/09/6e5c9485e70bef82a43cd9499539dd671f49c850f4dbcb21fcbadf3f22cc/openvino-2026.2.1-21919-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3a7f4ec8ed2a7ae4069a96ded7f9ff9b7a97230e411141db8b68669ba43c0bde", size = 58358073, upload-time = "2026-06-17T09:20:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/74/e3/4c8c164cf98f7db7c3131d55ee31e5d59aff685a7c77bcfc03661ea601e4/openvino-2026.2.1-21919-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:8d839065ba0b1098cdcce7049d0f1c4fb97f422e6450b9aea63f1aedea1a2154", size = 28351927, upload-time = "2026-06-17T09:20:50.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/0b7104491b55b31596cfc2228f7b4b4469ab6e01d12e30795eebd94d13ed/openvino-2026.2.1-21919-cp311-cp311-win_amd64.whl", hash = "sha256:a3c775db07981aa1d995f0fe587a4bc5384dcbda9c1fecdfad9bb6f20f340782", size = 76163398, upload-time = "2026-06-17T09:20:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/25/5c3e836afd92c9c07a411ea9af9e9ac3fff40aae612e93c5c0a997da83a1/openvino-2026.2.1-21919-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7afed0219cd29fa73f54b7351ffab8e7c7fefb64290dd5264d7c447617ee09ff", size = 31169975, upload-time = "2026-06-17T09:20:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/62/c2/46b88970c6d8ff40703b58a96bec40c354ca1e6977511e01af0fa0f12b8a/openvino-2026.2.1-21919-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:efae5ceb94943aeab8a3be04241cbb89110070ce9bacfbfbe947d8d7ef691a82", size = 58367861, upload-time = "2026-06-17T09:21:02.968Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1f/e43283e3a2879b0ebdf1b4591461504255bfc639471ea3698a35ebcbcab2/openvino-2026.2.1-21919-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:6154b26f63804cb671e20b8872abccafbe88b15fdd41fbd35ffa08bb4ac5e29b", size = 28337803, upload-time = "2026-06-17T09:21:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/93/81/f4605ab2bdbb3daff7cadf7d5265e87c4fe95d01af00ae8e1638b9a4abe9/openvino-2026.2.1-21919-cp312-cp312-win_amd64.whl", hash = "sha256:6928a707c447dbd43dab62c8bbc24a7953fd1b5e1fbea0f89a4135ca1ee4083b", size = 76171228, upload-time = "2026-06-17T09:21:11.156Z" }, + { url = "https://files.pythonhosted.org/packages/64/b5/5788f1ce7906f33f003de3bb55c2477d9911c19f72d7ed3f7b719538d4a3/openvino-2026.2.1-21919-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddd393566ab5cdcf106028e532f0f3789d7796fac848284c4d2ef31e3b81ef8", size = 31170279, upload-time = "2026-06-17T09:21:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/ba/41/7045a639e3879b0c858533f64c93e9c998f292b810dfaff58c23540c738a/openvino-2026.2.1-21919-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:00483723163c8d4d6f436ad0e4d05247494de4d280e4b065f67c537b440f4b56", size = 58367437, upload-time = "2026-06-17T09:21:20.215Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/3f402a78755c1c848128126f145fc32a6d091c2257c543522d9ca979456a/openvino-2026.2.1-21919-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f9619c51027b6b0514e82ade09794b109a45553e8afcb073eedb1c8c1ffd43c4", size = 28344673, upload-time = "2026-06-17T09:21:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/13b9bfc20c02706d015e96e5b3e932227d658b3826b4aa5007cdc2455ed4/openvino-2026.2.1-21919-cp313-cp313-win_amd64.whl", hash = "sha256:197435104d18657a7cc4d05850615b913618ebb0d3baaec052fda25a2d831c74", size = 76171396, upload-time = "2026-06-17T09:21:29.03Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c0/1bd1e01be4eb7434711638b551521b3859f6df13a7b5cdaf591870ee9065/openvino-2026.2.1-21919-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f1dfd9d8ffbe21c2d40089c8137903432535b89ee8eb3d177b75604f4273ad4", size = 31151204, upload-time = "2026-06-17T09:21:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/43/65/b7f2a382e1da48c6cce109e7960714aa0a0608f2fb6b00388594b5d8edd2/openvino-2026.2.1-21919-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0ec716ddd84a63534613171d27c8657c8417734a1cccedeb40533e8673a12c6d", size = 58369839, upload-time = "2026-06-17T09:21:37.636Z" }, + { url = "https://files.pythonhosted.org/packages/45/4e/c0730932f7e0c34f627cedb569ac1f21222c4c5e0ed904a7f86e4508849d/openvino-2026.2.1-21919-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:6fb513f36dde11dcccf5143e44b37bb64368689e3d746c283e9907e181e32226", size = 28351270, upload-time = "2026-06-17T09:21:41.833Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2c/c4f651c675c0271efdf7733c1a1adc9bfbf6cfcb5fce384565582f65e646/openvino-2026.2.1-21919-cp314-cp314-win_amd64.whl", hash = "sha256:c53788fcf66b5059eb5feeaba8f96782f3b672ea760df7cebbb96a28467eb472", size = 76173523, upload-time = "2026-06-17T09:21:47.05Z" }, + { url = "https://files.pythonhosted.org/packages/18/e6/8c6942a06b86bdea8e55c630770946f5ef5d17a4131cec2ff677a1d836fb/openvino-2026.2.1-21919-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a4d99ffc01216fe7fbb41b246db2d92ae00a7e980de9e1c8ead5eac81ca3202", size = 31379331, upload-time = "2026-06-17T09:21:50.722Z" }, + { url = "https://files.pythonhosted.org/packages/38/71/890efb337ad5d0ab1e70bfb0e3382bbb9efaed1f67dd27b77a275acbec13/openvino-2026.2.1-21919-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:863c55be074494a0e879f780eb2a7b25cf102105fd1835af1046376fbcb35a93", size = 58413666, upload-time = "2026-06-17T09:21:55.225Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/56015860afcb19f20a684a2b1fa6f8662b53db7dde0b5219ce73e250d5f4/openvino-2026.2.1-21919-cp314-cp314t-manylinux_2_35_aarch64.whl", hash = "sha256:daf95cfd841070e17588f66826a59cccb562beceb98ce78637e3296dfecbce3c", size = 25494110, upload-time = "2026-06-17T09:21:59.279Z" }, + { url = "https://files.pythonhosted.org/packages/a5/60/a8fd84f8289003dec261840f32a06476c793830d4562c2c2d76724ffdcc6/openvino-2026.2.1-21919-cp314-cp314t-win_amd64.whl", hash = "sha256:7ebc506896b082acd744b479d400e090ed566c7d1ff64a0071f2d54460032f9a", size = 76338896, upload-time = "2026-06-17T09:22:05.122Z" }, +] + +[[package]] +name = "openvino-telemetry" +version = "2025.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/8a/89d82f1a9d913fb266c2e6dc2f6030935db24b7152963a8db6c4f039787f/openvino_telemetry-2025.2.0.tar.gz", hash = "sha256:8bf8127218e51e99547bf38b8fb85a8b31c9bf96e6f3a82eb0b3b6a34155977c", size = 18894, upload-time = "2025-07-07T10:29:51.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ac/5ab0ca0aa269ad3c73f7bfc3801b10e5f56f75a31bf68c1ae8bd51cf70a4/openvino_telemetry-2025.2.0-py3-none-any.whl", hash = "sha256:bcb667e83a44f202ecf4cfa49281715c6d7e21499daec04ff853b7f964833599", size = 25227, upload-time = "2025-07-07T10:29:50.189Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/3a/a258c2fbc6c6bdf428611388f5698ba5d57ffdf0755e1cab474d9cc47813/rich_toolkit-0.20.3.tar.gz", hash = "sha256:223dd2cfba325ed55e94933b9e53f3aca13e9fdf76622bd564c18109a2273c1b", size = 205355, upload-time = "2026-07-13T14:38:06.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/ce/639d0d0ce3d25c5edbd1afecd308bb35dc04883a45ac9f0855c8aee4e919/rich_toolkit-0.20.3-py3-none-any.whl", hash = "sha256:419aa87516d5f3849cca553c6dcf707c02a36d508fcf996946606725d34a3002", size = 36195, upload-time = "2026-07-13T14:38:05.687Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/1f/ed17a390348156ca99fe622b97cd7d2f1969b5f49df89084b0f28e7953e9/sentry_sdk-2.65.0.tar.gz", hash = "sha256:c94dc945d54bad49d4f20448b1e6b217ca2f92f46d05c3e83d41764af685c3d1", size = 932133, upload-time = "2026-07-13T11:33:19.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/3b/326ad4c03b5da89b5124c8890af66e8119c4d2e10abc0619e0d67d9f7c7f/sentry_sdk-2.65.0-py3-none-any.whl", hash = "sha256:3595169677a808e4d0e1ea6ffb89443459549c7a98392ed71c77c847182ab6bf", size = 503869, upload-time = "2026-07-13T11:33:17.71Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version < '3.12' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "fsspec", marker = "sys_platform == 'darwin'" }, + { name = "jinja2", marker = "sys_platform == 'darwin'" }, + { name = "networkx", marker = "sys_platform == 'darwin'" }, + { name = "setuptools", marker = "sys_platform == 'darwin'" }, + { name = "sympy", marker = "sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", upload-time = "2026-07-08T12:26:13Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", upload-time = "2026-07-08T12:26:18Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", upload-time = "2026-07-08T12:26:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", upload-time = "2026-07-08T12:26:28Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", upload-time = "2026-07-08T12:26:33Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform != 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin'", + "python_full_version < '3.12' and sys_platform != 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "sys_platform != 'darwin'" }, + { name = "fsspec", marker = "sys_platform != 'darwin'" }, + { name = "jinja2", marker = "sys_platform != 'darwin'" }, + { name = "networkx", marker = "sys_platform != 'darwin'" }, + { name = "setuptools", marker = "sys_platform != 'darwin'" }, + { name = "sympy", marker = "sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:6e9817dbdf5ea76789babd46e457eac5bf14ff566cf85f8addbfdff2d56601ce", upload-time = "2026-07-08T19:27:52Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:84453b69508ec79902f899c5ed9495acb9e2bbe9fda5f1d5d6f19e3c3842e1a7", upload-time = "2026-07-08T19:28:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6746dbcbeb526eb61330b76b41ff1b4eb848951103a892eeb080dfa2b264667b", upload-time = "2026-07-08T19:28:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:10717d8b3b67c45a4788bf7ffc0bab1ea1e5ebbedd24466be6100102d141fac1", upload-time = "2026-07-08T19:28:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:2b3d093abd919ad934c43d47e73ba63ceba7cbd7269fc2e9c1e4fc29e8fe45fa", upload-time = "2026-07-08T19:28:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:ffadde149901c8afa138daa38d898264003cfcf1a3336ca5cd964b5af227d867", upload-time = "2026-07-08T19:28:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f307c2c32d764ffc6ff6893b801fad6d4752f3e67966cb8abf1843427c02604", upload-time = "2026-07-08T19:28:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ca4a9394b0c771238a4f73590fdbbc4debad85ed0fa63d026ae1b085da7d6e2", upload-time = "2026-07-08T19:29:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:a8b450c1e58e5800e5b4691dac412f8d2d65a1dc3298166f91596603a3531e6f", upload-time = "2026-07-08T19:29:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:fa0762705b933624d59f6823db9ce7ec2e35b3e1e9c319c9db51fbeecfc3e319", upload-time = "2026-07-08T19:29:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:966d020354f465672dc7dd10d3a5c6cd17d7eb48620aa1d265b48a1f78f06898", upload-time = "2026-07-08T19:29:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0b8f7d0423027ae8b90c7977c627f3379f325363a08224dffad9b4b2d684a83d", upload-time = "2026-07-08T19:29:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3fbf9c9d1f3c10c2d59d04aca426dee9ccc6ceb32d255c61e93acc3b4f75fae6", upload-time = "2026-07-08T19:29:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:a17ff48608634db245e17e8bb00a9558554a49aeb1e4f5fe6cd039af2a10515b", upload-time = "2026-07-08T19:30:05Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:ac7aaf322be4777765a53bed7264a214dd81b3a1d276b93150515a3c5f75e4b0", upload-time = "2026-07-08T19:30:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:dec241fef3984c0d1edadd1f58708e218d4eae881ceef7bc10cf9964d41b68b9", upload-time = "2026-07-08T19:30:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca021f9eb2f8345c83fa03e3a04587308afb8df71bd472670b3ece00df58621c", upload-time = "2026-07-08T19:30:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d20fa53ee744502fa4c69818a720b05ca0d37abd055d4f6e66cae155114bc691", upload-time = "2026-07-08T19:30:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:e2e5134decf00e218da62318f3dc5df156231d367871918e91eba95ab0ad43ab", upload-time = "2026-07-08T19:30:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:991cc14b39e751122c01f017be6448533989868731cb5eecd1006893d26787c2", upload-time = "2026-07-08T19:31:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7b8d26e29bceafbdaa8d63bfe7612f23875b5af2cc07e13f809c3ed890bbe1d8", upload-time = "2026-07-08T19:31:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b222c15a0fc2ce207d1c1a59700b46c8fa6748df1f447ad11e5c870dde0933d9", upload-time = "2026-07-08T19:31:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:a43376bd094124ef626bfdd3d4c2c62eacb0b5ddc99776f4a32d4fd16f1f3420", upload-time = "2026-07-08T19:31:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5002ca81af00ae69b57540f615b58b8ae922b6d4848176b366a52bd2196e6", upload-time = "2026-07-08T19:32:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:1a3a35229fdc13446b4eab50e7fcf9399ff941e89a3b761497786297a5d8dde5", upload-time = "2026-07-08T19:32:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:8e109528e6bab044815daebaf71770fbaace3a66ef1c816cb55c875350f78a60", upload-time = "2026-07-08T19:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:222a6681467cc7f6f05cd3068dfbc603def3a1e46d1d4620c1c8cdf6178bd563", upload-time = "2026-07-08T19:32:44Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "websockets" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/13/d47429afcc2c28616c32640009c84ea3f95660dab805766345b9682468e0/websockets-16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9b1d7a63cba8e6b9b77e499a81eab29d31100298d090ad4507d1048c0b9cae0", size = 179770, upload-time = "2026-07-10T06:30:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/2f0a722039a1e0107be73ed672ba604449b4956e48733e8e6b8a005aea42/websockets-16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bedbc5efeb96621aa2921d2d92608246691399418cac22acba427eb11877ea1f", size = 177455, upload-time = "2026-07-10T06:30:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/43/6a/c26b0ae449e93d256ce5cdd50d5fe97b575a63e8dcd311a1faa972fd6bc6/websockets-16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd847ab82133015afe65d778e7966ab42dba16bd7ad2e5b8a7918db6539f3f94", size = 177731, upload-time = "2026-07-10T06:30:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/381550b344a02f0d2f84cda25e79b54575291bc7022128a41163fe8ba5b0/websockets-16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2fb33ccb16ee40a95cc676d7b0ff451a9a2632f11a0dbc2e666326892b2e1de", size = 187066, upload-time = "2026-07-10T06:30:50.505Z" }, + { url = "https://files.pythonhosted.org/packages/4a/87/5ab1ec2086910f23cfb9ec0c1c29fbcc24a9d190b5198b1557c00ce4a47e/websockets-16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f15b6d9ea9c2eaf6ccab964a082b09bfa6634a495bb0c2e9e7ee6943f58976", size = 188301, upload-time = "2026-07-10T06:30:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/bbbb8e6fac4cfc53d7aaa69a3d531bf10799354b0021f4b58914aced8c1a/websockets-16.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:638cf57c48b4ad8ac1ff1e453f4f97db2426b690ddc111e6da96b27b4a340bc3", size = 191594, upload-time = "2026-07-10T06:30:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/5c/da/6c0c349443d6e999f481e3d9a0e57e7ac2956d75d6391bec24b92af3fe13/websockets-16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c1c85f61bc9d5eac57ce705d848dc2d2ce3680638300bf4e1da7d749e2cf4ce", size = 188862, upload-time = "2026-07-10T06:30:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ea/a368d37c010425a5451f42052fe804e754e23333e8448aef5d55c8a8d64f/websockets-16.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eeab6d27f51c7e579023c971f5e6dff200deadf01faf6831beaecd32052dfaef", size = 187633, upload-time = "2026-07-10T06:30:56.055Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4e/2ecd59add10d0855ec03dbdedfcdacdbd1aaabcd44b7dcbeda27538662e9/websockets-16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ed64e5a97b0b97a0b66e18bfe281317a75fbbd5afe692f939ea8d14a4292f2c", size = 185089, upload-time = "2026-07-10T06:30:57.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/c6c3dcd7a01097bb0d42f4e9ef21a2c2a491d36b77cd0870ab59f9e8e77f/websockets-16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3b021d0ed4bc16eea9775f62c9fa71acdacba0fc790b38581754dedf29ca60", size = 187790, upload-time = "2026-07-10T06:30:58.731Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3e/775d36885d5e48ab8020aaf377de0ff5fbeb8bc2682a7e46419e4a14521c/websockets-16.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6eb604a4167f0a0d53c2243dfc667a29f0b43c3436057184e070bb82a1000fa2", size = 186381, upload-time = "2026-07-10T06:31:00.355Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/6305c00812a92e47d0582604c02bd759db0118bbafc13f707d712dbcf898/websockets-16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9a3f125e44c3e34d61d111652e608e0f5b85ce08c225c8d56ad0eb822fa40030", size = 188193, upload-time = "2026-07-10T06:31:01.677Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/96bf8302c81d961585b4d34a2ddd3f229782f9b8c57bc78bbf98f1b1a4ac/websockets-16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8fdf0b00d0d1f30d1f06a92cab46fe542eec3eb302a7aee7163f142d0780f216", size = 185771, upload-time = "2026-07-10T06:31:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/e8fe44b1d2dc417d740d9959d28fd2a846f268e7df38a686c04ac7dfe947/websockets-16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67b56828712f5fa7852de4c0265c28827311a657a4d275b7312ed0d1a918bee4", size = 186803, upload-time = "2026-07-10T06:31:04.34Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/b07d3a4e1eb2ab03e94e7f53f0c7a628e85fde6ad86011f7afd08f27b985/websockets-16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39c7e7730be33b8f0cd6f0aa8e8c82f9cdd1813f159765e073b2ece65f4824b5", size = 187041, upload-time = "2026-07-10T06:31:05.567Z" }, + { url = "https://files.pythonhosted.org/packages/a6/fd/e0abb8acc435642ac4a671490f6cf781c882f3fe682cdced9080ea455ab5/websockets-16.1-cp311-cp311-win32.whl", hash = "sha256:c54fe94fb2f11e11b48920c5f971e298cec73ac35db56efe57a49db63dfc95d4", size = 180158, upload-time = "2026-07-10T06:31:06.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/85574d9458d3b913090087b817df0cc47b68e9a01dd0ab6ac04b77f49b0a/websockets-16.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9f4fb9ae8b802e55609685db98382d48fd3feb1397804e1e774968dea0f28c7", size = 180456, upload-time = "2026-07-10T06:31:08.247Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" }, + { url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/63/df158b155420b566f025e75613424ad9649a24bcb0e9f259321ab3d58bea/websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47", size = 179791, upload-time = "2026-07-10T06:31:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/00fe9414dfeafa6fe54eae9f5716c8c8e9ac59d192be3b893c096d395846/websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b", size = 177472, upload-time = "2026-07-10T06:31:34.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/76/b10633424d40681b4e892ffd08ca5226322b2426e62d4ab71eae484c3a32/websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37", size = 177737, upload-time = "2026-07-10T06:31:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/d3bb03b2229bb1afd72008742d586cf1ea240dce64dd48c71c8c7fd3294c/websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881", size = 187403, upload-time = "2026-07-10T06:31:37.496Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/cc2e80478f688fc3c39c67dc1fac6a0783858058914ebc2489917462cb42/websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600", size = 188639, upload-time = "2026-07-10T06:31:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/15/d6/ad87b2507e57de1cbf897a56c963f2925962ed5e85fbe06aaa83ced27acd/websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83", size = 190078, upload-time = "2026-07-10T06:31:40.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1a/5b37b3fd335d5811f29fc829f2646a3e6d1463a4bf09c3100708684c766e/websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045", size = 189267, upload-time = "2026-07-10T06:31:41.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/98/06afc33e9450d4230f94c664db78875d90f5f6a5fb77f0bc6ec15ae74e1c/websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9", size = 188022, upload-time = "2026-07-10T06:31:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/42fef5d5887c18cf2d148b02debf56cecb9cfbffc68027cde9b12c8f432c/websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf", size = 185435, upload-time = "2026-07-10T06:31:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9b/8021c133add5fe40ed40312553a6cd1408c069d7efe3444ad483d4973ed3/websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057", size = 188080, upload-time = "2026-07-10T06:31:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/69/54/1e37384f395eaa127383aab15c1c45e200890a7d7b99db5c312233d193e0/websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d", size = 186678, upload-time = "2026-07-10T06:31:47.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/1caeacab5bc2081e4519288d248bc8bd2de30652e6eaa94be6be09a1fe5b/websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb", size = 188554, upload-time = "2026-07-10T06:31:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/b3dca5fad71487b726e31cb0acf56f226792c1cc34e6ab18cbf146bd2d74/websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7", size = 186109, upload-time = "2026-07-10T06:31:50.508Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/8f246c3712f07f207b52ea5fb47f3b2b66fafec7303162644c74aed51c6a/websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9", size = 187061, upload-time = "2026-07-10T06:31:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/27d6c92a01696b6495386af4fc941d7d0a13f2eab2bf9c336111d7321491/websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d", size = 187347, upload-time = "2026-07-10T06:31:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d5/eeee439921f55d5eaeabcea18d0f7ce32cdc39cb8fc1e185431a094c5c7b/websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4", size = 180149, upload-time = "2026-07-10T06:31:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/971e98d4a4864cf263f9e94c5b2b7c9a9b7682d77bfbba4e732c55ee85a9/websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5", size = 180458, upload-time = "2026-07-10T06:31:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e6/da1dc11507f8118145a81c751fe0c77e5e1c11b8554496addb39389e2dc2/websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e", size = 179833, upload-time = "2026-07-10T06:31:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/c0d46f62e31e232487b2c123bc3cfd9a4e45684ca7dc0c37f0987f29baae/websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b", size = 177524, upload-time = "2026-07-10T06:31:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/4a/33/abd966074b34a51e4f134e0aaed80f5a4a0a35163ea5ac58a1bc5a076d23/websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe", size = 177743, upload-time = "2026-07-10T06:32:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/ea/30/646e47b8a8dff04e227bdab512e6dde60663a647eeac7bbd6edddd92bbc5/websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09", size = 187474, upload-time = "2026-07-10T06:32:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/d2/72/890ab9d77494af93ea65268230bfbc0a90ba789401ed7a44356a44785644/websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209", size = 188717, upload-time = "2026-07-10T06:32:04.156Z" }, + { url = "https://files.pythonhosted.org/packages/d5/aa/baedbbaa6bf9ed6029617ed5e8976535bd805f483ca9b3484e7ad9ee08bf/websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352", size = 190090, upload-time = "2026-07-10T06:32:05.822Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/d813ec94e18002571ef4959d87a630eff6e01b72a51bcb0832b75ae8c51a/websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105", size = 189320, upload-time = "2026-07-10T06:32:07.223Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3c/8ec52a6662f3df64090fba28cd521d405d54759268d8e820477037e8c80d/websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367", size = 188068, upload-time = "2026-07-10T06:32:08.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/f0ae6042b14f86fa5f996c6563ea4cf107adc036ccbedc9d4f418d0095f9/websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87", size = 185493, upload-time = "2026-07-10T06:32:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/5ffc53af9939c49fd653d147fa5b8f78ced1f6bce6c49a7446860945b0ce/websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953", size = 188141, upload-time = "2026-07-10T06:32:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/67/62/729206c0ee577a4db8eae6dd06e0eef725a1287c6df11b2ef831d003df31/websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502", size = 186653, upload-time = "2026-07-10T06:32:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/1b/86/e8806a99ec4589914f255e6b658853fe537bf359c05e6ba5762ad9c27917/websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca", size = 188614, upload-time = "2026-07-10T06:32:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/89/38/ac554e2fc6ff0b8deeff9798b92e7abd8f99e2bd9731532e7033de208220/websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47", size = 186165, upload-time = "2026-07-10T06:32:15.626Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c5/4ef4d8e53342f94f3c49e1ae089b32c1e8b3878e15e0022c7708c647f351/websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d", size = 187119, upload-time = "2026-07-10T06:32:17.114Z" }, + { url = "https://files.pythonhosted.org/packages/3a/33/4788b1dd417bd97eeb2698af3b9df6775ac656f96e9987da0419a067602f/websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee", size = 187411, upload-time = "2026-07-10T06:32:18.629Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/00d37aad6dc3244ce349e2864815362e50b3cfc00cac28d216db20efe40f/websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c", size = 179822, upload-time = "2026-07-10T06:32:20.233Z" }, + { url = "https://files.pythonhosted.org/packages/9d/37/2a8cb0eaddee5eaebda47a90a3ba0898d1ce3d866b02a4857fea17d82e5b/websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145", size = 180167, upload-time = "2026-07-10T06:32:21.749Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/262ad5fcaef4198997b165060f09a63f861e76939b1786ab546ccc3f8120/websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268", size = 180166, upload-time = "2026-07-10T06:32:23.278Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/36377db690f4292826e4501a6dec2801dc55fd1cf0405923b04937e478df/websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901", size = 177697, upload-time = "2026-07-10T06:32:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/07171abce1e39799a76f473608580fe98bd43a1230f5146159622c02bccf/websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79", size = 177902, upload-time = "2026-07-10T06:32:26.564Z" }, + { url = "https://files.pythonhosted.org/packages/14/17/c831f48e250bc4749f57c00dcce73337c41cd32f6d59a64567b84e782601/websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3", size = 187766, upload-time = "2026-07-10T06:32:27.981Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2e/4dfe63e245b0ecfaf470cf082d25c6ce35808159135fd88c82653a6b11ab/websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2", size = 188939, upload-time = "2026-07-10T06:32:29.365Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/5faf65aebd9562f6b4bc473d24ce38cc56f84eb5f5bee66ed9b86733f93c/websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9", size = 191081, upload-time = "2026-07-10T06:32:30.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/2634f2f2c0556c1aae6501ed6840019cc569dd6fdbcac6494378daea4dc0/websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff", size = 189513, upload-time = "2026-07-10T06:32:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/2c700b51196104f09715b326b1f092ed25326bdf79a03e00a4842e503743/websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a", size = 188240, upload-time = "2026-07-10T06:32:33.897Z" }, + { url = "https://files.pythonhosted.org/packages/f1/20/86283636e499a1a357fa9441f690ba34f255e731f2fea174132b3b762b57/websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b", size = 185955, upload-time = "2026-07-10T06:32:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/d7fb734b0095d43bc7f1c9f68afd50adb4176e7e513403e8c70ad7daa4fa/websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd", size = 188491, upload-time = "2026-07-10T06:32:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/168a192689db468405ecf3b8e4a2c18811936b0724d017ad7e6d252734f0/websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97", size = 186983, upload-time = "2026-07-10T06:32:38.207Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9b/66795fa91ebe49019ebe4fa910282172252e37046b80e08fc52e0c365150/websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3", size = 188890, upload-time = "2026-07-10T06:32:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/5a/32/126bbc844be5afb3613fd43211dac10a9645f4cf39741d04acaa2ec7030c/websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805", size = 186583, upload-time = "2026-07-10T06:32:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/22/b9/0b5db9cbcf6e4970db4496893244a8d92e07f71a8ef27cf34b08aa02fef1/websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938", size = 187353, upload-time = "2026-07-10T06:32:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/254b2131a10d831b76e2c18dfe7add9729c6292c674a8085bf8de01ad151/websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a", size = 187784, upload-time = "2026-07-10T06:32:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/21/dc/e7288aa8e3ac5a88a0924619984d663c1abf2a87d0ea98290c66fdaee0ec/websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341", size = 179947, upload-time = "2026-07-10T06:32:45.495Z" }, + { url = "https://files.pythonhosted.org/packages/d3/de/37edf1260ff0fbbd2f82433489c4cfbe799ac2ff21355331609879329fe6/websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07", size = 180291, upload-time = "2026-07-10T06:32:47.119Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f4/84ef884775bbe77c46cce79bc7d705ea3bc6574cc00acf81af89754c077d/websockets-16.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7289d899c79e763e6221c8dcb8959361cb43274418538d7c7ad16a43b01d12f9", size = 177387, upload-time = "2026-07-10T06:32:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d9/6831ec6f65e1eeac770375f4f4b604f23df9bafaa1b47004bc5f9488d513/websockets-16.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22e9e3719f5131bd62da4db63c8da63eb8c91cc99e16c1cbd122f130e1ae07a", size = 177663, upload-time = "2026-07-10T06:32:50.043Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/21d4922fa7fe855813a8b38f181a0ecf02a586e16c1f095fd05471f78cc2/websockets-16.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83bdabafef431247e6b11a9aab8a0893fd8e82e1ed95b32e0373625b03ffce4a", size = 178501, upload-time = "2026-07-10T06:32:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/7a0320df854dacd09507ca972cb04a4dc5aae279583cc5b80ad5f5819533/websockets-16.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b8d13ceabc5c60995f201b5211d76876e17e68706ebf5d3bc666b32eefff1a6", size = 179397, upload-time = "2026-07-10T06:32:52.892Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/0da1eb8c8da2ace7b578c8523d32618af85e62a9ebad56051d4a14a38a1c/websockets-16.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81495f9c0085361c582efbc3207fb877174cfe03370f17d9cd70624404aa526f", size = 180546, upload-time = "2026-07-10T06:32:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" }, +] diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/main.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/main.py index 869546d4..91ac4941 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/main.py +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/main.py @@ -47,6 +47,7 @@ from modules.lipsync.wav2lip.wav2lip_avatar_generator import generate_wav2lip_avatar from pathlib import Path +from threading import Lock import psutil import asyncio from fastapi.responses import StreamingResponse @@ -63,6 +64,9 @@ class Chat(BaseModel): model: Optional[str] = None speed: Optional[str] = None tts_url: Optional[str] = None + # Fill in-between frames with RIFE interpolation (needs the frame + # generation service; only activates when inference can't keep up). + frame_generation: bool = False class ChatOption(BaseModel): @@ -185,24 +189,16 @@ def parse_arguments(): ), ) parser.add_argument( - "--frame_gen", - type=str, - default="auto", - choices=["auto", "on", "off"], - help=( - "RIFE frame generation between lipsync frames. 'auto' (default) " - "measures Wav2Lip inference FPS at startup and enables frame " - "generation only when inference cannot match the avatar video FPS; " - "'on' forces it; 'off' disables it." - ), - ) - parser.add_argument( - "--frame_gen_device", + "--frame_gen_url", type=str, default=None, help=( - "OpenVINO device for the RIFE frame generation model " - "(CPU/GPU/GPU.1/NPU). Defaults to --device." + "Base URL of the frame generation service " + "(default: http://localhost:8031). Frame generation is requested " + "per lipsync request (frame_generation=true); it only activates " + "when Wav2Lip inference alone cannot match the avatar video FPS, " + "and requires the frame generation service to be running. The " + "interpolation device is configured on that service." ), ) return parser.parse_args() @@ -213,9 +209,15 @@ def parse_arguments(): OV_INT8_PATH = "models/wav2lip/checkpoints/wav2lipv2_ov_int8/wav2lip.xml" OV_DEVICES = {"cpu", "gpu", "npu", "auto"} -# Frame generation decision made at startup (see setup_frame_generation) and -# shared with every WebRTC session. -frame_gen_state = {"plan": None} +# Frame generation planning state. The Wav2Lip inference FPS is always +# measured at startup (setup_frame_generation) so a per-request +# frame_generation flag can be honored at any time — the planner needs it to +# know how many frames must be filled. The plan itself additionally needs the +# frame generation service (for its interpolation benchmark), which may start +# after this worker, so it is built at startup when that service is already +# reachable and otherwise lazily by the first request that asks for it +# (ensure_frame_gen_plan). +frame_gen_state = {"plan": None, "context": None, "url": None, "lock": Lock()} def download_wav2lip_checkpoint(source: str): @@ -336,15 +338,13 @@ def preload_ov_model(args): def setup_frame_generation(args): - """Measure device speeds and decide the frame generation plan at startup, - so the model is downloaded, loaded and warmed before any session starts - inferencing.""" + """Measure the Wav2Lip inference FPS at startup and, when the frame + generation service is already reachable, build the frame generation plan + right away so the remote interpolator is benchmarked and warmed before + any session starts inferencing.""" from modules.base.constants import CONSTANTS - from modules.frame_generation.planner import get_avatar_fps, plan_frame_generation - from modules.lipsync.wav2lip.wav2lip_avatar import ( - get_shared_inference, - measure_wav2lip_inference_fps, - ) + from modules.frame_generation.planner import get_avatar_fps + from modules.lipsync.wav2lip.wav2lip_avatar import measure_wav2lip_inference_fps try: with open(Path(args.config).resolve()) as f: @@ -371,23 +371,107 @@ def setup_frame_generation(args): inference_fps = measure_wav2lip_inference_fps( model_path, args.device, batch_size, image_size ) - plan = plan_frame_generation( - mode=args.frame_gen, - device=args.frame_gen_device or args.device, - avatar_path=avatar_path, - avatar_fps=get_avatar_fps(avatar_path), - inference_fps=inference_fps, - batch_size=batch_size, - image_size=image_size, - max_output_fps=1.0 / CONSTANTS.VIDEO_PTIME, - ) + frame_gen_state["url"] = args.frame_gen_url + frame_gen_state["context"] = { + "avatar_type": "wav2lip", + "avatar_fps": get_avatar_fps(avatar_path), + "inference_fps": inference_fps, + "batch_size": batch_size, + "image_size": image_size, + "max_output_fps": 1.0 / CONSTANTS.VIDEO_PTIME, + "model_path": model_path, + "device": args.device, + } + + ensure_frame_gen_plan() + + +def ensure_frame_gen_plan(): + """Return the frame generation plan, building it on first use. + + The plan needs the frame generation service for its interpolation + benchmark, and that service may start after the lipsync worker, so a + missing plan is retried whenever a request asks for frame generation. + Once built (enabled or not) it is reused for the lifetime of the worker. + """ + from modules.frame_generation.client import check_frame_gen_service + from modules.frame_generation.planner import plan_frame_generation + + with frame_gen_state["lock"]: + if frame_gen_state["plan"] is not None: + return frame_gen_state["plan"] + + ctx = frame_gen_state["context"] + if ctx is None: + # Startup measurement was skipped (non-OpenVINO device or missing + # config); frame generation cannot be planned. + return None + + client = check_frame_gen_service(frame_gen_state["url"]) + if client is None: + return None + + plan = plan_frame_generation( + client=client, + avatar_fps=ctx["avatar_fps"], + inference_fps=ctx["inference_fps"], + batch_size=ctx["batch_size"], + image_size=ctx["image_size"], + max_output_fps=ctx["max_output_fps"], + ) + if plan.enabled: + # Precompile the static keyframe-batch model variant sessions + # will use, so the first frame-generated batch doesn't pay + # compile time. + if ctx["avatar_type"] == "musetalk": + from modules.lipsync.musetalk.musetalk_models import ( + get_shared_musetalk_inference, + ) - if plan.enabled: - # Precompile the static keyframe-batch Wav2Lip variant sessions will - # use, so the first connection doesn't pay compile time. - get_shared_inference(model_path, args.device, len(plan.keyframes), image_size) + get_shared_musetalk_inference(ctx["device"], len(plan.keyframes)) + else: + from modules.lipsync.wav2lip.wav2lip_avatar import ( + get_shared_inference, + ) - frame_gen_state["plan"] = plan + get_shared_inference( + ctx["model_path"], + ctx["device"], + len(plan.keyframes), + ctx["image_size"], + ) + frame_gen_state["plan"] = plan + return plan + + +def apply_frame_generation(streamer, requested): + """Toggle frame generation on a session for its upcoming utterance. + + Returns whether frame generation is actually active: the request may ask + for it while the frame generation service is down (no plan) or while + inference alone already reaches the avatar frame rate (plan disabled). + """ + plan = ensure_frame_gen_plan() if requested else None + active = streamer.avatar.set_frame_generation(requested, plan) + if requested and not active: + if plan is None: + reason = ( + "the frame generation service is unreachable or frame " + "generation setup was skipped at startup" + ) + elif not plan.enabled: + reason = ( + "inference alone already matches the avatar frame rate " + f"({plan.inference_fps:.1f} vs {plan.target_fps:.1f} FPS; " + f"frame generation benchmarked at {plan.framegen_fps:.1f} " + "FPS, 0 interpolated frames needed)" + ) + else: + reason = "the avatar model does not support frame generation" + getLogger(__name__).warning( + f"Frame generation was requested but is not active: {reason}." + ) + return active def get_avatar_type(args) -> str: @@ -403,13 +487,13 @@ def get_avatar_type(args) -> str: def initialize_musetalk_models(args): """Download/convert the MuseTalk networks, generate the default avatar and - decide the frame generation plan, mirroring the wav2lip startup path.""" + measure the inference FPS for frame generation planning, mirroring the + wav2lip startup path (setup_frame_generation).""" from modules.base.constants import CONSTANTS - from modules.frame_generation.planner import get_avatar_fps, plan_frame_generation + from modules.frame_generation.planner import get_avatar_fps from modules.lipsync.musetalk.musetalk_models import ( FACE_SIZE, ensure_musetalk_openvino_models, - get_shared_musetalk_inference, get_shared_whisper_encoder, measure_musetalk_inference_fps, ) @@ -435,20 +519,19 @@ def initialize_musetalk_models(args): get_shared_whisper_encoder(args.device) inference_fps = measure_musetalk_inference_fps(args.device, batch_size) - plan = plan_frame_generation( - mode=args.frame_gen, - device=args.frame_gen_device or args.device, - avatar_path=avatar_path, - avatar_fps=get_avatar_fps(avatar_path), - inference_fps=inference_fps, - batch_size=batch_size, - image_size=FACE_SIZE, - max_output_fps=1.0 / CONSTANTS.VIDEO_PTIME, - ) - if plan.enabled: - # Precompile the static keyframe-batch variant sessions will use. - get_shared_musetalk_inference(args.device, len(plan.keyframes)) - frame_gen_state["plan"] = plan + frame_gen_state["url"] = args.frame_gen_url + frame_gen_state["context"] = { + "avatar_type": "musetalk", + "avatar_fps": get_avatar_fps(avatar_path), + "inference_fps": inference_fps, + "batch_size": batch_size, + "image_size": FACE_SIZE, + "max_output_fps": 1.0 / CONSTANTS.VIDEO_PTIME, + "model_path": None, + "device": args.device, + } + + ensure_frame_gen_plan() def initialize_models(args): @@ -624,9 +707,17 @@ async def chat(chat: Chat): return JSONResponse({"status": "invalid session id"}) if chat.chat_type == "echo": + # Building the plan on first use benchmarks the interpolator, so + # keep it off the event loop. + frame_gen_active = await asyncio.to_thread( + apply_frame_generation, avatars[session_id], chat.frame_generation + ) avatars[session_id].echo( chat.text, chat.voice, chat.model, chat.speed, chat.tts_url ) + return JSONResponse( + {"status": "success", "frame_generation": frame_gen_active} + ) elif chat.chat_type == "clear": avatars[session_id].llm_clear_history() @@ -654,6 +745,7 @@ async def audio_lipsync( session_id: str = Form(...), text_overlay: str = Form(None), language_code: str = Form("en-US"), + frame_generation: bool = Form(False), ): if not session_id or session_id not in avatars: raise HTTPException(status_code=400, detail="Invalid or missing session_id") @@ -684,12 +776,18 @@ async def audio_lipsync( if text_overlay: metadata = {"message": text_overlay, "language_code": language_code} + # Building the plan on first use benchmarks the interpolator, so + # keep it off the event loop. + frame_gen_active = await asyncio.to_thread( + apply_frame_generation, avatar_streamer, frame_generation + ) avatar_streamer.process_audio(audio_array, metadata) return JSONResponse( { "status": "success", "session_id": session_id, + "frame_generation": frame_gen_active, "audio_info": { "filename": file.filename, "duration_seconds": len(audio_array) / sample_rate, diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/client.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/client.py new file mode 100644 index 00000000..cf46f21a --- /dev/null +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/client.py @@ -0,0 +1,105 @@ +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +"""HTTP client for the standalone frame generation worker. + +The RIFE interpolator runs in its own service (workers/frame-generation) so +its device is configured independently of the lipsync worker. This client +keeps the in-process generator's interpolate_gaps() contract, so the lipsync +inference loop is agnostic to where interpolation actually runs. +""" + +import io + +import numpy as np +import requests + +from modules.base.logger import getLogger + +DEFAULT_FRAME_GEN_URL = "http://localhost:8031" + + +class FrameGenerationClient: + """Thin client over the frame generation service's binary npz API.""" + + def __init__(self, base_url=DEFAULT_FRAME_GEN_URL, timeout=30.0): + self.base_url = (base_url or DEFAULT_FRAME_GEN_URL).rstrip("/") + self.timeout = timeout + self._session = requests.Session() + + def is_ready(self): + """True when the service is up and past model download/warmup.""" + try: + resp = self._session.get(f"{self.base_url}/healthcheck", timeout=5.0) + return resp.status_code == 200 + except requests.RequestException: + return False + + def benchmark(self, image_size, gap_sizes, rounds=3): + """Measured interpolated frames/sec on the given gap schedule. + + The service also warms any lazily-compiled static shapes for the + schedule, so benchmarking the production schedule doubles as warmup. + Benchmarks compile + run the model, which can take minutes on + GPU/NPU first time; no timeout is applied here. + """ + resp = self._session.post( + f"{self.base_url}/v1/frame-generation/benchmark", + json={ + "image_size": image_size, + "gap_sizes": list(gap_sizes), + "rounds": rounds, + }, + timeout=(5.0, 900.0), + ) + resp.raise_for_status() + return float(resp.json()["fps"]) + + def interpolate_gaps(self, gaps): + """ + Fill several keyframe gaps with interpolated frames. + + Same contract as OpenVINOFrameGenerator.interpolate_gaps: + + Args: + gaps: list of (frame_a, frame_b, n_frames) tuples, frames as + HxWx3 arrays in 0..255. + + Returns: + list: for each gap, a list of n_frames interpolated HxWx3 + float32 frames in 0..255. + """ + frames_a = np.stack([np.clip(a, 0, 255).astype(np.uint8) for a, _, _ in gaps]) + frames_b = np.stack([np.clip(b, 0, 255).astype(np.uint8) for _, b, _ in gaps]) + counts = np.array([n for _, _, n in gaps], dtype=np.int64) + + payload = io.BytesIO() + np.savez(payload, frames_a=frames_a, frames_b=frames_b, counts=counts) + + resp = self._session.post( + f"{self.base_url}/v1/frame-generation/interpolate", + data=payload.getvalue(), + headers={"Content-Type": "application/octet-stream"}, + timeout=self.timeout, + ) + resp.raise_for_status() + + data = np.load(io.BytesIO(resp.content), allow_pickle=False) + results = [] + for i in range(len(gaps)): + fill = data[f"gap_{i}"] + results.append([frame.astype(np.float32) for frame in fill]) + return results + + +def check_frame_gen_service(base_url): + """Log a helpful message and return a ready client, or None if down.""" + client = FrameGenerationClient(base_url) + if client.is_ready(): + return client + getLogger(__file__).warning( + f"Frame generation service is not reachable at {client.base_url}. " + "Start the Frame Generation service (workers/frame-generation) to " + "enable frame generation." + ) + return None diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/planner.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/planner.py index 96b8da87..e15c6a57 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/planner.py +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/frame_generation/planner.py @@ -1,65 +1,37 @@ # Copyright (C) 2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -"""Startup planning for RIFE frame generation in the lipsync worker. - -At service startup the worker measures how many frames per second the Wav2Lip -model can infer on the chosen device and compares it with the avatar video's -frame rate. When inference alone cannot keep up (or the user forces it on), -frame generation is enabled: only every N-th frame is inferred by Wav2Lip and -the frames between keyframes are filled by the RIFE interpolator. - -The planner loads and warms the frame generation model once, verifies the +"""Planning for RIFE frame generation in the lipsync worker. + +The worker measures how many frames per second the Wav2Lip model can infer on +the chosen device at startup and compares it with the avatar video's frame +rate. When inference alone cannot keep up, frame generation is enabled: only +every N-th frame is inferred by Wav2Lip and the frames between keyframes are +filled by the RIFE interpolator. Whether an utterance actually uses the plan +is decided per lipsync request (frame_generation=true). + +Interpolation itself runs in the standalone frame generation service +(workers/frame-generation), reached through FrameGenerationClient; its device +is configured on that service. The planner benchmarks the remote interpolator +on the exact schedule lip_sync will use (which also warms it), verifies the combined pipeline can reach the target frame rate, and logs the avatar FPS, the measured Wav2Lip inference FPS and the measured frame generation FPS. """ import json import os -import shutil -import time from dataclasses import dataclass, field -from glob import glob from threading import Lock -import numpy as np - from modules.base.logger import getLogger -RIFE_MODEL_PATH = "models/rife/flownet.safetensors" DEFAULT_AVATAR_FPS = 25.0 MAX_MULTIPLIER = 4 # never interpolate more than 3 consecutive frames # Keep 15% headroom when judging real-time feasibility: audio processing, -# frame merging and queue handoffs share the same wall clock. +# frame merging, interpolation round-trips and queue handoffs share the same +# wall clock. LOAD_MARGIN = 0.85 -_GENERATOR_CACHE = {} -_CACHE_LOCK = Lock() - - -def download_rife_model(model_path=RIFE_MODEL_PATH, source="huggingface"): - """Fetch the RIFE flownet safetensors weights if not already present.""" - if os.path.exists(model_path): - return model_path - - getLogger(__file__).info("Downloading RIFE frame generation model...") - if source == "modelscope": - from modelscope import snapshot_download - - repo_dir = snapshot_download( - "TensorForger/RIFE-safetensors", allow_patterns=["flownet.safetensors"] - ) - src = os.path.join(repo_dir, "flownet.safetensors") - else: - from huggingface_hub import hf_hub_download - - src = hf_hub_download("TensorForger/RIFE-safetensors", "flownet.safetensors") - - os.makedirs(os.path.dirname(model_path), exist_ok=True) - shutil.copy(src, model_path) - getLogger(__file__).info(f"RIFE model ready at {model_path}") - return model_path - def get_avatar_fps(avatar_path): """Frame rate of the avatar's source video, recorded in its config.json by @@ -75,33 +47,6 @@ def get_avatar_fps(avatar_path): return DEFAULT_AVATAR_FPS -def get_shared_frame_generator(device, model_path=RIFE_MODEL_PATH): - """Load (once) and share an OpenVINO frame generator and its inference lock. - - The lock must be held around interpolate_gaps because the underlying - inference request is not thread-safe and is shared by every session. - """ - key = device.lower() - with _CACHE_LOCK: - entry = _GENERATOR_CACHE.get(key) - if entry is None: - download_rife_model(model_path) - from modules.frame_generation.frame_generator_ov import ( - OpenVINOFrameGenerator, - ) - - generator = OpenVINOFrameGenerator(device, model_path) - entry = {"generator": generator, "lock": Lock()} - _GENERATOR_CACHE[key] = entry - return entry["generator"], entry["lock"] - - -def release_shared_frame_generator(device): - """Drop the cached generator (e.g. when auto mode decides it is unneeded).""" - with _CACHE_LOCK: - _GENERATOR_CACHE.pop(device.lower(), None) - - def keyframe_positions(batch_size, multiplier): """Indices inside a batch that get real Wav2Lip inference; the rest are interpolated. Both batch endpoints are keyframes so every interpolated @@ -125,7 +70,8 @@ def _schedule_gaps(batch_size, multiplier): @dataclass class FrameGenPlan: """Decision shared with every lipsync session: whether frame generation is - on, which batch positions are Wav2Lip keyframes, and the shared generator.""" + on, which batch positions are Wav2Lip keyframes, and the client for the + frame generation service.""" enabled: bool = False multiplier: int = 1 @@ -139,83 +85,18 @@ class FrameGenPlan: effective_fps: float = 0.0 -def _benchmark_frames(avatar_path, image_size, count): - """Consecutive avatar face crops resized to the interpolator's input size, - matching the float BGR [0, 255] frames lip_sync feeds it at runtime. - - Wav2lip avatars store pre-cropped faces in face_images; MuseTalk avatars - only keep full_images, whose resized frames still contain the face. Cycles - when the avatar has fewer frames than the schedule needs; falls back to - flat synthetic frames if no avatar image can be read (interpolation time - is content-independent, so only realism is lost, not accuracy). - """ - try: - import cv2 - - for subdir in ("face_images", "full_images"): - pattern = os.path.join(avatar_path, subdir, "*.[jpJP][pnPN]*[gG]") - paths = sorted( - glob(pattern), - key=lambda p: int(os.path.splitext(os.path.basename(p))[0]), - )[:count] - images = [img for p in paths if (img := cv2.imread(p)) is not None] - if images: - return [ - cv2.resize( - images[i % len(images)], (image_size, image_size) - ).astype(np.float32) - for i in range(count) - ] - except Exception: - pass - getLogger(__file__).warning( - f"No readable avatar images under {avatar_path}; benchmarking frame " - "generation on synthetic frames." - ) - return [ - np.full((image_size, image_size, 3), 64.0 + 32.0 * i, dtype=np.float32) - for i in range(count) - ] - - -def measure_framegen_fps(generator, image_size, gap_sizes, avatar_path, rounds=3): - """Median interpolated frames/sec on the exact schedule lip_sync will use, - fed with the avatar's own face frames. - - Also serves as the schedule-specific warmup: it primes any lazily-compiled - static shapes (NPU) before the first real batch arrives. - """ - frames = _benchmark_frames(avatar_path, image_size, len(gap_sizes) + 1) - gaps = [(frames[i], frames[i + 1], n) for i, n in enumerate(gap_sizes)] - - generator.interpolate_gaps(gaps) - times = [] - for _ in range(rounds): - start = time.perf_counter() - results = generator.interpolate_gaps(gaps) - times.append(time.perf_counter() - start) - - produced = sum(len(r) for r in results) - return produced / sorted(times)[len(times) // 2] - - def plan_frame_generation( - mode, - device, - avatar_path, + client, avatar_fps, inference_fps, batch_size, image_size, max_output_fps=None, - model_path=RIFE_MODEL_PATH, ): """Decide whether (and how densely) to interpolate, given measured speeds. Args: - mode: "auto" | "on" | "off". - device: device for the frame generation model. - avatar_path: avatar directory; its face frames feed the benchmark. + client: FrameGenerationClient for the frame generation service. avatar_fps: frame rate of the avatar's source video (the target). inference_fps: measured Wav2Lip frames/sec on the inference device. batch_size: frames per lipsync batch. @@ -223,7 +104,8 @@ def plan_frame_generation( max_output_fps: hard cap of the streaming pipeline, if any. Returns: - FrameGenPlan + FrameGenPlan (enabled=False when inference alone already reaches the + target frame rate, so there is nothing to fill). """ log = getLogger(__file__) @@ -242,48 +124,34 @@ def plan_frame_generation( effective_fps=inference_fps, ) - if mode == "off": - log.info( - f"FPS summary: avatar={avatar_fps:.1f}, wav2lip inference=" - f"{inference_fps:.1f}, frame generation=off" - ) - if inference_fps < target_fps: - log.warning( - f"Frame generation is off but Wav2Lip inference " - f"({inference_fps:.1f} FPS) is below the avatar frame rate " - f"({target_fps:.1f} FPS); playback may stutter." - ) - return plan - - # Load and warm the generator on the requested device so the measurement - # (and, if enabled, the first real batch) runs on a ready model. - generator, lock = get_shared_frame_generator(device, model_path) - frame_time = generator.warm_up(image_size) - estimated_fg_fps = 1.0 / frame_time + # A single-frame gap benchmark doubles as warmup on the service side and + # gives a conservative per-frame throughput estimate. + estimated_fg_fps = client.benchmark(image_size, [1]) - if mode == "auto" and inference_fps >= target_fps: + if inference_fps >= target_fps: plan.framegen_fps = estimated_fg_fps log.info( f"FPS summary: avatar={avatar_fps:.1f}, wav2lip inference=" f"{inference_fps:.1f}, frame generation~{estimated_fg_fps:.1f} " - f"(single-pair estimate)" + f"(single-pair estimate), total={inference_fps:.1f}" ) log.info( "Frame generation not needed: inference alone matches the avatar " - "frame rate; releasing the frame generation model." + f"frame rate, so 0 of every {batch_size} frames need " + "interpolation." ) - release_shared_frame_generator(device) return plan # Pick the lowest multiplier whose keyframe inference + interpolation fits - # in real time (lowest = most real frames). The single-pair warmup time is - # a conservative throughput estimate for this selection; the chosen - # schedule is then measured for real. + # in real time (lowest = most real frames). The single-pair estimate is + # used for this selection; the chosen schedule is then measured for real. budget = LOAD_MARGIN * batch_size / target_fps def batch_cost(multiplier, fg_fps): n_keyframes = len(keyframe_positions(batch_size, multiplier)) - return n_keyframes / inference_fps + (batch_size - n_keyframes) / fg_fps + return ( + n_keyframes / inference_fps + (batch_size - n_keyframes) / fg_fps + ) multiplier = next( ( @@ -301,27 +169,31 @@ def batch_cost(multiplier, fg_fps): ) positions = keyframe_positions(batch_size, multiplier) - framegen_fps = measure_framegen_fps( - generator, image_size, _schedule_gaps(batch_size, multiplier), avatar_path + framegen_fps = client.benchmark( + image_size, _schedule_gaps(batch_size, multiplier) ) effective_fps = batch_size / batch_cost(multiplier, framegen_fps) plan.enabled = True plan.multiplier = multiplier plan.keyframes = positions - plan.generator = generator - plan.lock = lock + plan.generator = client + # Serializes sessions' interpolate calls so batches stay ordered; the + # service additionally serializes inference on its side. + plan.lock = Lock() plan.framegen_fps = framegen_fps plan.effective_fps = effective_fps log.info( f"FPS summary: avatar={avatar_fps:.1f}, wav2lip inference=" - f"{inference_fps:.1f}, frame generation={framegen_fps:.1f}" + f"{inference_fps:.1f}, frame generation={framegen_fps:.1f}, " + f"total~{effective_fps:.1f}" ) log.info( - f"Frame generation enabled on {device}: {len(positions)} of every " - f"{batch_size} frames inferred (multiplier {multiplier}), the rest " - f"interpolated => ~{effective_fps:.1f} FPS effective." + f"Frame generation enabled via {client.base_url}: {len(positions)} of " + f"every {batch_size} frames inferred (multiplier {multiplier}), the " + f"remaining {batch_size - len(positions)} interpolated => " + f"~{effective_fps:.1f} FPS effective." ) if effective_fps < target_fps: log.warning( diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/lipsync_avatar.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/lipsync_avatar.py index 8978db6f..c835e4d9 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/lipsync_avatar.py +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/lipsync_avatar.py @@ -181,13 +181,17 @@ def __init__(self, avatar_id, configs, device, frame_gen_plan=None): self.load_model() - # Frame generation: infer only the plan's keyframe positions and fill - # the frames in between with RIFE interpolation. - self.frame_gen_plan = ( - frame_gen_plan if frame_gen_plan and frame_gen_plan.enabled else None - ) - if self.frame_gen_plan: - self._prepare_keyframe_inference() + # Frame generation: when active, infer only the plan's keyframe + # positions and fill the frames in between with RIFE interpolation. + # The plan is adopted here when it already exists, or later through + # set_frame_generation() (the plan may only get built after this + # session connected, once the frame generation service is up). + # Whether an utterance actually uses it is the per-request + # frame_gen_active flag. + self.frame_gen_plan = None + self.frame_gen_active = False + if frame_gen_plan and frame_gen_plan.enabled: + self._adopt_frame_gen_plan(frame_gen_plan) self.cv_frames, self.face_frames, self.face_frames_len, self.coords_list = ( self.load_avatar(self.avatar_path) @@ -258,6 +262,26 @@ def _prepare_keyframe_inference(self): # Shared pipeline # ------------------------------------------------------------------ + def _adopt_frame_gen_plan(self, plan): + self.frame_gen_plan = plan + self._prepare_keyframe_inference() + + def set_frame_generation(self, active, plan=None): + """Toggle frame generation for this session's upcoming utterances. + + Called per lipsync request. Adopts `plan` on first activation when + the session was created before the plan existed. The flag is read at + batch granularity by lip_sync, so overlapping requests with different + flags resolve to whichever was set last. Returns whether frame + generation is now active. + """ + if active and self.frame_gen_plan is None and plan and plan.enabled: + self._adopt_frame_gen_plan(plan) + # _prepare_keyframe_inference may have vetoed the plan (unsupported + # model), so re-check it before activating. + self.frame_gen_active = bool(active) and self.frame_gen_plan is not None + return self.frame_gen_active + def __del__(self): getLogger(__file__).info("Avatar deleted") self.unload_models() @@ -459,7 +483,7 @@ def lip_sync(self, signal_event, debug=False): ) index = index + 1 else: - if self.frame_gen_plan: + if self.frame_gen_plan and self.frame_gen_active: pred = self._run_keyframed_inference(feature_batch, index, debug) n_keyframes = len(self.frame_gen_plan.keyframes) self.frames_inferred += n_keyframes diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/musetalk/musetalk_avatar.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/musetalk/musetalk_avatar.py index 8cb58ddc..2d09fd59 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/musetalk/musetalk_avatar.py +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/musetalk/musetalk_avatar.py @@ -85,6 +85,7 @@ def unload_models(self): self.whisper_entry = None self.kf_entry = None self.frame_gen_plan = None + self.frame_gen_active = False def _prepare_keyframe_inference(self): # Keyframe-only inference uses a dedicated UNet + VAE decoder pair diff --git a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/wav2lip/wav2lip_avatar.py b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/wav2lip/wav2lip_avatar.py index 18385ddf..9c346027 100644 --- a/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/wav2lip/wav2lip_avatar.py +++ b/samples/ai/edge-ai-demo-studio/workers/lipsync/modules/lipsync/wav2lip/wav2lip_avatar.py @@ -140,6 +140,7 @@ def unload_models(self): self.kf_request = None self.kf_lock = None self.frame_gen_plan = None + self.frame_gen_active = False def _convert_to_openvino(self): """Convert PyTorch checkpoint to OpenVINO IR if not already done. diff --git a/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/paddleocr_vl/model.py b/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/paddleocr_vl/model.py index 4bf3d729..b6ffc27a 100644 --- a/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/paddleocr_vl/model.py +++ b/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/paddleocr_vl/model.py @@ -101,6 +101,12 @@ def __init__( # ── Loading / conversion ─────────────────────────────────────────────── def load(self) -> None: + if self.device.split(".")[0].upper() == "NPU": + raise RuntimeError( + f"{self.name} does not support NPU. Load it on CPU or GPU, or " + f"pick a PP-OCR preset (ppocrv5 / ppocrv5-server / ppocrv3) to " + f"run OCR on the NPU." + ) _ensure_vendor_on_path() try: import ov_paddleocr_vl as vl # noqa: WPS433 (vendored module) diff --git a/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/ppocr/model.py b/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/ppocr/model.py index df1ecb7f..e5210658 100644 --- a/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/ppocr/model.py +++ b/samples/ai/edge-ai-demo-studio/workers/ocr/paddleocr/models/ppocr/model.py @@ -5,6 +5,7 @@ import logging import math import tarfile +import time from pathlib import Path from typing import Any @@ -26,6 +27,33 @@ _REC_BATCH = 6 _DEFAULT_DROP_SCORE = 0.5 +# ── Static-shape ladders (NPU) ────────────────────────────────────────────── +# +# The NPU compiler rejects unbounded dynamic dimensions, so on NPU each model is +# compiled at a fixed shape picked from a ladder and the image is resized to +# fill it *exactly*. Filling rather than letterbox-padding is deliberate: both +# networks carry squeeze-excite blocks whose GlobalAveragePool runs over the +# whole feature map, so padded area leaks into the channel attention and wrecks +# detection (empirically: two words merge into one box at 960x960 with 84% pad). +# A little aspect distortion is harmless by comparison — the dynamic path +# already distorts by rounding to multiples of 32, and recognition re-crops from +# the untouched original image either way. +# +# Detection: the long side is pinned to det_limit_side_len and the short side +# comes from this ladder, so a shape is chosen by aspect ratio alone. +_DET_SHORT_SIDES = (960, 768, 640, 512, 416, 320, 256, 192, 160, 128, 96, 64, 32) +# Recognition: each crop is padded out to the next width up. Keeping the rungs +# tight (multiples of 64) keeps that padding close to what the dynamic path +# already adds when it pads a batch out to its widest crop. +_REC_WIDTHS = (64, 128, 192, 256, 320, 384, 448, 512, 640, 768, 960) + +# Compiled NPU blobs are cached on disk, keyed by shape. They are not small (a +# 960x960 detector is ~36 MB, a recogniser ~9.5 MB) and the ladders above allow +# enough combinations to reach a few hundred MB, so the directory is capped and +# trimmed oldest-first. Losing a blob only costs its ~0.5-1s recompile the next +# time that shape comes round. +_OV_CACHE_MAX_BYTES = 512 * 1024 * 1024 + # ImageNet normalisation used by PaddleOCR detection models. _DET_MEAN = np.array([0.485, 0.456, 0.406]).reshape(1, 1, 3).astype("float32") _DET_STD = np.array([0.229, 0.224, 0.225]).reshape(1, 1, 3).astype("float32") @@ -88,6 +116,35 @@ def _ensure_model_file(url: str, cache_dir: Path) -> Path: raise ValueError(f"Unsupported model URL (expected .onnx or .tar): {url}") +def _prune_blob_cache(cache_dir: Path, max_bytes: int = _OV_CACHE_MAX_BYTES) -> None: + """Trim the compiled-blob cache to ``max_bytes``, oldest first. + + Ordering is by mtime, i.e. when a shape was first compiled rather than when + it was last used — OpenVINO only touches a blob at compile time, and a + process keeps its compiled models in memory, so there is no read to observe. + Close enough for a cache whose worst case is a recompile. + """ + try: + blobs = [(b, b.stat()) for b in cache_dir.glob("*.blob")] + except OSError: + return + total = sum(st.st_size for _, st in blobs) + if total <= max_bytes: + return + logger.info( + f"[cache] {cache_dir} at {total / 1e6:.0f} MB, trimming to " + f"{max_bytes / 1e6:.0f} MB" + ) + for blob, st in sorted(blobs, key=lambda item: item[1].st_mtime): + if total <= max_bytes: + break + try: + blob.unlink() + except OSError: # already gone, or in use by another worker + continue + total -= st.st_size + + def _ensure_text_file(url: str, cache_dir: Path) -> Path: """Download a plain text asset (e.g. the character dictionary).""" cache_dir.mkdir(parents=True, exist_ok=True) @@ -97,20 +154,52 @@ def _ensure_text_file(url: str, cache_dir: Path) -> Path: # ── Detection: preprocessing ──────────────────────────────────────────────── -def _det_preprocess(img_bgr: np.ndarray, limit_side_len: int = 960) -> np.ndarray: +def _det_static_shape( + img_shape: tuple[int, int], limit_side_len: int +) -> tuple[int, int]: + """Pick the compiled detection shape ``(H, W)`` closest to this aspect ratio. + + The long side is pinned to ``limit_side_len`` (32-aligned) and the short + side is the rung of :data:`_DET_SHORT_SIDES` whose ratio is nearest in log + space, so the error is proportional rather than absolute. Images smaller + than the limit are scaled up to it; that costs a little inference time but + keeps the number of distinct compiled shapes small, which matters far more + on a device where every shape is a separate blob to compile and hold. + """ + h, w = img_shape + long_side = max(int(round(limit_side_len / 32) * 32), 32) + target = min(h, w) / max(h, w) + short = min( + (s for s in _DET_SHORT_SIDES if s <= long_side), + key=lambda s: abs(math.log((s / long_side) / target)), + ) + return (short, long_side) if w >= h else (long_side, short) + + +def _det_preprocess( + img_bgr: np.ndarray, + limit_side_len: int = 960, + static_shape: tuple[int, int] | None = None, +) -> np.ndarray: """Resize (longest side <= limit, multiple of 32) + ImageNet-normalise. Returns the ``(1, 3, H, W)`` float32 network input. Boxes are rescaled back - to the original image directly in :func:`_postprocess_det`. + to the original image directly in :func:`_postprocess_det`, which only needs + the source shape — the resize fills the network input completely, in both + the dynamic and the static (``static_shape``) case, so there is no padding + offset to undo. """ - h, w = img_bgr.shape[:2] - if max(h, w) > limit_side_len: - ratio = limit_side_len / (h if h > w else w) + if static_shape is not None: + resize_h, resize_w = static_shape else: - ratio = 1.0 + h, w = img_bgr.shape[:2] + if max(h, w) > limit_side_len: + ratio = limit_side_len / (h if h > w else w) + else: + ratio = 1.0 - resize_h = max(int(round(h * ratio / 32) * 32), 32) - resize_w = max(int(round(w * ratio / 32) * 32), 32) + resize_h = max(int(round(h * ratio / 32) * 32), 32) + resize_w = max(int(round(w * ratio / 32) * 32), 32) resized = cv2.resize(img_bgr, (resize_w, resize_h)) img = resized.astype("float32") / 255.0 @@ -288,10 +377,18 @@ def _get_rotate_crop_image(img: np.ndarray, points: np.ndarray) -> np.ndarray: def _resize_norm_img( - img: np.ndarray, max_wh_ratio: float, img_height: int + img: np.ndarray, + max_wh_ratio: float, + img_height: int, + target_w: int | None = None, ) -> np.ndarray: - """Resize a crop to fixed height, normalise to [-1, 1], right-pad width.""" - img_w = int(img_height * max_wh_ratio) + """Resize a crop to fixed height, normalise to [-1, 1], right-pad width. + + ``target_w`` pins the padded width exactly, which the static (NPU) path uses + to pad every crop out to its compiled width rung; otherwise the width + follows ``max_wh_ratio`` as usual. + """ + img_w = int(target_w) if target_w is not None else int(img_height * max_wh_ratio) h, w = img.shape[:2] ratio = w / float(h) resized_w = min(img_w, int(math.ceil(img_height * ratio))) @@ -368,6 +465,13 @@ def __init__( self._det_out = None self._rec = None self._rec_out = None + # Static-shape (NPU) path: the model files, plus models compiled lazily + # and keyed by input shape. All stay unset on CPU/GPU, where a single + # dynamic-shape compilation covers every input. + self._static_paths: tuple[Path, Path] | None = None + self._compile_cfg: dict[str, Any] = {} + self._det_cache: dict[tuple[int, ...], tuple[Any, Any]] = {} + self._rec_cache: dict[tuple[int, ...], tuple[Any, Any]] = {} self._decoder: _CTCLabelDecode | None = None # ── Loading ──────────────────────────────────────────────────────────── @@ -377,30 +481,90 @@ def load(self) -> None: det_path = _ensure_model_file(self._det_url, self._cache_dir) rec_path = _ensure_model_file(self._rec_url, self._cache_dir) dict_path = _ensure_text_file(self._dict_url, self._cache_dir) + self._decoder = _CTCLabelDecode(str(dict_path), use_space_char=True) - det_model = core.read_model(det_path) - self._reshape_input(det_model, ov.PartialShape([-1, 3, -1, -1])) - self._det = core.compile_model(det_model, self.device) - self._det_out = self._det.output(0) - - rec_model = core.read_model(rec_path) - rec_shape = rec_model.input(0).partial_shape - rec_shape[3] = -1 # dynamic width - self._reshape_input(rec_model, rec_shape) - self._rec = core.compile_model(rec_model, self.device) - self._rec_out = self._rec.output(0) + if self._needs_static_shapes(): + # Nothing is compiled up front: an image touches exactly one + # detection shape and only the few recognition widths its crops + # need, so walking the whole ladder eagerly would throw most of the + # work away. Blobs land in CACHE_DIR, so each shape pays for its + # (slow) NPU compilation once per machine rather than once per run. + self._static_paths = (det_path, rec_path) + self._compile_cfg = {"CACHE_DIR": str(self._cache_dir / "ov_cache")} + logger.info( + f"[{self.name}] {self.device} needs static shapes: detection at " + f"a {self.det_limit_side_len}px long side, recognition widths " + f"{list(_REC_WIDTHS)}; compiled on first use." + ) + else: + det_model = core.read_model(det_path) + self._reshape_input(det_model, ov.PartialShape([-1, 3, -1, -1])) + self._det = core.compile_model(det_model, self.device) + self._det_out = self._det.output(0) + + rec_model = core.read_model(rec_path) + rec_shape = rec_model.input(0).partial_shape + rec_shape[3] = -1 # dynamic width + self._reshape_input(rec_model, rec_shape) + self._rec = core.compile_model(rec_model, self.device) + self._rec_out = self._rec.output(0) - self._decoder = _CTCLabelDecode(str(dict_path), use_space_char=True) logger.info( f"[{self.name}] det={Path(det_path).name} " f"rec={Path(rec_path).name} dict={Path(dict_path).name}" ) + def _needs_static_shapes(self) -> bool: + """True when the target device rejects unbounded dynamic dimensions. + + The NPU compiler refuses any model with an unbounded dimension ("Upper + bounds are not specified for node ..."), so on NPU both stages run at + fixed shapes drawn from a ladder. CPU and GPU keep the dynamic shapes, + which suit them better anyway: nothing is resized to a rung or padded + out to one. + """ + return self.device.split(".")[0].upper() == "NPU" + + def _compiled( + self, + path: Path, + shape: tuple[int, ...], + cache: dict[tuple[int, ...], tuple[Any, Any]], + ) -> tuple[Any, Any]: + """Compile ``path`` at a fixed ``shape``, memoised in ``cache``. + + Called from the inference path, which the base class already serialises + behind the model lock, so the cache needs no extra guarding. + """ + hit = cache.get(shape) + if hit is None: + t0 = time.time() + core = get_core() + model = core.read_model(path) + self._reshape_input(model, ov.PartialShape(list(shape)), strict=True) + compiled = core.compile_model(model, self.device, self._compile_cfg) + hit = cache[shape] = (compiled, compiled.output(0)) + _prune_blob_cache(Path(self._compile_cfg["CACHE_DIR"])) + logger.info( + f"[{self.name}] compiled {Path(path).stem} at {list(shape)} on " + f"{self.device} in {(time.time() - t0) * 1000:.0f} ms" + ) + return hit + @staticmethod - def _reshape_input(model: ov.Model, shape: ov.PartialShape) -> None: + def _reshape_input( + model: ov.Model, shape: ov.PartialShape, strict: bool = False + ) -> None: try: model.reshape({model.input(0): shape}) except Exception as exc: # static models: keep their fixed shape + if strict: + # Quietly keeping the original shape would hand the NPU a model + # it cannot compile, surfacing as an opaque error much later. + raise RuntimeError( + f"Could not reshape model input to {shape}, the static " + f"shape this device requires: {exc}" + ) from exc logger.debug(f"reshape skipped: {exc}") # ── Inference ────────────────────────────────────────────────────────── @@ -409,9 +573,7 @@ def _infer(self, image: np.ndarray, **kwargs: Any) -> OCRResult: drop_score = float(kwargs.get("drop_score", _DEFAULT_DROP_SCORE)) src_h, src_w = image.shape[:2] - net_in = _det_preprocess(image, self.det_limit_side_len) - pred = self._det([net_in])[self._det_out] - dt_boxes = _postprocess_det(pred, (src_h, src_w)) + dt_boxes = self._detect(image) if len(dt_boxes) == 0: return OCRResult( @@ -441,12 +603,36 @@ def _infer(self, image: np.ndarray, **kwargs: Any) -> OCRResult: extra={"device": self.device, "num_detected": len(dt_boxes)}, ) + def _detect(self, image: np.ndarray) -> np.ndarray: + """Detect text boxes, in original-image coordinates.""" + src_h, src_w = image.shape[:2] + if self._static_paths is None: + net_in = _det_preprocess(image, self.det_limit_side_len) + det, det_out = self._det, self._det_out + else: + shape = _det_static_shape((src_h, src_w), self.det_limit_side_len) + net_in = _det_preprocess( + image, self.det_limit_side_len, static_shape=shape + ) + det, det_out = self._compiled( + self._static_paths[0], (1, 3, *shape), self._det_cache + ) + return _postprocess_det(det([net_in])[det_out], (src_h, src_w)) + def _recognise(self, crops: list[np.ndarray]) -> list[tuple[str, float]]: - """Batch the crops (sorted by aspect ratio) through recognition.""" - results: list[tuple[str, float]] = [("", 0.0)] * len(crops) + """Batch the crops (sorted by aspect ratio) through recognition. + + The static (NPU) path runs them one at a time instead — see + :meth:`_recognise_one`. + """ ratios = [c.shape[1] / max(c.shape[0], 1) for c in crops] - order = np.argsort(ratios) + if self._static_paths is not None: + # Sorting by aspect ratio only exists to hold a batch's padding + # down, and this path does not batch, so take them in order. + return [self._recognise_one(c, r) for c, r in zip(crops, ratios)] + results: list[tuple[str, float]] = [("", 0.0)] * len(crops) + order = np.argsort(ratios) for beg in range(0, len(crops), _REC_BATCH): end = min(len(crops), beg + _REC_BATCH) max_ratio = max(ratios[order[i]] for i in range(beg, end)) @@ -461,7 +647,33 @@ def _recognise(self, crops: list[np.ndarray]) -> list[tuple[str, float]]: results[order[beg + k]] = decoded return results + def _recognise_one(self, crop: np.ndarray, ratio: float) -> tuple[str, float]: + """Recognise a single crop at the narrowest compiled width that fits. + + The static path deliberately runs one crop at a time instead of filling + a fixed ``_REC_BATCH``, which would mean padding the tail batch with + blank rows. On NPU batch 1 measured level with batch 6 at the narrow + widths and ~2.4x faster at the widest, while compiling in a fraction of + the time and caching much smaller blobs. Going one at a time also picks + the width per crop rather than per batch, so a batch's widest crop no + longer inflates the padding on all the others. + """ + needed = int(math.ceil(self.rec_image_height * ratio)) + width = next((w for w in _REC_WIDTHS if w >= needed), _REC_WIDTHS[-1]) + rec, rec_out = self._compiled( + self._static_paths[1], + (1, 3, self.rec_image_height, width), + self._rec_cache, + ) + net_in = _resize_norm_img( + crop, ratio, self.rec_image_height, target_w=width + )[np.newaxis, ...] + return self._decoder(rec([net_in])[rec_out])[0] + def _teardown(self) -> None: self._det = self._det_out = None self._rec = self._rec_out = None + self._static_paths = None + self._det_cache = {} + self._rec_cache = {} self._decoder = None diff --git a/samples/ai/edge-ai-demo-studio/workers/suite/manufacturing-ai-suite/pallet-defect-detection/start.sh b/samples/ai/edge-ai-demo-studio/workers/suite/manufacturing-ai-suite/pallet-defect-detection/start.sh index 48490ff0..99593771 100644 --- a/samples/ai/edge-ai-demo-studio/workers/suite/manufacturing-ai-suite/pallet-defect-detection/start.sh +++ b/samples/ai/edge-ai-demo-studio/workers/suite/manufacturing-ai-suite/pallet-defect-detection/start.sh @@ -344,6 +344,8 @@ HEADER generate_override +mkdir -p "$SUITE_DIR/apps/$SAMPLE_APP/Certificates/ssl" + # ── Step 6: Bring up the docker compose stack ───────────────────── COMPOSE_ARGS=(--env-file "$ENV_FILE" -f "$COMPOSE_FILE") if [ -f "$OVERRIDE_FILE" ] && [ -s "$OVERRIDE_FILE" ] && ! grep -q '^{}$' "$OVERRIDE_FILE"; then diff --git a/samples/ai/edge-ai-demo-studio/workers/suite/metro-ai-suite/image-based-video-search/start.sh b/samples/ai/edge-ai-demo-studio/workers/suite/metro-ai-suite/image-based-video-search/start.sh index 35f21fa4..dd7e0498 100755 --- a/samples/ai/edge-ai-demo-studio/workers/suite/metro-ai-suite/image-based-video-search/start.sh +++ b/samples/ai/edge-ai-demo-studio/workers/suite/metro-ai-suite/image-based-video-search/start.sh @@ -89,6 +89,7 @@ generate_override() { local config_host_path="$SUITE_DIR/src/dlstreamer-pipeline-server/configs/filter-pipeline/$selected_config" local models_public_path="$SUITE_DIR/src/dlstreamer-pipeline-server/models/public" local models_resnet_path="$SUITE_DIR/src/dlstreamer-pipeline-server/models/resnet-50-pytorch" + local broker_config_path="$SUITE_DIR/src/broker" emit_proxy_env() { local indent="$1" @@ -111,7 +112,7 @@ services: HEADER if $has_proxy; then - for service in milvus-ui feature-matching streaming-pipeline rtsp-server broker app; do + for service in milvus-ui feature-matching streaming-pipeline rtsp-server app; do printf ' %s:\n environment:\n' "$service" emit_proxy_env " " done @@ -168,6 +169,12 @@ HEADER emit_proxy_env " " fi + printf ' broker:\n volumes: !override\n - "%s:/mosquitto/config:ro"\n' "$broker_config_path" + if $has_proxy; then + printf ' environment:\n' + emit_proxy_env " " + fi + printf ' nginx:\n ports: !override\n - "%s:80"\n - "%s:443"\n - "%s:8554"\n' \ "$IBVS_HTTP_PORT" "$IBVS_HTTPS_PORT" "$IBVS_RTSP_PORT" @@ -302,6 +309,7 @@ ensure_models() { docker run --rm --user=root \ -e http_proxy="${http_proxy:-}" -e https_proxy="${https_proxy:-}" -e no_proxy="${no_proxy:-}" \ -e HTTP_PROXY="${HTTP_PROXY:-}" -e HTTPS_PROXY="${HTTPS_PROXY:-}" \ + -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \ -v "$MODELS_PATH:/output" \ intel/dlstreamer:2026.0.0-ubuntu24 bash -c ' mkdir -p /output/public @@ -318,6 +326,7 @@ ensure_models() { sleep 2 done fi + chown -R "$HOST_UID:$HOST_GID" /output ' touch "$SETUP_SENTINEL"